Commit 5c4ab8d3509da6b0be688e6606d251044d661afa

Authored by “wangming”
1 parent b1bff74f

111

netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqDailyReport/DeductAmountStatisticsInput.cs 0 → 100644
  1 +using System;
  2 +
  3 +namespace NCC.Extend.Entitys.Dto.LqDailyReport
  4 +{
  5 + /// <summary>
  6 + /// 储值扣减金额统计输入参数
  7 + /// </summary>
  8 + public class DeductAmountStatisticsInput
  9 + {
  10 + /// <summary>
  11 + /// 开始时间(可选,不传则默认为当月1号)
  12 + /// </summary>
  13 + public DateTime? StartTime { get; set; }
  14 +
  15 + /// <summary>
  16 + /// 结束时间(可选,不传则默认为今天)
  17 + /// </summary>
  18 + public DateTime? EndTime { get; set; }
  19 + }
  20 +}
  21 +
... ...
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqDailyReport/DeductAmountStatisticsOutput.cs 0 → 100644
  1 +using System;
  2 +
  3 +namespace NCC.Extend.Entitys.Dto.LqDailyReport
  4 +{
  5 + /// <summary>
  6 + /// 储值扣减金额统计输出结果
  7 + /// </summary>
  8 + public class DeductAmountStatisticsOutput
  9 + {
  10 + /// <summary>
  11 + /// 医美储扣金额
  12 + /// </summary>
  13 + public decimal YiMeiAmount { get; set; }
  14 +
  15 + /// <summary>
  16 + /// 生美储扣金额
  17 + /// </summary>
  18 + public decimal ShengMeiAmount { get; set; }
  19 +
  20 + /// <summary>
  21 + /// 科美储扣金额
  22 + /// </summary>
  23 + public decimal KeMeiAmount { get; set; }
  24 +
  25 + /// <summary>
  26 + /// 其他品项储扣金额
  27 + /// </summary>
  28 + public decimal OtherAmount { get; set; }
  29 +
  30 + /// <summary>
  31 + /// 总储扣金额
  32 + /// </summary>
  33 + public decimal TotalAmount { get; set; }
  34 + }
  35 +}
  36 +
... ...
netcore/src/Modularity/Extend/NCC.Extend.Interfaces/LqDailyReport/ILqDailyReportService.cs
... ... @@ -57,6 +57,13 @@ namespace NCC.Extend.Interfaces.LqDailyReport
57 57 /// <param name="input">查询参数</param>
58 58 /// <returns>科技部老师统计列表</returns>
59 59 Task<List<TechTeacherDailyStatisticsOutput>> GetTechTeacherDailyStatistics(TechTeacherDailyStatisticsInput input);
  60 +
  61 + /// <summary>
  62 + /// 获取储值扣减金额统计
  63 + /// </summary>
  64 + /// <param name="input">查询参数</param>
  65 + /// <returns>储值扣减金额统计结果</returns>
  66 + Task<DeductAmountStatisticsOutput> GetDeductAmountStatistics(DeductAmountStatisticsInput input);
60 67 }
61 68 }
62 69  
... ...
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();
... ... @@ -472,6 +472,16 @@ namespace NCC.Extend
472 472 /// <remarks>
473 473 /// 根据指定日期统计各天王团的业绩完成情况,包括目标业绩、开单业绩、退卡金额、实际完成业绩、完成率
474 474 ///
  475 + /// 业绩统计规则:
  476 + /// - 教育部(教育一部、教育二部):统计生美品项(lq_xmzl.qt2='生美')相关的开单和退卡业绩
  477 + /// - 科技部(科技一部、科技二部):统计科美品项(lq_xmzl.qt2='科美')相关的开单和退卡业绩
  478 + /// - 大项目部(大项目一部、大项目二部):统计医美品项(lq_xmzl.qt2='医美')相关的开单和退卡业绩
  479 + ///
  480 + /// 业绩分配规则:
  481 + /// - 生美品项的开单和退卡业绩全部归教育部(不按目标比例分配)
  482 + /// - 科美品项的开单和退卡业绩全部归科技部(不按目标比例分配)
  483 + /// - 医美品项的开单和退卡业绩全部归大项目部(不按目标比例分配)
  484 + ///
