Commit 2f7e426fe806bcad525e96b84b9589bb87ebd62a

Authored by “wangming”
1 parent cfb31365

优化天王团业绩完成情况统计:添加储扣金额字段,按品项类型分步查询

netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqDailyReport/TianwangGroupPerformanceCompletionOutput.cs
... ... @@ -52,6 +52,11 @@ namespace NCC.Extend.Entitys.Dto.LqDailyReport
52 52 /// 门店数量
53 53 /// </summary>
54 54 public int StoreCount { get; set; }
  55 +
  56 + /// <summary>
  57 + /// 储扣金额(储值扣减金额总和)
  58 + /// </summary>
  59 + public decimal DeductAmount { get; set; }
55 60 }
56 61 }
57 62  
... ...
netcore/src/Modularity/Extend/NCC.Extend/LqDailyReportService.cs
... ... @@ -456,7 +456,7 @@ namespace NCC.Extend
456 456 var actualCompletedPerformance = unit.BillingPerformance - unit.RefundPerformance; // 实际完成业绩
457 457 unit.CompletedPerformance = actualCompletedPerformance; // 实际完成业绩
458 458 var completionRate = unit.TargetPerformance > 0 ? (actualCompletedPerformance / unit.TargetPerformance * 100m) : 0m;
459   - unit.CompletionRate = decimal.Round(completionRate, 2);
  459 + unit.CompletionRate = decimal.Round(completionRate, 2);
460 460 }
461 461  
462 462 var outputList = businessUnitDict.Values.OrderByDescending(x => x.CompletedPerformance).ToList();
... ... @@ -470,17 +470,23 @@ namespace NCC.Extend
470 470 /// 获取天王团业绩完成情况
471 471 /// </summary>
472 472 /// <remarks>
473   - /// 根据指定日期统计各天王团的业绩完成情况,包括目标业绩、开单业绩、退卡金额、实际完成业绩、完成率
  473 + /// 根据指定日期统计各天王团的业绩完成情况,包括目标业绩、开单业绩、储扣金额、退卡金额、实际完成业绩、完成率
474 474 ///
475 475 /// 业绩统计规则:
476   - /// - 教育部(教育一部、教育二部):统计生美品项(lq_xmzl.qt2='生美')相关的开单和退卡业绩
477   - /// - 科技部(科技一部、科技二部):统计科美品项(lq_xmzl.qt2='科美')相关的开单和退卡业绩
478   - /// - 大项目部(大项目一部、大项目二部):统计医美品项(lq_xmzl.qt2='医美')相关的开单和退卡业绩
  476 + /// - 教育部(教育一部、教育二部):统计生美品项(lq_xmzl.qt2='生美')相关的开单、储扣和退卡业绩
  477 + /// - 科技部(科技一部、科技二部):统计科美品项(lq_xmzl.qt2='科美')相关的开单、储扣和退卡业绩
  478 + /// - 大项目部(大项目一部、大项目二部):统计医美品项(lq_xmzl.qt2='医美')相关的开单、储扣和退卡业绩
479 479 ///
480 480 /// 业绩分配规则:
481   - /// - 生美品项的开单和退卡业绩全部归教育部(不按目标比例分配)
482   - /// - 科美品项的开单和退卡业绩全部归科技部(不按目标比例分配)
483   - /// - 医美品项的开单和退卡业绩全部归大项目部(不按目标比例分配)
  481 + /// - 生美品项的开单、储扣和退卡业绩全部归教育部(不按目标比例分配)
  482 + /// - 科美品项的开单、储扣和退卡业绩全部归科技部(不按目标比例分配)
  483 + /// - 医美品项的开单、储扣和退卡业绩全部归大项目部(不按目标比例分配)
  484 + ///
  485 + /// 业绩计算规则:
  486 + /// - 开单业绩:原始开单业绩(不减去储扣金额)
  487 + /// - 储扣金额:储值扣减金额总和
  488 + /// - 退卡业绩:退卡金额总和
  489 + /// - 实际完成业绩 = 开单业绩 - 储扣金额 - 退卡金额
484 490 ///
485 491 /// 示例请求:
486 492 /// ```json
... ... @@ -500,10 +506,11 @@ namespace NCC.Extend
500 506 /// - DepartmentId: 部门ID
501 507 /// - DepartmentName: 部门名称(教育一部、教育二部、科技一部、科技二部、大项目一部、大项目二部)
502 508 /// - TargetPerformance: 目标业绩(来自门店目标表lq_md_target,根据部门类型使用对应字段:教育部使用F_EducationDepartmentTarget,科技部使用F_TechDepartmentTarget,大项目部使用F_MajorProjectDepartmentTarget,根据开始时间所在月份获取,通过对应归属字段关联,如果未查询到则为0)
503   - /// - BillingPerformance: 开单业绩(指定时间范围内,按品项分类过滤后的开单业绩总和,按门店目标比例分配)
504   - /// - RefundPerformance: 退款业绩(指定时间范围内,按品项分类过滤后的退卡业绩总和,按门店目标比例分配)
  509 + /// - BillingPerformance: 开单业绩(原始开单业绩,不减去储扣金额)
  510 + /// - DeductAmount: 储扣金额(储值扣减金额总和)
  511 + /// - RefundPerformance: 退卡业绩(退卡金额总和)
