Commit a7da69e393fc8e461e92fcb23b68e932051542c5
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 | + | ... | ... |