From 5652f5e02814a1d81e4e28ed886820d0946df552 Mon Sep 17 00:00:00 2001 From: “wangming” <“wangming@antissoft.com”> Date: Thu, 5 Feb 2026 10:51:47 +0800 Subject: [PATCH] refactor: update documentation and improve image moderation service --- docs/generate_saas_excel.py | 182 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ docs/美业SaaS系统功能规划.md | 38 +++++++++++++++++++------------------- docs/美业SaaS系统功能规划.xlsx | Bin 0 -> 17522 bytes docs/美业系统升级项目需求拆分表.xlsx | Bin 17889 -> 0 bytes netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqStoreDashboard/StoreDashboardStatisticsOutput.cs | 6 +++--- netcore/src/Modularity/Extend/NCC.Extend/ImageModerationService.cs | 262 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------------------------------------------------------------------------------------------------------------------- netcore/src/Modularity/Extend/NCC.Extend/LqAnnualSummaryService.cs | 5 +++-- netcore/src/Modularity/Extend/NCC.Extend/LqReportService.cs | 42 ++++++++++++++++++++++++++++++------------ netcore/src/Modularity/Extend/NCC.Extend/LqStatisticsService.cs | 4 ++-- netcore/src/Modularity/Extend/NCC.Extend/LqStoreDashboardService.cs | 92 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------------------- 10 files changed, 430 insertions(+), 201 deletions(-) create mode 100644 docs/generate_saas_excel.py create mode 100644 docs/美业SaaS系统功能规划.xlsx 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 Binary files /dev/null and b/docs/美业SaaS系统功能规划.xlsx differ diff --git a/docs/美业系统升级项目需求拆分表.xlsx b/docs/美业系统升级项目需求拆分表.xlsx index f2928e3..f1ea248 100644 Binary files a/docs/美业系统升级项目需求拆分表.xlsx and b/docs/美业系统升级项目需求拆分表.xlsx differ 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 = $@" -- libgit2 0.21.4