505 512 /// - ActualPerformance: 开单业绩(与BillingPerformance相同,用于兼容)
506   - /// - CompletedPerformance: 实际完成业绩(开单业绩 - 退款业绩
  513 + /// - CompletedPerformance: 实际完成业绩(开单业绩 - 储扣金额 - 退卡金额
507 514 /// - CompletionRate: 完成率(百分比,CompletedPerformance / TargetPerformance * 100)
508 515 /// - StoreCount: 门店数量(根据门店目标表中归属该部门的门店数统计)
509 516 /// </remarks>
... ... @@ -582,7 +589,8 @@ namespace NCC.Extend
582 589 ActualPerformance = 0,
583 590 CompletedPerformance = 0,
584 591 CompletionRate = 0,
585   - StoreCount = Convert.ToInt32(dept.StoreCount)
  592 + StoreCount = Convert.ToInt32(dept.StoreCount),
  593 + DeductAmount = 0
586 594 };
587 595 // 记录部门对应的字段类型
588 596 departmentFieldMap[deptId] = deptType.Value.fieldName;
... ... @@ -620,6 +628,7 @@ namespace NCC.Extend
620 628 }
621 629 }
622 630  
  631 + // 如果目标表中没有对应月份的数据,直接返回空业绩列表
623 632 if (!storeIds.Any())
624 633 {
625 634 return departmentDict.Values.OrderByDescending(x => x.CompletedPerformance).ToList();
... ... @@ -630,7 +639,7 @@ namespace NCC.Extend
630 639 // 2.2 查询目标表数据(一次性查询,数据量小)
631 640 // 直接查询,不使用 MAX(),因为每个门店在目标表中应该只有一条记录
632 641 var targetSql = $@"
633   - SELECT
  642 + SELECT
634 643 F_StoreId,
635 644 F_EducationDepartment,
636 645 F_TechDepartment,
... ... @@ -650,220 +659,163 @@ namespace NCC.Extend
650 659 }
651 660 }
652 661  
653   - // 2.3 从品项明细表统计业绩,按门店+品项分类分组
  662 + // 如果targetDict为空,说明目标表中没有数据,直接返回
  663 + if (!targetDict.Any())
  664 + {
  665 + return departmentDict.Values.OrderByDescending(x => x.CompletedPerformance).ToList();
  666 + }
  667 +
  668 + // 2.3 分步骤查询:按生美、科美、医美分别查询开单业绩、退卡业绩、储扣金额
