Commit 5652f5e02814a1d81e4e28ed886820d0946df552

Authored by “wangming”
1 parent 8375c906

refactor: update documentation and improve image moderation service

- Updated the SaaS documentation to clarify the separation of core business modules.
- Enhanced the ImageModerationService for better error handling and configuration management.
- Improved code readability by removing unnecessary whitespace and optimizing LINQ queries in various services.
- Adjusted performance metrics calculations in the LqStoreDashboardService for accuracy in reporting.
- Updated DTOs to reflect changes in performance metrics definitions.
docs/generate_saas_excel.py 0 → 100644
  1 +#!/usr/bin/env python3
  2 +# -*- coding: utf-8 -*-
  3 +"""
  4 +美业SaaS系统功能规划 - Excel生成脚本
  5 +根据功能规划文档生成包含:功能、详细说明、是否可复用、复用后人天、全新开发人天的Excel
  6 +"""
  7 +
  8 +import openpyxl
  9 +from openpyxl.styles import Font, Alignment, Border, Side, PatternFill
  10 +from openpyxl.utils import get_column_letter
  11 +
  12 +# 功能数据:基于美业SaaS系统功能规划.md
  13 +# 格式:(一级模块, 二级模块, 详细说明, 是否可复用, 复用说明, 复用后人天, 全新开发人天)
  14 +DATA = [
  15 + # 一、SaaS平台基础能力
  16 + ("一、SaaS平台基础能力", "1.1 多租户管理", "租户注册与开通、租户信息管理、服务套餐管理、租户数据隔离、计费与结算", "否", "SaaS核心能力,需全新设计", 25, 25),
  17 + ("一、SaaS平台基础能力", "1.2 权限与组织架构", "组织架构模板、门店模式配置、角色权限、用户管理、数据权限控制", "部分", "OrganizeService、UserService、base_organize可参考,需多租户改造", 8, 20),
  18 + ("一、SaaS平台基础能力", "1.3 系统配置管理", "业务规则配置、系统参数、界面个性化、审批流程、通知规则", "部分", "现有配置表可参考,需租户级配置", 5, 15),
  19 + ("一、SaaS平台基础能力", "1.4 数据安全与隔离", "数据加密、访问控制、操作审计、数据库/文件/缓存隔离", "部分", "现有权限体系可参考", 3, 12),
  20 + ("一、SaaS平台基础能力", "1.5 SaaS订阅与计费系统", "套餐管理、订阅周期、功能开关、试用期、超额限制、订阅管理", "否", "商业化闭环核心,需全新开发", 30, 30),
  21 + ("一、SaaS平台基础能力", "1.6 数据迁移与初始化工具", "客户/卡项/员工/项目数据导入、迁移方案、映射转换、初始化向导", "部分", "现有导入逻辑可复用,需支持多格式和映射", 8, 25),
  22 + ("一、SaaS平台基础能力", "1.7 文件存储与CDN系统", "对象存储+CDN架构、存储配额、CDN配置、图片优化、文件管理", "部分", "现有文件上传可改造,需OSS+CDN架构", 5, 20),
  23 + ("一、SaaS平台基础能力", "1.8 操作日志与审计系统", "用户操作日志、数据变更记录、关键行为审计、财务专项审计、删除作废审计", "部分", "现有日志可扩展,需增强审计能力", 6, 18),
  24 + ("一、SaaS平台基础能力", "1.9 异常与纠纷处理系统", "异常工单、纠纷处理、投诉管理、处理流程", "否", "美业痛点,需全新设计", 15, 15),
  25 +
  26 + # 二、核心业务模块
  27 + ("二、核心业务模块", "2.1 门店管理系统", "门店信息维护、归属管理、运营数据、权限管理、合作医院管理(医美结算)", "是", "LqMdxxService、LqMdTargetService、LqMdFzhtService可拆分,医美结算需新增", 12, 25),
  28 + ("二、核心业务模块", "2.2 人员管理系统", "员工档案、变动管理、归属快照、战队管理、岗位管理、技能与项目能力、考勤管理", "是", "EmployeeService、UserService、LqAttendanceSummaryService可拆分,技能模型需新增", 15, 35),
  29 + ("二、核心业务模块", "2.3 客户管理系统", "客户档案、标签系统、客户分群、跟进管理、客户关怀、价值分析", "是", "LqKhxxService可拆分,标签分群需增强", 10, 25),
  30 + ("二、核心业务模块", "2.4 开单耗卡系统", "开单、套餐拆分组合、业绩归属、提成规则、开单模板、耗卡、批量耗卡", "是", "LqKdKdjlbService、LqOrderRecordsService、LqXhHyhkService、LqPackageInfoService可拆分", 18, 40),
  31 + ("二、核心业务模块", "2.5 业绩统计系统", "个人/门店/团队业绩、业绩分类、合作医院业绩、趋势分析、排行榜", "是", "LqStatisticsService、LqYjmxbService、LqJlmxbService可拆分", 10, 25),
  32 + ("二、核心业务模块", "2.6 工资核算系统", "工资字段配置、字段来源、公式配置、多岗位核算、工资管理", "是", "LqSalaryService及各类岗位工资Service可拆分,需支持灵活配置", 15, 35),
  33 +
  34 + # 三、营销与增长系统
  35 + ("三、营销与增长系统", "3.1 拓客与线索管理", "渠道管理、渠道成本、效果分析、新客体验卡、0元引流、到店核销、线索池、跟进管理、转化率分析", "否", "无现成模块,需全新开发", 35, 35),
  36 + ("三、营销与增长系统", "3.2 裂变与转介绍", "老带新、邀请有礼、返积分/返余额", "否", "无现成模块", 15, 15),
  37 + ("三、营销与增长系统", "3.3 新客转化", "新客转化策略、转化分析", "否", "无现成模块", 10, 10),
  38 + ("三、营销与增长系统", "3.4 老客复购", "营销活动、会员卡、积分、优惠券、会员等级", "部分", "LqXhHyhkService卡项逻辑可参考,营销体系需新建", 8, 30),
  39 + ("三、营销与增长系统", "3.5 客户生命周期管理(CLM)", "生命周期状态机、阶段流转、对应营销策略", "否", "无现成模块", 20, 20),
  40 +
  41 + # 四、预约与服务模块
  42 + ("四、预约与服务模块", "4.1 预约管理系统", "在线预约、预约类型、时间段选择、预约确认修改、预约列表、冲突检测", "是", "ScheduleService可拆分,需增强在线预约能力", 8, 20),
  43 + ("四、预约与服务模块", "4.2 服务项目管理", "品项管理(生美/医美/科美/产品)、套餐、分类、价格、服务流程", "部分", "LqCpxxService、LqXmzlService、LqPackageInfoService可参考", 6, 18),
  44 + ("四、预约与服务模块", "4.2.3 服务过程记录", "服务备注、前后对比图、注意事项、服务记录管理", "是", "LqYyjlService等可拆分", 5, 12),
  45 + ("四、预约与服务模块", "4.3 健康师排班管理", "排班计划、自动/手动排班、班次管理", "部分", "ScheduleService排班逻辑可参考", 5, 15),
  46 + ("四、预约与服务模块", "4.4 服务评价系统", "服务评价、健康师评价、评价统计、评价应用", "否", "无现成模块", 10, 10),
  47 + ("四、预约与服务模块", "4.5 爽约/取消/超时管理", "爽约记录、黑名单、预约限制、取消规则、超时管理", "否", "无现成模块", 15, 15),
  48 +
  49 + # 五、支付与财务模块
  50 + ("五、支付与财务模块", "5.1 支付管理系统", "微信/支付宝/银行卡/现金/储值卡支付、支付记录", "部分", "现有支付逻辑可参考,需对接多支付渠道", 5, 20),
  51 + ("五、支付与财务模块", "5.2 财务管理系统", "财务核算、资金管理、应收应付、财务报表", "是", "LqFinancialReportService、LqSkzhService可拆分", 12, 30),
  52 + ("五、支付与财务模块", "5.3 对账管理系统", "对账功能、对账分析", "部分", "现有对账逻辑可参考", 5, 15),
  53 + ("五、支付与财务模块", "5.4 发票管理系统", "发票管理、发票配置", "否", "无现成模块", 10, 10),
  54 + ("五、支付与财务模块", "5.5 分账与结算规则", "分账管理、结算管理、医美结算", "部分", "LqCooperationCostService医美结算可参考", 8, 25),
  55 + ("五、支付与财务模块", "5.6 财务风控", "风控规则、风控管理、合规管理", "否", "无现成模块", 15, 15),
  56 + ("五、支付与财务模块", "5.7 经营与业财分析", "收入确认、成本归集、项目毛利、人效分析、门店盈亏、经营财报", "是", "LqReportService、LqAnnualSummaryService、LqStoreExpenseService可拆分", 15, 35),
  57 +
  58 + # 六、库存与供应链模块
  59 + ("六、库存与供应链模块", "6.1 库存管理系统", "库存管理、库存使用、出入库、盘点", "是", "LqInventoryService、LqInventoryUsageService、LqStoreConsumableInventoryService可拆分", 8, 20),
  60 + ("六、库存与供应链模块", "6.2 采购管理系统", "采购流程、采购分析", "部分", "LqPurchaseRecordsService可参考", 5, 15),
  61 + ("六、库存与供应链模块", "6.3 供应商管理系统", "供应商管理、供应商合作", "部分", "LqLaundrySupplierService可参考", 3, 10),
  62 + ("六、库存与供应链模块", "6.4 设备管理系统", "设备档案、维护、使用管理", "否", "无现成模块", 12, 12),
  63 + ("六、库存与供应链模块", "6.5 耗材自动关联项目", "项目耗材配置、耗材管理", "部分", "现有耗材关联逻辑可参考", 5, 12),
  64 +
  65 + # 七、数据分析与决策支持
  66 + ("七、数据分析与决策支持", "7.1 数据驾驶舱", "集团/事业部/门店/移动端驾驶舱", "是", "LqStoreDashboardService、LqTkDashboardService、LqBusinessUnitDashboardService、LqTechDepartmentDashboardService可拆分", 12, 30),
  67 + ("七、数据分析与决策支持", "7.2 报表分析系统", "报表类型、报表功能、自定义报表", "是", "LqReportService、LqStatisticsService、LqDailyReportService可拆分", 10, 25),
  68 + ("七、数据分析与决策支持", "7.3 客户画像分析", "客户画像、客户分析", "是", "MemberPortraitService可拆分", 6, 15),
  69 + ("七、数据分析与决策支持", "7.4 经营分析系统", "经营分析、决策支持", "是", "LqReportService、LqAnnualSummaryService可拆分", 8, 20),
  70 + ("七、数据分析与决策支持", "7.5 自定义指标与看板", "自定义指标、拖拽式看板", "否", "无现成模块", 20, 20),
  71 + ("七、数据分析与决策支持", "7.6 经营对标分析", "对标分析、对标管理", "是", "LqReportService对标逻辑可参考", 6, 15),
  72 +
  73 + # 八、移动端与小程序
  74 + ("八、移动端与小程序", "8.1 员工端(健康师)", "开单、耗卡、客户备注、排班查看", "是", "现有移动端开单耗卡逻辑可拆分", 10, 25),
  75 + ("八、移动端与小程序", "8.2 店长端", "今日业绩、人员状态、异常提醒", "是", "LqStoreDashboardService、门店看板可拆分", 8, 20),
  76 + ("八、移动端与小程序", "8.3 老板端", "多店汇总、盈亏、趋势分析", "是", "LqReportService、经营报表可拆分", 8, 20),
  77 + ("八、移动端与小程序", "8.4 客户端小程序", "预约、会员中心、订单、营销活动", "否", "无现成C端小程序", 30, 30),
  78 +
  79 + # 九、消息与通知系统
  80 + ("九、消息与通知系统", "9.1 消息中心", "消息类型、消息管理", "部分", "现有消息逻辑可参考", 3, 10),
  81 + ("九、消息与通知系统", "9.2 通知推送系统", "多渠道通知、通知配置", "否", "无现成推送体系", 12, 12),
  82 + ("九、消息与通知系统", "9.3 营销消息系统", "营销消息、消息效果", "否", "无现成模块", 8, 8),
  83 + ("九、消息与通知系统", "9.4 事件触发规则引擎", "规则引擎配置、规则管理", "否", "无现成规则引擎", 20, 20),
  84 +
  85 + # 十、第三方平台对接
  86 + ("十、第三方平台对接", "10.1 团购平台对接", "美团/大众点评等对接、团购数据分析", "否", "无现成对接,现有系统无此功能", 25, 25),
  87 + ("十、第三方平台对接", "10.2 设备仪器对接", "设备对接、设备管理", "否", "无现成对接,现有系统无此功能", 15, 15),
  88 + ("十、第三方平台对接", "10.3 考勤平台对接", "企业微信/钉钉考勤对接、考勤管理", "否", "无现成对接,现有系统无此功能", 12, 12),
  89 + ("十、第三方平台对接", "10.4 支付/财务平台对接", "支付平台、财务平台对接", "否", "无现成对接,现有系统无此功能", 15, 15),
  90 +
  91 + # 十一及之后:现有系统均无,复用后人天=全新开发人天(还需多少天=全部)
  92 + ("十一、问卷调查系统", "11.1 问卷配置管理", "问卷创建、问卷模板", "否", "无现成模块,现有系统无此功能", 12, 12),
  93 + ("十一、问卷调查系统", "11.2 问卷发放与收集", "问卷发放、问卷收集", "否", "无现成模块,现有系统无此功能", 8, 8),
  94 + ("十一、问卷调查系统", "11.3 问卷数据分析", "问卷统计、问卷报告", "否", "无现成模块,现有系统无此功能", 8, 8),
  95 + ("十一、问卷调查系统", "11.4 满意度与复购联动", "满意度联动、复购联动", "否", "无现成模块,现有系统无此功能", 10, 10),
  96 +
  97 + ("十二、AI智能助手", "12.0 AI经营助手", "今日重点关注、流失预警、项目盈利分析、AI对话", "否", "无现成模块,现有系统无此功能", 25, 25),
  98 + ("十二、AI智能助手", "12.1 AI客户价值评分", "客户价值模型、流失预警", "否", "无现成模块,现有系统无此功能", 15, 15),
  99 + ("十二、AI智能助手", "12.2 AI回访话术生成", "话术生成、话术优化", "否", "无现成模块,现有系统无此功能", 10, 10),
  100 + ("十二、AI智能助手", "12.3 AI项目推荐", "智能推荐、推荐优化", "否", "无现成模块,现有系统无此功能", 15, 15),
  101 + ("十二、AI智能助手", "12.4 AI经营分析", "智能分析、分析优化", "否", "无现成模块,现有系统无此功能", 15, 15),
  102 + ("十二、AI智能助手", "12.5 AI成本核算", "AI成本管理、成本报表", "否", "无现成模块,现有系统无此功能", 12, 12),
  103 + ("十二、AI智能助手", "12.6 AI客户流失预警", "流失预警模型、预警管理", "否", "无现成模块,现有系统无此功能", 12, 12),
  104 + ("十二、AI智能助手", "12.7 AI员工效率分析", "效率分析模型、效率管理", "否", "无现成模块,现有系统无此功能", 12, 12),
  105 +
  106 + ("十三、商城系统", "13.1 商品管理", "商品信息、商品运营", "否", "无现成模块,现有系统无此功能", 15, 15),
  107 + ("十三、商城系统", "13.2 订单管理", "订单处理、订单管理", "否", "无现成模块,现有系统无此功能", 15, 15),
  108 + ("十三、商城系统", "13.3 积分兑换", "积分兑换配置、兑换功能", "否", "无现成模块,现有系统无此功能", 8, 8),
  109 + ("十三、商城系统", "13.4 商城运营", "商城配置、数据分析", "否", "无现成模块,现有系统无此功能", 10, 10),
  110 + ("十三、商城系统", "13.5 到店服务与商品联动", "服务后推荐、联动管理", "否", "无现成模块,现有系统无此功能", 12, 12),
  111 +
  112 + ("十四、连锁总部策略引擎", "14.1 统一价格策略", "价格策略管理", "否", "无现成模块,现有系统无此功能", 15, 15),
  113 + ("十四、连锁总部策略引擎", "14.2 统一活动下发", "活动策略管理", "否", "无现成模块,现有系统无此功能", 10, 10),
  114 + ("十四、连锁总部策略引擎", "14.3 门店执行差异分析", "差异分析", "否", "无现成模块,现有系统无此功能", 12, 12),
  115 +
  116 + ("十五、开放平台与生态", "15.1 API开放平台", "API管理、API功能", "否", "无现成模块,现有系统无此功能", 20, 20),
  117 + ("十五、开放平台与生态", "15.2 第三方集成", "支付集成、其他集成", "否", "无现成模块,现有系统无此功能", 15, 15),
  118 + ("十五、开放平台与生态", "15.3 应用市场", "应用管理、生态建设", "否", "无现成模块,现有系统无此功能", 25, 25),
  119 +]
  120 +
  121 +def create_excel(filepath):
  122 + wb = openpyxl.Workbook()
  123 + ws = wb.active
  124 + ws.title = "美业SaaS功能规划"
  125 +
  126 + # 表头
  127 + headers = ["一级模块", "二级模块/功能", "功能详细说明", "是否可复用", "复用说明(现有系统可复用内容)", "复用后还需人天", "全新开发人天"]
  128 + for col, h in enumerate(headers, 1):
  129 + cell = ws.cell(row=1, column=col, value=h)
  130 + cell.font = Font(bold=True)
  131 + cell.alignment = Alignment(horizontal="center", vertical="center", wrap_text=True)
  132 + cell.fill = PatternFill(start_color="4472C4", end_color="4472C4", fill_type="solid")
  133 + cell.font = Font(bold=True, color="FFFFFF")
  134 +
  135 + # 数据
  136 + for row, item in enumerate(DATA, 2):
  137 + for col, val in enumerate(item, 1):
  138 + cell = ws.cell(row=row, column=col, value=val)
  139 + cell.alignment = Alignment(vertical="center", wrap_text=True)
  140 + if col == 4: # 是否可复用
  141 + if val == "是":
  142 + cell.fill = PatternFill(start_color="C6EFCE", end_color="C6EFCE", fill_type="solid")
  143 + elif val == "部分":
  144 + cell.fill = PatternFill(start_color="FFEB9C", end_color="FFEB9C", fill_type="solid")
  145 + else:
  146 + cell.fill = PatternFill(start_color="FFC7CE", end_color="FFC7CE", fill_type="solid")
  147 +
  148 + # 列宽
  149 + ws.column_dimensions["A"].width = 22
  150 + ws.column_dimensions["B"].width = 26
  151 + ws.column_dimensions["C"].width = 50
  152 + ws.column_dimensions["D"].width = 12
  153 + ws.column_dimensions["E"].width = 45
  154 + ws.column_dimensions["F"].width = 12
  155 + ws.column_dimensions["G"].width = 14
  156 +
  157 + # 汇总行(复用后人天=E列,全新开发人天=G列)
  158 + total_row = len(DATA) + 3
  159 + ws.cell(row=total_row, column=1, value="合计")
  160 + ws.cell(row=total_row, column=1).font = Font(bold=True)
  161 + ws.cell(row=total_row, column=6, value=f"=SUM(F2:F{len(DATA)+1})")
  162 + ws.cell(row=total_row, column=7, value=f"=SUM(G2:G{len(DATA)+1})")
  163 + ws.cell(row=total_row, column=6).font = Font(bold=True)
  164 + ws.cell(row=total_row, column=7).font = Font(bold=True)
  165 +
  166 + # 说明行
  167 + ws.cell(row=total_row+2, column=1, value="说明:")
  168 + ws.cell(row=total_row+2, column=1).font = Font(bold=True)
  169 + ws.cell(row=total_row+3, column=1, value="1. 是否可复用:基于现有绿纤美业ERP系统,标注「是」= 可从现有系统拆分/改造;「部分」= 部分逻辑可复用;「否」= 需全新开发")
  170 + ws.merge_cells(start_row=total_row+3, start_column=1, end_row=total_row+3, end_column=7)
  171 + ws.cell(row=total_row+4, column=1, value="2. 复用说明:列出现有系统中可复用的Service或逻辑,便于开发时参考")
  172 + ws.merge_cells(start_row=total_row+4, start_column=1, end_row=total_row+4, end_column=7)
  173 + ws.cell(row=total_row+5, column=1, value="3. 复用后还需人天:复用现有代码/逻辑后,仍需投入的开发人天(含改造、适配、测试);无可复用时=全新开发人天")
  174 + ws.merge_cells(start_row=total_row+5, start_column=1, end_row=total_row+5, end_column=7)
  175 + ws.cell(row=total_row+6, column=1, value="4. 全新开发人天:若从零开发该功能,预估的开发人天")
  176 + ws.merge_cells(start_row=total_row+6, start_column=1, end_row=total_row+6, end_column=7)
  177 +
  178 + wb.save(filepath)
  179 + print(f"Excel 已生成: {filepath}")
  180 +
  181 +if __name__ == "__main__":
  182 + create_excel("/Users/mr.wang/代码库/绿纤/lvqianmeiye_ERP/docs/美业SaaS系统功能规划.xlsx")
