diff --git a/docs/generate_saas_excel.py b/docs/generate_saas_excel.py
new file mode 100644
index 0000000..40f0ecb
--- /dev/null
+++ b/docs/generate_saas_excel.py
@@ -0,0 +1,182 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+美业SaaS系统功能规划 - Excel生成脚本
+根据功能规划文档生成包含:功能、详细说明、是否可复用、复用后人天、全新开发人天的Excel
+"""
+
+import openpyxl
+from openpyxl.styles import Font, Alignment, Border, Side, PatternFill
+from openpyxl.utils import get_column_letter
+
+# 功能数据:基于美业SaaS系统功能规划.md
+# 格式:(一级模块, 二级模块, 详细说明, 是否可复用, 复用说明, 复用后人天, 全新开发人天)
+DATA = [
+ # 一、SaaS平台基础能力
+ ("一、SaaS平台基础能力", "1.1 多租户管理", "租户注册与开通、租户信息管理、服务套餐管理、租户数据隔离、计费与结算", "否", "SaaS核心能力,需全新设计", 25, 25),
+ ("一、SaaS平台基础能力", "1.2 权限与组织架构", "组织架构模板、门店模式配置、角色权限、用户管理、数据权限控制", "部分", "OrganizeService、UserService、base_organize可参考,需多租户改造", 8, 20),
+ ("一、SaaS平台基础能力", "1.3 系统配置管理", "业务规则配置、系统参数、界面个性化、审批流程、通知规则", "部分", "现有配置表可参考,需租户级配置", 5, 15),
+ ("一、SaaS平台基础能力", "1.4 数据安全与隔离", "数据加密、访问控制、操作审计、数据库/文件/缓存隔离", "部分", "现有权限体系可参考", 3, 12),
+ ("一、SaaS平台基础能力", "1.5 SaaS订阅与计费系统", "套餐管理、订阅周期、功能开关、试用期、超额限制、订阅管理", "否", "商业化闭环核心,需全新开发", 30, 30),
+ ("一、SaaS平台基础能力", "1.6 数据迁移与初始化工具", "客户/卡项/员工/项目数据导入、迁移方案、映射转换、初始化向导", "部分", "现有导入逻辑可复用,需支持多格式和映射", 8, 25),
+ ("一、SaaS平台基础能力", "1.7 文件存储与CDN系统", "对象存储+CDN架构、存储配额、CDN配置、图片优化、文件管理", "部分", "现有文件上传可改造,需OSS+CDN架构", 5, 20),
+ ("一、SaaS平台基础能力", "1.8 操作日志与审计系统", "用户操作日志、数据变更记录、关键行为审计、财务专项审计、删除作废审计", "部分", "现有日志可扩展,需增强审计能力", 6, 18),
+ ("一、SaaS平台基础能力", "1.9 异常与纠纷处理系统", "异常工单、纠纷处理、投诉管理、处理流程", "否", "美业痛点,需全新设计", 15, 15),
+
+ # 二、核心业务模块
+ ("二、核心业务模块", "2.1 门店管理系统", "门店信息维护、归属管理、运营数据、权限管理、合作医院管理(医美结算)", "是", "LqMdxxService、LqMdTargetService、LqMdFzhtService可拆分,医美结算需新增", 12, 25),
+ ("二、核心业务模块", "2.2 人员管理系统", "员工档案、变动管理、归属快照、战队管理、岗位管理、技能与项目能力、考勤管理", "是", "EmployeeService、UserService、LqAttendanceSummaryService可拆分,技能模型需新增", 15, 35),
+ ("二、核心业务模块", "2.3 客户管理系统", "客户档案、标签系统、客户分群、跟进管理、客户关怀、价值分析", "是", "LqKhxxService可拆分,标签分群需增强", 10, 25),
+ ("二、核心业务模块", "2.4 开单耗卡系统", "开单、套餐拆分组合、业绩归属、提成规则、开单模板、耗卡、批量耗卡", "是", "LqKdKdjlbService、LqOrderRecordsService、LqXhHyhkService、LqPackageInfoService可拆分", 18, 40),
+ ("二、核心业务模块", "2.5 业绩统计系统", "个人/门店/团队业绩、业绩分类、合作医院业绩、趋势分析、排行榜", "是", "LqStatisticsService、LqYjmxbService、LqJlmxbService可拆分", 10, 25),
+ ("二、核心业务模块", "2.6 工资核算系统", "工资字段配置、字段来源、公式配置、多岗位核算、工资管理", "是", "LqSalaryService及各类岗位工资Service可拆分,需支持灵活配置", 15, 35),
+
+ # 三、营销与增长系统
+ ("三、营销与增长系统", "3.1 拓客与线索管理", "渠道管理、渠道成本、效果分析、新客体验卡、0元引流、到店核销、线索池、跟进管理、转化率分析", "否", "无现成模块,需全新开发", 35, 35),
+ ("三、营销与增长系统", "3.2 裂变与转介绍", "老带新、邀请有礼、返积分/返余额", "否", "无现成模块", 15, 15),
+ ("三、营销与增长系统", "3.3 新客转化", "新客转化策略、转化分析", "否", "无现成模块", 10, 10),
+ ("三、营销与增长系统", "3.4 老客复购", "营销活动、会员卡、积分、优惠券、会员等级", "部分", "LqXhHyhkService卡项逻辑可参考,营销体系需新建", 8, 30),
+ ("三、营销与增长系统", "3.5 客户生命周期管理(CLM)", "生命周期状态机、阶段流转、对应营销策略", "否", "无现成模块", 20, 20),
+
+ # 四、预约与服务模块
+ ("四、预约与服务模块", "4.1 预约管理系统", "在线预约、预约类型、时间段选择、预约确认修改、预约列表、冲突检测", "是", "ScheduleService可拆分,需增强在线预约能力", 8, 20),
+ ("四、预约与服务模块", "4.2 服务项目管理", "品项管理(生美/医美/科美/产品)、套餐、分类、价格、服务流程", "部分", "LqCpxxService、LqXmzlService、LqPackageInfoService可参考", 6, 18),
+ ("四、预约与服务模块", "4.2.3 服务过程记录", "服务备注、前后对比图、注意事项、服务记录管理", "是", "LqYyjlService等可拆分", 5, 12),
+ ("四、预约与服务模块", "4.3 健康师排班管理", "排班计划、自动/手动排班、班次管理", "部分", "ScheduleService排班逻辑可参考", 5, 15),
+ ("四、预约与服务模块", "4.4 服务评价系统", "服务评价、健康师评价、评价统计、评价应用", "否", "无现成模块", 10, 10),
+ ("四、预约与服务模块", "4.5 爽约/取消/超时管理", "爽约记录、黑名单、预约限制、取消规则、超时管理", "否", "无现成模块", 15, 15),
+
+ # 五、支付与财务模块
+ ("五、支付与财务模块", "5.1 支付管理系统", "微信/支付宝/银行卡/现金/储值卡支付、支付记录", "部分", "现有支付逻辑可参考,需对接多支付渠道", 5, 20),
+ ("五、支付与财务模块", "5.2 财务管理系统", "财务核算、资金管理、应收应付、财务报表", "是", "LqFinancialReportService、LqSkzhService可拆分", 12, 30),
+ ("五、支付与财务模块", "5.3 对账管理系统", "对账功能、对账分析", "部分", "现有对账逻辑可参考", 5, 15),
+ ("五、支付与财务模块", "5.4 发票管理系统", "发票管理、发票配置", "否", "无现成模块", 10, 10),
+ ("五、支付与财务模块", "5.5 分账与结算规则", "分账管理、结算管理、医美结算", "部分", "LqCooperationCostService医美结算可参考", 8, 25),
+ ("五、支付与财务模块", "5.6 财务风控", "风控规则、风控管理、合规管理", "否", "无现成模块", 15, 15),
+ ("五、支付与财务模块", "5.7 经营与业财分析", "收入确认、成本归集、项目毛利、人效分析、门店盈亏、经营财报", "是", "LqReportService、LqAnnualSummaryService、LqStoreExpenseService可拆分", 15, 35),
+
+ # 六、库存与供应链模块
+ ("六、库存与供应链模块", "6.1 库存管理系统", "库存管理、库存使用、出入库、盘点", "是", "LqInventoryService、LqInventoryUsageService、LqStoreConsumableInventoryService可拆分", 8, 20),
+ ("六、库存与供应链模块", "6.2 采购管理系统", "采购流程、采购分析", "部分", "LqPurchaseRecordsService可参考", 5, 15),
+ ("六、库存与供应链模块", "6.3 供应商管理系统", "供应商管理、供应商合作", "部分", "LqLaundrySupplierService可参考", 3, 10),
+ ("六、库存与供应链模块", "6.4 设备管理系统", "设备档案、维护、使用管理", "否", "无现成模块", 12, 12),
+ ("六、库存与供应链模块", "6.5 耗材自动关联项目", "项目耗材配置、耗材管理", "部分", "现有耗材关联逻辑可参考", 5, 12),
+
+ # 七、数据分析与决策支持
+ ("七、数据分析与决策支持", "7.1 数据驾驶舱", "集团/事业部/门店/移动端驾驶舱", "是", "LqStoreDashboardService、LqTkDashboardService、LqBusinessUnitDashboardService、LqTechDepartmentDashboardService可拆分", 12, 30),
+ ("七、数据分析与决策支持", "7.2 报表分析系统", "报表类型、报表功能、自定义报表", "是", "LqReportService、LqStatisticsService、LqDailyReportService可拆分", 10, 25),
+ ("七、数据分析与决策支持", "7.3 客户画像分析", "客户画像、客户分析", "是", "MemberPortraitService可拆分", 6, 15),
+ ("七、数据分析与决策支持", "7.4 经营分析系统", "经营分析、决策支持", "是", "LqReportService、LqAnnualSummaryService可拆分", 8, 20),
+ ("七、数据分析与决策支持", "7.5 自定义指标与看板", "自定义指标、拖拽式看板", "否", "无现成模块", 20, 20),
+ ("七、数据分析与决策支持", "7.6 经营对标分析", "对标分析、对标管理", "是", "LqReportService对标逻辑可参考", 6, 15),
+
+ # 八、移动端与小程序
+ ("八、移动端与小程序", "8.1 员工端(健康师)", "开单、耗卡、客户备注、排班查看", "是", "现有移动端开单耗卡逻辑可拆分", 10, 25),
+ ("八、移动端与小程序", "8.2 店长端", "今日业绩、人员状态、异常提醒", "是", "LqStoreDashboardService、门店看板可拆分", 8, 20),
+ ("八、移动端与小程序", "8.3 老板端", "多店汇总、盈亏、趋势分析", "是", "LqReportService、经营报表可拆分", 8, 20),
+ ("八、移动端与小程序", "8.4 客户端小程序", "预约、会员中心、订单、营销活动", "否", "无现成C端小程序", 30, 30),
+
+ # 九、消息与通知系统
+ ("九、消息与通知系统", "9.1 消息中心", "消息类型、消息管理", "部分", "现有消息逻辑可参考", 3, 10),
+ ("九、消息与通知系统", "9.2 通知推送系统", "多渠道通知、通知配置", "否", "无现成推送体系", 12, 12),
+ ("九、消息与通知系统", "9.3 营销消息系统", "营销消息、消息效果", "否", "无现成模块", 8, 8),
+ ("九、消息与通知系统", "9.4 事件触发规则引擎", "规则引擎配置、规则管理", "否", "无现成规则引擎", 20, 20),
+
+ # 十、第三方平台对接
+ ("十、第三方平台对接", "10.1 团购平台对接", "美团/大众点评等对接、团购数据分析", "否", "无现成对接,现有系统无此功能", 25, 25),
+ ("十、第三方平台对接", "10.2 设备仪器对接", "设备对接、设备管理", "否", "无现成对接,现有系统无此功能", 15, 15),
+ ("十、第三方平台对接", "10.3 考勤平台对接", "企业微信/钉钉考勤对接、考勤管理", "否", "无现成对接,现有系统无此功能", 12, 12),
+ ("十、第三方平台对接", "10.4 支付/财务平台对接", "支付平台、财务平台对接", "否", "无现成对接,现有系统无此功能", 15, 15),
+
+ # 十一及之后:现有系统均无,复用后人天=全新开发人天(还需多少天=全部)
+ ("十一、问卷调查系统", "11.1 问卷配置管理", "问卷创建、问卷模板", "否", "无现成模块,现有系统无此功能", 12, 12),
+ ("十一、问卷调查系统", "11.2 问卷发放与收集", "问卷发放、问卷收集", "否", "无现成模块,现有系统无此功能", 8, 8),
+ ("十一、问卷调查系统", "11.3 问卷数据分析", "问卷统计、问卷报告", "否", "无现成模块,现有系统无此功能", 8, 8),
+ ("十一、问卷调查系统", "11.4 满意度与复购联动", "满意度联动、复购联动", "否", "无现成模块,现有系统无此功能", 10, 10),
+
+ ("十二、AI智能助手", "12.0 AI经营助手", "今日重点关注、流失预警、项目盈利分析、AI对话", "否", "无现成模块,现有系统无此功能", 25, 25),
+ ("十二、AI智能助手", "12.1 AI客户价值评分", "客户价值模型、流失预警", "否", "无现成模块,现有系统无此功能", 15, 15),
+ ("十二、AI智能助手", "12.2 AI回访话术生成", "话术生成、话术优化", "否", "无现成模块,现有系统无此功能", 10, 10),
+ ("十二、AI智能助手", "12.3 AI项目推荐", "智能推荐、推荐优化", "否", "无现成模块,现有系统无此功能", 15, 15),
+ ("十二、AI智能助手", "12.4 AI经营分析", "智能分析、分析优化", "否", "无现成模块,现有系统无此功能", 15, 15),
+ ("十二、AI智能助手", "12.5 AI成本核算", "AI成本管理、成本报表", "否", "无现成模块,现有系统无此功能", 12, 12),
+ ("十二、AI智能助手", "12.6 AI客户流失预警", "流失预警模型、预警管理", "否", "无现成模块,现有系统无此功能", 12, 12),
+ ("十二、AI智能助手", "12.7 AI员工效率分析", "效率分析模型、效率管理", "否", "无现成模块,现有系统无此功能", 12, 12),
+
+ ("十三、商城系统", "13.1 商品管理", "商品信息、商品运营", "否", "无现成模块,现有系统无此功能", 15, 15),
+ ("十三、商城系统", "13.2 订单管理", "订单处理、订单管理", "否", "无现成模块,现有系统无此功能", 15, 15),
+ ("十三、商城系统", "13.3 积分兑换", "积分兑换配置、兑换功能", "否", "无现成模块,现有系统无此功能", 8, 8),
+ ("十三、商城系统", "13.4 商城运营", "商城配置、数据分析", "否", "无现成模块,现有系统无此功能", 10, 10),
+ ("十三、商城系统", "13.5 到店服务与商品联动", "服务后推荐、联动管理", "否", "无现成模块,现有系统无此功能", 12, 12),
+
+ ("十四、连锁总部策略引擎", "14.1 统一价格策略", "价格策略管理", "否", "无现成模块,现有系统无此功能", 15, 15),
+ ("十四、连锁总部策略引擎", "14.2 统一活动下发", "活动策略管理", "否", "无现成模块,现有系统无此功能", 10, 10),
+ ("十四、连锁总部策略引擎", "14.3 门店执行差异分析", "差异分析", "否", "无现成模块,现有系统无此功能", 12, 12),
+
+ ("十五、开放平台与生态", "15.1 API开放平台", "API管理、API功能", "否", "无现成模块,现有系统无此功能", 20, 20),
+ ("十五、开放平台与生态", "15.2 第三方集成", "支付集成、其他集成", "否", "无现成模块,现有系统无此功能", 15, 15),
+ ("十五、开放平台与生态", "15.3 应用市场", "应用管理、生态建设", "否", "无现成模块,现有系统无此功能", 25, 25),
+]
+
+def create_excel(filepath):
+ wb = openpyxl.Workbook()
+ ws = wb.active
+ ws.title = "美业SaaS功能规划"
+
+ # 表头
+ headers = ["一级模块", "二级模块/功能", "功能详细说明", "是否可复用", "复用说明(现有系统可复用内容)", "复用后还需人天", "全新开发人天"]
+ for col, h in enumerate(headers, 1):
+ cell = ws.cell(row=1, column=col, value=h)
+ cell.font = Font(bold=True)
+ cell.alignment = Alignment(horizontal="center", vertical="center", wrap_text=True)
+ cell.fill = PatternFill(start_color="4472C4", end_color="4472C4", fill_type="solid")
+ cell.font = Font(bold=True, color="FFFFFF")
+
+ # 数据
+ for row, item in enumerate(DATA, 2):
+ for col, val in enumerate(item, 1):
+ cell = ws.cell(row=row, column=col, value=val)
+ cell.alignment = Alignment(vertical="center", wrap_text=True)
+ if col == 4: # 是否可复用
+ if val == "是":
+ cell.fill = PatternFill(start_color="C6EFCE", end_color="C6EFCE", fill_type="solid")
+ elif val == "部分":
+ cell.fill = PatternFill(start_color="FFEB9C", end_color="FFEB9C", fill_type="solid")
+ else:
+ cell.fill = PatternFill(start_color="FFC7CE", end_color="FFC7CE", fill_type="solid")
+
+ # 列宽
+ ws.column_dimensions["A"].width = 22
+ ws.column_dimensions["B"].width = 26
+ ws.column_dimensions["C"].width = 50
+ ws.column_dimensions["D"].width = 12
+ ws.column_dimensions["E"].width = 45
+ ws.column_dimensions["F"].width = 12
+ ws.column_dimensions["G"].width = 14
+
+ # 汇总行(复用后人天=E列,全新开发人天=G列)
+ total_row = len(DATA) + 3
+ ws.cell(row=total_row, column=1, value="合计")
+ ws.cell(row=total_row, column=1).font = Font(bold=True)
+ ws.cell(row=total_row, column=6, value=f"=SUM(F2:F{len(DATA)+1})")
+ ws.cell(row=total_row, column=7, value=f"=SUM(G2:G{len(DATA)+1})")
+ ws.cell(row=total_row, column=6).font = Font(bold=True)
+ ws.cell(row=total_row, column=7).font = Font(bold=True)
+
+ # 说明行
+ ws.cell(row=total_row+2, column=1, value="说明:")
+ ws.cell(row=total_row+2, column=1).font = Font(bold=True)
+ ws.cell(row=total_row+3, column=1, value="1. 是否可复用:基于现有绿纤美业ERP系统,标注「是」= 可从现有系统拆分/改造;「部分」= 部分逻辑可复用;「否」= 需全新开发")
+ ws.merge_cells(start_row=total_row+3, start_column=1, end_row=total_row+3, end_column=7)
+ ws.cell(row=total_row+4, column=1, value="2. 复用说明:列出现有系统中可复用的Service或逻辑,便于开发时参考")
+ ws.merge_cells(start_row=total_row+4, start_column=1, end_row=total_row+4, end_column=7)
+ ws.cell(row=total_row+5, column=1, value="3. 复用后还需人天:复用现有代码/逻辑后,仍需投入的开发人天(含改造、适配、测试);无可复用时=全新开发人天")
+ ws.merge_cells(start_row=total_row+5, start_column=1, end_row=total_row+5, end_column=7)
+ ws.cell(row=total_row+6, column=1, value="4. 全新开发人天:若从零开发该功能,预估的开发人天")
+ ws.merge_cells(start_row=total_row+6, start_column=1, end_row=total_row+6, end_column=7)
+
+ wb.save(filepath)
+ print(f"Excel 已生成: {filepath}")
+
+if __name__ == "__main__":
+ create_excel("/Users/mr.wang/代码库/绿纤/lvqianmeiye_ERP/docs/美业SaaS系统功能规划.xlsx")
diff --git a/docs/美业SaaS系统功能规划.md b/docs/美业SaaS系统功能规划.md
index 8df73d9..6d505fb 100644
--- a/docs/美业SaaS系统功能规划.md
+++ b/docs/美业SaaS系统功能规划.md
@@ -20,16 +20,16 @@
- [1.9 异常与纠纷处理系统](#19-异常与纠纷处理系统) ⭐ **美业真实痛点**
### 二、核心业务模块
-- [2.1 门店管理系统](#21-门店管理系统)
+- [2.1 门店管理系统](#21-门店管理系统)(从现有系统拆分)
- [2.1.3 合作医院管理](#213-合作医院管理)
-- [2.2 人员管理系统](#22-人员管理系统)
+- [2.2 人员管理系统](#22-人员管理系统)(从现有系统拆分)
- [2.2.3 考勤管理系统](#223-考勤管理系统)
- [2.2.4 员工技能与项目能力模型](#224-员工技能与项目能力模型)
-- [2.3 客户管理系统](#23-客户管理系统)
-- [2.4 开单耗卡系统](#24-开单耗卡系统)
+- [2.3 客户管理系统](#23-客户管理系统)(从现有系统拆分)
+- [2.4 开单耗卡系统](#24-开单耗卡系统)(从现有系统拆分)
- [2.4.1 套餐拆分与组合开单](#241-套餐拆分与组合开单)
-- [2.5 业绩统计系统](#25-业绩统计系统)
-- [2.6 工资核算系统](#26-工资核算系统)
+- [2.5 业绩统计系统](#25-业绩统计系统)(从现有系统拆分)
+- [2.6 工资核算系统](#26-工资核算系统)(从现有系统拆分)
### 三、营销与增长系统
- [3.1 拓客与线索管理](#31-拓客与线索管理) ⭐ **把人弄进来**
@@ -39,41 +39,41 @@
- [3.5 客户生命周期管理(CLM)](#35-客户生命周期管理clm)
### 四、预约与服务模块
-- [4.1 预约管理系统](#41-预约管理系统)
+- [4.1 预约管理系统](#41-预约管理系统)(从现有系统拆分)
- [4.2 服务项目管理](#42-服务项目管理)
- - [4.2.3 服务过程记录(轻量版)](#423-服务过程记录轻量版)
+ - [4.2.3 服务过程记录(轻量版)](#423-服务过程记录轻量版)(从现有系统拆分)
- [4.3 健康师排班管理](#43-健康师排班管理)
- [4.4 服务评价系统](#44-服务评价系统)
- [4.5 爽约/取消/超时管理](#45-爽约取消超时管理)
### 五、支付与财务模块
- [5.1 支付管理系统](#51-支付管理系统)
-- [5.2 财务管理系统](#52-财务管理系统)
+- [5.2 财务管理系统](#52-财务管理系统)(从现有系统拆分)
- [5.3 对账管理系统](#53-对账管理系统)
- [5.4 发票管理系统](#54-发票管理系统)
- [5.5 分账与结算规则](#55-分账与结算规则)
- [5.6 财务风控](#56-财务风控)
-- [5.7 经营与业财分析](#57-经营与业财分析) ⭐ **核心差异化**
+- [5.7 经营与业财分析](#57-经营与业财分析) ⭐ **核心差异化**(从现有系统拆分)
### 六、库存与供应链模块
-- [6.1 库存管理系统](#61-库存管理系统)
+- [6.1 库存管理系统](#61-库存管理系统)(从现有系统拆分)
- [6.2 采购管理系统](#62-采购管理系统)
- [6.3 供应商管理系统](#63-供应商管理系统)
- [6.4 设备管理系统](#64-设备管理系统)
- [6.5 耗材自动关联项目](#65-耗材自动关联项目)
### 七、数据分析与决策支持
-- [7.1 数据驾驶舱](#71-数据驾驶舱)
-- [7.2 报表分析系统](#72-报表分析系统)
-- [7.3 客户画像分析](#73-客户画像分析)
-- [7.4 经营分析系统](#74-经营分析系统)
+- [7.1 数据驾驶舱](#71-数据驾驶舱)(从现有系统拆分)
+- [7.2 报表分析系统](#72-报表分析系统)(从现有系统拆分)
+- [7.3 客户画像分析](#73-客户画像分析)(从现有系统拆分)
+- [7.4 经营分析系统](#74-经营分析系统)(从现有系统拆分)
- [7.5 自定义指标与看板](#75-自定义指标与看板)
-- [7.6 经营对标分析](#76-经营对标分析)
+- [7.6 经营对标分析](#76-经营对标分析)(从现有系统拆分)
### 八、移动端与小程序
-- [8.1 员工端(健康师)](#81-员工端健康师) ⭐ **开单、消耗、客户备注、排班查看**
-- [8.2 店长端](#82-店长端) ⭐ **今日业绩、人员状态、异常提醒**
-- [8.3 老板端](#83-老板端) ⭐ **多店汇总、盈亏、趋势分析**
+- [8.1 员工端(健康师)](#81-员工端健康师) ⭐ **开单、消耗、客户备注、排班查看**(从现有系统拆分)
+- [8.2 店长端](#82-店长端) ⭐ **今日业绩、人员状态、异常提醒**(从现有系统拆分)
+- [8.3 老板端](#83-老板端) ⭐ **多店汇总、盈亏、趋势分析**(从现有系统拆分)
- [8.4 客户端小程序](#84-客户端小程序)
### 九、消息与通知系统
diff --git a/docs/美业SaaS系统功能规划.xlsx b/docs/美业SaaS系统功能规划.xlsx
new file mode 100644
index 0000000..ebc21b2
--- /dev/null
+++ b/docs/美业SaaS系统功能规划.xlsx
diff --git a/docs/美业系统升级项目需求拆分表.xlsx b/docs/美业系统升级项目需求拆分表.xlsx
index f2928e3..f1ea248 100644
--- a/docs/美业系统升级项目需求拆分表.xlsx
+++ b/docs/美业系统升级项目需求拆分表.xlsx
diff --git a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqStoreDashboard/StoreDashboardStatisticsOutput.cs b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqStoreDashboard/StoreDashboardStatisticsOutput.cs
index c44bb33..b89c0b4 100644
--- a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqStoreDashboard/StoreDashboardStatisticsOutput.cs
+++ b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqStoreDashboard/StoreDashboardStatisticsOutput.cs
@@ -96,17 +96,17 @@ namespace NCC.Extend.Entitys.Dto.LqStoreDashboard
public decimal AvgProjectPerHead { get; set; }
///
- /// 生美业绩(消耗业绩)
+ /// 生美业绩(实收业绩 = 开单实收 - 退款)
///
public decimal LifeBeautyPerformance { get; set; }
///
- /// 医美业绩(消耗业绩)
+ /// 医美业绩(实收业绩 = 开单实收 - 退款)
///
public decimal MedicalBeautyPerformance { get; set; }
///
- /// 科美业绩(消耗业绩)
+ /// 科美业绩(实收业绩 = 开单实收 - 退款)
///
public decimal TechBeautyPerformance { get; set; }
diff --git a/netcore/src/Modularity/Extend/NCC.Extend/ImageModerationService.cs b/netcore/src/Modularity/Extend/NCC.Extend/ImageModerationService.cs
index 9c02322..f9ee04f 100644
--- a/netcore/src/Modularity/Extend/NCC.Extend/ImageModerationService.cs
+++ b/netcore/src/Modularity/Extend/NCC.Extend/ImageModerationService.cs
@@ -38,24 +38,24 @@ namespace NCC.Extend
{
_httpClient = new HttpClient();
_enabled = App.Configuration["NCC_App:ImageModeration:Enabled"] == "true";
- _endpoint = App.Configuration["NCC_App:ImageModeration:Endpoint"]
+ _endpoint = App.Configuration["NCC_App:ImageModeration:Endpoint"]
?? "https://green.cn-shanghai.aliyuncs.com";
- _region = App.Configuration["NCC_App:ImageModeration:Region"]
+ _region = App.Configuration["NCC_App:ImageModeration:Region"]
?? "cn-shanghai";
-
+
// 优先使用ImageModeration配置,如果没有则使用AliyunOSS的配置
_accessKeyId = App.Configuration["NCC_App:ImageModeration:AccessKeyId"];
if (string.IsNullOrEmpty(_accessKeyId))
{
_accessKeyId = App.Configuration["NCC_App:AliyunOSS:AccessKeyId"];
}
-
+
_accessKeySecret = App.Configuration["NCC_App:ImageModeration:AccessKeySecret"];
if (string.IsNullOrEmpty(_accessKeySecret))
{
_accessKeySecret = App.Configuration["NCC_App:AliyunOSS:AccessKeySecret"];
}
-
+
// 判断是否为增强版(endpoint 包含 green-cip)
_isEnhancedVersion = _endpoint.Contains("green-cip");
}
@@ -80,19 +80,19 @@ namespace NCC.Extend
{
if (!_enabled)
{
- return new ModerationResult
- {
- Passed = true,
- Message = "审核未启用,直接通过"
+ return new ModerationResult
+ {
+ Passed = true,
+ Message = "审核未启用,直接通过"
};
}
if (string.IsNullOrEmpty(filePath) || !File.Exists(filePath))
{
- return new ModerationResult
- {
- Passed = false,
- Message = "文件不存在或路径为空"
+ return new ModerationResult
+ {
+ Passed = false,
+ Message = "文件不存在或路径为空"
};
}
@@ -105,9 +105,9 @@ namespace NCC.Extend
{
// 审核异常时,根据配置决定是否通过
var failOnError = App.Configuration["NCC_App:ImageModeration:FailOnError"] == "true";
- return new ModerationResult
- {
- Passed = !failOnError,
+ return new ModerationResult
+ {
+ Passed = !failOnError,
Message = $"审核异常:{ex.Message}",
RawResponse = ex.ToString()
};
@@ -123,19 +123,19 @@ namespace NCC.Extend
{
if (!_enabled)
{
- return new ModerationResult
- {
- Passed = true,
- Message = "审核未启用,直接通过"
+ return new ModerationResult
+ {
+ Passed = true,
+ Message = "审核未启用,直接通过"
};
}
if (imageBytes == null || imageBytes.Length == 0)
{
- return new ModerationResult
- {
- Passed = false,
- Message = "图片数据为空"
+ return new ModerationResult
+ {
+ Passed = false,
+ Message = "图片数据为空"
};
}
@@ -143,9 +143,9 @@ namespace NCC.Extend
if (string.IsNullOrEmpty(_accessKeyId) || string.IsNullOrEmpty(_accessKeySecret))
{
var failOnError = App.Configuration["NCC_App:ImageModeration:FailOnError"] == "true";
- return new ModerationResult
- {
- Passed = !failOnError,
+ return new ModerationResult
+ {
+ Passed = !failOnError,
Message = "图片审核服务未配置 AccessKey,无法调用审核接口",
RawResponse = "AccessKeyId 或 AccessKeySecret 未配置"
};
@@ -167,9 +167,9 @@ namespace NCC.Extend
{
// 审核异常时,根据配置决定是否通过
var failOnError = App.Configuration["NCC_App:ImageModeration:FailOnError"] == "true";
- return new ModerationResult
- {
- Passed = !failOnError,
+ return new ModerationResult
+ {
+ Passed = !failOnError,
Message = $"审核异常:{ex.Message}",
RawResponse = ex.ToString()
};
@@ -437,104 +437,104 @@ namespace NCC.Extend
{
// 构建请求URL
var url = $"{_endpoint}/green/image/scan";
-
- // 构建请求体
- var requestBody = new
+
+ // 构建请求体
+ var requestBody = new
+ {
+ scenes = new[] { "porn", "terrorism", "ad", "qrcode" },
+ tasks = new[]
{
- scenes = new[] { "porn", "terrorism", "ad", "qrcode" },
- tasks = new[]
- {
new
{
dataId = Guid.NewGuid().ToString(),
url = Convert.ToBase64String(imageBytes)
}
}
- };
+ };
- var jsonContent = JsonConvert.SerializeObject(requestBody);
- var contentBytes = Encoding.UTF8.GetBytes(jsonContent);
- var content = new ByteArrayContent(contentBytes);
- content.Headers.TryAddWithoutValidation("Content-Type", "application/json");
+ var jsonContent = JsonConvert.SerializeObject(requestBody);
+ var contentBytes = Encoding.UTF8.GetBytes(jsonContent);
+ var content = new ByteArrayContent(contentBytes);
+ content.Headers.TryAddWithoutValidation("Content-Type", "application/json");
- // 计算 Content-MD5(请求体的 MD5,然后 Base64)
- string contentMd5;
- using (var md5 = MD5.Create())
- {
- var hashBytes = md5.ComputeHash(contentBytes);
- contentMd5 = Convert.ToBase64String(hashBytes);
- }
+ // 计算 Content-MD5(请求体的 MD5,然后 Base64)
+ string contentMd5;
+ using (var md5 = MD5.Create())
+ {
+ var hashBytes = md5.ComputeHash(contentBytes);
+ contentMd5 = Convert.ToBase64String(hashBytes);
+ }
+
+ // 生成请求时间(RFC 1123 格式)
+ var dateStr = DateTime.UtcNow.ToString("r");
- // 生成请求时间(RFC 1123 格式)
- var dateStr = DateTime.UtcNow.ToString("r");
-
- // 生成签名随机数
- var signatureNonce = Guid.NewGuid().ToString();
-
- // 构建规范化的请求头(x-acs- 开头的头,按字典序排序)
- var acsHeaders = new SortedDictionary
+ // 生成签名随机数
+ var signatureNonce = Guid.NewGuid().ToString();
+
+ // 构建规范化的请求头(x-acs- 开头的头,按字典序排序)
+ var acsHeaders = new SortedDictionary
{
{ "x-acs-signature-method", "HMAC-SHA1" },
{ "x-acs-signature-nonce", signatureNonce },
{ "x-acs-signature-version", "1.0" },
{ "x-acs-version", "2018-05-09" }
};
-
- // 构建 CanonicalizedHeaders(格式:key:value\n)
- var canonicalizedHeaders = new StringBuilder();
+
+ // 构建 CanonicalizedHeaders(格式:key:value\n)
+ var canonicalizedHeaders = new StringBuilder();
+ foreach (var header in acsHeaders)
+ {
+ canonicalizedHeaders.Append($"{header.Key}:{header.Value}\n");
+ }
+
+ // 构建 CanonicalizedResource(请求路径)
+ var uri = new Uri(url);
+ var canonicalizedResource = uri.AbsolutePath;
+
+ // 构建待签名字符串
+ var stringToSign = new StringBuilder();
+ stringToSign.Append("POST\n"); // HTTP-Verb
+ stringToSign.Append("application/json\n"); // Accept
+ stringToSign.Append($"{contentMd5}\n"); // Content-MD5
+ stringToSign.Append("application/json\n"); // Content-Type
+ stringToSign.Append($"{dateStr}\n"); // Date
+ stringToSign.Append(canonicalizedHeaders.ToString()); // CanonicalizedHeaders
+ stringToSign.Append(canonicalizedResource); // CanonicalizedResource
+
+ // 计算签名(HMAC-SHA1)
+ string signature;
+ using (var hmac = new HMACSHA1(Encoding.UTF8.GetBytes(_accessKeySecret)))
+ {
+ var signatureBytes = hmac.ComputeHash(Encoding.UTF8.GetBytes(stringToSign.ToString()));
+ signature = Convert.ToBase64String(signatureBytes);
+ }
+
+ // 构建 Authorization 头
+ var authorization = $"acs {_accessKeyId}:{signature}";
+
+ // 添加 Content-MD5 到内容头(Content-MD5 是内容头,不是请求头)
+ content.Headers.TryAddWithoutValidation("Content-MD5", contentMd5);
+
+ // 创建请求并添加所有必需的请求头
+ using (var request = new HttpRequestMessage(HttpMethod.Post, url))
+ {
+ request.Content = content;
+
+ // 添加标准 HTTP 头
+ request.Headers.Add("Accept", "application/json");
+ request.Headers.Add("Date", dateStr);
+
+ // 添加阿里云协议头
foreach (var header in acsHeaders)
{
- canonicalizedHeaders.Append($"{header.Key}:{header.Value}\n");
+ request.Headers.Add(header.Key, header.Value);
}
-
- // 构建 CanonicalizedResource(请求路径)
- var uri = new Uri(url);
- var canonicalizedResource = uri.AbsolutePath;
-
- // 构建待签名字符串
- var stringToSign = new StringBuilder();
- stringToSign.Append("POST\n"); // HTTP-Verb
- stringToSign.Append("application/json\n"); // Accept
- stringToSign.Append($"{contentMd5}\n"); // Content-MD5
- stringToSign.Append("application/json\n"); // Content-Type
- stringToSign.Append($"{dateStr}\n"); // Date
- stringToSign.Append(canonicalizedHeaders.ToString()); // CanonicalizedHeaders
- stringToSign.Append(canonicalizedResource); // CanonicalizedResource
-
- // 计算签名(HMAC-SHA1)
- string signature;
- using (var hmac = new HMACSHA1(Encoding.UTF8.GetBytes(_accessKeySecret)))
- {
- var signatureBytes = hmac.ComputeHash(Encoding.UTF8.GetBytes(stringToSign.ToString()));
- signature = Convert.ToBase64String(signatureBytes);
- }
-
- // 构建 Authorization 头
- var authorization = $"acs {_accessKeyId}:{signature}";
-
- // 添加 Content-MD5 到内容头(Content-MD5 是内容头,不是请求头)
- content.Headers.TryAddWithoutValidation("Content-MD5", contentMd5);
-
- // 创建请求并添加所有必需的请求头
- using (var request = new HttpRequestMessage(HttpMethod.Post, url))
- {
- request.Content = content;
-
- // 添加标准 HTTP 头
- request.Headers.Add("Accept", "application/json");
- request.Headers.Add("Date", dateStr);
-
- // 添加阿里云协议头
- foreach (var header in acsHeaders)
- {
- request.Headers.Add(header.Key, header.Value);
- }
-
- // 添加 Authorization 头
- request.Headers.Add("Authorization", authorization);
-
- var response = await _httpClient.SendAsync(request);
- var responseContent = await response.Content.ReadAsStringAsync();
+
+ // 添加 Authorization 头
+ request.Headers.Add("Authorization", authorization);
+
+ var response = await _httpClient.SendAsync(request);
+ var responseContent = await response.Content.ReadAsStringAsync();
// 保存原始响应
var rawResponse = responseContent;
@@ -543,9 +543,9 @@ namespace NCC.Extend
{
// HTTP请求失败,根据配置决定是否通过
var failOnError = App.Configuration["NCC_App:ImageModeration:FailOnError"] == "true";
- return new ModerationResult
- {
- Passed = !failOnError,
+ return new ModerationResult
+ {
+ Passed = !failOnError,
Message = $"HTTP请求失败:{response.StatusCode}",
RawResponse = rawResponse
};
@@ -556,9 +556,9 @@ namespace NCC.Extend
if (result == null)
{
var failOnError = App.Configuration["NCC_App:ImageModeration:FailOnError"] == "true";
- return new ModerationResult
- {
- Passed = !failOnError,
+ return new ModerationResult
+ {
+ Passed = !failOnError,
Message = "响应解析失败",
RawResponse = rawResponse
};
@@ -574,9 +574,9 @@ namespace NCC.Extend
var friendlyMsg = apiCode == 596
? "内容安全服务未开通或当前账号无权限,请到阿里云控制台开通「内容安全」并确认当前 AccessKey 有权限"
: (apiMsg.Length > 0 ? apiMsg : "审核接口返回失败");
- return new ModerationResult
- {
- Passed = !failOnError,
+ return new ModerationResult
+ {
+ Passed = !failOnError,
Message = friendlyMsg,
RawResponse = rawResponse,
Details = result
@@ -588,9 +588,9 @@ namespace NCC.Extend
if (data == null || data.Count == 0)
{
var failOnError = App.Configuration["NCC_App:ImageModeration:FailOnError"] == "true";
- return new ModerationResult
- {
- Passed = !failOnError,
+ return new ModerationResult
+ {
+ Passed = !failOnError,
Message = "审核结果数据为空",
RawResponse = rawResponse,
Details = result
@@ -601,9 +601,9 @@ namespace NCC.Extend
var results = taskResult["results"] as JArray;
if (results == null || results.Count == 0)
{
- return new ModerationResult
- {
- Passed = true,
+ return new ModerationResult
+ {
+ Passed = true,
Message = "没有审核结果,默认通过",
RawResponse = rawResponse,
Details = taskResult
@@ -624,7 +624,7 @@ namespace NCC.Extend
label = item["label"]?.ToString(),
rate = item["rate"]?.ToString()
});
-
+
if (suggestion == "block")
{
blockScenes.Add(scene ?? "unknown");
@@ -633,9 +633,9 @@ namespace NCC.Extend
if (blockScenes.Count > 0)
{
- return new ModerationResult
- {
- Passed = false,
+ return new ModerationResult
+ {
+ Passed = false,
Message = $"图片审核未通过,违规场景:{string.Join(", ", blockScenes)}",
RawResponse = rawResponse,
Details = new
@@ -646,9 +646,9 @@ namespace NCC.Extend
};
}
- return new ModerationResult
- {
- Passed = true,
+ return new ModerationResult
+ {
+ Passed = true,
Message = "图片审核通过",
RawResponse = rawResponse,
Details = new
diff --git a/netcore/src/Modularity/Extend/NCC.Extend/LqAnnualSummaryService.cs b/netcore/src/Modularity/Extend/NCC.Extend/LqAnnualSummaryService.cs
index bd48802..302d0a3 100644
--- a/netcore/src/Modularity/Extend/NCC.Extend/LqAnnualSummaryService.cs
+++ b/netcore/src/Modularity/Extend/NCC.Extend/LqAnnualSummaryService.cs
@@ -872,7 +872,8 @@ namespace NCC.Extend
var allStoreGroups = currentData.Select(x => new { x.StoreId, x.StoreName, x.BusinessUnitName, x.BusinessUnitId })
.Union(lastYearData.Select(x => new { x.StoreId, x.StoreName, x.BusinessUnitName, x.BusinessUnitId }))
.GroupBy(x => x.StoreId)
- .Select(g => {
+ .Select(g =>
+ {
// 优先选择有 BusinessUnitId 的记录,如果都有或都没有,选择第一条
var preferred = g.OrderByDescending(x => !string.IsNullOrEmpty(x.BusinessUnitId))
.ThenByDescending(x => x.BusinessUnitName)
@@ -882,7 +883,7 @@ namespace NCC.Extend
.OrderBy(x => x.BusinessUnitName)
.ThenBy(x => x.StoreName)
.ToList();
-
+
var allStores = allStoreGroups;
// 批量获取门店开业时间
diff --git a/netcore/src/Modularity/Extend/NCC.Extend/LqReportService.cs b/netcore/src/Modularity/Extend/NCC.Extend/LqReportService.cs
index 24b4b1d..5b0b15f 100644
--- a/netcore/src/Modularity/Extend/NCC.Extend/LqReportService.cs
+++ b/netcore/src/Modularity/Extend/NCC.Extend/LqReportService.cs
@@ -4200,18 +4200,36 @@ namespace NCC.Extend
.OrderBy(x => x.TkCount, OrderByType.Desc)
.ToListAsync();
- // 2. 拓客人员拓客人数排名前五
- var personRanking = await baseQuery
- .GroupBy(x => x.ExpansionUserId)
- .Select(x => new
- {
- UserId = x.ExpansionUserId,
- UserName = SqlFunc.Subqueryable().Where(u => u.Id == x.ExpansionUserId).Select(u => u.RealName),
- TkCount = SqlFunc.AggregateCount(x.Id)
- })
- .OrderBy(x => x.TkCount, OrderByType.Desc)
- .Take(5)
- .ToListAsync();
+ // 2. 拓客人员拓客人数排名前五(按拓客人员ID聚合去重,同一人员只出现一次)
+ var personRankingSql = $@"
+ SELECT
+ tk.F_ExpansionUserId AS UserId,
+ COALESCE(MAX(u.F_REALNAME), '') AS UserName,
+ COUNT(tk.F_Id) AS TkCount
+ FROM lq_tkjlb tk
+ LEFT JOIN BASE_USER u ON tk.F_ExpansionUserId = u.F_Id
+ WHERE tk.F_ExpansionTime >= '{startTime:yyyy-MM-dd HH:mm:ss}'
+ AND tk.F_ExpansionTime <= '{endTime:yyyy-MM-dd HH:mm:ss}'";
+ if (!string.IsNullOrEmpty(input.EventId))
+ {
+ personRankingSql += $" AND tk.F_EventId = '{input.EventId}'";
+ }
+ if (input.StoreIds != null && input.StoreIds.Any())
+ {
+ var storeIdsStr = string.Join("','", input.StoreIds);
+ personRankingSql += $" AND tk.F_StoreId IN ('{storeIdsStr}')";
+ }
+ personRankingSql += @"
+ GROUP BY tk.F_ExpansionUserId
+ ORDER BY TkCount DESC
+ LIMIT 5";
+ var personRankingRaw = await _db.Ado.SqlQueryAsync(personRankingSql);
+ var personRanking = (personRankingRaw ?? Enumerable.Empty()).Select(x => new
+ {
+ UserId = (string)x.UserId,
+ UserName = (string)(x.UserName ?? ""),
+ TkCount = Convert.ToInt32(x.TkCount ?? 0)
+ }).ToList();
// 3. 活动列表(用于筛选)
var eventList = await _db.Queryable()
diff --git a/netcore/src/Modularity/Extend/NCC.Extend/LqStatisticsService.cs b/netcore/src/Modularity/Extend/NCC.Extend/LqStatisticsService.cs
index 34e5b72..b7e6a72 100644
--- a/netcore/src/Modularity/Extend/NCC.Extend/LqStatisticsService.cs
+++ b/netcore/src/Modularity/Extend/NCC.Extend/LqStatisticsService.cs
@@ -858,8 +858,8 @@ namespace NCC.Extend.LqStatistics
$"📊 今日日报已生成,点击链接查看\n\n"
+ $"本月全门店目标业绩:{monthlyStats.TargetPerformance:N0}元\n"
+ $"本月已完成业绩:{monthlyStats.ActualPerformance:N0}元\n"
- + $"完成率:{monthlyStats.CompletionRate:F2}%\n\n"
- + $"https://erp.lvqianmeiye.com/html/dailyReportnew.html";
+ + $"完成率:{monthlyStats.CompletionRate:F2}%";
+ // + $"https://erp.lvqianmeiye.com/html/dailyReportnew.html";
var result = await _weChatBotService.SendTextMessage(messageContent);
return new
diff --git a/netcore/src/Modularity/Extend/NCC.Extend/LqStoreDashboardService.cs b/netcore/src/Modularity/Extend/NCC.Extend/LqStoreDashboardService.cs
index 3c04cbe..a7a8ee2 100644
--- a/netcore/src/Modularity/Extend/NCC.Extend/LqStoreDashboardService.cs
+++ b/netcore/src/Modularity/Extend/NCC.Extend/LqStoreDashboardService.cs
@@ -61,6 +61,7 @@ namespace NCC.Extend
/// - ConsumePerformance: 消耗业绩(当月消耗金额汇总)
/// - CompletionRate: 完成率(消耗业绩/目标业绩 × 100%)
/// - NetPerformance: 净业绩(开单业绩 - 退卡金额)
+ /// - LifeBeautyPerformance/MedicalBeautyPerformance/TechBeautyPerformance: 生美/医美/科美实收业绩(开单实收 - 退款,按品项分类统计)
///
/// 查询参数
/// 门店驾驶舱统计数据
@@ -177,26 +178,27 @@ namespace NCC.Extend
// 9. 计算净业绩(开单业绩 - 退卡金额)
var netPerformance = billingAmount - refundAmount;
- // 10. 获取人头数(去重后的消费会员数)
- var headCount = await _db.Queryable()
- .Where(x => x.Md == input.StoreId && x.IsEffective == 1)
- .Where(x => x.Hksj.HasValue && x.Hksj.Value >= startDate && x.Hksj.Value <= endDateTime)
- .Select(x => x.Hy)
- .Distinct()
- .CountAsync();
-
- // 11. 获取人次(日度去重客户数)- 使用SQL查询
- var personCountSql = $@"
- SELECT COUNT(DISTINCT CONCAT(xh.Hy, '-', DATE_FORMAT(xh.Hksj, '%Y-%m-%d'))) as PersonCount
+ // 10、11. 人头数与人次用同一套条件、同一条 SQL 统计,避免口径不一致导致人头>人次的异常
+ // - 人头数:去重后的消费会员数(排除 Hy 为空/NULL,否则 MySQL COUNT(DISTINCT Hy) 会把 NULL 算成 1,而人次 CONCAT 不统计 NULL,导致人头>人次)
+ // - 人次:日度去重(同一会员同一天多笔耗卡算 1 人次),理论上人次 >= 人头
+ var headPersonSql = $@"
+ SELECT
+ COUNT(DISTINCT CASE WHEN xh.Hy IS NOT NULL AND TRIM(IFNULL(xh.Hy, '')) != '' THEN xh.Hy END) as HeadCount,
+ 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
FROM lq_xh_hyhk xh
WHERE xh.Md = '{input.StoreId}'
AND xh.F_IsEffective = 1
+ AND xh.Hksj IS NOT NULL
AND xh.Hksj >= '{startDate:yyyy-MM-dd HH:mm:ss}'
AND xh.Hksj <= '{endDateTime:yyyy-MM-dd HH:mm:ss}'";
- var personCountResult = await _db.Ado.SqlQueryAsync(personCountSql);
- var personCount = personCountResult?.FirstOrDefault() != null
- ? Convert.ToInt32(personCountResult.FirstOrDefault().PersonCount ?? 0)
- : 0;
+ var headPersonResult = await _db.Ado.SqlQuerySingleAsync(headPersonSql);
+ var headCount = headPersonResult != null ? Convert.ToInt32(headPersonResult.HeadCount ?? 0) : 0;
+ var personCount = headPersonResult != null ? Convert.ToInt32(headPersonResult.PersonCount ?? 0) : 0;
+ // 兜底:按定义人次不应小于人头,若因边界情况出现则取较大值
+ if (personCount < headCount)
+ {
+ personCount = headCount;
+ }
// 12. 获取项目数(消耗的项目总数,从品项明细表统计原始项目数)
var projectCountSql = $@"
@@ -222,24 +224,50 @@ namespace NCC.Extend
// 15. 计算人均项目数(项目数/人头数)
var avgProjectPerHead = headCount > 0 ? projectCount / (decimal)headCount : 0m;
- // 16. 计算各分类消耗业绩(生美、医美、科美)
- var categoryPerformanceSql = $@"
+ // 16. 计算各分类实收业绩(生美、医美、科美)= 开单健康师业绩(按品项分类)- 退卡健康师业绩(按品项分类)
+ // 口径与开单业绩一致:开单业绩来自开单表 sfyj,健康师业绩表 jksyj 按开单汇总即等于 sfyj,故生美+医美+科美合计与开单业绩一致
+ // 16.1 开单实收:从开单健康师业绩表 lq_kd_jksyj 按 F_ItemCategory 汇总 jksyj(与开单表 kdrq 同批开单)
+ var categoryBillingSql = $@"
SELECT
- 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,
- 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,
- 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
- FROM lq_xh_pxmx xhpx
- INNER JOIN lq_xh_hyhk xh ON xhpx.F_ConsumeInfoId = xh.F_Id AND xh.F_IsEffective = 1
- LEFT JOIN lq_xmzl xmzl ON xhpx.px = xmzl.F_Id AND xmzl.F_IsEffective = 1
- WHERE xhpx.F_IsEffective = 1
- AND xh.md = '{input.StoreId}'
- AND xh.hksj >= '{startDate:yyyy-MM-dd 00:00:00}'
- AND xh.hksj <= '{endDateTime:yyyy-MM-dd HH:mm:ss}'";
- var categoryPerformanceResult = await _db.Ado.SqlQueryAsync(categoryPerformanceSql);
- var categoryPerformance = categoryPerformanceResult?.FirstOrDefault();
- var lifeBeautyPerformance = categoryPerformance != null ? Convert.ToDecimal(categoryPerformance.LifeBeautyPerformance ?? 0) : 0m;
- var medicalBeautyPerformance = categoryPerformance != null ? Convert.ToDecimal(categoryPerformance.MedicalBeautyPerformance ?? 0) : 0m;
- var techBeautyPerformance = categoryPerformance != null ? Convert.ToDecimal(categoryPerformance.TechBeautyPerformance ?? 0) : 0m;
+ COALESCE(SUM(CASE WHEN COALESCE(jksyj.F_ItemCategory, '') = '生美' THEN CAST(jksyj.jksyj AS DECIMAL(18,2)) ELSE 0 END), 0) as LifeBeautyBilling,
+ COALESCE(SUM(CASE WHEN COALESCE(jksyj.F_ItemCategory, '') = '医美' THEN CAST(jksyj.jksyj AS DECIMAL(18,2)) ELSE 0 END), 0) as MedicalBeautyBilling,
+ COALESCE(SUM(CASE WHEN COALESCE(jksyj.F_ItemCategory, '') = '科美' THEN CAST(jksyj.jksyj AS DECIMAL(18,2)) ELSE 0 END), 0) as TechBeautyBilling
+ FROM lq_kd_jksyj jksyj
+ INNER JOIN lq_kd_kdjlb billing ON jksyj.glkdbh = billing.F_Id
+ WHERE jksyj.F_IsEffective = 1
+ AND billing.F_IsEffective = 1
+ AND billing.djmd = '{input.StoreId}'
+ AND billing.kdrq >= '{startDate:yyyy-MM-dd HH:mm:ss}'
+ AND billing.kdrq <= '{endDateTime:yyyy-MM-dd HH:mm:ss}'";
+ var categoryBillingResult = await _db.Ado.SqlQueryAsync(categoryBillingSql);
+ var categoryBilling = categoryBillingResult?.FirstOrDefault();
+ var lifeBeautyBilling = categoryBilling != null ? Convert.ToDecimal(categoryBilling.LifeBeautyBilling ?? 0) : 0m;
+ var medicalBeautyBilling = categoryBilling != null ? Convert.ToDecimal(categoryBilling.MedicalBeautyBilling ?? 0) : 0m;
+ var techBeautyBilling = categoryBilling != null ? Convert.ToDecimal(categoryBilling.TechBeautyBilling ?? 0) : 0m;
+
+ // 16.2 退款金额:从退卡健康师业绩表 lq_hytk_jksyj 按 F_ItemCategory 汇总 jksyj
+ var categoryRefundSql = $@"
+ SELECT
+ COALESCE(SUM(CASE WHEN COALESCE(jksyj.F_ItemCategory, '') = '生美' THEN CAST(jksyj.jksyj AS DECIMAL(18,2)) ELSE 0 END), 0) as LifeBeautyRefund,
+ COALESCE(SUM(CASE WHEN COALESCE(jksyj.F_ItemCategory, '') = '医美' THEN CAST(jksyj.jksyj AS DECIMAL(18,2)) ELSE 0 END), 0) as MedicalBeautyRefund,
+ COALESCE(SUM(CASE WHEN COALESCE(jksyj.F_ItemCategory, '') = '科美' THEN CAST(jksyj.jksyj AS DECIMAL(18,2)) ELSE 0 END), 0) as TechBeautyRefund
+ FROM lq_hytk_jksyj jksyj
+ INNER JOIN lq_hytk_hytk refund ON jksyj.gltkbh = refund.F_Id
+ WHERE jksyj.F_IsEffective = 1
+ AND refund.F_IsEffective = 1
+ AND refund.md = '{input.StoreId}'
+ AND refund.tksj >= '{startDate:yyyy-MM-dd HH:mm:ss}'
+ AND refund.tksj <= '{endDateTime:yyyy-MM-dd HH:mm:ss}'";
+ var categoryRefundResult = await _db.Ado.SqlQueryAsync(categoryRefundSql);
+ var categoryRefund = categoryRefundResult?.FirstOrDefault();
+ var lifeBeautyRefund = categoryRefund != null ? Convert.ToDecimal(categoryRefund.LifeBeautyRefund ?? 0) : 0m;
+ var medicalBeautyRefund = categoryRefund != null ? Convert.ToDecimal(categoryRefund.MedicalBeautyRefund ?? 0) : 0m;
+ var techBeautyRefund = categoryRefund != null ? Convert.ToDecimal(categoryRefund.TechBeautyRefund ?? 0) : 0m;
+
+ // 16.3 实收业绩 = 开单健康师业绩 - 退卡健康师业绩
+ var lifeBeautyPerformance = lifeBeautyBilling - lifeBeautyRefund;
+ var medicalBeautyPerformance = medicalBeautyBilling - medicalBeautyRefund;
+ var techBeautyPerformance = techBeautyBilling - techBeautyRefund;
// 17. 获取储扣统计(储扣金额、次数、渗透率等)
var deductSql = $@"