654 669 // 重要:业绩从品项明细表(lq_kd_pxmx)的 F_ActualPrice 获取,不是从开单记录表的 sfyj 获取
655 670 // 因为一个开单可能包含多个品项,每个品项属于不同的分类(生美、科美、医美)
656 671 // 例如:开单A实付500元,包含科美100元、医美250元、生美150元
657 672 // 那么科技部统计100元,大项目部统计250元,教育部统计150元
658   - var billingSql = $@"
659   - SELECT
660   - temp.djmd as StoreId,
661   - item.qt2 as ItemType,
662   - COALESCE(SUM(temp.F_ActualPrice), 0) as StoreAmount
663   - FROM (
664   - SELECT pxmx.px, pxmx.F_ActualPrice, billing.djmd
665   - FROM lq_kd_pxmx pxmx
666   - INNER JOIN lq_kd_kdjlb billing ON pxmx.glkdbh = billing.F_Id
667   - WHERE pxmx.F_IsEffective = 1
668   - AND billing.F_IsEffective = 1
669   - AND billing.djmd IN ('{storeIdsStr}')
670   - AND billing.kdrq >= '{startDate:yyyy-MM-dd} 00:00:00'
671   - AND billing.kdrq < '{endDate.AddDays(1):yyyy-MM-dd} 00:00:00'
672   - ) temp
673   - INNER JOIN lq_xmzl item ON temp.px = item.F_Id
674   - WHERE item.F_IsEffective = 1
675   - AND item.qt2 IN ('生美', '科美', '医美')
676   - GROUP BY temp.djmd, item.qt2";
677   -
678   - var billingData = await _db.Ado.SqlQueryAsync<dynamic>(billingSql);
679   -
680   - // 2.4 从退卡明细表统计退卡业绩,按门店+品项分类分组
681   - // 重要:退卡业绩从退卡明细表(lq_hytk_mx)的 tkje 获取,按品项分类统计
682   - var refundSql = $@"
683   - SELECT
684   - temp.md as StoreId,
685   - item.qt2 as ItemType,
686   - COALESCE(SUM(temp.tkje), 0) as StoreRefundAmount
687   - FROM (
688   - SELECT refund_mx.px, refund_mx.tkje, refund.md
689   - FROM lq_hytk_mx refund_mx
690   - INNER JOIN lq_hytk_hytk refund ON refund_mx.F_RefundInfoId = refund.F_Id
691   - WHERE refund_mx.F_IsEffective = 1
692   - AND refund.F_IsEffective = 1
693   - AND refund.md IN ('{storeIdsStr}')
694   - AND refund.tksj >= '{startDate:yyyy-MM-dd} 00:00:00'
695   - AND refund.tksj < '{endDate.AddDays(1):yyyy-MM-dd} 00:00:00'
696   - ) temp
697   - INNER JOIN lq_xmzl item ON temp.px = item.F_Id
698   - WHERE item.F_IsEffective = 1
699   - AND item.qt2 IN ('生美', '科美', '医美')
700   - GROUP BY temp.md, item.qt2";
701   -
702   - var refundData = await _db.Ado.SqlQueryAsync<dynamic>(refundSql);
703   -
704   - // 2.5 从储扣明细表统计储扣金额,按门店+品项分类分组
705   - // 重要:储扣金额从储扣明细表(lq_kd_deductinfo)的 F_Amount 获取,按品项分类统计
706   - var deductSql = $@"
707   - SELECT
708   - billing.djmd as StoreId,
709   - item.qt2 as ItemType,
710   - COALESCE(SUM(deduct.F_Amount), 0) as StoreDeductAmount
711   - FROM lq_kd_deductinfo deduct
712   - INNER JOIN lq_kd_kdjlb billing ON deduct.F_BillingId = billing.F_Id
713   - INNER JOIN lq_xmzl item ON deduct.F_ItemId = item.F_Id
714   - WHERE deduct.F_IsEffective = 1
715   - AND billing.F_IsEffective = 1
716   - AND item.F_IsEffective = 1
717   - AND billing.djmd IN ('{storeIdsStr}')
718   - AND billing.kdrq >= '{startDate:yyyy-MM-dd} 00:00:00'
719   - AND billing.kdrq < '{endDate.AddDays(1):yyyy-MM-dd} 00:00:00'
720   - AND item.qt2 IN ('生美', '科美', '医美')
721   - GROUP BY billing.djmd, item.qt2";
722   -
723   - var deductData = await _db.Ado.SqlQueryAsync<dynamic>(deductSql);
724   -
725   - // 2.6 根据品项分类和目标表分配业绩到对应部门
726   - // 分配规则:
727   - // - 生美品项 → 分配给教育部门(F_EducationDepartment)
728   - // - 科美品项 → 分配给科技部门(F_TechDepartment)
729   - // - 医美品项 → 分配给大项目部门(F_MajorProjectDepartment)
730   - // 注意:一个门店可能同时属于多个部门,但每个品项的业绩只分配给对应的一个部门
731   - foreach (var billing in billingData ?? Enumerable.Empty<dynamic>())
732   - {
733   - var storeId = billing?.StoreId?.ToString();
734   - var itemType = billing?.ItemType?.ToString();
735   - var storeAmount = billing?.StoreAmount != null ? Convert.ToDecimal(billing.StoreAmount) : 0m;
736 673  
737   - if (string.IsNullOrEmpty(storeId) || string.IsNullOrEmpty(itemType) || storeAmount <= 0 || !targetDict.ContainsKey(storeId))
738   - continue;
739   -
740   - var target = targetDict[storeId];
741   - if (target == null)
742   - continue;
  674 + // 品项类型列表
  675 + var itemTypes = new[] { "生美", "科美", "医美" };
743 676  
744   - string targetDeptId = null;
745   -
746   - // 根据品项分类,分配到对应的部门
  677 + // 循环处理每个品项类型
  678 + foreach (var itemType in itemTypes)
  679 + {
  680 + // 确定该品项类型对应的部门字段
  681 + string deptField = "";
747 682 if (itemType == "生美")
748 683 {
749   - // 生美品项 → 教育部门
750   - targetDeptId = target.F_EducationDepartment?.ToString();
  684 + deptField = "F_EducationDepartment";
751 685 }
752 686 else if (itemType == "科美")
753 687 {
754   - // 科美品项 → 科技部门
755   - targetDeptId = target.F_TechDepartment?.ToString();
  688 + deptField = "F_TechDepartment";
756 689 }
757 690 else if (itemType == "医美")
758 691 {
759   - // 医美品项 → 大项目部门
760   - targetDeptId = target.F_MajorProjectDepartment?.ToString();
  692 + deptField = "F_MajorProjectDepartment";
761 693 }
762 694  
763   - // 只分配给在查询列表中的部门,避免重复统计
764   - if (!string.IsNullOrEmpty(targetDeptId) && departmentDict.ContainsKey(targetDeptId))
765   - {
766   - departmentDict[targetDeptId].BillingPerformance += storeAmount;
767   - }
768   - }
769   -
770   - // 2.6.1 减去储扣金额
771   - foreach (var deduct in deductData ?? Enumerable.Empty<dynamic>())
772   - {
773   - var storeId = deduct?.StoreId?.ToString();
774   - var itemType = deduct?.ItemType?.ToString();
775   - var storeDeductAmount = deduct?.StoreDeductAmount != null ? Convert.ToDecimal(deduct.StoreDeductAmount) : 0m;
  695 + // 2.3.1 查询开单业绩(按品项类型)
  696 + var billingSql = $@"
  697 + SELECT
  698 + target.{deptField} as TargetDeptId,
  699 + COALESCE(SUM(pxmx.F_ActualPrice), 0) as StoreAmount
  700 + FROM lq_kd_pxmx pxmx
  701 + INNER JOIN lq_kd_kdjlb billing ON pxmx.glkdbh = billing.F_Id
  702 + INNER JOIN lq_xmzl item ON pxmx.px = item.F_Id
  703 + INNER JOIN lq_md_target target ON billing.djmd = target.F_StoreId AND target.F_Month = '{month}'
  704 + WHERE pxmx.F_IsEffective = 1
  705 + AND billing.F_IsEffective = 1
  706 + AND item.F_IsEffective = 1
  707 + AND item.qt2 = '{itemType}'
  708 + AND billing.djmd IN ('{storeIdsStr}')
  709 + AND billing.kdrq >= '{startDate.ToString("yyyy-MM-dd")} 00:00:00'
  710 + AND billing.kdrq < '{endDate.AddDays(1).ToString("yyyy-MM-dd")} 00:00:00'
  711 + GROUP BY target.{deptField}";
776 712  
777   - if (string.IsNullOrEmpty(storeId) || string.IsNullOrEmpty(itemType) || storeDeductAmount <= 0 || !targetDict.ContainsKey(storeId))
778   - continue;
  713 + var billingData = await _db.Ado.SqlQueryAsync<dynamic>(billingSql);
779 714  
780   - var target = targetDict[storeId];
781   - if (target == null)
782   - continue;
  715 + // 分配开单业绩到对应部门
  716 + foreach (var billing in billingData ?? Enumerable.Empty<dynamic>())
  717 + {
  718 + var storeAmount = billing?.StoreAmount != null ? Convert.ToDecimal(billing.StoreAmount) : 0m;
  719 + var targetDeptId = billing?.TargetDeptId?.ToString();
783 720  
784   - string targetDeptId = null;
  721 + if (storeAmount <= 0 || string.IsNullOrEmpty(targetDeptId))
  722 + continue;
785 723  
786   - // 根据品项分类,从对应的部门减去储扣金额
787   - if (itemType == "生美")
788   - {
789   - // 生美品项储扣 → 从教育部门减去
790   - targetDeptId = target.F_EducationDepartment?.ToString();
791   - }
792   - else if (itemType == "科美")
793   - {
794   - // 科美品项储扣 → 从科技部门减去
795   - targetDeptId = target.F_TechDepartment?.ToString();
796   - }
797   - else if (itemType == "医美")
798   - {
799   - // 医美品项储扣 → 从大项目部门减去
800   - targetDeptId = target.F_MajorProjectDepartment?.ToString();
  724 + // 只分配给在查询列表中的部门,避免重复统计
  725 + if (departmentDict.ContainsKey(targetDeptId))
  726 + {
  727 + departmentDict[targetDeptId].BillingPerformance += storeAmount;
  728 + }
801 729 }
802 730  
803   - // 只从在查询列表中的部门减去,避免重复统计
804   - if (!string.IsNullOrEmpty(targetDeptId) && departmentDict.ContainsKey(targetDeptId))
  731 + // 2.3.2 查询退卡业绩(按品项类型)
  732 + var refundSql = $@"
  733 + SELECT
  734 + target.{deptField} as TargetDeptId,
  735 + COALESCE(SUM(refund_mx.tkje), 0) as StoreRefundAmount
  736 + FROM lq_hytk_mx refund_mx
  737 + INNER JOIN lq_hytk_hytk refund ON refund_mx.F_RefundInfoId = refund.F_Id
  738 + INNER JOIN lq_xmzl item ON refund_mx.px = item.F_Id
  739 + INNER JOIN lq_md_target target ON refund.md = target.F_StoreId AND target.F_Month = '{month}'
  740 + WHERE refund_mx.F_IsEffective = 1
  741 + AND refund.F_IsEffective = 1
  742 + AND item.F_IsEffective = 1
  743 + AND item.qt2 = '{itemType}'
  744 + AND refund.md IN ('{storeIdsStr}')
  745 + AND refund.tksj >= '{startDate.ToString("yyyy-MM-dd")} 00:00:00'
  746 + AND refund.tksj < '{endDate.AddDays(1).ToString("yyyy-MM-dd")} 00:00:00'
  747 + GROUP BY target.{deptField}";
  748 +
  749 + var refundData = await _db.Ado.SqlQueryAsync<dynamic>(refundSql);
  750 +
  751 + // 分配退卡业绩到对应部门
  752 + foreach (var refund in refundData ?? Enumerable.Empty<dynamic>())
805 753 {
806   - departmentDict[targetDeptId].BillingPerformance -= storeDeductAmount;
807   - }
808   - }
  754 + var storeRefundAmount = refund?.StoreRefundAmount != null ? Convert.ToDecimal(refund.StoreRefundAmount) : 0m;
  755 + var targetDeptId = refund?.TargetDeptId?.ToString();
809 756  
810   - // 2.7 根据品项分类和目标表分配退卡业绩到对应部门
811   - // 分配规则与开单业绩相同:
812   - // - 生美品项退卡 → 分配给教育部门
813   - // - 科美品项退卡 → 分配给科技部门
814   - // - 医美品项退卡 → 分配给大项目部门
815   - foreach (var refund in refundData ?? Enumerable.Empty<dynamic>())
816   - {
817   - var storeId = refund?.StoreId?.ToString();
818   - var itemType = refund?.ItemType?.ToString();
819   - var storeRefundAmount = refund?.StoreRefundAmount != null ? Convert.ToDecimal(refund.StoreRefundAmount) : 0m;
  757 + if (storeRefundAmount <= 0 || string.IsNullOrEmpty(targetDeptId))
  758 + continue;
820 759  
821   - if (string.IsNullOrEmpty(storeId) || string.IsNullOrEmpty(itemType) || storeRefundAmount <= 0 || !targetDict.ContainsKey(storeId))
822   - continue;
  760 + // 只分配给在查询列表中的部门,避免重复统计
  761 + if (departmentDict.ContainsKey(targetDeptId))
  762 + {
  763 + departmentDict[targetDeptId].RefundPerformance += storeRefundAmount;
  764 + }
  765 + }
823 766  
824   - var target = targetDict[storeId];
825   - if (target == null)
826   - continue;
  767 + // 2.3.3 查询储扣金额(按品项类型)
  768 + var deductSql = $@"
  769 + SELECT
  770 + target.{deptField} as TargetDeptId,
  771 + COALESCE(SUM(deduct.F_Amount), 0) as StoreDeductAmount
  772 + FROM lq_kd_deductinfo deduct
  773 + INNER JOIN lq_kd_kdjlb billing ON deduct.F_BillingId = billing.F_Id
  774 + INNER JOIN lq_xmzl item ON deduct.F_ItemId = item.F_Id
  775 + INNER JOIN lq_md_target target ON billing.djmd = target.F_StoreId AND target.F_Month = '{month}'
  776 + WHERE deduct.F_IsEffective = 1
  777 + AND billing.F_IsEffective = 1
  778 + AND item.F_IsEffective = 1
  779 + AND item.qt2 = '{itemType}'
  780 + AND billing.djmd IN ('{storeIdsStr}')
  781 + AND billing.kdrq >= '{startDate.ToString("yyyy-MM-dd")} 00:00:00'
  782 + AND billing.kdrq < '{endDate.AddDays(1).ToString("yyyy-MM-dd")} 00:00:00'
  783 + GROUP BY target.{deptField}";
827 784  
828   - string targetDeptId = null;
  785 + var deductData = await _db.Ado.SqlQueryAsync<dynamic>(deductSql);
829 786  
830   - // 根据品项分类,分配到对应的部门
831   - if (itemType == "生美")
  787 + // 分配储扣金额到对应部门
  788 + foreach (var deduct in deductData ?? Enumerable.Empty<dynamic>())
832 789 {
833   - // 生美品项退卡 → 教育部门
834   - targetDeptId = target.F_EducationDepartment?.ToString();
835   - }
836   - else if (itemType == "科美")
837   - {
838   - // 科美品项退卡 → 科技部门
839   - targetDeptId = target.F_TechDepartment?.ToString();
840   - }
841   - else if (itemType == "医美")
842   - {
843   - // 医美品项退卡 → 大项目部门
844   - targetDeptId = target.F_MajorProjectDepartment?.ToString();
845   - }
  790 + var storeDeductAmount = deduct?.StoreDeductAmount != null ? Convert.ToDecimal(deduct.StoreDeductAmount) : 0m;
  791 + var targetDeptId = deduct?.TargetDeptId?.ToString();
846 792  
847   - // 只分配给在查询列表中的部门,避免重复统计
848   - if (!string.IsNullOrEmpty(targetDeptId) && departmentDict.ContainsKey(targetDeptId))
849   - {
850   - departmentDict[targetDeptId].RefundPerformance += storeRefundAmount;
  793 + if (storeDeductAmount <= 0 || string.IsNullOrEmpty(targetDeptId))
  794 + continue;
  795 +
  796 + // 只累加到在查询列表中的部门,避免重复统计
  797 + if (departmentDict.ContainsKey(targetDeptId))
  798 + {
  799 + departmentDict[targetDeptId].DeductAmount += storeDeductAmount;
  800 + }
851 801 }
852 802 }
853 803  
854 804 // 第三步:计算实际完成业绩和完成率,并保留两位小数
855 805 foreach (var dept in departmentDict.Values)
856 806 {
857   - dept.BillingPerformance = decimal.Round(dept.BillingPerformance, 2);
858   - dept.RefundPerformance = decimal.Round(dept.RefundPerformance, 2);
859   - dept.ActualPerformance = dept.BillingPerformance; // 开单业绩
860   - var actualCompletedPerformance = dept.BillingPerformance - dept.RefundPerformance; // 实际完成业绩
  807 + dept.BillingPerformance = decimal.Round(dept.BillingPerformance, 2); // 开单业绩(原始,不减去储扣)
  808 + dept.RefundPerformance = decimal.Round(dept.RefundPerformance, 2); // 退卡业绩
  809 + dept.DeductAmount = decimal.Round(dept.DeductAmount, 2); // 储扣金额,保留两位小数
  810 + dept.ActualPerformance = dept.BillingPerformance; // 开单业绩(与BillingPerformance相同,用于兼容)
  811 + // 实际完成业绩 = 开单业绩 - 储扣金额 - 退卡金额
  812 + var actualCompletedPerformance = dept.BillingPerformance - dept.DeductAmount - dept.RefundPerformance;
861 813 dept.CompletedPerformance = decimal.Round(actualCompletedPerformance, 2); // 实际完成业绩,保留两位小数
862 814  
863   - var completionRate = dept.TargetPerformance > 0
864   - ? (actualCompletedPerformance / dept.TargetPerformance * 100m)
865   - : 0m;
866   - dept.CompletionRate = decimal.Round(completionRate, 2);
  815 + var completionRate = dept.TargetPerformance > 0
  816 + ? (actualCompletedPerformance / dept.TargetPerformance * 100m)
  817 + : 0m;
  818 + dept.CompletionRate = decimal.Round(completionRate, 2);
867 819 }
868 820  
869 821 var outputList = departmentDict.Values
... ... @@ -1174,12 +1126,13 @@ namespace NCC.Extend
1174 1126 }
1175 1127  
1176 1128 // SQL查询:统计科技部老师的消耗业绩、见客数、项目数
  1129 + // 注意:GROUP BY 中移除了 user.F_RealName,避免同一老师ID因姓名不同产生重复记录