... ...
docs/美业SaaS系统功能规划.md
... ... @@ -20,16 +20,16 @@
20 20 - [1.9 异常与纠纷处理系统](#19-异常与纠纷处理系统) ⭐ **美业真实痛点**
21 21  
22 22 ### 二、核心业务模块
23   -- [2.1 门店管理系统](#21-门店管理系统)
  23 +- [2.1 门店管理系统](#21-门店管理系统)(从现有系统拆分)
24 24 - [2.1.3 合作医院管理](#213-合作医院管理)
25   -- [2.2 人员管理系统](#22-人员管理系统)
  25 +- [2.2 人员管理系统](#22-人员管理系统)(从现有系统拆分)
26 26 - [2.2.3 考勤管理系统](#223-考勤管理系统)
27 27 - [2.2.4 员工技能与项目能力模型](#224-员工技能与项目能力模型)
28   -- [2.3 客户管理系统](#23-客户管理系统)
29   -- [2.4 开单耗卡系统](#24-开单耗卡系统)
  28 +- [2.3 客户管理系统](#23-客户管理系统)(从现有系统拆分)
  29 +- [2.4 开单耗卡系统](#24-开单耗卡系统)(从现有系统拆分)
30 30 - [2.4.1 套餐拆分与组合开单](#241-套餐拆分与组合开单)
31   -- [2.5 业绩统计系统](#25-业绩统计系统)
32   -- [2.6 工资核算系统](#26-工资核算系统)
  31 +- [2.5 业绩统计系统](#25-业绩统计系统)(从现有系统拆分)
  32 +- [2.6 工资核算系统](#26-工资核算系统)(从现有系统拆分)
33 33  
34 34 ### 三、营销与增长系统
35 35 - [3.1 拓客与线索管理](#31-拓客与线索管理) ⭐ **把人弄进来**
... ... @@ -39,41 +39,41 @@
39 39 - [3.5 客户生命周期管理(CLM)](#35-客户生命周期管理clm)
40 40  
41 41 ### 四、预约与服务模块
42   -- [4.1 预约管理系统](#41-预约管理系统)
  42 +- [4.1 预约管理系统](#41-预约管理系统)(从现有系统拆分)
43 43 - [4.2 服务项目管理](#42-服务项目管理)
44   - - [4.2.3 服务过程记录(轻量版)](#423-服务过程记录轻量版)
  44 + - [4.2.3 服务过程记录(轻量版)](#423-服务过程记录轻量版)(从现有系统拆分)
45 45 - [4.3 健康师排班管理](#43-健康师排班管理)
46 46 - [4.4 服务评价系统](#44-服务评价系统)
47 47 - [4.5 爽约/取消/超时管理](#45-爽约取消超时管理)
48 48  
49 49 ### 五、支付与财务模块
50 50 - [5.1 支付管理系统](#51-支付管理系统)
51   -- [5.2 财务管理系统](#52-财务管理系统)
  51 +- [5.2 财务管理系统](#52-财务管理系统)(从现有系统拆分)
52 52 - [5.3 对账管理系统](#53-对账管理系统)
53 53 - [5.4 发票管理系统](#54-发票管理系统)
54 54 - [5.5 分账与结算规则](#55-分账与结算规则)
55 55 - [5.6 财务风控](#56-财务风控)
56   -- [5.7 经营与业财分析](#57-经营与业财分析) ⭐ **核心差异化**
  56 +- [5.7 经营与业财分析](#57-经营与业财分析) ⭐ **核心差异化**(从现有系统拆分)
57 57  
58 58 ### 六、库存与供应链模块
59   -- [6.1 库存管理系统](#61-库存管理系统)
  59 +- [6.1 库存管理系统](#61-库存管理系统)(从现有系统拆分)
60 60 - [6.2 采购管理系统](#62-采购管理系统)
61 61 - [6.3 供应商管理系统](#63-供应商管理系统)
62 62 - [6.4 设备管理系统](#64-设备管理系统)
63 63 - [6.5 耗材自动关联项目](#65-耗材自动关联项目)
64 64  
65 65 ### 七、数据分析与决策支持
66   -- [7.1 数据驾驶舱](#71-数据驾驶舱)
67   -- [7.2 报表分析系统](#72-报表分析系统)
68   -- [7.3 客户画像分析](#73-客户画像分析)
69   -- [7.4 经营分析系统](#74-经营分析系统)
  66 +- [7.1 数据驾驶舱](#71-数据驾驶舱)(从现有系统拆分)
  67 +- [7.2 报表分析系统](#72-报表分析系统)(从现有系统拆分)
  68 +- [7.3 客户画像分析](#73-客户画像分析)(从现有系统拆分)
  69 +- [7.4 经营分析系统](#74-经营分析系统)(从现有系统拆分)
70 70 - [7.5 自定义指标与看板](#75-自定义指标与看板)
71   -- [7.6 经营对标分析](#76-经营对标分析)
  71 +- [7.6 经营对标分析](#76-经营对标分析)(从现有系统拆分)
72 72  
73 73 ### 八、移动端与小程序
74   -- [8.1 员工端(健康师)](#81-员工端健康师) ⭐ **开单、消耗、客户备注、排班查看**
75   -- [8.2 店长端](#82-店长端) ⭐ **今日业绩、人员状态、异常提醒**
76   -- [8.3 老板端](#83-老板端) ⭐ **多店汇总、盈亏、趋势分析**
  74 +- [8.1 员工端(健康师)](#81-员工端健康师) ⭐ **开单、消耗、客户备注、排班查看**(从现有系统拆分)
  75 +- [8.2 店长端](#82-店长端) ⭐ **今日业绩、人员状态、异常提醒**(从现有系统拆分)
  76 +- [8.3 老板端](#83-老板端) ⭐ **多店汇总、盈亏、趋势分析**(从现有系统拆分)
77 77 - [8.4 客户端小程序](#84-客户端小程序)
78 78  
79 79 ### 九、消息与通知系统
... ...
docs/美业SaaS系统功能规划.xlsx 0 → 100644
No preview for this file type
docs/美业系统升级项目需求拆分表.xlsx
No preview for this file type
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqStoreDashboard/StoreDashboardStatisticsOutput.cs
... ... @@ -96,17 +96,17 @@ namespace NCC.Extend.Entitys.Dto.LqStoreDashboard
96 96 public decimal AvgProjectPerHead { get; set; }
97 97  
98 98 /// <summary>
99   - /// 生美业绩(消耗业绩
  99 + /// 生美业绩(实收业绩 = 开单实收 - 退款
100 100 /// </summary>
101 101 public decimal LifeBeautyPerformance { get; set; }
102 102  
103 103 /// <summary>
104   - /// 医美业绩(消耗业绩
  104 + /// 医美业绩(实收业绩 = 开单实收 - 退款
105 105 /// </summary>
106 106 public decimal MedicalBeautyPerformance { get; set; }
107 107  
108 108 /// <summary>
109   - /// 科美业绩(消耗业绩
  109 + /// 科美业绩(实收业绩 = 开单实收 - 退款
110 110 /// </summary>
111 111 public decimal TechBeautyPerformance { get; set; }
112 112  
... ...
netcore/src/Modularity/Extend/NCC.Extend/ImageModerationService.cs
... ... @@ -38,24 +38,24 @@ namespace NCC.Extend
38 38 {
39 39 _httpClient = new HttpClient();
40 40 _enabled = App.Configuration["NCC_App:ImageModeration:Enabled"] == "true";
41   - _endpoint = App.Configuration["NCC_App:ImageModeration:Endpoint"]
  41 + _endpoint = App.Configuration["NCC_App:ImageModeration:Endpoint"]
42 42 ?? "https://green.cn-shanghai.aliyuncs.com";
43   - _region = App.Configuration["NCC_App:ImageModeration:Region"]
  43 + _region = App.Configuration["NCC_App:ImageModeration:Region"]
44 44 ?? "cn-shanghai";
45   -
  45 +
46 46 // 优先使用ImageModeration配置,如果没有则使用AliyunOSS的配置
47 47 _accessKeyId = App.Configuration["NCC_App:ImageModeration:AccessKeyId"];
48 48 if (string.IsNullOrEmpty(_accessKeyId))
49 49 {
50 50 _accessKeyId = App.Configuration["NCC_App:AliyunOSS:AccessKeyId"];
51 51 }
52   -
  52 +
53 53 _accessKeySecret = App.Configuration["NCC_App:ImageModeration:AccessKeySecret"];
54 54 if (string.IsNullOrEmpty(_accessKeySecret))
55 55 {
56 56 _accessKeySecret = App.Configuration["NCC_App:AliyunOSS:AccessKeySecret"];
57 57 }
58   -
  58 +
59 59 // 判断是否为增强版(endpoint 包含 green-cip)
60 60 _isEnhancedVersion = _endpoint.Contains("green-cip");
61 61 }
... ... @@ -80,19 +80,19 @@ namespace NCC.Extend
80 80 {
81 81 if (!_enabled)
82 82 {
83   - return new ModerationResult
84   - {
85   - Passed = true,
86   - Message = "审核未启用,直接通过"
  83 + return new ModerationResult
  84 + {
  85 + Passed = true,
  86 + Message = "审核未启用,直接通过"
87 87 };
88 88 }
89 89  
90 90 if (string.IsNullOrEmpty(filePath) || !File.Exists(filePath))
91 91 {
92   - return new ModerationResult
93   - {
94   - Passed = false,
95   - Message = "文件不存在或路径为空"
  92 + return new ModerationResult
  93 + {
  94 + Passed = false,
  95 + Message = "文件不存在或路径为空"
96 96 };
97 97 }
98 98  
... ... @@ -105,9 +105,9 @@ namespace NCC.Extend
105 105 {
106 106 // 审核异常时,根据配置决定是否通过
107 107 var failOnError = App.Configuration["NCC_App:ImageModeration:FailOnError"] == "true";
108   - return new ModerationResult
109   - {
110   - Passed = !failOnError,
  108 + return new ModerationResult
  109 + {
  110 + Passed = !failOnError,
111 111 Message = $"审核异常:{ex.Message}",
112 112 RawResponse = ex.ToString()
113 113 };
... ... @@ -123,19 +123,19 @@ namespace NCC.Extend
123 123 {
124 124 if (!_enabled)
125 125 {
126   - return new ModerationResult
127   - {
128   - Passed = true,
129   - Message = "审核未启用,直接通过"
  126 + return new ModerationResult
  127 + {
  128 + Passed = true,
  129 + Message = "审核未启用,直接通过"
130 130 };
131 131 }
132 132  
133 133 if (imageBytes == null || imageBytes.Length == 0)
134 134 {
135   - return new ModerationResult
136   - {
137   - Passed = false,
138   - Message = "图片数据为空"
  135 + return new ModerationResult
  136 + {
  137 + Passed = false,
  138 + Message = "图片数据为空"
139 139 };
140 140 }
141 141  
... ... @@ -143,9 +143,9 @@ namespace NCC.Extend
143 143 if (string.IsNullOrEmpty(_accessKeyId) || string.IsNullOrEmpty(_accessKeySecret))
144 144 {
145 145 var failOnError = App.Configuration["NCC_App:ImageModeration:FailOnError"] == "true";
146   - return new ModerationResult
147   - {
148   - Passed = !failOnError,
  146 + return new ModerationResult
  147 + {
  148 + Passed = !failOnError,
149 149 Message = "图片审核服务未配置 AccessKey,无法调用审核接口",
150 150 RawResponse = "AccessKeyId 或 AccessKeySecret 未配置"
151 151 };
... ... @@ -167,9 +167,9 @@ namespace NCC.Extend
167 167 {
168 168 // 审核异常时,根据配置决定是否通过
169 169 var failOnError = App.Configuration["NCC_App:ImageModeration:FailOnError"] == "true";
170   - return new ModerationResult
171   - {
172   - Passed = !failOnError,
  170 + return new ModerationResult
  171 + {
  172 + Passed = !failOnError,
173 173 Message = $"审核异常:{ex.Message}",
174 174 RawResponse = ex.ToString()
175 175 };
... ... @@ -437,104 +437,104 @@ namespace NCC.Extend
437 437 {
438 438 // 构建请求URL
439 439 var url = $"{_endpoint}/green/image/scan";
440   -
441   - // 构建请求体
442   - var requestBody = new
  440 +
  441 + // 构建请求体
  442 + var requestBody = new
  443 + {
  444 + scenes = new[] { "porn", "terrorism", "ad", "qrcode" },
  445 + tasks = new[]
443 446 {
444   - scenes = new[] { "porn", "terrorism", "ad", "qrcode" },
445   - tasks = new[]
446   - {
447 447 new
448 448 {
449 449 dataId = Guid.NewGuid().ToString(),
450 450 url = Convert.ToBase64String(imageBytes)
451 451 }
452 452 }
453   - };
  453 + };
454 454  
455   - var jsonContent = JsonConvert.SerializeObject(requestBody);
456   - var contentBytes = Encoding.UTF8.GetBytes(jsonContent);
457   - var content = new ByteArrayContent(contentBytes);
458   - content.Headers.TryAddWithoutValidation("Content-Type", "application/json");
  455 + var jsonContent = JsonConvert.SerializeObject(requestBody);
  456 + var contentBytes = Encoding.UTF8.GetBytes(jsonContent);
  457 + var content = new ByteArrayContent(contentBytes);
  458 + content.Headers.TryAddWithoutValidation("Content-Type", "application/json");
459 459  
460   - // 计算 Content-MD5(请求体的 MD5,然后 Base64)
461   - string contentMd5;
462   - using (var md5 = MD5.Create())
463   - {
464   - var hashBytes = md5.ComputeHash(contentBytes);
465   - contentMd5 = Convert.ToBase64String(hashBytes);
466   - }
  460 + // 计算 Content-MD5(请求体的 MD5,然后 Base64)
  461 + string contentMd5;
  462 + using (var md5 = MD5.Create())
  463 + {
  464 + var hashBytes = md5.ComputeHash(contentBytes);
  465 + contentMd5 = Convert.ToBase64String(hashBytes);
  466 + }
  467 +
  468 + // 生成请求时间(RFC 1123 格式)
  469 + var dateStr = DateTime.UtcNow.ToString("r");
467 470  
468   - // 生成请求时间(RFC 1123 格式)
469   - var dateStr = DateTime.UtcNow.ToString("r");
470   -
471   - // 生成签名随机数
472   - var signatureNonce = Guid.NewGuid().ToString();
473   -
474   - // 构建规范化的请求头(x-acs- 开头的头,按字典序排序)
475   - var acsHeaders = new SortedDictionary<string, string>
  471 + // 生成签名随机数
  472 + var signatureNonce = Guid.NewGuid().ToString();
  473 +
  474 + // 构建规范化的请求头(x-acs- 开头的头,按字典序排序)
  475 + var acsHeaders = new SortedDictionary<string, string>
476 476 {
477 477 { "x-acs-signature-method", "HMAC-SHA1" },
478 478 { "x-acs-signature-nonce", signatureNonce },
479 479 { "x-acs-signature-version", "1.0" },
480 480 { "x-acs-version", "2018-05-09" }
481 481 };
482   -
483   - // 构建 CanonicalizedHeaders(格式:key:value\n)
484   - var canonicalizedHeaders = new StringBuilder();
  482 +
  483 + // 构建 CanonicalizedHeaders(格式:key:value\n)
  484 + var canonicalizedHeaders = new StringBuilder();
  485 + foreach (var header in acsHeaders)
  486 + {
  487 + canonicalizedHeaders.Append($"{header.Key}:{header.Value}\n");
  488 + }
  489 +
  490 + // 构建 CanonicalizedResource(请求路径)
  491 + var uri = new Uri(url);
  492 + var canonicalizedResource = uri.AbsolutePath;
  493 +
  494 + // 构建待签名字符串
  495 + var stringToSign = new StringBuilder();
  496 + stringToSign.Append("POST\n"); // HTTP-Verb
  497 + stringToSign.Append("application/json\n"); // Accept
  498 + stringToSign.Append($"{contentMd5}\n"); // Content-MD5
  499 + stringToSign.Append("application/json\n"); // Content-Type
  500 + stringToSign.Append($"{dateStr}\n"); // Date
  501 + stringToSign.Append(canonicalizedHeaders.ToString()); // CanonicalizedHeaders
  502 + stringToSign.Append(canonicalizedResource); // CanonicalizedResource
  503 +
  504 + // 计算签名(HMAC-SHA1)
  505 + string signature;
  506 + using (var hmac = new HMACSHA1(Encoding.UTF8.GetBytes(_accessKeySecret)))
  507 + {
  508 + var signatureBytes = hmac.ComputeHash(Encoding.UTF8.GetBytes(stringToSign.ToString()));
  509 + signature = Convert.ToBase64String(signatureBytes);
  510 + }
  511 +
  512 + // 构建 Authorization 头
  513 + var authorization = $"acs {_accessKeyId}:{signature}";
  514 +
  515 + // 添加 Content-MD5 到内容头(Content-MD5 是内容头,不是请求头)
  516 + content.Headers.TryAddWithoutValidation("Content-MD5", contentMd5);
  517 +
  518 + // 创建请求并添加所有必需的请求头
  519 + using (var request = new HttpRequestMessage(HttpMethod.Post, url))
  520 + {
  521 + request.Content = content;
  522 +
  523 + // 添加标准 HTTP 头
  524 + request.Headers.Add("Accept", "application/json");
  525 + request.Headers.Add("Date", dateStr);
  526 +
  527 + // 添加阿里云协议头
485 528 foreach (var header in acsHeaders)
486 529 {
487   - canonicalizedHeaders.Append($"{header.Key}:{header.Value}\n");
  530 + request.Headers.Add(header.Key, header.Value);
488 531 }
489   -
490   - // 构建 CanonicalizedResource(请求路径)
491   - var uri = new Uri(url);
492   - var canonicalizedResource = uri.AbsolutePath;
493   -
494   - // 构建待签名字符串
495   - var stringToSign = new StringBuilder();
496   - stringToSign.Append("POST\n"); // HTTP-Verb
497   - stringToSign.Append("application/json\n"); // Accept
498   - stringToSign.Append($"{contentMd5}\n"); // Content-MD5
499   - stringToSign.Append("application/json\n"); // Content-Type
500   - stringToSign.Append($"{dateStr}\n"); // Date
501   - stringToSign.Append(canonicalizedHeaders.ToString()); // CanonicalizedHeaders
502   - stringToSign.Append(canonicalizedResource); // CanonicalizedResource
503   -
504   - // 计算签名(HMAC-SHA1)
505   - string signature;
506   - using (var hmac = new HMACSHA1(Encoding.UTF8.GetBytes(_accessKeySecret)))
507   - {
508   - var signatureBytes = hmac.ComputeHash(Encoding.UTF8.GetBytes(stringToSign.ToString()));
509   - signature = Convert.ToBase64String(signatureBytes);
510   - }
511   -
512   - // 构建 Authorization 头
513   - var authorization = $"acs {_accessKeyId}:{signature}";
514   -
515   - // 添加 Content-MD5 到内容头(Content-MD5 是内容头,不是请求头)
516   - content.Headers.TryAddWithoutValidation("Content-MD5", contentMd5);
517   -
518   - // 创建请求并添加所有必需的请求头
519   - using (var request = new HttpRequestMessage(HttpMethod.Post, url))
520   - {
521   - request.Content = content;
522   -
523   - // 添加标准 HTTP 头
524   - request.Headers.Add("Accept", "application/json");
525   - request.Headers.Add("Date", dateStr);
526   -
527   - // 添加阿里云协议头
528   - foreach (var header in acsHeaders)
529   - {
530   - request.Headers.Add(header.Key, header.Value);
531   - }
532   -
533   - // 添加 Authorization 头
534   - request.Headers.Add("Authorization", authorization);
535   -
536   - var response = await _httpClient.SendAsync(request);
537   - var responseContent = await response.Content.ReadAsStringAsync();
  532 +
  533 + // 添加 Authorization 头
  534 + request.Headers.Add("Authorization", authorization);
  535 +
  536 + var response = await _httpClient.SendAsync(request);
  537 + var responseContent = await response.Content.ReadAsStringAsync();
538 538  
539 539 // 保存原始响应
540 540 var rawResponse = responseContent;
... ... @@ -543,9 +543,9 @@ namespace NCC.Extend
543 543 {
544 544 // HTTP请求失败,根据配置决定是否通过
545 545 var failOnError = App.Configuration["NCC_App:ImageModeration:FailOnError"] == "true";
546   - return new ModerationResult
547   - {
548   - Passed = !failOnError,
  546 + return new ModerationResult
  547 + {
  548 + Passed = !failOnError,
549 549 Message = $"HTTP请求失败:{response.StatusCode}",
550 550 RawResponse = rawResponse
551 551 };
... ... @@ -556,9 +556,9 @@ namespace NCC.Extend
556 556 if (result == null)
557 557 {
558 558 var failOnError = App.Configuration["NCC_App:ImageModeration:FailOnError"] == "true";
559   - return new ModerationResult
560   - {
561   - Passed = !failOnError,
  559 + return new ModerationResult
  560 + {
  561 + Passed = !failOnError,
562 562 Message = "响应解析失败",
563 563 RawResponse = rawResponse
564 564 };
... ... @@ -574,9 +574,9 @@ namespace NCC.Extend
574 574 var friendlyMsg = apiCode == 596
575 575 ? "内容安全服务未开通或当前账号无权限,请到阿里云控制台开通「内容安全」并确认当前 AccessKey 有权限"
576 576 : (apiMsg.Length > 0 ? apiMsg : "审核接口返回失败");
577   - return new ModerationResult
578   - {
579   - Passed = !failOnError,
  577 + return new ModerationResult
  578 + {
  579 + Passed = !failOnError,
580 580 Message = friendlyMsg,
581 581 RawResponse = rawResponse,
582 582 Details = result
... ... @@ -588,9 +588,9 @@ namespace NCC.Extend
588 588 if (data == null || data.Count == 0)
589 589 {
590 590 var failOnError = App.Configuration["NCC_App:ImageModeration:FailOnError"] == "true";
591   - return new ModerationResult
592   - {
593   - Passed = !failOnError,
  591 + return new ModerationResult
  592 + {
  593 + Passed = !failOnError,
594 594 Message = "审核结果数据为空",
595 595 RawResponse = rawResponse,
596 596 Details = result
... ... @@ -601,9 +601,9 @@ namespace NCC.Extend
601 601 var results = taskResult["results"] as JArray;
602 602 if (results == null || results.Count == 0)
603 603 {
604   - return new ModerationResult
605   - {
606   - Passed = true,
  604 + return new ModerationResult
  605 + {
  606 + Passed = true,
607 607 Message = "没有审核结果,默认通过",
608 608 RawResponse = rawResponse,
609 609 Details = taskResult
... ... @@ -624,7 +624,7 @@ namespace NCC.Extend
624 624 label = item["label"]?.ToString(),
625 625 rate = item["rate"]?.ToString()
626 626 });
627   -
  627 +
628 628 if (suggestion == "block")
629 629 {
630 630 blockScenes.Add(scene ?? "unknown");
... ... @@ -633,9 +633,9 @@ namespace NCC.Extend
633 633  
634 634 if (blockScenes.Count > 0)
635 635 {
636   - return new ModerationResult
637   - {
638   - Passed = false,
  636 + return new ModerationResult
  637 + {
  638 + Passed = false,
639 639 Message = $"图片审核未通过,违规场景:{string.Join(", ", blockScenes)}",
640 640 RawResponse = rawResponse,
641 641 Details = new
... ... @@ -646,9 +646,9 @@ namespace NCC.Extend
646 646 };
647 647 }
648 648  
649   - return new ModerationResult
650   - {
651   - Passed = true,
  649 + return new ModerationResult
  650 + {
  651 + Passed = true,
652 652 Message = "图片审核通过",
653 653 RawResponse = rawResponse,
654 654 Details = new
... ...
netcore/src/Modularity/Extend/NCC.Extend/LqAnnualSummaryService.cs
... ... @@ -872,7 +872,8 @@ namespace NCC.Extend
872 872 var allStoreGroups = currentData.Select(x => new { x.StoreId, x.StoreName, x.BusinessUnitName, x.BusinessUnitId })
873 873 .Union(lastYearData.Select(x => new { x.StoreId, x.StoreName, x.BusinessUnitName, x.BusinessUnitId }))
874 874 .GroupBy(x => x.StoreId)
875   - .Select(g => {
  875 + .Select(g =>
  876 + {
876 877 // 优先选择有 BusinessUnitId 的记录,如果都有或都没有,选择第一条
877 878 var preferred = g.OrderByDescending(x => !string.IsNullOrEmpty(x.BusinessUnitId))
878 879 .ThenByDescending(x => x.BusinessUnitName)
... ... @@ -882,7 +883,7 @@ namespace NCC.Extend
882 883 .OrderBy(x => x.BusinessUnitName)
883 884 .ThenBy(x => x.StoreName)
884 885 .ToList();
885   -
  886 +
886 887 var allStores = allStoreGroups;
887 888  
888 889 // 批量获取门店开业时间
... ...
netcore/src/Modularity/Extend/NCC.Extend/LqReportService.cs
... ... @@ -4200,18 +4200,36 @@ namespace NCC.Extend
4200 4200 .OrderBy(x => x.TkCount, OrderByType.Desc)
4201 4201 .ToListAsync();
4202 4202  
4203   - // 2. 拓客人员拓客人数排名前五
4204   - var personRanking = await baseQuery
4205   - .GroupBy(x => x.ExpansionUserId)
4206   - .Select(x => new
4207   - {
4208   - UserId = x.ExpansionUserId,
4209   - UserName = SqlFunc.Subqueryable<UserEntity>().Where(u => u.Id == x.ExpansionUserId).Select(u => u.RealName),
4210   - TkCount = SqlFunc.AggregateCount(x.Id)
4211   - })
4212   - .OrderBy(x => x.TkCount, OrderByType.Desc)
4213   - .Take(5)
4214   - .ToListAsync();
  4203 + // 2. 拓客人员拓客人数排名前五(按拓客人员ID聚合去重,同一人员只出现一次)
  4204 + var personRankingSql = $@"
  4205 + SELECT
  4206 + tk.F_ExpansionUserId AS UserId,
  4207 + COALESCE(MAX(u.F_REALNAME), '') AS UserName,
  4208 + COUNT(tk.F_Id) AS TkCount
  4209 + FROM lq_tkjlb tk
  4210 + LEFT JOIN BASE_USER u ON tk.F_ExpansionUserId = u.F_Id
  4211 + WHERE tk.F_ExpansionTime >= '{startTime:yyyy-MM-dd HH:mm:ss}'
  4212 + AND tk.F_ExpansionTime <= '{endTime:yyyy-MM-dd HH:mm:ss}'";
  4213 + if (!string.IsNullOrEmpty(input.EventId))
  4214 + {
  4215 + personRankingSql += $" AND tk.F_EventId = '{input.EventId}'";
  4216 + }
  4217 + if (input.StoreIds != null && input.StoreIds.Any())
  4218 + {
  4219 + var storeIdsStr = string.Join("','", input.StoreIds);
  4220 + personRankingSql += $" AND tk.F_StoreId IN ('{storeIdsStr}')";
  4221 + }
  4222 + personRankingSql += @"
  4223 + GROUP BY tk.F_ExpansionUserId
  4224 + ORDER BY TkCount DESC
  4225 + LIMIT 5";
  4226 + var personRankingRaw = await _db.Ado.SqlQueryAsync<dynamic>(personRankingSql);
  4227 + var personRanking = (personRankingRaw ?? Enumerable.Empty<dynamic>()).Select(x => new
  4228 + {
  4229 + UserId = (string)x.UserId,
  4230 + UserName = (string)(x.UserName ?? ""),
  4231 + TkCount = Convert.ToInt32(x.TkCount ?? 0)
  4232 + }).ToList();
4215 4233  
4216 4234 // 3. 活动列表(用于筛选)
4217 4235 var eventList = await _db.Queryable<LqTkjlbEntity>()
... ...
netcore/src/Modularity/Extend/NCC.Extend/LqStatisticsService.cs
... ... @@ -858,8 +858,8 @@ namespace NCC.Extend.LqStatistics
858 858 $"📊 今日日报已生成,点击链接查看\n\n"
859 859 + $"本月全门店目标业绩:{monthlyStats.TargetPerformance:N0}元\n"
860 860 + $"本月已完成业绩:{monthlyStats.ActualPerformance:N0}元\n"
861   - + $"完成率:{monthlyStats.CompletionRate:F2}%\n\n"
862   - + $"https://erp.lvqianmeiye.com/html/dailyReportnew.html";
  861 + + $"完成率:{monthlyStats.CompletionRate:F2}%";
  862 + // + $"https://erp.lvqianmeiye.com/html/dailyReportnew.html";
863 863  
864 864 var result = await _weChatBotService.SendTextMessage(messageContent);
865 865 return new
... ...
netcore/src/Modularity/Extend/NCC.Extend/LqStoreDashboardService.cs
... ... @@ -61,6 +61,7 @@ namespace NCC.Extend
61 61 /// - ConsumePerformance: 消耗业绩(当月消耗金额汇总)
62 62 /// - CompletionRate: 完成率(消耗业绩/目标业绩 × 100%)
63 63 /// - NetPerformance: 净业绩(开单业绩 - 退卡金额)
  64 + /// - LifeBeautyPerformance/MedicalBeautyPerformance/TechBeautyPerformance: 生美/医美/科美实收业绩(开单实收 - 退款,按品项分类统计)
64 65 /// </remarks>
65 66 /// <param name="input">查询参数</param>
66 67 /// <returns>门店驾驶舱统计数据</returns>
... ... @@ -177,26 +178,27 @@ namespace NCC.Extend
177 178 // 9. 计算净业绩(开单业绩 - 退卡金额)
178 179 var netPerformance = billingAmount - refundAmount;
179 180  
180   - // 10. 获取人头数(去重后的消费会员数)
181   - var headCount = await _db.Queryable<LqXhHyhkEntity>()
182   - .Where(x => x.Md == input.StoreId && x.IsEffective == 1)
183   - .Where(x => x.Hksj.HasValue && x.Hksj.Value >= startDate && x.Hksj.Value <= endDateTime)
184   - .Select(x => x.Hy)
185   - .Distinct()
186   - .CountAsync();
187   -
188   - // 11. 获取人次(日度去重客户数)- 使用SQL查询
189   - var personCountSql = $@"
190   - SELECT COUNT(DISTINCT CONCAT(xh.Hy, '-', DATE_FORMAT(xh.Hksj, '%Y-%m-%d'))) as PersonCount
  181 + // 10、11. 人头数与人次用同一套条件、同一条 SQL 统计,避免口径不一致导致人头>人次的异常
  182 + // - 人头数:去重后的消费会员数(排除 Hy 为空/NULL,否则 MySQL COUNT(DISTINCT Hy) 会把 NULL 算成 1,而人次 CONCAT 不统计 NULL,导致人头>人次)
  183 + // - 人次:日度去重(同一会员同一天多笔耗卡算 1 人次),理论上人次 >= 人头
  184 + var headPersonSql = $@"
  185 + SELECT
  186 + COUNT(DISTINCT CASE WHEN xh.Hy IS NOT NULL AND TRIM(IFNULL(xh.Hy, '')) != '' THEN xh.Hy END) as HeadCount,
  187 + COUNT(DISTINCT CASE WHEN xh.Hy IS NOT NULL AND TRIM(IFNULL(xh.Hy, '')) != '' THEN CONCAT(xh.Hy, '-', DATE_FORMAT(xh.Hksj, '%Y-%m-%d')) END) as PersonCount
191 188 FROM lq_xh_hyhk xh
192 189 WHERE xh.Md = '{input.StoreId}'
193 190 AND xh.F_IsEffective = 1
  191 + AND xh.Hksj IS NOT NULL
194 192 AND xh.Hksj >= '{startDate:yyyy-MM-dd HH:mm:ss}'
195 193 AND xh.Hksj <= '{endDateTime:yyyy-MM-dd HH:mm:ss}'";
196   - var personCountResult = await _db.Ado.SqlQueryAsync<dynamic>(personCountSql);
197   - var personCount = personCountResult?.FirstOrDefault() != null
198   - ? Convert.ToInt32(personCountResult.FirstOrDefault().PersonCount ?? 0)
199   - : 0;
  194 + var headPersonResult = await _db.Ado.SqlQuerySingleAsync<dynamic>(headPersonSql);
  195 + var headCount = headPersonResult != null ? Convert.ToInt32(headPersonResult.HeadCount ?? 0) : 0;
  196 + var personCount = headPersonResult != null ? Convert.ToInt32(headPersonResult.PersonCount ?? 0) : 0;
  197 + // 兜底:按定义人次不应小于人头,若因边界情况出现则取较大值
  198 + if (personCount < headCount)
  199 + {
  200 + personCount = headCount;
  201 + }
200 202  
201 203 // 12. 获取项目数(消耗的项目总数,从品项明细表统计原始项目数)
202 204 var projectCountSql = $@"
... ... @@ -222,24 +224,50 @@ namespace NCC.Extend
222 224 // 15. 计算人均项目数(项目数/人头数)
223 225 var avgProjectPerHead = headCount > 0 ? projectCount / (decimal)headCount : 0m;
224 226  
225   - // 16. 计算各分类消耗业绩(生美、医美、科美)
226   - var categoryPerformanceSql = $@"
  227 + // 16. 计算各分类实收业绩(生美、医美、科美)= 开单健康师业绩(按品项分类)- 退卡健康师业绩(按品项分类)
  228 + // 口径与开单业绩一致:开单业绩来自开单表 sfyj,健康师业绩表 jksyj 按开单汇总即等于 sfyj,故生美+医美+科美合计与开单业绩一致
  229 + // 16.1 开单实收:从开单健康师业绩表 lq_kd_jksyj 按 F_ItemCategory 汇总 jksyj(与开单表 kdrq 同批开单)
  230 + var categoryBillingSql = $@"
227 231 SELECT
228   - COALESCE(SUM(CASE WHEN COALESCE(xmzl.qt2, '其他') = '生美' THEN CAST(COALESCE(xhpx.F_TotalPrice, xhpx.pxjg * xhpx.F_ProjectNumber, 0) AS DECIMAL(18,2)) ELSE 0 END), 0) as LifeBeautyPerformance,
229   - COALESCE(SUM(CASE WHEN COALESCE(xmzl.qt2, '其他') = '医美' THEN CAST(COALESCE(xhpx.F_TotalPrice, xhpx.pxjg * xhpx.F_ProjectNumber, 0) AS DECIMAL(18,2)) ELSE 0 END), 0) as MedicalBeautyPerformance,
230   - COALESCE(SUM(CASE WHEN COALESCE(xmzl.qt2, '其他') = '科美' THEN CAST(COALESCE(xhpx.F_TotalPrice, xhpx.pxjg * xhpx.F_ProjectNumber, 0) AS DECIMAL(18,2)) ELSE 0 END), 0) as TechBeautyPerformance
231   - FROM lq_xh_pxmx xhpx
232   - INNER JOIN lq_xh_hyhk xh ON xhpx.F_ConsumeInfoId = xh.F_Id AND xh.F_IsEffective = 1
233   - LEFT JOIN lq_xmzl xmzl ON xhpx.px = xmzl.F_Id AND xmzl.F_IsEffective = 1
234   - WHERE xhpx.F_IsEffective = 1
235   - AND xh.md = '{input.StoreId}'
236   - AND xh.hksj >= '{startDate:yyyy-MM-dd 00:00:00}'
237   - AND xh.hksj <= '{endDateTime:yyyy-MM-dd HH:mm:ss}'";
238   - var categoryPerformanceResult = await _db.Ado.SqlQueryAsync<dynamic>(categoryPerformanceSql);
239   - var categoryPerformance = categoryPerformanceResult?.FirstOrDefault();
240   - var lifeBeautyPerformance = categoryPerformance != null ? Convert.ToDecimal(categoryPerformance.LifeBeautyPerformance ?? 0) : 0m;
241   - var medicalBeautyPerformance = categoryPerformance != null ? Convert.ToDecimal(categoryPerformance.MedicalBeautyPerformance ?? 0) : 0m;
242   - var techBeautyPerformance = categoryPerformance != null ? Convert.ToDecimal(categoryPerformance.TechBeautyPerformance ?? 0) : 0m;
  232 + COALESCE(SUM(CASE WHEN COALESCE(jksyj.F_ItemCategory, '') = '生美' THEN CAST(jksyj.jksyj AS DECIMAL(18,2)) ELSE 0 END), 0) as LifeBeautyBilling,
  233 + COALESCE(SUM(CASE WHEN COALESCE(jksyj.F_ItemCategory, '') = '医美' THEN CAST(jksyj.jksyj AS DECIMAL(18,2)) ELSE 0 END), 0) as MedicalBeautyBilling,
  234 + COALESCE(SUM(CASE WHEN COALESCE(jksyj.F_ItemCategory, '') = '科美' THEN CAST(jksyj.jksyj AS DECIMAL(18,2)) ELSE 0 END), 0) as TechBeautyBilling
  235 + FROM lq_kd_jksyj jksyj
  236 + INNER JOIN lq_kd_kdjlb billing ON jksyj.glkdbh = billing.F_Id
  237 + WHERE jksyj.F_IsEffective = 1
  238 + AND billing.F_IsEffective = 1
  239 + AND billing.djmd = '{input.StoreId}'
  240 + AND billing.kdrq >= '{startDate:yyyy-MM-dd HH:mm:ss}'
  241 + AND billing.kdrq <= '{endDateTime:yyyy-MM-dd HH:mm:ss}'";
  242 + var categoryBillingResult = await _db.Ado.SqlQueryAsync<dynamic>(categoryBillingSql);
  243 + var categoryBilling = categoryBillingResult?.FirstOrDefault();
  244 + var lifeBeautyBilling = categoryBilling != null ? Convert.ToDecimal(categoryBilling.LifeBeautyBilling ?? 0) : 0m;
  245 + var medicalBeautyBilling = categoryBilling != null ? Convert.ToDecimal(categoryBilling.MedicalBeautyBilling ?? 0) : 0m;
  246 + var techBeautyBilling = categoryBilling != null ? Convert.ToDecimal(categoryBilling.TechBeautyBilling ?? 0) : 0m;
  247 +
  248 + // 16.2 退款金额:从退卡健康师业绩表 lq_hytk_jksyj 按 F_ItemCategory 汇总 jksyj
  249 + var categoryRefundSql = $@"
  250 + SELECT
  251 + COALESCE(SUM(CASE WHEN COALESCE(jksyj.F_ItemCategory, '') = '生美' THEN CAST(jksyj.jksyj AS DECIMAL(18,2)) ELSE 0 END), 0) as LifeBeautyRefund,
  252 + COALESCE(SUM(CASE WHEN COALESCE(jksyj.F_ItemCategory, '') = '医美' THEN CAST(jksyj.jksyj AS DECIMAL(18,2)) ELSE 0 END), 0) as MedicalBeautyRefund,
  253 + COALESCE(SUM(CASE WHEN COALESCE(jksyj.F_ItemCategory, '') = '科美' THEN CAST(jksyj.jksyj AS DECIMAL(18,2)) ELSE 0 END), 0) as TechBeautyRefund
  254 + FROM lq_hytk_jksyj jksyj
  255 + INNER JOIN lq_hytk_hytk refund ON jksyj.gltkbh = refund.F_Id
  256 + WHERE jksyj.F_IsEffective = 1
  257 + AND refund.F_IsEffective = 1
  258 + AND refund.md = '{input.StoreId}'
  259 + AND refund.tksj >= '{startDate:yyyy-MM-dd HH:mm:ss}'
  260 + AND refund.tksj <= '{endDateTime:yyyy-MM-dd HH:mm:ss}'";
  261 + var categoryRefundResult = await _db.Ado.SqlQueryAsync<dynamic>(categoryRefundSql);
  262 + var categoryRefund = categoryRefundResult?.FirstOrDefault();
  263 + var lifeBeautyRefund = categoryRefund != null ? Convert.ToDecimal(categoryRefund.LifeBeautyRefund ?? 0) : 0m;
  264 + var medicalBeautyRefund = categoryRefund != null ? Convert.ToDecimal(categoryRefund.MedicalBeautyRefund ?? 0) : 0m;
  265 + var techBeautyRefund = categoryRefund != null ? Convert.ToDecimal(categoryRefund.TechBeautyRefund ?? 0) : 0m;
  266 +
  267 + // 16.3 实收业绩 = 开单健康师业绩 - 退卡健康师业绩
  268 + var lifeBeautyPerformance = lifeBeautyBilling - lifeBeautyRefund;
  269 + var medicalBeautyPerformance = medicalBeautyBilling - medicalBeautyRefund;
  270 + var techBeautyPerformance = techBeautyBilling - techBeautyRefund;
243 271  
244 272 // 17. 获取储扣统计(储扣金额、次数、渗透率等)
245 273 var deductSql = $@"
... ...