From 8b44d4bd9ad36ec5c24615ba9a16d10c1cbc4f1c Mon Sep 17 00:00:00 2001 From: “wangming” <“wangming@antissoft.com”> Date: Fri, 19 Dec 2025 14:08:51 +0800 Subject: [PATCH] feat: 添加健康师额外工资和合作成本导入的清理功能 --- antis-ncc-admin/.env.development | 4 ++-- antis-ncc-admin/src/api/extend/healthCoachSalary.js | 3 ++- antis-ncc-admin/src/views/lqCooperationCost/import-dialog.vue | 37 ++++++++++++++++++++++++++++++++++++- antis-ncc-admin/src/views/wageManagement/extra-data-dialog.vue | 9 ++++++++- excel/健康师额外数据模板_2025年11月_20251219120434.xlsx | Bin 0 -> 26051 bytes netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqMajorProjectTeacherSalary/MajorProjectTeacherSalaryOutput.cs | 4 ++-- netcore/src/Modularity/Extend/NCC.Extend/LqAssistantSalaryService.cs | 77 +++++++++++++++++++++-------------------------------------------------------- netcore/src/Modularity/Extend/NCC.Extend/LqCooperationCostService.cs | 108 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------------------------- netcore/src/Modularity/Extend/NCC.Extend/LqMajorProjectDirectorSalaryService.cs | 43 ++++++++++++++++++++++++++++++------------- netcore/src/Modularity/Extend/NCC.Extend/LqMajorProjectTeacherSalaryService.cs | 88 ++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------------- netcore/src/Modularity/Extend/NCC.Extend/LqSalaryExtraCalculationService.cs | 20 +++++++++++++++++++- netcore/src/Modularity/Extend/NCC.Extend/LqShareStatisticsHqService.cs | 59 +++++++++++++++++++++++++++++++++++++++++++++++++++-------- netcore/src/Modularity/Extend/NCC.Extend/Utils/WeChatBotService.cs | 2 +- sql/排查生美业绩统计差异-简化版.sql | 2 ++ sql/排查生美业绩统计差异详细.sql | 2 ++ sql/检查生美业绩统计差异.sql | 2 ++ test_tianwang_api.py | 2 ++ 17 files changed, 303 insertions(+), 159 deletions(-) create mode 100644 excel/健康师额外数据模板_2025年11月_20251219120434.xlsx diff --git a/antis-ncc-admin/.env.development b/antis-ncc-admin/.env.development index 198955a..6462393 100644 --- a/antis-ncc-admin/.env.development +++ b/antis-ncc-admin/.env.development @@ -2,8 +2,8 @@ VUE_CLI_BABEL_TRANSPILE_MODULES = true # VUE_APP_BASE_API = 'https://erp.lvqianmeiye.com' -VUE_APP_BASE_API = 'http://erp_test.lvqianmeiye.com' -# VUE_APP_BASE_API = 'http://localhost:2011' +# VUE_APP_BASE_API = 'http://erp_test.lvqianmeiye.com' +VUE_APP_BASE_API = 'http://localhost:2011' # VUE_APP_BASE_API = 'http://localhost:2011' VUE_APP_IMG_API = '' VUE_APP_BASE_WSS = 'ws://192.168.110.45:2011/websocket' diff --git a/antis-ncc-admin/src/api/extend/healthCoachSalary.js b/antis-ncc-admin/src/api/extend/healthCoachSalary.js index 1480373..69bcef0 100644 --- a/antis-ncc-admin/src/api/extend/healthCoachSalary.js +++ b/antis-ncc-admin/src/api/extend/healthCoachSalary.js @@ -27,9 +27,10 @@ export function getExtraCalculationList(params) { } // 导入额外计算数据 -export function importExtraCalculationFromExcel(file) { +export function importExtraCalculationFromExcel(file, clearBeforeImport = false) { const formData = new FormData() formData.append('file', file) + formData.append('clearBeforeImport', clearBeforeImport) return request({ url: '/api/Extend/lqsalaryextracalculation/ImportFromExcel', method: 'POST', diff --git a/antis-ncc-admin/src/views/lqCooperationCost/import-dialog.vue b/antis-ncc-admin/src/views/lqCooperationCost/import-dialog.vue index 9a6a4ef..f78767e 100644 --- a/antis-ncc-admin/src/views/lqCooperationCost/import-dialog.vue +++ b/antis-ncc-admin/src/views/lqCooperationCost/import-dialog.vue @@ -21,12 +21,25 @@
将文件拖到此处,或点击上传
只能上传xlsx/xls文件,且不超过10MB
+
+ + 清理导入月份数据 + +
{{ uploadResult.title }}
{{ uploadResult.description }}
{{ msg }}
+
+
成功导入的记录(包含成本类型):
+
+ 门店ID: {{ record.storeId }}, 年份: {{ record.year }}, 月份: {{ record.month }}, + 金额: {{ record.totalAmount }}, 成本类型: {{ record.costType || '无' }}, + 备注: {{ record.remarks || '无' }} +
+
@@ -190,6 +196,7 @@ export default { loading: false, exportTemplateLoading: false, importLoading: false, + clearBeforeImport: true, // 是否需要清理导入月份数据,默认true list: [], total: 0, queryParams: { @@ -451,7 +458,7 @@ export default { this.importLoading = true try { - const response = await importExtraCalculationFromExcel(file) + const response = await importExtraCalculationFromExcel(file, this.clearBeforeImport) if (response.code === 200) { this.$message.success('导入成功') diff --git a/excel/健康师额外数据模板_2025年11月_20251219120434.xlsx b/excel/健康师额外数据模板_2025年11月_20251219120434.xlsx new file mode 100644 index 0000000..5287535 Binary files /dev/null and b/excel/健康师额外数据模板_2025年11月_20251219120434.xlsx differ diff --git a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqMajorProjectTeacherSalary/MajorProjectTeacherSalaryOutput.cs b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqMajorProjectTeacherSalary/MajorProjectTeacherSalaryOutput.cs index 42d3c48..dcedd21 100644 --- a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqMajorProjectTeacherSalary/MajorProjectTeacherSalaryOutput.cs +++ b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqMajorProjectTeacherSalary/MajorProjectTeacherSalaryOutput.cs @@ -18,12 +18,12 @@ namespace NCC.Extend.Entitys.Dto.LqMajorProjectTeacherSalary public string StatisticsMonth { get; set; } /// - /// 门店ID + /// 部门ID(使用StoreId字段存储部门ID) /// public string StoreId { get; set; } /// - /// 门店名称 + /// 部门名称(使用StoreName字段存储部门名称) /// public string StoreName { get; set; } diff --git a/netcore/src/Modularity/Extend/NCC.Extend/LqAssistantSalaryService.cs b/netcore/src/Modularity/Extend/NCC.Extend/LqAssistantSalaryService.cs index c46ff4e..a4d1f61 100644 --- a/netcore/src/Modularity/Extend/NCC.Extend/LqAssistantSalaryService.cs +++ b/netcore/src/Modularity/Extend/NCC.Extend/LqAssistantSalaryService.cs @@ -334,53 +334,18 @@ namespace NCC.Extend decimal performanceRatio = salary.StoreTotalPerformance / salary.StoreLifeline; // 根据岗位类型确定提成比例 - if (isDirector) + // 店助和店助主任使用相同的阶梯提成规则 + // 先计算总提成金额(阶梯计算) + decimal totalCommission = CalculateAssistantCommission(salary.StoreTotalPerformance, salary.StoreLifeline); + + // 计算平均提成比例(用于显示) + if (salary.StoreTotalPerformance > 0) { - // 店助主任:使用固定比例(按规则文档,业绩≥100%时使用阶梯提成,但为保持比例固定,使用平均比例) - // 业绩 < 70%:0% - // 70% ≤ 业绩 < 100%:0.4% - // 业绩 ≥ 100%:使用阶梯提成计算总提成,然后计算平均比例 - if (performanceRatio < 0.7m) - { - commissionRate = 0; - } - else if (performanceRatio < 1.0m) - { - commissionRate = 0.004m; // 0.4% - } - else - { - // 业绩 ≥ 100%:使用阶梯提成计算总提成,然后计算平均比例 - // ≤生命线部分:0.6%,>生命线部分:1% - decimal storeTotalCommission = CalculateDirectorCommission(salary.StoreTotalPerformance, salary.StoreLifeline); - if (salary.StoreTotalPerformance > 0) - { - commissionRate = storeTotalCommission / salary.StoreTotalPerformance; - } - else - { - commissionRate = 0; - } - } + commissionRate = totalCommission / salary.StoreTotalPerformance; } else { - // 店助:使用固定比例 - // 业绩 < 70%:0% - // 70% ≤ 业绩 < 100%:0.4% - // 业绩 ≥ 100%:0.6% - if (performanceRatio < 0.7m) - { - commissionRate = 0; - } - else if (performanceRatio < 1.0m) - { - commissionRate = 0.004m; // 0.4% - } - else - { - commissionRate = 0.006m; // 0.6% - } + commissionRate = 0; } } @@ -467,12 +432,15 @@ namespace NCC.Extend } // 2.11 按在店天数比例计算店助的提成和奖励 - // 逻辑:提成金额 = 门店业绩 × 提成比例 / 当月天数 × 在店天数 + // 逻辑:提成金额 = 门店总提成(阶梯计算) / 当月天数 × 在店天数 // 阶段奖励 = 门店总奖励 / 当月天数 × 在店天数 if (daysInMonth > 0 && workingDays > 0) { - // 按比例计算提成:门店业绩 × 提成比例 / 当月天数 × 在店天数 - salary.CommissionAmount = salary.StoreTotalPerformance * commissionRate / daysInMonth * workingDays; + // 先计算门店总提成(阶梯计算)- 店助和店助主任使用相同的规则 + decimal storeTotalCommission = CalculateAssistantCommission(salary.StoreTotalPerformance, salary.StoreLifeline); + + // 按比例计算提成:门店总提成 / 当月天数 × 在店天数 + salary.CommissionAmount = storeTotalCommission / daysInMonth * workingDays; // 按比例计算奖励 salary.StageRewardAmount = storeTotalStageReward / daysInMonth * workingDays; @@ -650,21 +618,18 @@ namespace NCC.Extend else { // 门店业绩 ≥ 门店生命线 × 100% → 阶梯提成 - // 70%以下部分:0% - // 70%-100%部分:0.4% - // ≤生命线部分:0.6%,>生命线部分:1% - decimal stage70 = storeLifeline * 0.7m; + // ≤生命线部分(整个生命线):0.6% + // >生命线部分:1% decimal stage100 = storeLifeline; - decimal performance70To100 = stage100 - stage70; - decimal performanceAbove100 = storePerformance - stage100; + decimal performanceUpToLifeline = stage100; // ≤生命线部分(整个生命线) + decimal performanceAbove100 = storePerformance - stage100; // >生命线部分 - // 70%-100%部分:0.4% - decimal commission70To100 = performance70To100 * 0.004m; - // ≤生命线部分(0-70%):0%,70%-100%部分:0.4%,已计算 + // ≤生命线部分:0.6% + decimal commissionUpToLifeline = performanceUpToLifeline * 0.006m; // >生命线部分:1% decimal commissionAbove100 = performanceAbove100 * 0.01m; // 1% - return commission70To100 + commissionAbove100; + return commissionUpToLifeline + commissionAbove100; } } } diff --git a/netcore/src/Modularity/Extend/NCC.Extend/LqCooperationCostService.cs b/netcore/src/Modularity/Extend/NCC.Extend/LqCooperationCostService.cs index f59cf88..d4c88ab 100644 --- a/netcore/src/Modularity/Extend/NCC.Extend/LqCooperationCostService.cs +++ b/netcore/src/Modularity/Extend/NCC.Extend/LqCooperationCostService.cs @@ -409,11 +409,12 @@ namespace NCC.Extend.LqCooperationCost /// Content-Type: multipart/form-data /// /// Excel文件 + /// 是否需要清理导入月份数据(默认:true,清理) /// 导入结果 /// 导入成功 /// 文件格式错误或数据验证失败 [HttpPost("Actions/Import")] - public async Task Import(IFormFile file) + public async Task Import(IFormFile file, bool clearBeforeImport = true) { try { @@ -433,6 +434,7 @@ namespace NCC.Extend.LqCooperationCost var successCount = 0; var failCount = 0; var failMessages = new List(); + var importData = new List<(string StoreId, int Year, string Month, decimal TotalAmount, string CostType, string Remarks)>(); // 保存临时文件 var tempFilePath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString() + Path.GetExtension(file.FileName)); @@ -517,44 +519,14 @@ namespace NCC.Extend.LqCooperationCost continue; } - // 检查是否已存在相同门店、年份、月份的记录 - var exists = await _db.Queryable() - .Where(x => x.StoreId == storeId && x.Year == year && x.Month == monthText && x.IsEffective == StatusEnum.有效.GetHashCode()) - .AnyAsync(); + // 注意:一个门店在同一个月份可以有多笔成本记录(包括相同成本类型、相同金额) + // 因为业务上可能需要记录多笔成本,比如多次合作项目、多次设备维护等 + // 统计时会按门店汇总所有记录的金额,所以不需要做重复检查 + // 如果用户需要避免重复导入,可以使用"清理导入月份数据"功能 - if (exists) - { - failMessages.Add($"第{i + 1}行:该门店{year}年{monthText}月的记录已存在"); - failCount++; - continue; - } - - // 创建记录 - var entity = new LqCooperationCostEntity - { - Id = YitIdHelper.NextId().ToString(), - StoreId = storeId, - StoreName = storeName, - Year = year, - Month = monthText, - TotalAmount = totalAmount, - CostType = costType, - Remarks = remarks, - IsEffective = StatusEnum.有效.GetHashCode(), - CreateUser = _userManager.UserId, - CreateTime = DateTime.Now - }; - - var isOk = await _db.Insertable(entity).ExecuteCommandAsync(); - if (isOk > 0) - { - successCount++; - } - else - { - failMessages.Add($"第{i + 1}行:保存失败"); - failCount++; - } + // 添加到导入数据列表 + importData.Add((storeId, year, monthText, totalAmount, costType, remarks)); + successCount++; } catch (Exception ex) { @@ -572,13 +544,71 @@ namespace NCC.Extend.LqCooperationCost } } + // 如果需要清理导入月份数据,先删除导入数据中所有涉及的年份+月份组合的数据 + if (clearBeforeImport && importData.Any()) + { + // 获取导入数据中所有唯一的年份+月份组合 + var yearMonthPairs = importData + .Select(x => new { x.Year, x.Month }) + .Distinct() + .ToList(); + + foreach (var pair in yearMonthPairs) + { + await _db.Deleteable() + .Where(x => x.Year == pair.Year && x.Month == pair.Month && x.IsEffective == StatusEnum.有效.GetHashCode()) + .ExecuteCommandAsync(); + } + } + + // 批量插入导入数据 + if (importData.Any()) + { + // 批量获取门店信息 + var storeIds = importData.Select(x => x.StoreId).Distinct().ToList(); + var stores = await _db.Queryable() + .Where(x => storeIds.Contains(x.Id)) + .Select(x => new { x.Id, x.Dm }) + .ToListAsync(); + var storeDict = stores.ToDictionary(x => x.Id, x => x.Dm); + + var entities = importData.Select(x => new LqCooperationCostEntity + { + Id = YitIdHelper.NextId().ToString(), + StoreId = x.StoreId, + StoreName = storeDict.ContainsKey(x.StoreId) ? storeDict[x.StoreId] : "", + Year = x.Year, + Month = x.Month, + TotalAmount = x.TotalAmount, + CostType = x.CostType, + Remarks = x.Remarks, + IsEffective = StatusEnum.有效.GetHashCode(), + CreateUser = _userManager.UserId, + CreateTime = DateTime.Now + }).ToList(); + + await _db.Insertable(entities).ExecuteCommandAsync(); + } + + // 构建返回结果,包含成功导入的记录的成本类型信息 + var successRecords = importData.Select(x => new + { + storeId = x.StoreId, + year = x.Year, + month = x.Month, + totalAmount = x.TotalAmount, + costType = x.CostType, + remarks = x.Remarks + }).ToList(); + return new { success = true, message = $"导入完成:成功{successCount}条,失败{failCount}条", successCount = successCount, failCount = failCount, - failMessages = failMessages + failMessages = failMessages, + successRecords = successRecords }; } catch (Exception ex) diff --git a/netcore/src/Modularity/Extend/NCC.Extend/LqMajorProjectDirectorSalaryService.cs b/netcore/src/Modularity/Extend/NCC.Extend/LqMajorProjectDirectorSalaryService.cs index 345ed02..3caeb68 100644 --- a/netcore/src/Modularity/Extend/NCC.Extend/LqMajorProjectDirectorSalaryService.cs +++ b/netcore/src/Modularity/Extend/NCC.Extend/LqMajorProjectDirectorSalaryService.cs @@ -7,7 +7,10 @@ using NCC.DynamicApiController; using NCC.Extend.Entitys.Dto.LqMajorProjectDirectorSalary; using NCC.Extend.Entitys.lq_attendance_summary; using NCC.Extend.Entitys.lq_hytk_hytk; +using NCC.Extend.Entitys.lq_hytk_mx; using NCC.Extend.Entitys.lq_kd_kdjlb; +using NCC.Extend.Entitys.lq_kd_pxmx; +using NCC.Extend.Entitys.lq_kd_jksyj; using NCC.Extend.Entitys.lq_md_target; using NCC.Extend.Entitys.lq_mdxx; using NCC.Extend.Entitys.lq_major_project_director_salary_statistics; @@ -214,19 +217,33 @@ namespace NCC.Extend { foreach (var storeId in allManagedStoreIds) { - // 该门店的开单金额 - var storeBillingAmount = await _db.Queryable() - .Where(x => x.IsEffective == 1 - && x.Djmd == storeId - && x.Kdrq >= startDate && x.Kdrq <= endDate.AddDays(1)) - .SumAsync(x => (decimal?)x.Sfyj) ?? 0m; - - // 该门店的退卡金额(优先使用ActualRefundAmount,如果没有则使用Tkje) - var storeRefundAmount = await _db.Queryable() - .Where(x => x.IsEffective == 1 - && x.Md == storeId - && x.Tksj >= startDate && x.Tksj <= endDate.AddDays(1)) - .SumAsync(x => (decimal?)(x.ActualRefundAmount ?? x.Tkje ?? 0)) ?? 0m; + // 该门店的开单金额(只统计医美类型的开单,从lq_kd_jksyj表统计医美品项的健康师业绩) + var storeBillingList = await _db.Queryable( + (jksyj, kdjlb) => new JoinQueryInfos( + JoinType.Inner, jksyj.Glkdbh == kdjlb.Id)) + .Where((jksyj, kdjlb) => + kdjlb.IsEffective == 1 + && jksyj.IsEffective == 1 + && kdjlb.Djmd == storeId + && kdjlb.Kdrq >= startDate && kdjlb.Kdrq <= endDate.AddDays(1) + && jksyj.ItemCategory == "医美" + && !string.IsNullOrEmpty(jksyj.Jksyj)) + .Select((jksyj, kdjlb) => jksyj.Jksyj) + .ToListAsync(); + var storeBillingAmount = storeBillingList + .Sum(x => decimal.TryParse(x, out var val) ? val : 0m); + + // 该门店的退卡金额(只统计医美类型的退卡,从lq_hytk_mx表统计医美品项的退卡金额) + var storeRefundAmount = await _db.Queryable( + (mx, hytk) => new JoinQueryInfos( + JoinType.Inner, mx.RefundInfoId == hytk.Id)) + .Where((mx, hytk) => + hytk.IsEffective == 1 + && mx.IsEffective == 1 + && hytk.Md == storeId + && hytk.Tksj >= startDate && hytk.Tksj <= endDate.AddDays(1) + && mx.ItemCategory == "医美") + .SumAsync((mx, hytk) => (decimal?)(mx.Tkje ?? 0)) ?? 0m; // 该门店的净总业绩 var storeTotalPerformance = storeBillingAmount - storeRefundAmount; diff --git a/netcore/src/Modularity/Extend/NCC.Extend/LqMajorProjectTeacherSalaryService.cs b/netcore/src/Modularity/Extend/NCC.Extend/LqMajorProjectTeacherSalaryService.cs index c2de6aa..4aebcb7 100644 --- a/netcore/src/Modularity/Extend/NCC.Extend/LqMajorProjectTeacherSalaryService.cs +++ b/netcore/src/Modularity/Extend/NCC.Extend/LqMajorProjectTeacherSalaryService.cs @@ -7,7 +7,10 @@ using NCC.DynamicApiController; using NCC.Extend.Entitys.Dto.LqMajorProjectTeacherSalary; using NCC.Extend.Entitys.lq_attendance_summary; using NCC.Extend.Entitys.lq_hytk_hytk; +using NCC.Extend.Entitys.lq_hytk_mx; using NCC.Extend.Entitys.lq_kd_kdjlb; +using NCC.Extend.Entitys.lq_kd_pxmx; +using NCC.Extend.Entitys.lq_kd_jksyj; using NCC.Extend.Entitys.lq_md_major_project_teacher_assignment; using NCC.Extend.Entitys.lq_md_xdbhsj; using NCC.Extend.Entitys.lq_mdxx; @@ -182,25 +185,40 @@ namespace NCC.Extend .ToDictionary(g => g.Key, g => g.First()); // 1.4 门店总业绩计算 (开单实付 - 退卡金额) - // 开单实付(从lq_kd_kdjlb表统计sfyj字段) - var storeBillingList = await _db.Queryable() - .Where(x => x.Kdrq >= startDate && x.Kdrq <= endDate.AddDays(1) && x.IsEffective == 1) - .Select(x => new { x.Djmd, x.Sfyj }) + // 开单实付(只统计医美类型的开单,从lq_kd_jksyj表统计医美品项的健康师业绩) + var storeBillingList = await _db.Queryable( + (jksyj, kdjlb) => new JoinQueryInfos( + JoinType.Inner, jksyj.Glkdbh == kdjlb.Id)) + .Where((jksyj, kdjlb) => + kdjlb.Kdrq >= startDate && + kdjlb.Kdrq <= endDate.AddDays(1) && + kdjlb.IsEffective == 1 && + jksyj.IsEffective == 1 && + jksyj.ItemCategory == "医美" && + !string.IsNullOrEmpty(jksyj.Jksyj)) + .Select((jksyj, kdjlb) => new { kdjlb.Djmd, Jksyj = jksyj.Jksyj }) .ToListAsync(); var storeBillingDict = storeBillingList .Where(x => !string.IsNullOrEmpty(x.Djmd)) .GroupBy(x => x.Djmd) - .ToDictionary(g => g.Key, g => g.Sum(x => x.Sfyj)); - - // 退卡金额(从lq_hytk_hytk表统计,使用F_ActualRefundAmount,如果没有则使用tkje) - var storeRefundList = await _db.Queryable() - .Where(x => x.Tksj >= startDate && x.Tksj <= endDate.AddDays(1) && x.IsEffective == 1) - .Select(x => new { x.Md, x.ActualRefundAmount, x.Tkje }) + .ToDictionary(g => g.Key, g => g.Sum(x => decimal.TryParse(x.Jksyj, out var val) ? val : 0m)); + + // 退卡金额(只统计医美类型的退卡,从lq_hytk_mx表统计医美品项的退卡金额) + var storeRefundList = await _db.Queryable( + (mx, hytk) => new JoinQueryInfos( + JoinType.Inner, mx.RefundInfoId == hytk.Id)) + .Where((mx, hytk) => + hytk.Tksj >= startDate && + hytk.Tksj <= endDate.AddDays(1) && + hytk.IsEffective == 1 && + mx.IsEffective == 1 && + mx.ItemCategory == "医美") + .Select((mx, hytk) => new { hytk.Md, mx.Tkje }) .ToListAsync(); var storeRefundDict = storeRefundList .Where(x => !string.IsNullOrEmpty(x.Md)) .GroupBy(x => x.Md) - .ToDictionary(g => g.Key, g => g.Sum(x => x.ActualRefundAmount ?? x.Tkje ?? 0)); + .ToDictionary(g => g.Key, g => g.Sum(x => x.Tkje ?? 0)); // 1.5 考勤数据 (lq_attendance_summary) var attendanceList = await _db.Queryable() @@ -212,10 +230,18 @@ namespace NCC.Extend var teacherIds = assignmentList.Select(x => x.TeacherId).Where(x => !string.IsNullOrEmpty(x)).Distinct().ToList(); var userList = await _db.Queryable() .Where(x => teacherIds.Contains(x.Id)) - .Select(x => new { x.Id, x.RealName, x.Account, x.IsOnJob }) + .Select(x => new { x.Id, x.RealName, x.Account, x.IsOnJob, x.OrganizeId }) .ToListAsync(); var userDict = userList.ToDictionary(x => x.Id, x => x); + // 1.7 获取部门信息 (BASE_ORGANIZE) + var organizeIds = userList.Where(x => !string.IsNullOrEmpty(x.OrganizeId)).Select(x => x.OrganizeId).Distinct().ToList(); + var organizeList = await _db.Queryable() + .Where(x => organizeIds.Contains(x.Id)) + .Select(x => new { x.Id, x.FullName }) + .ToListAsync(); + var organizeDict = organizeList.ToDictionary(x => x.Id, x => x); + // 2. 按大项目部老师聚合数据 var teacherStats = new Dictionary(); @@ -268,35 +294,29 @@ namespace NCC.Extend salary.EmployeeName = user.RealName ?? ""; salary.EmployeeAccount = user.Account ?? ""; salary.IsTerminated = user.IsOnJob == 0 ? 1 : 0; - } - // 2.3 填充门店信息 - if (storeDict.ContainsKey(storeId)) - { - var store = storeDict[storeId]; - salary.StoreId = storeId; - salary.StoreName = store.Dm ?? ""; - salary.StoreType = store.StoreType; - salary.StoreCategory = store.StoreCategory; + // 2.3 填充部门信息(使用StoreId和StoreName字段存储部门信息) + if (!string.IsNullOrEmpty(user.OrganizeId) && organizeDict.ContainsKey(user.OrganizeId)) + { + var organize = organizeDict[user.OrganizeId]; + salary.StoreId = user.OrganizeId; // 存储部门ID + salary.StoreName = organize.FullName ?? ""; // 存储部门名称 + } + else + { + salary.StoreId = user.OrganizeId ?? ""; + salary.StoreName = ""; + } } else { - salary.StoreId = storeId; + salary.StoreId = ""; salary.StoreName = ""; } - // 2.4 新店保护信息 - if (!string.IsNullOrEmpty(salary.StoreId) && newStoreProtectionDict.ContainsKey(salary.StoreId)) - { - var protection = newStoreProtectionDict[salary.StoreId]; - salary.IsNewStore = "是"; - salary.NewStoreProtectionStage = protection.Stage; - } - else - { - salary.IsNewStore = "否"; - salary.NewStoreProtectionStage = 0; - } + // 2.4 新店保护信息(不再使用,因为现在存储的是部门信息) + salary.IsNewStore = "否"; + salary.NewStoreProtectionStage = 0; // 2.5 统计门店总业绩(开单业绩 - 退卡业绩) var billing = storeBillingDict.ContainsKey(storeId) ? storeBillingDict[storeId] : 0; diff --git a/netcore/src/Modularity/Extend/NCC.Extend/LqSalaryExtraCalculationService.cs b/netcore/src/Modularity/Extend/NCC.Extend/LqSalaryExtraCalculationService.cs index d59f08a..4a7b3a2 100644 --- a/netcore/src/Modularity/Extend/NCC.Extend/LqSalaryExtraCalculationService.cs +++ b/netcore/src/Modularity/Extend/NCC.Extend/LqSalaryExtraCalculationService.cs @@ -106,9 +106,10 @@ namespace NCC.Extend /// 从Excel导入健康师工资额外计算数据 /// /// Excel文件 + /// 是否需要清理导入月份数据(默认:true,清理) /// 导入结果 [HttpPost("ImportFromExcel")] - public async Task ImportFromExcel(IFormFile file) + public async Task ImportFromExcel(IFormFile file, bool clearBeforeImport = true) { try { @@ -277,6 +278,23 @@ namespace NCC.Extend throw NCCException.Oh("Excel文件中没有有效的数据行"); } + // 如果需要清理导入月份数据,先删除导入数据中所有涉及的年份+月份组合的数据 + if (clearBeforeImport) + { + // 获取导入数据中所有唯一的年份+月份组合 + var yearMonthPairs = importData + .Select(x => new { x.Year, x.Month }) + .Distinct() + .ToList(); + + foreach (var pair in yearMonthPairs) + { + await _db.Deleteable() + .Where(x => x.Year == pair.Year && x.Month == pair.Month) + .ExecuteCommandAsync(); + } + } + // 处理导入数据 return await ProcessImportData(importData); } diff --git a/netcore/src/Modularity/Extend/NCC.Extend/LqShareStatisticsHqService.cs b/netcore/src/Modularity/Extend/NCC.Extend/LqShareStatisticsHqService.cs index 1addbd2..971f99e 100644 --- a/netcore/src/Modularity/Extend/NCC.Extend/LqShareStatisticsHqService.cs +++ b/netcore/src/Modularity/Extend/NCC.Extend/LqShareStatisticsHqService.cs @@ -8,6 +8,8 @@ using NCC.Extend.Entitys.lq_kd_kdjlb; using NCC.Extend.Entitys.lq_hytk_hytk; using NCC.Extend.Entitys.lq_kd_pxmx; using NCC.Extend.Entitys.lq_contract_rent_detail; +using NCC.Extend.Entitys.lq_contract; +using NCC.Extend.Entitys; using SqlSugar; using System; using System.Linq; @@ -151,23 +153,64 @@ namespace NCC.Extend /// private async Task CalculateCost(LqShareStatisticsHqEntity entity, DateTime startDate, DateTime endDate, string statisticsMonth) { - // 1. 成本-报销 (TODO: 需要确认总部报销的判定方式) - entity.CostReimbursement = 0; + // 1. 成本-报销:筛选一级分类为"总部费用"的申请,审批状态为"已通过" + var reimbursementAmount = await _db.Queryable( + (app, purchase, category) => app.PurchaseRecordsId == purchase.Id && purchase.ReimbursementCategoryId == category.Id) + .Where((app, purchase, category) => + category.Level1Name == "总部费用" + && app.ApprovalStatus == "已通过" + && app.ApplicationTime >= startDate + && app.ApplicationTime <= endDate.AddDays(1)) + .SumAsync((app, purchase, category) => purchase.Amount); + entity.CostReimbursement = reimbursementAmount; // 2. 成本-人工 (保留) entity.CostLabor = 0; // 3. 成本-教育部房租 - // TODO: 需要确认如何识别教育部合同 - entity.CostEducationRent = 0; + // 从合同月租明细表获取:门店是总部,分类是教育,统计月份匹配 + // 使用月份字符串匹配,避免时区问题 + var educationRent = await _db.Ado.SqlQuerySingleAsync( + $@"SELECT COALESCE(SUM(d.F_DueAmount), 0) + FROM lq_contract_rent_detail d + INNER JOIN lq_contract c ON d.F_ContractId = c.F_Id + WHERE c.F_StoreId = '1649328471923847168' + AND c.F_Category = '教育' + AND DATE_FORMAT(d.F_PaymentMonth, '%Y%m') = @StatisticsMonth + AND c.F_IsEffective = 1 + AND d.F_IsEffective = 1", + new { StatisticsMonth = statisticsMonth }); + entity.CostEducationRent = educationRent; // 4. 成本-仓库房租 - // TODO: 需要确认如何识别仓库合同 - entity.CostWarehouseRent = 0; + // 从合同月租明细表获取:门店是总部,分类是仓库,统计月份匹配 + // 使用月份字符串匹配,避免时区问题 + var warehouseRent = await _db.Ado.SqlQuerySingleAsync( + $@"SELECT COALESCE(SUM(d.F_DueAmount), 0) + FROM lq_contract_rent_detail d + INNER JOIN lq_contract c ON d.F_ContractId = c.F_Id + WHERE c.F_StoreId = '1649328471923847168' + AND c.F_Category = '仓库' + AND DATE_FORMAT(d.F_PaymentMonth, '%Y%m') = @StatisticsMonth + AND c.F_IsEffective = 1 + AND d.F_IsEffective = 1", + new { StatisticsMonth = statisticsMonth }); + entity.CostWarehouseRent = warehouseRent; // 5. 成本-总部房租 - // TODO: 需要确认如何识别总部合同 - entity.CostHQRent = 0; + // 从合同月租明细表获取:门店是总部,分类是总部,统计月份匹配 + // 使用月份字符串匹配,避免时区问题 + var hqRent = await _db.Ado.SqlQuerySingleAsync( + $@"SELECT COALESCE(SUM(d.F_DueAmount), 0) + FROM lq_contract_rent_detail d + INNER JOIN lq_contract c ON d.F_ContractId = c.F_Id + WHERE c.F_StoreId = '1649328471923847168' + AND c.F_Category = '总部' + AND DATE_FORMAT(d.F_PaymentMonth, '%Y%m') = @StatisticsMonth + AND c.F_IsEffective = 1 + AND d.F_IsEffective = 1", + new { StatisticsMonth = statisticsMonth }); + entity.CostHQRent = hqRent; } /// diff --git a/netcore/src/Modularity/Extend/NCC.Extend/Utils/WeChatBotService.cs b/netcore/src/Modularity/Extend/NCC.Extend/Utils/WeChatBotService.cs index 8d62fe3..b61edc8 100644 --- a/netcore/src/Modularity/Extend/NCC.Extend/Utils/WeChatBotService.cs +++ b/netcore/src/Modularity/Extend/NCC.Extend/Utils/WeChatBotService.cs @@ -25,7 +25,7 @@ namespace NCC.Extend.Utils _httpClient = httpClient; // 从配置文件中读取企业微信机器人配置 - _botApiUrl = App.Configuration["WeChatBot:BotApiUrl"] ?? "http://wx.lvqianmeiye.com/api/Bot/send-text"; + _botApiUrl = App.Configuration["WeChatBot:BotApiUrl"] ?? "https://wx.lvqianmeiye.com/api/Bot/send-text"; // 从配置文件中读取Webhook地址(正式或测试地址,通过配置文件切换) _webhookUrl = App.Configuration["WeChatBot:WebhookUrl"]; diff --git a/sql/排查生美业绩统计差异-简化版.sql b/sql/排查生美业绩统计差异-简化版.sql index ab20c6b..80fc2a2 100644 --- a/sql/排查生美业绩统计差异-简化版.sql +++ b/sql/排查生美业绩统计差异-简化版.sql @@ -124,3 +124,5 @@ ORDER BY 生美业绩 DESC; + + diff --git a/sql/排查生美业绩统计差异详细.sql b/sql/排查生美业绩统计差异详细.sql index 2342e33..92502d8 100644 --- a/sql/排查生美业绩统计差异详细.sql +++ b/sql/排查生美业绩统计差异详细.sql @@ -176,3 +176,5 @@ HAVING COUNT(*) > 1; + + diff --git a/sql/检查生美业绩统计差异.sql b/sql/检查生美业绩统计差异.sql index b6b0e02..4e362bb 100644 --- a/sql/检查生美业绩统计差异.sql +++ b/sql/检查生美业绩统计差异.sql @@ -146,3 +146,5 @@ ORDER BY 生美业绩 DESC; + + diff --git a/test_tianwang_api.py b/test_tianwang_api.py index a6c559b..e631863 100644 --- a/test_tianwang_api.py +++ b/test_tianwang_api.py @@ -77,3 +77,5 @@ print(f"\n教育一部+教育二部合计 BillingPerformance: {total2}") + + -- libgit2 0.21.4