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