1177 1130 var consumeSql = $@"
1178 1131 SELECT
1179 1132 techDept.F_Id as TechDepartmentId,
1180 1133 techDept.F_FullName as TechDepartmentName,
1181 1134 consume.kjbls as TeacherId,
1182   - user.F_RealName as TeacherName,
  1135 + MAX(user.F_RealName) as TeacherName,
1183 1136 COUNT(DISTINCT hyhk.hy) as CustomerCount,
1184 1137 SUM(consume.F_hdpxNumber) as ConsumeProjectCount,
1185 1138 SUM(consume.kjblsyj) as ConsumeAchievement
... ... @@ -1194,7 +1147,7 @@ namespace NCC.Extend
1194 1147 AND DATE(hyhk.hksj) <= '{endDate:yyyy-MM-dd}'
1195 1148 {techFilter}
1196 1149 {teacherFilter}
1197   - GROUP BY techDept.F_Id, techDept.F_FullName, consume.kjbls, user.F_RealName";
  1150 + GROUP BY techDept.F_Id, techDept.F_FullName, consume.kjbls";
1198 1151  
1199 1152 var consumeResult = await _db.Ado.SqlQueryAsync<dynamic>(consumeSql);
1200 1153  
... ... @@ -1202,6 +1155,7 @@ namespace NCC.Extend
1202 1155 var orderSql = $@"
1203 1156 SELECT
1204 1157 techDept.F_Id as TechDepartmentId,
  1158 + techDept.F_FullName as TechDepartmentName,
