Commit 55bd8d57359a331eb51d2e359fcea545f79e80ba
1 parent
fd65ae16
fix: 修复全员战报明细到店数统计和导出接口返回格式
- 修复全员战报明细到店数统计问题:确保到店数不超过拓客数,同一会员多次到店只统计一次 - 修复全部门店开单记录汇总导出接口返回格式:使用统一的响应格式(code, msg, data) - 添加 lq_xh_hyhk 表备注字段功能:支持备注字段的创建、更新、查询和导出 - 优化到店数统计SQL逻辑:先获取拓客会员列表(去重),再统计有到店记录的会员数
Showing
15 changed files
with
1526 additions
and
14 deletions
docs/lq_xh_hyhk备注字段-接口说明.md
0 → 100644
| 1 | +# lq_xh_hyhk 表备注字段 - 接口说明 | |
| 2 | + | |
| 3 | +## 📡 修改的接口列表 | |
| 4 | + | |
| 5 | +### 1. 创建耗卡记录 | |
| 6 | +**接口地址**: `POST /api/Extend/LqXhHyhk` | |
| 7 | +**接口说明**: 创建新的耗卡记录,支持传入备注信息(可选,最大2000字符) | |
| 8 | +### 2. 更新耗卡记录 | |
| 9 | +**接口地址**: `PUT /api/Extend/LqXhHyhk/{id}` | |
| 10 | +**接口说明**: 更新耗卡记录信息,支持修改备注(可选,最大2000字符) | |
| 11 | +### 3. 更新耗卡备注 | |
| 12 | +**接口地址**: `PUT /api/Extend/LqXhHyhk/{id}/UpdateRemark` | |
| 13 | +**接口说明**: 专门用于更新耗卡记录的备注信息 | |
| 14 | +### 4. 查询耗卡详情 | |
| 15 | +**接口地址**: `GET /api/Extend/LqXhHyhk/{id}` | |
| 16 | +**接口说明**: 根据ID查询耗卡记录详情,返回包含备注信息 | |
| 17 | +### 5. 查询耗卡列表 | |
| 18 | +**接口地址**: `GET /api/Extend/LqXhHyhk` | |
| 19 | +**接口说明**: 分页查询耗卡记录列表,返回包含备注信息 | |
| 20 | +### 6. 按健康师ID查询耗卡列表 | |
| 21 | +**接口地址**: `GET /api/Extend/LqXhHyhk/GetListByJksId` | |
| 22 | +**接口说明**: 根据健康师ID查询耗卡记录列表,返回包含备注信息 | |
| 23 | +### 7. 按科技部老师ID查询耗卡列表 | |
| 24 | +**接口地址**: `GET /api/Extend/LqXhHyhk/GetListByKjbId` | |
| 25 | +**接口说明**: 根据科技部老师ID查询耗卡记录列表,返回包含备注信息 | |
| 0 | 26 | \ No newline at end of file | ... | ... |
docs/lq_xh_hyhk表添加备注字段-修改清单.md
0 → 100644
| 1 | +# lq_xh_hyhk 表添加备注字段 - 修改清单 | |
| 2 | + | |
| 3 | +## 📋 概述 | |
| 4 | + | |
| 5 | +在 `lq_xh_hyhk`(会员耗卡)表中添加备注字段,用于记录耗卡相关的备注信息。 | |
| 6 | + | |
| 7 | +## 🗄️ 数据库修改 | |
| 8 | + | |
| 9 | +### 1. 数据库表结构修改 | |
| 10 | + | |
| 11 | +**表名**: `lq_xh_hyhk` | |
| 12 | + | |
| 13 | +**新增字段**: | |
| 14 | +- **字段名**: `F_Remark` | |
| 15 | +- **数据类型**: `VARCHAR(500)` 或 `TEXT`(根据备注长度需求) | |
| 16 | +- **是否可空**: `YES` | |
| 17 | +- **默认值**: `NULL` | |
| 18 | +- **字段注释**: `备注` | |
| 19 | + | |
| 20 | +**SQL语句**: | |
| 21 | +```sql | |
| 22 | +ALTER TABLE lq_xh_hyhk | |
| 23 | +ADD COLUMN F_Remark VARCHAR(500) NULL COMMENT '备注' AFTER F_CancelRemark; | |
| 24 | +``` | |
| 25 | + | |
| 26 | +## 💻 后端代码修改 | |
| 27 | + | |
| 28 | +### 2. 实体类 (Entity) | |
| 29 | + | |
| 30 | +**文件**: `netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/lq_xh_hyhk/LqXhHyhkEntity.cs` | |
| 31 | + | |
| 32 | +**修改内容**: 在 `LqXhHyhkEntity` 类中添加备注属性 | |
| 33 | + | |
| 34 | +```csharp | |
| 35 | +/// <summary> | |
| 36 | +/// 备注 | |
| 37 | +/// </summary> | |
| 38 | +[SugarColumn(ColumnName = "F_Remark")] | |
| 39 | +public string Remark { get; set; } | |
| 40 | +``` | |
| 41 | + | |
| 42 | +**位置**: 建议添加在 `CancelRemark` 字段之后(第128行之后) | |
| 43 | + | |
| 44 | +--- | |
| 45 | + | |
| 46 | +### 3. DTO 类修改 | |
| 47 | + | |
| 48 | +#### 3.1 创建输入DTO | |
| 49 | + | |
| 50 | +**文件**: `netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqXhHyhk/LqXhHyhkCrInput.cs` | |
| 51 | + | |
| 52 | +**修改内容**: 添加备注字段 | |
| 53 | + | |
| 54 | +```csharp | |
| 55 | +/// <summary> | |
| 56 | +/// 备注 | |
| 57 | +/// </summary> | |
| 58 | +public string remark { get; set; } | |
| 59 | +``` | |
| 60 | + | |
| 61 | +**位置**: 建议添加在 `appointmentId` 字段之后(第90行之后) | |
| 62 | + | |
| 63 | +--- | |
| 64 | + | |
| 65 | +#### 3.2 详情输出DTO | |
| 66 | + | |
| 67 | +**文件**: `netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqXhHyhk/LqXhHyhkInfoOutput.cs` | |
| 68 | + | |
| 69 | +**修改内容**: 添加备注字段 | |
| 70 | + | |
| 71 | +```csharp | |
| 72 | +/// <summary> | |
| 73 | +/// 备注 | |
| 74 | +/// </summary> | |
| 75 | +public string remark { get; set; } | |
| 76 | +``` | |
| 77 | + | |
| 78 | +**位置**: 建议添加在 `cancelRemark` 字段之后(第113行之后) | |
| 79 | + | |
| 80 | +--- | |
| 81 | + | |
| 82 | +#### 3.3 列表输出DTO | |
| 83 | + | |
| 84 | +**文件**: `netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqXhHyhk/LqXhHyhkListOutput.cs` | |
| 85 | + | |
| 86 | +**修改内容**: 添加备注字段 | |
| 87 | + | |
| 88 | +```csharp | |
| 89 | +/// <summary> | |
| 90 | +/// 备注 | |
| 91 | +/// </summary> | |
| 92 | +public string remark { get; set; } | |
| 93 | +``` | |
| 94 | + | |
| 95 | +**位置**: 建议添加在 `signatureFile` 字段之后(第95行之后) | |
| 96 | + | |
| 97 | +--- | |
| 98 | + | |
| 99 | +**注意**: `LqXhHyhkUpInput` 继承自 `LqXhHyhkCrInput`,会自动包含备注字段,无需单独修改。 | |
| 100 | + | |
| 101 | +--- | |
| 102 | + | |
| 103 | +### 4. Service 方法修改 | |
| 104 | + | |
| 105 | +**文件**: `netcore/src/Modularity/Extend/NCC.Extend/LqXhHyhkService.cs` | |
| 106 | + | |
| 107 | +#### 4.1 GetInfo 方法(查询详情) | |
| 108 | + | |
| 109 | +**位置**: 第101行 `GetInfo(string id)` | |
| 110 | + | |
| 111 | +**修改说明**: | |
| 112 | +- 使用 `entity.Adapt<LqXhHyhkInfoOutput>()` 自动映射,无需手动修改 | |
| 113 | +- 但需要确保 `LqXhHyhkInfoOutput` 已包含 `remark` 字段 | |
| 114 | + | |
| 115 | +**影响**: ✅ 自动支持(通过 Adapt 映射) | |
| 116 | + | |
| 117 | +--- | |
| 118 | + | |
| 119 | +#### 4.2 GetList 方法(查询列表) | |
| 120 | + | |
| 121 | +**位置**: 第293行 `GetList([FromQuery] LqXhHyhkListQueryInput input)` | |
| 122 | + | |
| 123 | +**修改内容**: 在多个 `Select` 语句中添加 `remark` 字段 | |
| 124 | + | |
| 125 | +**需要修改的位置**: | |
| 126 | + | |
| 127 | +1. **第342-370行**: 基础查询的 Select 语句 | |
| 128 | + ```csharp | |
| 129 | + remark = it.Remark, | |
| 130 | + ``` | |
| 131 | + | |
| 132 | +2. **第590-611行**: 按健康师ID查询的 Select 语句(GetListByJksId) | |
| 133 | + ```csharp | |
| 134 | + remark = hyhk.Remark, | |
| 135 | + ``` | |
| 136 | + | |
| 137 | +3. **第720-741行**: 按科技部老师ID查询的 Select 语句(GetListByKjbId) | |
| 138 | + ```csharp | |
| 139 | + remark = hyhk.Remark, | |
| 140 | + ``` | |
| 141 | + | |
| 142 | +4. **第1142-1165行**: 其他查询条件的 Select 语句 | |
| 143 | + ```csharp | |
| 144 | + remark = it.Remark, | |
| 145 | + ``` | |
| 146 | + | |
| 147 | +--- | |
| 148 | + | |
| 149 | +#### 4.3 Create 方法(创建记录) | |
| 150 | + | |
| 151 | +**位置**: 第875行 `Create([FromBody] LqXhHyhkCrInput input)` | |
| 152 | + | |
| 153 | +**修改说明**: | |
| 154 | +- 使用 `input.Adapt<LqXhHyhkEntity>()` 自动映射,无需手动修改 | |
| 155 | +- 但需要确保 `LqXhHyhkCrInput` 已包含 `remark` 字段 | |
| 156 | + | |
| 157 | +**影响**: ✅ 自动支持(通过 Adapt 映射) | |
| 158 | + | |
| 159 | +--- | |
| 160 | + | |
| 161 | +#### 4.4 Update 方法(更新记录) | |
| 162 | + | |
| 163 | +**位置**: 第1259行 `Update(string id, [FromBody] LqXhHyhkUpInput input)` | |
| 164 | + | |
| 165 | +**修改说明**: | |
| 166 | +- 使用 `input.Adapt<LqXhHyhkEntity>()` 自动映射,无需手动修改 | |
| 167 | +- 但需要确保 `LqXhHyhkUpInput`(继承自 `LqXhHyhkCrInput`)已包含 `remark` 字段 | |
| 168 | + | |
| 169 | +**影响**: ✅ 自动支持(通过 Adapt 映射) | |
| 170 | + | |
| 171 | +--- | |
| 172 | + | |
| 173 | +#### 4.5 其他方法检查 | |
| 174 | + | |
| 175 | +以下方法可能也需要检查,但根据代码分析,它们主要使用实体类,会自动支持: | |
| 176 | + | |
| 177 | +- `Delete` 方法(第1504行)- 删除操作,无需修改 | |
| 178 | +- `Cancel` 方法(第1618行)- 作废操作,可能需要显示备注,但不需要修改逻辑 | |
| 179 | +- `ExportConsumeItemDetailList` 方法(第2340行)- 导出功能,如果导出包含备注,需要修改 | |
| 180 | + | |
| 181 | +--- | |
| 182 | + | |
| 183 | +### 5. 其他 Service 检查 | |
| 184 | + | |
| 185 | +以下 Service 可能使用了 `LqXhHyhkEntity`,需要检查是否需要修改: | |
| 186 | + | |
| 187 | +1. **LqStatisticsService.cs** - 统计服务 | |
| 188 | + - 检查是否有统计查询使用备注字段 | |
| 189 | + - 通常统计不需要备注,可能无需修改 | |
| 190 | + | |
| 191 | +2. **LqTechDepartmentDashboardService.cs** - 科技部驾驶舱 | |
| 192 | + - 检查是否有查询使用备注字段 | |
| 193 | + - 通常驾驶舱不需要备注,可能无需修改 | |
| 194 | + | |
| 195 | +3. **LqStoreDashboardService.cs** - 门店驾驶舱 | |
| 196 | + - 检查是否有查询使用备注字段 | |
| 197 | + - 通常驾驶舱不需要备注,可能无需修改 | |
| 198 | + | |
| 199 | +4. **LqTechTeacherSalaryService.cs** - 科技部老师工资 | |
| 200 | + - 检查是否有查询使用备注字段 | |
| 201 | + - 通常工资计算不需要备注,可能无需修改 | |
| 202 | + | |
| 203 | +5. **LqKhxxService.cs** - 客户资料服务 | |
| 204 | + - 检查是否有关联查询使用备注字段 | |
| 205 | + | |
| 206 | +6. **LqTkjlbService.cs** - 拓客记录服务 | |
| 207 | + - 检查是否有关联查询使用备注字段 | |
| 208 | + | |
| 209 | +7. **LqBusinessUnitDashboardService.cs** - 事业部驾驶舱 | |
| 210 | + - 检查是否有查询使用备注字段 | |
| 211 | + | |
| 212 | +8. **LqReportService.cs** - 报表服务 | |
| 213 | + - 检查是否有报表使用备注字段 | |
| 214 | + | |
| 215 | +9. **MemberPortraitService.cs** - 会员画像服务 | |
| 216 | + - 检查是否有查询使用备注字段 | |
| 217 | + | |
| 218 | +10. **LqXmzlService.cs** - 项目资料服务 | |
| 219 | + - 检查是否有关联查询使用备注字段 | |
| 220 | + | |
| 221 | +11. **LqXhFeedbackService.cs** - 耗卡反馈服务 | |
| 222 | + - 检查是否有关联查询使用备注字段 | |
| 223 | + | |
| 224 | +**建议**: 这些服务通常不需要备注字段,但需要根据实际业务需求确认。 | |
| 225 | + | |
| 226 | +--- | |
| 227 | + | |
| 228 | +## 🎨 前端代码修改 | |
| 229 | + | |
| 230 | +### 6. 前端页面修改 | |
| 231 | + | |
| 232 | +#### 6.1 表单页面(新建/编辑) | |
| 233 | + | |
| 234 | +**文件**: `antis-ncc-admin/src/views/lqXhHyhk/Form.vue` | |
| 235 | + | |
| 236 | +**修改内容**: | |
| 237 | +- 在表单中添加备注输入框 | |
| 238 | +- 位置建议:在"费用信息"区域之后,"品项明细"之前 | |
| 239 | + | |
| 240 | +**代码示例**: | |
| 241 | +```vue | |
| 242 | +<el-col :span="24"> | |
| 243 | + <el-form-item label="备注" prop="remark"> | |
| 244 | + <el-input | |
| 245 | + v-model="dataForm.remark" | |
| 246 | + type="textarea" | |
| 247 | + :rows="3" | |
| 248 | + placeholder="请输入备注信息" | |
| 249 | + clearable | |
| 250 | + :style='{"width":"100%"}' | |
| 251 | + maxlength="500" | |
| 252 | + show-word-limit> | |
| 253 | + </el-input> | |
| 254 | + </el-form-item> | |
| 255 | +</el-col> | |
| 256 | +``` | |
| 257 | + | |
| 258 | +**需要修改的位置**: | |
| 259 | +- 在 `dataForm` 对象中添加 `remark: ''` | |
| 260 | +- 在表单模板中添加备注输入框(建议在第86行之后,第94行之前) | |
| 261 | + | |
| 262 | +--- | |
| 263 | + | |
| 264 | +#### 6.2 详情页面 | |
| 265 | + | |
| 266 | +**文件**: `antis-ncc-admin/src/views/lqXhHyhk/detail.vue` | |
| 267 | + | |
| 268 | +**修改内容**: | |
| 269 | +- 在详情页面中显示备注信息 | |
| 270 | +- 位置建议:在"费用信息"卡片之后,或单独一个"备注信息"卡片 | |
| 271 | + | |
| 272 | +**代码示例**: | |
| 273 | +```vue | |
| 274 | +<!-- 备注信息卡片 --> | |
| 275 | +<el-card class="info-card" shadow="hover" v-if="dataForm.remark"> | |
| 276 | + <div slot="header" class="card-header"> | |
| 277 | + <i class="el-icon-edit-outline"></i> | |
| 278 | + <span class="card-title">备注信息</span> | |
| 279 | + </div> | |
| 280 | + <div class="info-item"> | |
| 281 | + <p class="remark-content">{{ dataForm.remark || '无' }}</p> | |
| 282 | + </div> | |
| 283 | +</el-card> | |
| 284 | +``` | |
| 285 | + | |
| 286 | +**需要修改的位置**: | |
| 287 | +- 在详情数据绑定中添加备注字段的显示(建议在第88行之后) | |
| 288 | + | |
| 289 | +--- | |
| 290 | + | |
| 291 | +#### 6.3 列表页面 | |
| 292 | + | |
| 293 | +**文件**: `antis-ncc-admin/src/views/lqXhHyhk/index.vue` | |
| 294 | + | |
| 295 | +**修改内容**: | |
| 296 | +- 在列表表格中添加备注列(可选,根据业务需求) | |
| 297 | +- 如果备注内容较长,可以考虑使用 tooltip 显示完整内容 | |
| 298 | + | |
| 299 | +**代码示例**: | |
| 300 | +```vue | |
| 301 | +<el-table-column prop="remark" label="备注" width="200" show-overflow-tooltip> | |
| 302 | + <template slot-scope="scope"> | |
| 303 | + <span>{{ scope.row.remark || '无' }}</span> | |
| 304 | + </template> | |
| 305 | +</el-table-column> | |
| 306 | +``` | |
| 307 | + | |
| 308 | +**需要修改的位置**: | |
| 309 | +- 在表格列定义中添加备注列(根据实际表格结构确定位置) | |
| 310 | + | |
| 311 | +--- | |
| 312 | + | |
| 313 | +### 7. 导出功能检查 | |
| 314 | + | |
| 315 | +**文件**: `netcore/src/Modularity/Extend/NCC.Extend/LqXhHyhkService.cs` | |
| 316 | + | |
| 317 | +**方法**: `ExportConsumeItemDetailList`(第2340行) | |
| 318 | + | |
| 319 | +**修改说明**: | |
| 320 | +- 如果导出功能需要包含备注字段,需要在导出DTO和Excel配置中添加 | |
| 321 | +- 需要检查 `ConsumeItemDetailExportOutput` DTO 是否包含备注字段 | |
| 322 | +- 需要在 Excel 配置的 `paramList` 中添加备注列 | |
| 323 | + | |
| 324 | +**影响**: ⚠️ 需要根据业务需求确认是否在导出中包含备注 | |
| 325 | + | |
| 326 | +--- | |
| 327 | + | |
| 328 | +## 📝 修改优先级 | |
| 329 | + | |
| 330 | +### 高优先级(必须修改) | |
| 331 | + | |
| 332 | +1. ✅ 数据库表结构修改 | |
| 333 | +2. ✅ 实体类 `LqXhHyhkEntity.cs` | |
| 334 | +3. ✅ DTO类: | |
| 335 | + - `LqXhHyhkCrInput.cs` | |
| 336 | + - `LqXhHyhkInfoOutput.cs` | |
| 337 | + - `LqXhHyhkListOutput.cs` | |
| 338 | +4. ✅ Service `GetList` 方法中的 Select 语句 | |
| 339 | +5. ✅ 前端表单页面 `Form.vue` | |
| 340 | +6. ✅ 前端详情页面 `detail.vue` | |
| 341 | + | |
| 342 | +### 中优先级(建议修改) | |
| 343 | + | |
| 344 | +7. ⚠️ 前端列表页面 `index.vue`(根据业务需求) | |
| 345 | +8. ⚠️ 导出功能(根据业务需求) | |
| 346 | + | |
| 347 | +### 低优先级(可选修改) | |
| 348 | + | |
| 349 | +9. ⚠️ 其他 Service 检查(根据实际使用情况) | |
| 350 | + | |
| 351 | +--- | |
| 352 | + | |
| 353 | +## 🔍 测试检查清单 | |
| 354 | + | |
| 355 | +修改完成后,需要测试以下功能: | |
| 356 | + | |
| 357 | +- [ ] 创建耗卡记录时,可以输入备注 | |
| 358 | +- [ ] 更新耗卡记录时,可以修改备注 | |
| 359 | +- [ ] 查询耗卡详情时,可以显示备注 | |
| 360 | +- [ ] 查询耗卡列表时,可以显示备注(如果列表包含) | |
| 361 | +- [ ] 备注字段可以为空 | |
| 362 | +- [ ] 备注字段长度限制正确(500字符) | |
| 363 | +- [ ] 前端表单验证正常 | |
| 364 | +- [ ] 数据库字段添加成功 | |
| 365 | + | |
| 366 | +--- | |
| 367 | + | |
| 368 | +## 📌 注意事项 | |
| 369 | + | |
| 370 | +1. **字段命名**: 使用 `F_Remark` 作为数据库字段名,`Remark` 作为实体类属性名,`remark` 作为 DTO 属性名(小写开头,符合现有命名规范) | |
| 371 | + | |
| 372 | +2. **字段长度**: 建议使用 `VARCHAR(500)`,如果可能超过500字符,可以使用 `TEXT` 类型 | |
| 373 | + | |
| 374 | +3. **兼容性**: 新增字段为可空字段,不会影响现有数据 | |
| 375 | + | |
| 376 | +4. **自动映射**: Create 和 Update 方法使用 `Adapt` 自动映射,只要 DTO 包含字段即可自动支持 | |
| 377 | + | |
| 378 | +5. **列表查询**: GetList 方法中有多个 Select 语句,需要全部添加备注字段 | |
| 379 | + | |
| 380 | +6. **前端显示**: 备注内容可能较长,建议使用 `textarea` 输入,使用 `show-overflow-tooltip` 或单独卡片显示 | |
| 381 | + | |
| 382 | +--- | |
| 383 | + | |
| 384 | +## 📚 相关文件清单 | |
| 385 | + | |
| 386 | +### 后端文件 | |
| 387 | +1. `netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/lq_xh_hyhk/LqXhHyhkEntity.cs` | |
| 388 | +2. `netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqXhHyhk/LqXhHyhkCrInput.cs` | |
| 389 | +3. `netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqXhHyhk/LqXhHyhkInfoOutput.cs` | |
| 390 | +4. `netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqXhHyhk/LqXhHyhkListOutput.cs` | |
| 391 | +5. `netcore/src/Modularity/Extend/NCC.Extend/LqXhHyhkService.cs` | |
| 392 | + | |
| 393 | +### 前端文件 | |
| 394 | +1. `antis-ncc-admin/src/views/lqXhHyhk/Form.vue` | |
| 395 | +2. `antis-ncc-admin/src/views/lqXhHyhk/detail.vue` | |
| 396 | +3. `antis-ncc-admin/src/views/lqXhHyhk/index.vue`(可选) | |
| 397 | + | |
| 398 | +### 数据库 | |
| 399 | +1. `lq_xh_hyhk` 表结构修改 | |
| 400 | + | |
| 401 | +--- | |
| 402 | + | |
| 403 | +## ✅ 总结 | |
| 404 | + | |
| 405 | +本次修改涉及: | |
| 406 | +- **数据库**: 1个表 | |
| 407 | +- **后端实体类**: 1个 | |
| 408 | +- **后端DTO**: 3个 | |
| 409 | +- **后端Service**: 1个(多个方法) | |
| 410 | +- **前端页面**: 2-3个 | |
| 411 | + | |
| 412 | +预计修改工作量:**中等**(主要是重复性的字段添加和映射) | ... | ... |
docs/全员战报明细到店数统计问题修复.md
0 → 100644
| 1 | +# 全员战报明细到店数统计问题修复 | |
| 2 | + | |
| 3 | +## 📋 问题描述 | |
| 4 | + | |
| 5 | +在全员战报明细统计中,发现有的数据的到店数比拓客数还多,不符合业务逻辑。 | |
| 6 | + | |
| 7 | +**业务规则**: | |
| 8 | +- 一个人多次到店的话在这里就算一次 | |
| 9 | +- 到店数应该小于等于拓客数 | |
| 10 | + | |
| 11 | +## 🔍 问题分析 | |
| 12 | + | |
| 13 | +### 问题位置 | |
| 14 | +**文件**: `netcore/src/Modularity/Extend/NCC.Extend/LqTkDashboardService.cs` | |
| 15 | +**方法**: `GetEmployeeParticipationStatistics` | |
| 16 | +**行数**: 第575-588行(修复前) | |
| 17 | + | |
| 18 | +### 问题原因 | |
| 19 | + | |
| 20 | +**修复前的SQL逻辑**: | |
| 21 | +```sql | |
| 22 | +LEFT JOIN ( | |
| 23 | + -- 到店人数统计 | |
| 24 | + SELECT | |
| 25 | + tk.F_ExpansionUserId as EmployeeId, | |
| 26 | + COUNT(DISTINCT tk.F_MemberId) as VisitCount | |
| 27 | + FROM lq_tkjlb tk | |
| 28 | + INNER JOIN lq_xh_hyhk xh ON tk.F_MemberId = xh.hy AND xh.F_IsEffective = 1 | |
| 29 | + WHERE {eventFilter} | |
| 30 | + AND tk.F_ExpansionTime >= '{startTimeStr}' | |
| 31 | + AND tk.F_ExpansionTime <= '{endTimeStr}' | |
| 32 | + AND xh.hksj >= '{startTimeStr}' | |
| 33 | + AND xh.hksj <= '{endTimeStr}' | |
| 34 | + GROUP BY tk.F_ExpansionUserId | |
| 35 | +) visit ON emp.EmployeeId = visit.EmployeeId | |
| 36 | +``` | |
| 37 | + | |
| 38 | +**问题点**: | |
| 39 | +1. **缺少时间关联约束**:没有限制到店时间必须在拓客时间之后,可能统计到拓客之前的到店记录 | |
| 40 | +2. **JOIN导致重复计数**:当同一个会员被同一个员工多次拓客,且该会员多次到店时,`INNER JOIN` 会产生多行记录 | |
| 41 | +3. **虽然使用了 DISTINCT,但 JOIN 的逻辑可能导致计数不准确** | |
| 42 | + | |
| 43 | +### 问题场景示例 | |
| 44 | + | |
| 45 | +假设: | |
| 46 | +- 员工A在1月1日拓客了会员B | |
| 47 | +- 员工A在1月5日又拓客了会员B(重复拓客) | |
| 48 | +- 会员B在1月3日到店1次 | |
| 49 | +- 会员B在1月6日到店1次 | |
| 50 | + | |
| 51 | +**修复前的统计结果**: | |
| 52 | +- 拓客数:1(COUNT(DISTINCT tk.F_MemberId)) | |
| 53 | +- 到店数:2(因为 JOIN 产生了2条记录:1月1日拓客+1月3日到店,1月5日拓客+1月6日到店) | |
| 54 | + | |
| 55 | +**问题**:到店数(2)> 拓客数(1),不符合逻辑 | |
| 56 | + | |
| 57 | +## ✅ 修复方案 | |
| 58 | + | |
| 59 | +### 修复后的SQL逻辑(最终版本) | |
| 60 | + | |
| 61 | +```sql | |
| 62 | +LEFT JOIN ( | |
| 63 | + -- 到店人数统计(一个人多次到店只算一次) | |
| 64 | + -- 先找出每个员工拓客的所有会员(去重),然后统计这些会员中有到店记录的会员数 | |
| 65 | + SELECT | |
| 66 | + emp_members.EmployeeId, | |
| 67 | + COUNT(DISTINCT CASE WHEN xh.hy IS NOT NULL THEN emp_members.MemberId END) as VisitCount | |
| 68 | + FROM ( | |
| 69 | + -- 先获取每个员工拓客的所有会员(去重),并记录首次拓客时间 | |
| 70 | + SELECT DISTINCT | |
| 71 | + tk.F_ExpansionUserId as EmployeeId, | |
| 72 | + tk.F_MemberId as MemberId, | |
| 73 | + MIN(tk.F_ExpansionTime) as FirstExpansionTime | |
| 74 | + FROM lq_tkjlb tk | |
| 75 | + WHERE {eventFilter} | |
| 76 | + AND tk.F_ExpansionTime >= '{startTimeStr}' | |
| 77 | + AND tk.F_ExpansionTime <= '{endTimeStr}' | |
| 78 | + AND tk.F_MemberId IS NOT NULL | |
| 79 | + GROUP BY tk.F_ExpansionUserId, tk.F_MemberId | |
| 80 | + ) emp_members | |
| 81 | + LEFT JOIN lq_xh_hyhk xh ON emp_members.MemberId = xh.hy | |
| 82 | + AND xh.F_IsEffective = 1 | |
| 83 | + AND xh.hksj >= emp_members.FirstExpansionTime | |
| 84 | + AND xh.hksj >= '{startTimeStr}' | |
| 85 | + AND xh.hksj <= '{endTimeStr}' | |
| 86 | + GROUP BY emp_members.EmployeeId | |
| 87 | +) visit ON emp.EmployeeId = visit.EmployeeId | |
| 88 | +``` | |
| 89 | + | |
| 90 | +### 修复要点 | |
| 91 | + | |
| 92 | +1. **先获取拓客会员列表(去重)**: | |
| 93 | + - 使用子查询先找出每个员工拓客的所有会员(去重) | |
| 94 | + - 记录每个会员的首次拓客时间(`MIN(tk.F_ExpansionTime)`) | |
| 95 | + - 确保同一个会员只出现一次,即使被多次拓客 | |
| 96 | + | |
| 97 | +2. **统计有到店记录的会员数**: | |
| 98 | + - 在去重后的会员列表中,LEFT JOIN 耗卡表 | |
| 99 | + - 使用 `COUNT(DISTINCT CASE WHEN xh.hy IS NOT NULL THEN emp_members.MemberId END)` 统计有到店记录的会员数 | |
| 100 | + - 确保到店时间在首次拓客时间之后(`xh.hksj >= emp_members.FirstExpansionTime`) | |
| 101 | + | |
| 102 | +3. **双重去重保障**: | |
| 103 | + - 第一层:在子查询中使用 `GROUP BY tk.F_ExpansionUserId, tk.F_MemberId` 确保每个员工-会员组合只出现一次 | |
| 104 | + - 第二层:使用 `COUNT(DISTINCT ...)` 确保最终统计时每个会员只算一次 | |
| 105 | + | |
| 106 | +### 修复后的统计结果(使用上面的示例) | |
| 107 | + | |
| 108 | +- 拓客数:1(COUNT(DISTINCT tk.F_MemberId)) | |
| 109 | +- 到店数:1(会员B有到店记录,只算一次) | |
| 110 | + | |
| 111 | +**结果**:到店数(1)≤ 拓客数(1),符合逻辑 ✅ | |
| 112 | + | |
| 113 | +## 📝 修改文件 | |
| 114 | + | |
| 115 | +- **文件**: `netcore/src/Modularity/Extend/NCC.Extend/LqTkDashboardService.cs` | |
| 116 | +- **方法**: `GetEmployeeParticipationStatistics` | |
| 117 | +- **修改位置**: 第575-590行 | |
| 118 | + | |
| 119 | +## ✅ 验证建议 | |
| 120 | + | |
| 121 | +1. **验证到店数 ≤ 拓客数**:检查所有员工的统计数据,确保到店数不超过拓客数 | |
| 122 | +2. **验证去重逻辑**:选择一个多次到店的会员,验证其只被统计一次 | |
| 123 | +3. **验证时间约束**:选择一个拓客前有到店记录的会员,验证不会被统计到 | |
| 124 | + | |
| 125 | +## 🔗 相关接口 | |
| 126 | + | |
| 127 | +**接口地址**: `POST /api/Extend/LqTkDashboard/GetEmployeeParticipationStatistics` | |
| 128 | + | |
| 129 | +**接口说明**: 获取员工参与统计(全员战报明细) | |
| 130 | + | |
| 131 | +**返回字段**: | |
| 132 | +- `ExpansionCount`: 拓客数 | |
| 133 | +- `VisitCount`: 到店数(修复后) | |
| 134 | +- `BillingCount`: 开单数 | |
| 135 | +- `BillingAmount`: 开单金额 | ... | ... |
docs/全部门店开单记录汇总导出接口说明.md
0 → 100644
| 1 | +# 全部门店开单记录汇总导出接口 | |
| 2 | + | |
| 3 | +## 接口地址 | |
| 4 | + | |
| 5 | +``` | |
| 6 | +GET /api/Extend/lqkdkdjlb/billing-record-summary-export-all-stores | |
| 7 | +``` | |
| 8 | + | |
| 9 | +## 接口说明 | |
| 10 | + | |
| 11 | +按照Excel格式导出全部门店的开单记录汇总,每个品项每个健康师一行。如果某个品项有多个健康师,会展开为多行数据。 | |
| 12 | + | |
| 13 | +## 请求参数 | |
| 14 | + | |
| 15 | +| 参数名 | 类型 | 必填 | 说明 | | |
| 16 | +|--------|------|------|------| | |
| 17 | +| StartTime | DateTime | 是 | 开始时间(格式:yyyy-MM-dd 或 yyyy-MM-dd HH:mm:ss) | | |
| 18 | +| EndTime | DateTime | 是 | 结束时间(格式:yyyy-MM-dd 或 yyyy-MM-dd HH:mm:ss) | | |
| 19 | +| MemberId | string | 否 | 客户ID(会员ID),筛选指定客户 | | |
| 20 | +| ItemCategory | string | 否 | 品项分类,筛选指定分类 | | |
| 21 | +| ItemId | string | 否 | 品项ID,筛选指定品项 | | |
| 22 | +| HealthCoachId | string | 否 | 健康师ID,筛选指定健康师 | | |
| 23 | + | |
| 24 | +## 请求示例 | |
| 25 | + | |
| 26 | +### 基础请求(仅时间范围) | |
| 27 | + | |
| 28 | +```javascript | |
| 29 | +// Axios 示例 | |
| 30 | +axios.get('/api/Extend/lqkdkdjlb/billing-record-summary-export-all-stores', { | |
| 31 | + params: { | |
| 32 | + StartTime: '2026-01-01', | |
| 33 | + EndTime: '2026-01-31' | |
| 34 | + }, | |
| 35 | + headers: { | |
| 36 | + 'Authorization': 'Bearer ' + token | |
| 37 | + }, | |
| 38 | + responseType: 'blob' // 重要:设置为 blob 以接收文件 | |
| 39 | +}).then(response => { | |
| 40 | + // 创建下载链接 | |
| 41 | + const url = window.URL.createObjectURL(new Blob([response.data])); | |
| 42 | + const link = document.createElement('a'); | |
| 43 | + link.href = url; | |
| 44 | + link.setAttribute('download', '开单记录汇总.xls'); | |
| 45 | + document.body.appendChild(link); | |
| 46 | + link.click(); | |
| 47 | + link.remove(); | |
| 48 | +}); | |
| 49 | +``` | |
| 50 | + | |
| 51 | +### 完整请求(包含筛选条件) | |
| 52 | + | |
| 53 | +```javascript | |
| 54 | +axios.get('/api/Extend/lqkdkdjlb/billing-record-summary-export-all-stores', { | |
| 55 | + params: { | |
| 56 | + StartTime: '2026-01-01', | |
| 57 | + EndTime: '2026-01-31', | |
| 58 | + MemberId: '123456789', // 可选 | |
| 59 | + ItemCategory: '分类名称', // 可选 | |
| 60 | + ItemId: '987654321', // 可选 | |
| 61 | + HealthCoachId: '111222333' // 可选 | |
| 62 | + }, | |
| 63 | + headers: { | |
| 64 | + 'Authorization': 'Bearer ' + token | |
| 65 | + }, | |
| 66 | + responseType: 'blob' | |
| 67 | +}).then(response => { | |
| 68 | + // 处理文件下载 | |
| 69 | + const url = window.URL.createObjectURL(new Blob([response.data])); | |
| 70 | + const link = document.createElement('a'); | |
| 71 | + link.href = url; | |
| 72 | + link.setAttribute('download', '开单记录汇总.xls'); | |
| 73 | + document.body.appendChild(link); | |
| 74 | + link.click(); | |
| 75 | + link.remove(); | |
| 76 | +}); | |
| 77 | +``` | |
| 78 | + | |
| 79 | +## 响应格式 | |
| 80 | + | |
| 81 | +### 成功响应(200) | |
| 82 | + | |
| 83 | +```json | |
| 84 | +{ | |
| 85 | + "code": 200, | |
| 86 | + "msg": "操作成功", | |
| 87 | + "data": { | |
| 88 | + "name": "开单记录汇总_2026-01-22_152816.xls", | |
| 89 | + "url": "/api/File/Download?encryption=..." | |
| 90 | + } | |
| 91 | +} | |
| 92 | +``` | |
| 93 | + | |
| 94 | +### 错误响应(400/500) | |
| 95 | + | |
| 96 | +```json | |
| 97 | +{ | |
| 98 | + "code": 400, | |
| 99 | + "msg": "开始时间和结束时间不能为空" | |
| 100 | +} | |
| 101 | +``` | |
| 102 | + | |
| 103 | +## Excel 列说明 | |
| 104 | + | |
| 105 | +导出的Excel文件包含以下列(按顺序): | |
| 106 | + | |
| 107 | +1. **开单日期** - 开单记录的日期 | |
| 108 | +2. **单据门店** - 开单门店的中文名称 | |
| 109 | +3. **客户姓名** - 客户姓名 | |
| 110 | +4. **客户电话** - 客户电话 | |
| 111 | +5. **金三角** - 金三角信息 | |
| 112 | +6. **客户类型** - 客户类型 | |
| 113 | +7. **健康师** - 健康师姓名 | |
| 114 | +8. **业绩金额** - 健康师业绩金额(格式:¥0.00) | |
| 115 | +9. **相关品项** - 品项名称 | |
| 116 | +10. **分类** - 品项分类 | |
| 117 | +11. **类型** - 品项类型(购买/体验/赠送等) | |
| 118 | +12. **单价** - 品项单价(格式:¥0.00) | |
| 119 | +13. **次数** - 项目次数 | |
| 120 | +14. **品项金额** - 品项总金额(格式:¥0.00) | |
| 121 | +15. **已付金额** - 已付金额(格式:¥0.00) | |
| 122 | +16. **欠款金额** - 欠款金额(格式:¥0.00) | |
| 123 | +17. **总金额** - 总金额(格式:¥0.00) | |
| 124 | +18. **支付方式** - 支付方式 | |
| 125 | +19. **合作机构** - 合作机构名称 | |
| 126 | +20. **结算机构** - 结算机构名称 | |
| 127 | +21. **活动名称** - 活动名称 | |
| 128 | +22. **备注** - 备注信息 | |
| 129 | +23. **开单时间** - 开单时间(格式:yyyy/MM/dd HH:mm:ss) | |
| 130 | + | |
| 131 | +## 数据展开规则 | |
| 132 | + | |
| 133 | +- 如果一个开单记录有多个品项,每个品项一行 | |
| 134 | +- 如果一个品项有多个健康师,每个健康师一行 | |
| 135 | +- 如果一个品项没有健康师,健康师相关字段显示"无",业绩金额显示"¥0.00" | |
| 136 | + | |
| 137 | +## 注意事项 | |
| 138 | + | |
| 139 | +1. **时间格式**:建议使用 `yyyy-MM-dd` 格式,系统会自动处理时间范围 | |
| 140 | +2. **文件下载**:前端需要设置 `responseType: 'blob'` 来接收文件 | |
| 141 | +3. **文件格式**:导出文件为 `.xls` 格式(Excel 97-2003) | |
| 142 | +4. **数据量**:如果数据量很大,导出可能需要较长时间,建议前端显示加载提示 | |
| 143 | +5. **权限**:需要登录并携带有效的 Authorization token | |
| 144 | + | |
| 145 | +## Vue 2 + Element UI 完整示例 | |
| 146 | + | |
| 147 | +```vue | |
| 148 | +<template> | |
| 149 | + <div> | |
| 150 | + <el-button | |
| 151 | + type="primary" | |
| 152 | + :loading="exporting" | |
| 153 | + @click="handleExport" | |
| 154 | + > | |
| 155 | + 导出全部门店开单记录汇总 | |
| 156 | + </el-button> | |
| 157 | + </div> | |
| 158 | +</template> | |
| 159 | + | |
| 160 | +<script> | |
| 161 | +import { exportBillingRecordSummaryAllStores } from '@/api/billing' | |
| 162 | + | |
| 163 | +export default { | |
| 164 | + data() { | |
| 165 | + return { | |
| 166 | + exporting: false, | |
| 167 | + queryParams: { | |
| 168 | + StartTime: '2026-01-01', | |
| 169 | + EndTime: '2026-01-31' | |
| 170 | + } | |
| 171 | + } | |
| 172 | + }, | |
| 173 | + methods: { | |
| 174 | + async handleExport() { | |
| 175 | + try { | |
| 176 | + this.exporting = true | |
| 177 | + this.$message.info('正在导出,请稍候...') | |
| 178 | + | |
| 179 | + const response = await exportBillingRecordSummaryAllStores(this.queryParams) | |
| 180 | + | |
| 181 | + if (response.code === 200 && response.data) { | |
| 182 | + // 使用返回的下载URL | |
| 183 | + const downloadUrl = this.$store.getters.baseUrl + response.data.url | |
| 184 | + window.open(downloadUrl, '_blank') | |
| 185 | + | |
| 186 | + this.$message.success('导出成功') | |
| 187 | + } else { | |
| 188 | + this.$message.error(response.msg || '导出失败') | |
| 189 | + } | |
| 190 | + } catch (error) { | |
| 191 | + console.error('导出失败:', error) | |
| 192 | + this.$message.error('导出失败,请稍后重试') | |
| 193 | + } finally { | |
| 194 | + this.exporting = false | |
| 195 | + } | |
| 196 | + } | |
| 197 | + } | |
| 198 | +} | |
| 199 | +</script> | |
| 200 | +``` | |
| 201 | + | |
| 202 | +## API 封装示例(api/billing.js) | |
| 203 | + | |
| 204 | +```javascript | |
| 205 | +import request from '@/utils/request' | |
| 206 | + | |
| 207 | +/** | |
| 208 | + * 导出全部门店开单记录汇总 | |
| 209 | + * @param {Object} params 查询参数 | |
| 210 | + * @param {string} params.StartTime 开始时间 | |
| 211 | + * @param {string} params.EndTime 结束时间 | |
| 212 | + * @param {string} [params.MemberId] 客户ID | |
| 213 | + * @param {string} [params.ItemCategory] 品项分类 | |
| 214 | + * @param {string} [params.ItemId] 品项ID | |
| 215 | + * @param {string} [params.HealthCoachId] 健康师ID | |
| 216 | + * @returns {Promise} | |
| 217 | + */ | |
| 218 | +export function exportBillingRecordSummaryAllStores(params) { | |
| 219 | + return request({ | |
| 220 | + url: '/api/Extend/lqkdkdjlb/billing-record-summary-export-all-stores', | |
| 221 | + method: 'get', | |
| 222 | + params: params | |
| 223 | + }) | |
| 224 | +} | |
| 225 | +``` | ... | ... |
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqKdKdjlb/BillingRecordSummaryExportAllStoresInput.cs
0 → 100644
| 1 | +using System; | |
| 2 | +using System.ComponentModel.DataAnnotations; | |
| 3 | +using NCC.Common.Filter; | |
| 4 | + | |
| 5 | +namespace NCC.Extend.Entitys.Dto.LqKdKdjlb | |
| 6 | +{ | |
| 7 | + /// <summary> | |
| 8 | + /// 全部门店开单记录汇总导出查询输入 | |
| 9 | + /// </summary> | |
| 10 | + public class BillingRecordSummaryExportAllStoresInput : PageInputBase | |
| 11 | + { | |
| 12 | + /// <summary> | |
| 13 | + /// 开始时间 | |
| 14 | + /// </summary> | |
| 15 | + [Display(Name = "开始时间", Description = "查询开单记录的开始时间")] | |
| 16 | + public DateTime? StartTime { get; set; } | |
| 17 | + | |
| 18 | + /// <summary> | |
| 19 | + /// 结束时间 | |
| 20 | + /// </summary> | |
| 21 | + [Display(Name = "结束时间", Description = "查询开单记录的结束时间")] | |
| 22 | + public DateTime? EndTime { get; set; } | |
| 23 | + | |
| 24 | + /// <summary> | |
| 25 | + /// 品项分类 | |
| 26 | + /// </summary> | |
| 27 | + [Display(Name = "品项分类", Description = "筛选品项分类")] | |
| 28 | + public string ItemCategory { get; set; } | |
| 29 | + | |
| 30 | + /// <summary> | |
| 31 | + /// 品项ID | |
| 32 | + /// </summary> | |
| 33 | + [Display(Name = "品项ID", Description = "筛选品项ID")] | |
| 34 | + public string ItemId { get; set; } | |
| 35 | + | |
| 36 | + /// <summary> | |
| 37 | + /// 健康师ID | |
| 38 | + /// </summary> | |
| 39 | + [Display(Name = "健康师ID", Description = "筛选健康师ID")] | |
| 40 | + public string HealthCoachId { get; set; } | |
| 41 | + | |
| 42 | + /// <summary> | |
| 43 | + /// 客户ID(会员ID) | |
| 44 | + /// </summary> | |
| 45 | + [Display(Name = "客户ID", Description = "筛选客户ID")] | |
| 46 | + public string MemberId { get; set; } | |
| 47 | + } | |
| 48 | +} | ... | ... |
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqKdKdjlb/BillingRecordSummaryExportOutput.cs
0 → 100644
| 1 | +using System; | |
| 2 | + | |
| 3 | +namespace NCC.Extend.Entitys.Dto.LqKdKdjlb | |
| 4 | +{ | |
| 5 | + /// <summary> | |
| 6 | + /// 开单记录汇总导出输出 | |
| 7 | + /// </summary> | |
| 8 | + public class BillingRecordSummaryExportOutput | |
| 9 | + { | |
| 10 | + /// <summary> | |
| 11 | + /// 开单日期 | |
| 12 | + /// </summary> | |
| 13 | + public string BillingDate { get; set; } | |
| 14 | + | |
| 15 | + /// <summary> | |
| 16 | + /// 单据门店 | |
| 17 | + /// </summary> | |
| 18 | + public string StoreName { get; set; } | |
| 19 | + | |
| 20 | + /// <summary> | |
| 21 | + /// 客户姓名 | |
| 22 | + /// </summary> | |
| 23 | + public string CustomerName { get; set; } | |
| 24 | + | |
| 25 | + /// <summary> | |
| 26 | + /// 客户电话 | |
| 27 | + /// </summary> | |
| 28 | + public string CustomerPhone { get; set; } | |
| 29 | + | |
| 30 | + /// <summary> | |
| 31 | + /// 金三角 | |
| 32 | + /// </summary> | |
| 33 | + public string GoldTriangle { get; set; } | |
| 34 | + | |
| 35 | + /// <summary> | |
| 36 | + /// 客户类型 | |
| 37 | + /// </summary> | |
| 38 | + public string CustomerType { get; set; } | |
| 39 | + | |
| 40 | + /// <summary> | |
| 41 | + /// 健康师 | |
| 42 | + /// </summary> | |
| 43 | + public string HealthCoach { get; set; } | |
| 44 | + | |
| 45 | + /// <summary> | |
| 46 | + /// 业绩金额 | |
| 47 | + /// </summary> | |
| 48 | + public string PerformanceAmount { get; set; } | |
| 49 | + | |
| 50 | + /// <summary> | |
| 51 | + /// 相关品项 | |
| 52 | + /// </summary> | |
| 53 | + public string RelatedItem { get; set; } | |
| 54 | + | |
| 55 | + /// <summary> | |
| 56 | + /// 分类 | |
| 57 | + /// </summary> | |
| 58 | + public string Category { get; set; } | |
| 59 | + | |
| 60 | + /// <summary> | |
| 61 | + /// 类型 | |
| 62 | + /// </summary> | |
| 63 | + public string Type { get; set; } | |
| 64 | + | |
| 65 | + /// <summary> | |
| 66 | + /// 单价 | |
| 67 | + /// </summary> | |
| 68 | + public string UnitPrice { get; set; } | |
| 69 | + | |
| 70 | + /// <summary> | |
| 71 | + /// 次数 | |
| 72 | + /// </summary> | |
| 73 | + public string ProjectNumber { get; set; } | |
| 74 | + | |
| 75 | + /// <summary> | |
| 76 | + /// 品项金额 | |
| 77 | + /// </summary> | |
| 78 | + public string ItemAmount { get; set; } | |
| 79 | + | |
| 80 | + /// <summary> | |
| 81 | + /// 已付金额 | |
| 82 | + /// </summary> | |
| 83 | + public string PaidAmount { get; set; } | |
| 84 | + | |
| 85 | + /// <summary> | |
| 86 | + /// 欠款金额 | |
| 87 | + /// </summary> | |
| 88 | + public string DebtAmount { get; set; } | |
| 89 | + | |
| 90 | + /// <summary> | |
| 91 | + /// 总金额 | |
| 92 | + /// </summary> | |
| 93 | + public string TotalAmount { get; set; } | |
| 94 | + | |
| 95 | + /// <summary> | |
| 96 | + /// 支付方式 | |
| 97 | + /// </summary> | |
| 98 | + public string PaymentMethod { get; set; } | |
| 99 | + | |
| 100 | + /// <summary> | |
| 101 | + /// 合作机构 | |
| 102 | + /// </summary> | |
| 103 | + public string CooperationInstitution { get; set; } | |
| 104 | + | |
| 105 | + /// <summary> | |
| 106 | + /// 结算机构 | |
| 107 | + /// </summary> | |
| 108 | + public string SettlementInstitution { get; set; } | |
| 109 | + | |
| 110 | + /// <summary> | |
| 111 | + /// 活动名称 | |
| 112 | + /// </summary> | |
| 113 | + public string ActivityName { get; set; } | |
| 114 | + | |
| 115 | + /// <summary> | |
| 116 | + /// 备注 | |
| 117 | + /// </summary> | |
| 118 | + public string Remark { get; set; } | |
| 119 | + | |
| 120 | + /// <summary> | |
| 121 | + /// 开单时间 | |
| 122 | + /// </summary> | |
| 123 | + public string CreateTime { get; set; } | |
| 124 | + } | |
| 125 | +} | ... | ... |
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqXhHyhk/LqXhHyhkCrInput.cs
| ... | ... | @@ -90,6 +90,11 @@ namespace NCC.Extend.Entitys.Dto.LqXhHyhk |
| 90 | 90 | public string appointmentId { get; set; } |
| 91 | 91 | |
| 92 | 92 | /// <summary> |
| 93 | + /// 备注 | |
| 94 | + /// </summary> | |
| 95 | + public string remark { get; set; } | |
| 96 | + | |
| 97 | + /// <summary> | |
| 93 | 98 | /// 耗卡_品项明细 |
| 94 | 99 | /// </summary> |
| 95 | 100 | public List<LqXhPxmxCrInput> lqXhPxmxList { get; set; } | ... | ... |
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqXhHyhk/LqXhHyhkInfoOutput.cs
| ... | ... | @@ -113,6 +113,11 @@ namespace NCC.Extend.Entitys.Dto.LqXhHyhk |
| 113 | 113 | public string cancelRemark { get; set; } |
| 114 | 114 | |
| 115 | 115 | /// <summary> |
| 116 | + /// 备注 | |
| 117 | + /// </summary> | |
| 118 | + public string remark { get; set; } | |
| 119 | + | |
| 120 | + /// <summary> | |
| 116 | 121 | /// 加班系数(NULL或0表示非加班单,大于0表示加班单,如 0.5、1、1.5) |
| 117 | 122 | /// </summary> |
| 118 | 123 | public decimal? overtimeCoefficient { get; set; } | ... | ... |
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqXhHyhk/LqXhHyhkListOutput.cs
| ... | ... | @@ -95,6 +95,11 @@ namespace NCC.Extend.Entitys.Dto.LqXhHyhk |
| 95 | 95 | public string signatureFile { get; set; } |
| 96 | 96 | |
| 97 | 97 | /// <summary> |
| 98 | + /// 备注 | |
| 99 | + /// </summary> | |
| 100 | + public string remark { get; set; } | |
| 101 | + | |
| 102 | + /// <summary> | |
| 98 | 103 | /// 是否有效 |
| 99 | 104 | /// </summary> |
| 100 | 105 | public int isEffective { get; set; } | ... | ... |
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqXhHyhk/LqXhHyhkUpdateRemarkInput.cs
0 → 100644
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/lq_xh_hyhk/LqXhHyhkEntity.cs
| ... | ... | @@ -128,6 +128,12 @@ namespace NCC.Extend.Entitys.lq_xh_hyhk |
| 128 | 128 | public string CancelRemark { get; set; } |
| 129 | 129 | |
| 130 | 130 | /// <summary> |
| 131 | + /// 备注 | |
| 132 | + /// </summary> | |
| 133 | + [SugarColumn(ColumnName = "F_Remark")] | |
| 134 | + public string Remark { get; set; } | |
| 135 | + | |
| 136 | + /// <summary> | |
| 131 | 137 | /// 加班系数(NULL或0表示非加班单,大于0表示加班单,如 0.5、1、1.5) |
| 132 | 138 | /// </summary> |
| 133 | 139 | [SugarColumn(ColumnName = "F_OvertimeCoefficient")] | ... | ... |
netcore/src/Modularity/Extend/NCC.Extend/LqKdKdjlbService.cs
| 1 | 1 | using System; |
| 2 | 2 | using System.Collections.Generic; |
| 3 | +using System.IO; | |
| 3 | 4 | using System.Linq; |
| 4 | 5 | using System.Net.Http; |
| 5 | 6 | using System.Threading.Tasks; |
| ... | ... | @@ -96,6 +97,431 @@ namespace NCC.Extend.LqKdKdjlb |
| 96 | 97 | _dailyReportService = dailyReportService; |
| 97 | 98 | } |
| 98 | 99 | |
| 100 | + #region 导出全部门店开单记录汇总 | |
| 101 | + /// <summary> | |
| 102 | + /// 导出全部门店开单记录汇总 | |
| 103 | + /// </summary> | |
| 104 | + /// <remarks> | |
| 105 | + /// 按照Excel格式导出全部门店的开单记录汇总,每个品项每个健康师一行 | |
| 106 | + /// | |
| 107 | + /// 示例请求: | |
| 108 | + /// GET /api/Extend/lqkdkdjlb/billing-record-summary-export-all-stores?StartTime=2026-01-01&EndTime=2026-01-31 | |
| 109 | + /// | |
| 110 | + /// 参数说明: | |
| 111 | + /// - StartTime: 开始时间(必填) | |
| 112 | + /// - EndTime: 结束时间(必填) | |
| 113 | + /// - MemberId: 客户ID(可选) | |
| 114 | + /// - ItemCategory: 品项分类(可选) | |
| 115 | + /// - ItemId: 品项ID(可选) | |
| 116 | + /// - HealthCoachId: 健康师ID(可选) | |
| 117 | + /// </remarks> | |
| 118 | + /// <param name="input">查询参数</param> | |
| 119 | + /// <returns>Excel文件下载链接</returns> | |
| 120 | + /// <response code="200">导出成功</response> | |
| 121 | + /// <response code="400">参数错误</response> | |
| 122 | + /// <response code="500">服务器错误</response> | |
| 123 | + [HttpGet("billing-record-summary-export-all-stores"), NonUnify] | |
| 124 | + public async Task<dynamic> ExportBillingRecordSummaryAllStores([FromQuery] BillingRecordSummaryExportAllStoresInput input) | |
| 125 | + { | |
| 126 | + try | |
| 127 | + { | |
| 128 | + // 如果开始时间和结束时间为空,就默认是当月 | |
| 129 | + if (input.StartTime == null || input.EndTime == null) | |
| 130 | + { | |
| 131 | + input.StartTime = new DateTime(DateTime.Now.Year, DateTime.Now.Month, 1); | |
| 132 | + input.EndTime = DateTime.Now.AddDays(DateTime.DaysInMonth(DateTime.Now.Year, DateTime.Now.Month)); | |
| 133 | + } | |
| 134 | + | |
| 135 | + // 确保时间不为null | |
| 136 | + if (input.StartTime == null || input.EndTime == null) | |
| 137 | + { | |
| 138 | + throw NCCException.Oh("开始时间和结束时间不能为空"); | |
| 139 | + } | |
| 140 | + | |
| 141 | + _logger.LogInformation($"开始导出全部门店开单记录汇总,时间范围: {input.StartTime} 到 {input.EndTime}"); | |
| 142 | + | |
| 143 | + // 构建开单记录查询条件(全部门店) | |
| 144 | + var startTime = input.StartTime.Value; | |
| 145 | + var endTime = input.EndTime.Value; | |
| 146 | + var billingQuery = _db.Queryable<LqKdKdjlbEntity>() | |
| 147 | + .Where(w => w.Kdrq.HasValue && w.Kdrq.Value >= startTime && w.Kdrq.Value <= endTime && w.IsEffective == StatusEnum.有效.GetHashCode()); | |
| 148 | + | |
| 149 | + // 客户筛选 | |
| 150 | + if (!string.IsNullOrEmpty(input.MemberId)) | |
| 151 | + { | |
| 152 | + billingQuery = billingQuery.Where(w => w.Kdhy == input.MemberId); | |
| 153 | + } | |
| 154 | + | |
| 155 | + // 查询所有开单记录 | |
| 156 | + var billingRecords = await billingQuery | |
| 157 | + .Select(it => new | |
| 158 | + { | |
| 159 | + id = it.Id, | |
| 160 | + djmd = it.Djmd, | |
| 161 | + djmdName = SqlFunc.Subqueryable<LqMdxxEntity>().Where(x => x.Id == it.Djmd).Select(x => x.Dm), | |
| 162 | + jsj = it.Jsj, | |
| 163 | + kdrq = it.Kdrq, | |
| 164 | + gjlx = it.Gjlx, | |
| 165 | + zdyj = it.Zdyj, | |
| 166 | + sfyj = it.Sfyj, | |
| 167 | + qk = it.Qk, | |
| 168 | + hgjg = it.Hgjg, | |
| 169 | + hgjgName = SqlFunc.Subqueryable<LqHzfEntity>().Where(x => x.Id == it.Hgjg).Select(x => x.Hzmc), | |
| 170 | + kdhyc = SqlFunc.Subqueryable<LqKhxxEntity>().Where(x => x.Id == it.Kdhy).Select(x => x.Khmc), | |
| 171 | + kdhysjh = SqlFunc.Subqueryable<LqKhxxEntity>().Where(x => x.Id == it.Kdhy).Select(x => x.Sjh), | |
| 172 | + fkfs = it.Fkfs, | |
| 173 | + fkyy = it.Fkyy, | |
| 174 | + fkyyName = SqlFunc.Subqueryable<LqHzfEntity>().Where(x => x.Id == it.Fkyy).Select(x => x.Hzmc), | |
| 175 | + activityId = it.ActivityId, | |
| 176 | + activityName = SqlFunc.Subqueryable<LqPackageInfoEntity>().Where(x => x.Id == it.ActivityId).Select(x => x.ActivityName), | |
| 177 | + khly = it.Khly, | |
| 178 | + bz = it.Bz, | |
| 179 | + createTime = it.CreateTime | |
| 180 | + }) | |
| 181 | + .ToListAsync(); | |
| 182 | + | |
| 183 | + if (!billingRecords.Any()) | |
| 184 | + { | |
| 185 | + throw NCCException.Oh(ErrorCode.COM1005, "该时间段内无开单记录"); | |
| 186 | + } | |
| 187 | + | |
| 188 | + _logger.LogInformation($"查询到 {billingRecords.Count} 条开单记录"); | |
| 189 | + | |
| 190 | + if (billingRecords == null || !billingRecords.Any()) | |
| 191 | + { | |
| 192 | + _logger.LogWarning("开单记录为空,无法继续导出"); | |
| 193 | + throw NCCException.Oh(ErrorCode.COM1005, "该时间段内无开单记录"); | |
| 194 | + } | |
| 195 | + | |
| 196 | + var billingIds = billingRecords.Select(x => x.id).ToList(); | |
| 197 | + _logger.LogInformation($"提取到 {billingIds.Count} 个开单记录ID"); | |
| 198 | + | |
| 199 | + // 构建品项明细查询条件 | |
| 200 | + var itemDetailsQuery = _db.Queryable<LqKdPxmxEntity>() | |
| 201 | + .Where(w => billingIds.Contains(w.Glkdbh) && w.IsEffective == StatusEnum.有效.GetHashCode()); | |
| 202 | + | |
| 203 | + // 品项分类筛选 | |
| 204 | + if (!string.IsNullOrEmpty(input.ItemCategory)) | |
| 205 | + { | |
| 206 | + itemDetailsQuery = itemDetailsQuery.Where(w => w.ItemCategory == input.ItemCategory); | |
| 207 | + } | |
| 208 | + | |
| 209 | + // 品项筛选 | |
| 210 | + if (!string.IsNullOrEmpty(input.ItemId)) | |
| 211 | + { | |
| 212 | + itemDetailsQuery = itemDetailsQuery.Where(w => w.Px == input.ItemId); | |
| 213 | + } | |
| 214 | + | |
| 215 | + // 查询品项明细 | |
| 216 | + _logger.LogInformation($"开始查询品项明细,开单记录ID数量: {billingIds.Count}"); | |
| 217 | + var itemDetails = await itemDetailsQuery | |
| 218 | + .Select(it => new | |
| 219 | + { | |
| 220 | + id = it.Id, | |
| 221 | + glkdbh = it.Glkdbh, | |
| 222 | + px = it.Px, | |
| 223 | + pxmc = it.Pxmc, | |
| 224 | + pxjg = it.Pxjg, | |
| 225 | + sourceType = it.SourceType, | |
| 226 | + totalPrice = it.TotalPrice, | |
| 227 | + actualPrice = it.ActualPrice, | |
| 228 | + projectNumber = it.ProjectNumber, | |
| 229 | + remark = it.Remark, | |
| 230 | + itemCategory = it.ItemCategory, | |
| 231 | + }) | |
| 232 | + .ToListAsync(); | |
| 233 | + _logger.LogInformation($"查询到 {itemDetails?.Count ?? 0} 条品项明细"); | |
| 234 | + | |
| 235 | + // 构建健康师业绩查询条件 | |
| 236 | + var healthTeacherQuery = _db.Queryable<LqKdJksyjEntity>() | |
| 237 | + .Where(w => billingIds.Contains(w.Glkdbh) && w.IsEffective == StatusEnum.有效.GetHashCode()); | |
| 238 | + | |
| 239 | + // 健康师筛选 | |
| 240 | + if (!string.IsNullOrEmpty(input.HealthCoachId)) | |
| 241 | + { | |
| 242 | + healthTeacherQuery = healthTeacherQuery.Where(w => w.Jks == input.HealthCoachId || w.Jkszh == input.HealthCoachId); | |
| 243 | + } | |
| 244 | + | |
| 245 | + // 品项分类筛选(健康师业绩表) | |
| 246 | + if (!string.IsNullOrEmpty(input.ItemCategory)) | |
| 247 | + { | |
| 248 | + healthTeacherQuery = healthTeacherQuery.Where(w => w.ItemCategory == input.ItemCategory); | |
| 249 | + } | |
| 250 | + | |
| 251 | + // 品项筛选(健康师业绩表) | |
| 252 | + if (!string.IsNullOrEmpty(input.ItemId)) | |
| 253 | + { | |
| 254 | + healthTeacherQuery = healthTeacherQuery.Where(w => w.ItemId == input.ItemId); | |
| 255 | + } | |
| 256 | + | |
| 257 | + // 查询健康师业绩数据 | |
| 258 | + _logger.LogInformation($"开始查询健康师业绩数据,开单记录ID数量: {billingIds.Count}"); | |
| 259 | + var healthTeacherData = await healthTeacherQuery | |
| 260 | + .Select(it => new | |
| 261 | + { | |
| 262 | + id = it.Id, | |
| 263 | + glkdbh = it.Glkdbh, | |
| 264 | + jks = it.Jks, | |
| 265 | + jksxm = it.Jksxm, | |
| 266 | + jkszh = it.Jkszh, | |
| 267 | + jksyj = it.Jksyj, | |
| 268 | + kdpxid = it.Kdpxid, | |
| 269 | + yjsj = it.Yjsj, | |
| 270 | + jsj_id = it.Jsj_id, | |
| 271 | + itemCategory = it.ItemCategory, | |
| 272 | + storeId = it.StoreId, | |
| 273 | + itemId = it.ItemId, | |
| 274 | + itemName = it.ItemName, | |
| 275 | + }) | |
| 276 | + .ToListAsync(); | |
| 277 | + _logger.LogInformation($"查询到 {healthTeacherData?.Count ?? 0} 条健康师业绩数据"); | |
| 278 | + | |
| 279 | + // 批量查询金三角信息 | |
| 280 | + var goldTriangleDict = new Dictionary<string, string>(); | |
| 281 | + var jsjIds = billingRecords.Where(x => !string.IsNullOrEmpty(x.jsj)).Select(x => x.jsj).Distinct().ToList(); | |
| 282 | + if (jsjIds.Any()) | |
| 283 | + { | |
| 284 | + var jsjList = await _db.Queryable<LqJinsanjiaoUserEntity>() | |
| 285 | + .Where(x => jsjIds.Contains(x.Id)) | |
| 286 | + .Select(x => new { x.Id, x.UserName }) | |
| 287 | + .ToListAsync(); | |
| 288 | + goldTriangleDict = jsjList.ToDictionary(x => x.Id, x => x.UserName ?? ""); | |
| 289 | + } | |
| 290 | + | |
| 291 | + // 按品项明细ID分组健康师业绩 | |
| 292 | + var healthTeacherDict = healthTeacherData | |
| 293 | + .Where(x => !string.IsNullOrEmpty(x.kdpxid)) | |
| 294 | + .GroupBy(x => x.kdpxid) | |
| 295 | + .ToDictionary(g => g.Key, g => g.ToList()); | |
| 296 | + | |
| 297 | + // 展开数据:每个品项每个健康师一行 | |
| 298 | + var exportData = new List<BillingRecordSummaryExportOutput>(); | |
| 299 | + int processedCount = 0; | |
| 300 | + | |
| 301 | + foreach (var itemDetail in itemDetails) | |
| 302 | + { | |
| 303 | + var billing = billingRecords.FirstOrDefault(b => b.id == itemDetail.glkdbh); | |
| 304 | + if (billing == null) continue; | |
| 305 | + | |
| 306 | + // 获取该品项的健康师业绩列表 | |
| 307 | + List<dynamic> healthTeachers; | |
| 308 | + if (healthTeacherDict.ContainsKey(itemDetail.id)) | |
| 309 | + { | |
| 310 | + healthTeachers = healthTeacherDict[itemDetail.id].Cast<dynamic>().ToList(); | |
| 311 | + } | |
| 312 | + else | |
| 313 | + { | |
| 314 | + healthTeachers = new List<dynamic>(); | |
| 315 | + } | |
| 316 | + | |
| 317 | + // 格式化金额 | |
| 318 | + string FormatAmount(decimal? amount) | |
| 319 | + { | |
| 320 | + if (amount == null || amount == 0) return "¥0.00"; | |
| 321 | + return $"¥{amount.Value:F2}"; | |
| 322 | + } | |
| 323 | + | |
| 324 | + // 格式化金额(非可空类型) | |
| 325 | + string FormatAmountNonNull(decimal amount) | |
| 326 | + { | |
| 327 | + if (amount == 0) return "¥0.00"; | |
| 328 | + return $"¥{amount:F2}"; | |
| 329 | + } | |
| 330 | + | |
| 331 | + // 格式化业绩金额(字符串类型) | |
| 332 | + string FormatPerformanceAmount(string amount) | |
| 333 | + { | |
| 334 | + if (string.IsNullOrEmpty(amount)) return "¥0.00"; | |
| 335 | + if (decimal.TryParse(amount, out decimal decAmount)) | |
| 336 | + { | |
| 337 | + return $"¥{decAmount:F2}"; | |
| 338 | + } | |
| 339 | + return "¥0.00"; | |
| 340 | + } | |
| 341 | + | |
| 342 | + // 格式化日期 | |
| 343 | + string FormatDate(DateTime? date) | |
| 344 | + { | |
| 345 | + if (date == null) return "无"; | |
| 346 | + return date.Value.ToString("yyyy-MM-dd"); | |
| 347 | + } | |
| 348 | + | |
| 349 | + // 格式化日期时间 | |
| 350 | + string FormatDateTime(DateTime? dateTime) | |
| 351 | + { | |
| 352 | + if (dateTime == null) return "无"; | |
| 353 | + return dateTime.Value.ToString("yyyy/MM/dd HH:mm:ss"); | |
| 354 | + } | |
| 355 | + | |
| 356 | + // 获取金三角名称 | |
| 357 | + string GetGoldTriangleName(string jsjId) | |
| 358 | + { | |
| 359 | + if (string.IsNullOrEmpty(jsjId)) return "无"; | |
| 360 | + return goldTriangleDict.ContainsKey(jsjId) ? goldTriangleDict[jsjId] : "无"; | |
| 361 | + } | |
| 362 | + | |
| 363 | + // 如果没有健康师,也要导出一行(健康师信息为空) | |
| 364 | + if (healthTeachers == null || !healthTeachers.Any()) | |
| 365 | + { | |
| 366 | + exportData.Add(new BillingRecordSummaryExportOutput | |
| 367 | + { | |
| 368 | + BillingDate = FormatDate(billing.kdrq), | |
| 369 | + StoreName = billing.djmdName ?? "无", | |
| 370 | + CustomerName = billing.kdhyc ?? "无", | |
| 371 | + CustomerPhone = billing.kdhysjh ?? "无", | |
| 372 | + GoldTriangle = GetGoldTriangleName(billing.jsj), | |
| 373 | + CustomerType = billing.gjlx?.ToString() ?? "0", | |
| 374 | + HealthCoach = "无", | |
| 375 | + PerformanceAmount = "¥0.00", | |
| 376 | + RelatedItem = itemDetail.pxmc ?? "无", | |
| 377 | + Category = itemDetail.itemCategory ?? "无", | |
| 378 | + Type = itemDetail.sourceType ?? "无", | |
| 379 | + UnitPrice = FormatAmountNonNull(itemDetail.pxjg), | |
| 380 | + ProjectNumber = itemDetail.projectNumber.ToString(), | |
| 381 | + ItemAmount = FormatAmountNonNull(itemDetail.totalPrice), | |
| 382 | + PaidAmount = FormatAmount(billing.sfyj), | |
| 383 | + DebtAmount = FormatAmount(billing.qk), | |
| 384 | + TotalAmount = FormatAmount(billing.zdyj), | |
| 385 | + PaymentMethod = billing.fkfs ?? "无", | |
| 386 | + CooperationInstitution = billing.fkyyName ?? "无", | |
| 387 | + SettlementInstitution = billing.hgjgName ?? "无", | |
| 388 | + ActivityName = billing.activityName ?? "无", | |
| 389 | + Remark = billing.bz ?? "无", | |
| 390 | + CreateTime = FormatDateTime(billing.createTime) | |
| 391 | + }); | |
| 392 | + } | |
| 393 | + else | |
| 394 | + { | |
| 395 | + // 每个健康师一行 | |
| 396 | + foreach (var healthTeacher in healthTeachers) | |
| 397 | + { | |
| 398 | + exportData.Add(new BillingRecordSummaryExportOutput | |
| 399 | + { | |
| 400 | + BillingDate = FormatDate(billing.kdrq), | |
| 401 | + StoreName = billing.djmdName ?? "无", | |
| 402 | + CustomerName = billing.kdhyc ?? "无", | |
| 403 | + CustomerPhone = billing.kdhysjh ?? "无", | |
| 404 | + GoldTriangle = GetGoldTriangleName(billing.jsj), | |
| 405 | + CustomerType = billing.gjlx?.ToString() ?? "0", | |
| 406 | + HealthCoach = healthTeacher.jksxm ?? "无", | |
| 407 | + PerformanceAmount = FormatPerformanceAmount(healthTeacher.jksyj), | |
| 408 | + RelatedItem = itemDetail.pxmc ?? "无", | |
| 409 | + Category = itemDetail.itemCategory ?? "无", | |
| 410 | + Type = itemDetail.sourceType ?? "无", | |
| 411 | + UnitPrice = FormatAmountNonNull(itemDetail.pxjg), | |
| 412 | + ProjectNumber = itemDetail.projectNumber.ToString(), | |
| 413 | + ItemAmount = FormatAmountNonNull(itemDetail.totalPrice), | |
| 414 | + PaidAmount = FormatAmount(billing.sfyj), | |
| 415 | + DebtAmount = FormatAmount(billing.qk), | |
| 416 | + TotalAmount = FormatAmount(billing.zdyj), | |
| 417 | + PaymentMethod = billing.fkfs ?? "无", | |
| 418 | + CooperationInstitution = billing.fkyyName ?? "无", | |
| 419 | + SettlementInstitution = billing.hgjgName ?? "无", | |
| 420 | + ActivityName = billing.activityName ?? "无", | |
| 421 | + Remark = billing.bz ?? "无", | |
| 422 | + CreateTime = FormatDateTime(billing.createTime) | |
| 423 | + }); | |
| 424 | + } | |
| 425 | + } | |
| 426 | + | |
| 427 | + processedCount++; | |
| 428 | + if (processedCount % 1000 == 0) | |
| 429 | + { | |
| 430 | + _logger.LogInformation($"已处理 {processedCount}/{itemDetails.Count} 条品项明细,当前导出数据行数: {exportData.Count}"); | |
| 431 | + } | |
| 432 | + } | |
| 433 | + | |
| 434 | + _logger.LogInformation($"数据展开完成,处理品项明细数量: {processedCount}, 导出数据行数: {exportData.Count}"); | |
| 435 | + | |
| 436 | + if (!exportData.Any()) | |
| 437 | + { | |
| 438 | + throw NCCException.Oh(ErrorCode.COM1005, "没有符合条件的数据可导出"); | |
| 439 | + } | |
| 440 | + | |
| 441 | + // 配置Excel导出 | |
| 442 | + List<ParamsModel> paramList = | |
| 443 | + "[{\"value\":\"开单日期\",\"field\":\"BillingDate\"},{\"value\":\"单据门店\",\"field\":\"StoreName\"},{\"value\":\"客户姓名\",\"field\":\"CustomerName\"},{\"value\":\"客户电话\",\"field\":\"CustomerPhone\"},{\"value\":\"金三角\",\"field\":\"GoldTriangle\"},{\"value\":\"客户类型\",\"field\":\"CustomerType\"},{\"value\":\"健康师\",\"field\":\"HealthCoach\"},{\"value\":\"业绩金额\",\"field\":\"PerformanceAmount\"},{\"value\":\"相关品项\",\"field\":\"RelatedItem\"},{\"value\":\"分类\",\"field\":\"Category\"},{\"value\":\"类型\",\"field\":\"Type\"},{\"value\":\"单价\",\"field\":\"UnitPrice\"},{\"value\":\"次数\",\"field\":\"ProjectNumber\"},{\"value\":\"品项金额\",\"field\":\"ItemAmount\"},{\"value\":\"已付金额\",\"field\":\"PaidAmount\"},{\"value\":\"欠款金额\",\"field\":\"DebtAmount\"},{\"value\":\"总金额\",\"field\":\"TotalAmount\"},{\"value\":\"支付方式\",\"field\":\"PaymentMethod\"},{\"value\":\"合作机构\",\"field\":\"CooperationInstitution\"},{\"value\":\"结算机构\",\"field\":\"SettlementInstitution\"},{\"value\":\"活动名称\",\"field\":\"ActivityName\"},{\"value\":\"备注\",\"field\":\"Remark\"},{\"value\":\"开单时间\",\"field\":\"CreateTime\"}]".ToList<ParamsModel>(); | |
| 444 | + | |
| 445 | + ExcelConfig excelconfig = new ExcelConfig(); | |
| 446 | + excelconfig.FileName = "开单记录汇总_" + DateTime.Now.ToString("yyyy-MM-dd_HHmmss") + ".xls"; | |
| 447 | + excelconfig.HeadFont = "微软雅黑"; | |
| 448 | + excelconfig.HeadPoint = 10; | |
| 449 | + excelconfig.IsAllSizeColumn = true; | |
| 450 | + excelconfig.ColumnModel = new List<ExcelColumnModel>(); | |
| 451 | + | |
| 452 | + foreach (var param in paramList) | |
| 453 | + { | |
| 454 | + excelconfig.ColumnModel.Add(new ExcelColumnModel() { Column = param.field, ExcelColumn = param.value }); | |
| 455 | + } | |
| 456 | + | |
| 457 | + // 查找项目根目录并创建ExportFiles文件夹 | |
| 458 | + var baseDir = AppContext.BaseDirectory; | |
| 459 | + var projectRoot = baseDir; | |
| 460 | + var dir = new DirectoryInfo(baseDir); | |
| 461 | + while (dir != null && dir.Parent != null) | |
| 462 | + { | |
| 463 | + try | |
| 464 | + { | |
| 465 | + if (dir.GetDirectories(".git").Any() || dir.GetFiles("*.sln").Any()) | |
| 466 | + { | |
| 467 | + projectRoot = dir.FullName; | |
| 468 | + break; | |
| 469 | + } | |
| 470 | + } | |
| 471 | + catch | |
| 472 | + { | |
| 473 | + // 忽略访问错误,继续向上查找 | |
| 474 | + } | |
| 475 | + dir = dir.Parent; | |
| 476 | + } | |
| 477 | + | |
| 478 | + if (projectRoot == baseDir) | |
| 479 | + { | |
| 480 | + dir = new DirectoryInfo(baseDir); | |
| 481 | + while (dir != null && dir.Parent != null) | |
| 482 | + { | |
| 483 | + try | |
| 484 | + { | |
| 485 | + if (dir.GetFiles("*.sln").Any()) | |
| 486 | + { | |
| 487 | + projectRoot = dir.FullName; | |
| 488 | + break; | |
| 489 | + } | |
| 490 | + } | |
| 491 | + catch | |
| 492 | + { | |
| 493 | + // 忽略访问错误,继续向上查找 | |
| 494 | + } | |
| 495 | + dir = dir.Parent; | |
| 496 | + } | |
| 497 | + } | |
| 498 | + | |
| 499 | + var exportFilesPath = Path.Combine(projectRoot, "ExportFiles"); | |
| 500 | + if (!Directory.Exists(exportFilesPath)) | |
| 501 | + { | |
| 502 | + Directory.CreateDirectory(exportFilesPath); | |
| 503 | + } | |
| 504 | + | |
| 505 | + var addPath = Path.Combine(exportFilesPath, excelconfig.FileName); | |
| 506 | + ExcelExportHelper<BillingRecordSummaryExportOutput>.Export(exportData, excelconfig, addPath); | |
| 507 | + | |
| 508 | + var fileName = _userManager.UserId + "|" + addPath + "|xls"; | |
| 509 | + var downloadUrl = "/api/File/Download?encryption=" + DESCEncryption.Encrypt(fileName, "NCC"); | |
| 510 | + var output = new { name = excelconfig.FileName, url = downloadUrl }; | |
| 511 | + | |
| 512 | + _logger.LogInformation($"导出完成,文件路径: {addPath}, 下载地址: {downloadUrl}"); | |
| 513 | + | |
| 514 | + // 返回统一格式 | |
| 515 | + return new { code = 200, msg = "导出成功", data = output }; | |
| 516 | + } | |
| 517 | + catch (Exception ex) | |
| 518 | + { | |
| 519 | + _logger.LogError(ex, $"导出全部门店开单记录汇总失败,异常详情: {ex.ToString()}"); | |
| 520 | + throw NCCException.Oh($"导出全部门店开单记录汇总失败:{ex.Message}"); | |
| 521 | + } | |
| 522 | + } | |
| 523 | + #endregion | |
| 524 | + | |
| 99 | 525 | #region 获取开单记录表 |
| 100 | 526 | /// <summary> |
| 101 | 527 | /// 获取开单记录表 | ... | ... |
netcore/src/Modularity/Extend/NCC.Extend/LqTkDashboardService.cs
| ... | ... | @@ -552,15 +552,15 @@ namespace NCC.Extend.LqTkDashboard |
| 552 | 552 | COALESCE(bigOrder.BigOrderCount, 0) as BigOrderCount, |
| 553 | 553 | COALESCE(bigOrder.BigOrderAmount, 0) as BigOrderAmount |
| 554 | 554 | FROM ( |
| 555 | - -- 基础员工拓客数据 | |
| 555 | + -- 基础员工拓客数据(按员工统计,不按TeamName分组) | |
| 556 | 556 | SELECT |
| 557 | 557 | tk.F_ExpansionUserId as EmployeeId, |
| 558 | 558 | COALESCE(u.F_REALNAME, '') as EmployeeName, |
| 559 | 559 | COALESCE(org.F_FullName, '') as DepartmentName, |
| 560 | 560 | COALESCE(u.F_GW, '') as Position, |
| 561 | - COALESCE(tk.F_StoreId, '') as StoreId, | |
| 562 | - COALESCE(md.dm, '') as StoreName, | |
| 563 | - COALESCE(tk.F_TeamName, '') as TeamName, | |
| 561 | + COALESCE(MAX(tk.F_StoreId), '') as StoreId, | |
| 562 | + COALESCE(MAX(md.dm), '') as StoreName, | |
| 563 | + COALESCE(MAX(tk.F_TeamName), '') as TeamName, | |
| 564 | 564 | COUNT(DISTINCT tk.F_MemberId) as ExpansionCount, |
| 565 | 565 | COALESCE(SUM(CAST(tk.F_BuyNumber AS SIGNED)), 0) as ExpansionCardCount |
| 566 | 566 | FROM lq_tkjlb tk |
| ... | ... | @@ -570,21 +570,34 @@ namespace NCC.Extend.LqTkDashboard |
| 570 | 570 | WHERE {eventFilter} |
| 571 | 571 | AND tk.F_ExpansionTime >= '{startTimeStr}' |
| 572 | 572 | AND tk.F_ExpansionTime <= '{endTimeStr}' |
| 573 | - GROUP BY tk.F_ExpansionUserId, u.F_REALNAME, org.F_FullName, u.F_GW, tk.F_StoreId, md.dm, tk.F_TeamName | |
| 573 | + GROUP BY tk.F_ExpansionUserId, u.F_REALNAME, org.F_FullName, u.F_GW | |
| 574 | 574 | ) emp |
| 575 | 575 | LEFT JOIN ( |
| 576 | - -- 到店人数统计 | |
| 576 | + -- 到店人数统计(一个人多次到店只算一次) | |
| 577 | + -- 先找出该员工拓客的所有会员(去重),然后统计这些会员中有到店记录的会员数 | |
| 578 | + -- 确保到店时间在首次拓客时间之后 | |
| 577 | 579 | SELECT |
| 578 | - tk.F_ExpansionUserId as EmployeeId, | |
| 579 | - COUNT(DISTINCT tk.F_MemberId) as VisitCount | |
| 580 | - FROM lq_tkjlb tk | |
| 581 | - INNER JOIN lq_xh_hyhk xh ON tk.F_MemberId = xh.hy AND xh.F_IsEffective = 1 | |
| 582 | - WHERE {eventFilter} | |
| 583 | - AND tk.F_ExpansionTime >= '{startTimeStr}' | |
| 584 | - AND tk.F_ExpansionTime <= '{endTimeStr}' | |
| 580 | + emp_members.EmployeeId, | |
| 581 | + COUNT(DISTINCT CASE WHEN xh.hy IS NOT NULL THEN emp_members.MemberId END) as VisitCount | |
| 582 | + FROM ( | |
| 583 | + -- 先获取该员工拓客的所有会员(去重),并记录每个会员的首次拓客时间 | |
| 584 | + SELECT | |
| 585 | + tk.F_ExpansionUserId as EmployeeId, | |
| 586 | + tk.F_MemberId as MemberId, | |
| 587 | + MIN(tk.F_ExpansionTime) as FirstExpansionTime | |
| 588 | + FROM lq_tkjlb tk | |
| 589 | + WHERE {eventFilter} | |
| 590 | + AND tk.F_ExpansionTime >= '{startTimeStr}' | |
| 591 | + AND tk.F_ExpansionTime <= '{endTimeStr}' | |
| 592 | + AND tk.F_MemberId IS NOT NULL | |
| 593 | + GROUP BY tk.F_ExpansionUserId, tk.F_MemberId | |
| 594 | + ) emp_members | |
| 595 | + LEFT JOIN lq_xh_hyhk xh ON emp_members.MemberId = xh.hy | |
| 596 | + AND xh.F_IsEffective = 1 | |
| 597 | + AND xh.hksj >= emp_members.FirstExpansionTime | |
| 585 | 598 | AND xh.hksj >= '{startTimeStr}' |
| 586 | 599 | AND xh.hksj <= '{endTimeStr}' |
| 587 | - GROUP BY tk.F_ExpansionUserId | |
| 600 | + GROUP BY emp_members.EmployeeId | |
| 588 | 601 | ) visit ON emp.EmployeeId = visit.EmployeeId |
| 589 | 602 | LEFT JOIN ( |
| 590 | 603 | -- 开单人数和金额统计 | ... | ... |
netcore/src/Modularity/Extend/NCC.Extend/LqXhHyhkService.cs
| ... | ... | @@ -370,6 +370,7 @@ namespace NCC.Extend.LqXhHyhk |
| 370 | 370 | memberPhone = SqlFunc.Subqueryable<LqKhxxEntity>().Where(w => w.Id == it.Hy).Select(w => w.Sjh), |
| 371 | 371 | isEffective = it.IsEffective, |
| 372 | 372 | signatureFile = it.SignatureFile, |
| 373 | + remark = it.Remark, | |
| 373 | 374 | overtimeCoefficient = it.OvertimeCoefficient, |
| 374 | 375 | originalSgfy = it.OriginalSgfy, |
| 375 | 376 | overtimeSgfy = it.OvertimeSgfy, |
| ... | ... | @@ -605,6 +606,7 @@ namespace NCC.Extend.LqXhHyhk |
| 605 | 606 | memberPhone = SqlFunc.Subqueryable<LqKhxxEntity>().Where(w => w.Id == hyhk.Hy).Select(w => w.Sjh), |
| 606 | 607 | isEffective = hyhk.IsEffective, |
| 607 | 608 | signatureFile = hyhk.SignatureFile, |
| 609 | + remark = hyhk.Remark, | |
| 608 | 610 | overtimeCoefficient = hyhk.OvertimeCoefficient, |
| 609 | 611 | originalSgfy = hyhk.OriginalSgfy, |
| 610 | 612 | overtimeSgfy = hyhk.OvertimeSgfy, |
| ... | ... | @@ -769,6 +771,7 @@ namespace NCC.Extend.LqXhHyhk |
| 769 | 771 | memberPhone = SqlFunc.Subqueryable<LqKhxxEntity>().Where(w => w.Id == hyhk.Hy).Select(w => w.Sjh), |
| 770 | 772 | isEffective = hyhk.IsEffective, |
| 771 | 773 | signatureFile = hyhk.SignatureFile, |
| 774 | + remark = hyhk.Remark, | |
| 772 | 775 | overtimeCoefficient = hyhk.OvertimeCoefficient, |
| 773 | 776 | originalSgfy = hyhk.OriginalSgfy, |
| 774 | 777 | overtimeSgfy = hyhk.OvertimeSgfy, |
| ... | ... | @@ -1155,6 +1158,7 @@ namespace NCC.Extend.LqXhHyhk |
| 1155 | 1158 | sfykjb = it.Sfykjb, |
| 1156 | 1159 | hksj = it.Hksj, |
| 1157 | 1160 | czry = it.Czry, |
| 1161 | + remark = it.Remark, | |
| 1158 | 1162 | }) |
| 1159 | 1163 | .MergeTable() |
| 1160 | 1164 | .OrderBy(sidx + " " + input.sort) |
| ... | ... | @@ -1495,6 +1499,64 @@ namespace NCC.Extend.LqXhHyhk |
| 1495 | 1499 | } |
| 1496 | 1500 | #endregion |
| 1497 | 1501 | |
| 1502 | + #region 更新耗卡备注 | |
| 1503 | + /// <summary> | |
| 1504 | + /// 更新耗卡备注 | |
| 1505 | + /// </summary> | |
| 1506 | + /// <remarks> | |
| 1507 | + /// 专门用于更新耗卡记录的备注信息 | |
| 1508 | + /// | |
| 1509 | + /// 示例请求: | |
| 1510 | + /// PUT /api/Extend/LqXhHyhk/{id}/UpdateRemark | |
| 1511 | + /// | |
| 1512 | + /// 请求体: | |
| 1513 | + /// ```json | |
| 1514 | + /// { | |
| 1515 | + /// "remark": "备注信息(最大2000字符)" | |
| 1516 | + /// } | |
| 1517 | + /// ``` | |
| 1518 | + /// </remarks> | |
| 1519 | + /// <param name="id">耗卡记录ID</param> | |
| 1520 | + /// <param name="input">备注信息</param> | |
| 1521 | + /// <returns>无返回值</returns> | |
| 1522 | + /// <response code="200">更新成功</response> | |
| 1523 | + /// <response code="400">参数错误</response> | |
| 1524 | + /// <response code="404">耗卡记录不存在</response> | |
| 1525 | + /// <response code="500">服务器错误</response> | |
| 1526 | + [HttpPut("{id}/UpdateRemark")] | |
| 1527 | + public async Task UpdateRemark(string id, [FromBody] LqXhHyhkUpdateRemarkInput input) | |
| 1528 | + { | |
| 1529 | + try | |
| 1530 | + { | |
| 1531 | + // 查询记录 | |
| 1532 | + var entity = await _db.Queryable<LqXhHyhkEntity>() | |
| 1533 | + .Where(p => p.Id == id && p.IsEffective == StatusEnum.有效.GetHashCode()) | |
| 1534 | + .FirstAsync(); | |
| 1535 | + | |
| 1536 | + if (entity == null) | |
| 1537 | + { | |
| 1538 | + throw NCCException.Oh(ErrorCode.COM1005, "耗卡记录不存在或已作废"); | |
| 1539 | + } | |
| 1540 | + | |
| 1541 | + // 更新备注 | |
| 1542 | + entity.Remark = input.remark; | |
| 1543 | + entity.UpdateTime = DateTime.Now; | |
| 1544 | + | |
| 1545 | + // 保存更新 | |
| 1546 | + await _db.Updateable(entity) | |
| 1547 | + .UpdateColumns(it => new { it.Remark, it.UpdateTime }) | |
| 1548 | + .ExecuteCommandAsync(); | |
| 1549 | + | |
| 1550 | + _logger.LogInformation($"更新耗卡备注成功,ID:{id}"); | |
| 1551 | + } | |
| 1552 | + catch (Exception ex) | |
| 1553 | + { | |
| 1554 | + _logger.LogError(ex, "更新耗卡备注失败,ID:{Id}", id); | |
| 1555 | + throw; | |
| 1556 | + } | |
| 1557 | + } | |
| 1558 | + #endregion | |
| 1559 | + | |
| 1498 | 1560 | #region 删除会员耗卡 |
| 1499 | 1561 | /// <summary> |
| 1500 | 1562 | /// 删除会员耗卡 | ... | ... |