Commit 8b44d4bd9ad36ec5c24615ba9a16d10c1cbc4f1c
1 parent
6af31905
feat: 添加健康师额外工资和合作成本导入的清理功能
- 健康师额外工资导入:添加清理导入月份数据参数,默认true - 合作成本导入:添加清理导入月份数据参数,默认true,支持多成本类型和多笔记录 - 合作成本导入返回结果:添加成本类型字段到successRecords - 移除合作成本导入的重复检查,支持同一门店同一月份多笔记录
Showing
17 changed files
with
303 additions
and
159 deletions
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/extend/healthCoachSalary.js
| ... | ... | @@ -27,9 +27,10 @@ export function getExtraCalculationList(params) { |
| 27 | 27 | } |
| 28 | 28 | |
| 29 | 29 | // 导入额外计算数据 |
| 30 | -export function importExtraCalculationFromExcel(file) { | |
| 30 | +export function importExtraCalculationFromExcel(file, clearBeforeImport = false) { | |
| 31 | 31 | const formData = new FormData() |
| 32 | 32 | formData.append('file', file) |
| 33 | + formData.append('clearBeforeImport', clearBeforeImport) | |
| 33 | 34 | return request({ |
| 34 | 35 | url: '/api/Extend/lqsalaryextracalculation/ImportFromExcel', |
| 35 | 36 | method: 'POST', | ... | ... |
antis-ncc-admin/src/views/lqCooperationCost/import-dialog.vue
| ... | ... | @@ -21,12 +21,25 @@ |
| 21 | 21 | <div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div> |
| 22 | 22 | <div class="el-upload__tip" slot="tip">只能上传xlsx/xls文件,且不超过10MB</div> |
| 23 | 23 | </el-upload> |
| 24 | + <div style="margin-top: 15px;"> | |
| 25 | + <el-checkbox v-model="clearBeforeImport"> | |
| 26 | + 清理导入月份数据 | |
| 27 | + </el-checkbox> | |
| 28 | + </div> | |
| 24 | 29 | <div v-if="uploadResult" class="upload-result" :class="uploadResult.type"> |
| 25 | 30 | <div class="result-title">{{ uploadResult.title }}</div> |
| 26 | 31 | <div class="result-description">{{ uploadResult.description }}</div> |
| 27 | 32 | <div v-if="uploadResult.failMessages && uploadResult.failMessages.length > 0" class="fail-messages"> |
| 28 | 33 | <div v-for="(msg, index) in uploadResult.failMessages" :key="index" class="fail-message">{{ msg }}</div> |
| 29 | 34 | </div> |
| 35 | + <div v-if="uploadResult.successRecords && uploadResult.successRecords.length > 0" class="success-records"> | |
| 36 | + <div class="success-title">成功导入的记录(包含成本类型):</div> | |
| 37 | + <div v-for="(record, index) in uploadResult.successRecords" :key="index" class="success-record"> | |
| 38 | + 门店ID: {{ record.storeId }}, 年份: {{ record.year }}, 月份: {{ record.month }}, | |
| 39 | + 金额: {{ record.totalAmount }}, 成本类型: {{ record.costType || '无' }}, | |
| 40 | + 备注: {{ record.remarks || '无' }} | |
| 41 | + </div> | |
| 42 | + </div> | |
| 30 | 43 | </div> |
| 31 | 44 | </div> |
| 32 | 45 | <div slot="footer" class="dialog-footer"> |
| ... | ... | @@ -45,6 +58,7 @@ export default { |
| 45 | 58 | visible: false, |
| 46 | 59 | fileList: [], |
| 47 | 60 | uploading: false, |
| 61 | + clearBeforeImport: true, // 是否需要清理导入月份数据,默认true | |
| 48 | 62 | uploadResult: null |
| 49 | 63 | } |
| 50 | 64 | }, |
| ... | ... | @@ -53,6 +67,7 @@ export default { |
| 53 | 67 | this.visible = true |
| 54 | 68 | this.fileList = [] |
| 55 | 69 | this.uploading = false |
| 70 | + this.clearBeforeImport = true | |
| 56 | 71 | this.uploadResult = null |
| 57 | 72 | }, |
| 58 | 73 | handleFileChange(file, fileList) { |
| ... | ... | @@ -73,6 +88,7 @@ export default { |
| 73 | 88 | this.uploadResult = null |
| 74 | 89 | const formData = new FormData() |
| 75 | 90 | formData.append('file', file.raw) |
| 91 | + formData.append('clearBeforeImport', this.clearBeforeImport) | |
| 76 | 92 | request({ |
| 77 | 93 | url: '/api/Extend/LqCooperationCost/Actions/Import', |
| 78 | 94 | method: 'POST', |
| ... | ... | @@ -88,7 +104,8 @@ export default { |
| 88 | 104 | type: 'success', |
| 89 | 105 | title: '导入成功', |
| 90 | 106 | description: `成功导入 ${data.successCount || 0} 条,失败 ${data.failCount || 0} 条`, |
| 91 | - failMessages: data.failMessages || [] | |
| 107 | + failMessages: data.failMessages || [], | |
| 108 | + successRecords: data.successRecords || [] | |
| 92 | 109 | } |
| 93 | 110 | this.$message.success('导入成功') |
| 94 | 111 | setTimeout(() => { |
| ... | ... | @@ -163,6 +180,24 @@ export default { |
| 163 | 180 | margin-bottom: 5px; |
| 164 | 181 | } |
| 165 | 182 | } |
| 183 | + | |
| 184 | + .success-records { | |
| 185 | + margin-top: 10px; | |
| 186 | + padding-top: 10px; | |
| 187 | + border-top: 1px solid #ddd; | |
| 188 | + | |
| 189 | + .success-title { | |
| 190 | + font-weight: 600; | |
| 191 | + margin-bottom: 8px; | |
| 192 | + color: #67C23A; | |
| 193 | + } | |
| 194 | + | |
| 195 | + .success-record { | |
| 196 | + color: #606266; | |
| 197 | + margin-bottom: 5px; | |
| 198 | + font-size: 12px; | |
| 199 | + } | |
| 200 | + } | |
| 166 | 201 | } |
| 167 | 202 | </style> |
| 168 | 203 | ... | ... |
antis-ncc-admin/src/views/wageManagement/extra-data-dialog.vue
| ... | ... | @@ -57,6 +57,12 @@ |
| 57 | 57 | > |
| 58 | 58 | 导入 |
| 59 | 59 | </el-button> |
| 60 | + <el-checkbox | |
| 61 | + v-model="clearBeforeImport" | |
| 62 | + style="margin-left: 20px;" | |
| 63 | + > | |
| 64 | + 清理导入月份数据 | |
| 65 | + </el-checkbox> | |
| 60 | 66 | </div> |
| 61 | 67 | |
| 62 | 68 | <!-- 数据表格 --> |
| ... | ... | @@ -190,6 +196,7 @@ export default { |
| 190 | 196 | loading: false, |
| 191 | 197 | exportTemplateLoading: false, |
| 192 | 198 | importLoading: false, |
| 199 | + clearBeforeImport: true, // 是否需要清理导入月份数据,默认true | |
| 193 | 200 | list: [], |
| 194 | 201 | total: 0, |
| 195 | 202 | queryParams: { |
| ... | ... | @@ -451,7 +458,7 @@ export default { |
| 451 | 458 | |
| 452 | 459 | this.importLoading = true |
| 453 | 460 | try { |
| 454 | - const response = await importExtraCalculationFromExcel(file) | |
| 461 | + const response = await importExtraCalculationFromExcel(file, this.clearBeforeImport) | |
| 455 | 462 | |
| 456 | 463 | if (response.code === 200) { |
| 457 | 464 | this.$message.success('导入成功') | ... | ... |
excel/健康师额外数据模板_2025年11月_20251219120434.xlsx
0 → 100644
No preview for this file type
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqMajorProjectTeacherSalary/MajorProjectTeacherSalaryOutput.cs
| ... | ... | @@ -18,12 +18,12 @@ namespace NCC.Extend.Entitys.Dto.LqMajorProjectTeacherSalary |
| 18 | 18 | public string StatisticsMonth { get; set; } |
| 19 | 19 | |
| 20 | 20 | /// <summary> |
| 21 | - /// 门店ID | |
| 21 | + /// 部门ID(使用StoreId字段存储部门ID) | |
| 22 | 22 | /// </summary> |
| 23 | 23 | public string StoreId { get; set; } |
| 24 | 24 | |
| 25 | 25 | /// <summary> |
| 26 | - /// 门店名称 | |
| 26 | + /// 部门名称(使用StoreName字段存储部门名称) | |
| 27 | 27 | /// </summary> |
| 28 | 28 | public string StoreName { get; set; } |
| 29 | 29 | ... | ... |
netcore/src/Modularity/Extend/NCC.Extend/LqAssistantSalaryService.cs
| ... | ... | @@ -334,53 +334,18 @@ namespace NCC.Extend |
| 334 | 334 | decimal performanceRatio = salary.StoreTotalPerformance / salary.StoreLifeline; |
| 335 | 335 | |
| 336 | 336 | // 根据岗位类型确定提成比例 |
| 337 | - if (isDirector) | |
| 337 | + // 店助和店助主任使用相同的阶梯提成规则 | |
| 338 | + // 先计算总提成金额(阶梯计算) | |
| 339 | + decimal totalCommission = CalculateAssistantCommission(salary.StoreTotalPerformance, salary.StoreLifeline); | |
| 340 | + | |
| 341 | + // 计算平均提成比例(用于显示) | |
| 342 | + if (salary.StoreTotalPerformance > 0) | |
| 338 | 343 | { |
| 339 | - // 店助主任:使用固定比例(按规则文档,业绩≥100%时使用阶梯提成,但为保持比例固定,使用平均比例) | |
| 340 | - // 业绩 < 70%:0% | |
| 341 | - // 70% ≤ 业绩 < 100%:0.4% | |
| 342 | - // 业绩 ≥ 100%:使用阶梯提成计算总提成,然后计算平均比例 | |
| 343 | - if (performanceRatio < 0.7m) | |
| 344 | - { | |
| 345 | - commissionRate = 0; | |
| 346 | - } | |
| 347 | - else if (performanceRatio < 1.0m) | |
| 348 | - { | |
| 349 | - commissionRate = 0.004m; // 0.4% | |
| 350 | - } | |
| 351 | - else | |
| 352 | - { | |
| 353 | - // 业绩 ≥ 100%:使用阶梯提成计算总提成,然后计算平均比例 | |
| 354 | - // ≤生命线部分:0.6%,>生命线部分:1% | |
| 355 | - decimal storeTotalCommission = CalculateDirectorCommission(salary.StoreTotalPerformance, salary.StoreLifeline); | |
| 356 | - if (salary.StoreTotalPerformance > 0) | |
| 357 | - { | |
| 358 | - commissionRate = storeTotalCommission / salary.StoreTotalPerformance; | |
| 359 | - } | |
| 360 | - else | |
| 361 | - { | |
| 362 | - commissionRate = 0; | |
| 363 | - } | |
| 364 | - } | |
| 344 | + commissionRate = totalCommission / salary.StoreTotalPerformance; | |
| 365 | 345 | } |
| 366 | 346 | else |
| 367 | 347 | { |
| 368 | - // 店助:使用固定比例 | |
| 369 | - // 业绩 < 70%:0% | |
| 370 | - // 70% ≤ 业绩 < 100%:0.4% | |
| 371 | - // 业绩 ≥ 100%:0.6% | |
| 372 | - if (performanceRatio < 0.7m) | |
| 373 | - { | |
| 374 | - commissionRate = 0; | |
| 375 | - } | |
| 376 | - else if (performanceRatio < 1.0m) | |
| 377 | - { | |
| 378 | - commissionRate = 0.004m; // 0.4% | |
| 379 | - } | |
| 380 | - else | |
| 381 | - { | |
| 382 | - commissionRate = 0.006m; // 0.6% | |
| 383 | - } | |
| 348 | + commissionRate = 0; | |
| 384 | 349 | } |
| 385 | 350 | } |
| 386 | 351 | |
| ... | ... | @@ -467,12 +432,15 @@ namespace NCC.Extend |
| 467 | 432 | } |
| 468 | 433 | |
| 469 | 434 | // 2.11 按在店天数比例计算店助的提成和奖励 |
| 470 | - // 逻辑:提成金额 = 门店业绩 × 提成比例 / 当月天数 × 在店天数 | |
| 435 | + // 逻辑:提成金额 = 门店总提成(阶梯计算) / 当月天数 × 在店天数 | |
| 471 | 436 | // 阶段奖励 = 门店总奖励 / 当月天数 × 在店天数 |
| 472 | 437 | if (daysInMonth > 0 && workingDays > 0) |
| 473 | 438 | { |
| 474 | - // 按比例计算提成:门店业绩 × 提成比例 / 当月天数 × 在店天数 | |
| 475 | - salary.CommissionAmount = salary.StoreTotalPerformance * commissionRate / daysInMonth * workingDays; | |
| 439 | + // 先计算门店总提成(阶梯计算)- 店助和店助主任使用相同的规则 | |
| 440 | + decimal storeTotalCommission = CalculateAssistantCommission(salary.StoreTotalPerformance, salary.StoreLifeline); | |
| 441 | + | |
| 442 | + // 按比例计算提成:门店总提成 / 当月天数 × 在店天数 | |
| 443 | + salary.CommissionAmount = storeTotalCommission / daysInMonth * workingDays; | |
| 476 | 444 | |
| 477 | 445 | // 按比例计算奖励 |
| 478 | 446 | salary.StageRewardAmount = storeTotalStageReward / daysInMonth * workingDays; |
| ... | ... | @@ -650,21 +618,18 @@ namespace NCC.Extend |
| 650 | 618 | else |
| 651 | 619 | { |
| 652 | 620 | // 门店业绩 ≥ 门店生命线 × 100% → 阶梯提成 |
| 653 | - // 70%以下部分:0% | |
| 654 | - // 70%-100%部分:0.4% | |
| 655 | - // ≤生命线部分:0.6%,>生命线部分:1% | |
| 656 | - decimal stage70 = storeLifeline * 0.7m; | |
| 621 | + // ≤生命线部分(整个生命线):0.6% | |
| 622 | + // >生命线部分:1% | |
| 657 | 623 | decimal stage100 = storeLifeline; |
| 658 | - decimal performance70To100 = stage100 - stage70; | |
| 659 | - decimal performanceAbove100 = storePerformance - stage100; | |
| 624 | + decimal performanceUpToLifeline = stage100; // ≤生命线部分(整个生命线) | |
| 625 | + decimal performanceAbove100 = storePerformance - stage100; // >生命线部分 | |
| 660 | 626 | |
| 661 | - // 70%-100%部分:0.4% | |
| 662 | - decimal commission70To100 = performance70To100 * 0.004m; | |
| 663 | - // ≤生命线部分(0-70%):0%,70%-100%部分:0.4%,已计算 | |
| 627 | + // ≤生命线部分:0.6% | |
| 628 | + decimal commissionUpToLifeline = performanceUpToLifeline * 0.006m; | |
| 664 | 629 | // >生命线部分:1% |
| 665 | 630 | decimal commissionAbove100 = performanceAbove100 * 0.01m; // 1% |
| 666 | 631 | |
| 667 | - return commission70To100 + commissionAbove100; | |
| 632 | + return commissionUpToLifeline + commissionAbove100; | |
| 668 | 633 | } |
| 669 | 634 | } |
| 670 | 635 | } | ... | ... |
netcore/src/Modularity/Extend/NCC.Extend/LqCooperationCostService.cs
| ... | ... | @@ -409,11 +409,12 @@ namespace NCC.Extend.LqCooperationCost |
| 409 | 409 | /// Content-Type: multipart/form-data |
| 410 | 410 | /// </remarks> |
| 411 | 411 | /// <param name="file">Excel文件</param> |
| 412 | + /// <param name="clearBeforeImport">是否需要清理导入月份数据(默认:true,清理)</param> | |
| 412 | 413 | /// <returns>导入结果</returns> |
| 413 | 414 | /// <response code="200">导入成功</response> |
| 414 | 415 | /// <response code="400">文件格式错误或数据验证失败</response> |
| 415 | 416 | [HttpPost("Actions/Import")] |
| 416 | - public async Task<dynamic> Import(IFormFile file) | |
| 417 | + public async Task<dynamic> Import(IFormFile file, bool clearBeforeImport = true) | |
| 417 | 418 | { |
| 418 | 419 | try |
| 419 | 420 | { |
| ... | ... | @@ -433,6 +434,7 @@ namespace NCC.Extend.LqCooperationCost |
| 433 | 434 | var successCount = 0; |
| 434 | 435 | var failCount = 0; |
| 435 | 436 | var failMessages = new List<string>(); |
| 437 | + var importData = new List<(string StoreId, int Year, string Month, decimal TotalAmount, string CostType, string Remarks)>(); | |
| 436 | 438 | |
| 437 | 439 | // 保存临时文件 |
| 438 | 440 | var tempFilePath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString() + Path.GetExtension(file.FileName)); |
| ... | ... | @@ -517,44 +519,14 @@ namespace NCC.Extend.LqCooperationCost |
| 517 | 519 | continue; |
| 518 | 520 | } |
| 519 | 521 | |
| 520 | - // 检查是否已存在相同门店、年份、月份的记录 | |
| 521 | - var exists = await _db.Queryable<LqCooperationCostEntity>() | |
| 522 | - .Where(x => x.StoreId == storeId && x.Year == year && x.Month == monthText && x.IsEffective == StatusEnum.有效.GetHashCode()) | |
| 523 | - .AnyAsync(); | |
| 522 | + // 注意:一个门店在同一个月份可以有多笔成本记录(包括相同成本类型、相同金额) | |
| 523 | + // 因为业务上可能需要记录多笔成本,比如多次合作项目、多次设备维护等 | |
| 524 | + // 统计时会按门店汇总所有记录的金额,所以不需要做重复检查 | |
| 525 | + // 如果用户需要避免重复导入,可以使用"清理导入月份数据"功能 | |
| 524 | 526 | |
| 525 | - if (exists) | |
| 526 | - { | |
| 527 | - failMessages.Add($"第{i + 1}行:该门店{year}年{monthText}月的记录已存在"); | |
| 528 | - failCount++; | |
| 529 | - continue; | |
| 530 | - } | |
| 531 | - | |
| 532 | - // 创建记录 | |
| 533 | - var entity = new LqCooperationCostEntity | |
| 534 | - { | |
| 535 | - Id = YitIdHelper.NextId().ToString(), | |
| 536 | - StoreId = storeId, | |
| 537 | - StoreName = storeName, | |
| 538 | - Year = year, | |
| 539 | - Month = monthText, | |
| 540 | - TotalAmount = totalAmount, | |
| 541 | - CostType = costType, | |
| 542 | - Remarks = remarks, | |
| 543 | - IsEffective = StatusEnum.有效.GetHashCode(), | |
| 544 | - CreateUser = _userManager.UserId, | |
| 545 | - CreateTime = DateTime.Now | |
| 546 | - }; | |
| 547 | - | |
| 548 | - var isOk = await _db.Insertable(entity).ExecuteCommandAsync(); | |
| 549 | - if (isOk > 0) | |
| 550 | - { | |
| 551 | - successCount++; | |
| 552 | - } | |
| 553 | - else | |
| 554 | - { | |
| 555 | - failMessages.Add($"第{i + 1}行:保存失败"); | |
| 556 | - failCount++; | |
| 557 | - } | |
| 527 | + // 添加到导入数据列表 | |
| 528 | + importData.Add((storeId, year, monthText, totalAmount, costType, remarks)); | |
| 529 | + successCount++; | |
| 558 | 530 | } |
| 559 | 531 | catch (Exception ex) |
| 560 | 532 | { |
| ... | ... | @@ -572,13 +544,71 @@ namespace NCC.Extend.LqCooperationCost |
| 572 | 544 | } |
| 573 | 545 | } |
| 574 | 546 | |
| 547 | + // 如果需要清理导入月份数据,先删除导入数据中所有涉及的年份+月份组合的数据 | |
| 548 | + if (clearBeforeImport && importData.Any()) | |
| 549 | + { | |
| 550 | + // 获取导入数据中所有唯一的年份+月份组合 | |
| 551 | + var yearMonthPairs = importData | |
| 552 | + .Select(x => new { x.Year, x.Month }) | |
| 553 | + .Distinct() | |
| 554 | + .ToList(); | |
| 555 | + | |
| 556 | + foreach (var pair in yearMonthPairs) | |
| 557 | + { | |
| 558 | + await _db.Deleteable<LqCooperationCostEntity>() | |
| 559 | + .Where(x => x.Year == pair.Year && x.Month == pair.Month && x.IsEffective == StatusEnum.有效.GetHashCode()) | |
| 560 | + .ExecuteCommandAsync(); | |
| 561 | + } | |
| 562 | + } | |
| 563 | + | |
| 564 | + // 批量插入导入数据 | |
| 565 | + if (importData.Any()) | |
| 566 | + { | |
| 567 | + // 批量获取门店信息 | |
| 568 | + var storeIds = importData.Select(x => x.StoreId).Distinct().ToList(); | |
| 569 | + var stores = await _db.Queryable<LqMdxxEntity>() | |
| 570 | + .Where(x => storeIds.Contains(x.Id)) | |
| 571 | + .Select(x => new { x.Id, x.Dm }) | |
| 572 | + .ToListAsync(); | |
| 573 | + var storeDict = stores.ToDictionary(x => x.Id, x => x.Dm); | |
| 574 | + | |
| 575 | + var entities = importData.Select(x => new LqCooperationCostEntity | |
| 576 | + { | |
| 577 | + Id = YitIdHelper.NextId().ToString(), | |
| 578 | + StoreId = x.StoreId, | |
| 579 | + StoreName = storeDict.ContainsKey(x.StoreId) ? storeDict[x.StoreId] : "", | |
| 580 | + Year = x.Year, | |
| 581 | + Month = x.Month, | |
| 582 | + TotalAmount = x.TotalAmount, | |
| 583 | + CostType = x.CostType, | |
| 584 | + Remarks = x.Remarks, | |
| 585 | + IsEffective = StatusEnum.有效.GetHashCode(), | |
| 586 | + CreateUser = _userManager.UserId, | |
| 587 | + CreateTime = DateTime.Now | |
| 588 | + }).ToList(); | |
| 589 | + | |
| 590 | + await _db.Insertable(entities).ExecuteCommandAsync(); | |
| 591 | + } | |
| 592 | + | |
| 593 | + // 构建返回结果,包含成功导入的记录的成本类型信息 | |
| 594 | + var successRecords = importData.Select(x => new | |
| 595 | + { | |
| 596 | + storeId = x.StoreId, | |
| 597 | + year = x.Year, | |
| 598 | + month = x.Month, | |
| 599 | + totalAmount = x.TotalAmount, | |
| 600 | + costType = x.CostType, | |
| 601 | + remarks = x.Remarks | |
| 602 | + }).ToList(); | |
| 603 | + | |
| 575 | 604 | return new |
| 576 | 605 | { |
| 577 | 606 | success = true, |
| 578 | 607 | message = $"导入完成:成功{successCount}条,失败{failCount}条", |
| 579 | 608 | successCount = successCount, |
| 580 | 609 | failCount = failCount, |
| 581 | - failMessages = failMessages | |
| 610 | + failMessages = failMessages, | |
| 611 | + successRecords = successRecords | |
| 582 | 612 | }; |
| 583 | 613 | } |
| 584 | 614 | catch (Exception ex) | ... | ... |
netcore/src/Modularity/Extend/NCC.Extend/LqMajorProjectDirectorSalaryService.cs
| ... | ... | @@ -7,7 +7,10 @@ using NCC.DynamicApiController; |
| 7 | 7 | using NCC.Extend.Entitys.Dto.LqMajorProjectDirectorSalary; |
| 8 | 8 | using NCC.Extend.Entitys.lq_attendance_summary; |
| 9 | 9 | using NCC.Extend.Entitys.lq_hytk_hytk; |
| 10 | +using NCC.Extend.Entitys.lq_hytk_mx; | |
| 10 | 11 | using NCC.Extend.Entitys.lq_kd_kdjlb; |
| 12 | +using NCC.Extend.Entitys.lq_kd_pxmx; | |
| 13 | +using NCC.Extend.Entitys.lq_kd_jksyj; | |
| 11 | 14 | using NCC.Extend.Entitys.lq_md_target; |
| 12 | 15 | using NCC.Extend.Entitys.lq_mdxx; |
| 13 | 16 | using NCC.Extend.Entitys.lq_major_project_director_salary_statistics; |
| ... | ... | @@ -214,19 +217,33 @@ namespace NCC.Extend |
| 214 | 217 | { |
| 215 | 218 | foreach (var storeId in allManagedStoreIds) |
| 216 | 219 | { |
| 217 | - // 该门店的开单金额 | |
| 218 | - var storeBillingAmount = await _db.Queryable<LqKdKdjlbEntity>() | |
| 219 | - .Where(x => x.IsEffective == 1 | |
| 220 | - && x.Djmd == storeId | |
| 221 | - && x.Kdrq >= startDate && x.Kdrq <= endDate.AddDays(1)) | |
| 222 | - .SumAsync(x => (decimal?)x.Sfyj) ?? 0m; | |
| 223 | - | |
| 224 | - // 该门店的退卡金额(优先使用ActualRefundAmount,如果没有则使用Tkje) | |
| 225 | - var storeRefundAmount = await _db.Queryable<LqHytkHytkEntity>() | |
| 226 | - .Where(x => x.IsEffective == 1 | |
| 227 | - && x.Md == storeId | |
| 228 | - && x.Tksj >= startDate && x.Tksj <= endDate.AddDays(1)) | |
| 229 | - .SumAsync(x => (decimal?)(x.ActualRefundAmount ?? x.Tkje ?? 0)) ?? 0m; | |
| 220 | + // 该门店的开单金额(只统计医美类型的开单,从lq_kd_jksyj表统计医美品项的健康师业绩) | |
| 221 | + var storeBillingList = await _db.Queryable<LqKdJksyjEntity, LqKdKdjlbEntity>( | |
| 222 | + (jksyj, kdjlb) => new JoinQueryInfos( | |
| 223 | + JoinType.Inner, jksyj.Glkdbh == kdjlb.Id)) | |
| 224 | + .Where((jksyj, kdjlb) => | |
| 225 | + kdjlb.IsEffective == 1 | |
| 226 | + && jksyj.IsEffective == 1 | |
| 227 | + && kdjlb.Djmd == storeId | |
| 228 | + && kdjlb.Kdrq >= startDate && kdjlb.Kdrq <= endDate.AddDays(1) | |
| 229 | + && jksyj.ItemCategory == "医美" | |
| 230 | + && !string.IsNullOrEmpty(jksyj.Jksyj)) | |
| 231 | + .Select((jksyj, kdjlb) => jksyj.Jksyj) | |
| 232 | + .ToListAsync(); | |
| 233 | + var storeBillingAmount = storeBillingList | |
| 234 | + .Sum(x => decimal.TryParse(x, out var val) ? val : 0m); | |
| 235 | + | |
| 236 | + // 该门店的退卡金额(只统计医美类型的退卡,从lq_hytk_mx表统计医美品项的退卡金额) | |
| 237 | + var storeRefundAmount = await _db.Queryable<LqHytkMxEntity, LqHytkHytkEntity>( | |
| 238 | + (mx, hytk) => new JoinQueryInfos( | |
| 239 | + JoinType.Inner, mx.RefundInfoId == hytk.Id)) | |
| 240 | + .Where((mx, hytk) => | |
| 241 | + hytk.IsEffective == 1 | |
| 242 | + && mx.IsEffective == 1 | |
| 243 | + && hytk.Md == storeId | |
| 244 | + && hytk.Tksj >= startDate && hytk.Tksj <= endDate.AddDays(1) | |
| 245 | + && mx.ItemCategory == "医美") | |
| 246 | + .SumAsync((mx, hytk) => (decimal?)(mx.Tkje ?? 0)) ?? 0m; | |
| 230 | 247 | |
| 231 | 248 | // 该门店的净总业绩 |
| 232 | 249 | var storeTotalPerformance = storeBillingAmount - storeRefundAmount; | ... | ... |
netcore/src/Modularity/Extend/NCC.Extend/LqMajorProjectTeacherSalaryService.cs
| ... | ... | @@ -7,7 +7,10 @@ using NCC.DynamicApiController; |
| 7 | 7 | using NCC.Extend.Entitys.Dto.LqMajorProjectTeacherSalary; |
| 8 | 8 | using NCC.Extend.Entitys.lq_attendance_summary; |
| 9 | 9 | using NCC.Extend.Entitys.lq_hytk_hytk; |
| 10 | +using NCC.Extend.Entitys.lq_hytk_mx; | |
| 10 | 11 | using NCC.Extend.Entitys.lq_kd_kdjlb; |
| 12 | +using NCC.Extend.Entitys.lq_kd_pxmx; | |
| 13 | +using NCC.Extend.Entitys.lq_kd_jksyj; | |
| 11 | 14 | using NCC.Extend.Entitys.lq_md_major_project_teacher_assignment; |
| 12 | 15 | using NCC.Extend.Entitys.lq_md_xdbhsj; |
| 13 | 16 | using NCC.Extend.Entitys.lq_mdxx; |
| ... | ... | @@ -182,25 +185,40 @@ namespace NCC.Extend |
| 182 | 185 | .ToDictionary(g => g.Key, g => g.First()); |
| 183 | 186 | |
| 184 | 187 | // 1.4 门店总业绩计算 (开单实付 - 退卡金额) |
| 185 | - // 开单实付(从lq_kd_kdjlb表统计sfyj字段) | |
| 186 | - var storeBillingList = await _db.Queryable<LqKdKdjlbEntity>() | |
| 187 | - .Where(x => x.Kdrq >= startDate && x.Kdrq <= endDate.AddDays(1) && x.IsEffective == 1) | |
| 188 | - .Select(x => new { x.Djmd, x.Sfyj }) | |
| 188 | + // 开单实付(只统计医美类型的开单,从lq_kd_jksyj表统计医美品项的健康师业绩) | |
| 189 | + var storeBillingList = await _db.Queryable<LqKdJksyjEntity, LqKdKdjlbEntity>( | |
| 190 | + (jksyj, kdjlb) => new JoinQueryInfos( | |
| 191 | + JoinType.Inner, jksyj.Glkdbh == kdjlb.Id)) | |
| 192 | + .Where((jksyj, kdjlb) => | |
| 193 | + kdjlb.Kdrq >= startDate && | |
| 194 | + kdjlb.Kdrq <= endDate.AddDays(1) && | |
| 195 | + kdjlb.IsEffective == 1 && | |
| 196 | + jksyj.IsEffective == 1 && | |
| 197 | + jksyj.ItemCategory == "医美" && | |
| 198 | + !string.IsNullOrEmpty(jksyj.Jksyj)) | |
| 199 | + .Select((jksyj, kdjlb) => new { kdjlb.Djmd, Jksyj = jksyj.Jksyj }) | |
| 189 | 200 | .ToListAsync(); |
| 190 | 201 | var storeBillingDict = storeBillingList |
| 191 | 202 | .Where(x => !string.IsNullOrEmpty(x.Djmd)) |
| 192 | 203 | .GroupBy(x => x.Djmd) |
| 193 | - .ToDictionary(g => g.Key, g => g.Sum(x => x.Sfyj)); | |
| 194 | - | |
| 195 | - // 退卡金额(从lq_hytk_hytk表统计,使用F_ActualRefundAmount,如果没有则使用tkje) | |
| 196 | - var storeRefundList = await _db.Queryable<LqHytkHytkEntity>() | |
| 197 | - .Where(x => x.Tksj >= startDate && x.Tksj <= endDate.AddDays(1) && x.IsEffective == 1) | |
| 198 | - .Select(x => new { x.Md, x.ActualRefundAmount, x.Tkje }) | |
| 204 | + .ToDictionary(g => g.Key, g => g.Sum(x => decimal.TryParse(x.Jksyj, out var val) ? val : 0m)); | |
| 205 | + | |
| 206 | + // 退卡金额(只统计医美类型的退卡,从lq_hytk_mx表统计医美品项的退卡金额) | |
| 207 | + var storeRefundList = await _db.Queryable<LqHytkMxEntity, LqHytkHytkEntity>( | |
| 208 | + (mx, hytk) => new JoinQueryInfos( | |
| 209 | + JoinType.Inner, mx.RefundInfoId == hytk.Id)) | |
| 210 | + .Where((mx, hytk) => | |
| 211 | + hytk.Tksj >= startDate && | |
| 212 | + hytk.Tksj <= endDate.AddDays(1) && | |
| 213 | + hytk.IsEffective == 1 && | |
| 214 | + mx.IsEffective == 1 && | |
| 215 | + mx.ItemCategory == "医美") | |
| 216 | + .Select((mx, hytk) => new { hytk.Md, mx.Tkje }) | |
| 199 | 217 | .ToListAsync(); |
| 200 | 218 | var storeRefundDict = storeRefundList |
| 201 | 219 | .Where(x => !string.IsNullOrEmpty(x.Md)) |
| 202 | 220 | .GroupBy(x => x.Md) |
| 203 | - .ToDictionary(g => g.Key, g => g.Sum(x => x.ActualRefundAmount ?? x.Tkje ?? 0)); | |
| 221 | + .ToDictionary(g => g.Key, g => g.Sum(x => x.Tkje ?? 0)); | |
| 204 | 222 | |
| 205 | 223 | // 1.5 考勤数据 (lq_attendance_summary) |
| 206 | 224 | var attendanceList = await _db.Queryable<LqAttendanceSummaryEntity>() |
| ... | ... | @@ -212,10 +230,18 @@ namespace NCC.Extend |
| 212 | 230 | var teacherIds = assignmentList.Select(x => x.TeacherId).Where(x => !string.IsNullOrEmpty(x)).Distinct().ToList(); |
| 213 | 231 | var userList = await _db.Queryable<UserEntity>() |
| 214 | 232 | .Where(x => teacherIds.Contains(x.Id)) |
| 215 | - .Select(x => new { x.Id, x.RealName, x.Account, x.IsOnJob }) | |
| 233 | + .Select(x => new { x.Id, x.RealName, x.Account, x.IsOnJob, x.OrganizeId }) | |
| 216 | 234 | .ToListAsync(); |
| 217 | 235 | var userDict = userList.ToDictionary(x => x.Id, x => x); |
| 218 | 236 | |
| 237 | + // 1.7 获取部门信息 (BASE_ORGANIZE) | |
| 238 | + var organizeIds = userList.Where(x => !string.IsNullOrEmpty(x.OrganizeId)).Select(x => x.OrganizeId).Distinct().ToList(); | |
| 239 | + var organizeList = await _db.Queryable<OrganizeEntity>() | |
| 240 | + .Where(x => organizeIds.Contains(x.Id)) | |
| 241 | + .Select(x => new { x.Id, x.FullName }) | |
| 242 | + .ToListAsync(); | |
| 243 | + var organizeDict = organizeList.ToDictionary(x => x.Id, x => x); | |
| 244 | + | |
| 219 | 245 | // 2. 按大项目部老师聚合数据 |
| 220 | 246 | var teacherStats = new Dictionary<string, LqMajorProjectTeacherSalaryStatisticsEntity>(); |
| 221 | 247 | |
| ... | ... | @@ -268,35 +294,29 @@ namespace NCC.Extend |
| 268 | 294 | salary.EmployeeName = user.RealName ?? ""; |
| 269 | 295 | salary.EmployeeAccount = user.Account ?? ""; |
| 270 | 296 | salary.IsTerminated = user.IsOnJob == 0 ? 1 : 0; |
| 271 | - } | |
| 272 | 297 | |
| 273 | - // 2.3 填充门店信息 | |
| 274 | - if (storeDict.ContainsKey(storeId)) | |
| 275 | - { | |
| 276 | - var store = storeDict[storeId]; | |
| 277 | - salary.StoreId = storeId; | |
| 278 | - salary.StoreName = store.Dm ?? ""; | |
| 279 | - salary.StoreType = store.StoreType; | |
| 280 | - salary.StoreCategory = store.StoreCategory; | |
| 298 | + // 2.3 填充部门信息(使用StoreId和StoreName字段存储部门信息) | |
| 299 | + if (!string.IsNullOrEmpty(user.OrganizeId) && organizeDict.ContainsKey(user.OrganizeId)) | |
| 300 | + { | |
| 301 | + var organize = organizeDict[user.OrganizeId]; | |
| 302 | + salary.StoreId = user.OrganizeId; // 存储部门ID | |
| 303 | + salary.StoreName = organize.FullName ?? ""; // 存储部门名称 | |
| 304 | + } | |
| 305 | + else | |
| 306 | + { | |
| 307 | + salary.StoreId = user.OrganizeId ?? ""; | |
| 308 | + salary.StoreName = ""; | |
| 309 | + } | |
| 281 | 310 | } |
| 282 | 311 | else |
| 283 | 312 | { |
| 284 | - salary.StoreId = storeId; | |
| 313 | + salary.StoreId = ""; | |
| 285 | 314 | salary.StoreName = ""; |
| 286 | 315 | } |
| 287 | 316 | |
| 288 | - // 2.4 新店保护信息 | |
| 289 | - if (!string.IsNullOrEmpty(salary.StoreId) && newStoreProtectionDict.ContainsKey(salary.StoreId)) | |
| 290 | - { | |
| 291 | - var protection = newStoreProtectionDict[salary.StoreId]; | |
| 292 | - salary.IsNewStore = "是"; | |
| 293 | - salary.NewStoreProtectionStage = protection.Stage; | |
| 294 | - } | |
| 295 | - else | |
| 296 | - { | |
| 297 | - salary.IsNewStore = "否"; | |
| 298 | - salary.NewStoreProtectionStage = 0; | |
| 299 | - } | |
| 317 | + // 2.4 新店保护信息(不再使用,因为现在存储的是部门信息) | |
| 318 | + salary.IsNewStore = "否"; | |
| 319 | + salary.NewStoreProtectionStage = 0; | |
| 300 | 320 | |
| 301 | 321 | // 2.5 统计门店总业绩(开单业绩 - 退卡业绩) |
| 302 | 322 | var billing = storeBillingDict.ContainsKey(storeId) ? storeBillingDict[storeId] : 0; | ... | ... |
netcore/src/Modularity/Extend/NCC.Extend/LqSalaryExtraCalculationService.cs
| ... | ... | @@ -106,9 +106,10 @@ namespace NCC.Extend |
| 106 | 106 | /// 从Excel导入健康师工资额外计算数据 |
| 107 | 107 | /// </summary> |
| 108 | 108 | /// <param name="file">Excel文件</param> |
| 109 | + /// <param name="clearBeforeImport">是否需要清理导入月份数据(默认:true,清理)</param> | |
| 109 | 110 | /// <returns>导入结果</returns> |
| 110 | 111 | [HttpPost("ImportFromExcel")] |
| 111 | - public async Task<dynamic> ImportFromExcel(IFormFile file) | |
| 112 | + public async Task<dynamic> ImportFromExcel(IFormFile file, bool clearBeforeImport = true) | |
| 112 | 113 | { |
| 113 | 114 | try |
| 114 | 115 | { |
| ... | ... | @@ -277,6 +278,23 @@ namespace NCC.Extend |
| 277 | 278 | throw NCCException.Oh("Excel文件中没有有效的数据行"); |
| 278 | 279 | } |
| 279 | 280 | |
| 281 | + // 如果需要清理导入月份数据,先删除导入数据中所有涉及的年份+月份组合的数据 | |
| 282 | + if (clearBeforeImport) | |
| 283 | + { | |
| 284 | + // 获取导入数据中所有唯一的年份+月份组合 | |
| 285 | + var yearMonthPairs = importData | |
| 286 | + .Select(x => new { x.Year, x.Month }) | |
| 287 | + .Distinct() | |
| 288 | + .ToList(); | |
| 289 | + | |
| 290 | + foreach (var pair in yearMonthPairs) | |
| 291 | + { | |
| 292 | + await _db.Deleteable<LqSalaryExtraCalculationEntity>() | |
| 293 | + .Where(x => x.Year == pair.Year && x.Month == pair.Month) | |
| 294 | + .ExecuteCommandAsync(); | |
| 295 | + } | |
| 296 | + } | |
| 297 | + | |
| 280 | 298 | // 处理导入数据 |
| 281 | 299 | return await ProcessImportData(importData); |
| 282 | 300 | } | ... | ... |
netcore/src/Modularity/Extend/NCC.Extend/LqShareStatisticsHqService.cs
| ... | ... | @@ -8,6 +8,8 @@ using NCC.Extend.Entitys.lq_kd_kdjlb; |
| 8 | 8 | using NCC.Extend.Entitys.lq_hytk_hytk; |
| 9 | 9 | using NCC.Extend.Entitys.lq_kd_pxmx; |
| 10 | 10 | using NCC.Extend.Entitys.lq_contract_rent_detail; |
| 11 | +using NCC.Extend.Entitys.lq_contract; | |
| 12 | +using NCC.Extend.Entitys; | |
| 11 | 13 | using SqlSugar; |
| 12 | 14 | using System; |
| 13 | 15 | using System.Linq; |
| ... | ... | @@ -151,23 +153,64 @@ namespace NCC.Extend |
| 151 | 153 | /// </summary> |
| 152 | 154 | private async Task CalculateCost(LqShareStatisticsHqEntity entity, DateTime startDate, DateTime endDate, string statisticsMonth) |
| 153 | 155 | { |
| 154 | - // 1. 成本-报销 (TODO: 需要确认总部报销的判定方式) | |
| 155 | - entity.CostReimbursement = 0; | |
| 156 | + // 1. 成本-报销:筛选一级分类为"总部费用"的申请,审批状态为"已通过" | |
| 157 | + var reimbursementAmount = await _db.Queryable<LqReimbursementApplicationEntity, LqPurchaseRecordsEntity, LqReimbursementCategoryEntity>( | |
| 158 | + (app, purchase, category) => app.PurchaseRecordsId == purchase.Id && purchase.ReimbursementCategoryId == category.Id) | |
| 159 | + .Where((app, purchase, category) => | |
| 160 | + category.Level1Name == "总部费用" | |
| 161 | + && app.ApprovalStatus == "已通过" | |
| 162 | + && app.ApplicationTime >= startDate | |
| 163 | + && app.ApplicationTime <= endDate.AddDays(1)) | |
| 164 | + .SumAsync((app, purchase, category) => purchase.Amount); | |
| 165 | + entity.CostReimbursement = reimbursementAmount; | |
| 156 | 166 | |
| 157 | 167 | // 2. 成本-人工 (保留) |
| 158 | 168 | entity.CostLabor = 0; |
| 159 | 169 | |
| 160 | 170 | // 3. 成本-教育部房租 |
| 161 | - // TODO: 需要确认如何识别教育部合同 | |
| 162 | - entity.CostEducationRent = 0; | |
| 171 | + // 从合同月租明细表获取:门店是总部,分类是教育,统计月份匹配 | |
| 172 | + // 使用月份字符串匹配,避免时区问题 | |
| 173 | + var educationRent = await _db.Ado.SqlQuerySingleAsync<decimal>( | |
| 174 | + $@"SELECT COALESCE(SUM(d.F_DueAmount), 0) | |
| 175 | + FROM lq_contract_rent_detail d | |
| 176 | + INNER JOIN lq_contract c ON d.F_ContractId = c.F_Id | |
| 177 | + WHERE c.F_StoreId = '1649328471923847168' | |
| 178 | + AND c.F_Category = '教育' | |
| 179 | + AND DATE_FORMAT(d.F_PaymentMonth, '%Y%m') = @StatisticsMonth | |
| 180 | + AND c.F_IsEffective = 1 | |
| 181 | + AND d.F_IsEffective = 1", | |
| 182 | + new { StatisticsMonth = statisticsMonth }); | |
| 183 | + entity.CostEducationRent = educationRent; | |
| 163 | 184 | |
| 164 | 185 | // 4. 成本-仓库房租 |
| 165 | - // TODO: 需要确认如何识别仓库合同 | |
| 166 | - entity.CostWarehouseRent = 0; | |
| 186 | + // 从合同月租明细表获取:门店是总部,分类是仓库,统计月份匹配 | |
| 187 | + // 使用月份字符串匹配,避免时区问题 | |
| 188 | + var warehouseRent = await _db.Ado.SqlQuerySingleAsync<decimal>( | |
| 189 | + $@"SELECT COALESCE(SUM(d.F_DueAmount), 0) | |
| 190 | + FROM lq_contract_rent_detail d | |
| 191 | + INNER JOIN lq_contract c ON d.F_ContractId = c.F_Id | |
| 192 | + WHERE c.F_StoreId = '1649328471923847168' | |
| 193 | + AND c.F_Category = '仓库' | |
| 194 | + AND DATE_FORMAT(d.F_PaymentMonth, '%Y%m') = @StatisticsMonth | |
| 195 | + AND c.F_IsEffective = 1 | |
| 196 | + AND d.F_IsEffective = 1", | |
| 197 | + new { StatisticsMonth = statisticsMonth }); | |
| 198 | + entity.CostWarehouseRent = warehouseRent; | |
| 167 | 199 | |
| 168 | 200 | // 5. 成本-总部房租 |
| 169 | - // TODO: 需要确认如何识别总部合同 | |
| 170 | - entity.CostHQRent = 0; | |
| 201 | + // 从合同月租明细表获取:门店是总部,分类是总部,统计月份匹配 | |
| 202 | + // 使用月份字符串匹配,避免时区问题 | |
| 203 | + var hqRent = await _db.Ado.SqlQuerySingleAsync<decimal>( | |
| 204 | + $@"SELECT COALESCE(SUM(d.F_DueAmount), 0) | |
| 205 | + FROM lq_contract_rent_detail d | |
| 206 | + INNER JOIN lq_contract c ON d.F_ContractId = c.F_Id | |
| 207 | + WHERE c.F_StoreId = '1649328471923847168' | |
| 208 | + AND c.F_Category = '总部' | |
| 209 | + AND DATE_FORMAT(d.F_PaymentMonth, '%Y%m') = @StatisticsMonth | |
| 210 | + AND c.F_IsEffective = 1 | |
| 211 | + AND d.F_IsEffective = 1", | |
| 212 | + new { StatisticsMonth = statisticsMonth }); | |
| 213 | + entity.CostHQRent = hqRent; | |
| 171 | 214 | } |
| 172 | 215 | |
| 173 | 216 | /// <summary> | ... | ... |
netcore/src/Modularity/Extend/NCC.Extend/Utils/WeChatBotService.cs
| ... | ... | @@ -25,7 +25,7 @@ namespace NCC.Extend.Utils |
| 25 | 25 | _httpClient = httpClient; |
| 26 | 26 | |
| 27 | 27 | // 从配置文件中读取企业微信机器人配置 |
| 28 | - _botApiUrl = App.Configuration["WeChatBot:BotApiUrl"] ?? "http://wx.lvqianmeiye.com/api/Bot/send-text"; | |
| 28 | + _botApiUrl = App.Configuration["WeChatBot:BotApiUrl"] ?? "https://wx.lvqianmeiye.com/api/Bot/send-text"; | |
| 29 | 29 | |
| 30 | 30 | // 从配置文件中读取Webhook地址(正式或测试地址,通过配置文件切换) |
| 31 | 31 | _webhookUrl = App.Configuration["WeChatBot:WebhookUrl"]; | ... | ... |
sql/排查生美业绩统计差异-简化版.sql
sql/排查生美业绩统计差异详细.sql
sql/检查生美业绩统计差异.sql