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,16 +20,16 @@
20 - [1.9 异常与纠纷处理系统](#19-异常与纠纷处理系统) ⭐ **美业真实痛点** 20 - [1.9 异常与纠纷处理系统](#19-异常与纠纷处理系统) ⭐ **美业真实痛点**
21 21
22 ### 二、核心业务模块 22 ### 二、核心业务模块
23 -- [2.1 门店管理系统](#21-门店管理系统) 23 +- [2.1 门店管理系统](#21-门店管理系统)(从现有系统拆分)
24 - [2.1.3 合作医院管理](#213-合作医院管理) 24 - [2.1.3 合作医院管理](#213-合作医院管理)
25 -- [2.2 人员管理系统](#22-人员管理系统) 25 +- [2.2 人员管理系统](#22-人员管理系统)(从现有系统拆分)
26 - [2.2.3 考勤管理系统](#223-考勤管理系统) 26 - [2.2.3 考勤管理系统](#223-考勤管理系统)
27 - [2.2.4 员工技能与项目能力模型](#224-员工技能与项目能力模型) 27 - [2.2.4 员工技能与项目能力模型](#224-员工技能与项目能力模型)
28 -- [2.3 客户管理系统](#23-客户管理系统)  
29 -- [2.4 开单耗卡系统](#24-开单耗卡系统) 28 +- [2.3 客户管理系统](#23-客户管理系统)(从现有系统拆分)
  29 +- [2.4 开单耗卡系统](#24-开单耗卡系统)(从现有系统拆分)
30 - [2.4.1 套餐拆分与组合开单](#241-套餐拆分与组合开单) 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 - [3.1 拓客与线索管理](#31-拓客与线索管理) ⭐ **把人弄进来** 35 - [3.1 拓客与线索管理](#31-拓客与线索管理) ⭐ **把人弄进来**
@@ -39,41 +39,41 @@ @@ -39,41 +39,41 @@
39 - [3.5 客户生命周期管理(CLM)](#35-客户生命周期管理clm) 39 - [3.5 客户生命周期管理(CLM)](#35-客户生命周期管理clm)
40 40
41 ### 四、预约与服务模块 41 ### 四、预约与服务模块
42 -- [4.1 预约管理系统](#41-预约管理系统) 42 +- [4.1 预约管理系统](#41-预约管理系统)(从现有系统拆分)
43 - [4.2 服务项目管理](#42-服务项目管理) 43 - [4.2 服务项目管理](#42-服务项目管理)
44 - - [4.2.3 服务过程记录(轻量版)](#423-服务过程记录轻量版) 44 + - [4.2.3 服务过程记录(轻量版)](#423-服务过程记录轻量版)(从现有系统拆分)
45 - [4.3 健康师排班管理](#43-健康师排班管理) 45 - [4.3 健康师排班管理](#43-健康师排班管理)
46 - [4.4 服务评价系统](#44-服务评价系统) 46 - [4.4 服务评价系统](#44-服务评价系统)
47 - [4.5 爽约/取消/超时管理](#45-爽约取消超时管理) 47 - [4.5 爽约/取消/超时管理](#45-爽约取消超时管理)
48 48
49 ### 五、支付与财务模块 49 ### 五、支付与财务模块
50 - [5.1 支付管理系统](#51-支付管理系统) 50 - [5.1 支付管理系统](#51-支付管理系统)
51 -- [5.2 财务管理系统](#52-财务管理系统) 51 +- [5.2 财务管理系统](#52-财务管理系统)(从现有系统拆分)
52 - [5.3 对账管理系统](#53-对账管理系统) 52 - [5.3 对账管理系统](#53-对账管理系统)
53 - [5.4 发票管理系统](#54-发票管理系统) 53 - [5.4 发票管理系统](#54-发票管理系统)
54 - [5.5 分账与结算规则](#55-分账与结算规则) 54 - [5.5 分账与结算规则](#55-分账与结算规则)
55 - [5.6 财务风控](#56-财务风控) 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 - [6.2 采购管理系统](#62-采购管理系统) 60 - [6.2 采购管理系统](#62-采购管理系统)
61 - [6.3 供应商管理系统](#63-供应商管理系统) 61 - [6.3 供应商管理系统](#63-供应商管理系统)
62 - [6.4 设备管理系统](#64-设备管理系统) 62 - [6.4 设备管理系统](#64-设备管理系统)
63 - [6.5 耗材自动关联项目](#65-耗材自动关联项目) 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 - [7.5 自定义指标与看板](#75-自定义指标与看板) 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 - [8.4 客户端小程序](#84-客户端小程序) 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,17 +96,17 @@ namespace NCC.Extend.Entitys.Dto.LqStoreDashboard
96 public decimal AvgProjectPerHead { get; set; } 96 public decimal AvgProjectPerHead { get; set; }
97 97
98 /// <summary> 98 /// <summary>
99 - /// 生美业绩(消耗业绩 99 + /// 生美业绩(实收业绩 = 开单实收 - 退款
100 /// </summary> 100 /// </summary>
101 public decimal LifeBeautyPerformance { get; set; } 101 public decimal LifeBeautyPerformance { get; set; }
102 102
103 /// <summary> 103 /// <summary>
104 - /// 医美业绩(消耗业绩 104 + /// 医美业绩(实收业绩 = 开单实收 - 退款
105 /// </summary> 105 /// </summary>
106 public decimal MedicalBeautyPerformance { get; set; } 106 public decimal MedicalBeautyPerformance { get; set; }
107 107
108 /// <summary> 108 /// <summary>
109 - /// 科美业绩(消耗业绩 109 + /// 科美业绩(实收业绩 = 开单实收 - 退款
110 /// </summary> 110 /// </summary>
111 public decimal TechBeautyPerformance { get; set; } 111 public decimal TechBeautyPerformance { get; set; }
112 112
netcore/src/Modularity/Extend/NCC.Extend/ImageModerationService.cs
@@ -38,24 +38,24 @@ namespace NCC.Extend @@ -38,24 +38,24 @@ namespace NCC.Extend
38 { 38 {
39 _httpClient = new HttpClient(); 39 _httpClient = new HttpClient();
40 _enabled = App.Configuration["NCC_App:ImageModeration:Enabled"] == "true"; 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 ?? "https://green.cn-shanghai.aliyuncs.com"; 42 ?? "https://green.cn-shanghai.aliyuncs.com";
43 - _region = App.Configuration["NCC_App:ImageModeration:Region"] 43 + _region = App.Configuration["NCC_App:ImageModeration:Region"]
44 ?? "cn-shanghai"; 44 ?? "cn-shanghai";
45 - 45 +
46 // 优先使用ImageModeration配置,如果没有则使用AliyunOSS的配置 46 // 优先使用ImageModeration配置,如果没有则使用AliyunOSS的配置
47 _accessKeyId = App.Configuration["NCC_App:ImageModeration:AccessKeyId"]; 47 _accessKeyId = App.Configuration["NCC_App:ImageModeration:AccessKeyId"];
48 if (string.IsNullOrEmpty(_accessKeyId)) 48 if (string.IsNullOrEmpty(_accessKeyId))
49 { 49 {
50 _accessKeyId = App.Configuration["NCC_App:AliyunOSS:AccessKeyId"]; 50 _accessKeyId = App.Configuration["NCC_App:AliyunOSS:AccessKeyId"];
51 } 51 }
52 - 52 +
53 _accessKeySecret = App.Configuration["NCC_App:ImageModeration:AccessKeySecret"]; 53 _accessKeySecret = App.Configuration["NCC_App:ImageModeration:AccessKeySecret"];
54 if (string.IsNullOrEmpty(_accessKeySecret)) 54 if (string.IsNullOrEmpty(_accessKeySecret))
55 { 55 {
56 _accessKeySecret = App.Configuration["NCC_App:AliyunOSS:AccessKeySecret"]; 56 _accessKeySecret = App.Configuration["NCC_App:AliyunOSS:AccessKeySecret"];
57 } 57 }
58 - 58 +
59 // 判断是否为增强版(endpoint 包含 green-cip) 59 // 判断是否为增强版(endpoint 包含 green-cip)
60 _isEnhancedVersion = _endpoint.Contains("green-cip"); 60 _isEnhancedVersion = _endpoint.Contains("green-cip");
61 } 61 }
@@ -80,19 +80,19 @@ namespace NCC.Extend @@ -80,19 +80,19 @@ namespace NCC.Extend
80 { 80 {
81 if (!_enabled) 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 if (string.IsNullOrEmpty(filePath) || !File.Exists(filePath)) 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,9 +105,9 @@ namespace NCC.Extend
105 { 105 {
106 // 审核异常时,根据配置决定是否通过 106 // 审核异常时,根据配置决定是否通过
107 var failOnError = App.Configuration["NCC_App:ImageModeration:FailOnError"] == "true"; 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 Message = $"审核异常:{ex.Message}", 111 Message = $"审核异常:{ex.Message}",
112 RawResponse = ex.ToString() 112 RawResponse = ex.ToString()
113 }; 113 };
@@ -123,19 +123,19 @@ namespace NCC.Extend @@ -123,19 +123,19 @@ namespace NCC.Extend
123 { 123 {
124 if (!_enabled) 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 if (imageBytes == null || imageBytes.Length == 0) 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,9 +143,9 @@ namespace NCC.Extend
143 if (string.IsNullOrEmpty(_accessKeyId) || string.IsNullOrEmpty(_accessKeySecret)) 143 if (string.IsNullOrEmpty(_accessKeyId) || string.IsNullOrEmpty(_accessKeySecret))
144 { 144 {
145 var failOnError = App.Configuration["NCC_App:ImageModeration:FailOnError"] == "true"; 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 Message = "图片审核服务未配置 AccessKey,无法调用审核接口", 149 Message = "图片审核服务未配置 AccessKey,无法调用审核接口",
150 RawResponse = "AccessKeyId 或 AccessKeySecret 未配置" 150 RawResponse = "AccessKeyId 或 AccessKeySecret 未配置"
151 }; 151 };
@@ -167,9 +167,9 @@ namespace NCC.Extend @@ -167,9 +167,9 @@ namespace NCC.Extend
167 { 167 {
168 // 审核异常时,根据配置决定是否通过 168 // 审核异常时,根据配置决定是否通过
169 var failOnError = App.Configuration["NCC_App:ImageModeration:FailOnError"] == "true"; 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 Message = $"审核异常:{ex.Message}", 173 Message = $"审核异常:{ex.Message}",
174 RawResponse = ex.ToString() 174 RawResponse = ex.ToString()
175 }; 175 };
@@ -437,104 +437,104 @@ namespace NCC.Extend @@ -437,104 +437,104 @@ namespace NCC.Extend
437 { 437 {
438 // 构建请求URL 438 // 构建请求URL
439 var url = $"{_endpoint}/green/image/scan"; 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 new 447 new
448 { 448 {
449 dataId = Guid.NewGuid().ToString(), 449 dataId = Guid.NewGuid().ToString(),
450 url = Convert.ToBase64String(imageBytes) 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 { "x-acs-signature-method", "HMAC-SHA1" }, 477 { "x-acs-signature-method", "HMAC-SHA1" },
478 { "x-acs-signature-nonce", signatureNonce }, 478 { "x-acs-signature-nonce", signatureNonce },
479 { "x-acs-signature-version", "1.0" }, 479 { "x-acs-signature-version", "1.0" },
480 { "x-acs-version", "2018-05-09" } 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 foreach (var header in acsHeaders) 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 var rawResponse = responseContent; 540 var rawResponse = responseContent;
@@ -543,9 +543,9 @@ namespace NCC.Extend @@ -543,9 +543,9 @@ namespace NCC.Extend
543 { 543 {
544 // HTTP请求失败,根据配置决定是否通过 544 // HTTP请求失败,根据配置决定是否通过
545 var failOnError = App.Configuration["NCC_App:ImageModeration:FailOnError"] == "true"; 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 Message = $"HTTP请求失败:{response.StatusCode}", 549 Message = $"HTTP请求失败:{response.StatusCode}",
550 RawResponse = rawResponse 550 RawResponse = rawResponse
551 }; 551 };
@@ -556,9 +556,9 @@ namespace NCC.Extend @@ -556,9 +556,9 @@ namespace NCC.Extend
556 if (result == null) 556 if (result == null)
557 { 557 {
558 var failOnError = App.Configuration["NCC_App:ImageModeration:FailOnError"] == "true"; 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 Message = "响应解析失败", 562 Message = "响应解析失败",
563 RawResponse = rawResponse 563 RawResponse = rawResponse
564 }; 564 };
@@ -574,9 +574,9 @@ namespace NCC.Extend @@ -574,9 +574,9 @@ namespace NCC.Extend
574 var friendlyMsg = apiCode == 596 574 var friendlyMsg = apiCode == 596
575 ? "内容安全服务未开通或当前账号无权限,请到阿里云控制台开通「内容安全」并确认当前 AccessKey 有权限" 575 ? "内容安全服务未开通或当前账号无权限,请到阿里云控制台开通「内容安全」并确认当前 AccessKey 有权限"
576 : (apiMsg.Length > 0 ? apiMsg : "审核接口返回失败"); 576 : (apiMsg.Length > 0 ? apiMsg : "审核接口返回失败");
577 - return new ModerationResult  
578 - {  
579 - Passed = !failOnError, 577 + return new ModerationResult
  578 + {
  579 + Passed = !failOnError,
580 Message = friendlyMsg, 580 Message = friendlyMsg,
581 RawResponse = rawResponse, 581 RawResponse = rawResponse,
582 Details = result 582 Details = result
@@ -588,9 +588,9 @@ namespace NCC.Extend @@ -588,9 +588,9 @@ namespace NCC.Extend
588 if (data == null || data.Count == 0) 588 if (data == null || data.Count == 0)
589 { 589 {
590 var failOnError = App.Configuration["NCC_App:ImageModeration:FailOnError"] == "true"; 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 Message = "审核结果数据为空", 594 Message = "审核结果数据为空",
595 RawResponse = rawResponse, 595 RawResponse = rawResponse,
596 Details = result 596 Details = result
@@ -601,9 +601,9 @@ namespace NCC.Extend @@ -601,9 +601,9 @@ namespace NCC.Extend
601 var results = taskResult["results"] as JArray; 601 var results = taskResult["results"] as JArray;
602 if (results == null || results.Count == 0) 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 Message = "没有审核结果,默认通过", 607 Message = "没有审核结果,默认通过",
608 RawResponse = rawResponse, 608 RawResponse = rawResponse,
609 Details = taskResult 609 Details = taskResult
@@ -624,7 +624,7 @@ namespace NCC.Extend @@ -624,7 +624,7 @@ namespace NCC.Extend
624 label = item["label"]?.ToString(), 624 label = item["label"]?.ToString(),
625 rate = item["rate"]?.ToString() 625 rate = item["rate"]?.ToString()
626 }); 626 });
627 - 627 +
628 if (suggestion == "block") 628 if (suggestion == "block")
629 { 629 {
630 blockScenes.Add(scene ?? "unknown"); 630 blockScenes.Add(scene ?? "unknown");
@@ -633,9 +633,9 @@ namespace NCC.Extend @@ -633,9 +633,9 @@ namespace NCC.Extend
633 633
634 if (blockScenes.Count > 0) 634 if (blockScenes.Count > 0)
635 { 635 {
636 - return new ModerationResult  
637 - {  
638 - Passed = false, 636 + return new ModerationResult
  637 + {
  638 + Passed = false,
639 Message = $"图片审核未通过,违规场景:{string.Join(", ", blockScenes)}", 639 Message = $"图片审核未通过,违规场景:{string.Join(", ", blockScenes)}",
640 RawResponse = rawResponse, 640 RawResponse = rawResponse,
641 Details = new 641 Details = new
@@ -646,9 +646,9 @@ namespace NCC.Extend @@ -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 Message = "图片审核通过", 652 Message = "图片审核通过",
653 RawResponse = rawResponse, 653 RawResponse = rawResponse,
654 Details = new 654 Details = new
netcore/src/Modularity/Extend/NCC.Extend/LqAnnualSummaryService.cs
@@ -872,7 +872,8 @@ namespace NCC.Extend @@ -872,7 +872,8 @@ namespace NCC.Extend
872 var allStoreGroups = currentData.Select(x => new { x.StoreId, x.StoreName, x.BusinessUnitName, x.BusinessUnitId }) 872 var allStoreGroups = currentData.Select(x => new { x.StoreId, x.StoreName, x.BusinessUnitName, x.BusinessUnitId })
873 .Union(lastYearData.Select(x => new { x.StoreId, x.StoreName, x.BusinessUnitName, x.BusinessUnitId })) 873 .Union(lastYearData.Select(x => new { x.StoreId, x.StoreName, x.BusinessUnitName, x.BusinessUnitId }))
874 .GroupBy(x => x.StoreId) 874 .GroupBy(x => x.StoreId)
875 - .Select(g => { 875 + .Select(g =>
  876 + {
876 // 优先选择有 BusinessUnitId 的记录,如果都有或都没有,选择第一条 877 // 优先选择有 BusinessUnitId 的记录,如果都有或都没有,选择第一条
877 var preferred = g.OrderByDescending(x => !string.IsNullOrEmpty(x.BusinessUnitId)) 878 var preferred = g.OrderByDescending(x => !string.IsNullOrEmpty(x.BusinessUnitId))
878 .ThenByDescending(x => x.BusinessUnitName) 879 .ThenByDescending(x => x.BusinessUnitName)
@@ -882,7 +883,7 @@ namespace NCC.Extend @@ -882,7 +883,7 @@ namespace NCC.Extend
882 .OrderBy(x => x.BusinessUnitName) 883 .OrderBy(x => x.BusinessUnitName)
883 .ThenBy(x => x.StoreName) 884 .ThenBy(x => x.StoreName)
884 .ToList(); 885 .ToList();
885 - 886 +
886 var allStores = allStoreGroups; 887 var allStores = allStoreGroups;
887 888
888 // 批量获取门店开业时间 889 // 批量获取门店开业时间
netcore/src/Modularity/Extend/NCC.Extend/LqReportService.cs
@@ -4200,18 +4200,36 @@ namespace NCC.Extend @@ -4200,18 +4200,36 @@ namespace NCC.Extend
4200 .OrderBy(x => x.TkCount, OrderByType.Desc) 4200 .OrderBy(x => x.TkCount, OrderByType.Desc)
4201 .ToListAsync(); 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 // 3. 活动列表(用于筛选) 4234 // 3. 活动列表(用于筛选)
4217 var eventList = await _db.Queryable<LqTkjlbEntity>() 4235 var eventList = await _db.Queryable<LqTkjlbEntity>()
netcore/src/Modularity/Extend/NCC.Extend/LqStatisticsService.cs
@@ -858,8 +858,8 @@ namespace NCC.Extend.LqStatistics @@ -858,8 +858,8 @@ namespace NCC.Extend.LqStatistics
858 $"📊 今日日报已生成,点击链接查看\n\n" 858 $"📊 今日日报已生成,点击链接查看\n\n"
859 + $"本月全门店目标业绩:{monthlyStats.TargetPerformance:N0}元\n" 859 + $"本月全门店目标业绩:{monthlyStats.TargetPerformance:N0}元\n"
860 + $"本月已完成业绩:{monthlyStats.ActualPerformance:N0}元\n" 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 var result = await _weChatBotService.SendTextMessage(messageContent); 864 var result = await _weChatBotService.SendTextMessage(messageContent);
865 return new 865 return new
netcore/src/Modularity/Extend/NCC.Extend/LqStoreDashboardService.cs
@@ -61,6 +61,7 @@ namespace NCC.Extend @@ -61,6 +61,7 @@ namespace NCC.Extend
61 /// - ConsumePerformance: 消耗业绩(当月消耗金额汇总) 61 /// - ConsumePerformance: 消耗业绩(当月消耗金额汇总)
62 /// - CompletionRate: 完成率(消耗业绩/目标业绩 × 100%) 62 /// - CompletionRate: 完成率(消耗业绩/目标业绩 × 100%)
63 /// - NetPerformance: 净业绩(开单业绩 - 退卡金额) 63 /// - NetPerformance: 净业绩(开单业绩 - 退卡金额)
  64 + /// - LifeBeautyPerformance/MedicalBeautyPerformance/TechBeautyPerformance: 生美/医美/科美实收业绩(开单实收 - 退款,按品项分类统计)
64 /// </remarks> 65 /// </remarks>
65 /// <param name="input">查询参数</param> 66 /// <param name="input">查询参数</param>
66 /// <returns>门店驾驶舱统计数据</returns> 67 /// <returns>门店驾驶舱统计数据</returns>
@@ -177,26 +178,27 @@ namespace NCC.Extend @@ -177,26 +178,27 @@ namespace NCC.Extend
177 // 9. 计算净业绩(开单业绩 - 退卡金额) 178 // 9. 计算净业绩(开单业绩 - 退卡金额)
178 var netPerformance = billingAmount - refundAmount; 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 FROM lq_xh_hyhk xh 188 FROM lq_xh_hyhk xh
192 WHERE xh.Md = '{input.StoreId}' 189 WHERE xh.Md = '{input.StoreId}'
193 AND xh.F_IsEffective = 1 190 AND xh.F_IsEffective = 1
  191 + AND xh.Hksj IS NOT NULL
194 AND xh.Hksj >= '{startDate:yyyy-MM-dd HH:mm:ss}' 192 AND xh.Hksj >= '{startDate:yyyy-MM-dd HH:mm:ss}'
195 AND xh.Hksj <= '{endDateTime:yyyy-MM-dd HH:mm:ss}'"; 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 // 12. 获取项目数(消耗的项目总数,从品项明细表统计原始项目数) 203 // 12. 获取项目数(消耗的项目总数,从品项明细表统计原始项目数)
202 var projectCountSql = $@" 204 var projectCountSql = $@"
@@ -222,24 +224,50 @@ namespace NCC.Extend @@ -222,24 +224,50 @@ namespace NCC.Extend
222 // 15. 计算人均项目数(项目数/人头数) 224 // 15. 计算人均项目数(项目数/人头数)
223 var avgProjectPerHead = headCount > 0 ? projectCount / (decimal)headCount : 0m; 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 SELECT 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 // 17. 获取储扣统计(储扣金额、次数、渗透率等) 272 // 17. 获取储扣统计(储扣金额、次数、渗透率等)
245 var deductSql = $@" 273 var deductSql = $@"