475 485 /// 示例请求:
476 486 /// ```json
477 487 /// {
... ... @@ -490,8 +500,8 @@ namespace NCC.Extend
490 500 /// - DepartmentId: 部门ID
491 501 /// - DepartmentName: 部门名称(教育一部、教育二部、科技一部、科技二部、大项目一部、大项目二部)
492 502 /// - TargetPerformance: 目标业绩(来自门店目标表lq_md_target,根据部门类型使用对应字段:教育部使用F_EducationDepartmentTarget,科技部使用F_TechDepartmentTarget,大项目部使用F_MajorProjectDepartmentTarget,根据开始时间所在月份获取,通过对应归属字段关联,如果未查询到则为0)
493   - /// - BillingPerformance: 开单业绩(指定时间范围内的开单业绩总和)
494   - /// - RefundPerformance: 退款业绩(指定时间范围内的退卡业绩总和)
  503 + /// - BillingPerformance: 开单业绩(指定时间范围内,按品项分类过滤后的开单业绩总和,按门店目标比例分配)
  504 + /// - RefundPerformance: 退款业绩(指定时间范围内,按品项分类过滤后的退卡业绩总和,按门店目标比例分配)
495 505 /// - ActualPerformance: 开单业绩(与BillingPerformance相同,用于兼容)
496 506 /// - CompletedPerformance: 实际完成业绩(开单业绩 - 退款业绩)
497 507 /// - CompletionRate: 完成率(百分比,CompletedPerformance / TargetPerformance * 100)
... ... @@ -520,12 +530,12 @@ namespace NCC.Extend
520 530 departmentFilter = $"AND o.F_Id IN ('{filterDeptIdsStr}')";
521 531 }
522 532  
523   - // 天王团部门信息(字段名、目标字段名和部门名称的映射)
524   - var tianwangDepartments = new Dictionary<string, (string fieldName, string targetFieldName, string[] departmentNames)>
  533 + // 天王团部门信息(字段名、目标字段名、部门名称和品项分类的映射)
  534 + var tianwangDepartments = new Dictionary<string, (string fieldName, string targetFieldName, string[] departmentNames, string itemType)>
