Commit 8b44d4bd9ad36ec5c24615ba9a16d10c1cbc4f1c

Authored by “wangming”
1 parent 6af31905

feat: 添加健康师额外工资和合作成本导入的清理功能

- 健康师额外工资导入:添加清理导入月份数据参数,默认true
- 合作成本导入:添加清理导入月份数据参数,默认true,支持多成本类型和多笔记录
- 合作成本导入返回结果:添加成本类型字段到successRecords
- 移除合作成本导入的重复检查,支持同一门店同一月份多笔记录
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
... ... @@ -124,3 +124,5 @@ ORDER BY 生美业绩 DESC;
124 124  
125 125  
126 126  
  127 +
  128 +
... ...
sql/排查生美业绩统计差异详细.sql
... ... @@ -176,3 +176,5 @@ HAVING COUNT(*) &gt; 1;
176 176  
177 177  
178 178  
  179 +
  180 +
... ...
sql/检查生美业绩统计差异.sql
... ... @@ -146,3 +146,5 @@ ORDER BY 生美业绩 DESC;
146 146  
147 147  
148 148  
  149 +
  150 +
... ...
test_tianwang_api.py
... ... @@ -77,3 +77,5 @@ print(f&quot;\n教育一部+教育二部合计 BillingPerformance: {total2}&quot;)
77 77  
78 78  
79 79  
  80 +
  81 +
... ...