diff --git a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqDailyReport/TianwangGroupPerformanceCompletionOutput.cs b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqDailyReport/TianwangGroupPerformanceCompletionOutput.cs index e35dd64..26df5a9 100644 --- a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqDailyReport/TianwangGroupPerformanceCompletionOutput.cs +++ b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqDailyReport/TianwangGroupPerformanceCompletionOutput.cs @@ -52,6 +52,11 @@ namespace NCC.Extend.Entitys.Dto.LqDailyReport /// 门店数量 /// public int StoreCount { get; set; } + + /// + /// 储扣金额(储值扣减金额总和) + /// + public decimal DeductAmount { get; set; } } } diff --git a/netcore/src/Modularity/Extend/NCC.Extend/LqDailyReportService.cs b/netcore/src/Modularity/Extend/NCC.Extend/LqDailyReportService.cs index 2e086ef..fc78e8d 100644 --- a/netcore/src/Modularity/Extend/NCC.Extend/LqDailyReportService.cs +++ b/netcore/src/Modularity/Extend/NCC.Extend/LqDailyReportService.cs @@ -456,7 +456,7 @@ namespace NCC.Extend var actualCompletedPerformance = unit.BillingPerformance - unit.RefundPerformance; // 实际完成业绩 unit.CompletedPerformance = actualCompletedPerformance; // 实际完成业绩 var completionRate = unit.TargetPerformance > 0 ? (actualCompletedPerformance / unit.TargetPerformance * 100m) : 0m; - unit.CompletionRate = decimal.Round(completionRate, 2); + unit.CompletionRate = decimal.Round(completionRate, 2); } var outputList = businessUnitDict.Values.OrderByDescending(x => x.CompletedPerformance).ToList(); @@ -470,17 +470,23 @@ namespace NCC.Extend /// 获取天王团业绩完成情况 /// /// - /// 根据指定日期统计各天王团的业绩完成情况,包括目标业绩、开单业绩、退卡金额、实际完成业绩、完成率 + /// 根据指定日期统计各天王团的业绩完成情况,包括目标业绩、开单业绩、储扣金额、退卡金额、实际完成业绩、完成率 /// /// 业绩统计规则: - /// - 教育部(教育一部、教育二部):统计生美品项(lq_xmzl.qt2='生美')相关的开单和退卡业绩 - /// - 科技部(科技一部、科技二部):统计科美品项(lq_xmzl.qt2='科美')相关的开单和退卡业绩 - /// - 大项目部(大项目一部、大项目二部):统计医美品项(lq_xmzl.qt2='医美')相关的开单和退卡业绩 + /// - 教育部(教育一部、教育二部):统计生美品项(lq_xmzl.qt2='生美')相关的开单、储扣和退卡业绩 + /// - 科技部(科技一部、科技二部):统计科美品项(lq_xmzl.qt2='科美')相关的开单、储扣和退卡业绩 + /// - 大项目部(大项目一部、大项目二部):统计医美品项(lq_xmzl.qt2='医美')相关的开单、储扣和退卡业绩 /// /// 业绩分配规则: - /// - 生美品项的开单和退卡业绩全部归教育部(不按目标比例分配) - /// - 科美品项的开单和退卡业绩全部归科技部(不按目标比例分配) - /// - 医美品项的开单和退卡业绩全部归大项目部(不按目标比例分配) + /// - 生美品项的开单、储扣和退卡业绩全部归教育部(不按目标比例分配) + /// - 科美品项的开单、储扣和退卡业绩全部归科技部(不按目标比例分配) + /// - 医美品项的开单、储扣和退卡业绩全部归大项目部(不按目标比例分配) + /// + /// 业绩计算规则: + /// - 开单业绩:原始开单业绩(不减去储扣金额) + /// - 储扣金额:储值扣减金额总和 + /// - 退卡业绩:退卡金额总和 + /// - 实际完成业绩 = 开单业绩 - 储扣金额 - 退卡金额 /// /// 示例请求: /// ```json @@ -500,10 +506,11 @@ namespace NCC.Extend /// - DepartmentId: 部门ID /// - DepartmentName: 部门名称(教育一部、教育二部、科技一部、科技二部、大项目一部、大项目二部) /// - TargetPerformance: 目标业绩(来自门店目标表lq_md_target,根据部门类型使用对应字段:教育部使用F_EducationDepartmentTarget,科技部使用F_TechDepartmentTarget,大项目部使用F_MajorProjectDepartmentTarget,根据开始时间所在月份获取,通过对应归属字段关联,如果未查询到则为0) - /// - BillingPerformance: 开单业绩(指定时间范围内,按品项分类过滤后的开单业绩总和,按门店目标比例分配) - /// - RefundPerformance: 退款业绩(指定时间范围内,按品项分类过滤后的退卡业绩总和,按门店目标比例分配) + /// - BillingPerformance: 开单业绩(原始开单业绩,不减去储扣金额) + /// - DeductAmount: 储扣金额(储值扣减金额总和) + /// - RefundPerformance: 退卡业绩(退卡金额总和) /// - ActualPerformance: 开单业绩(与BillingPerformance相同,用于兼容) - /// - CompletedPerformance: 实际完成业绩(开单业绩 - 退款业绩) + /// - CompletedPerformance: 实际完成业绩(开单业绩 - 储扣金额 - 退卡金额) /// - CompletionRate: 完成率(百分比,CompletedPerformance / TargetPerformance * 100) /// - StoreCount: 门店数量(根据门店目标表中归属该部门的门店数统计) /// @@ -582,7 +589,8 @@ namespace NCC.Extend ActualPerformance = 0, CompletedPerformance = 0, CompletionRate = 0, - StoreCount = Convert.ToInt32(dept.StoreCount) + StoreCount = Convert.ToInt32(dept.StoreCount), + DeductAmount = 0 }; // 记录部门对应的字段类型 departmentFieldMap[deptId] = deptType.Value.fieldName; @@ -620,6 +628,7 @@ namespace NCC.Extend } } + // 如果目标表中没有对应月份的数据,直接返回空业绩列表 if (!storeIds.Any()) { return departmentDict.Values.OrderByDescending(x => x.CompletedPerformance).ToList(); @@ -630,7 +639,7 @@ namespace NCC.Extend // 2.2 查询目标表数据(一次性查询,数据量小) // 直接查询,不使用 MAX(),因为每个门店在目标表中应该只有一条记录 var targetSql = $@" - SELECT + SELECT F_StoreId, F_EducationDepartment, F_TechDepartment, @@ -650,220 +659,163 @@ namespace NCC.Extend } } - // 2.3 从品项明细表统计业绩,按门店+品项分类分组 + // 如果targetDict为空,说明目标表中没有数据,直接返回 + if (!targetDict.Any()) + { + return departmentDict.Values.OrderByDescending(x => x.CompletedPerformance).ToList(); + } + + // 2.3 分步骤查询:按生美、科美、医美分别查询开单业绩、退卡业绩、储扣金额 // 重要:业绩从品项明细表(lq_kd_pxmx)的 F_ActualPrice 获取,不是从开单记录表的 sfyj 获取 // 因为一个开单可能包含多个品项,每个品项属于不同的分类(生美、科美、医美) // 例如:开单A实付500元,包含科美100元、医美250元、生美150元 // 那么科技部统计100元,大项目部统计250元,教育部统计150元 - var billingSql = $@" - SELECT - temp.djmd as StoreId, - item.qt2 as ItemType, - COALESCE(SUM(temp.F_ActualPrice), 0) as StoreAmount - FROM ( - SELECT pxmx.px, pxmx.F_ActualPrice, billing.djmd - FROM lq_kd_pxmx pxmx - INNER JOIN lq_kd_kdjlb billing ON pxmx.glkdbh = billing.F_Id - WHERE pxmx.F_IsEffective = 1 - AND billing.F_IsEffective = 1 - AND billing.djmd IN ('{storeIdsStr}') - AND billing.kdrq >= '{startDate:yyyy-MM-dd} 00:00:00' - AND billing.kdrq < '{endDate.AddDays(1):yyyy-MM-dd} 00:00:00' - ) temp - INNER JOIN lq_xmzl item ON temp.px = item.F_Id - WHERE item.F_IsEffective = 1 - AND item.qt2 IN ('生美', '科美', '医美') - GROUP BY temp.djmd, item.qt2"; - - var billingData = await _db.Ado.SqlQueryAsync(billingSql); - - // 2.4 从退卡明细表统计退卡业绩,按门店+品项分类分组 - // 重要:退卡业绩从退卡明细表(lq_hytk_mx)的 tkje 获取,按品项分类统计 - var refundSql = $@" - SELECT - temp.md as StoreId, - item.qt2 as ItemType, - COALESCE(SUM(temp.tkje), 0) as StoreRefundAmount - FROM ( - SELECT refund_mx.px, refund_mx.tkje, refund.md - FROM lq_hytk_mx refund_mx - INNER JOIN lq_hytk_hytk refund ON refund_mx.F_RefundInfoId = refund.F_Id - WHERE refund_mx.F_IsEffective = 1 - AND refund.F_IsEffective = 1 - AND refund.md IN ('{storeIdsStr}') - AND refund.tksj >= '{startDate:yyyy-MM-dd} 00:00:00' - AND refund.tksj < '{endDate.AddDays(1):yyyy-MM-dd} 00:00:00' - ) temp - INNER JOIN lq_xmzl item ON temp.px = item.F_Id - WHERE item.F_IsEffective = 1 - AND item.qt2 IN ('生美', '科美', '医美') - GROUP BY temp.md, item.qt2"; - - var refundData = await _db.Ado.SqlQueryAsync(refundSql); - - // 2.5 从储扣明细表统计储扣金额,按门店+品项分类分组 - // 重要:储扣金额从储扣明细表(lq_kd_deductinfo)的 F_Amount 获取,按品项分类统计 - var deductSql = $@" - SELECT - billing.djmd as StoreId, - item.qt2 as ItemType, - COALESCE(SUM(deduct.F_Amount), 0) as StoreDeductAmount - FROM lq_kd_deductinfo deduct - INNER JOIN lq_kd_kdjlb billing ON deduct.F_BillingId = billing.F_Id - INNER JOIN lq_xmzl item ON deduct.F_ItemId = item.F_Id - WHERE deduct.F_IsEffective = 1 - AND billing.F_IsEffective = 1 - AND item.F_IsEffective = 1 - AND billing.djmd IN ('{storeIdsStr}') - AND billing.kdrq >= '{startDate:yyyy-MM-dd} 00:00:00' - AND billing.kdrq < '{endDate.AddDays(1):yyyy-MM-dd} 00:00:00' - AND item.qt2 IN ('生美', '科美', '医美') - GROUP BY billing.djmd, item.qt2"; - - var deductData = await _db.Ado.SqlQueryAsync(deductSql); - - // 2.6 根据品项分类和目标表分配业绩到对应部门 - // 分配规则: - // - 生美品项 → 分配给教育部门(F_EducationDepartment) - // - 科美品项 → 分配给科技部门(F_TechDepartment) - // - 医美品项 → 分配给大项目部门(F_MajorProjectDepartment) - // 注意:一个门店可能同时属于多个部门,但每个品项的业绩只分配给对应的一个部门 - foreach (var billing in billingData ?? Enumerable.Empty()) - { - var storeId = billing?.StoreId?.ToString(); - var itemType = billing?.ItemType?.ToString(); - var storeAmount = billing?.StoreAmount != null ? Convert.ToDecimal(billing.StoreAmount) : 0m; - if (string.IsNullOrEmpty(storeId) || string.IsNullOrEmpty(itemType) || storeAmount <= 0 || !targetDict.ContainsKey(storeId)) - continue; - - var target = targetDict[storeId]; - if (target == null) - continue; + // 品项类型列表 + var itemTypes = new[] { "生美", "科美", "医美" }; - string targetDeptId = null; - - // 根据品项分类,分配到对应的部门 + // 循环处理每个品项类型 + foreach (var itemType in itemTypes) + { + // 确定该品项类型对应的部门字段 + string deptField = ""; if (itemType == "生美") { - // 生美品项 → 教育部门 - targetDeptId = target.F_EducationDepartment?.ToString(); + deptField = "F_EducationDepartment"; } else if (itemType == "科美") { - // 科美品项 → 科技部门 - targetDeptId = target.F_TechDepartment?.ToString(); + deptField = "F_TechDepartment"; } else if (itemType == "医美") { - // 医美品项 → 大项目部门 - targetDeptId = target.F_MajorProjectDepartment?.ToString(); + deptField = "F_MajorProjectDepartment"; } - // 只分配给在查询列表中的部门,避免重复统计 - if (!string.IsNullOrEmpty(targetDeptId) && departmentDict.ContainsKey(targetDeptId)) - { - departmentDict[targetDeptId].BillingPerformance += storeAmount; - } - } - - // 2.6.1 减去储扣金额 - foreach (var deduct in deductData ?? Enumerable.Empty()) - { - var storeId = deduct?.StoreId?.ToString(); - var itemType = deduct?.ItemType?.ToString(); - var storeDeductAmount = deduct?.StoreDeductAmount != null ? Convert.ToDecimal(deduct.StoreDeductAmount) : 0m; + // 2.3.1 查询开单业绩(按品项类型) + var billingSql = $@" + SELECT + target.{deptField} as TargetDeptId, + COALESCE(SUM(pxmx.F_ActualPrice), 0) as StoreAmount + FROM lq_kd_pxmx pxmx + INNER JOIN lq_kd_kdjlb billing ON pxmx.glkdbh = billing.F_Id + INNER JOIN lq_xmzl item ON pxmx.px = item.F_Id + INNER JOIN lq_md_target target ON billing.djmd = target.F_StoreId AND target.F_Month = '{month}' + WHERE pxmx.F_IsEffective = 1 + AND billing.F_IsEffective = 1 + AND item.F_IsEffective = 1 + AND item.qt2 = '{itemType}' + AND billing.djmd IN ('{storeIdsStr}') + AND billing.kdrq >= '{startDate.ToString("yyyy-MM-dd")} 00:00:00' + AND billing.kdrq < '{endDate.AddDays(1).ToString("yyyy-MM-dd")} 00:00:00' + GROUP BY target.{deptField}"; - if (string.IsNullOrEmpty(storeId) || string.IsNullOrEmpty(itemType) || storeDeductAmount <= 0 || !targetDict.ContainsKey(storeId)) - continue; + var billingData = await _db.Ado.SqlQueryAsync(billingSql); - var target = targetDict[storeId]; - if (target == null) - continue; + // 分配开单业绩到对应部门 + foreach (var billing in billingData ?? Enumerable.Empty()) + { + var storeAmount = billing?.StoreAmount != null ? Convert.ToDecimal(billing.StoreAmount) : 0m; + var targetDeptId = billing?.TargetDeptId?.ToString(); - string targetDeptId = null; + if (storeAmount <= 0 || string.IsNullOrEmpty(targetDeptId)) + continue; - // 根据品项分类,从对应的部门减去储扣金额 - if (itemType == "生美") - { - // 生美品项储扣 → 从教育部门减去 - targetDeptId = target.F_EducationDepartment?.ToString(); - } - else if (itemType == "科美") - { - // 科美品项储扣 → 从科技部门减去 - targetDeptId = target.F_TechDepartment?.ToString(); - } - else if (itemType == "医美") - { - // 医美品项储扣 → 从大项目部门减去 - targetDeptId = target.F_MajorProjectDepartment?.ToString(); + // 只分配给在查询列表中的部门,避免重复统计 + if (departmentDict.ContainsKey(targetDeptId)) + { + departmentDict[targetDeptId].BillingPerformance += storeAmount; + } } - // 只从在查询列表中的部门减去,避免重复统计 - if (!string.IsNullOrEmpty(targetDeptId) && departmentDict.ContainsKey(targetDeptId)) + // 2.3.2 查询退卡业绩(按品项类型) + var refundSql = $@" + SELECT + target.{deptField} as TargetDeptId, + COALESCE(SUM(refund_mx.tkje), 0) as StoreRefundAmount + FROM lq_hytk_mx refund_mx + INNER JOIN lq_hytk_hytk refund ON refund_mx.F_RefundInfoId = refund.F_Id + INNER JOIN lq_xmzl item ON refund_mx.px = item.F_Id + INNER JOIN lq_md_target target ON refund.md = target.F_StoreId AND target.F_Month = '{month}' + WHERE refund_mx.F_IsEffective = 1 + AND refund.F_IsEffective = 1 + AND item.F_IsEffective = 1 + AND item.qt2 = '{itemType}' + AND refund.md IN ('{storeIdsStr}') + AND refund.tksj >= '{startDate.ToString("yyyy-MM-dd")} 00:00:00' + AND refund.tksj < '{endDate.AddDays(1).ToString("yyyy-MM-dd")} 00:00:00' + GROUP BY target.{deptField}"; + + var refundData = await _db.Ado.SqlQueryAsync(refundSql); + + // 分配退卡业绩到对应部门 + foreach (var refund in refundData ?? Enumerable.Empty()) { - departmentDict[targetDeptId].BillingPerformance -= storeDeductAmount; - } - } + var storeRefundAmount = refund?.StoreRefundAmount != null ? Convert.ToDecimal(refund.StoreRefundAmount) : 0m; + var targetDeptId = refund?.TargetDeptId?.ToString(); - // 2.7 根据品项分类和目标表分配退卡业绩到对应部门 - // 分配规则与开单业绩相同: - // - 生美品项退卡 → 分配给教育部门 - // - 科美品项退卡 → 分配给科技部门 - // - 医美品项退卡 → 分配给大项目部门 - foreach (var refund in refundData ?? Enumerable.Empty()) - { - var storeId = refund?.StoreId?.ToString(); - var itemType = refund?.ItemType?.ToString(); - var storeRefundAmount = refund?.StoreRefundAmount != null ? Convert.ToDecimal(refund.StoreRefundAmount) : 0m; + if (storeRefundAmount <= 0 || string.IsNullOrEmpty(targetDeptId)) + continue; - if (string.IsNullOrEmpty(storeId) || string.IsNullOrEmpty(itemType) || storeRefundAmount <= 0 || !targetDict.ContainsKey(storeId)) - continue; + // 只分配给在查询列表中的部门,避免重复统计 + if (departmentDict.ContainsKey(targetDeptId)) + { + departmentDict[targetDeptId].RefundPerformance += storeRefundAmount; + } + } - var target = targetDict[storeId]; - if (target == null) - continue; + // 2.3.3 查询储扣金额(按品项类型) + var deductSql = $@" + SELECT + target.{deptField} as TargetDeptId, + COALESCE(SUM(deduct.F_Amount), 0) as StoreDeductAmount + FROM lq_kd_deductinfo deduct + INNER JOIN lq_kd_kdjlb billing ON deduct.F_BillingId = billing.F_Id + INNER JOIN lq_xmzl item ON deduct.F_ItemId = item.F_Id + INNER JOIN lq_md_target target ON billing.djmd = target.F_StoreId AND target.F_Month = '{month}' + WHERE deduct.F_IsEffective = 1 + AND billing.F_IsEffective = 1 + AND item.F_IsEffective = 1 + AND item.qt2 = '{itemType}' + AND billing.djmd IN ('{storeIdsStr}') + AND billing.kdrq >= '{startDate.ToString("yyyy-MM-dd")} 00:00:00' + AND billing.kdrq < '{endDate.AddDays(1).ToString("yyyy-MM-dd")} 00:00:00' + GROUP BY target.{deptField}"; - string targetDeptId = null; + var deductData = await _db.Ado.SqlQueryAsync(deductSql); - // 根据品项分类,分配到对应的部门 - if (itemType == "生美") + // 分配储扣金额到对应部门 + foreach (var deduct in deductData ?? Enumerable.Empty()) { - // 生美品项退卡 → 教育部门 - targetDeptId = target.F_EducationDepartment?.ToString(); - } - else if (itemType == "科美") - { - // 科美品项退卡 → 科技部门 - targetDeptId = target.F_TechDepartment?.ToString(); - } - else if (itemType == "医美") - { - // 医美品项退卡 → 大项目部门 - targetDeptId = target.F_MajorProjectDepartment?.ToString(); - } + var storeDeductAmount = deduct?.StoreDeductAmount != null ? Convert.ToDecimal(deduct.StoreDeductAmount) : 0m; + var targetDeptId = deduct?.TargetDeptId?.ToString(); - // 只分配给在查询列表中的部门,避免重复统计 - if (!string.IsNullOrEmpty(targetDeptId) && departmentDict.ContainsKey(targetDeptId)) - { - departmentDict[targetDeptId].RefundPerformance += storeRefundAmount; + if (storeDeductAmount <= 0 || string.IsNullOrEmpty(targetDeptId)) + continue; + + // 只累加到在查询列表中的部门,避免重复统计 + if (departmentDict.ContainsKey(targetDeptId)) + { + departmentDict[targetDeptId].DeductAmount += storeDeductAmount; + } } } // 第三步:计算实际完成业绩和完成率,并保留两位小数 foreach (var dept in departmentDict.Values) { - dept.BillingPerformance = decimal.Round(dept.BillingPerformance, 2); - dept.RefundPerformance = decimal.Round(dept.RefundPerformance, 2); - dept.ActualPerformance = dept.BillingPerformance; // 开单业绩 - var actualCompletedPerformance = dept.BillingPerformance - dept.RefundPerformance; // 实际完成业绩 + dept.BillingPerformance = decimal.Round(dept.BillingPerformance, 2); // 开单业绩(原始,不减去储扣) + dept.RefundPerformance = decimal.Round(dept.RefundPerformance, 2); // 退卡业绩 + dept.DeductAmount = decimal.Round(dept.DeductAmount, 2); // 储扣金额,保留两位小数 + dept.ActualPerformance = dept.BillingPerformance; // 开单业绩(与BillingPerformance相同,用于兼容) + // 实际完成业绩 = 开单业绩 - 储扣金额 - 退卡金额 + var actualCompletedPerformance = dept.BillingPerformance - dept.DeductAmount - dept.RefundPerformance; dept.CompletedPerformance = decimal.Round(actualCompletedPerformance, 2); // 实际完成业绩,保留两位小数 - var completionRate = dept.TargetPerformance > 0 - ? (actualCompletedPerformance / dept.TargetPerformance * 100m) - : 0m; - dept.CompletionRate = decimal.Round(completionRate, 2); + var completionRate = dept.TargetPerformance > 0 + ? (actualCompletedPerformance / dept.TargetPerformance * 100m) + : 0m; + dept.CompletionRate = decimal.Round(completionRate, 2); } var outputList = departmentDict.Values @@ -1174,12 +1126,13 @@ namespace NCC.Extend } // SQL查询:统计科技部老师的消耗业绩、见客数、项目数 + // 注意:GROUP BY 中移除了 user.F_RealName,避免同一老师ID因姓名不同产生重复记录 var consumeSql = $@" SELECT techDept.F_Id as TechDepartmentId, techDept.F_FullName as TechDepartmentName, consume.kjbls as TeacherId, - user.F_RealName as TeacherName, + MAX(user.F_RealName) as TeacherName, COUNT(DISTINCT hyhk.hy) as CustomerCount, SUM(consume.F_hdpxNumber) as ConsumeProjectCount, SUM(consume.kjblsyj) as ConsumeAchievement @@ -1194,7 +1147,7 @@ namespace NCC.Extend AND DATE(hyhk.hksj) <= '{endDate:yyyy-MM-dd}' {techFilter} {teacherFilter} - GROUP BY techDept.F_Id, techDept.F_FullName, consume.kjbls, user.F_RealName"; + GROUP BY techDept.F_Id, techDept.F_FullName, consume.kjbls"; var consumeResult = await _db.Ado.SqlQueryAsync(consumeSql); @@ -1202,6 +1155,7 @@ namespace NCC.Extend var orderSql = $@" SELECT techDept.F_Id as TechDepartmentId, + techDept.F_FullName as TechDepartmentName, ord.kjbls as TeacherId, SUM(ord.kjblsyj) as OrderAchievement FROM lq_kd_kjbsyj ord @@ -1214,36 +1168,211 @@ namespace NCC.Extend AND DATE(kdjlb.kdrq) <= '{endDate:yyyy-MM-dd}' {techFilter} {teacherFilterForOrder} - GROUP BY techDept.F_Id, ord.kjbls"; + GROUP BY techDept.F_Id, techDept.F_FullName, ord.kjbls"; var orderResult = await _db.Ado.SqlQueryAsync(orderSql); - // 合并数据 + // 合并数据:按员工ID汇总,避免同一员工在多个科技部重复出现 + // 使用 TeacherId 作为唯一键,汇总所有科技部的数据 var teacherDict = new Dictionary(); + // 记录每个员工在各个科技部的消耗业绩,用于确定主要科技部 + var teacherDeptConsumePerformance = new Dictionary>(); + // 记录每个员工在各个科技部的开单业绩,用于确定主要科技部(当消耗业绩为0时) + var teacherDeptOrderPerformance = new Dictionary>(); + // 第一步:处理消耗数据,按员工ID汇总 foreach (var item in consumeResult ?? Enumerable.Empty()) { - var key = $"{item.TechDepartmentId}_{item.TeacherId}"; - teacherDict[key] = new TechTeacherDailyStatisticsOutput + var teacherId = item.TeacherId?.ToString(); + var techDeptId = item.TechDepartmentId?.ToString(); + var techDeptName = item.TechDepartmentName?.ToString(); + var consumeAchievement = Convert.ToDecimal(item.ConsumeAchievement); + + if (string.IsNullOrEmpty(teacherId)) + continue; + + // 记录员工在各科技部的消耗业绩(累加,因为同一员工在同一科技部可能有多个门店的数据) + if (!teacherDeptConsumePerformance.ContainsKey(teacherId)) { - TechDepartmentId = item.TechDepartmentId?.ToString(), - TechDepartmentName = item.TechDepartmentName?.ToString(), - TeacherId = item.TeacherId?.ToString(), - TeacherName = item.TeacherName?.ToString(), - CustomerCount = Convert.ToInt32(item.CustomerCount), - ConsumeProjectCount = Convert.ToInt32(item.ConsumeProjectCount), - ConsumeAchievement = Convert.ToDecimal(item.ConsumeAchievement), - OrderAchievement = 0 - }; + teacherDeptConsumePerformance[teacherId] = new Dictionary(); + } + if (!string.IsNullOrEmpty(techDeptId)) + { + if (!teacherDeptConsumePerformance[teacherId].ContainsKey(techDeptId)) + { + teacherDeptConsumePerformance[teacherId][techDeptId] = 0; + } + teacherDeptConsumePerformance[teacherId][techDeptId] += consumeAchievement; + } + + // 按员工ID汇总数据 + if (!teacherDict.ContainsKey(teacherId)) + { + teacherDict[teacherId] = new TechTeacherDailyStatisticsOutput + { + TechDepartmentId = techDeptId, + TechDepartmentName = techDeptName, + TeacherId = teacherId, + TeacherName = item.TeacherName?.ToString(), + CustomerCount = 0, + ConsumeProjectCount = 0, + ConsumeAchievement = 0, + OrderAchievement = 0 + }; + } + + // 汇总数据(累加项目数和业绩,见客数后面统一重新计算去重) + var teacher = teacherDict[teacherId]; + teacher.ConsumeProjectCount += Convert.ToInt32(item.ConsumeProjectCount); + teacher.ConsumeAchievement += consumeAchievement; } - // 合并开单业绩 + // 第二步:处理开单业绩,按员工ID汇总 foreach (var item in orderResult ?? Enumerable.Empty()) { - var key = $"{item.TechDepartmentId}_{item.TeacherId}"; - if (teacherDict.ContainsKey(key)) + var teacherId = item.TeacherId?.ToString(); + var techDeptId = item.TechDepartmentId?.ToString(); + var techDeptName = item.TechDepartmentName?.ToString(); + var orderAchievement = Convert.ToDecimal(item.OrderAchievement); + + if (string.IsNullOrEmpty(teacherId)) + continue; + + // 记录员工在各科技部的开单业绩(累加,因为同一员工在同一科技部可能有多个门店的数据) + if (!teacherDeptOrderPerformance.ContainsKey(teacherId)) + { + teacherDeptOrderPerformance[teacherId] = new Dictionary(); + } + if (!string.IsNullOrEmpty(techDeptId)) { - teacherDict[key].OrderAchievement = Convert.ToDecimal(item.OrderAchievement); + if (!teacherDeptOrderPerformance[teacherId].ContainsKey(techDeptId)) + { + teacherDeptOrderPerformance[teacherId][techDeptId] = 0; + } + teacherDeptOrderPerformance[teacherId][techDeptId] += orderAchievement; + } + + if (!teacherDict.ContainsKey(teacherId)) + { + // 如果消耗数据中没有,但开单数据中有,需要创建记录 + teacherDict[teacherId] = new TechTeacherDailyStatisticsOutput + { + TechDepartmentId = techDeptId, + TechDepartmentName = techDeptName, + TeacherId = teacherId, + TeacherName = null, + CustomerCount = 0, + ConsumeProjectCount = 0, + ConsumeAchievement = 0, + OrderAchievement = 0 + }; + } + + teacherDict[teacherId].OrderAchievement += orderAchievement; + } + + // 第三步:确定每个员工的主要科技部 + // 优先按消耗业绩最多的科技部,如果消耗业绩为0,则按开单业绩最多的科技部 + foreach (var teacherId in teacherDict.Keys.ToList()) + { + var teacher = teacherDict[teacherId]; + string mainDeptId = null; + string mainDeptName = null; + + // 优先按消耗业绩确定主要科技部 + if (teacherDeptConsumePerformance.ContainsKey(teacherId) && teacherDeptConsumePerformance[teacherId].Any()) + { + var mainDept = teacherDeptConsumePerformance[teacherId] + .OrderByDescending(x => x.Value) + .FirstOrDefault(); + + if (!string.IsNullOrEmpty(mainDept.Key) && mainDept.Value > 0) + { + mainDeptId = mainDept.Key; + // 从消耗数据中获取该科技部的名称 + var mainDeptData = consumeResult?.FirstOrDefault(x => + x.TeacherId?.ToString() == teacherId && + x.TechDepartmentId?.ToString() == mainDeptId); + + if (mainDeptData != null) + { + mainDeptName = mainDeptData.TechDepartmentName?.ToString(); + } + } + } + + // 如果消耗业绩为0或没有,则按开单业绩确定主要科技部 + if (string.IsNullOrEmpty(mainDeptId) && teacherDeptOrderPerformance.ContainsKey(teacherId) && teacherDeptOrderPerformance[teacherId].Any()) + { + var mainDept = teacherDeptOrderPerformance[teacherId] + .OrderByDescending(x => x.Value) + .FirstOrDefault(); + + if (!string.IsNullOrEmpty(mainDept.Key) && mainDept.Value > 0) + { + mainDeptId = mainDept.Key; + // 从开单数据中获取该科技部的名称 + var mainDeptData = orderResult?.FirstOrDefault(x => + x.TeacherId?.ToString() == teacherId && + x.TechDepartmentId?.ToString() == mainDeptId); + + if (mainDeptData != null) + { + mainDeptName = mainDeptData.TechDepartmentName?.ToString(); + } + } + } + + // 如果仍然没有确定主要科技部,但已有科技部ID和名称,保持原样 + if (!string.IsNullOrEmpty(mainDeptId)) + { + teacher.TechDepartmentId = mainDeptId; + if (!string.IsNullOrEmpty(mainDeptName)) + { + teacher.TechDepartmentName = mainDeptName; + } + } + else if (string.IsNullOrEmpty(teacher.TechDepartmentName) && !string.IsNullOrEmpty(teacher.TechDepartmentId)) + { + // 如果仍然没有科技部名称,尝试从开单数据中获取 + var orderDeptData = orderResult?.FirstOrDefault(x => + x.TeacherId?.ToString() == teacherId && + x.TechDepartmentId?.ToString() == teacher.TechDepartmentId); + + if (orderDeptData != null && !string.IsNullOrEmpty(orderDeptData.TechDepartmentName?.ToString())) + { + teacher.TechDepartmentName = orderDeptData.TechDepartmentName?.ToString(); + } + } + } + + // 第四步:重新计算见客数(去重,因为同一个客户可能在多个科技部被统计) + // 由于已经汇总了数据,这里需要重新查询去重后的见客数 + var teacherIds = teacherDict.Keys.ToList(); + if (teacherIds.Any()) + { + var teacherIdsStr = string.Join("','", teacherIds); + var customerCountSql = $@" + SELECT + consume.kjbls as TeacherId, + COUNT(DISTINCT hyhk.hy) as CustomerCount + FROM lq_xh_kjbsyj consume + INNER JOIN lq_xh_hyhk hyhk ON consume.glkdbh = hyhk.F_Id + WHERE consume.F_IsEffective = 1 + AND hyhk.F_IsEffective = 1 + AND DATE(hyhk.hksj) >= '{startDate:yyyy-MM-dd}' + AND DATE(hyhk.hksj) <= '{endDate:yyyy-MM-dd}' + AND consume.kjbls IN ('{teacherIdsStr}') + GROUP BY consume.kjbls"; + + var customerCountResult = await _db.Ado.SqlQueryAsync(customerCountSql); + foreach (var item in customerCountResult ?? Enumerable.Empty()) + { + var teacherId = item.TeacherId?.ToString(); + if (!string.IsNullOrEmpty(teacherId) && teacherDict.ContainsKey(teacherId)) + { + teacherDict[teacherId].CustomerCount = Convert.ToInt32(item.CustomerCount); + } } }