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,6 +52,11 @@ namespace NCC.Extend.Entitys.Dto.LqDailyReport
52 /// 门店数量 52 /// 门店数量
53 /// </summary> 53 /// </summary>
54 public int StoreCount { get; set; } 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,7 +456,7 @@ namespace NCC.Extend
456 var actualCompletedPerformance = unit.BillingPerformance - unit.RefundPerformance; // 实际完成业绩 456 var actualCompletedPerformance = unit.BillingPerformance - unit.RefundPerformance; // 实际完成业绩
457 unit.CompletedPerformance = actualCompletedPerformance; // 实际完成业绩 457 unit.CompletedPerformance = actualCompletedPerformance; // 实际完成业绩
458 var completionRate = unit.TargetPerformance > 0 ? (actualCompletedPerformance / unit.TargetPerformance * 100m) : 0m; 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 var outputList = businessUnitDict.Values.OrderByDescending(x => x.CompletedPerformance).ToList(); 462 var outputList = businessUnitDict.Values.OrderByDescending(x => x.CompletedPerformance).ToList();
@@ -470,17 +470,23 @@ namespace NCC.Extend @@ -470,17 +470,23 @@ namespace NCC.Extend
470 /// 获取天王团业绩完成情况 470 /// 获取天王团业绩完成情况
471 /// </summary> 471 /// </summary>
472 /// <remarks> 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 /// ```json 492 /// ```json
@@ -500,10 +506,11 @@ namespace NCC.Extend @@ -500,10 +506,11 @@ namespace NCC.Extend
500 /// - DepartmentId: 部门ID 506 /// - DepartmentId: 部门ID
501 /// - DepartmentName: 部门名称(教育一部、教育二部、科技一部、科技二部、大项目一部、大项目二部) 507 /// - DepartmentName: 部门名称(教育一部、教育二部、科技一部、科技二部、大项目一部、大项目二部)
502 /// - TargetPerformance: 目标业绩(来自门店目标表lq_md_target,根据部门类型使用对应字段:教育部使用F_EducationDepartmentTarget,科技部使用F_TechDepartmentTarget,大项目部使用F_MajorProjectDepartmentTarget,根据开始时间所在月份获取,通过对应归属字段关联,如果未查询到则为0) 508 /// - TargetPerformance: 目标业绩(来自门店目标表lq_md_target,根据部门类型使用对应字段:教育部使用F_EducationDepartmentTarget,科技部使用F_TechDepartmentTarget,大项目部使用F_MajorProjectDepartmentTarget,根据开始时间所在月份获取,通过对应归属字段关联,如果未查询到则为0)
503 - /// - BillingPerformance: 开单业绩(指定时间范围内,按品项分类过滤后的开单业绩总和,按门店目标比例分配)  
504 - /// - RefundPerformance: 退款业绩(指定时间范围内,按品项分类过滤后的退卡业绩总和,按门店目标比例分配) 509 + /// - BillingPerformance: 开单业绩(原始开单业绩,不减去储扣金额)
  510 + /// - DeductAmount: 储扣金额(储值扣减金额总和)
  511 + /// - RefundPerformance: 退卡业绩(退卡金额总和)
505 /// - ActualPerformance: 开单业绩(与BillingPerformance相同,用于兼容) 512 /// - ActualPerformance: 开单业绩(与BillingPerformance相同,用于兼容)
506 - /// - CompletedPerformance: 实际完成业绩(开单业绩 - 退款业绩 513 + /// - CompletedPerformance: 实际完成业绩(开单业绩 - 储扣金额 - 退卡金额
507 /// - CompletionRate: 完成率(百分比,CompletedPerformance / TargetPerformance * 100) 514 /// - CompletionRate: 完成率(百分比,CompletedPerformance / TargetPerformance * 100)
508 /// - StoreCount: 门店数量(根据门店目标表中归属该部门的门店数统计) 515 /// - StoreCount: 门店数量(根据门店目标表中归属该部门的门店数统计)
509 /// </remarks> 516 /// </remarks>
@@ -582,7 +589,8 @@ namespace NCC.Extend @@ -582,7 +589,8 @@ namespace NCC.Extend
582 ActualPerformance = 0, 589 ActualPerformance = 0,
583 CompletedPerformance = 0, 590 CompletedPerformance = 0,
584 CompletionRate = 0, 591 CompletionRate = 0,
585 - StoreCount = Convert.ToInt32(dept.StoreCount) 592 + StoreCount = Convert.ToInt32(dept.StoreCount),
  593 + DeductAmount = 0