525 535 {
526   - { "jyb", ("F_EducationDepartment", "F_EducationDepartmentTarget", new[] { "教育一部", "教育二部" }) },
527   - { "kjb", ("F_TechDepartment", "F_TechDepartmentTarget", new[] { "科技一部", "科技二部" }) },
528   - { "dxmb", ("F_MajorProjectDepartment", "F_MajorProjectDepartmentTarget", new[] { "大项目一部", "大项目二部" }) }
  536 + { "jyb", ("F_EducationDepartment", "F_EducationDepartmentTarget", new[] { "教育一部", "教育二部" }, "生美") },
  537 + { "kjb", ("F_TechDepartment", "F_TechDepartmentTarget", new[] { "科技一部", "科技二部" }, "科美") },
  538 + { "dxmb", ("F_MajorProjectDepartment", "F_MajorProjectDepartmentTarget", new[] { "大项目一部", "大项目二部" }, "医美") }
529 539 };
530 540  
531 541 // 分步查询,提高效率
... ... @@ -585,122 +595,275 @@ namespace NCC.Extend
585 595 return new List<TianwangGroupPerformanceCompletionOutput>();
586 596 }
587 597  
588   - // 第二步:统计各天王团的开单业绩(按门店目标比例分配,避免重复计算)
589   - // 使用目标表中的部门ID进行关联,而不是门店表中的部门字段
590   - var deptIdsStrForQuery = string.Join("','", departmentDict.Keys);
591   - var billingPerformanceSql = $@"
  598 + // 第二步:优化策略 - 先获取门店列表,只查询这些门店的数据,大幅提升性能
  599 + var deptIdsStr = string.Join("','", departmentDict.Keys);
  600 +
  601 + // 2.1 先快速获取有目标记录的门店ID列表(使用索引,非常快,通常<100ms)
  602 + var storeIdsSql = $@"
  603 + SELECT DISTINCT F_StoreId
  604 + FROM lq_md_target
  605 + WHERE F_Month = '{month}'
  606 + AND (
  607 + F_EducationDepartment IN ('{deptIdsStr}')
  608 + OR F_TechDepartment IN ('{deptIdsStr}')
  609 + OR F_MajorProjectDepartment IN ('{deptIdsStr}')
  610 + )";
  611 +
  612 + var storeIdsData = await _db.Ado.SqlQueryAsync<dynamic>(storeIdsSql);
  613 + var storeIds = new List<string>();
  614 + foreach (var row in storeIdsData ?? Enumerable.Empty<dynamic>())
  615 + {
  616 + var storeId = row?.F_StoreId?.ToString();
  617 + if (!string.IsNullOrEmpty(storeId))
  618 + {
  619 + storeIds.Add(storeId);
  620 + }
  621 + }
  622 +
  623 + if (!storeIds.Any())
  624 + {
  625 + return departmentDict.Values.OrderByDescending(x => x.CompletedPerformance).ToList();
  626 + }
  627 +
  628 + var storeIdsStr = string.Join("','", storeIds);
  629 +
  630 + // 2.2 查询目标表数据(一次性查询,数据量小)
  631 + // 直接查询,不使用 MAX(),因为每个门店在目标表中应该只有一条记录
  632 + var targetSql = $@"
  633 + SELECT
  634 + F_StoreId,
  635 + F_EducationDepartment,
  636 + F_TechDepartment,
  637 + F_MajorProjectDepartment
  638 + FROM lq_md_target
  639 + WHERE F_Month = '{month}'
  640 + AND F_StoreId IN ('{storeIdsStr}')";
  641 +
  642 + var targetData = await _db.Ado.SqlQueryAsync<dynamic>(targetSql);
  643 + var targetDict = new Dictionary<string, dynamic>();
  644 + foreach (var target in targetData ?? Enumerable.Empty<dynamic>())
  645 + {
  646 + var storeId = target?.F_StoreId?.ToString();
  647 + if (!string.IsNullOrEmpty(storeId))
  648 + {
  649 + targetDict[storeId] = target;
  650 + }
  651 + }
  652 +
  653 + // 2.3 从品项明细表统计业绩,按门店+品项分类分组
  654 + // 重要:业绩从品项明细表(lq_kd_pxmx)的 F_ActualPrice 获取,不是从开单记录表的 sfyj 获取
  655 + // 因为一个开单可能包含多个品项,每个品项属于不同的分类(生美、科美、医美)
  656 + // 例如:开单A实付500元,包含科美100元、医美250元、生美150元
  657 + // 那么科技部统计100元,大项目部统计250元,教育部统计150元
  658 + var billingSql = $@"
