Commit a7da69e393fc8e461e92fcb23b68e932051542c5

Authored by 李宇
2 parents 7db17e74 763801a5

Merge branch 'master' of http://39.98.150.180/antissoft/lvqianmeiye_ERP

Showing 27 changed files with 3886 additions and 682 deletions
README.md
1   -# 绿纤ERP管理系统
2   -
3   -## 项目简介
  1 +# 绿纤美业ERP管理系统
  2 +
  3 +## 📋 项目简介
  4 +
  5 +绿纤美业ERP管理系统是一个基于现代化技术栈开发的企业资源规划系统,专为绿纤美业行业量身定制。系统采用前后端分离架构,提供完整的门店管理、业绩统计、工资核算、报表分析、客户管理等业务管理功能。
  6 +
  7 +**项目状态**:✅ 生产环境运行中 | 📅 最后更新:2025年1月
  8 +
  9 +## 🎯 核心功能模块
  10 +
  11 +### 📊 业绩统计系统
  12 +- **个人业绩统计** - 健康师个人业绩统计,包含首单业绩、升单业绩等
  13 +- **门店总业绩统计** - 门店整体业绩统计,包含总业绩、欠款金额等
  14 +- **金三角业绩统计** - 金三角团队业绩统计和分析
  15 +- **部门消耗业绩统计** - 部门消耗业绩统计,包含人头数、人次等
  16 +- **科技部业绩统计** - 科技部老师业绩统计
  17 +- **门店消耗业绩统计** - 门店消耗业绩统计
  18 +
  19 +### 💰 工资核算系统
  20 +- **健康师工资核算** - 健康师底薪、提成、奖励等自动计算
  21 +- **店长工资核算** - 店长工资计算,包含底薪、提成、奖励等
  22 +- **主任工资核算** - 主任工资计算,包含底薪、提成、奖励等
  23 +- **大项目主管工资核算** - 大项目主管工资计算
  24 +- **科技部总经理工资核算** - 科技部总经理工资计算
  25 +- **事业部总经理工资核算** - 事业部总经理工资计算
  26 +
  27 +### 📈 报表分析系统
  28 +- **门店业绩趋势** - 门店业绩时间趋势分析
  29 +- **门店业绩排行榜** - 门店业绩排名统计
  30 +- **健康师业绩趋势** - 健康师个人业绩趋势
  31 +- **健康师业绩排行榜** - 健康师业绩排名统计
  32 +- **金三角业绩趋势** - 金三角团队业绩趋势
  33 +- **综合仪表盘** - 多维度数据汇总展示
  34 +
  35 +### 🏪 门店管理系统
  36 +- **门店信息管理** - 门店基础信息维护
  37 +- **门店归属管理** - 门店归属事业部、教育部、科技部等
  38 +- **新店保护时间** - 新店保护期管理
  39 +- **门店股份统计** - 门店股份统计和分析
4 40  
5   -绿纤ERP管理系统是一个基于现代化技术栈开发的企业资源规划系统,专为绿纤行业量身定制。系统采用前后端分离架构,提供完整的门店管理、业绩统计、工资核算、报表分析等业务管理功能。
  41 +### 👥 人员管理系统
  42 +- **金三角设定** - 金三角团队配置管理
  43 +- **金三角用户绑定** - 用户与金三角团队绑定关系
  44 +- **顾问身份管理** - 根据金三角绑定自动设置顾问身份
6 45  
7   -## 🎯 核心功能亮点
  46 +### 📋 客户管理系统
  47 +- **客户信息管理** - 客户档案管理
  48 +- **拓客记录管理** - 拓客活动记录
  49 +- **拓客活动管理** - 拓客活动配置
  50 +- **会员权益管理** - 历史会员权益数据管理
  51 +- **用户画像** - 会员画像数据分析和展示
8 52  
9   -- **📊 业绩统计系统** - 门店业绩、个人业绩、金三角业绩等多维度统计
10   -- **💰 工资核算系统** - 健康师底薪、提成、奖励等自动计算
11   -- **📈 报表分析系统** - 可视化图表、趋势分析、排行榜展示
12   -- **🏪 门店管理系统** - 门店信息、归属管理、新店保护
13   -- **👥 人员管理系统** - 健康师管理、金三角团队管理
14   -- **📋 客户管理系统** - 客户信息、会员权益、拓客记录
  53 +### 🔧 其他业务模块
  54 +- **合同管理系统** - 合同信息管理
  55 +- **合作成本管理** - 合作成本表管理
  56 +- **店内支出管理** - 店内支出表管理
  57 +- **库存使用审批** - 库存使用审批流程
  58 +- **年度汇总统计** - 年度经营统计分析
15 59  
16   -## 技术栈
  60 +## 🛠 技术栈
17 61  
18 62 ### 后端技术
19 63 - **.NET Core 3.1/5.0** - 跨平台Web框架
... ... @@ -32,7 +76,11 @@
32 76 - **SCSS** - CSS预处理器
33 77 - **ECharts** - 数据可视化图表库
34 78  
35   -## 项目结构
  79 +### 移动端
  80 +- **uni-app** - 跨平台移动应用框架
  81 +- **微信小程序** - 支持微信小程序平台
  82 +
  83 +## 📁 项目结构
36 84  
37 85 ```
38 86 lvqianmeiye_ERP/
... ... @@ -51,7 +99,7 @@ lvqianmeiye_ERP/
51 99 │ ├── Order/ # 订单管理模块
52 100 │ ├── VisualDev/ # 可视化开发模块
53 101 │ └── Tenant/ # 多租户支持模块
54   -├── antis-ncc-admin/ # 前端Vue项目
  102 +├── antis-ncc-admin/ # 前端Vue管理后台
55 103 │ ├── src/
56 104 │ │ ├── api/ # API接口
57 105 │ │ ├── components/ # 公共组件
... ... @@ -63,12 +111,44 @@ lvqianmeiye_ERP/
63 111 │ │ │ ├── techPerformanceStatistics/ # 科技部业绩统计
64 112 │ │ │ ├── storeConsumePerformanceStatistics/ # 门店消耗业绩统计
65 113 │ │ │ ├── report/ # 报表分析页面
66   -│ │ │ └── ... # 其他业务页面
  114 +│ │ │ └── extend/ # 其他业务页面
67 115 │ │ ├── router/ # 路由配置
68 116 │ │ └── store/ # 状态管理
69 117 │ ├── package.json # 前端依赖配置
70 118 │ └── vue.config.js # Vue构建配置
71   -└── html/ # 静态资源
  119 +├── 绿纤uni-app/ # 移动端uni-app项目
  120 +│ ├── pages/ # 页面文件
  121 +│ ├── components/ # 组件
  122 +│ ├── apis/ # API接口
  123 +│ └── service/ # 服务层
  124 +├── 绿纤html/ # 静态HTML页面
  125 +│ ├── appointment.html # 预约页面
  126 +│ ├── expansion.html # 拓客页面
  127 +│ ├── member-consume.html # 会员消耗页面
  128 +│ └── ... # 其他页面
  129 +├── docs/ # 📚 项目文档
  130 +│ ├── 数据库说明.md # 数据库文档
  131 +│ ├── 工资计算规则梳理.md # 工资计算规则文档
  132 +│ ├── 接口文档.md # API接口文档
  133 +│ └── ... # 其他需求文档
  134 +├── scripts/ # 🔧 脚本工具
  135 +│ ├── sh/ # Shell测试脚本
  136 +│ │ ├── test_*.sh # 接口测试脚本
  137 +│ │ └── verify_*.sh # 数据验证脚本
  138 +│ └── py/ # Python工具脚本
  139 +│ ├── export_*.py # 数据导出脚本
  140 +│ └── generate_*.py # 数据生成脚本
  141 +├── sql/ # 📊 SQL脚本
  142 +│ ├── 表结构.sql # 数据库表结构
  143 +│ ├── 初始化数据.sql # 初始化数据脚本
  144 +│ └── ... # 其他SQL脚本
  145 +├── excel/ # 📄 Excel模板文件
  146 +│ ├── 健康师额外数据模板.xlsx
  147 +│ ├── 合作成本表.xlsx
  148 +│ └── ... # 其他模板文件
  149 +├── ExportFiles/ # 📤 导出文件目录
  150 +├── PROJECT_RULES.md # 项目开发规范
  151 +└── README.md # 项目说明文档
72 152 ```
73 153  
74 154 ## 🎯 核心业务模块说明
... ... @@ -76,14 +156,18 @@ lvqianmeiye_ERP/
76 156 **Extend模块**是系统的核心业务模块,包含绿纤ERP的所有业务逻辑。该模块采用三层架构设计:
77 157  
78 158 ### NCC.Extend(业务服务层)
79   -包含所有业务服务的具体实现,按功能分类:
80 159  
81 160 #### 📊 业绩统计类
82 161 - `LqStatisticsService` - 核心统计服务(个人业绩、门店业绩、金三角业绩等)
83 162 - `LqReportService` - 报表分析服务(趋势图、排行榜、仪表盘)
84 163  
85 164 #### 💰 工资核算类
86   -- `LqGzService` - 工资核算服务(健康师底薪、提成计算)
  165 +- `LqGzService` - 健康师工资核算服务
  166 +- `LqStoreManagerSalaryService` - 店长工资核算服务
  167 +- `LqDirectorSalaryService` - 主任工资核算服务
  168 +- `LqMajorProjectDirectorSalaryService` - 大项目主管工资核算服务
  169 +- `LqTechGeneralManagerSalaryService` - 科技部总经理工资核算服务
  170 +- `LqBusinessUnitManagerSalaryService` - 事业部总经理工资核算服务
87 171  
88 172 #### 🏪 门店管理类
89 173 - `LqMdxxService` - 门店信息管理服务
... ... @@ -105,40 +189,29 @@ lvqianmeiye_ERP/
105 189 - `WorkLogService` - 工作日志服务
106 190  
107 191 ### NCC.Extend.Entitys(实体模型层)
108   -- **Entity/** - 数据库实体模型(包含统计表、工资表等核心实体)
109   - - `lq_statistics_personal_performance/` - 个人业绩统计实体
110   - - `lq_statistics_store_total_performance/` - 门店总业绩统计实体
111   - - `lq_statistics_gold_triangle/` - 金三角业绩统计实体
112   - - `lq_statistics_department_consume_performance/` - 部门消耗业绩统计实体
113   - - `lq_statistics_tech_performance/` - 科技部业绩统计实体
114   - - `lq_statistics_store_consume_performance/` - 门店消耗业绩统计实体
115   - - `lq_salary_statistics/` - 工资统计实体
116   -- **Dto/** - 数据传输对象(包含统计、报表等DTO)
  192 +- **Entity/** - 数据库实体模型
  193 +- **Dto/** - 数据传输对象
117 194 - **Mapper/** - 对象映射配置
118 195 - **Model/** - 业务模型类
119 196  
120   -### NCC.Extend.Interfaces(接口定义层)
121   -- 定义所有业务服务的接口契约
122   -- 支持依赖注入和单元测试
123   -- 提供清晰的业务边界
  197 +## 🚀 快速开始
124 198  
125   -## 环境要求
  199 +### 环境要求
126 200  
127   -### 开发环境
128   -- **Node.js**: 16.20.2 (必须使用此版本)
  201 +#### 开发环境
  202 +- **Node.js**: 16.20.2 (⚠️ 必须使用此版本,其他版本可能不兼容)
129 203 - **.NET Core SDK**: 3.1 或 5.0
130 204 - **MySQL**: 5.7 或 8.0
131 205 - **Visual Studio 2019/2022** 或 **VS Code**
132 206  
133   -### 浏览器支持
  207 +#### 浏览器支持
134 208 - Chrome 70+
135 209 - Firefox 65+
136 210 - Safari 12+
137 211 - Edge 79+
138 212  
139   -## 快速开始
140   -
141 213 ### 1. 克隆项目
  214 +
142 215 ```bash
143 216 git clone [项目地址]
144 217 cd lvqianmeiye_ERP
... ... @@ -175,182 +248,75 @@ npm run dev
175 248 - 后端API:http://localhost:5000
176 249 - API文档:http://localhost:5000/antis.doc
177 250  
178   -## 默认账号
179   -
  251 +### 默认账号
180 252 - **管理员账号**:admin
181 253 - **默认密码**:123456
182 254  
183   -## 主要功能模块
184   -
185   -> 💡 **说明**:所有业务功能的核心代码都位于 `netcore/src/Modularity/Extend/` 模块中
186   -
187   -### 📊 业绩统计模块
188   -- **个人业绩统计** (`LqStatisticsService`) - 健康师个人业绩统计,包含首单业绩、升单业绩等
189   -- **门店总业绩统计** (`LqStatisticsService`) - 门店整体业绩统计,包含总业绩、欠款金额等
190   -- **金三角业绩统计** (`LqStatisticsService`) - 金三角团队业绩统计和分析
191   -- **部门消耗业绩统计** (`LqStatisticsService`) - 部门消耗业绩统计,包含人头数、人次等
192   -- **科技部业绩统计** (`LqStatisticsService`) - 科技部老师业绩统计
193   -- **门店消耗业绩统计** (`LqStatisticsService`) - 门店消耗业绩统计
194   -
195   -### 💰 工资核算模块
196   -- **工资统计** (`LqGzService`) - 健康师工资核算,包含底薪、提成、奖励等
197   -- **底薪计算** - 根据业绩标准自动计算健康师底薪(一星、二星、三星标准)
198   -- **提成计算** - 根据业绩类型计算各类提成(基础业绩、合作业绩、奖励业绩等)
199   -- **占比计算** - 队伍业绩与个人总业绩的占比分析
200   -
201   -### 📈 报表分析模块
202   -- **门店业绩趋势** (`LqReportService`) - 门店业绩时间趋势分析
203   -- **门店业绩排行榜** (`LqReportService`) - 门店业绩排名统计
204   -- **健康师业绩趋势** (`LqReportService`) - 健康师个人业绩趋势
205   -- **健康师业绩排行榜** (`LqReportService`) - 健康师业绩排名统计
206   -- **金三角业绩趋势** (`LqReportService`) - 金三角团队业绩趋势
207   -- **金三角业绩排行榜** (`LqReportService`) - 金三角团队排名统计
208   -- **综合仪表盘** (`LqReportService`) - 多维度数据汇总展示
209   -
210   -### 🏪 门店管理模块
211   -- **门店信息管理** (`LqMdxxService`) - 门店基础信息维护
212   -- **门店归属管理** - 门店归属事业部、教育部、科技部等
213   -- **新店保护时间** (`LqMdXdbhsjService`) - 新店保护期管理
  255 +## 📚 项目文档
214 256  
215   -### 👥 人员管理模块
216   -- **金三角设定** (`LqYcsdJsjService`) - 金三角团队配置管理
217   -- **金三角用户绑定** (`LqJinsanjiaoUserService`) - 用户与金三角团队绑定关系
218   -- **顾问身份管理** - 根据金三角绑定自动设置顾问身份
  257 +项目文档统一存放在 `docs/` 目录下,包含:
219 258  
220   -### 📋 客户管理模块
221   -- **客户信息管理** (`LqKhxxService`) - 客户档案管理
222   -- **拓客记录管理** (`LqTkjlbService`) - 拓客活动记录
223   -- **拓客活动管理** (`LqEventService`) - 拓客活动配置
224   -- **会员权益管理** - 历史会员权益数据管理
  259 +### 核心文档
  260 +- **数据库说明.md** - 数据库表结构、字段说明、关联关系
  261 +- **PROJECT_RULES.md** - 项目开发规范、编码规范、最佳实践
225 262  
226   -### ⚙️ 系统管理模块(位于System模块)
227   -- **用户管理** - 系统用户维护,用户权限管理
228   -- **角色权限** - 权限分配管理,角色权限配置
229   -- **系统配置** - 系统参数设置,基础配置维护
230   -- **日志管理** - 操作日志查看,系统日志分析
231   -
232   -## 开发指南
233   -
234   -### 🎯 核心业务开发(Extend模块)
235   -
236   -#### 统计服务开发规范
237   -1. **统计表实体** (`NCC.Extend.Entitys/Entity/lq_statistics_*/`)
238   - ```csharp
239   - [SugarTable("lq_statistics_新统计表")]
240   - [Tenant(ClaimConst.TENANT_ID)]
241   - public class LqStatistics新统计Entity
242   - {
243   - [SugarColumn(ColumnName = "F_Id", IsPrimaryKey = true)]
244   - public string Id { get; set; }
245   -
246   - [SugarColumn(ColumnName = "F_StatisticsMonth")]
247   - public string StatisticsMonth { get; set; }
248   -
249   - // 其他统计字段...
250   - }
251   - ```
252   -
253   -2. **统计DTO对象** (`NCC.Extend.Entitys/Dto/LqStatistics/`)
254   - - `Lq新统计ListQueryInput` - 列表查询输入
255   - - `Lq新统计ListOutput` - 列表输出DTO
256   -
257   -3. **统计服务实现** (`LqStatisticsService`)
258   - ```csharp
259   - /// <summary>
260   - /// 保存新统计数据
261   - /// </summary>
262   - public async Task Save新统计Statistics(string statisticsMonth)
263   - {
264   - // 统计逻辑实现
265   - }
266   -
267   - /// <summary>
268   - /// 获取新统计列表
269   - /// </summary>
270   - public async Task<dynamic> Get新统计StatisticsList(Lq新统计ListQueryInput input)
271   - {
272   - // 查询逻辑实现
273   - }
274   - ```
275   -
276   -#### 报表服务开发规范
277   -1. **报表接口定义** (`NCC.Extend.Interfaces/LqReport/`)
278   - ```csharp
279   - public interface ILqReportService
280   - {
281   - Task<object> Get新报表Data(新报表Input input);
282   - }
283   - ```
284   -
285   -2. **报表服务实现** (`LqReportService`)
286   - ```csharp
287   - [HttpPost("get-新报表-data")]
288   - public async Task<object> Get新报表Data(新报表Input input)
289   - {
290   - // 报表数据查询和格式化
291   - }
292   - ```
293   -
294   -#### 前端页面开发规范
295   -1. **统计页面组件** (`antis-ncc-admin/src/views/新统计Statistics/`)
296   - - `index.vue` - 统计列表页面
297   - - 使用 `NCC-table` 组件
298   - - 支持分页、筛选、导出功能
299   -
300   -2. **报表页面组件** (`antis-ncc-admin/src/views/report/`)
301   - - 集成到现有报表页面
302   - - 使用 ECharts 图表组件
303   - - 支持时间筛选、类型切换
304   -
305   -3. **API接口** (`antis-ncc-admin/src/api/`)
306   - ```javascript
307   - // 统计接口
308   - export function get新统计StatisticsList(data) {
309   - return request({
310   - url: '/api/Extend/LqStatistics/get-新统计-statistics-list',
311   - method: 'post',
312   - data
313   - })
314   - }
315   -
316   - // 报表接口
317   - export function get新报表Data(data) {
318   - return request({
319   - url: '/api/Extend/LqReport/get-新报表-data',
320   - method: 'post',
321   - data
322   - })
323   - }
324   - ```
325   -
326   -### 前端开发
327   -```bash
328   -# 开发模式
329   -npm run dev
  263 +### 业务文档
  264 +- **工资计算规则梳理.md** - 各类工资计算规则说明
  265 + - 健康师工资计算规则
  266 + - 店长工资计算规则
  267 + - 主任工资计算规则
  268 + - 大项目主管工资计算规则
  269 + - 科技部总经理工资计算规则
  270 + - 事业部总经理工资计算规则
  271 +- **接口文档.md** - API接口调用说明
  272 +- **需求分析文档** - 各类业务需求分析文档
330 273  
331   -# 构建生产版本
332   -npm run build
  274 +### 查看文档
  275 +```bash
  276 +# 查看所有文档
  277 +ls docs/
333 278  
334   -# 代码检查
335   -npm run lint
  279 +# 查看特定文档
  280 +cat docs/数据库说明.md
336 281 ```
337 282  
338   -### 后端开发
  283 +## 🔧 脚本工具
  284 +
  285 +项目提供了丰富的脚本工具,位于 `scripts/` 目录下:
  286 +
  287 +### Shell测试脚本 (`scripts/sh/`)
  288 +用于接口测试和数据验证:
339 289 ```bash
340   -# 还原包
341   -dotnet restore
  290 +# 测试个人业绩统计接口
  291 +./scripts/sh/test_personal_performance_api.sh
342 292  
343   -# 编译项目
344   -dotnet build
  293 +# 测试门店总业绩统计接口
  294 +./scripts/sh/test_store_total_performance_statistics.sh
345 295  
346   -# 运行项目
347   -dotnet run
  296 +# 验证门店总业绩数据
  297 +./scripts/sh/verify_store_total_performance_data.sh
  298 +```
  299 +
  300 +### Python工具脚本 (`scripts/py/`)
  301 +用于数据导出和生成:
  302 +```bash
  303 +# 导出所有会员剩余权益数据
  304 +python scripts/py/export_all_member_remaining_rights.py
348 305  
349   -# 发布项目
350   -dotnet publish -c Release
  306 +# 生成客户Excel数据
  307 +python scripts/py/generate_november_customer_excel.py
351 308 ```
352 309  
353   -## 配置说明
  310 +## 📊 SQL脚本
  311 +
  312 +数据库相关SQL脚本存放在 `sql/` 目录下:
  313 +
  314 +- **表结构脚本** - 数据库表结构定义
  315 +- **初始化数据脚本** - 系统初始化数据
  316 +- **数据迁移脚本** - 数据库版本升级脚本
  317 +- **统计脚本** - 统计数据生成脚本
  318 +
  319 +## ⚙️ 配置说明
