Commit 2f7e426fe806bcad525e96b84b9589bb87ebd62a
1 parent
cfb31365
优化天王团业绩完成情况统计:添加储扣金额字段,按品项类型分步查询
Showing
2 changed files
with
342 additions
and
208 deletions
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqDailyReport/TianwangGroupPerformanceCompletionOutput.cs
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 | ... | ... |