592 659 SELECT
593   - o.F_Id as DepartmentId,
594   - COALESCE(SUM(
595   - CASE
596   - -- 如果总目标 > 0,按目标比例分配
597   - WHEN COALESCE(target.F_EducationDepartmentTarget, 0) + COALESCE(target.F_TechDepartmentTarget, 0) + COALESCE(target.F_MajorProjectDepartmentTarget, 0) > 0 THEN
598   - CASE
599   - WHEN target.F_EducationDepartment = o.F_Id THEN
600   - billing.sfyj * COALESCE(target.F_EducationDepartmentTarget, 0) /
601   - (COALESCE(target.F_EducationDepartmentTarget, 0) + COALESCE(target.F_TechDepartmentTarget, 0) + COALESCE(target.F_MajorProjectDepartmentTarget, 0))
602   - WHEN target.F_TechDepartment = o.F_Id THEN
603   - billing.sfyj * COALESCE(target.F_TechDepartmentTarget, 0) /
604   - (COALESCE(target.F_EducationDepartmentTarget, 0) + COALESCE(target.F_TechDepartmentTarget, 0) + COALESCE(target.F_MajorProjectDepartmentTarget, 0))
605   - WHEN target.F_MajorProjectDepartment = o.F_Id THEN
606   - billing.sfyj * COALESCE(target.F_MajorProjectDepartmentTarget, 0) /
607   - (COALESCE(target.F_EducationDepartmentTarget, 0) + COALESCE(target.F_TechDepartmentTarget, 0) + COALESCE(target.F_MajorProjectDepartmentTarget, 0))
608   - ELSE 0
609   - END
610   - ELSE 0
611   - END
612   - ), 0) as BillingPerformance
613   - FROM lq_kd_kdjlb billing
614   - INNER JOIN lq_mdxx store ON billing.djmd = store.F_Id
615   - INNER JOIN lq_md_target target ON target.F_StoreId = store.F_Id AND target.F_Month = '{month}'
616   - INNER JOIN base_organize o ON (
617   - (target.F_EducationDepartment = o.F_Id AND o.F_FullName IN ('教育一部', '教育二部'))
618   - OR (target.F_TechDepartment = o.F_Id AND o.F_FullName IN ('科技一部', '科技二部'))
619   - OR (target.F_MajorProjectDepartment = o.F_Id AND o.F_FullName IN ('大项目一部', '大项目二部'))
620   - )
621   - WHERE billing.F_IsEffective = 1
622   - AND o.F_Category = 'department'
623   - AND (o.F_DeleteMark IS NULL OR o.F_DeleteMark != 1)
  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}')
624 718 AND billing.kdrq >= '{startDate:yyyy-MM-dd} 00:00:00'
625 719 AND billing.kdrq < '{endDate.AddDays(1):yyyy-MM-dd} 00:00:00'
626   - AND o.F_Id IN ('{deptIdsStrForQuery}')
627   - GROUP BY o.F_Id";
  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;
628 736  
629   - var billingPerformanceData = await _db.Ado.SqlQueryAsync<dynamic>(billingPerformanceSql);
  737 + if (string.IsNullOrEmpty(storeId) || string.IsNullOrEmpty(itemType) || storeAmount <= 0 || !targetDict.ContainsKey(storeId))
  738 + continue;