1205 1159 ord.kjbls as TeacherId,
1206 1160 SUM(ord.kjblsyj) as OrderAchievement
1207 1161 FROM lq_kd_kjbsyj ord
... ... @@ -1214,36 +1168,211 @@ namespace NCC.Extend
1214 1168 AND DATE(kdjlb.kdrq) <= '{endDate:yyyy-MM-dd}'
1215 1169 {techFilter}
1216 1170 {teacherFilterForOrder}
1217   - GROUP BY techDept.F_Id, ord.kjbls";
  1171 + GROUP BY techDept.F_Id, techDept.F_FullName, ord.kjbls";
1218 1172  
1219 1173 var orderResult = await _db.Ado.SqlQueryAsync<dynamic>(orderSql);
1220 1174  
1221   - // 合并数据
  1175 + // 合并数据:按员工ID汇总,避免同一员工在多个科技部重复出现
  1176 + // 使用 TeacherId 作为唯一键,汇总所有科技部的数据
1222 1177 var teacherDict = new Dictionary<string, TechTeacherDailyStatisticsOutput>();
  1178 + // 记录每个员工在各个科技部的消耗业绩,用于确定主要科技部
  1179 + var teacherDeptConsumePerformance = new Dictionary<string, Dictionary<string, decimal>>();
  1180 + // 记录每个员工在各个科技部的开单业绩,用于确定主要科技部(当消耗业绩为0时)
  1181 + var teacherDeptOrderPerformance = new Dictionary<string, Dictionary<string, decimal>>();