586 }; 594 };
587 // 记录部门对应的字段类型 595 // 记录部门对应的字段类型
588 departmentFieldMap[deptId] = deptType.Value.fieldName; 596 departmentFieldMap[deptId] = deptType.Value.fieldName;
@@ -620,6 +628,7 @@ namespace NCC.Extend @@ -620,6 +628,7 @@ namespace NCC.Extend
620 } 628 }
621 } 629 }
622 630
  631 + // 如果目标表中没有对应月份的数据,直接返回空业绩列表
623 if (!storeIds.Any()) 632 if (!storeIds.Any())
624 { 633 {
625 return departmentDict.Values.OrderByDescending(x => x.CompletedPerformance).ToList(); 634 return departmentDict.Values.OrderByDescending(x => x.CompletedPerformance).ToList();
@@ -630,7 +639,7 @@ namespace NCC.Extend @@ -630,7 +639,7 @@ namespace NCC.Extend
630 // 2.2 查询目标表数据(一次性查询,数据量小) 639 // 2.2 查询目标表数据(一次性查询,数据量小)
631 // 直接查询,不使用 MAX(),因为每个门店在目标表中应该只有一条记录 640 // 直接查询,不使用 MAX(),因为每个门店在目标表中应该只有一条记录
632 var targetSql = $@" 641 var targetSql = $@"
633 - SELECT 642 + SELECT
634 F_StoreId, 643 F_StoreId,
635 F_EducationDepartment, 644 F_EducationDepartment,
636 F_TechDepartment, 645 F_TechDepartment,
@@ -650,220 +659,163 @@ namespace NCC.Extend @@ -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 // 重要:业绩从品项明细表(lq_kd_pxmx)的 F_ActualPrice 获取,不是从开单记录表的 sfyj 获取 669 // 重要:业绩从品项明细表(lq_kd_pxmx)的 F_ActualPrice 获取,不是从开单记录表的 sfyj 获取
655 // 因为一个开单可能包含多个品项,每个品项属于不同的分类(生美、科美、医美) 670 // 因为一个开单可能包含多个品项,每个品项属于不同的分类(生美、科美、医美)
656 // 例如:开单A实付500元,包含科美100元、医美250元、生美150元 671 // 例如:开单A实付500元,包含科美100元、医美250元、生美150元
657 // 那么科技部统计100元,大项目部统计250元,教育部统计150元 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 if (itemType == "生美") 682 if (itemType == "生美")
748 { 683 {
749 - // 生美品项 → 教育部门  
750 - targetDeptId = target.F_EducationDepartment?.ToString(); 684 + deptField = "F_EducationDepartment";
751 } 685 }
752 else if (itemType == "科美") 686 else if (itemType == "科美")
753 { 687 {
754 - // 科美品项 → 科技部门  
755 - targetDeptId = target.F_TechDepartment?.ToString(); 688 + deptField = "F_TechDepartment";
756 } 689 }
757 else if (itemType == "医美") 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 foreach (var dept in departmentDict.Values) 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 dept.CompletedPerformance = decimal.Round(actualCompletedPerformance, 2); // 实际完成业绩,保留两位小数 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 var outputList = departmentDict.Values 821 var outputList = departmentDict.Values
@@ -1174,12 +1126,13 @@ namespace NCC.Extend @@ -1174,12 +1126,13 @@ namespace NCC.Extend
1174 } 1126 }
1175 1127
1176 // SQL查询:统计科技部老师的消耗业绩、见客数、项目数 1128 // SQL查询:统计科技部老师的消耗业绩、见客数、项目数
  1129 + // 注意:GROUP BY 中移除了 user.F_RealName,避免同一老师ID因姓名不同产生重复记录