630 739  
631   - // 第三步:统计各天王团的退卡金额(按门店目标比例分配,避免重复计算)
632   - // 使用目标表中的部门ID进行关联,而不是门店表中的部门字段
633   - var refundPerformanceSql = $@"
634   - SELECT
635   - o.F_Id as DepartmentId,
636   - COALESCE(SUM(
637   - CASE
638   - -- 如果总目标 > 0,按目标比例分配
639   - WHEN COALESCE(target.F_EducationDepartmentTarget, 0) + COALESCE(target.F_TechDepartmentTarget, 0) + COALESCE(target.F_MajorProjectDepartmentTarget, 0) > 0 THEN
640   - CASE
641   - WHEN target.F_EducationDepartment = o.F_Id THEN
642   - refund.F_ActualRefundAmount * COALESCE(target.F_EducationDepartmentTarget, 0) /
643   - (COALESCE(target.F_EducationDepartmentTarget, 0) + COALESCE(target.F_TechDepartmentTarget, 0) + COALESCE(target.F_MajorProjectDepartmentTarget, 0))
644   - WHEN target.F_TechDepartment = o.F_Id THEN
645   - refund.F_ActualRefundAmount * COALESCE(target.F_TechDepartmentTarget, 0) /
646   - (COALESCE(target.F_EducationDepartmentTarget, 0) + COALESCE(target.F_TechDepartmentTarget, 0) + COALESCE(target.F_MajorProjectDepartmentTarget, 0))
647   - WHEN target.F_MajorProjectDepartment = o.F_Id THEN
648   - refund.F_ActualRefundAmount * COALESCE(target.F_MajorProjectDepartmentTarget, 0) /
649   - (COALESCE(target.F_EducationDepartmentTarget, 0) + COALESCE(target.F_TechDepartmentTarget, 0) + COALESCE(target.F_MajorProjectDepartmentTarget, 0))
650   - ELSE 0
651   - END
652   - ELSE 0
653   - END
654   - ), 0) as RefundPerformance
655   - FROM lq_hytk_hytk refund
656   - INNER JOIN lq_mdxx store ON refund.md = store.F_Id
657   - INNER JOIN lq_md_target target ON target.F_StoreId = store.F_Id AND target.F_Month = '{month}'
658   - INNER JOIN base_organize o ON (
659   - (target.F_EducationDepartment = o.F_Id AND o.F_FullName IN ('教育一部', '教育二部'))
660   - OR (target.F_TechDepartment = o.F_Id AND o.F_FullName IN ('科技一部', '科技二部'))
661   - OR (target.F_MajorProjectDepartment = o.F_Id AND o.F_FullName IN ('大项目一部', '大项目二部'))
662   - )
663   - WHERE refund.F_IsEffective = 1
664   - AND o.F_Category = 'department'
665   - AND (o.F_DeleteMark IS NULL OR o.F_DeleteMark != 1)
666   - AND refund.tksj >= '{startDate:yyyy-MM-dd} 00:00:00'
667   - AND refund.tksj < '{endDate.AddDays(1):yyyy-MM-dd} 00:00:00'
668   - AND o.F_Id IN ('{deptIdsStrForQuery}')
669   - GROUP BY o.F_Id";
  740 + var target = targetDict[storeId];
  741 + if (target == null)
  742 + continue;
670 743  
671   - var refundPerformanceData = await _db.Ado.SqlQueryAsync<dynamic>(refundPerformanceSql);
  744 + string targetDeptId = null;
672 745  
673   - // 第四步:合并开单业绩数据
674   - foreach (var item in billingPerformanceData ?? Enumerable.Empty<dynamic>())
  746 + // 根据品项分类,分配到对应的部门
  747 + if (itemType == "生美")
  748 + {
  749 + // 生美品项 → 教育部门
  750 + targetDeptId = target.F_EducationDepartment?.ToString();
  751 + }
  752 + else if (itemType == "科美")
  753 + {
  754 + // 科美品项 → 科技部门
  755 + targetDeptId = target.F_TechDepartment?.ToString();
  756 + }
  757 + else if (itemType == "医美")
  758 + {
  759 + // 医美品项 → 大项目部门
  760 + targetDeptId = target.F_MajorProjectDepartment?.ToString();
  761 + }
  762 +
  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;
  776 +
  777 + if (string.IsNullOrEmpty(storeId) || string.IsNullOrEmpty(itemType) || storeDeductAmount <= 0 || !targetDict.ContainsKey(storeId))
  778 + continue;
  779 +
  780 + var target = targetDict[storeId];
  781 + if (target == null)
  782 + continue;
  783 +
  784 + string targetDeptId = null;
  785 +
  786 + // 根据品项分类,从对应的部门减去储扣金额
  787 + if (itemType == "生美")
  788 + {
  789 + // 生美品项储扣 → 从教育部门减去
  790 + targetDeptId = target.F_EducationDepartment?.ToString();
  791 + }
  792 + else if (itemType == "科美")
675 793 {
676   - var departmentId = item.DepartmentId.ToString();
677   - if (departmentDict.ContainsKey(departmentId))
  794 + // 科美品项储扣 → 从科技部门减去
  795 + targetDeptId = target.F_TechDepartment?.ToString();
  796 + }
  797 + else if (itemType == "医美")
  798 + {
  799 + // 医美品项储扣 → 从大项目部门减去
  800 + targetDeptId = target.F_MajorProjectDepartment?.ToString();
  801 + }
  802 +
  803 + // 只从在查询列表中的部门减去,避免重复统计
  804 + if (!string.IsNullOrEmpty(targetDeptId) && departmentDict.ContainsKey(targetDeptId))