1223 1182  
  1183 + // 第一步:处理消耗数据,按员工ID汇总
1224 1184 foreach (var item in consumeResult ?? Enumerable.Empty<dynamic>())
1225 1185 {
1226   - var key = $"{item.TechDepartmentId}_{item.TeacherId}";
1227   - teacherDict[key] = new TechTeacherDailyStatisticsOutput
  1186 + var teacherId = item.TeacherId?.ToString();
  1187 + var techDeptId = item.TechDepartmentId?.ToString();
  1188 + var techDeptName = item.TechDepartmentName?.ToString();
  1189 + var consumeAchievement = Convert.ToDecimal(item.ConsumeAchievement);
  1190 +
  1191 + if (string.IsNullOrEmpty(teacherId))
  1192 + continue;
  1193 +
  1194 + // 记录员工在各科技部的消耗业绩(累加,因为同一员工在同一科技部可能有多个门店的数据)
  1195 + if (!teacherDeptConsumePerformance.ContainsKey(teacherId))
1228 1196 {
1229   - TechDepartmentId = item.TechDepartmentId?.ToString(),
1230   - TechDepartmentName = item.TechDepartmentName?.ToString(),
1231   - TeacherId = item.TeacherId?.ToString(),
1232   - TeacherName = item.TeacherName?.ToString(),
1233   - CustomerCount = Convert.ToInt32(item.CustomerCount),
1234   - ConsumeProjectCount = Convert.ToInt32(item.ConsumeProjectCount),
1235   - ConsumeAchievement = Convert.ToDecimal(item.ConsumeAchievement),
1236   - OrderAchievement = 0
1237   - };
  1197 + teacherDeptConsumePerformance[teacherId] = new Dictionary<string, decimal>();
  1198 + }
  1199 + if (!string.IsNullOrEmpty(techDeptId))
  1200 + {
  1201 + if (!teacherDeptConsumePerformance[teacherId].ContainsKey(techDeptId))
  1202 + {
  1203 + teacherDeptConsumePerformance[teacherId][techDeptId] = 0;
  1204 + }
  1205 + teacherDeptConsumePerformance[teacherId][techDeptId] += consumeAchievement;
  1206 + }
  1207 +
  1208 + // 按员工ID汇总数据
  1209 + if (!teacherDict.ContainsKey(teacherId))
  1210 + {
  1211 + teacherDict[teacherId] = new TechTeacherDailyStatisticsOutput
  1212 + {
  1213 + TechDepartmentId = techDeptId,
  1214 + TechDepartmentName = techDeptName,
  1215 + TeacherId = teacherId,
  1216 + TeacherName = item.TeacherName?.ToString(),
  1217 + CustomerCount = 0,
  1218 + ConsumeProjectCount = 0,
  1219 + ConsumeAchievement = 0,
  1220 + OrderAchievement = 0
  1221 + };
  1222 + }
  1223 +
  1224 + // 汇总数据(累加项目数和业绩,见客数后面统一重新计算去重)
  1225 + var teacher = teacherDict[teacherId];
  1226 + teacher.ConsumeProjectCount += Convert.ToInt32(item.ConsumeProjectCount);
  1227 + teacher.ConsumeAchievement += consumeAchievement;