1177 var consumeSql = $@" 1130 var consumeSql = $@"
1178 SELECT 1131 SELECT
1179 techDept.F_Id as TechDepartmentId, 1132 techDept.F_Id as TechDepartmentId,
1180 techDept.F_FullName as TechDepartmentName, 1133 techDept.F_FullName as TechDepartmentName,
1181 consume.kjbls as TeacherId, 1134 consume.kjbls as TeacherId,
1182 - user.F_RealName as TeacherName, 1135 + MAX(user.F_RealName) as TeacherName,
1183 COUNT(DISTINCT hyhk.hy) as CustomerCount, 1136 COUNT(DISTINCT hyhk.hy) as CustomerCount,
1184 SUM(consume.F_hdpxNumber) as ConsumeProjectCount, 1137 SUM(consume.F_hdpxNumber) as ConsumeProjectCount,
1185 SUM(consume.kjblsyj) as ConsumeAchievement 1138 SUM(consume.kjblsyj) as ConsumeAchievement
@@ -1194,7 +1147,7 @@ namespace NCC.Extend @@ -1194,7 +1147,7 @@ namespace NCC.Extend
1194 AND DATE(hyhk.hksj) <= '{endDate:yyyy-MM-dd}' 1147 AND DATE(hyhk.hksj) <= '{endDate:yyyy-MM-dd}'
1195 {techFilter} 1148 {techFilter}
1196 {teacherFilter} 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 var consumeResult = await _db.Ado.SqlQueryAsync<dynamic>(consumeSql); 1152 var consumeResult = await _db.Ado.SqlQueryAsync<dynamic>(consumeSql);
1200 1153
@@ -1202,6 +1155,7 @@ namespace NCC.Extend @@ -1202,6 +1155,7 @@ namespace NCC.Extend
1202 var orderSql = $@" 1155 var orderSql = $@"
1203 SELECT 1156 SELECT
1204 techDept.F_Id as TechDepartmentId, 1157 techDept.F_Id as TechDepartmentId,
  1158 + techDept.F_FullName as TechDepartmentName,
1205 ord.kjbls as TeacherId, 1159 ord.kjbls as TeacherId,
1206 SUM(ord.kjblsyj) as OrderAchievement 1160 SUM(ord.kjblsyj) as OrderAchievement
1207 FROM lq_kd_kjbsyj ord 1161 FROM lq_kd_kjbsyj ord
@@ -1214,36 +1168,211 @@ namespace NCC.Extend @@ -1214,36 +1168,211 @@ namespace NCC.Extend
1214 AND DATE(kdjlb.kdrq) <= '{endDate:yyyy-MM-dd}' 1168 AND DATE(kdjlb.kdrq) <= '{endDate:yyyy-MM-dd}'
1215 {techFilter} 1169 {techFilter}
1216 {teacherFilterForOrder} 1170 {teacherFilterForOrder}
1217 - GROUP BY techDept.F_Id, ord.kjbls"; 1171 + GROUP BY techDept.F_Id, techDept.F_FullName, ord.kjbls";
1218 1172
1219 var orderResult = await _db.Ado.SqlQueryAsync<dynamic>(orderSql); 1173 var orderResult = await _db.Ado.SqlQueryAsync<dynamic>(orderSql);
1220 1174
1221 - // 合并数据 1175 + // 合并数据:按员工ID汇总,避免同一员工在多个科技部重复出现
  1176 + // 使用 TeacherId 作为唯一键,汇总所有科技部的数据
1222 var teacherDict = new Dictionary<string, TechTeacherDailyStatisticsOutput>(); 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 foreach (var item in consumeResult ?? Enumerable.Empty<dynamic>()) 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 foreach (var item in orderResult ?? Enumerable.Empty<dynamic>()) 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