678 805 {
679   - departmentDict[departmentId].BillingPerformance = decimal.Round(Convert.ToDecimal(item.BillingPerformance), 2);
  806 + departmentDict[targetDeptId].BillingPerformance -= storeDeductAmount;
680 807 }
681 808 }
682 809  
683   - // 第五步:合并退卡金额数据
684   - foreach (var item in refundPerformanceData ?? Enumerable.Empty<dynamic>())
  810 + // 2.7 根据品项分类和目标表分配退卡业绩到对应部门
  811 + // 分配规则与开单业绩相同:
  812 + // - 生美品项退卡 → 分配给教育部门
  813 + // - 科美品项退卡 → 分配给科技部门
  814 + // - 医美品项退卡 → 分配给大项目部门
  815 + foreach (var refund in refundData ?? Enumerable.Empty<dynamic>())
685 816 {
686   - var departmentId = item.DepartmentId.ToString();
687   - if (departmentDict.ContainsKey(departmentId))
  817 + var storeId = refund?.StoreId?.ToString();
  818 + var itemType = refund?.ItemType?.ToString();
  819 + var storeRefundAmount = refund?.StoreRefundAmount != null ? Convert.ToDecimal(refund.StoreRefundAmount) : 0m;
  820 +
  821 + if (string.IsNullOrEmpty(storeId) || string.IsNullOrEmpty(itemType) || storeRefundAmount <= 0 || !targetDict.ContainsKey(storeId))
  822 + continue;
  823 +
  824 + var target = targetDict[storeId];
  825 + if (target == null)
  826 + continue;
  827 +
  828 + string targetDeptId = null;
  829 +
  830 + // 根据品项分类,分配到对应的部门
  831 + if (itemType == "生美")
  832 + {
  833 + // 生美品项退卡 → 教育部门
  834 + targetDeptId = target.F_EducationDepartment?.ToString();
  835 + }
  836 + else if (itemType == "科美")
688 837 {
689   - departmentDict[departmentId].RefundPerformance = decimal.Round(Convert.ToDecimal(item.RefundPerformance), 2);
  838 + // 科美品项退卡 → 科技部门
  839 + targetDeptId = target.F_TechDepartment?.ToString();
  840 + }
  841 + else if (itemType == "医美")
  842 + {
  843 + // 医美品项退卡 → 大项目部门
  844 + targetDeptId = target.F_MajorProjectDepartment?.ToString();
  845 + }
  846 +
  847 + // 只分配给在查询列表中的部门,避免重复统计
  848 + if (!string.IsNullOrEmpty(targetDeptId) && departmentDict.ContainsKey(targetDeptId))
  849 + {
  850 + departmentDict[targetDeptId].RefundPerformance += storeRefundAmount;
690 851 }
691 852 }
692 853  
693   - // 第六步:计算实际完成业绩和完成率
  854 + // 第三步:计算实际完成业绩和完成率,并保留两位小数