1238 1228 }
1239 1229  
1240   - // 合并开单业绩
  1230 + // 第二步:处理开单业绩,按员工ID汇总
1241 1231 foreach (var item in orderResult ?? Enumerable.Empty<dynamic>())
1242 1232 {
1243   - var key = $"{item.TechDepartmentId}_{item.TeacherId}";
1244   - if (teacherDict.ContainsKey(key))
  1233 + var teacherId = item.TeacherId?.ToString();
  1234 + var techDeptId = item.TechDepartmentId?.ToString();
  1235 + var techDeptName = item.TechDepartmentName?.ToString();
  1236 + var orderAchievement = Convert.ToDecimal(item.OrderAchievement);
  1237 +
  1238 + if (string.IsNullOrEmpty(teacherId))
  1239 + continue;
  1240 +
  1241 + // 记录员工在各科技部的开单业绩(累加,因为同一员工在同一科技部可能有多个门店的数据)
  1242 + if (!teacherDeptOrderPerformance.ContainsKey(teacherId))
  1243 + {
  1244 + teacherDeptOrderPerformance[teacherId] = new Dictionary<string, decimal>();
  1245 + }
  1246 + if (!string.IsNullOrEmpty(techDeptId))
1245 1247 {
1246   - teacherDict[key].OrderAchievement = Convert.ToDecimal(item.OrderAchievement);
  1248 + if (!teacherDeptOrderPerformance[teacherId].ContainsKey(techDeptId))
  1249 + {
  1250 + teacherDeptOrderPerformance[teacherId][techDeptId] = 0;
  1251 + }
  1252 + teacherDeptOrderPerformance[teacherId][techDeptId] += orderAchievement;
  1253 + }
  1254 +
  1255 + if (!teacherDict.ContainsKey(teacherId))
  1256 + {
  1257 + // 如果消耗数据中没有,但开单数据中有,需要创建记录
  1258 + teacherDict[teacherId] = new TechTeacherDailyStatisticsOutput
  1259 + {
  1260 + TechDepartmentId = techDeptId,
  1261 + TechDepartmentName = techDeptName,
  1262 + TeacherId = teacherId,
  1263 + TeacherName = null,
  1264 + CustomerCount = 0,
  1265 + ConsumeProjectCount = 0,
  1266 + ConsumeAchievement = 0,
  1267 + OrderAchievement = 0
  1268 + };
  1269 + }
  1270 +
  1271 + teacherDict[teacherId].OrderAchievement += orderAchievement;
  1272 + }
  1273 +
  1274 + // 第三步:确定每个员工的主要科技部
  1275 + // 优先按消耗业绩最多的科技部,如果消耗业绩为0,则按开单业绩最多的科技部
  1276 + foreach (var teacherId in teacherDict.Keys.ToList())
  1277 + {
  1278 + var teacher = teacherDict[teacherId];
  1279 + string mainDeptId = null;
  1280 + string mainDeptName = null;
  1281 +
  1282 + // 优先按消耗业绩确定主要科技部
  1283 + if (teacherDeptConsumePerformance.ContainsKey(teacherId) && teacherDeptConsumePerformance[teacherId].Any())
  1284 + {
  1285 + var mainDept = teacherDeptConsumePerformance[teacherId]
  1286 + .OrderByDescending(x => x.Value)
  1287 + .FirstOrDefault();
  1288 +
  1289 + if (!string.IsNullOrEmpty(mainDept.Key) && mainDept.Value > 0)
  1290 + {
  1291 + mainDeptId = mainDept.Key;
  1292 + // 从消耗数据中获取该科技部的名称
  1293 + var mainDeptData = consumeResult?.FirstOrDefault(x =>
  1294 + x.TeacherId?.ToString() == teacherId &&
  1295 + x.TechDepartmentId?.ToString() == mainDeptId);
  1296 +
  1297 + if (mainDeptData != null)
  1298 + {
  1299 + mainDeptName = mainDeptData.TechDepartmentName?.ToString();
  1300 + }
  1301 + }
  1302 + }
  1303 +
  1304 + // 如果消耗业绩为0或没有,则按开单业绩确定主要科技部
  1305 + if (string.IsNullOrEmpty(mainDeptId) && teacherDeptOrderPerformance.ContainsKey(teacherId) && teacherDeptOrderPerformance[teacherId].Any())
  1306 + {
  1307 + var mainDept = teacherDeptOrderPerformance[teacherId]
  1308 + .OrderByDescending(x => x.Value)
  1309 + .FirstOrDefault();
  1310 +
  1311 + if (!string.IsNullOrEmpty(mainDept.Key) && mainDept.Value > 0)
  1312 + {
  1313 + mainDeptId = mainDept.Key;
  1314 + // 从开单数据中获取该科技部的名称
  1315 + var mainDeptData = orderResult?.FirstOrDefault(x =>
  1316 + x.TeacherId?.ToString() == teacherId &&
  1317 + x.TechDepartmentId?.ToString() == mainDeptId);
  1318 +
  1319 + if (mainDeptData != null)
  1320 + {
  1321 + mainDeptName = mainDeptData.TechDepartmentName?.ToString();
  1322 + }
  1323 + }
  1324 + }
  1325 +
  1326 + // 如果仍然没有确定主要科技部,但已有科技部ID和名称,保持原样
  1327 + if (!string.IsNullOrEmpty(mainDeptId))
  1328 + {
  1329 + teacher.TechDepartmentId = mainDeptId;
  1330 + if (!string.IsNullOrEmpty(mainDeptName))
  1331 + {
  1332 + teacher.TechDepartmentName = mainDeptName;
  1333 + }
  1334 + }
  1335 + else if (string.IsNullOrEmpty(teacher.TechDepartmentName) && !string.IsNullOrEmpty(teacher.TechDepartmentId))
  1336 + {
  1337 + // 如果仍然没有科技部名称,尝试从开单数据中获取
  1338 + var orderDeptData = orderResult?.FirstOrDefault(x =>
  1339 + x.TeacherId?.ToString() == teacherId &&
  1340 + x.TechDepartmentId?.ToString() == teacher.TechDepartmentId);
  1341 +
  1342 + if (orderDeptData != null && !string.IsNullOrEmpty(orderDeptData.TechDepartmentName?.ToString()))
  1343 + {
  1344 + teacher.TechDepartmentName = orderDeptData.TechDepartmentName?.ToString();
  1345 + }
  1346 + }
  1347 + }
  1348 +
  1349 + // 第四步:重新计算见客数(去重,因为同一个客户可能在多个科技部被统计)
  1350 + // 由于已经汇总了数据,这里需要重新查询去重后的见客数
  1351 + var teacherIds = teacherDict.Keys.ToList();
  1352 + if (teacherIds.Any())
  1353 + {
  1354 + var teacherIdsStr = string.Join("','", teacherIds);
  1355 + var customerCountSql = $@"
  1356 + SELECT
  1357 + consume.kjbls as TeacherId,
  1358 + COUNT(DISTINCT hyhk.hy) as CustomerCount
  1359 + FROM lq_xh_kjbsyj consume
  1360 + INNER JOIN lq_xh_hyhk hyhk ON consume.glkdbh = hyhk.F_Id
  1361 + WHERE consume.F_IsEffective = 1
  1362 + AND hyhk.F_IsEffective = 1
  1363 + AND DATE(hyhk.hksj) >= '{startDate:yyyy-MM-dd}'
  1364 + AND DATE(hyhk.hksj) <= '{endDate:yyyy-MM-dd}'
  1365 + AND consume.kjbls IN ('{teacherIdsStr}')
  1366 + GROUP BY consume.kjbls";
  1367 +
  1368 + var customerCountResult = await _db.Ado.SqlQueryAsync<dynamic>(customerCountSql);
  1369 + foreach (var item in customerCountResult ?? Enumerable.Empty<dynamic>())
  1370 + {
  1371 + var teacherId = item.TeacherId?.ToString();
  1372 + if (!string.IsNullOrEmpty(teacherId) && teacherDict.ContainsKey(teacherId))
  1373 + {
  1374 + teacherDict[teacherId].CustomerCount = Convert.ToInt32(item.CustomerCount);
  1375 + }
1247 1376 }
1248 1377 }
1249 1378  
... ...