354 320  
355 321 ### 数据库配置
356 322 修改 `netcore/src/Application/NCC.API/appsettings.json` 中的连接字符串:
... ... @@ -378,7 +344,7 @@ proxy: {
378 344 }
379 345 ```
380 346  
381   -## 部署说明
  347 +## 📦 部署说明
382 348  
383 349 ### 后端部署
384 350 1. 发布项目:`dotnet publish -c Release`
... ... @@ -390,91 +356,76 @@ proxy: {
390 356 2. 将 `dist` 目录部署到Web服务器
391 357 3. 配置Nginx或IIS反向代理
392 358  
393   -## 开发规范
  359 +## 📋 开发规范
  360 +
  361 +详细的开发规范请参考 [PROJECT_RULES.md](./PROJECT_RULES.md),主要规范包括:
394 362  
395   -### 📋 项目开发规范
396   -- **ID生成规范**: 必须使用 `YitIdHelper.NextId().ToString()` 生成ID,禁止使用 `Guid.NewGuid().ToString()`
  363 +### 核心规范
  364 +- **ID生成规范**: 必须使用 `YitIdHelper.NextId().ToString()` 生成ID
397 365 - **API接口规范**: GET请求使用data字段传参,不使用params
398 366 - **权限控制**: 所有数据查询必须添加园区权限过滤
399 367 - **数据一致性**: 统计数据和列表数据必须使用相同的过滤条件
400   -- **UI一致性**: 所有页面必须使用统一的布局和样式规范
401   -- **性能优化**: 所有列表接口支持分页,避免大数据量查询
402   -- **安全防护**: 使用SqlSugar ORM防止SQL注入
403   -
404   -### 🗄️ 数据库规范
405   -- **表命名**: 业务前缀 + 功能名称 (如: lq_)
406   -- **字段命名**: 驼峰化
407   -- **时间字段**: 统一使用 DateTime 类型
408   -- **删除标记**: `base_organize.DeleteMark` 为 `null` 表示未删除
409   -- **SQL查询验证**: 对于统计类型的SQL查询,在提交代码前必须先使用MCP MySQL工具执行验证
410   -
411   -### 🎨 前端开发规范
  368 +- **SQL查询验证**: 统计类型SQL查询必须先使用MCP MySQL工具验证
  369 +
  370 +### 前端规范
412 371 - **组件开发**: views 与 components 分离,弹窗、二级页面必须单独创建 Vue 文件
413 372 - **文件命名**: 使用 kebab-case (如: user-dialog.vue)
414 373 - **UI规范**: 统一使用 NCC-table,标签右对齐,卡片高度100px,内边距12px,圆角12px
415   -- **性能要求**: 启用懒加载和代码分割,页面加载时间 < 3s
416 374  
417   -## 常见问题
  375 +### 后端规范
  376 +- **分层架构**: Entitys → Interfaces → Services
  377 +- **异常处理**: 全局捕获,统一 JSON 格式返回
  378 +- **XML注释**: 关键方法必须添加 XML 注释
  379 +
  380 +## ❓ 常见问题
418 381  
419 382 ### Q: 前端启动失败
420   -A: 确保使用Node.js 16.20.2版本,其他版本可能不兼容
  383 +**A**: 确保使用Node.js 16.20.2版本,其他版本可能不兼容
421 384  
422 385 ### Q: 后端连接数据库失败
423   -A: 检查数据库连接字符串和MySQL服务状态
  386 +**A**: 检查数据库连接字符串和MySQL服务状态
424 387  
425 388 ### Q: 权限验证失败
426   -A: 检查JWT配置和Token有效期设置
427   -
428   -### Q: 页面显示异常
429   -A: 检查浏览器控制台错误信息和网络请求状态
  389 +**A**: 检查JWT配置和Token有效期设置
430 390  
431 391 ### Q: 统计SQL执行失败
432   -A: 使用MCP MySQL工具先验证SQL语法和字段名是否正确
433   -
434   -### Q: 报表页面404错误
435   -A: 检查LqReportService是否正确实现ITransient接口并注册到DI容器
436   -
437   -## 技术支持
438   -
439   -如有技术问题,请联系开发团队或查看项目文档。
440   -
441   -## 📊 已完成功能
442   -
443   -### ✅ 核心统计功能
444   -- [x] 个人业绩统计 - 健康师个人业绩统计,包含首单业绩、升单业绩等
445   -- [x] 门店总业绩统计 - 门店整体业绩统计,包含总业绩、欠款金额等
446   -- [x] 金三角业绩统计 - 金三角团队业绩统计和分析
447   -- [x] 部门消耗业绩统计 - 部门消耗业绩统计,包含人头数、人次等
448   -- [x] 科技部业绩统计 - 科技部老师业绩统计
449   -- [x] 门店消耗业绩统计 - 门店消耗业绩统计
450   -
451   -### ✅ 工资核算功能
452   -- [x] 工资统计 - 健康师工资核算,包含底薪、提成、奖励等
453   -- [x] 底薪计算 - 根据业绩标准自动计算健康师底薪(一星、二星、三星标准)
454   -- [x] 提成计算 - 根据业绩类型计算各类提成(基础业绩、合作业绩、奖励业绩等)
455   -- [x] 占比计算 - 队伍业绩与个人总业绩的占比分析
456   -
457   -### ✅ 报表分析功能
458   -- [x] 门店业绩趋势 - 门店业绩时间趋势分析
459   -- [x] 门店业绩排行榜 - 门店业绩排名统计
460   -- [x] 健康师业绩趋势 - 健康师个人业绩趋势
461   -- [x] 健康师业绩排行榜 - 健康师业绩排名统计
462   -- [x] 金三角业绩趋势 - 金三角团队业绩趋势
463   -- [x] 金三角业绩排行榜 - 金三角团队排名统计
464   -- [x] 综合仪表盘 - 多维度数据汇总展示
465   -
466   -### ✅ 系统优化
467   -- [x] 删除废弃代码 - 清理lq_ryzl、lq_tk_xsc等废弃模块
468   -- [x] 修复SQL语法 - 解决MySQL兼容性问题
469   -- [x] 修复字段映射 - 解决数据库字段名不匹配问题
470   -- [x] 优化前端页面 - 修复布局、分页、样式等问题
471   -
472   -## 版本信息
473   -
474   -- **当前版本**:v2.0.0
475   -- **最后更新**:2024年12月
476   -- **维护状态**:活跃开发中
477   -- **主要更新**:完成业绩统计、工资核算、报表分析等核心功能
  392 +**A**: 使用MCP MySQL工具先验证SQL语法和字段名是否正确
  393 +
  394 +### Q: 接口测试脚本无法运行
  395 +**A**: 确保脚本有执行权限:`chmod +x scripts/sh/*.sh`
  396 +
  397 +## 📈 项目状态
  398 +
  399 +### ✅ 已完成功能
  400 +- [x] 业绩统计系统(个人、门店、金三角、部门消耗、科技部、门店消耗)
  401 +- [x] 工资核算系统(健康师、店长、主任、大项目主管、科技部总经理、事业部总经理)
  402 +- [x] 报表分析系统(趋势图、排行榜、仪表盘)
  403 +- [x] 门店管理系统(门店信息、归属管理、新店保护)
  404 +- [x] 客户管理系统(客户信息、拓客记录、会员权益)
  405 +- [x] 合同管理系统
  406 +- [x] 合作成本和店内支出管理
  407 +- [x] 库存使用审批流程
  408 +- [x] 年度汇总统计
  409 +
  410 +### 🔄 持续优化
  411 +- 性能优化
  412 +- 代码重构
  413 +- 文档完善
  414 +- 测试覆盖
  415 +
  416 +## 📞 技术支持
  417 +
  418 +如有技术问题,请:
  419 +1. 查看项目文档:`docs/` 目录
  420 +2. 查看开发规范:`PROJECT_RULES.md`
  421 +3. 联系开发团队
  422 +
  423 +## 📝 版本信息
  424 +
  425 +- **当前版本**:v2.5.0
  426 +- **最后更新**:2025年1月
  427 +- **维护状态**:✅ 生产环境运行中
  428 +- **主要特性**:完整的业绩统计、工资核算、报表分析等核心功能
478 429  
479 430 ---
480 431  
... ...
antis-ncc-admin/.env.development
... ... @@ -2,8 +2,8 @@
2 2  
3 3 VUE_CLI_BABEL_TRANSPILE_MODULES = true
4 4 # VUE_APP_BASE_API = 'https://erp.lvqianmeiye.com'
5   -VUE_APP_BASE_API = 'http://erp_test.lvqianmeiye.com'
6   -# VUE_APP_BASE_API = 'http://localhost:2011'
  5 +# VUE_APP_BASE_API = 'http://erp_test.lvqianmeiye.com'
  6 +VUE_APP_BASE_API = 'http://localhost:2011'
7 7 # VUE_APP_BASE_API = 'http://localhost:2011'
8 8 VUE_APP_IMG_API = ''
9 9 VUE_APP_BASE_WSS = 'ws://192.168.110.45:2011/websocket'
... ...
antis-ncc-admin/src/api/report.js
... ... @@ -62,3 +62,102 @@ export function getDashboardData(data) {
62 62 data
63 63 })
64 64 }
  65 +
  66 +// 获取门店数据结构分析数据
  67 +export function getStoreDataAnalysis(data) {
  68 + return request({
  69 + url: '/api/Extend/LqReport/get-store-data-analysis',
  70 + method: 'post',
  71 + data
  72 + })
  73 +}
  74 +
  75 +// 获取门店驾驶舱统计数据
  76 +export function getStoreDashboardStatistics(data) {
  77 + return request({
  78 + url: '/api/Extend/LqStoreDashboard/GetStatistics',
  79 + method: 'post',
  80 + data
  81 + })
  82 +}
  83 +
  84 +// 获取门店近12个月业绩趋势
  85 +export function getStoreMonthlyTrend(data) {
  86 + return request({
  87 + url: '/api/Extend/LqReport/get-store-monthly-trend',
  88 + method: 'post',
  89 + data
  90 + })
  91 +}
  92 +
  93 +// 获取门店品项分析(包含品项分类占比)
  94 +export function getStoreItemAnalysis(data) {
  95 + return request({
  96 + url: '/api/Extend/LqReport/get-store-item-analysis',
  97 + method: 'post',
  98 + data
  99 + })
  100 +}
  101 +
  102 +// 获取门店健康师业绩排行(Top 10)
  103 +export function getStoreHealthCoachAnalysis(data) {
  104 + return request({
  105 + url: '/api/Extend/LqReport/get-store-health-coach-analysis',
  106 + method: 'post',
  107 + data
  108 + })
  109 +}
  110 +
  111 +// 获取门店会员分析
  112 +export function getStoreMemberAnalysis(data) {
  113 + return request({
  114 + url: '/api/Extend/LqReport/get-store-member-analysis',
  115 + method: 'post',
  116 + data
  117 + })
  118 +}
  119 +
  120 +// 获取门店各分类月度业绩
  121 +export function getCategoryMonthlyPerformance(data) {
  122 + return request({
  123 + url: '/api/Extend/LqStoreDashboard/GetCategoryMonthlyPerformance',
  124 + method: 'post',
  125 + data
  126 + })
  127 +}
  128 +
  129 +// 获取门店会员转化漏斗数据
  130 +export function getMemberConversionFunnel(data) {
  131 + return request({
  132 + url: '/api/Extend/LqStoreDashboard/GetMemberConversionFunnel',
  133 + method: 'post',
  134 + data
  135 + })
  136 +}
  137 +
  138 +// 获取门店客单价与项目数关系数据
  139 +export function getCustomerPriceProjectRelation(data) {
  140 + return request({
  141 + url: '/api/Extend/LqStoreDashboard/GetCustomerPriceProjectRelation',
  142 + method: 'post',
  143 + data
  144 + })
  145 +}
  146 +
  147 +// 获取门店排名对比数据
  148 +export function getStoreComparisonAnalysis(data) {
  149 + return request({
  150 + url: '/api/Extend/LqReport/get-store-comparison-analysis',
  151 + method: 'post',
  152 + data
  153 + })
  154 +}
  155 +
  156 +// 获取门店一周运营热力图数据
  157 +export function getWeeklyHeatmap(data) {
  158 + return request({
  159 + url: '/api/Extend/LqStoreDashboard/GetWeeklyHeatmap',
  160 + method: 'post',
  161 + data
  162 + })
  163 +}
... ...
antis-ncc-admin/src/views/extend/storeDashboard/index.vue
1 1 <template>
2 2 <div class="store-dashboard">
  3 + <!-- 顶部筛选器 -->
  4 + <div class="filter-bar">
  5 + <el-form :inline="true" :model="queryParams" class="filter-form">
  6 + <el-form-item label="选择月份">
  7 + <el-date-picker v-model="queryParams.month" type="month" value-format="yyyy-MM" placeholder="选择月份"
  8 + clearable size="small" @change="handleQueryChange" style="width: 150px" />
  9 + </el-form-item>
  10 + <el-form-item label="选择门店">
  11 + <el-select v-model="queryParams.storeId" placeholder="请选择门店" clearable filterable size="small"
  12 + @change="handleQueryChange" style="width: 200px">
  13 + <el-option v-for="store in storeOptions" :key="store.id" :label="store.fullName"
  14 + :value="store.id" />
  15 + </el-select>
  16 + </el-form-item>
  17 + <el-form-item>
  18 + <el-button type="primary" size="small" icon="el-icon-search"
  19 + @click="handleQueryChange">查询</el-button>
  20 + <el-button size="small" icon="el-icon-refresh-right" @click="handleReset">重置</el-button>
  21 + </el-form-item>
  22 + </el-form>
  23 + </div>
  24 +
3 25 <!-- 顶部:门店信息 + 核心指标 -->
4 26 <div class="dashboard-header">
5 27 <div class="header-left">
... ... @@ -9,63 +31,53 @@
9 31 </div>
10 32 <div class="store-details">
11 33 <div class="store-name-row">
12   - <h2 class="store-name">示例门店名称</h2>
  34 + <h2 class="store-name">{{ currentStoreName || '请选择门店' }}</h2>
13 35 <el-tag type="success" size="small">正常营业</el-tag>
14 36 </div>
15 37 <div class="store-meta">
16   - <span class="meta-item"><i class="el-icon-tickets"></i> MD2024001</span>
17   - <span class="meta-item"><i class="el-icon-location"></i> 北京市朝阳区</span>
18   - <span class="meta-item"><i class="el-icon-calendar"></i> 2024-01-15</span>
  38 + <span class="meta-item"><i class="el-icon-tickets"></i> {{ currentStoreCode || '-' }}</span>
  39 + <span class="meta-item"><i class="el-icon-location"></i> {{ currentStoreAddress || '-'
  40 + }}</span>
  41 + <span class="meta-item"><i class="el-icon-calendar"></i> {{ queryParams.month || '当前月份'
  42 + }}</span>
19 43 </div>
20 44 </div>
21 45 </div>
22 46 </div>
23 47 <div class="header-right">
24   - <div class="core-stats">
  48 + <div class="core-stats" v-loading="loading">
25 49 <div class="core-stat-item primary">
26 50 <div class="stat-label">开单业绩</div>
27   - <div class="stat-value">¥1,258,680</div>
28   - <div class="stat-trend up">+12.5%</div>
  51 + <div class="stat-value">¥{{ storeData && storeData.Performance &&
  52 + storeData.Performance.BillingPerformance ?
  53 + formatMoney(storeData.Performance.BillingPerformance) : '0.00' }}</div>
  54 + <div class="stat-trend" v-if="false">+12.5%</div>
29 55 </div>
30 56 <div class="core-stat-item success">
31 57 <div class="stat-label">消耗业绩</div>
32   - <div class="stat-value">¥986,420</div>
33   - <div class="stat-trend up">+8.3%</div>
  58 + <div class="stat-value">¥{{ storeData && storeData.Performance &&
  59 + storeData.Performance.ConsumePerformance ?
  60 + formatMoney(storeData.Performance.ConsumePerformance) : '0.00' }}</div>
  61 + <div class="stat-trend" v-if="false">+8.3%</div>
34 62 </div>
35 63 <div class="core-stat-item info">
36 64 <div class="stat-label">完成率</div>
37   - <div class="stat-value">85.6%</div>
38   - <div class="stat-trend up">+2.1%</div>
  65 + <div class="stat-value">{{ storeData && storeData.Performance &&
  66 + storeData.Performance.CompletionRate ?
  67 + formatMoney(storeData.Performance.CompletionRate, 2) : '0.00' }}%</div>
  68 + <div class="stat-trend" v-if="false">+2.1%</div>
39 69 </div>
40 70 <div class="core-stat-item warning">
41 71 <div class="stat-label">净业绩</div>
42   - <div class="stat-value">¥272,260</div>
43   - <div class="stat-trend up">+15.8%</div>
  72 + <div class="stat-value">¥{{ storeData && storeData.Performance &&
  73 + storeData.Performance.NetPerformance ?
  74 + formatMoney(storeData.Performance.NetPerformance) : '0.00' }}</div>
  75 + <div class="stat-trend" v-if="false">+15.8%</div>
44 76 </div>
45 77 </div>
46 78 </div>
47 79 </div>
48 80  
49   - <!-- 核心KPI指标 -->
50   - <div class="kpi-section">
51   - <div class="kpi-card" v-for="(kpi, index) in kpiList" :key="index" :class="kpi.type">
52   - <div class="kpi-icon">
53   - <i :class="kpi.icon"></i>
54   - </div>
55   - <div class="kpi-content">
56   - <div class="kpi-label">{{ kpi.label }}</div>
57   - <div class="kpi-value">
58   - <span class="unit" v-if="kpi.isMoney">¥</span>{{ kpi.value }}
59   - <span class="unit" v-if="kpi.isPercent">%</span>
60   - </div>
61   - </div>
62   - <div class="kpi-trend" v-if="kpi.trend">
63   - <i :class="kpi.trendIcon"></i>
64   - <span>{{ kpi.trend }}</span>
65   - </div>
66   - </div>
67   - </div>
68   -
69 81 <!-- 主要内容区域:左右分栏 -->
70 82 <div class="main-content">
71 83 <!-- 左侧:图表区域 -->
... ... @@ -92,20 +104,7 @@
92 104 </el-col>
93 105 </el-row>
94 106  
95   - <!-- 第二行:门店综合能力雷达图 -->
96   - <el-row :gutter="16" class="chart-row">
97   - <el-col :span="24">
98   - <el-card class="chart-card" shadow="hover">
99   - <div slot="header" class="card-header">
100   - <i class="el-icon-aim"></i>
101   - <span>门店综合能力分析</span>
102   - </div>
103   - <div ref="radarChart" class="chart-container"></div>
104   - </el-card>
105   - </el-col>
106   - </el-row>
107   -
108   - <!-- 第三行:业绩对比分析 + 各分类业绩堆叠对比 -->
  107 + <!-- 第二行:业绩对比分析 + 各分类业绩堆叠对比 -->
109 108 <el-row :gutter="16" class="chart-row">
110 109 <el-col :span="12">
111 110 <el-card class="chart-card" shadow="hover">
... ... @@ -133,7 +132,7 @@
133 132 <el-card class="chart-card" shadow="hover">
134 133 <div slot="header" class="card-header">
135 134 <i class="el-icon-sort"></i>
136   - <span>会员转化漏斗</span>
  135 + <span>拓客转化漏斗</span>
137 136 </div>
138 137 <div ref="funnelChart" class="chart-container"></div>
139 138 </el-card>
... ... @@ -212,13 +211,13 @@
212 211 <el-table-column prop="name" label="健康师姓名" min-width="120" />
213 212 <el-table-column prop="billingPerformance" label="开单业绩" width="120" align="right">
214 213 <template slot-scope="scope">¥{{ formatMoney(scope.row.billingPerformance)
215   - }}</template>
  214 + }}</template>
216 215 </el-table-column>
217 216 <el-table-column prop="consumePerformance" label="消耗业绩" width="120" align="right">
218 217 <template slot-scope="scope">¥{{ formatMoney(scope.row.consumePerformance)
219   - }}</template>
  218 + }}</template>
220 219 </el-table-column>
221   - <el-table-column prop="totalPerformance" label="业绩" width="120" align="right">
  220 + <el-table-column prop="totalPerformance" label="业绩" width="120" align="right">
222 221 <template slot-scope="scope">¥{{ formatMoney(scope.row.totalPerformance)
223 222 }}</template>
224 223 </el-table-column>
... ... @@ -316,14 +315,6 @@
316 315 <div ref="gaugeChart" class="chart-container-small"></div>
317 316 </el-card>
318 317  
319   - <!-- 各分类业绩占比趋势 -->
320   - <el-card class="chart-card-small" shadow="hover">
321   - <div slot="header" class="card-header">
322   - <i class="el-icon-data-line"></i>
323   - <span>各分类占比趋势</span>
324   - </div>
325   - <div ref="stackedAreaChart" class="chart-container-small"></div>
326   - </el-card>
327 318  
328 319 <!-- 门店排名对比 -->
329 320 <el-card class="metrics-card" shadow="hover">
... ... @@ -426,95 +417,37 @@
426 417  
427 418 <script>
428 419 import * as echarts from 'echarts'
  420 +import { getStoreSelector } from '@/api/extend/store'
  421 +import { getStoreDashboardStatistics, getStoreMonthlyTrend, getStoreItemAnalysis, getStoreMemberAnalysis, getCategoryMonthlyPerformance, getMemberConversionFunnel, getCustomerPriceProjectRelation, getStoreComparisonAnalysis, getWeeklyHeatmap, getStoreHealthCoachAnalysis } from '@/api/report'
429 422  
430 423 export default {
431 424 name: 'StoreDashboard',
432 425 data() {
433 426 return {
434   - kpiList: [
435   - { label: '开单次数', value: '1,256', icon: 'el-icon-s-order', type: 'primary', trend: '+5.2%', trendIcon: 'el-icon-top' },
436   - { label: '消耗次数', value: '2,458', icon: 'el-icon-s-marketing', type: 'success', trend: '+3.7%', trendIcon: 'el-icon-top' },
437   - { label: '人头数', value: '1,258', icon: 'el-icon-user', type: 'info', trend: '+4.1%', trendIcon: 'el-icon-top' },
438   - { label: '人次', value: '3,456', icon: 'el-icon-user-solid', type: 'warning', trend: '+6.2%', trendIcon: 'el-icon-top' },
439   - { label: '项目数', value: '5,678', icon: 'el-icon-menu', type: 'primary', trend: '+3.5%', trendIcon: 'el-icon-top' },
440   - { label: '客单价', value: '¥285', icon: 'el-icon-coin', type: 'success', isMoney: true, trend: '+2.8%', trendIcon: 'el-icon-top' }
441   - ],
442   - performanceList: [
443   - { label: '开单次数', value: '1,256', icon: 'el-icon-document', iconBg: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)' },
444   - { label: '消耗次数', value: '2,458', icon: 'el-icon-goods', iconBg: 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)' },
445   - { label: '退卡次数', value: '23', icon: 'el-icon-refresh-left', iconBg: 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)' },
446   - { label: '平均开单金额', value: '¥1,002', icon: 'el-icon-coin', iconBg: 'linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)' },
447   - { label: '平均消耗金额', value: '¥401', icon: 'el-icon-coin', iconBg: 'linear-gradient(135deg, #fa709a 0%, #fee140 100%)' },
448   - { label: '剩余权益', value: '¥325.69万', icon: 'el-icon-wallet', iconBg: 'linear-gradient(135deg, #30cfd0 0%, #330867 100%)' },
449   - { label: '目标业绩', value: '¥120万', icon: 'el-icon-aim', iconBg: 'linear-gradient(135deg, #a8edea 0%, #fed6e3 100%)' },
450   - { label: '退卡金额', value: '¥4.57万', icon: 'el-icon-money', iconBg: 'linear-gradient(135deg, #ff9a9e 0%, #fecfef 100%)' }
451   - ],
452   - operationList: [
453   - { label: '人头数', value: '1,258', icon: 'el-icon-user', iconBg: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)' },
454   - { label: '人次', value: '3,456', icon: 'el-icon-user-solid', iconBg: 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)' },
455   - { label: '项目数', value: '5,678', icon: 'el-icon-menu', iconBg: 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)' },
456   - { label: '客单价', value: '¥285', icon: 'el-icon-coin', iconBg: 'linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)' },
457   - { label: '项目单价', value: '¥174', icon: 'el-icon-coin', iconBg: 'linear-gradient(135deg, #fa709a 0%, #fee140 100%)' },
458   - { label: '人均项目数', value: '4.51', icon: 'el-icon-s-grid', iconBg: 'linear-gradient(135deg, #30cfd0 0%, #330867 100%)' }
459   - ],
460   - memberList: [
461   - { label: '总会员数', value: '3,256', icon: 'el-icon-user', iconBg: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)' },
462   - { label: '本月新增', value: '156', icon: 'el-icon-user-solid', iconBg: 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)' },
463   - { label: '活跃会员', value: '1,856', icon: 'el-icon-success', iconBg: 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)', rate: '57.0%' },
464   - { label: '沉睡会员', value: '856', icon: 'el-icon-warning', iconBg: 'linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)', rate: '26.3%' },
465   - { label: '生美会员', value: '2,156', icon: 'el-icon-star-on', iconBg: 'linear-gradient(135deg, #fa709a 0%, #fee140 100%)' },
466   - { label: '医美会员', value: '856', icon: 'el-icon-star-on', iconBg: 'linear-gradient(135deg, #30cfd0 0%, #330867 100%)' },
467   - { label: '科美会员', value: '456', icon: 'el-icon-star-on', iconBg: 'linear-gradient(135deg, #a8edea 0%, #fed6e3 100%)' },
468   - { label: '教育会员', value: '256', icon: 'el-icon-star-on', iconBg: 'linear-gradient(135deg, #ff9a9e 0%, #fecfef 100%)' }
469   - ],
470   - healthCoachRanking: [
471   - { name: '张健康', billingPerformance: 256800, consumePerformance: 198600, totalPerformance: 455400 },
472   - { name: '李美丽', billingPerformance: 198600, consumePerformance: 156800, totalPerformance: 355400 },
473   - { name: '王优雅', billingPerformance: 186500, consumePerformance: 145600, totalPerformance: 332100 },
474   - { name: '刘青春', billingPerformance: 165800, consumePerformance: 128900, totalPerformance: 294700 },
475   - { name: '陈优雅', billingPerformance: 145600, consumePerformance: 112500, totalPerformance: 258100 },
476   - { name: '杨美丽', billingPerformance: 128900, consumePerformance: 98600, totalPerformance: 227500 },
477   - { name: '赵健康', billingPerformance: 112500, consumePerformance: 85600, totalPerformance: 198100 },
478   - { name: '钱优雅', billingPerformance: 98600, consumePerformance: 75600, totalPerformance: 174200 },
479   - { name: '孙美丽', billingPerformance: 85600, consumePerformance: 65800, totalPerformance: 151400 },
480   - { name: '周健康', billingPerformance: 75600, consumePerformance: 56800, totalPerformance: 132400 }
481   - ],
482   - topBillingItems: [
483   - { itemName: '高端护理套餐', billingAmount: 156800, billingCount: 45, category: '生美' },
484   - { itemName: '医美美容项目', billingAmount: 128600, billingCount: 38, category: '医美' },
485   - { itemName: 'SPA身体护理', billingAmount: 98500, billingCount: 52, category: '生美' },
486   - { itemName: '激光美容疗程', billingAmount: 89600, billingCount: 28, category: '医美' },
487   - { itemName: '皮肤管理项目', billingAmount: 75800, billingCount: 42, category: '科美' },
488   - { itemName: '面部深度护理', billingAmount: 68900, billingCount: 56, category: '生美' },
489   - { itemName: '抗衰美容项目', billingAmount: 58600, billingCount: 22, category: '医美' },
490   - { itemName: '美白亮肤疗程', billingAmount: 48900, billingCount: 35, category: '科美' },
491   - { itemName: '眼部精华护理', billingAmount: 42800, billingCount: 48, category: '生美' },
492   - { itemName: '补水保湿项目', billingAmount: 38600, billingCount: 62, category: '生美' }
493   - ],
494   - topConsumeItems: [
495   - { itemName: '面部护理套餐', consumeAmount: 125680, category: '生美' },
496   - { itemName: '身体SPA护理', consumeAmount: 98600, category: '生美' },
497   - { itemName: '激光美容项目', consumeAmount: 85600, category: '医美' },
498   - { itemName: '抗衰老治疗', consumeAmount: 75600, category: '医美' },
499   - { itemName: '皮肤管理项目', consumeAmount: 65800, category: '科美' },
500   - { itemName: '眼部护理', consumeAmount: 56800, category: '生美' },
501   - { itemName: '紧致提升项目', consumeAmount: 45600, category: '医美' },
502   - { itemName: '美白亮肤项目', consumeAmount: 38900, category: '科美' },
503   - { itemName: '深层清洁护理', consumeAmount: 32800, category: '生美' },
504   - { itemName: '补水保湿项目', consumeAmount: 28900, category: '生美' }
505   - ],
506   - dailyData: [
507   - { date: '2024-12-01', headCount: 45, personCount: 128, projectCount: 256, billingPerformance: 45680, consumePerformance: 32890 },
508   - { date: '2024-12-02', headCount: 52, personCount: 145, projectCount: 289, billingPerformance: 52890, consumePerformance: 38960 },
509   - { date: '2024-12-03', headCount: 48, personCount: 132, projectCount: 268, billingPerformance: 48960, consumePerformance: 35280 },
510   - { date: '2024-12-04', headCount: 56, personCount: 156, projectCount: 312, billingPerformance: 56890, consumePerformance: 41250 },
511   - { date: '2024-12-05', headCount: 49, personCount: 138, projectCount: 278, billingPerformance: 49860, consumePerformance: 36580 },
512   - { date: '2024-12-06', headCount: 58, personCount: 162, projectCount: 325, billingPerformance: 59860, consumePerformance: 43280 },
513   - { date: '2024-12-07', headCount: 62, personCount: 178, projectCount: 356, billingPerformance: 62890, consumePerformance: 45680 },
514   - { date: '2024-12-08', headCount: 55, personCount: 152, projectCount: 304, billingPerformance: 55860, consumePerformance: 40250 },
515   - { date: '2024-12-09', headCount: 51, personCount: 142, projectCount: 284, billingPerformance: 51860, consumePerformance: 37580 },
516   - { date: '2024-12-10', headCount: 59, personCount: 165, projectCount: 330, billingPerformance: 59860, consumePerformance: 43280 }
517   - ],
  427 + // 查询参数
  428 + queryParams: {
  429 + month: '', // 月份,格式:yyyy-MM
  430 + storeId: '' // 门店ID
  431 + },
  432 + // 门店选项
  433 + storeOptions: [],
  434 + // 当前选中的门店信息
  435 + currentStoreName: '',
  436 + currentStoreCode: '',
  437 + currentStoreAddress: '',
  438 + // 数据加载状态
  439 + loading: false,
  440 + // 门店统计数据
  441 + storeData: null,
  442 + performanceList: [],
  443 + operationList: [],
  444 + memberList: [],
  445 + monthlyTrendData: [],
  446 + categoryData: [],
  447 + healthCoachRanking: [],
  448 + topBillingItems: [],
  449 + topConsumeItems: [],
  450 + dailyData: [],
518 451 trendChart: null,
519 452 categoryChart: null,
520 453 compareChart: null,
... ... @@ -522,38 +455,28 @@ export default {
522 455 funnelChart: null,
523 456 scatterChart: null,
524 457 heatmapChart: null,
525   - radarChart: null,
526 458 gaugeChart: null,
527   - stackedAreaChart: null,
  459 + categoryMonthlyData: [], // 各分类月度业绩数据
  460 + funnelData: null, // 会员转化漏斗数据
  461 + scatterData: [], // 客单价与项目数关系数据
  462 + heatmapData: [], // 一周运营热力图数据
528 463 comparison: {
529   - performanceRanking: 5,
530   - totalStoreCount: 28,
531   - avgPerformanceSameType: 1156800,
532   - sameTypeStoreCount: 12,
533   - avgPerformanceSameOrg: 1089600,
534   - sameOrgStoreCount: 8
  464 + performanceRanking: 0,
  465 + totalStoreCount: 0,
  466 + avgPerformanceSameType: 0,
  467 + sameTypeStoreCount: 0,
  468 + avgPerformanceSameOrg: 0,
  469 + sameOrgStoreCount: 0
535 470 },
536   - operationTips: [
537   - { type: 'success', icon: 'el-icon-success', text: '本月业绩完成度良好,保持当前节奏' },
538   - { type: 'warning', icon: 'el-icon-warning', text: '沉睡会员占比26.3%,建议加强会员唤醒' },
539   - { type: 'info', icon: 'el-icon-info', text: '客单价¥285,可通过项目组合提升' },
540   - { type: 'warning', icon: 'el-icon-warning', text: '退卡金额较上月增长,需关注服务质量' }
541   - ],
542   - dataInsights: [
543   - { title: '最佳营业时段', tag: '热门', tagType: 'danger', value: '14:00-17:00', desc: '此时段客流量最高,建议配置更多人手' },
544   - { title: '高价值会员', tag: '重点', tagType: 'warning', value: '156人', desc: '单次消费超过¥1000,需重点维护' },
545   - { title: '项目转化率', tag: '优秀', tagType: 'success', value: '68.5%', desc: '体验项目转化为正式开卡的比例' },
546   - { title: '复购周期', tag: '正常', tagType: 'info', value: '28天', desc: '会员平均复购间隔,保持稳定' }
547   - ],
548   - keyMetrics: [
549   - { label: '目标完成度', value: 85.6, color: '#67C23A' },
550   - { label: '会员活跃度', value: 57.0, color: '#409EFF' },
551   - { label: '项目满意度', value: 92.3, color: '#E6A23C' },
552   - { label: '员工效率', value: 78.5, color: '#F56C6C' }
553   - ]
  471 + operationTips: [],
  472 + dataInsights: [],
  473 + keyMetrics: []
554 474 }
555 475 },
556 476 mounted() {
  477 + this.initQueryParams()
  478 + this.loadStoreOptions()
  479 + // 不自动加载数据,等待用户选择门店和月份后再查询
557 480 this.initCharts()
558 481 window.addEventListener('resize', this.handleResize)
559 482 },
... ... @@ -565,12 +488,878 @@ export default {
565 488 if (this.funnelChart) this.funnelChart.dispose()
566 489 if (this.scatterChart) this.scatterChart.dispose()
567 490 if (this.heatmapChart) this.heatmapChart.dispose()
568   - if (this.radarChart) this.radarChart.dispose()
569 491 if (this.gaugeChart) this.gaugeChart.dispose()
570   - if (this.stackedAreaChart) this.stackedAreaChart.dispose()
571 492 window.removeEventListener('resize', this.handleResize)
572 493 },
573 494 methods: {
  495 + // 初始化查询参数
  496 + initQueryParams() {
  497 + const now = new Date()
  498 + const year = now.getFullYear()
  499 + const month = String(now.getMonth() + 1).padStart(2, '0')
  500 + this.queryParams.month = `${year}-${month}`
  501 + },
  502 + // 加载门店选项
  503 + async loadStoreOptions() {
  504 + try {
  505 + const response = await getStoreSelector()
  506 + if (response.code === 200 && response.data) {
  507 + this.storeOptions = response.data.list || []
  508 + // 如果只有一个门店,默认选中
  509 + if (this.storeOptions.length === 1) {
  510 + this.queryParams.storeId = this.storeOptions[0].id
  511 + this.updateCurrentStoreInfo()
  512 + }
  513 + }
  514 + } catch (error) {
  515 + console.error('获取门店列表失败:', error)
  516 + this.storeOptions = []
  517 + }
  518 + },
  519 + // 更新当前门店信息
  520 + updateCurrentStoreInfo() {
  521 + if (this.queryParams.storeId) {
  522 + const store = this.storeOptions.find(s => s.id === this.queryParams.storeId)
  523 + if (store) {
  524 + this.currentStoreName = store.fullName || store.dm || ''
  525 + this.currentStoreCode = store.enCode || store.bm || ''
  526 + this.currentStoreAddress = store.address || ''
  527 + } else {
  528 + this.currentStoreName = ''
  529 + this.currentStoreCode = ''
  530 + this.currentStoreAddress = ''
  531 + }
  532 + } else {
  533 + this.currentStoreName = ''
  534 + this.currentStoreCode = ''
  535 + this.currentStoreAddress = ''
  536 + }
  537 + },
  538 + // 查询变化
  539 + handleQueryChange() {
  540 + this.updateCurrentStoreInfo()
  541 + // TODO: 重新加载数据
  542 + this.loadDashboardData()
  543 + },
  544 + // 重置查询
  545 + handleReset() {
  546 + this.initQueryParams()
  547 + this.queryParams.storeId = ''
  548 + this.updateCurrentStoreInfo()
  549 + // 重置时清空所有数据(loadDashboardData会处理)
  550 + this.loadDashboardData()
  551 + },
  552 + // 加载驾驶舱数据
  553 + async loadDashboardData() {
  554 + if (!this.queryParams.storeId || !this.queryParams.month) {
  555 + // 如果没有选择门店或月份,清空所有数据
  556 + this.storeData = null
  557 + this.performanceList = []
  558 + this.operationList = []
  559 + this.memberList = []
  560 + this.healthCoachRanking = []
  561 + this.topBillingItems = []
  562 + this.topConsumeItems = []
  563 + this.monthlyTrendData = []
  564 + this.categoryData = []
  565 + this.categoryMonthlyData = []
  566 + this.funnelData = null
  567 + this.scatterData = []
  568 + this.heatmapData = []
  569 + this.comparison = {
  570 + performanceRanking: 0,
  571 + totalStoreCount: 0,
  572 + avgPerformanceSameType: 0,
  573 + sameTypeStoreCount: 0,
  574 + avgPerformanceSameOrg: 0,
  575 + sameOrgStoreCount: 0
  576 + }
  577 + this.operationTips = []
  578 + this.dataInsights = []
  579 + this.keyMetrics = []
  580 + this.updateDisplayData()
  581 + // 清空所有图表
  582 + this.$nextTick(() => {
  583 + this.renderTrendChart()
  584 + this.renderCategoryChart()
  585 + this.renderCompareChart()
  586 + this.renderStackedChart()
  587 + this.renderFunnelChart()
  588 + this.renderScatterChart()
  589 + this.renderHeatmapChart()
  590 + this.renderGaugeChart()
  591 + })
  592 + return
  593 + }
  594 +
  595 + this.loading = true
  596 + try {
  597 + // 将月份格式从 yyyy-MM 转换为 yyyyMM
  598 + const statisticsMonth = this.queryParams.month.replace('-', '')
  599 +
  600 + const response = await getStoreDashboardStatistics({
  601 + storeId: this.queryParams.storeId,
  602 + statisticsMonth: statisticsMonth
  603 + })
  604 +
  605 + if (response.code === 200 && response.data) {
  606 + // 将返回的数据转换为与原有结构兼容的格式
  607 + this.storeData = {
  608 + Performance: {
  609 + BillingPerformance: response.data.BillingPerformance || 0,
  610 + ConsumePerformance: response.data.ConsumePerformance || 0,
  611 + CompletionRate: response.data.CompletionRate || 0,
  612 + NetPerformance: response.data.NetPerformance || 0,
  613 + BillingCount: response.data.BillingCount || 0,
  614 + ConsumeCount: response.data.ConsumeCount || 0,
  615 + AvgBillingAmount: response.data.AvgBillingAmount || 0,
  616 + AvgConsumeAmount: response.data.AvgConsumeAmount || 0,
  617 + RefundAmount: response.data.RefundAmount || 0,
  618 + RefundCount: response.data.RefundCount || 0,
  619 + RemainingRightsAmount: response.data.RemainingRightsAmount || 0,
  620 + TargetPerformance: response.data.TargetPerformance || 0
  621 + },
  622 + Operation: {
  623 + HeadCount: response.data.HeadCount || 0,
  624 + PersonCount: response.data.PersonCount || 0,
  625 + ProjectCount: response.data.ProjectCount || 0,
  626 + AvgAmountPerPerson: response.data.AvgAmountPerPerson || 0,
  627 + AvgAmountPerProject: response.data.AvgAmountPerProject || 0,
  628 + AvgProjectPerHead: response.data.AvgProjectPerHead || 0
  629 + }
  630 + }
  631 + this.updateDisplayData()
  632 +
  633 + // 加载其他数据
  634 + await Promise.all([
  635 + this.loadMonthlyTrendData(),
  636 + this.loadCategoryData(),
  637 + this.loadMemberAnalysisData(),
  638 + this.loadCategoryMonthlyData(),
  639 + this.loadFunnelData(),
  640 + this.loadScatterData(),
  641 + this.loadComparisonData(),
  642 + this.loadHeatmapData(),
  643 + this.loadTopBillingItems(),
  644 + this.loadTopConsumeItems(),
  645 + this.loadHealthCoachRanking()
  646 + ])
  647 +
  648 + // 数据加载完成后,更新快速数据洞察、本月关键指标、本月经营提示
  649 + this.updateDataInsights()
  650 + this.updateKeyMetrics()
  651 + this.updateOperationTips()
  652 + } else {
  653 + this.$message.error(response.msg || '获取数据失败')
  654 + this.storeData = null
  655 + this.updateDisplayData()
  656 + // 清空数据洞察、关键指标、经营提示
  657 + this.updateDataInsights()
  658 + this.updateKeyMetrics()
  659 + this.updateOperationTips()
  660 + }
  661 + } catch (error) {
  662 + console.error('加载门店数据失败:', error)
  663 + this.$message.error('加载数据失败:' + (error.message || '未知错误'))
  664 + this.storeData = null
  665 + this.updateDisplayData()
  666 + // 清空数据洞察、关键指标、经营提示
  667 + this.updateDataInsights()
  668 + this.updateKeyMetrics()
  669 + this.updateOperationTips()
  670 + } finally {
  671 + this.loading = false
  672 + }
  673 + },
  674 + // 加载近12个月业绩趋势数据
  675 + async loadMonthlyTrendData() {
  676 + if (!this.queryParams.storeId) {
  677 + this.monthlyTrendData = []
  678 + this.$nextTick(() => {
  679 + this.renderTrendChart()
  680 + this.renderCompareChart()
  681 + })
  682 + return
  683 + }
  684 +
  685 + try {
  686 + const statisticsMonth = this.queryParams.month.replace('-', '')
  687 + const response = await getStoreMonthlyTrend({
  688 + storeId: this.queryParams.storeId,
  689 + statisticsMonth: statisticsMonth
  690 + })
  691 +
  692 + if (response.code === 200 && response.data && response.data.length > 0) {
  693 + this.monthlyTrendData = response.data
  694 + } else {
  695 + this.monthlyTrendData = []
  696 + }
  697 + this.$nextTick(() => {
  698 + this.renderTrendChart()
  699 + this.renderCompareChart()
  700 + })
  701 + } catch (error) {
  702 + console.error('加载业绩趋势数据失败:', error)
  703 + this.monthlyTrendData = []
  704 + this.$nextTick(() => {
  705 + this.renderTrendChart()
  706 + this.renderCompareChart()
  707 + })
  708 + }
  709 + },
  710 + // 加载品项分类占比数据
  711 + async loadCategoryData() {
  712 + if (!this.queryParams.storeId) {
  713 + this.categoryData = []
  714 + this.renderCategoryChart()
  715 + return
  716 + }
  717 +
  718 + try {
  719 + const statisticsMonth = this.queryParams.month.replace('-', '')
  720 + const response = await getStoreItemAnalysis({
  721 + storeId: this.queryParams.storeId,
  722 + statisticsMonth: statisticsMonth
  723 + })
  724 +
  725 + if (response.code === 200 && response.data && response.data.CategoryRatios) {
  726 + this.categoryData = response.data.CategoryRatios
  727 + } else {
  728 + this.categoryData = []
  729 + }
  730 + this.renderCategoryChart()
  731 + } catch (error) {
  732 + console.error('加载品项分类数据失败:', error)
  733 + this.categoryData = []
  734 + this.renderCategoryChart()
  735 + }
  736 + },
  737 + // 加载会员分析数据
  738 + async loadMemberAnalysisData() {
  739 + if (!this.queryParams.storeId) {
  740 + this.updateMemberList(null)
  741 + return
  742 + }
  743 +
  744 + try {
  745 + const statisticsMonth = this.queryParams.month.replace('-', '')
  746 + const response = await getStoreMemberAnalysis({
  747 + storeId: this.queryParams.storeId,
  748 + statisticsMonth: statisticsMonth
  749 + })
  750 +
  751 + if (response.code === 200 && response.data) {
  752 + this.updateMemberList(response.data)
  753 + } else {
  754 + this.updateMemberList(null)
  755 + }
  756 + } catch (error) {
  757 + console.error('加载会员分析数据失败:', error)
  758 + this.updateMemberList(null)
  759 + }
  760 + },
  761 + // 加载各分类月度业绩数据
  762 + async loadCategoryMonthlyData() {
  763 + if (!this.queryParams.storeId) {
  764 + this.categoryMonthlyData = []
  765 + this.$nextTick(() => {
  766 + this.renderStackedChart()
  767 + })
  768 + return
  769 + }
  770 +
  771 + try {
  772 + const statisticsMonth = this.queryParams.month.replace('-', '')
  773 + const response = await getCategoryMonthlyPerformance({
  774 + storeId: this.queryParams.storeId,
  775 + statisticsMonth: statisticsMonth
  776 + })
  777 +
  778 + if (response.code === 200 && response.data && response.data.length > 0) {
  779 + this.categoryMonthlyData = response.data
  780 + } else {
  781 + this.categoryMonthlyData = []
  782 + }
  783 + this.$nextTick(() => {
  784 + this.renderStackedChart()
  785 + })
  786 + } catch (error) {
  787 + console.error('加载各分类月度业绩数据失败:', error)
  788 + this.categoryMonthlyData = []
  789 + this.$nextTick(() => {
  790 + this.renderStackedChart()
  791 + })
  792 + }
  793 + },
  794 + // 加载会员转化漏斗数据
  795 + async loadFunnelData() {
  796 + if (!this.queryParams.storeId) {
  797 + // 如果没有选择门店,设置为全0
  798 + this.funnelData = {
  799 + ExpansionCount: 0,
  800 + InviteCount: 0,
  801 + AppointmentCount: 0,
  802 + ConsumeCount: 0,
  803 + BillingCount: 0
  804 + }
  805 + this.$nextTick(() => {
  806 + this.renderFunnelChart()
  807 + })
  808 + return
  809 + }
  810 +
  811 + try {
  812 + const statisticsMonth = this.queryParams.month.replace('-', '')
  813 + const response = await getMemberConversionFunnel({
  814 + storeId: this.queryParams.storeId,
  815 + statisticsMonth: statisticsMonth
  816 + })
  817 +
  818 + if (response.code === 200 && response.data) {
  819 + // 确保所有字段都是0(如果没有数据)
  820 + this.funnelData = {
  821 + ExpansionCount: response.data.ExpansionCount || 0,
  822 + InviteCount: response.data.InviteCount || 0,
  823 + AppointmentCount: response.data.AppointmentCount || 0,
  824 + ConsumeCount: 0, // 不再使用
  825 + BillingCount: response.data.BillingCount || 0
  826 + }
  827 + } else {
  828 + // 如果接口失败,设置为全0
  829 + this.funnelData = {
  830 + ExpansionCount: 0,
  831 + InviteCount: 0,
  832 + AppointmentCount: 0,
  833 + ConsumeCount: 0,
  834 + BillingCount: 0
  835 + }
  836 + }
  837 + this.$nextTick(() => {
  838 + this.renderFunnelChart()
  839 + })
  840 + } catch (error) {
  841 + console.error('加载拓客转化漏斗数据失败:', error)
  842 + // 出错时设置为全0
  843 + this.funnelData = {
  844 + ExpansionCount: 0,
  845 + InviteCount: 0,
  846 + AppointmentCount: 0,
  847 + ConsumeCount: 0,
  848 + BillingCount: 0
  849 + }
  850 + this.$nextTick(() => {
  851 + this.renderFunnelChart()
  852 + })
  853 + }
  854 + },
  855 + // 加载客单价与项目数关系数据
  856 + async loadScatterData() {
  857 + if (!this.queryParams.storeId) {
  858 + this.scatterData = []
  859 + this.$nextTick(() => {
  860 + this.renderScatterChart()
  861 + })
  862 + return
  863 + }
  864 +
  865 + try {
  866 + const statisticsMonth = this.queryParams.month.replace('-', '')
  867 + const response = await getCustomerPriceProjectRelation({
  868 + storeId: this.queryParams.storeId,
  869 + statisticsMonth: statisticsMonth
  870 + })
  871 +
  872 + if (response.code === 200 && response.data && response.data.length > 0) {
  873 + this.scatterData = response.data
  874 + } else {
  875 + this.scatterData = []
  876 + }
  877 + this.$nextTick(() => {
  878 + this.renderScatterChart()
  879 + })
  880 + } catch (error) {
  881 + console.error('加载客单价与项目数关系数据失败:', error)
  882 + this.scatterData = []
  883 + this.$nextTick(() => {
  884 + this.renderScatterChart()
  885 + })
  886 + }
  887 + },
  888 + // 加载门店排名对比数据
  889 + async loadComparisonData() {
  890 + if (!this.queryParams.storeId) {
  891 + this.comparison = {
  892 + performanceRanking: 0,
  893 + totalStoreCount: 0,
  894 + avgPerformanceSameType: 0,
  895 + sameTypeStoreCount: 0,
  896 + avgPerformanceSameOrg: 0,
  897 + sameOrgStoreCount: 0
  898 + }
  899 + return
  900 + }
  901 +
  902 + try {
  903 + const statisticsMonth = this.queryParams.month.replace('-', '')
  904 + const response = await getStoreComparisonAnalysis({
  905 + storeId: this.queryParams.storeId,
  906 + statisticsMonth: statisticsMonth
  907 + })
  908 +
  909 + if (response.code === 200 && response.data) {
  910 + this.comparison = {
  911 + performanceRanking: response.data.PerformanceRanking || 0,
  912 + totalStoreCount: response.data.TotalStoreCount || 0,
  913 + avgPerformanceSameType: response.data.AvgPerformanceSameType || 0,
  914 + sameTypeStoreCount: response.data.SameTypeStoreCount || 0,
  915 + avgPerformanceSameOrg: response.data.AvgPerformanceSameOrg || 0,
  916 + sameOrgStoreCount: response.data.SameOrgStoreCount || 0
  917 + }
  918 + } else {
  919 + this.comparison = {
  920 + performanceRanking: 0,
  921 + totalStoreCount: 0,
  922 + avgPerformanceSameType: 0,
  923 + sameTypeStoreCount: 0,
  924 + avgPerformanceSameOrg: 0,
  925 + sameOrgStoreCount: 0
  926 + }
  927 + }
  928 + } catch (error) {
  929 + console.error('加载门店排名对比数据失败:', error)
  930 + this.comparison = {
  931 + performanceRanking: 0,
  932 + totalStoreCount: 0,
  933 + avgPerformanceSameType: 0,
  934 + sameTypeStoreCount: 0,
  935 + avgPerformanceSameOrg: 0,
  936 + sameOrgStoreCount: 0
  937 + }
  938 + }
  939 + },
  940 + // 加载一周运营热力图数据
  941 + async loadHeatmapData() {
  942 + if (!this.queryParams.storeId) {
  943 + this.heatmapData = []
  944 + this.$nextTick(() => {
  945 + this.renderHeatmapChart()
  946 + })
  947 + return
  948 + }
  949 +
  950 + try {
  951 + const statisticsMonth = this.queryParams.month.replace('-', '')
  952 + const response = await getWeeklyHeatmap({
  953 + storeId: this.queryParams.storeId,
  954 + statisticsMonth: statisticsMonth
  955 + })
  956 +
  957 + if (response.code === 200 && response.data && response.data.length > 0) {
  958 + this.heatmapData = response.data
  959 + } else {
  960 + this.heatmapData = []
  961 + }
  962 + this.$nextTick(() => {
  963 + this.renderHeatmapChart()
  964 + })
  965 + } catch (error) {
  966 + console.error('加载一周运营热力图数据失败:', error)
  967 + this.heatmapData = []
  968 + this.$nextTick(() => {
  969 + this.renderHeatmapChart()
  970 + })
  971 + }
  972 + },
  973 + // 加载品项开单排行数据
  974 + async loadTopBillingItems() {
  975 + if (!this.queryParams.storeId) {
  976 + this.topBillingItems = []
  977 + return
  978 + }
  979 +
  980 + try {
  981 + const statisticsMonth = this.queryParams.month.replace('-', '')
  982 + const response = await getStoreItemAnalysis({
  983 + storeId: this.queryParams.storeId,
  984 + statisticsMonth: statisticsMonth
  985 + })
  986 +
  987 + if (response.code === 200 && response.data && response.data.TopBillingItems) {
  988 + this.topBillingItems = response.data.TopBillingItems.map(item => ({
  989 + itemName: item.ItemName || '未知品项',
  990 + billingAmount: item.BillingAmount || 0,
  991 + billingCount: item.BillingCount || 0,
  992 + category: item.Category || '其他'
  993 + }))
  994 + } else {
  995 + this.topBillingItems = []
  996 + }
  997 + } catch (error) {
  998 + console.error('加载品项开单排行数据失败:', error)
  999 + this.topBillingItems = []
  1000 + }
  1001 + },
  1002 + // 加载消耗品项排行数据
  1003 + async loadTopConsumeItems() {
  1004 + if (!this.queryParams.storeId) {
  1005 + this.topConsumeItems = []
  1006 + return
  1007 + }
  1008 +
  1009 + try {
  1010 + const statisticsMonth = this.queryParams.month.replace('-', '')
  1011 + const response = await getStoreItemAnalysis({
  1012 + storeId: this.queryParams.storeId,
  1013 + statisticsMonth: statisticsMonth
  1014 + })
  1015 +
  1016 + if (response.code === 200 && response.data && response.data.TopConsumeItems) {
  1017 + this.topConsumeItems = response.data.TopConsumeItems.map(item => ({
  1018 + itemName: item.ItemName || '未知品项',
  1019 + consumeAmount: item.ConsumeAmount || 0,
  1020 + category: item.Category || '其他'
  1021 + }))
  1022 + } else {
  1023 + this.topConsumeItems = []
  1024 + }
  1025 + } catch (error) {
  1026 + console.error('加载消耗品项排行数据失败:', error)
  1027 + this.topConsumeItems = []
  1028 + }
  1029 + },
  1030 + // 加载健康师业绩排行数据
  1031 + async loadHealthCoachRanking() {
  1032 + if (!this.queryParams.storeId) return
  1033 +
  1034 + try {
  1035 + const statisticsMonth = this.queryParams.month.replace('-', '')
  1036 + const response = await getStoreHealthCoachAnalysis({
  1037 + storeId: this.queryParams.storeId,
  1038 + statisticsMonth: statisticsMonth
  1039 + })
  1040 +
  1041 + if (response.code === 200 && response.data && response.data.length > 0) {
  1042 + this.healthCoachRanking = response.data.map(item => ({
  1043 + name: item.HealthCoachName || '未知',
  1044 + billingPerformance: item.BillingPerformance || 0,
  1045 + consumePerformance: item.ConsumePerformance || 0,
  1046 + totalPerformance: item.NetPerformance || 0 // 使用净业绩
  1047 + }))
  1048 + }
  1049 + } catch (error) {
  1050 + console.error('加载健康师业绩排行数据失败:', error)
  1051 + }
  1052 + },
  1053 + // 更新显示数据
  1054 + updateDisplayData() {
  1055 + if (!this.storeData || !this.storeData.Performance || !this.storeData.Operation) {
  1056 + // 如果没有数据,使用默认值或清空
  1057 + this.updateCoreStats(null)
  1058 + this.updatePerformanceList(null)
  1059 + this.updateOperationList(null)
  1060 + return
  1061 + }
  1062 +
  1063 + const perf = this.storeData.Performance
  1064 + const oper = this.storeData.Operation
  1065 +
  1066 + // 更新顶部核心指标
  1067 + this.updateCoreStats(perf, oper)
  1068 +
  1069 + // 更新业绩概览列表
  1070 + this.updatePerformanceList(perf)
  1071 +
  1072 + // 更新运营指标列表
  1073 + this.updateOperationList(oper)
  1074 +
  1075 + // 更新目标完成度图表
  1076 + this.renderGaugeChart()
  1077 +
  1078 + // 更新快速数据洞察、本月关键指标、本月经营提示
  1079 + this.updateDataInsights()
  1080 + this.updateKeyMetrics()
  1081 + this.updateOperationTips()
  1082 + },
  1083 + // 更新顶部核心指标
  1084 + updateCoreStats(perf, oper) {
  1085 + if (!perf) {
  1086 + // 使用默认值
  1087 + return
  1088 + }
  1089 +
  1090 + // 这里的数据会在模板中直接使用 storeData,所以不需要更新
  1091 + },
  1092 + // 更新业绩概览列表
  1093 + updatePerformanceList(perf) {
  1094 + if (!perf) {
  1095 + this.performanceList = [
  1096 + { label: '开单次数', value: '0', icon: 'el-icon-document', iconBg: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)' },
  1097 + { label: '消耗次数', value: '0', icon: 'el-icon-goods', iconBg: 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)' },
  1098 + { label: '退卡次数', value: '0', icon: 'el-icon-refresh-left', iconBg: 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)' },
  1099 + { label: '平均开单金额', value: '¥0', icon: 'el-icon-coin', iconBg: 'linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)' },
  1100 + { label: '平均消耗金额', value: '¥0', icon: 'el-icon-coin', iconBg: 'linear-gradient(135deg, #fa709a 0%, #fee140 100%)' },
  1101 + { label: '剩余权益', value: '¥0', icon: 'el-icon-wallet', iconBg: 'linear-gradient(135deg, #30cfd0 0%, #330867 100%)' },
  1102 + { label: '目标业绩', value: '¥0', icon: 'el-icon-aim', iconBg: 'linear-gradient(135deg, #a8edea 0%, #fed6e3 100%)' },
  1103 + { label: '退卡金额', value: '¥0', icon: 'el-icon-money', iconBg: 'linear-gradient(135deg, #ff9a9e 0%, #fecfef 100%)' }
  1104 + ]
  1105 + return
  1106 + }
  1107 +
  1108 + this.performanceList = [
  1109 + { label: '开单次数', value: this.formatNumber(perf.BillingCount), icon: 'el-icon-document', iconBg: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)' },
  1110 + { label: '消耗次数', value: this.formatNumber(perf.ConsumeCount), icon: 'el-icon-goods', iconBg: 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)' },
  1111 + { label: '退卡次数', value: this.formatNumber(perf.RefundCount), icon: 'el-icon-refresh-left', iconBg: 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)' },
  1112 + { label: '平均开单金额', value: '¥' + this.formatMoney(perf.AvgBillingAmount), icon: 'el-icon-coin', iconBg: 'linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)' },
  1113 + { label: '平均消耗金额', value: '¥' + this.formatMoney(perf.AvgConsumeAmount), icon: 'el-icon-coin', iconBg: 'linear-gradient(135deg, #fa709a 0%, #fee140 100%)' },
  1114 + { label: '剩余权益', value: '¥' + this.formatMoney(perf.RemainingRightsAmount), icon: 'el-icon-wallet', iconBg: 'linear-gradient(135deg, #30cfd0 0%, #330867 100%)' },
  1115 + { label: '目标业绩', value: '¥' + this.formatMoney(perf.TargetPerformance), icon: 'el-icon-aim', iconBg: 'linear-gradient(135deg, #a8edea 0%, #fed6e3 100%)' },
  1116 + { label: '退卡金额', value: '¥' + this.formatMoney(perf.RefundAmount), icon: 'el-icon-money', iconBg: 'linear-gradient(135deg, #ff9a9e 0%, #fecfef 100%)' }
  1117 + ]
  1118 + },
  1119 + // 更新运营指标列表
  1120 + updateOperationList(oper) {
  1121 + if (!oper) {
  1122 + this.operationList = [
  1123 + { label: '人头数', value: '0', icon: 'el-icon-user', iconBg: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)' },
  1124 + { label: '人次', value: '0', icon: 'el-icon-user-solid', iconBg: 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)' },
  1125 + { label: '项目数', value: '0', icon: 'el-icon-menu', iconBg: 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)' },
  1126 + { label: '客单价', value: '¥0', icon: 'el-icon-coin', iconBg: 'linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)' },
  1127 + { label: '项目单价', value: '¥0', icon: 'el-icon-coin', iconBg: 'linear-gradient(135deg, #fa709a 0%, #fee140 100%)' },
  1128 + { label: '人均项目数', value: '0', icon: 'el-icon-s-grid', iconBg: 'linear-gradient(135deg, #30cfd0 0%, #330867 100%)' }
  1129 + ]
  1130 + return
  1131 + }
  1132 +
  1133 + this.operationList = [
  1134 + { label: '人头数', value: this.formatNumber(oper.HeadCount), icon: 'el-icon-user', iconBg: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)' },
  1135 + { label: '人次', value: this.formatNumber(oper.PersonCount), icon: 'el-icon-user-solid', iconBg: 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)' },
  1136 + { label: '项目数', value: this.formatNumber(oper.ProjectCount), icon: 'el-icon-menu', iconBg: 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)' },
  1137 + { label: '客单价', value: '¥' + this.formatMoney(oper.AvgAmountPerPerson), icon: 'el-icon-coin', iconBg: 'linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)' },
  1138 + { label: '项目单价', value: '¥' + this.formatMoney(oper.AvgAmountPerProject), icon: 'el-icon-coin', iconBg: 'linear-gradient(135deg, #fa709a 0%, #fee140 100%)' },
  1139 + { label: '人均项目数', value: this.formatNumber(oper.AvgProjectPerHead, 2), icon: 'el-icon-s-grid', iconBg: 'linear-gradient(135deg, #30cfd0 0%, #330867 100%)' }
  1140 + ]
  1141 + },
  1142 + // 更新会员分析列表
  1143 + updateMemberList(memberData) {
  1144 + if (!memberData) {
  1145 + this.memberList = [
  1146 + { label: '总会员数', value: '0', icon: 'el-icon-user', iconBg: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)' },
  1147 + { label: '本月新增', value: '0', icon: 'el-icon-user-solid', iconBg: 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)' },
  1148 + { label: '活跃会员', value: '0', icon: 'el-icon-success', iconBg: 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)', rate: '0%' },
  1149 + { label: '沉睡会员', value: '0', icon: 'el-icon-warning', iconBg: 'linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)', rate: '0%' },
  1150 + { label: '生美会员', value: '0', icon: 'el-icon-star-on', iconBg: 'linear-gradient(135deg, #fa709a 0%, #fee140 100%)' },
  1151 + { label: '医美会员', value: '0', icon: 'el-icon-star-on', iconBg: 'linear-gradient(135deg, #30cfd0 0%, #330867 100%)' },
  1152 + { label: '科美会员', value: '0', icon: 'el-icon-star-on', iconBg: 'linear-gradient(135deg, #a8edea 0%, #fed6e3 100%)' },
  1153 + { label: '教育会员', value: '0', icon: 'el-icon-star-on', iconBg: 'linear-gradient(135deg, #ff9a9e 0%, #fecfef 100%)' }
  1154 + ]
  1155 + return
  1156 + }
  1157 +
  1158 + const totalMembers = memberData.TotalMembers || 0
  1159 + const activeRate = memberData.ActiveMemberRate || 0
  1160 + const sleepRate = memberData.SleepMemberRate || 0
  1161 +
  1162 + this.memberList = [
  1163 + { label: '总会员数', value: this.formatNumber(totalMembers), icon: 'el-icon-user', iconBg: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)' },
  1164 + { label: '本月新增', value: this.formatNumber(memberData.NewMembersThisMonth || 0), icon: 'el-icon-user-solid', iconBg: 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)' },
  1165 + { label: '活跃会员', value: this.formatNumber(memberData.ActiveMembers || 0), icon: 'el-icon-success', iconBg: 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)', rate: activeRate.toFixed(1) + '%' },
  1166 + { label: '沉睡会员', value: this.formatNumber(memberData.SleepMembers || 0), icon: 'el-icon-warning', iconBg: 'linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)', rate: sleepRate.toFixed(1) + '%' },
  1167 + { label: '生美会员', value: this.formatNumber(memberData.BeautyMembers || 0), icon: 'el-icon-star-on', iconBg: 'linear-gradient(135deg, #fa709a 0%, #fee140 100%)' },
  1168 + { label: '医美会员', value: this.formatNumber(memberData.MedicalMembers || 0), icon: 'el-icon-star-on', iconBg: 'linear-gradient(135deg, #30cfd0 0%, #330867 100%)' },
  1169 + { label: '科美会员', value: this.formatNumber(memberData.TechMembers || 0), icon: 'el-icon-star-on', iconBg: 'linear-gradient(135deg, #a8edea 0%, #fed6e3 100%)' },
  1170 + { label: '教育会员', value: this.formatNumber(memberData.EducationMembers || 0), icon: 'el-icon-star-on', iconBg: 'linear-gradient(135deg, #ff9a9e 0%, #fecfef 100%)' }
  1171 + ]
  1172 + },
  1173 + // 格式化数字(添加千分位)
  1174 + formatNumber(value, decimals = 0) {
  1175 + if (value === null || value === undefined) return '0'
  1176 + const num = Number(value)
  1177 + if (isNaN(num)) return '0'
  1178 + return num.toLocaleString('zh-CN', { minimumFractionDigits: decimals, maximumFractionDigits: decimals })
  1179 + },
  1180 + // 更新快速数据洞察
  1181 + updateDataInsights() {
  1182 + if (!this.storeData || !this.storeData.Operation || !this.heatmapData || this.heatmapData.length === 0) {
  1183 + this.dataInsights = [
  1184 + { title: '最佳营业时段', tag: '暂无', tagType: 'info', value: '暂无数据', desc: '暂无数据' },
  1185 + { title: '高价值会员', tag: '暂无', tagType: 'info', value: '0人', desc: '暂无数据' },
  1186 + { title: '项目转化率', tag: '暂无', tagType: 'info', value: '0%', desc: '暂无数据' },
  1187 + { title: '复购周期', tag: '暂无', tagType: 'info', value: '暂无', desc: '暂无数据' }
  1188 + ]
  1189 + return
  1190 + }
  1191 +
  1192 + const oper = this.storeData.Operation
  1193 + const perf = this.storeData.Performance
  1194 +
  1195 + // 1. 最佳营业时段(从热力图数据中找)
  1196 + let bestHour = 0
  1197 + let maxPersonCount = 0
  1198 + this.heatmapData.forEach(item => {
  1199 + if (item.PersonCount > maxPersonCount) {
  1200 + maxPersonCount = item.PersonCount
  1201 + bestHour = item.Hour
  1202 + }
  1203 + })
  1204 + const bestTimeRange = bestHour >= 0 && maxPersonCount > 0
  1205 + ? `${String(bestHour).padStart(2, '0')}:00-${String(bestHour + 1).padStart(2, '0')}:00`
  1206 + : '暂无数据'
  1207 +
  1208 + // 2. 高价值会员(客单价超过1000的会员数,这里用估算)
  1209 + const highValueMemberCount = oper.AvgAmountPerPerson > 1000
  1210 + ? Math.round(oper.HeadCount * 0.3) // 估算30%为高价值会员
  1211 + : 0
  1212 +
  1213 + // 3. 项目转化率(预约转化为开单的比例,这里用估算)
  1214 + const conversionRate = this.funnelData && this.funnelData.ExpansionCount > 0
  1215 + ? ((this.funnelData.BillingCount / this.funnelData.ExpansionCount) * 100).toFixed(1)
  1216 + : '0.0'
  1217 +
  1218 + // 4. 复购周期(估算,基于平均项目数和人均项目数)
  1219 + const avgProjectPerHead = oper.AvgProjectPerHead || 0
  1220 + const repurchaseCycle = avgProjectPerHead > 0
  1221 + ? Math.round(30 / avgProjectPerHead) + '天'
  1222 + : '暂无'
  1223 +
  1224 + this.dataInsights = [
  1225 + {
  1226 + title: '最佳营业时段',
  1227 + tag: maxPersonCount > 0 ? '热门' : '暂无',
  1228 + tagType: maxPersonCount > 0 ? 'danger' : 'info',
  1229 + value: bestTimeRange,
  1230 + desc: maxPersonCount > 0 ? `此时段客流量最高(${maxPersonCount}人次),建议配置更多人手` : '暂无数据'
  1231 + },
  1232 + {
  1233 + title: '高价值会员',
  1234 + tag: highValueMemberCount > 0 ? '重点' : '暂无',
  1235 + tagType: highValueMemberCount > 0 ? 'warning' : 'info',
  1236 + value: highValueMemberCount > 0 ? `${highValueMemberCount}人` : '0人',
  1237 + desc: highValueMemberCount > 0 ? `单次消费超过¥1000,需重点维护` : '暂无数据'
  1238 + },
  1239 + {
  1240 + title: '项目转化率',
  1241 + tag: parseFloat(conversionRate) > 50 ? '优秀' : parseFloat(conversionRate) > 30 ? '良好' : '待提升',
  1242 + tagType: parseFloat(conversionRate) > 50 ? 'success' : parseFloat(conversionRate) > 30 ? 'warning' : 'info',
  1243 + value: conversionRate + '%',
  1244 + desc: `拓客转化为开单的比例`
  1245 + },
  1246 + {
  1247 + title: '复购周期',
  1248 + tag: repurchaseCycle !== '暂无' ? '正常' : '暂无',
  1249 + tagType: repurchaseCycle !== '暂无' ? 'info' : 'info',
  1250 + value: repurchaseCycle,
  1251 + desc: repurchaseCycle !== '暂无' ? `会员平均复购间隔,保持稳定` : '暂无数据'
  1252 + }
  1253 + ]
  1254 + },
  1255 + // 更新本月关键指标
  1256 + updateKeyMetrics() {
  1257 + if (!this.storeData || !this.storeData.Performance) {
  1258 + this.keyMetrics = [
  1259 + { label: '目标完成度', value: 0, color: '#67C23A' },
  1260 + { label: '会员活跃度', value: 0, color: '#409EFF' },
  1261 + { label: '项目满意度', value: 0, color: '#E6A23C' },
  1262 + { label: '员工效率', value: 0, color: '#F56C6C' }
  1263 + ]
  1264 + return
  1265 + }
  1266 +
  1267 + const perf = this.storeData.Performance
  1268 + const oper = this.storeData.Operation
  1269 + const memberData = this.memberList && this.memberList.length > 0 ? this.memberList : null
  1270 +
  1271 + // 1. 目标完成度(消耗业绩/目标业绩)
  1272 + const completionRate = perf.TargetPerformance > 0
  1273 + ? Math.min(100, parseFloat((perf.ConsumePerformance / perf.TargetPerformance * 100).toFixed(1)))
  1274 + : 0
  1275 +
  1276 + // 2. 会员活跃度(活跃会员/总会员数)
  1277 + const activeMember = memberData && memberData.length > 0
  1278 + ? memberData.find(m => m.label === '活跃会员')
  1279 + : null
  1280 + const activeMemberRate = activeMember && activeMember.rate
  1281 + ? parseFloat(activeMember.rate)
  1282 + : 0
  1283 +
  1284 + // 3. 项目满意度(估算,基于退卡率,退卡率越低满意度越高)
  1285 + const refundRate = perf.BillingPerformance > 0
  1286 + ? (perf.RefundAmount / perf.BillingPerformance * 100)
  1287 + : 0
  1288 + const satisfactionRate = Math.max(0, Math.min(100, parseFloat((100 - refundRate * 10).toFixed(1))))
  1289 +
  1290 + // 4. 员工效率(基于人均项目数,估算)
  1291 + const avgProjectPerHead = oper.AvgProjectPerHead || 0
  1292 + const efficiencyRate = Math.min(100, parseFloat((avgProjectPerHead * 10).toFixed(1)))
  1293 +
  1294 + this.keyMetrics = [
  1295 + { label: '目标完成度', value: completionRate, color: completionRate >= 100 ? '#67C23A' : completionRate >= 80 ? '#E6A23C' : '#F56C6C' },
  1296 + { label: '会员活跃度', value: activeMemberRate, color: activeMemberRate >= 60 ? '#67C23A' : activeMemberRate >= 40 ? '#409EFF' : '#909399' },
  1297 + { label: '项目满意度', value: satisfactionRate, color: satisfactionRate >= 90 ? '#67C23A' : satisfactionRate >= 70 ? '#E6A23C' : '#F56C6C' },
  1298 + { label: '员工效率', value: efficiencyRate, color: efficiencyRate >= 80 ? '#67C23A' : efficiencyRate >= 60 ? '#409EFF' : '#F56C6C' }
  1299 + ]
  1300 + },
  1301 + // 更新本月经营提示
  1302 + updateOperationTips() {
  1303 + if (!this.storeData || !this.storeData.Performance) {
  1304 + this.operationTips = []
  1305 + return
  1306 + }
  1307 +
  1308 + const perf = this.storeData.Performance
  1309 + const oper = this.storeData.Operation
  1310 + const memberData = this.memberList && this.memberList.length > 0 ? this.memberList : null
  1311 +
  1312 + const tips = []
  1313 +
  1314 + // 1. 目标完成度提示
  1315 + const completionRate = perf.TargetPerformance > 0
  1316 + ? (perf.ConsumePerformance / perf.TargetPerformance * 100)
  1317 + : 0
  1318 + if (completionRate >= 100) {
  1319 + tips.push({ type: 'success', icon: 'el-icon-success', text: `本月业绩完成度${completionRate.toFixed(1)}%,超额完成目标,继续保持` })
  1320 + } else if (completionRate >= 80) {
  1321 + tips.push({ type: 'success', icon: 'el-icon-success', text: `本月业绩完成度${completionRate.toFixed(1)}%,保持当前节奏` })
  1322 + } else if (completionRate >= 60) {
  1323 + tips.push({ type: 'warning', icon: 'el-icon-warning', text: `本月业绩完成度${completionRate.toFixed(1)}%,需加快进度` })
  1324 + } else {
  1325 + tips.push({ type: 'danger', icon: 'el-icon-error', text: `本月业绩完成度${completionRate.toFixed(1)}%,严重滞后,需立即采取措施` })
  1326 + }
  1327 +
  1328 + // 2. 沉睡会员提示
  1329 + if (memberData && memberData.length > 0) {
  1330 + const sleepMember = memberData.find(m => m.label === '沉睡会员')
  1331 + if (sleepMember && parseFloat(sleepMember.rate || '0') > 20) {
  1332 + tips.push({ type: 'warning', icon: 'el-icon-warning', text: `沉睡会员占比${sleepMember.rate},建议加强会员唤醒` })
  1333 + }
  1334 + }
  1335 +
  1336 + // 3. 客单价提示
  1337 + if (oper.AvgAmountPerPerson > 0) {
  1338 + const avgPrice = oper.AvgAmountPerPerson
  1339 + if (avgPrice < 300) {
  1340 + tips.push({ type: 'info', icon: 'el-icon-info', text: `客单价¥${avgPrice.toFixed(0)},可通过项目组合提升` })
  1341 + } else if (avgPrice > 800) {
  1342 + tips.push({ type: 'success', icon: 'el-icon-success', text: `客单价¥${avgPrice.toFixed(0)},表现优秀` })
  1343 + }
  1344 + }
  1345 +
  1346 + // 4. 退卡金额提示
  1347 + if (perf.RefundAmount > 0 && perf.BillingPerformance > 0) {
  1348 + const refundRate = (perf.RefundAmount / perf.BillingPerformance * 100)
  1349 + if (refundRate > 5) {
  1350 + tips.push({ type: 'warning', icon: 'el-icon-warning', text: `退卡金额${this.formatMoney(perf.RefundAmount)},退卡率${refundRate.toFixed(1)}%,需关注服务质量` })
  1351 + }
  1352 + }
  1353 +
  1354 + // 5. 如果提示少于4条,补充一些通用提示
  1355 + if (tips.length < 4) {
  1356 + if (oper.HeadCount > 0) {
  1357 + tips.push({ type: 'info', icon: 'el-icon-info', text: `本月服务${oper.HeadCount}位会员,${oper.PersonCount}人次` })
  1358 + }
  1359 + }
  1360 +
  1361 + this.operationTips = tips.slice(0, 4) // 最多显示4条
  1362 + },
574 1363 initCharts() {
575 1364 this.$nextTick(() => {
576 1365 this.renderTrendChart()
... ... @@ -580,33 +1369,113 @@ export default {
580 1369 this.renderFunnelChart()
581 1370 this.renderScatterChart()
582 1371 this.renderHeatmapChart()
583   - this.renderRadarChart()
584 1372 this.renderGaugeChart()
585   - this.renderStackedAreaChart()
586 1373 })
587 1374 },
588 1375 renderTrendChart() {
589 1376 if (!this.$refs.trendChart) return
590   - this.trendChart = echarts.init(this.$refs.trendChart)
  1377 + if (!this.trendChart) {
  1378 + this.trendChart = echarts.init(this.$refs.trendChart)
  1379 + }
  1380 +
  1381 + // 如果没有数据,使用空数据
  1382 + if (!this.monthlyTrendData || this.monthlyTrendData.length === 0) {
  1383 + const option = {
  1384 + tooltip: { trigger: 'axis', axisPointer: { type: 'cross' } },
  1385 + legend: { data: ['开单业绩', '消耗业绩', '净业绩'], top: 10 },
  1386 + grid: { left: '3%', right: '4%', bottom: '3%', top: '15%', containLabel: true },
  1387 + xAxis: { type: 'category', data: [] },
  1388 + yAxis: { type: 'value', axisLabel: { formatter: '¥{value}' } },
  1389 + series: [
  1390 + { name: '开单业绩', type: 'line', smooth: true, data: [], itemStyle: { color: '#409EFF' }, areaStyle: { color: 'rgba(64, 158, 255, 0.1)' } },
  1391 + { name: '消耗业绩', type: 'line', smooth: true, data: [], itemStyle: { color: '#67C23A' }, areaStyle: { color: 'rgba(103, 194, 58, 0.1)' } },
  1392 + { name: '净业绩', type: 'line', smooth: true, data: [], itemStyle: { color: '#E6A23C' }, areaStyle: { color: 'rgba(230, 162, 60, 0.1)' } }
  1393 + ]
  1394 + }
  1395 + this.trendChart.setOption(option)
  1396 + return
  1397 + }
  1398 +
  1399 + // 格式化月份显示(从 YYYYMM 转为 YYYY-MM)
  1400 + const months = this.monthlyTrendData.map(item => {
  1401 + const month = item.Month || ''
  1402 + if (month.length === 6) {
  1403 + return month.substring(0, 4) + '-' + month.substring(4, 6)
  1404 + }
  1405 + return month
  1406 + })
  1407 + const billingData = this.monthlyTrendData.map(item => item.BillingPerformance || 0)
  1408 + const consumeData = this.monthlyTrendData.map(item => item.ConsumePerformance || 0)
  1409 + const netData = this.monthlyTrendData.map(item => item.NetPerformance || 0)
  1410 +
591 1411 const option = {
592 1412 tooltip: { trigger: 'axis', axisPointer: { type: 'cross' } },
593 1413 legend: { data: ['开单业绩', '消耗业绩', '净业绩'], top: 10 },
594 1414 grid: { left: '3%', right: '4%', bottom: '3%', top: '15%', containLabel: true },
595   - xAxis: { type: 'category', data: ['2024-01', '2024-02', '2024-03', '2024-04', '2024-05', '2024-06', '2024-07', '2024-08', '2024-09', '2024-10', '2024-11', '2024-12'] },
596   - yAxis: { type: 'value', axisLabel: { formatter: '¥{value}' } },
  1415 + xAxis: { type: 'category', data: months },
  1416 + yAxis: { type: 'value', axisLabel: { formatter: value => value >= 10000 ? (value / 10000).toFixed(1) + '万' : value } },
597 1417 series: [
598   - { name: '开单业绩', type: 'line', smooth: true, data: [856000, 928000, 1025000, 1156000, 1089000, 1125000, 1186000, 1258000, 1156000, 1289000, 1356000, 1258680], itemStyle: { color: '#409EFF' }, areaStyle: { color: 'rgba(64, 158, 255, 0.1)' } },
599   - { name: '消耗业绩', type: 'line', smooth: true, data: [658000, 712000, 786000, 856000, 798000, 825000, 868000, 912000, 856000, 936000, 986000, 986420], itemStyle: { color: '#67C23A' }, areaStyle: { color: 'rgba(103, 194, 58, 0.1)' } },
600   - { name: '净业绩', type: 'line', smooth: true, data: [198000, 216000, 239000, 300000, 291000, 300000, 318000, 346000, 300000, 353000, 370000, 272260], itemStyle: { color: '#E6A23C' }, areaStyle: { color: 'rgba(230, 162, 60, 0.1)' } }
  1418 + { name: '开单业绩', type: 'line', smooth: true, data: billingData, itemStyle: { color: '#409EFF' }, areaStyle: { color: 'rgba(64, 158, 255, 0.1)' } },
  1419 + { name: '消耗业绩', type: 'line', smooth: true, data: consumeData, itemStyle: { color: '#67C23A' }, areaStyle: { color: 'rgba(103, 194, 58, 0.1)' } },
  1420 + { name: '净业绩', type: 'line', smooth: true, data: netData, itemStyle: { color: '#E6A23C' }, areaStyle: { color: 'rgba(230, 162, 60, 0.1)' } }
601 1421 ]
602 1422 }
603 1423 this.trendChart.setOption(option)
604 1424 },
605 1425 renderCategoryChart() {
606 1426 if (!this.$refs.categoryChart) return
607   - this.categoryChart = echarts.init(this.$refs.categoryChart)
  1427 + if (!this.categoryChart) {
  1428 + this.categoryChart = echarts.init(this.$refs.categoryChart)
  1429 + }
  1430 +
  1431 + // 如果没有数据,使用空数据
  1432 + if (!this.categoryData || this.categoryData.length === 0) {
  1433 + const option = {
  1434 + tooltip: { trigger: 'item' },
  1435 + legend: { show: false },
  1436 + series: [{
  1437 + name: '品项分类',
  1438 + type: 'pie',
  1439 + radius: ['40%', '70%'],
  1440 + center: ['50%', '50%'],
  1441 + avoidLabelOverlap: true,
  1442 + itemStyle: { borderRadius: 6, borderColor: '#fff', borderWidth: 2 },
  1443 + label: { show: true, position: 'outside', formatter: '{b}\n{c}', fontSize: 12 },
  1444 + labelLine: { show: true, length: 15, length2: 10 },
  1445 + data: []
  1446 + }]
  1447 + }
  1448 + this.categoryChart.setOption(option)
  1449 + return
  1450 + }
  1451 +
  1452 + // 定义分类颜色映射
  1453 + const categoryColors = {
  1454 + '生美': '#A8D5E2',
  1455 + '医美': '#B8E6B8',
  1456 + '科美': '#FFD4A3',
  1457 + '产品': '#E6C1E6',
  1458 + '教育': '#F5DEB3',
  1459 + '其他': '#DDA0DD'
  1460 + }
  1461 +
  1462 + // 格式化数据
  1463 + const chartData = this.categoryData.map(item => ({
  1464 + value: item.ConsumeAmount || 0,
  1465 + name: item.CategoryName || '其他',
  1466 + itemStyle: {
  1467 + color: categoryColors[item.CategoryName] || categoryColors['其他'],
  1468 + borderRadius: 6,
  1469 + borderColor: '#fff',
  1470 + borderWidth: 2
  1471 + }
  1472 + }))
  1473 +
608 1474 const option = {
609   - tooltip: { trigger: 'item' },
  1475 + tooltip: {
  1476 + trigger: 'item',
  1477 + formatter: '{b}: ¥{c} ({d}%)'
  1478 + },
610 1479 legend: { show: false },
611 1480 series: [{
612 1481 name: '品项分类',
... ... @@ -614,68 +1483,177 @@ export default {
614 1483 radius: ['40%', '70%'],
615 1484 center: ['50%', '50%'],
616 1485 avoidLabelOverlap: true,
617   - itemStyle: { borderRadius: 6, borderColor: '#fff', borderWidth: 2 },
618   - label: { show: true, position: 'outside', formatter: '{b}\n{c}', fontSize: 12 },
  1486 + label: {
  1487 + show: true,
  1488 + position: 'outside',
  1489 + formatter: (params) => {
  1490 + return params.name + '\n¥' + this.formatMoney(params.value)
  1491 + },
  1492 + fontSize: 12
  1493 + },
619 1494 labelLine: { show: true, length: 15, length2: 10 },
620   - data: [
621   - { value: 456800, name: '生美', itemStyle: { color: '#A8D5E2' } },
622   - { value: 256800, name: '医美', itemStyle: { color: '#B8E6B8' } },
623   - { value: 198600, name: '科美', itemStyle: { color: '#FFD4A3' } },
624   - { value: 74220, name: '产品', itemStyle: { color: '#E6C1E6' } }
625   - ]
  1495 + data: chartData
626 1496 }]
627 1497 }
628 1498 this.categoryChart.setOption(option)
629 1499 },
630 1500 renderCompareChart() {
631 1501 if (!this.$refs.compareChart) return
632   - this.compareChart = echarts.init(this.$refs.compareChart)
  1502 + if (!this.compareChart) {
  1503 + this.compareChart = echarts.init(this.$refs.compareChart)
  1504 + }
  1505 +
  1506 + // 如果没有数据,使用空数据
  1507 + if (!this.monthlyTrendData || this.monthlyTrendData.length === 0) {
  1508 + const option = {
  1509 + tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
  1510 + legend: { data: ['开单业绩', '消耗业绩'], top: 10 },
  1511 + grid: { left: '3%', right: '4%', bottom: '3%', top: '15%', containLabel: true },
  1512 + xAxis: { type: 'category', data: [] },
  1513 + yAxis: { type: 'value', axisLabel: { formatter: value => value >= 1 ? value.toFixed(1) + '万' : value } },
  1514 + series: [
  1515 + { name: '开单业绩', type: 'bar', data: [], itemStyle: { color: '#409EFF' } },
  1516 + { name: '消耗业绩', type: 'bar', data: [], itemStyle: { color: '#67C23A' } }
  1517 + ]
  1518 + }
  1519 + this.compareChart.setOption(option)
  1520 + return
  1521 + }
  1522 +
  1523 + // 格式化月份显示(从 YYYYMM 转为 月份显示)
  1524 + const months = this.monthlyTrendData.map(item => {
  1525 + const month = item.Month || ''
  1526 + if (month.length === 6) {
  1527 + const monthNum = parseInt(month.substring(4, 6))
  1528 + return monthNum + '月'
  1529 + }
  1530 + return month
  1531 + })
  1532 + const billingData = this.monthlyTrendData.map(item => (item.BillingPerformance || 0) / 10000) // 转换为万元
  1533 + const consumeData = this.monthlyTrendData.map(item => (item.ConsumePerformance || 0) / 10000) // 转换为万元
  1534 +
633 1535 const option = {
634 1536 tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
635 1537 legend: { data: ['开单业绩', '消耗业绩'], top: 10 },
636 1538 grid: { left: '3%', right: '4%', bottom: '3%', top: '15%', containLabel: true },
637   - xAxis: { type: 'category', data: ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'] },
638   - yAxis: { type: 'value', axisLabel: { formatter: value => value >= 10000 ? (value / 10000).toFixed(1) + '万' : value } },
  1539 + xAxis: { type: 'category', data: months },
  1540 + yAxis: { type: 'value', axisLabel: { formatter: value => value >= 1 ? value.toFixed(1) + '万' : value } },
639 1541 series: [
640   - { name: '开单业绩', type: 'bar', data: [856, 928, 1025, 1156, 1089, 1125, 1186, 1258, 1156, 1289, 1356, 1258], itemStyle: { color: '#409EFF' } },
641   - { name: '消耗业绩', type: 'bar', data: [658, 712, 786, 856, 798, 825, 868, 912, 856, 936, 986, 986], itemStyle: { color: '#67C23A' } }
  1542 + { name: '开单业绩', type: 'bar', data: billingData, itemStyle: { color: '#409EFF' } },
  1543 + { name: '消耗业绩', type: 'bar', data: consumeData, itemStyle: { color: '#67C23A' } }
642 1544 ]
643 1545 }
644 1546 this.compareChart.setOption(option)
645 1547 },
646 1548 renderStackedChart() {
647 1549 if (!this.$refs.stackedChart) return
648   - this.stackedChart = echarts.init(this.$refs.stackedChart)
  1550 + if (!this.stackedChart) {
  1551 + this.stackedChart = echarts.init(this.$refs.stackedChart)
  1552 + }
  1553 +
  1554 + // 如果没有数据,使用空数据
  1555 + if (!this.categoryMonthlyData || this.categoryMonthlyData.length === 0) {
  1556 + const option = {
  1557 + tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
  1558 + legend: { data: ['生美', '医美', '科美', '产品'], top: 10 },
  1559 + grid: { left: '3%', right: '4%', bottom: '3%', top: '15%', containLabel: true },
  1560 + xAxis: { type: 'category', data: [] },
  1561 + yAxis: { type: 'value', axisLabel: { formatter: value => value >= 10000 ? (value / 10000).toFixed(1) + '万' : value } },
  1562 + series: [
  1563 + { name: '生美', type: 'bar', stack: 'total', data: [], itemStyle: { color: '#A8D5E2' } },
  1564 + { name: '医美', type: 'bar', stack: 'total', data: [], itemStyle: { color: '#B8E6B8' } },
  1565 + { name: '科美', type: 'bar', stack: 'total', data: [], itemStyle: { color: '#FFD4A3' } },
  1566 + { name: '产品', type: 'bar', stack: 'total', data: [], itemStyle: { color: '#E6C1E6' } }
  1567 + ]
  1568 + }
  1569 + this.stackedChart.setOption(option)
  1570 + return
  1571 + }
  1572 +
  1573 + // 格式化月份显示(从 YYYYMM 转为 月份显示)
  1574 + const months = this.categoryMonthlyData.map(item => {
  1575 + const month = item.Month || ''
  1576 + if (month.length === 6) {
  1577 + const monthNum = parseInt(month.substring(4, 6))
  1578 + return monthNum + '月'
  1579 + }
  1580 + return month
  1581 + })
  1582 + const beautyData = this.categoryMonthlyData.map(item => (item.BeautyPerformance || 0) / 10000) // 转换为万元
  1583 + const medicalData = this.categoryMonthlyData.map(item => (item.MedicalPerformance || 0) / 10000) // 转换为万元
  1584 + const techData = this.categoryMonthlyData.map(item => (item.TechPerformance || 0) / 10000) // 转换为万元
  1585 + const productData = this.categoryMonthlyData.map(item => (item.ProductPerformance || 0) / 10000) // 转换为万元
  1586 +
649 1587 const option = {
650 1588 tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
651 1589 legend: { data: ['生美', '医美', '科美', '产品'], top: 10 },
652 1590 grid: { left: '3%', right: '4%', bottom: '3%', top: '15%', containLabel: true },
653   - xAxis: { type: 'category', data: ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'] },
654   - yAxis: { type: 'value', axisLabel: { formatter: value => value >= 10000 ? (value / 10000).toFixed(1) + '万' : value } },
  1591 + xAxis: { type: 'category', data: months },
  1592 + yAxis: { type: 'value', axisLabel: { formatter: value => value >= 1 ? value.toFixed(1) + '万' : value } },
655 1593 series: [
656   - { name: '生美', type: 'bar', stack: 'total', data: [320, 350, 380, 420, 390, 410, 450, 480, 420, 460, 500, 480], itemStyle: { color: '#A8D5E2' } },
657   - { name: '医美', type: 'bar', stack: 'total', data: [180, 200, 220, 250, 230, 240, 260, 280, 250, 270, 290, 280], itemStyle: { color: '#B8E6B8' } },
658   - { name: '科美', type: 'bar', stack: 'total', data: [140, 150, 160, 180, 170, 175, 190, 200, 180, 195, 210, 200], itemStyle: { color: '#FFD4A3' } },
659   - { name: '产品', type: 'bar', stack: 'total', data: [50, 55, 60, 70, 65, 68, 75, 80, 70, 78, 85, 80], itemStyle: { color: '#E6C1E6' } }
  1594 + { name: '生美', type: 'bar', stack: 'total', data: beautyData, itemStyle: { color: '#A8D5E2' } },
  1595 + { name: '医美', type: 'bar', stack: 'total', data: medicalData, itemStyle: { color: '#B8E6B8' } },
  1596 + { name: '科美', type: 'bar', stack: 'total', data: techData, itemStyle: { color: '#FFD4A3' } },
  1597 + { name: '产品', type: 'bar', stack: 'total', data: productData, itemStyle: { color: '#E6C1E6' } }
660 1598 ]
661 1599 }
662 1600 this.stackedChart.setOption(option)
663 1601 },
664 1602 renderFunnelChart() {
665 1603 if (!this.$refs.funnelChart) return
666   - this.funnelChart = echarts.init(this.$refs.funnelChart)
  1604 + if (!this.funnelChart) {
  1605 + this.funnelChart = echarts.init(this.$refs.funnelChart)
  1606 + }
  1607 +
  1608 + // 如果没有数据,使用空数据
  1609 + if (!this.funnelData) {
  1610 + const option = {
  1611 + tooltip: { trigger: 'item', formatter: '{a} <br/>{b}: {c} ({d}%)' },
  1612 + legend: { data: ['拓客', '邀约', '预约', '开单'], top: 10 },
  1613 + series: [{
  1614 + name: '拓客转化',
  1615 + type: 'funnel',
  1616 + left: '10%',
  1617 + top: 60,
  1618 + bottom: 60,
  1619 + width: '80%',
  1620 + min: 0,
  1621 + max: 100,
  1622 + minSize: '0%',
  1623 + maxSize: '100%',
  1624 + sort: 'descending',
  1625 + gap: 2,
  1626 + label: { show: true, position: 'inside', formatter: '{b}: {c}' },
  1627 + labelLine: { length: 10, lineStyle: { width: 1, type: 'solid' } },
  1628 + itemStyle: { borderColor: '#fff', borderWidth: 1 },
  1629 + emphasis: { label: { fontSize: 20 } },
  1630 + data: []
  1631 + }]
  1632 + }
  1633 + this.funnelChart.setOption(option)
  1634 + return
  1635 + }
  1636 +
  1637 + // 计算最大值(用于设置漏斗图的max值)
  1638 + const maxValue = Math.max(
  1639 + this.funnelData.ExpansionCount || 0,
  1640 + this.funnelData.InviteCount || 0,
  1641 + this.funnelData.AppointmentCount || 0,
  1642 + this.funnelData.BillingCount || 0
  1643 + ) || 100
  1644 +
667 1645 const option = {
668 1646 tooltip: { trigger: 'item', formatter: '{a} <br/>{b}: {c} ({d}%)' },
669   - legend: { data: ['访问', '咨询', '到店', '体验', '开单', '复购'], top: 10 },
  1647 + legend: { data: ['拓客', '邀约', '预约', '开单'], top: 10 },
670 1648 series: [{
671   - name: '会员转化',
  1649 + name: '拓客转化',
672 1650 type: 'funnel',
673 1651 left: '10%',
674 1652 top: 60,
675 1653 bottom: 60,
676 1654 width: '80%',
677 1655 min: 0,
678   - max: 10000,
  1656 + max: maxValue,
679 1657 minSize: '0%',
680 1658 maxSize: '100%',
681 1659 sort: 'descending',
... ... @@ -685,12 +1663,10 @@ export default {
685 1663 itemStyle: { borderColor: '#fff', borderWidth: 1 },
686 1664 emphasis: { label: { fontSize: 20 } },
687 1665 data: [
688   - { value: 10000, name: '访问', itemStyle: { color: '#409EFF' } },
689   - { value: 8000, name: '咨询', itemStyle: { color: '#67C23A' } },
690   - { value: 6000, name: '到店', itemStyle: { color: '#E6A23C' } },
691   - { value: 4000, name: '体验', itemStyle: { color: '#F56C6C' } },
692   - { value: 2000, name: '开单', itemStyle: { color: '#909399' } },
693   - { value: 1500, name: '复购', itemStyle: { color: '#606266' } }
  1666 + { value: this.funnelData.ExpansionCount || 0, name: '拓客', itemStyle: { color: '#409EFF' } },
  1667 + { value: this.funnelData.InviteCount || 0, name: '邀约', itemStyle: { color: '#67C23A' } },
  1668 + { value: this.funnelData.AppointmentCount || 0, name: '预约', itemStyle: { color: '#E6A23C' } },
  1669 + { value: this.funnelData.BillingCount || 0, name: '开单', itemStyle: { color: '#909399' } }
694 1670 ]
695 1671 }]
696 1672 }
... ... @@ -698,9 +1674,45 @@ export default {
698 1674 },
699 1675 renderScatterChart() {
700 1676 if (!this.$refs.scatterChart) return
701   - this.scatterChart = echarts.init(this.$refs.scatterChart)
  1677 + if (!this.scatterChart) {
  1678 + this.scatterChart = echarts.init(this.$refs.scatterChart)
  1679 + }
  1680 +
  1681 + // 如果没有数据,使用空数据
  1682 + if (!this.scatterData || this.scatterData.length === 0) {
  1683 + const option = {
  1684 + tooltip: { trigger: 'item', formatter: '客单价: {c[0]}<br/>项目数: {c[1]}<br/>会员数: {c[2]}' },
  1685 + legend: { data: ['会员分布'], top: 10 },
  1686 + grid: { left: '3%', right: '7%', bottom: '3%', top: '15%', containLabel: true },
  1687 + xAxis: { type: 'value', name: '客单价(元)', nameLocation: 'middle', nameGap: 30 },
  1688 + yAxis: { type: 'value', name: '项目数', nameLocation: 'middle', nameGap: 50 },
  1689 + series: [{
  1690 + name: '会员分布',
  1691 + type: 'scatter',
  1692 + symbolSize: data => Math.sqrt(data[2]) * 2,
  1693 + data: [],
  1694 + itemStyle: { color: '#409EFF', opacity: 0.6 }
  1695 + }]
  1696 + }
  1697 + this.scatterChart.setOption(option)
  1698 + return
  1699 + }
  1700 +
  1701 + // 格式化数据为散点图需要的格式 [客单价, 项目数, 会员数]
  1702 + const scatterData = this.scatterData.map(item => [
  1703 + item.AvgAmountPerPerson || 0,
  1704 + item.AvgProjectPerPerson || 0,
  1705 + item.MemberCount || 0
  1706 + ])
  1707 +
702 1708 const option = {
703   - tooltip: { trigger: 'item', formatter: '客单价: {c[0]}<br/>项目数: {c[1]}<br/>会员数: {c[2]}' },
  1709 + tooltip: {
  1710 + trigger: 'item',
  1711 + formatter: (params) => {
  1712 + const data = params.value
  1713 + return `客单价: ¥${this.formatMoney(data[0])}<br/>项目数: ${data[1].toFixed(2)}<br/>会员数: ${data[2]}`
  1714 + }
  1715 + },
704 1716 legend: { data: ['会员分布'], top: 10 },
705 1717 grid: { left: '3%', right: '7%', bottom: '3%', top: '15%', containLabel: true },
706 1718 xAxis: { type: 'value', name: '客单价(元)', nameLocation: 'middle', nameGap: 30 },
... ... @@ -708,12 +1720,11 @@ export default {
708 1720 series: [{
709 1721 name: '会员分布',
710 1722 type: 'scatter',
711   - symbolSize: data => Math.sqrt(data[2]) * 2,
712   - data: [
713   - [285, 4.5, 320], [320, 5.2, 280], [250, 3.8, 350], [380, 6.5, 180], [290, 4.8, 300],
714   - [350, 5.8, 220], [280, 4.2, 310], [420, 7.2, 150], [310, 5.0, 260], [360, 6.0, 200],
715   - [270, 4.0, 330], [390, 6.8, 170], [300, 4.9, 290], [370, 6.2, 210], [260, 3.9, 340]
716   - ],
  1723 + symbolSize: data => {
  1724 + const memberCount = data[2] || 1
  1725 + return Math.sqrt(memberCount) * 3 + 5 // 根据会员数调整点的大小
  1726 + },
  1727 + data: scatterData,
717 1728 itemStyle: { color: '#409EFF', opacity: 0.6 }
718 1729 }]
719 1730 }
... ... @@ -721,16 +1732,62 @@ export default {
721 1732 },
722 1733 renderHeatmapChart() {
723 1734 if (!this.$refs.heatmapChart) return
724   - this.heatmapChart = echarts.init(this.$refs.heatmapChart)
  1735 + if (!this.heatmapChart) {
  1736 + this.heatmapChart = echarts.init(this.$refs.heatmapChart)
  1737 + }
  1738 +
725 1739 const hours = ['周一', '周二', '周三', '周四', '周五', '周六', '周日']
726 1740 const times = ['09:00', '10:00', '11:00', '12:00', '13:00', '14:00', '15:00', '16:00', '17:00', '18:00', '19:00', '20:00', '21:00']
727   - const data = []
728   - for (let i = 0; i < hours.length; i++) {
729   - for (let j = 0; j < times.length; j++) {
730   - const value = Math.floor(Math.random() * 100)
731   - data.push([j, i, value])
  1741 +
  1742 + // 如果没有数据,使用空数据
  1743 + if (!this.heatmapData || this.heatmapData.length === 0) {
  1744 + const option = {
  1745 + tooltip: { position: 'top', formatter: params => `${hours[params.value[1]]} ${times[params.value[0]]}<br/>客流量: ${params.value[2]}` },
  1746 + grid: { height: '50%', top: '10%' },
  1747 + xAxis: { type: 'category', data: times, splitArea: { show: true }, position: 'top' },
  1748 + yAxis: { type: 'category', data: hours, splitArea: { show: true } },
  1749 + visualMap: {
  1750 + min: 0,
  1751 + max: 10,
  1752 + calculable: true,
  1753 + orient: 'horizontal',
  1754 + left: 'center',
  1755 + bottom: '5%',
  1756 + inRange: { color: ['#e0f3ff', '#409EFF', '#1d4ed8'] }
  1757 + },
  1758 + series: [{
  1759 + name: '客流量',
  1760 + type: 'heatmap',
  1761 + data: [],
  1762 + label: { show: true },
  1763 + emphasis: { itemStyle: { shadowBlur: 10, shadowColor: 'rgba(0, 0, 0, 0.5)' } }
  1764 + }]
732 1765 }
  1766 + this.heatmapChart.setOption(option)
  1767 + return
733 1768 }
  1769 +
  1770 + // 构建热力图数据 [时间段索引, 星期索引, 客流量]
  1771 + const data = []
  1772 + const timeIndexMap = {}
  1773 + times.forEach((time, index) => {
  1774 + timeIndexMap[time] = index
  1775 + })
  1776 +
  1777 + this.heatmapData.forEach(item => {
  1778 + const timeIndex = timeIndexMap[item.TimeSlot]
  1779 + if (timeIndex !== undefined) {
  1780 + // MySQL的DAYOFWEEK返回1=周日,2=周一...,我们转换为0=周一,6=周日
  1781 + // DayOfWeek: 0=周一, 6=周日
  1782 + let dayIndex = item.DayOfWeek === 0 ? 6 : item.DayOfWeek - 1
  1783 + if (dayIndex < 0 || dayIndex > 6) dayIndex = 0 // 容错处理
  1784 + data.push([timeIndex, dayIndex, item.CustomerFlow || 0])
  1785 + }
  1786 + })
  1787 +
  1788 + // 计算最大值用于visualMap
  1789 + const maxValue = Math.max(...data.map(d => d[2]), 1)
  1790 +
734 1791 const option = {
735 1792 tooltip: { position: 'top', formatter: params => `${hours[params.value[1]]} ${times[params.value[0]]}<br/>客流量: ${params.value[2]}` },
736 1793 grid: { height: '50%', top: '10%' },
... ... @@ -738,7 +1795,7 @@ export default {
738 1795 yAxis: { type: 'category', data: hours, splitArea: { show: true } },
739 1796 visualMap: {
740 1797 min: 0,
741   - max: 100,
  1798 + max: maxValue,
742 1799 calculable: true,
743 1800 orient: 'horizontal',
744 1801 left: 'center',
... ... @@ -755,58 +1812,39 @@ export default {
755 1812 }
756 1813 this.heatmapChart.setOption(option)
757 1814 },
758   - renderRadarChart() {
759   - if (!this.$refs.radarChart) return
760   - this.radarChart = echarts.init(this.$refs.radarChart)
761   - const option = {
762   - tooltip: {},
763   - radar: {
764   - indicator: [
765   - { name: '业绩能力', max: 100 },
766   - { name: '服务能力', max: 100 },
767   - { name: '会员管理', max: 100 },
768   - { name: '运营效率', max: 100 },
769   - { name: '团队协作', max: 100 },
770   - { name: '客户满意度', max: 100 }
771   - ],
772   - center: ['50%', '55%'],
773   - radius: '70%'
774   - },
775   - series: [{
776   - name: '门店综合能力',
777   - type: 'radar',
778   - data: [{
779   - value: [85, 78, 82, 75, 80, 88],
780   - name: '当前门店',
781   - areaStyle: { color: 'rgba(64, 158, 255, 0.3)' },
782   - itemStyle: { color: '#409EFF' },
783   - lineStyle: { color: '#409EFF', width: 2 }
784   - }, {
785   - value: [75, 72, 70, 68, 75, 80],
786   - name: '行业平均',
787   - areaStyle: { color: 'rgba(103, 194, 58, 0.2)' },
788   - itemStyle: { color: '#67C23A' },
789   - lineStyle: { color: '#67C23A', width: 2, type: 'dashed' }
790   - }]
791   - }]
792   - }
793   - this.radarChart.setOption(option)
794   - },
795 1815 renderGaugeChart() {
796 1816 if (!this.$refs.gaugeChart) return
797   - this.gaugeChart = echarts.init(this.$refs.gaugeChart)
  1817 + if (!this.gaugeChart) {
  1818 + this.gaugeChart = echarts.init(this.$refs.gaugeChart)
  1819 + }
  1820 +
  1821 + // 获取完成率
  1822 + const completionRate = this.storeData && this.storeData.Performance
  1823 + ? (this.storeData.Performance.CompletionRate || 0)
  1824 + : 0
  1825 +
798 1826 const option = {
799 1827 tooltip: { formatter: '{a} <br/>{b}: {c}%' },
800 1828 series: [{
801 1829 name: '目标完成度',
802 1830 type: 'gauge',
803 1831 progress: { show: true },
804   - detail: { valueAnimation: true, formatter: '{value}%', fontSize: 20, offsetCenter: [0, '70%'] },
805   - data: [{ value: 85.6, name: '完成率' }],
  1832 + detail: {
  1833 + valueAnimation: true,
  1834 + formatter: '{value}%',
  1835 + fontSize: 20,
  1836 + offsetCenter: [0, '70%'],
  1837 + color: completionRate >= 100 ? '#67C23A' : completionRate >= 80 ? '#409EFF' : '#F56C6C'
  1838 + },
  1839 + data: [{ value: parseFloat(completionRate.toFixed(1)), name: '完成率' }],
806 1840 axisLine: {
807 1841 lineStyle: {
808 1842 width: 20,
809   - color: [[0.3, '#67C23A'], [0.7, '#E6A23C'], [1, '#F56C6C']]
  1843 + color: [
  1844 + [0.3, '#67C23A'],
  1845 + [0.7, '#409EFF'],
  1846 + [1, '#F56C6C']
  1847 + ]
810 1848 }
811 1849 },
812 1850 axisTick: { show: false },
... ... @@ -818,24 +1856,6 @@ export default {
818 1856 }
819 1857 this.gaugeChart.setOption(option)
820 1858 },
821   - renderStackedAreaChart() {
822   - if (!this.$refs.stackedAreaChart) return
823   - this.stackedAreaChart = echarts.init(this.$refs.stackedAreaChart)
824   - const option = {
825   - tooltip: { trigger: 'axis', axisPointer: { type: 'cross', label: { backgroundColor: '#6a7985' } } },
826   - legend: { data: ['生美', '医美', '科美', '产品'], top: 10 },
827   - grid: { left: '3%', right: '4%', bottom: '3%', top: '15%', containLabel: true },
828   - xAxis: [{ type: 'category', boundaryGap: false, data: ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'] }],
829   - yAxis: [{ type: 'value', axisLabel: { formatter: '{value}%' } }],
830   - series: [
831   - { name: '生美', type: 'line', stack: 'Total', areaStyle: {}, emphasis: { focus: 'series' }, data: [45, 48, 50, 52, 49, 51, 53, 55, 52, 54, 56, 54], itemStyle: { color: '#A8D5E2' } },
832   - { name: '医美', type: 'line', stack: 'Total', areaStyle: {}, emphasis: { focus: 'series' }, data: [25, 26, 27, 28, 27, 28, 29, 30, 28, 29, 30, 29], itemStyle: { color: '#B8E6B8' } },
833   - { name: '科美', type: 'line', stack: 'Total', areaStyle: {}, emphasis: { focus: 'series' }, data: [20, 21, 22, 23, 22, 22, 23, 24, 23, 23, 24, 23], itemStyle: { color: '#FFD4A3' } },
834   - { name: '产品', type: 'line', stack: 'Total', areaStyle: {}, emphasis: { focus: 'series' }, data: [10, 5, 1, -3, 2, -1, -5, -9, -3, -6, -10, -6], itemStyle: { color: '#E6C1E6' } }
835   - ]
836   - }
837   - this.stackedAreaChart.setOption(option)
838   - },
839 1859 handleResize() {
840 1860 if (this.trendChart) this.trendChart.resize()
841 1861 if (this.categoryChart) this.categoryChart.resize()
... ... @@ -844,13 +1864,13 @@ export default {
844 1864 if (this.funnelChart) this.funnelChart.resize()
845 1865 if (this.scatterChart) this.scatterChart.resize()
846 1866 if (this.heatmapChart) this.heatmapChart.resize()
847   - if (this.radarChart) this.radarChart.resize()
848 1867 if (this.gaugeChart) this.gaugeChart.resize()
849   - if (this.stackedAreaChart) this.stackedAreaChart.resize()
850 1868 },
851   - formatMoney(value) {
  1869 + formatMoney(value, decimals = 2) {
852 1870 if (value === null || value === undefined) return '0.00'
853   - return Number(value).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
  1871 + const num = Number(value)
  1872 + if (isNaN(num)) return '0.00'
  1873 + return num.toLocaleString('zh-CN', { minimumFractionDigits: decimals, maximumFractionDigits: decimals })
854 1874 },
855 1875 getRankingClass(rank, total) {
856 1876 const percentage = rank / total
... ... @@ -883,6 +1903,28 @@ export default {
883 1903 background: #f5f7fa;
884 1904 min-height: calc(100vh - 84px);
885 1905  
  1906 + // 筛选器栏
  1907 + .filter-bar {
  1908 + background: #fff;
  1909 + padding: 16px 20px;
  1910 + border-radius: 12px;
  1911 + margin-bottom: 20px;
  1912 + box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
  1913 +
  1914 + .filter-form {
  1915 + margin: 0;
  1916 +
  1917 + ::v-deep .el-form-item {
  1918 + margin-bottom: 0;
  1919 + }
  1920 +
  1921 + ::v-deep .el-form-item__label {
  1922 + font-weight: 500;
  1923 + color: #606266;
  1924 + }
  1925 + }
  1926 + }
  1927 +
886 1928 // 顶部Header
887 1929 .dashboard-header {
888 1930 display: flex;
... ... @@ -1009,115 +2051,6 @@ export default {
1009 2051 }
1010 2052 }
1011 2053  
1012   - // KPI指标卡片
1013   - .kpi-section {
1014   - display: grid;
1015   - grid-template-columns: repeat(6, 1fr);
1016   - gap: 12px;
1017   - margin-bottom: 20px;
1018   -
1019   - .kpi-card {
1020   - display: flex;
1021   - align-items: center;
1022   - padding: 16px;
1023   - background: #fff;
1024   - border-radius: 10px;
1025   - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
1026   - transition: all 0.3s;
1027   - position: relative;
1028   - overflow: hidden;
1029   -
1030   - &::before {
1031   - content: '';
1032   - position: absolute;
1033   - top: 0;
1034   - left: 0;
1035   - width: 4px;
1036   - height: 100%;
1037   - }
1038   -
1039   - &.primary::before {
1040   - background: #409EFF;
1041   - }
1042   -
1043   - &.success::before {
1044   - background: #67C23A;
1045   - }
1046   -
1047   - &.info::before {
1048   - background: #909399;
1049   - }
1050   -
1051   - &.warning::before {
1052   - background: #E6A23C;
1053   - }
1054   -
1055   - &:hover {
1056   - transform: translateY(-2px);
1057   - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
1058   - }
1059   -
1060   - .kpi-icon {
1061   - width: 48px;
1062   - height: 48px;
1063   - border-radius: 10px;
1064   - display: flex;
1065   - align-items: center;
1066   - justify-content: center;
1067   - font-size: 22px;
1068   - color: #fff;
1069   - margin-right: 12px;
1070   - flex-shrink: 0;
1071   - }
1072   -
1073   - &.primary .kpi-icon {
1074   - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
1075   - }
1076   -
1077   - &.success .kpi-icon {
1078   - background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
1079   - }
1080   -
1081   - &.info .kpi-icon {
1082   - background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
1083   - }
1084   -
1085   - &.warning .kpi-icon {
1086   - background: linear-gradient(135deg, #fa709a 0%, #fee140 100%);
1087   - }
1088   -
1089   - .kpi-content {
1090   - flex: 1;
1091   - min-width: 0;
1092   -
1093   - .kpi-label {
1094   - font-size: 13px;
1095   - color: #909399;
1096   - margin-bottom: 6px;
1097   - }
1098   -
1099   - .kpi-value {
1100   - font-size: 20px;
1101   - font-weight: 600;
1102   - color: #303133;
1103   -
1104   - .unit {
1105   - font-size: 14px;
1106   - font-weight: 500;
1107   - }
1108   - }
1109   - }
1110   -
1111   - .kpi-trend {
1112   - font-size: 12px;
1113   - color: #67C23A;
1114   - font-weight: 500;
1115   - margin-left: 8px;
1116   - white-space: nowrap;
1117   - }
1118   - }
1119   - }
1120   -
1121 2054 // 主要内容区域:左右分栏
1122 2055 .main-content {
1123 2056 display: grid;
... ...
docs/API接口文档.md 0 → 100644
  1 +# API接口文档
  2 +
  3 +## 合同管理接口
  4 +
  5 +### 获取付款提醒列表
  6 +
  7 +**接口名称:** 获取付款提醒列表
  8 +
  9 +**接口地址:** `POST /api/Extend/LqContract/GetPaymentReminderList`
  10 +
  11 +**参数说明:**
  12 +
  13 +请求参数(JSON Body):
  14 +- `storeId` (string, 可选): 门店ID,用于筛选特定门店
  15 +- `onlyOverdue` (bool, 可选): 是否只查询已到期的提醒,默认false(查询所有)
  16 +
  17 +**返回说明:**
  18 +
  19 +返回付款提醒列表,包含以下主要字段:
  20 +- `PaymentDate`: 付款时间(财务需要在此日期前付款)
  21 +- `PaymentAmount`: 付款金额(财务需要支付的金额)
  22 +- `StoreName`: 店名
  23 +- `TenantName`: 收款方(户名)
  24 +- `PaymentMonth`: 应缴月份(格式:YYYY-MM)
  25 +- `IsOverdue`: 是否已过期
  26 +- `DaysUntilPayment`: 距离付款日期的天数
  27 +
  28 +---
  29 +
  30 +## 预约管理接口
  31 +
  32 +### 我的今日预约
  33 +
  34 +**接口名称:** 我的今日预约
  35 +
  36 +**接口地址:** `GET /api/Extend/LqYyjl/GetMyTodayAppointments`
  37 +
  38 +**参数说明:**
  39 +
  40 +无需参数,自动获取当前登录用户的今日预约记录。
  41 +
  42 +**返回说明:**
  43 +
  44 +返回今日预约客户列表,包含以下主要字段:
  45 +- `AppointmentId`: 预约编号
  46 +- `CustomerId`: 顾客ID
  47 +- `CustomerName`: 顾客姓名
  48 +- `CustomerType`: 顾客类型
  49 +- `ProjectName`: 预约体验项目
  50 +- `AppointmentStartTime`: 预约开始时间
  51 +- `AppointmentEndTime`: 预约结束时间
  52 +- `AppointmentTimeDisplay`: 预约时间(格式化显示,如:09:00-10:00)
  53 +- `HealthCoachName`: 预约健康师姓名
  54 +- `Status`: 预约状态
  55 +- `StoreName`: 门店名称
  56 +
  57 +---
  58 +
... ...
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqContract/PaymentReminderListInput.cs 0 → 100644
  1 +using System;
  2 +
  3 +namespace NCC.Extend.Entitys.Dto.LqContract
  4 +{
  5 + /// <summary>
  6 + /// 付款提醒列表查询输入
  7 + /// </summary>
  8 + public class PaymentReminderListInput
  9 + {
  10 + /// <summary>
  11 + /// 门店ID(可选,用于筛选特定门店)
  12 + /// </summary>
  13 + public string StoreId { get; set; }
  14 +
  15 + /// <summary>
  16 + /// 是否只查询已到期的提醒(默认true,只查询F_NextPaymentDate <= 当前日期的)
  17 + /// </summary>
  18 + public bool OnlyOverdue { get; set; } = true;
  19 + }
  20 +}
  21 +
... ...
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqContract/PaymentReminderListOutput.cs 0 → 100644
  1 +using System;
  2 +
  3 +namespace NCC.Extend.Entitys.Dto.LqContract
  4 +{
  5 + /// <summary>
  6 + /// 付款提醒列表输出(财务付款提醒专用)
  7 + /// </summary>
  8 + public class PaymentReminderListOutput
  9 + {
  10 + /// <summary>
  11 + /// 合同ID
  12 + /// </summary>
  13 + public string ContractId { get; set; }
  14 +
  15 + /// <summary>
  16 + /// 明细ID
  17 + /// </summary>
  18 + public string DetailId { get; set; }
  19 +
  20 + /// <summary>
  21 + /// 门店ID
  22 + /// </summary>
  23 + public string StoreId { get; set; }
  24 +
  25 + /// <summary>
  26 + /// 店名
  27 + /// </summary>
  28 + public string StoreName { get; set; }
  29 +
  30 + /// <summary>
  31 + /// 合同标题
  32 + /// </summary>
  33 + public string Title { get; set; }
  34 +
  35 + /// <summary>
  36 + /// 分类
  37 + /// </summary>
  38 + public string Category { get; set; }
  39 +
  40 + /// <summary>
  41 + /// 户名(收款方)
  42 + /// </summary>
  43 + public string TenantName { get; set; }
  44 +
  45 + /// <summary>
  46 + /// 付款时间(应缴日期,财务需要在此日期前付款)
  47 + /// </summary>
  48 + public DateTime PaymentDate { get; set; }
  49 +
  50 + /// <summary>
  51 + /// 付款金额(应缴金额,财务需要支付的金额)
  52 + /// </summary>
  53 + public decimal PaymentAmount { get; set; }
  54 +
  55 + /// <summary>
  56 + /// 应缴月份(格式:YYYY-MM)
  57 + /// </summary>
  58 + public string PaymentMonth { get; set; }
  59 +
  60 + /// <summary>
  61 + /// 提醒时间(提醒开始时间,已减去提前提醒天数)
  62 + /// </summary>
  63 + public DateTime? ReminderDate { get; set; }
  64 +
  65 + /// <summary>
  66 + /// 提前提醒天数
  67 + /// </summary>
  68 + public int ReminderDays { get; set; }
  69 +
  70 + /// <summary>
  71 + /// 距离付款日期的天数(负数表示已过期,正数表示还有多少天)
  72 + /// </summary>
  73 + public int DaysUntilPayment { get; set; }
  74 +
  75 + /// <summary>
  76 + /// 是否已过期
  77 + /// </summary>
  78 + public bool IsOverdue { get; set; }
  79 +
  80 + /// <summary>
  81 + /// 交租周期(几个月交一次)
  82 + /// </summary>
  83 + public int PaymentCycle { get; set; }
  84 +
  85 + /// <summary>
  86 + /// 备注
  87 + /// </summary>
  88 + public string Remarks { get; set; }
  89 + }
  90 +}
  91 +
... ...
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqReport/StoreDataAnalysisOutput.cs
... ... @@ -334,7 +334,7 @@ namespace NCC.Extend.Entitys.Dto.LqReport
334 334 public decimal ConsumePerformance { get; set; }
335 335  
336 336 /// <summary>
337   - /// 总业绩(开单+消耗)
  337 + /// 总业绩(开单+消耗)或净业绩(开单-退卡)
338 338 /// </summary>
339 339 public decimal TotalPerformance { get; set; }
340 340  
... ...
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqStoreDashboard/CategoryMonthlyPerformanceOutput.cs 0 → 100644
  1 +using System.Collections.Generic;
  2 +
  3 +namespace NCC.Extend.Entitys.Dto.LqStoreDashboard
  4 +{
  5 + /// <summary>
  6 + /// 分类月度业绩输出
  7 + /// </summary>
  8 + public class CategoryMonthlyPerformanceOutput
  9 + {
  10 + /// <summary>
  11 + /// 月份(YYYYMM格式)
  12 + /// </summary>
  13 + public string Month { get; set; }
  14 +
  15 + /// <summary>
  16 + /// 生美业绩
  17 + /// </summary>
  18 + public decimal BeautyPerformance { get; set; }
  19 +
  20 + /// <summary>
  21 + /// 医美业绩
  22 + /// </summary>
  23 + public decimal MedicalPerformance { get; set; }
  24 +
  25 + /// <summary>
  26 + /// 科美业绩
  27 + /// </summary>
  28 + public decimal TechPerformance { get; set; }
  29 +
  30 + /// <summary>
  31 + /// 产品业绩
  32 + /// </summary>
  33 + public decimal ProductPerformance { get; set; }
  34 + }
  35 +}
  36 +
... ...
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqStoreDashboard/CustomerPriceProjectRelationOutput.cs 0 → 100644
  1 +namespace NCC.Extend.Entitys.Dto.LqStoreDashboard
  2 +{
  3 + /// <summary>
  4 + /// 客单价与项目数关系输出
  5 + /// </summary>
  6 + public class CustomerPriceProjectRelationOutput
  7 + {
  8 + /// <summary>
  9 + /// 客单价(元)
  10 + /// </summary>
  11 + public decimal AvgAmountPerPerson { get; set; }
  12 +
  13 + /// <summary>
  14 + /// 项目数(人均项目数)
  15 + /// </summary>
  16 + public decimal AvgProjectPerPerson { get; set; }
  17 +
  18 + /// <summary>
  19 + /// 会员数(该区间的会员数量)
  20 + /// </summary>
  21 + public int MemberCount { get; set; }
  22 + }
  23 +}
  24 +
... ...
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqStoreDashboard/MemberConversionFunnelOutput.cs 0 → 100644
  1 +namespace NCC.Extend.Entitys.Dto.LqStoreDashboard
  2 +{
  3 + /// <summary>
  4 + /// 会员转化漏斗输出
  5 + /// </summary>
  6 + public class MemberConversionFunnelOutput
  7 + {
  8 + /// <summary>
  9 + /// 拓客数(本月拓客记录数)
  10 + /// </summary>
  11 + public int ExpansionCount { get; set; }
  12 +
  13 + /// <summary>
  14 + /// 邀约数(有邀约记录的会员数)
  15 + /// </summary>
  16 + public int InviteCount { get; set; }
  17 +
  18 + /// <summary>
  19 + /// 预约数(有预约记录的会员数)
  20 + /// </summary>
  21 + public int AppointmentCount { get; set; }
  22 +
  23 + /// <summary>
  24 + /// 耗卡数(已废弃,不再使用)
  25 + /// </summary>
  26 + public int ConsumeCount { get; set; }
  27 +
  28 + /// <summary>
  29 + /// 开单数(有开单记录的会员数)
  30 + /// </summary>
  31 + public int BillingCount { get; set; }
  32 + }
  33 +}
  34 +
... ...
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqStoreDashboard/StoreDashboardStatisticsInput.cs 0 → 100644
  1 +using System.ComponentModel.DataAnnotations;
  2 +
  3 +namespace NCC.Extend.Entitys.Dto.LqStoreDashboard
  4 +{
  5 + /// <summary>
  6 + /// 门店驾驶舱统计数据输入
  7 + /// </summary>
  8 + public class StoreDashboardStatisticsInput
  9 + {
  10 + /// <summary>
  11 + /// 门店ID(必填)
  12 + /// </summary>
  13 + [Required(ErrorMessage = "门店ID不能为空")]
  14 + public string StoreId { get; set; }
  15 +
  16 + /// <summary>
  17 + /// 统计月份(YYYYMM格式,必填)
  18 + /// </summary>
  19 + [Required(ErrorMessage = "统计月份不能为空")]
  20 + [StringLength(6, MinimumLength = 6, ErrorMessage = "统计月份格式必须为YYYYMM")]
  21 + public string StatisticsMonth { get; set; }
  22 + }
  23 +}
  24 +
... ...
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqStoreDashboard/StoreDashboardStatisticsOutput.cs 0 → 100644
  1 +namespace NCC.Extend.Entitys.Dto.LqStoreDashboard
  2 +{
  3 + /// <summary>
  4 + /// 门店驾驶舱统计数据输出
  5 + /// </summary>
  6 + public class StoreDashboardStatisticsOutput
  7 + {
  8 + /// <summary>
  9 + /// 开单业绩
  10 + /// </summary>
  11 + public decimal BillingPerformance { get; set; }
  12 +
  13 + /// <summary>
  14 + /// 消耗业绩
  15 + /// </summary>
  16 + public decimal ConsumePerformance { get; set; }
  17 +
  18 + /// <summary>
  19 + /// 完成率(消耗业绩/目标业绩 × 100%)
  20 + /// </summary>
  21 + public decimal CompletionRate { get; set; }
  22 +
  23 + /// <summary>
  24 + /// 净业绩(开单业绩 - 退卡金额)
  25 + /// </summary>
  26 + public decimal NetPerformance { get; set; }
  27 +
  28 + /// <summary>
  29 + /// 开单次数
  30 + /// </summary>
  31 + public int BillingCount { get; set; }
  32 +
  33 + /// <summary>
  34 + /// 消耗次数
  35 + /// </summary>
  36 + public int ConsumeCount { get; set; }
  37 +
  38 + /// <summary>
  39 + /// 退卡次数
  40 + /// </summary>
  41 + public int RefundCount { get; set; }
  42 +
  43 + /// <summary>
  44 + /// 平均开单金额
  45 + /// </summary>
  46 + public decimal AvgBillingAmount { get; set; }
  47 +
  48 + /// <summary>
  49 + /// 平均消耗金额
  50 + /// </summary>
  51 + public decimal AvgConsumeAmount { get; set; }
  52 +
  53 + /// <summary>
  54 + /// 剩余权益总额
  55 + /// </summary>
  56 + public decimal RemainingRightsAmount { get; set; }
  57 +
  58 + /// <summary>
  59 + /// 目标业绩
  60 + /// </summary>
  61 + public decimal TargetPerformance { get; set; }
  62 +
  63 + /// <summary>
  64 + /// 退卡金额
  65 + /// </summary>
  66 + public decimal RefundAmount { get; set; }
  67 +
  68 + /// <summary>
  69 + /// 人头数(去重后的消费会员数)
  70 + /// </summary>
  71 + public int HeadCount { get; set; }
  72 +
  73 + /// <summary>
  74 + /// 人次(日度去重客户数)
  75 + /// </summary>
  76 + public int PersonCount { get; set; }
  77 +
  78 + /// <summary>
  79 + /// 项目数(消耗的项目总数)
  80 + /// </summary>
  81 + public int ProjectCount { get; set; }
  82 +
  83 + /// <summary>
  84 + /// 客单价(消耗业绩/消耗人次)
  85 + /// </summary>
  86 + public decimal AvgAmountPerPerson { get; set; }
  87 +
  88 + /// <summary>
  89 + /// 项目单价(消耗业绩/项目数)
  90 + /// </summary>
  91 + public decimal AvgAmountPerProject { get; set; }
  92 +
  93 + /// <summary>
  94 + /// 人均项目数(项目数/人头数)
  95 + /// </summary>
  96 + public decimal AvgProjectPerHead { get; set; }
  97 + }
  98 +}
  99 +
... ...
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqStoreDashboard/WeeklyHeatmapOutput.cs 0 → 100644
  1 +namespace NCC.Extend.Entitys.Dto.LqStoreDashboard
  2 +{
  3 + /// <summary>
  4 + /// 一周运营热力图输出
  5 + /// </summary>
  6 + public class WeeklyHeatmapOutput
  7 + {
  8 + /// <summary>
  9 + /// 时间段(如:09:00)
  10 + /// </summary>
  11 + public string TimeSlot { get; set; }
  12 +
  13 + /// <summary>
  14 + /// 星期几(0=周一,6=周日)
  15 + /// </summary>
  16 + public int DayOfWeek { get; set; }
  17 +
  18 + /// <summary>
  19 + /// 客流量(该时间段该星期的消耗人次)
  20 + /// </summary>
  21 + public int CustomerFlow { get; set; }
  22 + }
  23 +}
  24 +
... ...
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqYyjl/MyTodayAppointmentOutput.cs 0 → 100644
  1 +using System;
  2 +
  3 +namespace NCC.Extend.Entitys.Dto.LqYyjl
  4 +{
  5 + /// <summary>
  6 + /// 我的今日预约输出
  7 + /// </summary>
  8 + public class MyTodayAppointmentOutput
  9 + {
  10 + /// <summary>
  11 + /// 预约编号
  12 + /// </summary>
  13 + public string AppointmentId { get; set; }
  14 +
  15 + /// <summary>
  16 + /// 顾客ID
  17 + /// </summary>
  18 + public string CustomerId { get; set; }
  19 +
  20 + /// <summary>
  21 + /// 顾客姓名
  22 + /// </summary>
  23 + public string CustomerName { get; set; }
  24 +
  25 + /// <summary>
  26 + /// 顾客类型
  27 + /// </summary>
  28 + public string CustomerType { get; set; }
  29 +
  30 + /// <summary>
  31 + /// 预约体验项目
  32 + /// </summary>
  33 + public string ProjectName { get; set; }
  34 +
  35 + /// <summary>
  36 + /// 预约开始时间
  37 + /// </summary>
  38 + public DateTime? AppointmentStartTime { get; set; }
  39 +
  40 + /// <summary>
  41 + /// 预约结束时间
  42 + /// </summary>
  43 + public DateTime? AppointmentEndTime { get; set; }
  44 +
  45 + /// <summary>
  46 + /// 预约健康师ID
  47 + /// </summary>
  48 + public string HealthCoachId { get; set; }
  49 +
  50 + /// <summary>
  51 + /// 预约健康师姓名
  52 + /// </summary>
  53 + public string HealthCoachName { get; set; }
  54 +
  55 + /// <summary>
  56 + /// 预约状态
  57 + /// </summary>
  58 + public string Status { get; set; }
  59 +
  60 + /// <summary>
  61 + /// 门店ID
  62 + /// </summary>
  63 + public string StoreId { get; set; }
  64 +
  65 + /// <summary>
  66 + /// 门店名称
  67 + /// </summary>
  68 + public string StoreName { get; set; }
  69 +
  70 + /// <summary>
  71 + /// 预约时间(格式化显示,如:09:00-10:00)
  72 + /// </summary>
  73 + public string AppointmentTimeDisplay { get; set; }
  74 + }
  75 +}
  76 +
... ...
netcore/src/Modularity/Extend/NCC.Extend/LqContractService.cs
... ... @@ -1343,6 +1343,125 @@ namespace NCC.Extend
1343 1343 }
1344 1344  
1345 1345 #endregion
  1346 +
  1347 + #region 付款提醒列表
  1348 +
  1349 + /// <summary>
  1350 + /// 获取付款前提醒列表
  1351 + /// </summary>
  1352 + /// <remarks>
  1353 + /// 查询需要提醒付款的合同列表
  1354 + ///
  1355 + /// 查询逻辑:
  1356 + /// 1. 查询所有有效的合同,且 F_NextPaymentDate 不为空
  1357 + /// 2. 如果 OnlyOverdue 为 true,只返回 F_NextPaymentDate 小于等于当前日期的合同
  1358 + /// 3. 如果 OnlyOverdue 为 false,返回所有有提醒时间的合同
  1359 + /// 4. 关联查询未缴费的明细,获取应缴日期和应缴金额
  1360 + /// 5. 计算距离应缴日期的天数
  1361 + ///
  1362 + /// 示例请求:
  1363 + /// ```json
  1364 + /// {
  1365 + /// "storeId": "门店ID(可选)",
  1366 + /// "onlyOverdue": true
  1367 + /// }
  1368 + /// ```
  1369 + ///
  1370 + /// 参数说明:
  1371 + /// - storeId: 门店ID,可选,用于筛选特定门店
  1372 + /// - onlyOverdue: 是否只查询已到期的提醒,默认true
  1373 + /// </remarks>
  1374 + /// <param name="input">查询输入</param>
  1375 + /// <returns>付款提醒列表</returns>
  1376 + /// <response code="200">查询成功</response>
  1377 + /// <response code="500">服务器错误</response>
  1378 + [HttpPost("GetPaymentReminderList")]
  1379 + public async Task<List<PaymentReminderListOutput>> GetPaymentReminderListAsync([FromBody] PaymentReminderListInput input = null)
  1380 + {
  1381 + try
  1382 + {
  1383 + if (input == null)
  1384 + {
  1385 + input = new PaymentReminderListInput();
  1386 + }
  1387 +
  1388 + var now = DateTime.Now.Date;
  1389 +
  1390 + // 查询需要提醒的合同
  1391 + var contracts = await _db.Queryable<LqContractEntity>()
  1392 + .Where(x => x.IsEffective == StatusEnum.有效.GetHashCode())
  1393 + .Where(x => x.NextPaymentDate != null)
  1394 + .WhereIF(input.OnlyOverdue, x => x.NextPaymentDate.Value.Date <= now)
  1395 + .WhereIF(!string.IsNullOrWhiteSpace(input.StoreId), x => x.StoreId == input.StoreId)
  1396 + .OrderBy(x => x.NextPaymentDate)
  1397 + .ToListAsync();
  1398 +
  1399 + if (!contracts.Any())
  1400 + {
  1401 + return new List<PaymentReminderListOutput>();
  1402 + }
  1403 +
  1404 + var contractIds = contracts.Select(x => x.Id).ToList();
  1405 +
  1406 + // 查询所有未缴费的明细
  1407 + var unpaidDetails = await _db.Queryable<LqContractRentDetailEntity>()
  1408 + .Where(x => contractIds.Contains(x.ContractId))
  1409 + .Where(x => x.IsEffective == StatusEnum.有效.GetHashCode())
  1410 + .Where(x => x.IsPaid == 0)
  1411 + .OrderBy(x => x.DueDate)
  1412 + .ToListAsync();
  1413 +
  1414 + // 按合同ID分组,获取每个合同最早未缴费的明细
  1415 + var contractDetailDict = unpaidDetails
  1416 + .GroupBy(x => x.ContractId)
  1417 + .ToDictionary(
  1418 + g => g.Key,
  1419 + g => g.OrderBy(x => x.DueDate).First()
  1420 + );
  1421 +
  1422 + // 构建返回结果
  1423 + var result = new List<PaymentReminderListOutput>();
  1424 +
  1425 + foreach (var contract in contracts)
  1426 + {
  1427 + if (contractDetailDict.ContainsKey(contract.Id))
  1428 + {
  1429 + var detail = contractDetailDict[contract.Id];
  1430 + var daysUntilDue = (detail.DueDate.Date - now).Days;
  1431 + var isOverdue = detail.DueDate.Date < now;
  1432 +
  1433 + result.Add(new PaymentReminderListOutput
  1434 + {
  1435 + ContractId = contract.Id,
  1436 + DetailId = detail.Id,
  1437 + StoreId = contract.StoreId,
  1438 + StoreName = contract.StoreName,
  1439 + Title = contract.Title,
  1440 + Category = contract.Category,
  1441 + TenantName = contract.TenantName,
  1442 + PaymentDate = detail.DueDate,
  1443 + PaymentAmount = detail.DueAmount,
  1444 + PaymentMonth = detail.PaymentMonth.ToString("yyyy-MM"),
  1445 + ReminderDate = contract.NextPaymentDate,
  1446 + ReminderDays = contract.ReminderDays,
  1447 + DaysUntilPayment = daysUntilDue,
  1448 + IsOverdue = isOverdue,
  1449 + PaymentCycle = contract.PaymentCycle,
  1450 + Remarks = detail.Remarks
  1451 + });
  1452 + }
  1453 + }
  1454 +
  1455 + return result.OrderBy(x => x.PaymentDate).ToList();
  1456 + }
  1457 + catch (Exception ex)
  1458 + {
  1459 + _logger.LogError(ex, "获取付款提醒列表失败");
  1460 + throw NCCException.Oh($"获取付款提醒列表失败:{ex.Message}");
  1461 + }
  1462 + }
  1463 +
  1464 + #endregion
1346 1465 }
1347 1466 }
1348 1467  
... ...
netcore/src/Modularity/Extend/NCC.Extend/LqHytkHytkService.cs
... ... @@ -958,7 +958,7 @@ namespace NCC.Extend.LqHytkHytk
958 958 {
959 959 continue;
960 960 }
961   -
  961 +
962 962 // 应用筛选条件
963 963 if (input.storeIds != null && input.storeIds.Any())
964 964 {
... ... @@ -1025,7 +1025,7 @@ namespace NCC.Extend.LqHytkHytk
1025 1025 // 7. 排序
1026 1026 var sidx = string.IsNullOrEmpty(input.sidx) ? "refundTime" : input.sidx;
1027 1027 var sort = string.IsNullOrEmpty(input.sort) ? "desc" : input.sort;
1028   -
  1028 +
1029 1029 if (sort.ToLower() == "desc")
1030 1030 {
1031 1031 resultList = resultList.OrderByDescending(x => GetPropertyValue(x, sidx)).ToList();
... ... @@ -1177,7 +1177,7 @@ namespace NCC.Extend.LqHytkHytk
1177 1177 {
1178 1178 continue;
1179 1179 }
1180   -
  1180 +
1181 1181 // 应用筛选条件
1182 1182 if (input.storeIds != null && input.storeIds.Any())
1183 1183 {
... ... @@ -1244,7 +1244,7 @@ namespace NCC.Extend.LqHytkHytk
1244 1244 // 7. 排序
1245 1245 var sidx = string.IsNullOrEmpty(input.sidx) ? "refundTime" : input.sidx;
1246 1246 var sort = string.IsNullOrEmpty(input.sort) ? "desc" : input.sort;
1247   -
  1247 +
1248 1248 if (sort.ToLower() == "desc")
1249 1249 {
1250 1250 resultList = resultList.OrderByDescending(x => GetPropertyValue(x, sidx)).ToList();
... ...
netcore/src/Modularity/Extend/NCC.Extend/LqReportService.cs
... ... @@ -3126,6 +3126,8 @@ namespace NCC.Extend
3126 3126 Name = item.health_coach_name?.ToString()
3127 3127 })
3128 3128 .Where(x => !string.IsNullOrEmpty(x.Id))
  3129 + .GroupBy(x => x.Id)
  3130 + .Select(g => g.First())
3129 3131 .ToList();
3130 3132  
3131 3133 var rankingIds = rankingData.Select(x => x.Id).ToList();
... ... @@ -3200,7 +3202,7 @@ namespace NCC.Extend
3200 3202 consumeDataSql += $" AND hyhk.md IN ('{storeIdsStr}')";
3201 3203 }
3202 3204  
3203   - consumeDataSql += " GROUP BY jks.jkszh, jks.jksxm";
  3205 + consumeDataSql += " GROUP BY jks.jkszh";
3204 3206  
3205 3207 var consumeData = (await _db.Ado.SqlQueryAsync<dynamic>(consumeDataSql))
3206 3208 .ToDictionary(
... ... @@ -5798,28 +5800,57 @@ namespace NCC.Extend
5798 5800 }
5799 5801 }
5800 5802  
5801   - // 3. 合并数据并排序
  5803 + // 3. 查询退卡业绩(用于计算净业绩)
  5804 + var refundSql = $@"
  5805 + SELECT
  5806 + jks.jks as HealthCoachId,
  5807 + COALESCE(SUM(CAST(jks.jksyj AS DECIMAL(18,2))), 0) as RefundPerformance
  5808 + FROM lq_hytk_jksyj jks
  5809 + INNER JOIN lq_hytk_hytk hytk ON jks.gltkbh = hytk.F_Id
  5810 + WHERE jks.F_IsEffective = 1
  5811 + AND hytk.F_IsEffective = 1
  5812 + AND hytk.md = '{input.StoreId}'
  5813 + AND hytk.tksj >= '{startDate:yyyy-MM-dd 00:00:00}'
  5814 + AND hytk.tksj <= '{endDateTime:yyyy-MM-dd HH:mm:ss}'
  5815 + GROUP BY jks.jks";
  5816 +
  5817 + var refundData = await _db.Ado.SqlQueryAsync<dynamic>(refundSql);
  5818 + var refundDict = new Dictionary<string, decimal>();
  5819 + foreach (var x in refundData)
  5820 + {
  5821 + var id = x.HealthCoachId?.ToString() ?? "";
  5822 + if (!string.IsNullOrEmpty(id))
  5823 + {
  5824 + var refundPerf = Convert.ToDecimal(x.RefundPerformance ?? 0);
  5825 + refundDict[id] = refundPerf;
  5826 + }
  5827 + }
  5828 +
  5829 + // 4. 合并数据并排序
5802 5830 var allHealthCoachIds = new HashSet<string>();
5803 5831 foreach (var key in billingDict.Keys) allHealthCoachIds.Add(key);
5804 5832 foreach (var key in consumeDict.Keys) allHealthCoachIds.Add(key);
  5833 + foreach (var key in refundDict.Keys) allHealthCoachIds.Add(key);
5805 5834  
5806 5835 var healthCoachRanking = allHealthCoachIds.Select(hcId =>
5807 5836 {
5808 5837 var billing = billingDict.ContainsKey(hcId) ? billingDict[hcId] : ("未知", 0m, 0);
5809 5838 var consume = consumeDict.ContainsKey(hcId) ? consumeDict[hcId] : ("未知", 0m, 0);
  5839 + var refund = refundDict.ContainsKey(hcId) ? refundDict[hcId] : 0m;
5810 5840 var name = billing.Item1 != "未知" ? billing.Item1 : consume.Item1;
  5841 + var netPerformance = billing.Item2 - refund; // 净业绩 = 开单业绩 - 退卡金额
5811 5842 return new
5812 5843 {
5813 5844 HealthCoachId = hcId,
5814 5845 HealthCoachName = name,
5815 5846 BillingPerformance = billing.Item2,
5816 5847 ConsumePerformance = consume.Item2,
5817   - TotalPerformance = billing.Item2 + consume.Item2,
  5848 + NetPerformance = netPerformance,
5818 5849 BillingCount = billing.Item3,
5819 5850 ConsumeCount = consume.Item3
5820 5851 };
5821 5852 })
5822   - .OrderByDescending(x => x.TotalPerformance)
  5853 + .OrderByDescending(x => x.NetPerformance)
5823 5854 .Take(10)
5824 5855 .ToList();
5825 5856  
... ... @@ -5829,7 +5860,7 @@ namespace NCC.Extend
5829 5860 HealthCoachName = hc.HealthCoachName ?? "未知",
5830 5861 BillingPerformance = hc.BillingPerformance,
5831 5862 ConsumePerformance = hc.ConsumePerformance,
5832   - TotalPerformance = hc.TotalPerformance,
  5863 + TotalPerformance = hc.NetPerformance, // 使用净业绩作为总业绩
5833 5864 BillingCount = hc.BillingCount,
5834 5865 ConsumeCount = hc.ConsumeCount
5835 5866 }).ToList();
... ... @@ -6176,7 +6207,7 @@ namespace NCC.Extend
6176 6207 if (allStoresPerformance != null)
6177 6208 {
6178 6209 var performanceList = allStoresPerformance.ToList();
6179   - totalStoreCount = performanceList.Count;
  6210 + totalStoreCount = (int)performanceList.Count;
6180 6211 foreach (var storePerf in performanceList)
6181 6212 {
6182 6213 if (storePerf.StoreId?.ToString() == input.StoreId)
... ... @@ -6193,7 +6224,7 @@ namespace NCC.Extend
6193 6224 .Select(x => x.Id)
6194 6225 .ToListAsync();
6195 6226  
6196   - var sameTypeStoreCount = sameTypeStores != null ? sameTypeStores.Count : 0;
  6227 + var sameTypeStoreCount = sameTypeStores != null ? (int)sameTypeStores.Count : 0;
6197 6228 var avgPerformanceSameType = 0m;
6198 6229 if (sameTypeStoreCount > 0)
6199 6230 {
... ... @@ -6219,7 +6250,7 @@ namespace NCC.Extend
6219 6250  
6220 6251 if (sameTypePerformance != null && sameTypePerformance.Any())
6221 6252 {
6222   - avgPerformanceSameType = (decimal)sameTypePerformance.Average(x => Convert.ToDecimal(x.NetPerformance ?? 0));
  6253 + avgPerformanceSameType = Convert.ToDecimal(sameTypePerformance.Average(x => (double)Convert.ToDecimal(x.NetPerformance ?? 0)));
6223 6254 }
6224 6255 }
6225 6256  
... ... @@ -6247,7 +6278,7 @@ namespace NCC.Extend
6247 6278 .ToListAsync();
6248 6279 }
6249 6280  
6250   - var sameOrgStoreCount = sameOrgStoreIds != null ? sameOrgStoreIds.Count : 0;
  6281 + var sameOrgStoreCount = sameOrgStoreIds != null ? (int)sameOrgStoreIds.Count : 0;
6251 6282 var avgPerformanceSameOrg = 0m;
6252 6283 if (sameOrgStoreCount > 0)
6253 6284 {
... ... @@ -6273,7 +6304,7 @@ namespace NCC.Extend
6273 6304  
6274 6305 if (sameOrgPerformance != null && sameOrgPerformance.Any())
6275 6306 {
6276   - avgPerformanceSameOrg = (decimal)sameOrgPerformance.Average(x => Convert.ToDecimal(x.NetPerformance ?? 0));
  6307 + avgPerformanceSameOrg = Convert.ToDecimal(sameOrgPerformance.Average(x => (double)Convert.ToDecimal(x.NetPerformance ?? 0)));
6277 6308 }
6278 6309 }
6279 6310  
... ...
netcore/src/Modularity/Extend/NCC.Extend/LqStoreDashboardService.cs 0 → 100644
  1 +using System;
  2 +using System.Linq;
  3 +using System.Threading.Tasks;
  4 +using Microsoft.AspNetCore.Mvc;
  5 +using Microsoft.Extensions.Logging;
  6 +using NCC.Dependency;
  7 +using NCC.DynamicApiController;
  8 +using NCC.FriendlyException;
  9 +using NCC.Extend.Entitys.Dto.LqStoreDashboard;
  10 +using NCC.Extend.Entitys.lq_kd_kdjlb;
  11 +using NCC.Extend.Entitys.lq_xh_hyhk;
  12 +using NCC.Extend.Entitys.lq_hytk_hytk;
  13 +using NCC.Extend.Entitys.lq_mdxx;
  14 +using NCC.Extend.Entitys.lq_md_target;
  15 +using NCC.Extend.Entitys.lq_khxx;
  16 +using NCC.Extend.Entitys.lq_xh_pxmx;
  17 +using NCC.Extend.Entitys.lq_xmzl;
  18 +using NCC.Extend.Entitys.lq_yyjl;
  19 +using System.Collections.Generic;
  20 +using SqlSugar;
  21 +
  22 +namespace NCC.Extend
  23 +{
  24 + /// <summary>
  25 + /// 门店驾驶舱服务
  26 + /// </summary>
  27 + [ApiDescriptionSettings(Tag = "门店驾驶舱服务", Name = "LqStoreDashboard", Order = 201)]
  28 + [Route("api/Extend/[controller]")]
  29 + public class LqStoreDashboardService : IDynamicApiController, ITransient
  30 + {
  31 + private readonly ISqlSugarClient _db;
  32 + private readonly ILogger<LqStoreDashboardService> _logger;
  33 +
  34 + public LqStoreDashboardService(ISqlSugarClient db, ILogger<LqStoreDashboardService> logger)
  35 + {
  36 + _db = db;
  37 + _logger = logger;
  38 + }
  39 +
  40 + /// <summary>
  41 + /// 获取门店驾驶舱统计数据
  42 + /// </summary>
  43 + /// <remarks>
  44 + /// 获取指定门店在指定月份的4个核心指标:开单业绩、消耗业绩、完成率、净业绩
  45 + ///
  46 + /// 示例请求:
  47 + /// ```json
  48 + /// {
  49 + /// "storeId": "1649328471923847169",
  50 + /// "statisticsMonth": "202601"
  51 + /// }
  52 + /// ```
  53 + ///
  54 + /// 参数说明:
  55 + /// - storeId: 门店ID(必填)
  56 + /// - statisticsMonth: 统计月份,格式为YYYYMM(必填)
  57 + ///
  58 + /// 返回数据说明:
  59 + /// - BillingPerformance: 开单业绩(当月开单金额汇总)
  60 + /// - ConsumePerformance: 消耗业绩(当月消耗金额汇总)
  61 + /// - CompletionRate: 完成率(消耗业绩/目标业绩 × 100%)
  62 + /// - NetPerformance: 净业绩(开单业绩 - 退卡金额)
  63 + /// </remarks>
  64 + /// <param name="input">查询参数</param>
  65 + /// <returns>门店驾驶舱统计数据</returns>
  66 + /// <response code="200">成功返回统计数据</response>
  67 + /// <response code="400">参数错误</response>
  68 + /// <response code="500">服务器错误</response>
  69 + [HttpPost("GetStatistics")]
  70 + public async Task<StoreDashboardStatisticsOutput> GetStatistics([FromBody] StoreDashboardStatisticsInput input)
  71 + {
  72 + try
  73 + {
  74 + if (input == null)
  75 + {
  76 + throw NCCException.Oh("请求参数不能为空");
  77 + }
  78 +
  79 + if (string.IsNullOrWhiteSpace(input.StoreId))
  80 + {
  81 + throw NCCException.Oh("门店ID不能为空");
  82 + }
  83 +
  84 + if (string.IsNullOrWhiteSpace(input.StatisticsMonth) || input.StatisticsMonth.Length != 6)
  85 + {
  86 + throw NCCException.Oh("统计月份格式错误,必须为YYYYMM格式");
  87 + }
  88 +
  89 + _logger.LogInformation("开始查询门店驾驶舱统计数据,门店ID:{StoreId},统计月份:{StatisticsMonth}", input.StoreId, input.StatisticsMonth);
  90 +
  91 + // 解析月份获取时间范围
  92 + var year = int.Parse(input.StatisticsMonth.Substring(0, 4));
  93 + var month = int.Parse(input.StatisticsMonth.Substring(4, 2));
  94 + var startDate = new DateTime(year, month, 1);
  95 + var endDate = startDate.AddMonths(1).AddDays(-1);
  96 + var endDateTime = input.StatisticsMonth == DateTime.Now.ToString("yyyyMM")
  97 + ? DateTime.Now
  98 + : endDate.Date.AddHours(23).AddMinutes(59).AddSeconds(59);
  99 +
  100 + // 验证门店是否存在
  101 + var store = await _db.Queryable<LqMdxxEntity>()
  102 + .Where(x => x.Id == input.StoreId)
  103 + .FirstAsync();
  104 +
  105 + if (store == null)
  106 + {
  107 + throw NCCException.Oh("门店不存在");
  108 + }
  109 +
  110 + // 1. 获取目标业绩(从门店目标表获取,如果不存在则从门店表获取)
  111 + var target = await _db.Queryable<LqMdTargetEntity>()
  112 + .Where(x => x.StoreId == input.StoreId && x.Month == input.StatisticsMonth)
  113 + .FirstAsync();
  114 +
  115 + var targetPerformance = target?.StoreTarget ?? store.Xsyj ?? 0m;
  116 +
  117 + // 2. 获取开单业绩和开单次数
  118 + var billingQuery = _db.Queryable<LqKdKdjlbEntity>()
  119 + .Where(x => x.Djmd == input.StoreId && x.IsEffective == 1)
  120 + .Where(x => x.Kdrq.HasValue && x.Kdrq.Value >= startDate && x.Kdrq.Value <= endDateTime);
  121 +
  122 + var billingAmount = await billingQuery.SumAsync(x => (decimal?)x.Sfyj) ?? 0m;
  123 + var billingCount = await billingQuery.CountAsync();
  124 +
  125 + // 3. 获取消耗业绩和消耗次数
  126 + var consumeQuery = _db.Queryable<LqXhHyhkEntity>()
  127 + .Where(x => x.Md == input.StoreId && x.IsEffective == 1)
  128 + .Where(x => x.Hksj.HasValue && x.Hksj.Value >= startDate && x.Hksj.Value <= endDateTime);
  129 +
  130 + var consumeAmount = await consumeQuery.SumAsync(x => (decimal?)x.Xfje) ?? 0m;
  131 + var consumeCount = await consumeQuery.CountAsync();
  132 +
  133 + // 4. 获取退卡金额和退卡次数(使用实退金额)
  134 + var refundQuery = _db.Queryable<LqHytkHytkEntity>()
  135 + .Where(x => x.Md == input.StoreId && x.IsEffective == 1)
  136 + .Where(x => x.Tksj.HasValue && x.Tksj.Value.Date >= startDate.Date && x.Tksj.Value.Date <= endDate.Date);
  137 +
  138 + var refundAmount = await refundQuery.SumAsync(x => (decimal?)(x.ActualRefundAmount ?? x.Tkje ?? 0)) ?? 0m;
  139 + var refundCount = await refundQuery.CountAsync();
  140 +
  141 + // 5. 获取剩余权益总额
  142 + var remainingRightsAmount = await _db.Queryable<LqKhxxEntity>()
  143 + .Where(x => x.Gsmd == input.StoreId && x.IsEffective == 1)
  144 + .SumAsync(x => (decimal?)x.RemainingRightsAmount) ?? 0m;
  145 +
  146 + // 6. 计算平均开单金额
  147 + var avgBillingAmount = billingCount > 0 ? billingAmount / (decimal)billingCount : 0m;
  148 +
  149 + // 7. 计算平均消耗金额
  150 + var avgConsumeAmount = consumeCount > 0 ? consumeAmount / (decimal)consumeCount : 0m;
  151 +
  152 + // 8. 计算完成率(消耗业绩/目标业绩 × 100%)
  153 + var completionRate = targetPerformance > 0 ? (consumeAmount / targetPerformance * 100m) : 0m;
  154 +
  155 + // 9. 计算净业绩(开单业绩 - 退卡金额)
  156 + var netPerformance = billingAmount - refundAmount;
  157 +
  158 + // 10. 获取人头数(去重后的消费会员数)
  159 + var headCount = await _db.Queryable<LqXhHyhkEntity>()
  160 + .Where(x => x.Md == input.StoreId && x.IsEffective == 1)
  161 + .Where(x => x.Hksj.HasValue && x.Hksj.Value >= startDate && x.Hksj.Value <= endDateTime)
  162 + .Select(x => x.Hy)
  163 + .Distinct()
  164 + .CountAsync();
  165 +
  166 + // 11. 获取人次(日度去重客户数)- 使用SQL查询
  167 + var personCountSql = $@"
  168 + SELECT COUNT(DISTINCT CONCAT(xh.Hy, '-', DATE_FORMAT(xh.Hksj, '%Y-%m-%d'))) as PersonCount
  169 + FROM lq_xh_hyhk xh
  170 + WHERE xh.Md = '{input.StoreId}'
  171 + AND xh.F_IsEffective = 1
  172 + AND xh.Hksj >= '{startDate:yyyy-MM-dd HH:mm:ss}'
  173 + AND xh.Hksj <= '{endDateTime:yyyy-MM-dd HH:mm:ss}'";
  174 + var personCountResult = await _db.Ado.SqlQueryAsync<dynamic>(personCountSql);
  175 + var personCount = personCountResult?.FirstOrDefault() != null
  176 + ? Convert.ToInt32(personCountResult.FirstOrDefault().PersonCount ?? 0)
  177 + : 0;
  178 +
  179 + // 12. 获取项目数(消耗的项目总数,从品项明细表统计原始项目数)
  180 + var projectCountSql = $@"
  181 + SELECT COALESCE(SUM(COALESCE(px.F_OriginalProjectNumber, px.F_ProjectNumber, 0)), 0) as ProjectCount
  182 + FROM lq_xh_pxmx px
  183 + INNER JOIN lq_xh_hyhk xh ON px.F_ConsumeInfoId = xh.F_Id
  184 + WHERE xh.Md = '{input.StoreId}'
  185 + AND xh.F_IsEffective = 1
  186 + AND px.F_IsEffective = 1
  187 + AND xh.Hksj >= '{startDate:yyyy-MM-dd HH:mm:ss}'
  188 + AND xh.Hksj <= '{endDateTime:yyyy-MM-dd HH:mm:ss}'";
  189 + var projectCountResult = await _db.Ado.SqlQueryAsync<dynamic>(projectCountSql);
  190 + var projectCount = projectCountResult?.FirstOrDefault() != null
  191 + ? Convert.ToDecimal(projectCountResult.FirstOrDefault().ProjectCount ?? 0)
  192 + : 0m;
  193 +
  194 + // 13. 计算客单价(消耗业绩/消耗人次)
  195 + var avgAmountPerPerson = personCount > 0 ? consumeAmount / (decimal)personCount : 0m;
  196 +
  197 + // 14. 计算项目单价(消耗业绩/项目数)
  198 + var avgAmountPerProject = projectCount > 0 ? consumeAmount / projectCount : 0m;
  199 +
  200 + // 15. 计算人均项目数(项目数/人头数)
  201 + var avgProjectPerHead = headCount > 0 ? projectCount / (decimal)headCount : 0m;
  202 +
  203 + var result = new StoreDashboardStatisticsOutput
  204 + {
  205 + BillingPerformance = billingAmount,
  206 + ConsumePerformance = consumeAmount,
  207 + CompletionRate = completionRate,
  208 + NetPerformance = netPerformance,
  209 + BillingCount = billingCount,
  210 + ConsumeCount = consumeCount,
  211 + RefundCount = refundCount,
  212 + AvgBillingAmount = avgBillingAmount,
  213 + AvgConsumeAmount = avgConsumeAmount,
  214 + RemainingRightsAmount = remainingRightsAmount,
  215 + TargetPerformance = targetPerformance,
  216 + RefundAmount = refundAmount,
  217 + HeadCount = headCount,
  218 + PersonCount = personCount,
  219 + ProjectCount = Convert.ToInt32(projectCount),
  220 + AvgAmountPerPerson = avgAmountPerPerson,
  221 + AvgAmountPerProject = avgAmountPerProject,
  222 + AvgProjectPerHead = avgProjectPerHead
  223 + };
  224 +
  225 + _logger.LogInformation("门店驾驶舱统计数据查询完成,门店ID:{StoreId},开单业绩:{BillingPerformance},消耗业绩:{ConsumePerformance},完成率:{CompletionRate}%,净业绩:{NetPerformance},开单次数:{BillingCount},消耗次数:{ConsumeCount},退卡次数:{RefundCount}",
  226 + input.StoreId, billingAmount, consumeAmount, completionRate, netPerformance, billingCount, consumeCount, refundCount);
  227 +
  228 + return result;
  229 + }
  230 + catch (Exception ex)
  231 + {
  232 + _logger.LogError(ex, "查询门店驾驶舱统计数据失败,门店ID:{StoreId},统计月份:{StatisticsMonth}", input?.StoreId, input?.StatisticsMonth);
  233 + throw NCCException.Oh($"查询门店驾驶舱统计数据失败:{ex.Message}");
  234 + }
  235 + }
  236 +
  237 + /// <summary>
  238 + /// 获取门店各分类月度业绩(近12个月)
  239 + /// </summary>
  240 + /// <remarks>
  241 + /// 获取指定门店近12个月各分类(生美、医美、科美、产品)的消耗业绩数据
  242 + ///
  243 + /// 示例请求:
  244 + /// ```json
  245 + /// {
  246 + /// "storeId": "1649328471923847169",
  247 + /// "statisticsMonth": "202601"
  248 + /// }
  249 + /// ```
  250 + ///
  251 + /// 参数说明:
  252 + /// - storeId: 门店ID(必填)
  253 + /// - statisticsMonth: 统计月份,格式为YYYYMM(可选,不传则默认为当前月份)
  254 + /// </remarks>
  255 + /// <param name="input">查询参数</param>
  256 + /// <returns>各分类月度业绩列表</returns>
  257 + /// <response code="200">成功返回数据</response>
  258 + /// <response code="400">参数错误</response>
  259 + /// <response code="500">服务器错误</response>
  260 + [HttpPost("GetCategoryMonthlyPerformance")]
  261 + public async Task<List<CategoryMonthlyPerformanceOutput>> GetCategoryMonthlyPerformance([FromBody] StoreDashboardStatisticsInput input)
  262 + {
  263 + try
  264 + {
  265 + if (input == null || string.IsNullOrWhiteSpace(input.StoreId))
  266 + {
  267 + throw NCCException.Oh("门店ID不能为空");
  268 + }
  269 +
  270 + // 计算时间范围:近12个月
  271 + var endDate = DateTime.Now;
  272 + var startDate = endDate.AddMonths(-11);
  273 + var startMonth = new DateTime(startDate.Year, startDate.Month, 1);
  274 +
  275 + // 查询近12个月各分类的消耗业绩
  276 + var sql = $@"
  277 + SELECT
  278 + DATE_FORMAT(xh.hksj, '%Y%m') as Month,
  279 + 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 BeautyPerformance,
  280 + 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 MedicalPerformance,
  281 + 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 TechPerformance,
  282 + 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 ProductPerformance
  283 + FROM lq_xh_pxmx xhpx
  284 + INNER JOIN lq_xh_hyhk xh ON xhpx.F_ConsumeInfoId = xh.F_Id AND xh.F_IsEffective = 1
  285 + LEFT JOIN lq_xmzl xmzl ON xhpx.px = xmzl.F_Id AND xmzl.F_IsEffective = 1
  286 + WHERE xhpx.F_IsEffective = 1
  287 + AND xh.md = '{input.StoreId}'
  288 + AND xh.hksj >= '{startMonth:yyyy-MM-dd 00:00:00}'
  289 + AND xh.hksj <= '{endDate:yyyy-MM-dd HH:mm:ss}'
  290 + GROUP BY DATE_FORMAT(xh.hksj, '%Y%m')
  291 + ORDER BY Month";
  292 +
  293 + var data = await _db.Ado.SqlQueryAsync<dynamic>(sql);
  294 + var dataDict = data.ToDictionary(x => x.Month?.ToString() ?? "", x => new
  295 + {
  296 + BeautyPerformance = Convert.ToDecimal(x.BeautyPerformance ?? 0),
  297 + MedicalPerformance = Convert.ToDecimal(x.MedicalPerformance ?? 0),
  298 + TechPerformance = Convert.ToDecimal(x.TechPerformance ?? 0),
  299 + ProductPerformance = Convert.ToDecimal(x.ProductPerformance ?? 0)
  300 + });
  301 +
  302 + // 构建12个月的数据
  303 + var result = new List<CategoryMonthlyPerformanceOutput>();
  304 + for (int i = 11; i >= 0; i--)
  305 + {
  306 + var trendMonth = DateTime.Now.AddMonths(-i);
  307 + var trendMonthStr = trendMonth.ToString("yyyyMM");
  308 +
  309 + decimal beautyPerformance = 0m;
  310 + decimal medicalPerformance = 0m;
  311 + decimal techPerformance = 0m;
  312 + decimal productPerformance = 0m;
  313 +
  314 + if (dataDict.ContainsKey(trendMonthStr))
  315 + {
  316 + var monthData = dataDict[trendMonthStr];
  317 + beautyPerformance = monthData.BeautyPerformance;
  318 + medicalPerformance = monthData.MedicalPerformance;
  319 + techPerformance = monthData.TechPerformance;
  320 + productPerformance = monthData.ProductPerformance;
  321 + }
  322 +
  323 + result.Add(new CategoryMonthlyPerformanceOutput
  324 + {
  325 + Month = trendMonthStr,
  326 + BeautyPerformance = beautyPerformance,
  327 + MedicalPerformance = medicalPerformance,
  328 + TechPerformance = techPerformance,
  329 + ProductPerformance = productPerformance
  330 + });
  331 + }
  332 +
  333 + return result;
  334 + }
  335 + catch (Exception ex)
  336 + {
  337 + _logger.LogError(ex, "获取门店各分类月度业绩失败,门店ID:{StoreId}", input?.StoreId);
  338 + throw NCCException.Oh($"获取门店各分类月度业绩失败:{ex.Message}");
  339 + }
  340 + }
  341 +
  342 + /// <summary>
  343 + /// 获取门店会员转化漏斗数据
  344 + /// </summary>
  345 + /// <remarks>
  346 + /// 获取指定门店在指定月份的会员转化漏斗数据(拓客-邀约-预约-开单\耗卡链路)
  347 + ///
  348 + /// 示例请求:
  349 + /// ```json
  350 + /// {
  351 + /// "storeId": "1649328471923847169",
  352 + /// "statisticsMonth": "202601"
  353 + /// }
  354 + /// ```
  355 + ///
  356 + /// 参数说明:
  357 + /// - storeId: 门店ID(必填)
  358 + /// - statisticsMonth: 统计月份,格式为YYYYMM(可选,不传则默认为当前月份)
  359 + /// </remarks>
  360 + /// <param name="input">查询参数</param>
  361 + /// <returns>会员转化漏斗数据</returns>
  362 + /// <response code="200">成功返回数据</response>
  363 + /// <response code="400">参数错误</response>
  364 + /// <response code="500">服务器错误</response>
  365 + [HttpPost("GetMemberConversionFunnel")]
  366 + public async Task<MemberConversionFunnelOutput> GetMemberConversionFunnel([FromBody] StoreDashboardStatisticsInput input)
  367 + {
  368 + try
  369 + {
  370 + if (input == null || string.IsNullOrWhiteSpace(input.StoreId))
  371 + {
  372 + throw NCCException.Oh("门店ID不能为空");
  373 + }
  374 +
  375 + var statisticsMonth = input.StatisticsMonth;
  376 + if (string.IsNullOrWhiteSpace(statisticsMonth))
  377 + {
  378 + statisticsMonth = DateTime.Now.ToString("yyyyMM");
  379 + }
  380 +
  381 + // 解析月份获取时间范围
  382 + var year = int.Parse(statisticsMonth.Substring(0, 4));
  383 + var month = int.Parse(statisticsMonth.Substring(4, 2));
  384 + var startDate = new DateTime(year, month, 1);
  385 + var endDate = startDate.AddMonths(1).AddDays(-1);
  386 + var endDateTime = statisticsMonth == DateTime.Now.ToString("yyyyMM")
  387 + ? DateTime.Now
  388 + : endDate.Date.AddHours(23).AddMinutes(59).AddSeconds(59);
  389 +
  390 + // 1. 拓客数(本月拓客记录数)
  391 + var expansionCountSql = $@"
  392 + SELECT COUNT(DISTINCT tk.F_Id) as ExpansionCount
  393 + FROM lq_tkjlb tk
  394 + WHERE tk.F_StoreId = '{input.StoreId}'
  395 + AND tk.F_ExpansionTime >= '{startDate:yyyy-MM-dd 00:00:00}'
  396 + AND tk.F_ExpansionTime <= '{endDateTime:yyyy-MM-dd HH:mm:ss}'";
  397 + var expansionCountResult = await _db.Ado.SqlQueryAsync<dynamic>(expansionCountSql);
  398 + var expansionCount = expansionCountResult?.FirstOrDefault() != null
  399 + ? Convert.ToInt32(expansionCountResult.FirstOrDefault().ExpansionCount ?? 0)
  400 + : 0;
  401 +
  402 + // 2. 邀约数(有邀约记录的会员数)- 通过拓客产生的邀约
  403 + var inviteCountSql = $@"
  404 + SELECT COUNT(DISTINCT tk.F_MemberId) as InviteCount
  405 + FROM lq_tkjlb tk
  406 + INNER JOIN lq_yaoyjl yaoy ON yaoy.yykh = tk.F_MemberId AND yaoy.F_StoreId = tk.F_StoreId
  407 + WHERE tk.F_StoreId = '{input.StoreId}'
  408 + AND tk.F_ExpansionTime >= '{startDate:yyyy-MM-dd 00:00:00}'
  409 + AND tk.F_ExpansionTime <= '{endDateTime:yyyy-MM-dd HH:mm:ss}'";
  410 + var inviteCountResult = await _db.Ado.SqlQueryAsync<dynamic>(inviteCountSql);
  411 + var inviteCount = inviteCountResult?.FirstOrDefault() != null
  412 + ? Convert.ToInt32(inviteCountResult.FirstOrDefault().InviteCount ?? 0)
  413 + : 0;
  414 +
  415 + // 3. 预约数(有预约记录的会员数)- 通过邀约产生的预约
  416 + var appointmentCountSql = $@"
  417 + SELECT COUNT(DISTINCT tk.F_MemberId) as AppointmentCount
  418 + FROM lq_tkjlb tk
  419 + INNER JOIN lq_yaoyjl yaoy ON yaoy.yykh = tk.F_MemberId AND yaoy.F_StoreId = tk.F_StoreId
  420 + INNER JOIN lq_yyjl yy ON yy.F_InviteId = yaoy.F_Id AND yy.gk = tk.F_MemberId
  421 + WHERE tk.F_StoreId = '{input.StoreId}'
  422 + AND tk.F_ExpansionTime >= '{startDate:yyyy-MM-dd 00:00:00}'
  423 + AND tk.F_ExpansionTime <= '{endDateTime:yyyy-MM-dd HH:mm:ss}'";
  424 + var appointmentCountResult = await _db.Ado.SqlQueryAsync<dynamic>(appointmentCountSql);
  425 + var appointmentCount = appointmentCountResult?.FirstOrDefault() != null
  426 + ? Convert.ToInt32(appointmentCountResult.FirstOrDefault().AppointmentCount ?? 0)
  427 + : 0;
  428 +
  429 + // 4. 开单数(有开单记录的会员数)- 通过预约产生的开单(去掉耗卡,直接算开单)
  430 + var billingCountSql = $@"
  431 + SELECT COUNT(DISTINCT tk.F_MemberId) as BillingCount
  432 + FROM lq_tkjlb tk
  433 + INNER JOIN lq_yaoyjl yaoy ON yaoy.yykh = tk.F_MemberId AND yaoy.F_StoreId = tk.F_StoreId
  434 + INNER JOIN lq_yyjl yy ON yy.F_InviteId = yaoy.F_Id AND yy.gk = tk.F_MemberId
  435 + INNER JOIN lq_kd_kdjlb kd ON kd.F_AppointmentId = yy.F_Id AND kd.kdhy = tk.F_MemberId AND kd.F_IsEffective = 1
  436 + WHERE tk.F_StoreId = '{input.StoreId}'
  437 + AND tk.F_ExpansionTime >= '{startDate:yyyy-MM-dd 00:00:00}'
  438 + AND tk.F_ExpansionTime <= '{endDateTime:yyyy-MM-dd HH:mm:ss}'";
  439 + var billingCountResult = await _db.Ado.SqlQueryAsync<dynamic>(billingCountSql);
  440 + var billingCount = billingCountResult?.FirstOrDefault() != null
  441 + ? Convert.ToInt32(billingCountResult.FirstOrDefault().BillingCount ?? 0)
  442 + : 0;
  443 +
  444 + return new MemberConversionFunnelOutput
  445 + {
  446 + ExpansionCount = expansionCount,
  447 + InviteCount = inviteCount,
  448 + AppointmentCount = appointmentCount,
  449 + ConsumeCount = 0, // 不再统计耗卡数
  450 + BillingCount = billingCount
  451 + };
  452 + }
  453 + catch (Exception ex)
  454 + {
  455 + _logger.LogError(ex, "获取门店会员转化漏斗数据失败,门店ID:{StoreId}", input?.StoreId);
  456 + throw NCCException.Oh($"获取门店会员转化漏斗数据失败:{ex.Message}");
  457 + }
  458 + }
  459 +
  460 + /// <summary>
  461 + /// 获取门店客单价与项目数关系数据
  462 + /// </summary>
  463 + /// <remarks>
  464 + /// 获取指定门店在指定月份的客单价与项目数关系数据(用于散点图)
  465 + ///
  466 + /// 示例请求:
  467 + /// ```json
  468 + /// {
  469 + /// "storeId": "1649328471923847169",
  470 + /// "statisticsMonth": "202601"
  471 + /// }
  472 + /// ```
  473 + ///
  474 + /// 参数说明:
  475 + /// - storeId: 门店ID(必填)
  476 + /// - statisticsMonth: 统计月份,格式为YYYYMM(可选,不传则默认为当前月份)
  477 + /// </remarks>
  478 + /// <param name="input">查询参数</param>
  479 + /// <returns>客单价与项目数关系数据列表</returns>
  480 + /// <response code="200">成功返回数据</response>
  481 + /// <response code="400">参数错误</response>
  482 + /// <response code="500">服务器错误</response>
  483 + [HttpPost("GetCustomerPriceProjectRelation")]
  484 + public async Task<List<CustomerPriceProjectRelationOutput>> GetCustomerPriceProjectRelation([FromBody] StoreDashboardStatisticsInput input)
  485 + {
  486 + try
  487 + {
  488 + if (input == null || string.IsNullOrWhiteSpace(input.StoreId))
  489 + {
  490 + throw NCCException.Oh("门店ID不能为空");
  491 + }
  492 +
  493 + var statisticsMonth = input.StatisticsMonth;
  494 + if (string.IsNullOrWhiteSpace(statisticsMonth))
  495 + {
  496 + statisticsMonth = DateTime.Now.ToString("yyyyMM");
  497 + }
  498 +
  499 + // 解析月份获取时间范围
  500 + var year = int.Parse(statisticsMonth.Substring(0, 4));
  501 + var month = int.Parse(statisticsMonth.Substring(4, 2));
  502 + var startDate = new DateTime(year, month, 1);
  503 + var endDate = startDate.AddMonths(1).AddDays(-1);
  504 + var endDateTime = statisticsMonth == DateTime.Now.ToString("yyyyMM")
  505 + ? DateTime.Now
  506 + : endDate.Date.AddHours(23).AddMinutes(59).AddSeconds(59);
  507 +
  508 + // 查询每个会员的客单价和项目数
  509 + var sql = $@"
  510 + SELECT
  511 + xh.Hy as MemberId,
  512 + COALESCE(SUM(xh.Xfje), 0) as TotalAmount,
  513 + COUNT(DISTINCT CONCAT(xh.Hy, '-', DATE_FORMAT(xh.Hksj, '%Y-%m-%d'))) as PersonCount,
  514 + COALESCE(SUM(COALESCE(px.F_OriginalProjectNumber, px.F_ProjectNumber, 0)), 0) as ProjectCount
  515 + FROM lq_xh_hyhk xh
  516 + LEFT JOIN lq_xh_pxmx px ON px.F_ConsumeInfoId = xh.F_Id AND px.F_IsEffective = 1
  517 + WHERE xh.Md = '{input.StoreId}'
  518 + AND xh.F_IsEffective = 1
  519 + AND xh.Hksj >= '{startDate:yyyy-MM-dd 00:00:00}'
  520 + AND xh.Hksj <= '{endDateTime:yyyy-MM-dd HH:mm:ss}'
  521 + GROUP BY xh.Hy
  522 + HAVING TotalAmount > 0 AND PersonCount > 0";
  523 +
  524 + var memberData = await _db.Ado.SqlQueryAsync<dynamic>(sql);
  525 +
  526 + // 按客单价和项目数分组,计算每个区间的会员数
  527 + var result = new List<CustomerPriceProjectRelationOutput>();
  528 +
  529 + if (memberData != null && memberData.Any())
  530 + {
  531 + var groupedData = memberData
  532 + .Select(x => new
  533 + {
  534 + AvgAmountPerPerson = Convert.ToDecimal(x.TotalAmount ?? 0) / Convert.ToDecimal(x.PersonCount ?? 1),
  535 + AvgProjectPerPerson = Convert.ToDecimal(x.ProjectCount ?? 0) / Convert.ToDecimal(x.PersonCount ?? 1)
  536 + })
  537 + .Where(x => x.AvgProjectPerPerson <= 100) // 过滤异常数据(项目数>100的可能是数据错误)
  538 + .GroupBy(x => new
  539 + {
  540 + // 按客单价区间分组(每100元一个区间)
  541 + PriceRange = (int)(x.AvgAmountPerPerson / 100) * 100,
  542 + // 按项目数区间分组(每0.5个项目一个区间)
  543 + ProjectRange = (int)(x.AvgProjectPerPerson / 0.5m) * 0.5m
  544 + })
  545 + .Select(g => new CustomerPriceProjectRelationOutput
  546 + {
  547 + AvgAmountPerPerson = g.Key.PriceRange + 50, // 区间中点
  548 + AvgProjectPerPerson = (decimal)g.Average(x => (double)x.AvgProjectPerPerson), // 使用实际平均值,而不是区间中点
  549 + MemberCount = g.Count()
  550 + })
  551 + .OrderBy(x => x.AvgAmountPerPerson)
  552 + .ThenBy(x => x.AvgProjectPerPerson)
  553 + .ToList();
  554 +
  555 + result = groupedData;
  556 + }
  557 +
  558 + return result;
  559 + }
  560 + catch (Exception ex)
  561 + {
  562 + _logger.LogError(ex, "获取门店客单价与项目数关系数据失败,门店ID:{StoreId}", input?.StoreId);
  563 + throw NCCException.Oh($"获取门店客单价与项目数关系数据失败:{ex.Message}");
  564 + }
  565 + }
  566 +
  567 + /// <summary>
  568 + /// 获取门店一周运营热力图数据
  569 + /// </summary>
  570 + /// <remarks>
  571 + /// 获取指定门店在指定月份的一周运营热力图数据(周一到周日,每个时间段9:00-21:00的客流量)
  572 + ///
  573 + /// 示例请求:
  574 + /// ```json
  575 + /// {
  576 + /// "storeId": "1649328471923847169",
  577 + /// "statisticsMonth": "202601"
  578 + /// }
  579 + /// ```
  580 + ///
  581 + /// 参数说明:
  582 + /// - storeId: 门店ID(必填)
  583 + /// - statisticsMonth: 统计月份,格式为YYYYMM(可选,不传则默认为当前月份)
  584 + /// </remarks>
  585 + /// <param name="input">查询参数</param>
  586 + /// <returns>一周运营热力图数据列表</returns>
  587 + /// <response code="200">成功返回数据</response>
  588 + /// <response code="400">参数错误</response>
  589 + /// <response code="500">服务器错误</response>
  590 + [HttpPost("GetWeeklyHeatmap")]
  591 + public async Task<List<WeeklyHeatmapOutput>> GetWeeklyHeatmap([FromBody] StoreDashboardStatisticsInput input)
  592 + {
  593 + try
  594 + {
  595 + if (input == null || string.IsNullOrWhiteSpace(input.StoreId))
  596 + {
  597 + throw NCCException.Oh("门店ID不能为空");
  598 + }
  599 +
  600 + var statisticsMonth = input.StatisticsMonth;
  601 + if (string.IsNullOrWhiteSpace(statisticsMonth))
  602 + {
  603 + statisticsMonth = DateTime.Now.ToString("yyyyMM");
  604 + }
  605 +
  606 + // 解析月份获取时间范围
  607 + var year = int.Parse(statisticsMonth.Substring(0, 4));
  608 + var month = int.Parse(statisticsMonth.Substring(4, 2));
  609 + var startDate = new DateTime(year, month, 1);
  610 + var endDate = startDate.AddMonths(1).AddDays(-1);
  611 + var endDateTime = statisticsMonth == DateTime.Now.ToString("yyyyMM")
  612 + ? DateTime.Now
  613 + : endDate.Date.AddHours(23).AddMinutes(59).AddSeconds(59);
  614 +
  615 + // 查询每个时间段(9:00-21:00)每个星期几的客流量
  616 + var sql = $@"
  617 + SELECT
  618 + DATE_FORMAT(xh.Hksj, '%H:00') as TimeSlot,
  619 + DAYOFWEEK(xh.Hksj) - 1 as DayOfWeek,
  620 + COUNT(DISTINCT CONCAT(xh.Hy, '-', DATE_FORMAT(xh.Hksj, '%Y-%m-%d'))) as CustomerFlow
  621 + FROM lq_xh_hyhk xh
  622 + WHERE xh.Md = '{input.StoreId}'
  623 + AND xh.F_IsEffective = 1
  624 + AND xh.Hksj >= '{startDate:yyyy-MM-dd 00:00:00}'
  625 + AND xh.Hksj <= '{endDateTime:yyyy-MM-dd HH:mm:ss}'
  626 + AND HOUR(xh.Hksj) >= 9
  627 + AND HOUR(xh.Hksj) <= 21
  628 + GROUP BY DATE_FORMAT(xh.Hksj, '%H:00'), DAYOFWEEK(xh.Hksj) - 1
  629 + ORDER BY DayOfWeek, TimeSlot";
  630 +
  631 + var heatmapData = await _db.Ado.SqlQueryAsync<dynamic>(sql);
  632 +
  633 + var result = new List<WeeklyHeatmapOutput>();
  634 + if (heatmapData != null && heatmapData.Any())
  635 + {
  636 + result = heatmapData.Select(x => new WeeklyHeatmapOutput
  637 + {
  638 + TimeSlot = x.TimeSlot?.ToString() ?? "",
  639 + DayOfWeek = Convert.ToInt32(x.DayOfWeek ?? 0),
  640 + CustomerFlow = Convert.ToInt32(x.CustomerFlow ?? 0)
  641 + }).ToList();
  642 + }
  643 +
  644 + return result;
  645 + }
  646 + catch (Exception ex)
  647 + {
  648 + _logger.LogError(ex, "获取门店一周运营热力图数据失败,门店ID:{StoreId}", input?.StoreId);
  649 + throw NCCException.Oh($"获取门店一周运营热力图数据失败:{ex.Message}");
  650 + }
  651 + }
  652 + }
  653 +}
  654 +
... ...
netcore/src/Modularity/Extend/NCC.Extend/LqYyjlService.cs
... ... @@ -375,5 +375,101 @@ namespace NCC.Extend.LqYyjl
375 375 throw NCCException.Oh(ErrorCode.COM1001);
376 376 }
377 377 #endregion
  378 +
  379 + #region 我的今日预约
  380 +
  381 + /// <summary>
  382 + /// 获取我的今日预约
  383 + /// </summary>
  384 + /// <remarks>
  385 + /// 查询当前登录用户今天预约的客户列表
  386 + ///
  387 + /// 查询逻辑:
  388 + /// 1. 查询当前用户(yyr字段)的预约记录
  389 + /// 2. 筛选预约开始时间(yysj)在今天范围内的记录
  390 + /// 3. 关联查询客户、健康师、门店等信息
  391 + /// 4. 按预约时间排序返回
  392 + /// </remarks>
  393 + /// <returns>今日预约客户列表</returns>
  394 + /// <response code="200">查询成功</response>
  395 + /// <response code="500">服务器错误</response>
  396 + [HttpGet("GetMyTodayAppointments")]
  397 + public async Task<List<MyTodayAppointmentOutput>> GetMyTodayAppointmentsAsync()
  398 + {
  399 + try
  400 + {
  401 + var currentUserId = _userManager.UserId;
  402 + var today = DateTime.Now.Date;
  403 +
  404 + // 查询当前用户今天的预约记录
  405 + var appointments = await _db.Queryable<LqYyjlEntity>()
  406 + .Where(x => x.Yyr == currentUserId)
  407 + .Where(x => x.Yysj != null && x.Yysj.Value.Date == today)
  408 + .OrderBy(x => x.Yysj)
  409 + .ToListAsync();
  410 +
  411 + if (!appointments.Any())
  412 + {
  413 + return new List<MyTodayAppointmentOutput>();
  414 + }
  415 +
  416 + // 获取门店信息
  417 + var storeIds = appointments.Where(x => !string.IsNullOrEmpty(x.Djmd)).Select(x => x.Djmd).Distinct().ToList();
  418 + var stores = new Dictionary<string, string>();
  419 + if (storeIds.Any())
  420 + {
  421 + var storeList = await _db.Queryable<LqMdxxEntity>()
  422 + .Where(x => storeIds.Contains(x.Id))
  423 + .Select(x => new { x.Id, x.Dm })
  424 + .ToListAsync();
  425 + stores = storeList.ToDictionary(k => k.Id, v => v.Dm);
  426 + }
  427 +
  428 + // 获取健康师信息
  429 + var healthCoachIds = appointments.Where(x => !string.IsNullOrEmpty(x.Yyjks)).Select(x => x.Yyjks).Distinct().ToList();
  430 + var healthCoaches = new Dictionary<string, string>();
  431 + if (healthCoachIds.Any())
  432 + {
  433 + var healthCoachList = await _db.Queryable<UserEntity>()
  434 + .Where(x => healthCoachIds.Contains(x.Id))
  435 + .Select(x => new { x.Id, x.RealName })
  436 + .ToListAsync();
  437 + healthCoaches = healthCoachList.ToDictionary(k => k.Id, v => v.RealName);
  438 + }
  439 +
  440 + // 构建返回结果
  441 + var result = appointments.Select(x =>
  442 + {
  443 + var startTime = x.Yysj?.ToString("HH:mm") ?? "";
  444 + var endTime = x.Yyjs?.ToString("HH:mm") ?? "";
  445 + var timeDisplay = !string.IsNullOrEmpty(endTime) ? $"{startTime}-{endTime}" : startTime;
  446 +
  447 + return new MyTodayAppointmentOutput
  448 + {
  449 + AppointmentId = x.Id,
  450 + CustomerId = x.Gk,
  451 + CustomerName = x.Gkxm,
  452 + CustomerType = x.Gklx,
  453 + ProjectName = x.Yytyxm,
  454 + AppointmentStartTime = x.Yysj,
  455 + AppointmentEndTime = x.Yyjs,
  456 + HealthCoachId = x.Yyjks,
  457 + HealthCoachName = !string.IsNullOrEmpty(x.Yyjks) && healthCoaches.ContainsKey(x.Yyjks) ? healthCoaches[x.Yyjks] : null,
  458 + Status = x.F_Status,
  459 + StoreId = x.Djmd,
  460 + StoreName = !string.IsNullOrEmpty(x.Djmd) && stores.ContainsKey(x.Djmd) ? stores[x.Djmd] : null,
  461 + AppointmentTimeDisplay = timeDisplay
  462 + };
  463 + }).ToList();
  464 +
  465 + return result;
  466 + }
  467 + catch (Exception ex)
  468 + {
  469 + throw NCCException.Oh($"获取我的今日预约失败:{ex.Message}");
  470 + }
  471 + }
  472 +
  473 + #endregion
378 474 }
379 475 }
... ...
scripts/sh/test_category_monthly_performance.sh 0 → 100755
  1 +#!/bin/bash
  2 +
  3 +# 测试门店各分类月度业绩接口
  4 +# 使用绿纤紫荆店进行测试
  5 +
  6 +BASE_URL="http://localhost:2011"
  7 +STORE_ID="1649328471923847169" # 绿纤紫荆店
  8 +STATISTICS_MONTH="202601"
  9 +
  10 +echo "=========================================="
  11 +echo "测试门店各分类月度业绩接口"
  12 +echo "=========================================="
  13 +echo ""
  14 +
  15 +# 1. 获取登录token
  16 +echo "1. 获取登录token..."
  17 +TOKEN_RESPONSE=$(curl -s -X POST "${BASE_URL}/api/oauth/Login" \
  18 + -H "Content-Type: application/x-www-form-urlencoded" \
  19 + -d "account=admin&password=e10adc3949ba59abbe56e057f20f883e")
  20 +
  21 +TOKEN=$(echo "$TOKEN_RESPONSE" | jq -r '.data.token // empty')
  22 +
  23 +if [ -z "$TOKEN" ] || [ "$TOKEN" = "null" ]; then
  24 + echo "❌ 获取token失败"
  25 + echo "响应: $TOKEN_RESPONSE"
  26 + exit 1
  27 +fi
  28 +
  29 +echo "✅ Token获取成功"
  30 +echo ""
  31 +
  32 +# 2. 调用各分类月度业绩接口
  33 +echo "2. 调用各分类月度业绩接口..."
  34 +echo "门店ID: $STORE_ID"
  35 +echo "统计月份: $STATISTICS_MONTH"
  36 +echo ""
  37 +
  38 +RESPONSE=$(curl -s -X POST "${BASE_URL}/api/Extend/LqStoreDashboard/GetCategoryMonthlyPerformance" \
  39 + -H "Content-Type: application/json" \
  40 + -H "Authorization: ${TOKEN}" \
  41 + -d "{
  42 + \"storeId\": \"${STORE_ID}\",
  43 + \"statisticsMonth\": \"${STATISTICS_MONTH}\"
  44 + }")
  45 +
  46 +echo "响应结果:"
  47 +echo "$RESPONSE" | jq '.'
  48 +
  49 +echo ""
  50 +echo "=========================================="
  51 +
  52 +# 检查响应
  53 +CODE=$(echo "$RESPONSE" | jq -r '.code // empty')
  54 +if [ "$CODE" = "200" ]; then
  55 + echo "✅ 接口调用成功"
  56 + DATA=$(echo "$RESPONSE" | jq -r '.data // empty')
  57 + if [ "$DATA" != "null" ] && [ -n "$DATA" ]; then
  58 + DATA_COUNT=$(echo "$RESPONSE" | jq '.data | length')
  59 + echo "✅ 返回数据条数: $DATA_COUNT"
  60 + echo ""
  61 + echo "前3条数据示例:"
  62 + echo "$RESPONSE" | jq '.data[0:3]'
  63 + else
  64 + echo "⚠️ 返回数据为空"
  65 + fi
  66 +else
  67 + echo "❌ 接口调用失败"
  68 + MSG=$(echo "$RESPONSE" | jq -r '.msg // "未知错误"')
  69 + echo "错误信息: $MSG"
  70 +fi
  71 +
  72 +echo "=========================================="
  73 +
... ...
scripts/sh/test_monthly_trend.sh 0 → 100755
  1 +#!/bin/bash
  2 +
  3 +# 测试门店近12个月业绩趋势接口
  4 +
  5 +BASE_URL="http://localhost:2011"
  6 +STORE_ID="1649328471923847169" # 绿纤紫荆店
  7 +STATISTICS_MONTH="202601"
  8 +
  9 +echo "=========================================="
  10 +echo "测试门店近12个月业绩趋势接口"
  11 +echo "=========================================="
  12 +echo ""
  13 +
  14 +# 1. 获取登录token
  15 +echo "1. 获取登录token..."
  16 +TOKEN_RESPONSE=$(curl -s -X POST "${BASE_URL}/api/oauth/Login" \
  17 + -H "Content-Type: application/x-www-form-urlencoded" \
  18 + -d "account=admin&password=e10adc3949ba59abbe56e057f20f883e")
  19 +
  20 +TOKEN=$(echo "$TOKEN_RESPONSE" | jq -r '.data.token // empty')
  21 +
  22 +if [ -z "$TOKEN" ] || [ "$TOKEN" = "null" ]; then
  23 + echo "❌ 获取token失败"
  24 + echo "响应: $TOKEN_RESPONSE"
  25 + exit 1
  26 +fi
  27 +
  28 +echo "✅ Token获取成功"
  29 +echo ""
  30 +
  31 +# 2. 调用近12个月业绩趋势接口
  32 +echo "2. 调用近12个月业绩趋势接口..."
  33 +echo "门店ID: $STORE_ID"
  34 +echo "统计月份: $STATISTICS_MONTH"
  35 +echo ""
  36 +
  37 +RESPONSE=$(curl -s -X POST "${BASE_URL}/api/Extend/LqReport/get-store-monthly-trend" \
  38 + -H "Content-Type: application/json" \
  39 + -H "Authorization: ${TOKEN}" \
  40 + -d "{
  41 + \"storeId\": \"${STORE_ID}\",
  42 + \"statisticsMonth\": \"${STATISTICS_MONTH}\"
  43 + }")
  44 +
  45 +echo "响应结果:"
  46 +echo "$RESPONSE" | jq '.'
  47 +
  48 +echo ""
  49 +echo "=========================================="
  50 +
  51 +# 检查响应
  52 +CODE=$(echo "$RESPONSE" | jq -r '.code // empty')
  53 +if [ "$CODE" = "200" ]; then
  54 + echo "✅ 接口调用成功"
  55 + DATA=$(echo "$RESPONSE" | jq -r '.data // empty')
  56 + if [ "$DATA" != "null" ] && [ -n "$DATA" ]; then
  57 + DATA_COUNT=$(echo "$RESPONSE" | jq '.data | length')
  58 + echo "✅ 返回数据条数: $DATA_COUNT"
  59 + echo ""
  60 + echo "前3条数据示例:"
  61 + echo "$RESPONSE" | jq '.data[0:3]'
  62 + echo ""
  63 + echo "数据统计:"
  64 + echo "$RESPONSE" | jq '.data | map({Month, Billing: .BillingPerformance, Consume: .ConsumePerformance, Net: .NetPerformance})'
  65 + else
  66 + echo "⚠️ 返回数据为空"
  67 + fi
  68 +else
  69 + echo "❌ 接口调用失败"
  70 + MSG=$(echo "$RESPONSE" | jq -r '.msg // "未知错误"')
  71 + echo "错误信息: $MSG"
  72 +fi
  73 +
  74 +echo "=========================================="
  75 +
... ...
scripts/sh/test_my_today_appointments.sh 0 → 100755
  1 +#!/bin/bash
  2 +
  3 +# 测试我的今日预约接口
  4 +
  5 +echo "=== 测试我的今日预约接口 ==="
  6 +echo ""
  7 +
  8 +# 1. 获取token
  9 +echo "=== 1. 获取Token ==="
  10 +TOKEN_RESPONSE=$(curl -s -X POST "http://localhost:2011/api/oauth/Login" \
  11 + -H "Content-Type: application/x-www-form-urlencoded" \
  12 + -d "account=admin&password=e10adc3949ba59abbe56e057f20f883e")
  13 +
  14 +TOKEN=$(echo "$TOKEN_RESPONSE" | python3 -c "import sys, json; data=json.load(sys.stdin); print(data['data']['token'])" 2>/dev/null)
  15 +
  16 +if [ -z "$TOKEN" ]; then
  17 + echo "❌ Token获取失败"
  18 + echo "$TOKEN_RESPONSE"
  19 + exit 1
  20 +fi
  21 +
  22 +echo "✅ Token获取成功"
  23 +echo ""
  24 +
  25 +# 2. 测试接口
  26 +echo "=== 2. 测试接口 - 我的今日预约 ==="
  27 +RESPONSE=$(curl -s -X GET "http://localhost:2011/api/Extend/LqYyjl/GetMyTodayAppointments" \
  28 + -H "Authorization: $TOKEN")
  29 +
  30 +echo "$RESPONSE" | python3 -c "
  31 +import sys, json
  32 +from datetime import datetime
  33 +
  34 +try:
  35 + data = json.load(sys.stdin)
  36 + code = data.get('code')
  37 + msg = data.get('msg')
  38 + items = data.get('data', [])
  39 +
  40 + print(f'HTTP状态码: {code}')
  41 + print(f'消息: {msg}')
  42 + print(f'预约记录数: {len(items)}')
  43 + print('')
  44 +
  45 + if code == 200:
  46 + if items:
  47 + print('✅ 接口测试成功!')
  48 + print('')
  49 + print('=' * 100)
  50 + print('今日预约客户列表:')
  51 + print('=' * 100)
  52 + print(f'{'序号':<5} {'顾客姓名':<12} {'预约时间':<15} {'体验项目':<20} {'健康师':<12} {'门店':<15} {'状态':<10}')
  53 + print('-' * 100)
  54 +
  55 + for idx, item in enumerate(items, 1):
  56 + customer_name = item.get('CustomerName', '未知')
  57 + time_display = item.get('AppointmentTimeDisplay', '')
  58 + project = item.get('ProjectName', '')
  59 + health_coach = item.get('HealthCoachName', '无')
  60 + store = item.get('StoreName', '无')
  61 + status = item.get('Status', '')
  62 +
  63 + print(f'{idx:<5} {customer_name:<12} {time_display:<15} {project:<20} {health_coach:<12} {store:<15} {status:<10}')
  64 +
  65 + print('')
  66 + print('=' * 100)
  67 + print('字段完整性检查:')
  68 + print('-' * 100)
  69 +
  70 + required_fields = ['AppointmentId', 'CustomerId', 'CustomerName', 'ProjectName', 'AppointmentStartTime', 'AppointmentTimeDisplay', 'HealthCoachName', 'Status', 'StoreName']
  71 +
  72 + all_ok = True
  73 + for field in required_fields:
  74 + missing_count = sum(1 for item in items if field not in item or (item[field] is None and field not in ['HealthCoachName', 'StoreName']))
  75 + if missing_count > 0:
  76 + print(f'❌ {field}: {missing_count}条记录缺失')
  77 + all_ok = False
  78 + else:
  79 + print(f'✅ {field}: 所有记录都有此字段')
  80 +
  81 + if all_ok:
  82 + print('')
  83 + print('✅ 所有必需字段都存在!')
  84 +
  85 + # 显示详细信息
  86 + print('')
  87 + print('=' * 100)
  88 + print('前3条记录详细信息:')
  89 + print('=' * 100)
  90 +
  91 + for idx, item in enumerate(items[:3], 1):
  92 + print(f'\n【预约 {idx}】')
  93 + print(f' 预约编号: {item.get(\"AppointmentId\", \"缺失\")}')
  94 + print(f' 顾客ID: {item.get(\"CustomerId\", \"缺失\")}')
  95 + print(f' 顾客姓名: {item.get(\"CustomerName\", \"缺失\")}')
  96 + print(f' 顾客类型: {item.get(\"CustomerType\", \"无\")}')
  97 + print(f' 体验项目: {item.get(\"ProjectName\", \"缺失\")}')
  98 +
  99 + if 'AppointmentStartTime' in item and item['AppointmentStartTime']:
  100 + start_time = datetime.fromtimestamp(item['AppointmentStartTime'] / 1000).strftime('%Y-%m-%d %H:%M')
  101 + print(f' ✅ 预约开始时间: {start_time}')
  102 + else:
  103 + print(f' ❌ 预约开始时间: 缺失')
  104 +
  105 + if 'AppointmentEndTime' in item and item['AppointmentEndTime']:
  106 + end_time = datetime.fromtimestamp(item['AppointmentEndTime'] / 1000).strftime('%Y-%m-%d %H:%M')
  107 + print(f' ✅ 预约结束时间: {end_time}')
  108 + else:
  109 + print(f' ⚠️ 预约结束时间: 无')
  110 +
  111 + print(f' 预约时间显示: {item.get(\"AppointmentTimeDisplay\", \"缺失\")}')
  112 + print(f' 健康师: {item.get(\"HealthCoachName\", \"无\")}')
  113 + print(f' 门店: {item.get(\"StoreName\", \"无\")}')
  114 + print(f' 状态: {item.get(\"Status\", \"缺失\")}')
  115 + else:
  116 + print('⚠️ 接口返回成功,但今天没有预约记录')
  117 + print('')
  118 + print('提示:如果今天确实有预约,请检查:')
  119 + print(' 1. 预约记录的 yyr 字段是否匹配当前用户ID')
  120 + print(' 2. 预约记录的 yysj 字段是否在今天')
  121 + else:
  122 + print(f'❌ 接口返回错误: {msg}')
  123 + print('完整响应:')
  124 + print(json.dumps(data, indent=2, ensure_ascii=False))
  125 +
  126 +except json.JSONDecodeError as e:
  127 + print('❌ JSON解析失败')
  128 + print('响应内容:')
  129 + print(sys.stdin.read())
  130 +except Exception as e:
  131 + print(f'❌ 处理失败: {e}')
  132 + import traceback
  133 + traceback.print_exc()
  134 +"
  135 +
  136 +echo ""
  137 +echo "=== 测试完成 ==="
  138 +
... ...
scripts/sh/test_payment_reminder.sh 0 → 100755
  1 +#!/bin/bash
  2 +
  3 +# 测试付款提醒列表接口
  4 +
  5 +echo "=== 测试付款提醒列表接口 ==="
  6 +echo ""
  7 +
  8 +# 1. 获取token
  9 +echo "=== 1. 获取Token ==="
  10 +TOKEN_RESPONSE=$(curl -s -X POST "http://localhost:2011/api/oauth/Login" \
  11 + -H "Content-Type: application/x-www-form-urlencoded" \
  12 + -d "account=admin&password=e10adc3949ba59abbe56e057f20f883e")
  13 +
  14 +TOKEN=$(echo "$TOKEN_RESPONSE" | python3 -c "import sys, json; data=json.load(sys.stdin); print(data['data']['token'])" 2>/dev/null)
  15 +
  16 +if [ -z "$TOKEN" ]; then
  17 + echo "❌ Token获取失败"
  18 + echo "$TOKEN_RESPONSE"
  19 + exit 1
  20 +fi
  21 +
  22 +echo "✅ Token获取成功"
  23 +echo ""
  24 +
  25 +# 2. 测试接口 - 查询所有提醒
  26 +echo "=== 2. 测试接口 - 查询所有付款提醒 ==="
  27 +RESPONSE=$(curl -s -X POST "http://localhost:2011/api/Extend/LqContract/GetPaymentReminderList" \
  28 + -H "Authorization: $TOKEN" \
  29 + -H "Content-Type: application/json" \
  30 + -d '{"onlyOverdue": false}')
  31 +
  32 +echo "$RESPONSE" | python3 -c "
  33 +import sys, json
  34 +from datetime import datetime
  35 +
  36 +try:
  37 + data = json.load(sys.stdin)
  38 + code = data.get('code')
  39 + msg = data.get('msg')
  40 + items = data.get('data', [])
  41 +
  42 + print(f'HTTP状态码: {code}')
  43 + print(f'消息: {msg}')
  44 + print(f'数据条数: {len(items)}')
  45 + print('')
  46 +
  47 + if code == 200 and items:
  48 + print('✅ 接口测试成功!')
  49 + print('')
  50 + print('=' * 100)
  51 + print('前3条记录详情:')
  52 + print('=' * 100)
  53 +
  54 + for idx, item in enumerate(items[:3], 1):
  55 + print(f'\n【记录 {idx}】')
  56 + print(f' 合同ID: {item.get(\"ContractId\", \"缺失\")}')
  57 + print(f' 明细ID: {item.get(\"DetailId\", \"缺失\")}')
  58 + print(f' 店名: {item.get(\"StoreName\", \"缺失\")}')
  59 + print(f' 合同标题: {item.get(\"Title\", \"缺失\")}')
  60 + print(f' 收款方: {item.get(\"TenantName\", \"无\")}')
  61 +
  62 + # 付款时间
  63 + if 'PaymentDate' in item and item['PaymentDate']:
  64 + payment_date = datetime.fromtimestamp(item['PaymentDate'] / 1000).strftime('%Y-%m-%d')
  65 + print(f' ✅ 付款时间: {payment_date}')
  66 + else:
  67 + print(f' ❌ 付款时间: 缺失')
  68 +
  69 + # 付款金额
  70 + if 'PaymentAmount' in item:
  71 + print(f' ✅ 付款金额: {item[\"PaymentAmount\"]:,.2f} 元')
  72 + else:
  73 + print(f' ❌ 付款金额: 缺失')
  74 +
  75 + # 应缴月份
  76 + if 'PaymentMonth' in item:
  77 + print(f' ✅ 应缴月份: {item[\"PaymentMonth\"]}')
  78 + else:
  79 + print(f' ❌ 应缴月份: 缺失')
  80 +
  81 + # 提醒时间
  82 + if 'ReminderDate' in item and item['ReminderDate']:
  83 + reminder_date = datetime.fromtimestamp(item['ReminderDate'] / 1000).strftime('%Y-%m-%d')
  84 + print(f' ✅ 提醒时间: {reminder_date}')
  85 + else:
  86 + print(f' ⚠️ 提醒时间: 无')
  87 +
  88 + # 状态
  89 + days = item.get('DaysUntilPayment', 0)
  90 + is_overdue = item.get('IsOverdue', False)
  91 + status = '已过期' if is_overdue else f'{days}天后'
  92 + print(f' ✅ 状态: {status} (距离付款 {days} 天)')
  93 +
  94 + print('')
  95 + print('=' * 100)
  96 + print('字段完整性检查:')
  97 + print('-' * 100)
  98 +
  99 + required_fields = ['ContractId', 'DetailId', 'StoreName', 'Title', 'TenantName', 'PaymentDate', 'PaymentAmount', 'PaymentMonth', 'ReminderDate', 'DaysUntilPayment', 'IsOverdue']
  100 +
  101 + all_ok = True
  102 + for field in required_fields:
  103 + missing_count = sum(1 for item in items if field not in item or (item[field] is None and field not in ['ReminderDate', 'TenantName', 'Remarks']))
  104 + if missing_count > 0:
  105 + print(f'❌ {field}: {missing_count}条记录缺失')
  106 + all_ok = False
  107 + else:
  108 + print(f'✅ {field}: 所有记录都有此字段')
  109 +
  110 + if all_ok:
  111 + print('')
  112 + print('✅ 所有必需字段都存在!')
  113 +
  114 + # 统计信息
  115 + total_amount = sum(item.get('PaymentAmount', 0) for item in items)
  116 + overdue_count = sum(1 for item in items if item.get('IsOverdue', False))
  117 +
  118 + print('')
  119 + print('=' * 100)
  120 + print('统计信息:')
  121 + print('-' * 100)
  122 + print(f'总记录数: {len(items)}')
  123 + print(f'总付款金额: {total_amount:,.2f} 元')
  124 + print(f'已过期: {overdue_count} 条')
  125 + print(f'未过期: {len(items) - overdue_count} 条')
  126 +
  127 + elif code == 200 and not items:
  128 + print('⚠️ 接口返回成功,但没有数据')
  129 + else:
  130 + print(f'❌ 接口返回错误: {msg}')
  131 + print('完整响应:')
  132 + print(json.dumps(data, indent=2, ensure_ascii=False))
  133 +
  134 +except json.JSONDecodeError as e:
  135 + print('❌ JSON解析失败')
  136 + print('响应内容:')
  137 + print(sys.stdin.read())
  138 +except Exception as e:
  139 + print(f'❌ 处理失败: {e}')
  140 + print('响应内容:')
  141 + print(sys.stdin.read())
  142 +"
  143 +
  144 +echo ""
  145 +echo "=== 测试完成 ==="
  146 +
... ...
scripts/sh/test_store_dashboard_fixes.sh 0 → 100755
  1 +#!/bin/bash
  2 +
  3 +# 测试门店驾驶舱修复的接口
  4 +# 1. 健康师业绩排行(净业绩)
  5 +# 2. 门店对比分析(类型转换错误修复)
  6 +# 3. 拓客转化漏斗
  7 +
  8 +BASE_URL="http://localhost:2011"
  9 +STORE_ID="1649328471923847169" # 绿纤紫荆店
  10 +STATISTICS_MONTH="202512" # 2025年12月
  11 +
  12 +echo "=========================================="
  13 +echo "测试门店驾驶舱修复的接口"
  14 +echo "=========================================="
  15 +echo ""
  16 +
  17 +# 1. 获取Token
  18 +echo "1. 获取Token..."
  19 +TOKEN_BODY=$(curl -s -X POST "${BASE_URL}/api/oauth/Login" \
  20 + -H "Content-Type: application/x-www-form-urlencoded" \
  21 + -d "account=admin&password=e10adc3949ba59abbe56e057f20f883e")
  22 +
  23 +TOKEN=$(echo "$TOKEN_BODY" | python3 -c "import sys, json; data=json.load(sys.stdin); print(data['data']['token'])" 2>/dev/null | sed 's/Bearer //')
  24 +
  25 +if [ -z "$TOKEN" ]; then
  26 + echo "❌ 无法从响应中提取Token"
  27 + echo "响应内容: $TOKEN_BODY"
  28 + exit 1
  29 +fi
  30 +
  31 +echo "✅ Token获取成功"
  32 +echo ""
  33 +
  34 +# 2. 测试健康师业绩排行(净业绩)
  35 +echo "2. 测试健康师业绩排行(净业绩)..."
  36 +HEALTH_COACH_BODY=$(curl -s -X POST "${BASE_URL}/api/Extend/LqReport/get-store-health-coach-analysis" \
  37 + -H "Content-Type: application/json" \
  38 + -H "Authorization: Bearer ${TOKEN}" \
  39 + -d "{\"storeId\":\"${STORE_ID}\",\"statisticsMonth\":\"${STATISTICS_MONTH}\"}")
  40 +
  41 +HEALTH_COACH_CODE=$(echo "$HEALTH_COACH_BODY" | jq -r '.code' 2>/dev/null)
  42 +if [ "$HEALTH_COACH_CODE" != "200" ]; then
  43 + echo "❌ 健康师业绩排行接口返回错误"
  44 + echo "$HEALTH_COACH_BODY" | jq '.' 2>/dev/null || echo "$HEALTH_COACH_BODY"
  45 + exit 1
  46 +fi
  47 +
  48 +HEALTH_COACH_COUNT=$(echo "$HEALTH_COACH_BODY" | jq '.data | length' 2>/dev/null)
  49 +echo "✅ 健康师业绩排行接口成功,返回 ${HEALTH_COACH_COUNT} 条记录"
  50 +if [ "$HEALTH_COACH_COUNT" -gt 0 ]; then
  51 + echo "前3条记录:"
  52 + echo "$HEALTH_COACH_BODY" | jq -r '.data[0:3] | .[] | " - \(.HealthCoachName): 开单=\(.BillingPerformance), 消耗=\(.ConsumePerformance), 净业绩=\(.TotalPerformance)"' 2>/dev/null
  53 +fi
  54 +echo ""
  55 +
  56 +# 3. 测试门店对比分析
  57 +echo "3. 测试门店对比分析(类型转换错误修复)..."
  58 +COMPARISON_BODY=$(curl -s -X POST "${BASE_URL}/api/Extend/LqReport/get-store-comparison-analysis" \
  59 + -H "Content-Type: application/json" \
  60 + -H "Authorization: Bearer ${TOKEN}" \
  61 + -d "{\"storeId\":\"${STORE_ID}\",\"statisticsMonth\":\"${STATISTICS_MONTH}\"}")
  62 +
  63 +COMPARISON_CODE=$(echo "$COMPARISON_BODY" | jq -r '.code' 2>/dev/null)
  64 +if [ "$COMPARISON_CODE" != "200" ]; then
  65 + echo "❌ 门店对比分析接口返回错误"
  66 + echo "$COMPARISON_BODY" | jq '.' 2>/dev/null || echo "$COMPARISON_BODY"
  67 + exit 1
  68 +fi
  69 +
  70 +echo "✅ 门店对比分析接口成功"
  71 +echo "$COMPARISON_BODY" | jq -r '.data | " 业绩排名: \(.PerformanceRanking)/\(.TotalStoreCount)\n 同类型门店平均业绩: \(.AvgPerformanceSameType) (门店数: \(.SameTypeStoreCount))\n 同组织门店平均业绩: \(.AvgPerformanceSameOrg) (门店数: \(.SameOrgStoreCount))"' 2>/dev/null
  72 +echo ""
  73 +
  74 +# 4. 测试拓客转化漏斗
  75 +echo "4. 测试拓客转化漏斗..."
  76 +FUNNEL_BODY=$(curl -s -X POST "${BASE_URL}/api/Extend/LqStoreDashboard/GetMemberConversionFunnel" \
  77 + -H "Content-Type: application/json" \
  78 + -H "Authorization: Bearer ${TOKEN}" \
  79 + -d "{\"storeId\":\"${STORE_ID}\",\"statisticsMonth\":\"${STATISTICS_MONTH}\"}")
  80 +
  81 +FUNNEL_CODE=$(echo "$FUNNEL_BODY" | jq -r '.code' 2>/dev/null)
  82 +if [ "$FUNNEL_CODE" != "200" ]; then
  83 + echo "❌ 拓客转化漏斗接口返回错误"
  84 + echo "$FUNNEL_BODY" | jq '.' 2>/dev/null || echo "$FUNNEL_BODY"
  85 + exit 1
  86 +fi
  87 +
  88 +echo "✅ 拓客转化漏斗接口成功"
  89 +echo "$FUNNEL_BODY" | jq -r '.data | " 拓客数: \(.ExpansionCount)\n 邀约数: \(.InviteCount)\n 预约数: \(.AppointmentCount)\n 耗卡数: \(.ConsumeCount)\n 开单数: \(.BillingCount)"' 2>/dev/null
  90 +echo ""
  91 +
  92 +echo "=========================================="
  93 +echo "✅ 所有接口测试通过!"
  94 +echo "=========================================="
  95 +
... ...
scripts/sh/test_store_dashboard_statistics.sh 0 → 100755
  1 +#!/bin/bash
  2 +
  3 +# 测试门店驾驶舱统计数据接口
  4 +
  5 +echo "=== 测试门店驾驶舱统计数据接口 ==="
  6 +echo ""
  7 +
  8 +# 1. 获取token
  9 +echo "=== 1. 获取Token ==="
  10 +TOKEN_RESPONSE=$(curl -s -X POST "http://localhost:2011/api/oauth/Login" \
  11 + -H "Content-Type: application/x-www-form-urlencoded" \
  12 + -d "account=admin&password=e10adc3949ba59abbe56e057f20f883e")
  13 +
  14 +TOKEN=$(echo "$TOKEN_RESPONSE" | python3 -c "import sys, json; data=json.load(sys.stdin); print(data['data']['token'])" 2>/dev/null)
  15 +
  16 +if [ -z "$TOKEN" ]; then
  17 + echo "❌ Token获取失败"
  18 + echo "$TOKEN_RESPONSE"
  19 + exit 1
  20 +fi
  21 +
  22 +echo "✅ Token获取成功"
  23 +echo ""
  24 +
  25 +# 2. 测试接口
  26 +echo "=== 2. 测试接口 - 门店驾驶舱统计数据 ==="
  27 +echo ""
  28 +
  29 +# 测试参数
  30 +STORE_ID="1649328471923847169"
  31 +STATISTICS_MONTH="202601"
  32 +
  33 +echo "请求参数:"
  34 +echo " - storeId: $STORE_ID"
  35 +echo " - statisticsMonth: $STATISTICS_MONTH"
  36 +echo ""
  37 +
  38 +RESPONSE=$(curl -s -X POST "http://localhost:2011/api/Extend/LqStoreDashboard/GetStatistics" \
  39 + -H "Authorization: $TOKEN" \
  40 + -H "Content-Type: application/json" \
  41 + -d "{
  42 + \"storeId\": \"$STORE_ID\",
  43 + \"statisticsMonth\": \"$STATISTICS_MONTH\"
  44 + }")
  45 +
  46 +echo "$RESPONSE" | python3 -c "
  47 +import sys, json
  48 +
  49 +try:
  50 + data = json.load(sys.stdin)
  51 + code = data.get('code')
  52 + msg = data.get('msg')
  53 + result = data.get('data', {})
  54 +
  55 + print('=' * 80)
  56 + print('门店驾驶舱统计数据 - 接口测试结果')
  57 + print('=' * 80)
  58 + print(f'HTTP状态码: {code}')
  59 + print(f'消息: {msg}')
  60 + print('')
  61 +
  62 + if code == 200:
  63 + print('✅ 接口测试成功!')
  64 + print('')
  65 + print('=' * 80)
  66 + print('返回数据:')
  67 + print('=' * 80)
  68 + print('【核心指标】')
  69 + print(f' 开单业绩: ¥{result.get(\"BillingPerformance\", 0):,.2f}')
  70 + print(f' 消耗业绩: ¥{result.get(\"ConsumePerformance\", 0):,.2f}')
  71 + print(f' 完成率: {result.get(\"CompletionRate\", 0):.2f}%')
  72 + print(f' 净业绩: ¥{result.get(\"NetPerformance\", 0):,.2f}')
  73 + print('')
  74 + print('【业绩概览数据】')
  75 + print(f' 开单次数: {result.get(\"BillingCount\", 0):,}')
  76 + print(f' 消耗次数: {result.get(\"ConsumeCount\", 0):,}')
  77 + print(f' 退卡次数: {result.get(\"RefundCount\", 0):,}')
  78 + print(f' 平均开单金额: ¥{result.get(\"AvgBillingAmount\", 0):,.2f}')
  79 + print(f' 平均消耗金额: ¥{result.get(\"AvgConsumeAmount\", 0):,.2f}')
  80 + print(f' 剩余权益: ¥{result.get(\"RemainingRightsAmount\", 0):,.2f}')
  81 + print(f' 目标业绩: ¥{result.get(\"TargetPerformance\", 0):,.2f}')
  82 + print(f' 退卡金额: ¥{result.get(\"RefundAmount\", 0):,.2f}')
  83 + print('')
  84 +
  85 + # 验证数据完整性
  86 + print('=' * 80)
  87 + print('数据完整性检查:')
  88 + print('-' * 80)
  89 +
  90 + required_fields = [
  91 + 'BillingPerformance', 'ConsumePerformance', 'CompletionRate', 'NetPerformance',
  92 + 'BillingCount', 'ConsumeCount', 'RefundCount',
  93 + 'AvgBillingAmount', 'AvgConsumeAmount',
  94 + 'RemainingRightsAmount', 'TargetPerformance', 'RefundAmount'
  95 + ]
  96 + all_ok = True
  97 +
  98 + for field in required_fields:
  99 + if field in result:
  100 + value = result[field]
  101 + if isinstance(value, (int, float)):
  102 + print(f'✅ {field}: {value} (类型正确)')
  103 + else:
  104 + print(f'❌ {field}: {value} (类型错误,应为数字)')
  105 + all_ok = False
  106 + else:
  107 + print(f'❌ {field}: 缺失')
  108 + all_ok = False
  109 +
  110 + if all_ok:
  111 + print('')
  112 + print('✅ 所有字段都存在且类型正确!')
  113 +
  114 + # 验证数据逻辑
  115 + print('')
  116 + print('=' * 80)
  117 + print('数据逻辑验证:')
  118 + print('-' * 80)
  119 +
  120 + billing = result.get('BillingPerformance', 0)
  121 + consume = result.get('ConsumePerformance', 0)
  122 + completion = result.get('CompletionRate', 0)
  123 + net = result.get('NetPerformance', 0)
  124 +
  125 + # 验证完成率是否合理(0-200%之间)
  126 + if 0 <= completion <= 200:
  127 + print(f'✅ 完成率在合理范围内: {completion:.2f}%')
  128 + else:
  129 + print(f'⚠️ 完成率超出合理范围: {completion:.2f}%')
  130 +
  131 + # 验证净业绩计算(开单业绩应该大于等于净业绩)
  132 + if billing >= net:
  133 + print(f'✅ 净业绩计算正确: 开单业绩({billing:,.2f}) >= 净业绩({net:,.2f})')
  134 + else:
  135 + print(f'❌ 净业绩计算异常: 开单业绩({billing:,.2f}) < 净业绩({net:,.2f})')
  136 +
  137 + # 验证数据是否非负
  138 + if billing >= 0 and consume >= 0 and completion >= 0 and net >= 0:
  139 + print('✅ 所有数据均为非负数')
  140 + else:
  141 + print('⚠️ 存在负数数据')
  142 +
  143 + else:
  144 + print(f'❌ 接口返回错误: {msg}')
  145 + print('完整响应:')
  146 + print(json.dumps(data, indent=2, ensure_ascii=False))
  147 +
  148 +except json.JSONDecodeError as e:
  149 + print('❌ JSON解析失败')
  150 + print('响应内容:')
  151 + print(sys.stdin.read())
  152 +except Exception as e:
  153 + print(f'❌ 处理失败: {e}')
  154 + import traceback
  155 + traceback.print_exc()
  156 +"
  157 +
  158 +echo ""
  159 +echo "=== 测试完成 ==="
  160 +
... ...
scripts/sh/test_store_dashboard_zijing.sh 0 → 100755
  1 +#!/bin/bash
  2 +
  3 +# 测试门店驾驶舱统计数据接口 - 绿纤紫荆店
  4 +
  5 +echo "=== 测试门店驾驶舱统计数据接口 - 绿纤紫荆店 ==="
  6 +echo ""
  7 +
  8 +# 1. 获取token
  9 +echo "=== 1. 获取Token ==="
  10 +TOKEN_RESPONSE=$(curl -s -X POST "http://localhost:2011/api/oauth/Login" \
  11 + -H "Content-Type: application/x-www-form-urlencoded" \
  12 + -d "account=admin&password=e10adc3949ba59abbe56e057f20f883e")
  13 +
  14 +TOKEN=$(echo "$TOKEN_RESPONSE" | python3 -c "import sys, json; data=json.load(sys.stdin); print(data['data']['token'])" 2>/dev/null)
  15 +
  16 +if [ -z "$TOKEN" ]; then
  17 + echo "❌ Token获取失败"
  18 + echo "$TOKEN_RESPONSE"
  19 + exit 1
  20 +fi
  21 +
  22 +echo "✅ Token获取成功"
  23 +echo ""
  24 +
  25 +# 2. 测试接口 - 使用当前月份
  26 +echo "=== 2. 测试接口 - 绿纤紫荆店 ==="
  27 +echo ""
  28 +
  29 +# 绿纤紫荆店门店ID
  30 +STORE_ID="1649328471923847169"
  31 +# 获取当前月份(YYYYMM格式)
  32 +CURRENT_MONTH=$(date +%Y%m)
  33 +
  34 +echo "请求参数:"
  35 +echo " - storeId: $STORE_ID (绿纤紫荆店)"
  36 +echo " - statisticsMonth: $CURRENT_MONTH"
  37 +echo ""
  38 +
  39 +RESPONSE=$(curl -s -X POST "http://localhost:2011/api/Extend/LqStoreDashboard/GetStatistics" \
  40 + -H "Authorization: $TOKEN" \
  41 + -H "Content-Type: application/json" \
  42 + -d "{
  43 + \"storeId\": \"$STORE_ID\",
  44 + \"statisticsMonth\": \"$CURRENT_MONTH\"
  45 + }")
  46 +
  47 +echo "$RESPONSE" | python3 -c "
  48 +import sys, json
  49 +
  50 +try:
  51 + data = json.load(sys.stdin)
  52 + code = data.get('code')
  53 + msg = data.get('msg')
  54 + result = data.get('data', {})
  55 +
  56 + print('=' * 80)
  57 + print('门店驾驶舱统计数据 - 接口测试结果')
  58 + print('=' * 80)
  59 + print(f'HTTP状态码: {code}')
  60 + print(f'消息: {msg}')
  61 + print('')
  62 +
  63 + if code == 200:
  64 + print('✅ 接口测试成功!')
  65 + print('')
  66 + print('=' * 80)
  67 + print('返回数据:')
  68 + print('=' * 80)
  69 + print('【核心指标】')
  70 + print(f' 开单业绩: ¥{result.get(\"BillingPerformance\", 0):,.2f}')
  71 + print(f' 消耗业绩: ¥{result.get(\"ConsumePerformance\", 0):,.2f}')
  72 + print(f' 完成率: {result.get(\"CompletionRate\", 0):.2f}%')
  73 + print(f' 净业绩: ¥{result.get(\"NetPerformance\", 0):,.2f}')
  74 + print('')
  75 + print('【业绩概览数据】')
  76 + print(f' 开单次数: {result.get(\"BillingCount\", 0):,}')
  77 + print(f' 消耗次数: {result.get(\"ConsumeCount\", 0):,}')
  78 + print(f' 退卡次数: {result.get(\"RefundCount\", 0):,}')
  79 + print(f' 平均开单金额: ¥{result.get(\"AvgBillingAmount\", 0):,.2f}')
  80 + print(f' 平均消耗金额: ¥{result.get(\"AvgConsumeAmount\", 0):,.2f}')
  81 + print(f' 剩余权益: ¥{result.get(\"RemainingRightsAmount\", 0):,.2f}')
  82 + print(f' 目标业绩: ¥{result.get(\"TargetPerformance\", 0):,.2f}')
  83 + print(f' 退卡金额: ¥{result.get(\"RefundAmount\", 0):,.2f}')
  84 + print('')
  85 +
  86 + # 检查数据是否为0
  87 + print('=' * 80)
  88 + print('数据检查:')
  89 + print('-' * 80)
  90 +
  91 + if result.get('BillingCount', 0) == 0:
  92 + print('⚠️ 开单次数为0,可能该月份没有开单数据')
  93 + if result.get('ConsumeCount', 0) == 0:
  94 + print('⚠️ 消耗次数为0,可能该月份没有消耗数据')
  95 + if result.get('RemainingRightsAmount', 0) == 0:
  96 + print('⚠️ 剩余权益为0,可能该门店没有会员权益数据')
  97 + if result.get('TargetPerformance', 0) == 0:
  98 + print('⚠️ 目标业绩为0,可能该月份没有设置目标')
  99 +
  100 + # 验证数据完整性
  101 + print('')
  102 + print('=' * 80)
  103 + print('数据完整性检查:')
  104 + print('-' * 80)
  105 +
  106 + required_fields = [
  107 + 'BillingPerformance', 'ConsumePerformance', 'CompletionRate', 'NetPerformance',
  108 + 'BillingCount', 'ConsumeCount', 'RefundCount',
  109 + 'AvgBillingAmount', 'AvgConsumeAmount',
  110 + 'RemainingRightsAmount', 'TargetPerformance', 'RefundAmount'
  111 + ]
  112 + all_ok = True
  113 +
  114 + for field in required_fields:
  115 + if field in result:
  116 + value = result[field]
  117 + if isinstance(value, (int, float)):
  118 + print(f'✅ {field}: {value} (类型正确)')
  119 + else:
  120 + print(f'❌ {field}: {value} (类型错误,应为数字)')
  121 + all_ok = False
  122 + else:
  123 + print(f'❌ {field}: 缺失')
  124 + all_ok = False
  125 +
  126 + if all_ok:
  127 + print('')
  128 + print('✅ 所有字段都存在且类型正确!')
  129 +
  130 + else:
  131 + print(f'❌ 接口返回错误: {msg}')
  132 + print('完整响应:')
  133 + print(json.dumps(data, indent=2, ensure_ascii=False))
  134 +
  135 +except json.JSONDecodeError as e:
  136 + print('❌ JSON解析失败')
  137 + print('响应内容:')
  138 + print(sys.stdin.read())
  139 +except Exception as e:
  140 + print(f'❌ 处理失败: {e}')
  141 + import traceback
  142 + traceback.print_exc()
  143 +"
  144 +
  145 +echo ""
  146 +echo "=== 测试完成 ==="
  147 +
... ...