694 855 foreach (var dept in departmentDict.Values)
695 856 {
  857 + dept.BillingPerformance = decimal.Round(dept.BillingPerformance, 2);
  858 + dept.RefundPerformance = decimal.Round(dept.RefundPerformance, 2);
696 859 dept.ActualPerformance = dept.BillingPerformance; // 开单业绩
697 860 var actualCompletedPerformance = dept.BillingPerformance - dept.RefundPerformance; // 实际完成业绩
698 861 dept.CompletedPerformance = decimal.Round(actualCompletedPerformance, 2); // 实际完成业绩,保留两位小数
699 862  
700   - var completionRate = dept.TargetPerformance > 0
  863 + var completionRate = dept.TargetPerformance > 0
701 864 ? (actualCompletedPerformance / dept.TargetPerformance * 100m)
702   - : 0m;
703   - dept.CompletionRate = decimal.Round(completionRate, 2);
  865 + : 0m;
  866 + dept.CompletionRate = decimal.Round(completionRate, 2);
704 867 }
705 868  
706 869 var outputList = departmentDict.Values
... ... @@ -1420,6 +1583,86 @@ namespace NCC.Extend
1420 1583 }
1421 1584 #endregion
1422 1585  
  1586 + #region 获取储值扣减金额统计
  1587 + /// <summary>
  1588 + /// 获取储值扣减金额统计
  1589 + /// </summary>
  1590 + /// <remarks>
  1591 + /// 根据指定时间范围统计储值扣减金额,按品项分类(医美、生美、科美、其他)分组统计
  1592 + ///
  1593 + /// 示例请求:
  1594 + /// ```json
  1595 + /// {
  1596 + /// "startTime": "2025-11-01",
  1597 + /// "endTime": "2025-11-30"
  1598 + /// }
  1599 + /// ```
  1600 + ///
  1601 + /// 参数说明:
  1602 + /// - startTime: 开始时间,可选,不传则默认为当月1号
  1603 + /// - endTime: 结束时间,可选,不传则默认为今天
  1604 + ///
  1605 + /// 返回说明:
  1606 + /// - YiMeiAmount: 医美储扣金额
  1607 + /// - ShengMeiAmount: 生美储扣金额
  1608 + /// - KeMeiAmount: 科美储扣金额
  1609 + /// - OtherAmount: 其他品项储扣金额
  1610 + /// - TotalAmount: 总储扣金额
  1611 + /// </remarks>
  1612 + /// <param name="input">查询参数</param>
  1613 + /// <returns>储值扣减金额统计结果</returns>
  1614 + /// <response code="200">成功返回储值扣减金额统计结果</response>
  1615 + /// <response code="400">日期格式错误或参数无效</response>
  1616 + /// <response code="500">服务器内部错误</response>
  1617 + [AllowAnonymous]
  1618 + [HttpPost("get-deduct-amount-statistics")]
  1619 + public async Task<DeductAmountStatisticsOutput> GetDeductAmountStatistics(DeductAmountStatisticsInput input)
  1620 + {
  1621 + // 获取时间范围
  1622 + var (startDate, endDate) = GetTimeRange(input.StartTime, input.EndTime);
  1623 +
  1624 + // 统计储扣金额,按品项分类分组
  1625 + var sql = $@"
  1626 + SELECT
  1627 + COALESCE(SUM(CASE WHEN item.qt2 = '医美' THEN deduct.F_Amount ELSE 0 END), 0) as YiMeiAmount,
  1628 + COALESCE(SUM(CASE WHEN item.qt2 = '生美' THEN deduct.F_Amount ELSE 0 END), 0) as ShengMeiAmount,
  1629 + COALESCE(SUM(CASE WHEN item.qt2 = '科美' THEN deduct.F_Amount ELSE 0 END), 0) as KeMeiAmount,
  1630 + COALESCE(SUM(CASE WHEN item.qt2 NOT IN ('医美', '生美', '科美') OR item.qt2 IS NULL THEN deduct.F_Amount ELSE 0 END), 0) as OtherAmount,
  1631 + COALESCE(SUM(deduct.F_Amount), 0) as TotalAmount
  1632 + FROM lq_kd_deductinfo deduct
  1633 + INNER JOIN lq_kd_kdjlb billing ON deduct.F_BillingId = billing.F_Id
  1634 + LEFT JOIN lq_xmzl item ON deduct.F_ItemId = item.F_Id AND item.F_IsEffective = 1
  1635 + WHERE deduct.F_IsEffective = 1
  1636 + AND billing.F_IsEffective = 1
  1637 + AND billing.kdrq >= '{startDate:yyyy-MM-dd} 00:00:00'
  1638 + AND billing.kdrq < '{endDate.AddDays(1):yyyy-MM-dd} 00:00:00'";
  1639 +
  1640 + var result = await _db.Ado.SqlQueryAsync<dynamic>(sql);
  1641 + var data = result.FirstOrDefault();
  1642 +
  1643 + if (data == null)
  1644 + {
  1645 + return new DeductAmountStatisticsOutput
  1646 + {
  1647 + YiMeiAmount = 0,
  1648 + ShengMeiAmount = 0,
  1649 + KeMeiAmount = 0,
  1650 + OtherAmount = 0,
  1651 + TotalAmount = 0
  1652 + };
  1653 + }
  1654 +
  1655 + return new DeductAmountStatisticsOutput
  1656 + {
  1657 + YiMeiAmount = decimal.Round(data.YiMeiAmount != null ? Convert.ToDecimal(data.YiMeiAmount) : 0m, 2),
  1658 + ShengMeiAmount = decimal.Round(data.ShengMeiAmount != null ? Convert.ToDecimal(data.ShengMeiAmount) : 0m, 2),
  1659 + KeMeiAmount = decimal.Round(data.KeMeiAmount != null ? Convert.ToDecimal(data.KeMeiAmount) : 0m, 2),
  1660 + OtherAmount = decimal.Round(data.OtherAmount != null ? Convert.ToDecimal(data.OtherAmount) : 0m, 2),
  1661 + TotalAmount = decimal.Round(data.TotalAmount != null ? Convert.ToDecimal(data.TotalAmount) : 0m, 2)
  1662 + };
  1663 + }
  1664 + #endregion
  1665 +
1423 1666  
1424 1667 }
1425 1668 }
... ...
netcore/src/Modularity/Extend/NCC.Extend/LqStatisticsService.cs
... ... @@ -3588,19 +3588,17 @@ namespace NCC.Extend.LqStatistics
3588 3588 /// </summary>
3589 3589 private async Task<(int TotalCount, decimal OriginalCount, decimal OvertimeCount, decimal AccompaniedCount)> GetConsumeProjectCountDetails(string userId, string month)
3590 3590 {
  3591 + // 直接从健康师业绩表统计项目数,使用业绩时间字段过滤,不需要JOIN其他表
3591 3592 var sql = $@"
3592 3593 SELECT
3593   - COALESCE(SUM(pxmx.F_ProjectNumber), 0) as TotalCount,
3594   - COALESCE(SUM(COALESCE(pxmx.F_OriginalProjectNumber, pxmx.F_ProjectNumber)), 0) as OriginalCount,
3595   - COALESCE(SUM(COALESCE(pxmx.F_OvertimeProjectNumber, 0)), 0) as OvertimeCount,
  3594 + COALESCE(SUM(jksyj.F_kdpxNumber), 0) as TotalCount,
  3595 + COALESCE(SUM(COALESCE(jksyj.F_OriginalKdpxNumber, jksyj.F_kdpxNumber)), 0) as OriginalCount,
  3596 + COALESCE(SUM(COALESCE(jksyj.F_OvertimeKdpxNumber, 0)), 0) as OvertimeCount,
3596 3597 COALESCE(SUM(COALESCE(jksyj.F_AccompaniedProjectNumber, 0)), 0) as AccompaniedCount
3597 3598 FROM lq_xh_jksyj jksyj
3598   - INNER JOIN lq_xh_hyhk hyhk ON jksyj.glkdbh = hyhk.F_Id
3599   - INNER JOIN lq_xh_pxmx pxmx ON pxmx.F_ConsumeInfoId = hyhk.F_Id
3600 3599 WHERE jksyj.jkszh = '{userId}'
3601 3600 AND jksyj.F_IsEffective = 1
3602   - AND hyhk.F_IsEffective = 1
3603   - AND DATE_FORMAT(hyhk.hksj, '%Y%m') = '{month}'";
  3601 + AND DATE_FORMAT(jksyj.yjsj, '%Y%m') = '{month}'";
3604 3602  
3605 3603 var result = await _db.Ado.SqlQueryAsync<dynamic>(sql);
3606 3604 var data = result.FirstOrDefault();
... ...