Commit 087f6aff2c3580bfbfcabbec3fb558c23c9cbd30

Authored by “wangming”
1 parent 6bac91b2

修复科技部总经理Cell金额计算问题及其他优化

- 修复科技部总经理工资计算中Cell金额计算逻辑
- 更新主任工资相关服务和前端页面
- 优化科技部老师工资计算逻辑
- 更新客户信息服务和报销申请服务
- 修复系统映射器和权限模型
- 完善工资管理前端页面显示
antis-ncc-admin/src/views/wageManagement/director-detail-dialog.vue
... ... @@ -288,7 +288,7 @@ export default {
288 288 getFieldsByCategory(category) {
289 289 if (!this.detailData) return []
290 290  
291   - const excludeFields = ['StoreName', 'EmployeeName', 'Position', 'Id', 'CreateTime', 'UpdateTime', 'CreateUser', 'UpdateUser', 'StatisticsMonth', 'GoldTriangleTeam', 'IsNewStore', 'NewStoreProtectionStage', 'IsLocked']
  291 + const excludeFields = ['StoreName', 'EmployeeName', 'Position', 'Id', 'CreateTime', 'UpdateTime', 'CreateUser', 'UpdateUser', 'StatisticsMonth', 'GoldTriangleTeam', 'IsNewStore', 'NewStoreProtectionStage', 'IsLocked', 'StoreId', 'EmployeeId', 'SalesPerformance']
292 292  
293 293 const categoryMap = {
294 294 performance: ['Performance', 'Lifeline', 'CompletionRate', 'Reached', 'HeadCount', 'Target', 'Consume'],
... ... @@ -356,10 +356,11 @@ export default {
356 356 const moneyFields = ['performance', 'commission', 'salary', 'subsidy', 'deduction', 'amount', 'fee', 'bonus', 'deposit', 'supplement', 'payment', 'consumption', 'reward', 'handwork', 'gross', 'guaranteed', 'transportation', 'allowance', 'total']
357 357 const percentFields = ['point', 'rate', 'percentage', 'percent']
358 358  
359   - if (moneyFields.some(field => lowerKey.includes(field)) && !lowerKey.includes('status')) {
360   - return 'money'
361   - } else if (percentFields.some(field => lowerKey.includes(field))) {
  359 + // 注意:先检查百分比字段,避免 CommissionRate 等字段被误识别为金额
  360 + if (percentFields.some(field => lowerKey.includes(field))) {
362 361 return 'percent'
  362 + } else if (moneyFields.some(field => lowerKey.includes(field)) && !lowerKey.includes('status')) {
  363 + return 'money'
363 364 }
364 365 return 'text'
365 366 },
... ... @@ -371,6 +372,12 @@ export default {
371 372 'StoreTotalPerformance': '门店总业绩',
372 373 'StoreBillingPerformance': '门店开单业绩',
373 374 'StoreRefundPerformance': '门店退卡业绩',
  375 + 'SalesPerformance': '销售业绩',
  376 + 'ProductMaterial': '产品物料',
  377 + 'CooperationCost': '合作项目成本',
  378 + 'StoreExpense': '店内支出',
  379 + 'LaundryCost': '洗毛巾费用',
  380 + 'GrossProfit': '毛利',
374 381 'StoreLifeline': '门店生命线',
375 382 'PerformanceCompletionRate': '业绩完成率',
376 383 'TotalPerformance': '总业绩',
... ... @@ -456,7 +463,13 @@ export default {
456 463 'StoreType': '门店类型',
457 464 'StoreCategory': '门店类别',
458 465 'IsNewStore': '是否新店',
459   - 'NewStoreProtectionStage': '新店保护阶段'
  466 + 'NewStoreProtectionStage': '新店保护阶段',
  467 + 'StoreId': '门店ID',
  468 + 'EmployeeId': '员工ID',
  469 + 'StatisticsMonth': '统计月份',
  470 + 'EmployeeConfirmStatus': '确认状态',
  471 + 'EmployeeConfirmTime': '员工确认时间',
  472 + 'EmployeeConfirmRemark': '员工确认备注'
460 473 }
461 474  
462 475 // 如果映射中存在,直接返回中文
... ... @@ -571,14 +584,24 @@ export default {
571 584 return highlightFields.includes(key)
572 585 },
573 586 // 获取字段顺序
  587 + // 注意:毛利(GrossProfit)需要放在生命线提成比例(CommissionRateBelowLifeline)前面
574 588 getFieldOrder(key) {
575 589 const orderMap = {
576 590 'TotalPerformance': 1,
577 591 'BasePerformance': 2,
578 592 'CooperationPerformance': 3,
579   - 'TotalCommission': 10,
580   - 'BasePerformanceCommission': 11,
581   - 'CooperationPerformanceCommission': 12,
  593 + 'StoreTotalPerformance': 4,
  594 + 'SalesPerformance': 5,
  595 + 'GrossProfit': 6, // 毛利放在生命线提成比例前面
  596 + 'StoreLifeline': 7,
  597 + 'CommissionRateBelowLifeline': 8, // 生命线提成比例
  598 + 'CommissionRateAboveLifeline': 9,
  599 + 'CommissionAmountBelowLifeline': 10,
  600 + 'CommissionAmountAboveLifeline': 11,
  601 + 'TotalCommissionAmount': 12,
  602 + 'TotalCommission': 13,
  603 + 'BasePerformanceCommission': 14,
  604 + 'CooperationPerformanceCommission': 15,
582 605 'FinalGrossSalary': 20,
583 606 'ActualSalary': 21,
584 607 'TotalSubsidy': 30,
... ...
antis-ncc-admin/src/views/wageManagement/director.vue
... ... @@ -418,7 +418,7 @@ export default {
418 418 }
419 419  
420 420 const columns = {}
421   - const excludeFields = ['StoreName', 'EmployeeName', 'Position', 'Id', 'CreateTime', 'UpdateTime', 'CreateUser', 'UpdateUser', 'StatisticsMonth']
  421 + const excludeFields = ['StoreName', 'EmployeeName', 'Position', 'Id', 'CreateTime', 'UpdateTime', 'CreateUser', 'UpdateUser', 'StatisticsMonth', 'StoreId', 'EmployeeId', 'SalesPerformance']
422 422  
423 423 // 金额字段关键词
424 424 const moneyFields = ['Performance', 'Commission', 'Salary', 'Subsidy', 'Deduction', 'Amount', 'Fee', 'Bonus', 'Deposit', 'Supplement', 'Payment', 'Consumption', 'Reward', 'Handwork', 'Gross', 'Guaranteed', 'Transportation', 'Allowance', 'Total']
... ... @@ -426,8 +426,12 @@ export default {
426 426 const percentFields = ['Point', 'Rate', 'Percentage', 'Percent']
427 427  
428 428 // 定义字段顺序(重要字段优先)
  429 + // 注意:毛利(GrossProfit)需要放在生命线提成比例(CommissionRateBelowLifeline)前面
429 430 const fieldOrder = [
430 431 'GoldTriangleTeam', 'TotalPerformance', 'BasePerformance', 'CooperationPerformance',
  432 + 'StoreTotalPerformance', 'GrossProfit',
  433 + 'StoreLifeline', 'CommissionRateBelowLifeline', 'CommissionRateAboveLifeline',
  434 + 'CommissionAmountBelowLifeline', 'CommissionAmountAboveLifeline', 'TotalCommissionAmount',
431 435 'TotalCommission', 'BasePerformanceCommission', 'CooperationPerformanceCommission',
432 436 'FinalGrossSalary', 'ActualSalary', 'TotalSubsidy', 'TotalDeduction',
433 437 'IsLocked', 'IsNewStore', 'NewStoreProtectionStage'
... ... @@ -487,11 +491,12 @@ export default {
487 491 const moneyFields = ['performance', 'commission', 'salary', 'subsidy', 'deduction', 'amount', 'fee', 'bonus', 'deposit', 'supplement', 'payment', 'consumption', 'reward', 'handwork', 'gross', 'guaranteed', 'transportation', 'allowance', 'total']
488 492 const percentFields = ['point', 'rate', 'percentage', 'percent']
489 493  
490   - // 注意:payment 需要排除 PaymentStatus 这种状态字段
491   - if (moneyFields.some(field => lowerKey.includes(field)) && !lowerKey.includes('status')) {
492   - return 'money'
493   - } else if (percentFields.some(field => lowerKey.includes(field))) {
  494 + // 注意:先检查百分比字段,避免 CommissionRate 等字段被误识别为金额
  495 + // payment 需要排除 PaymentStatus 这种状态字段
  496 + if (percentFields.some(field => lowerKey.includes(field))) {
494 497 return 'percent'
  498 + } else if (moneyFields.some(field => lowerKey.includes(field)) && !lowerKey.includes('status')) {
  499 + return 'money'
495 500 }
496 501 return 'text'
497 502 },
... ... @@ -504,6 +509,12 @@ export default {
504 509 'StoreTotalPerformance': '门店总业绩',
505 510 'StoreBillingPerformance': '门店开单业绩',
506 511 'StoreRefundPerformance': '门店退卡业绩',
  512 + 'SalesPerformance': '销售业绩',
  513 + 'ProductMaterial': '产品物料',
  514 + 'CooperationCost': '合作项目成本',
  515 + 'StoreExpense': '店内支出',
  516 + 'LaundryCost': '洗毛巾费用',
  517 + 'GrossProfit': '毛利',
507 518 'StoreLifeline': '门店生命线',
508 519 'PerformanceCompletionRate': '业绩完成率',
509 520 'TotalPerformance': '总业绩',
... ... @@ -584,7 +595,13 @@ export default {
584 595 'StoreType': '门店类型',
585 596 'StoreCategory': '门店类别',
586 597 'IsNewStore': '是否新店',
587   - 'NewStoreProtectionStage': '新店保护阶段'
  598 + 'NewStoreProtectionStage': '新店保护阶段',
  599 + 'StoreId': '门店ID',
  600 + 'EmployeeId': '员工ID',
  601 + 'StatisticsMonth': '统计月份',
  602 + 'EmployeeConfirmStatus': '确认状态',
  603 + 'EmployeeConfirmTime': '员工确认时间',
  604 + 'EmployeeConfirmRemark': '员工确认备注'
588 605 }
589 606  
590 607 return labelMap[key] || key
... ...
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqDirectorSalary/DirectorSalaryOutput.cs
... ... @@ -236,6 +236,121 @@ namespace NCC.Extend.Entitys.Dto.LqDirectorSalary
236 236 /// 新店保护阶段
237 237 /// </summary>
238 238 public int NewStoreProtectionStage { get; set; }
  239 +
  240 + /// <summary>
  241 + /// 门店ID
  242 + /// </summary>
  243 + public string StoreId { get; set; }
  244 +
  245 + /// <summary>
  246 + /// 员工ID
  247 + /// </summary>
  248 + public string EmployeeId { get; set; }
  249 +
  250 + /// <summary>
  251 + /// 统计月份(YYYYMM)
  252 + /// </summary>
  253 + public string StatisticsMonth { get; set; }
  254 +
  255 + /// <summary>
  256 + /// 销售业绩(开单业绩-退款业绩)
  257 + /// </summary>
  258 + public decimal SalesPerformance { get; set; }
  259 +
  260 + /// <summary>
  261 + /// 产品物料(仓库领用金额)
  262 + /// </summary>
  263 + public decimal ProductMaterial { get; set; }
  264 +
  265 + /// <summary>
  266 + /// 合作项目成本
  267 + /// </summary>
  268 + public decimal CooperationCost { get; set; }
  269 +
  270 + /// <summary>
  271 + /// 店内支出
  272 + /// </summary>
  273 + public decimal StoreExpense { get; set; }
  274 +
  275 + /// <summary>
  276 + /// 洗毛巾费用
  277 + /// </summary>
  278 + public decimal LaundryCost { get; set; }
  279 +
  280 + /// <summary>
  281 + /// 毛利(销售业绩-产品物料-合作项目成本-店内支出-洗毛巾)
  282 + /// </summary>
  283 + public decimal GrossProfit { get; set; }
  284 +
  285 + /// <summary>
  286 + /// 缺卡扣款
  287 + /// </summary>
  288 + public decimal MissingCard { get; set; }
  289 +
  290 + /// <summary>
  291 + /// 迟到扣款
  292 + /// </summary>
  293 + public decimal LateArrival { get; set; }
  294 +
  295 + /// <summary>
  296 + /// 请假扣款
  297 + /// </summary>
  298 + public decimal LeaveDeduction { get; set; }
  299 +
  300 + /// <summary>
  301 + /// 扣社保
  302 + /// </summary>
  303 + public decimal SocialInsuranceDeduction { get; set; }
  304 +
  305 + /// <summary>
  306 + /// 扣除奖励
  307 + /// </summary>
  308 + public decimal RewardDeduction { get; set; }
  309 +
  310 + /// <summary>
  311 + /// 扣住宿费
  312 + /// </summary>
  313 + public decimal AccommodationDeduction { get; set; }
  314 +
  315 + /// <summary>
  316 + /// 扣学习期费用
  317 + /// </summary>
  318 + public decimal StudyPeriodDeduction { get; set; }
  319 +
  320 + /// <summary>
  321 + /// 扣工作服费用
  322 + /// </summary>
  323 + public decimal WorkClothesDeduction { get; set; }
  324 +
  325 + /// <summary>
  326 + /// 当月培训补贴
  327 + /// </summary>
  328 + public decimal MonthlyTrainingSubsidy { get; set; }
  329 +
  330 + /// <summary>
  331 + /// 当月交通补贴
  332 + /// </summary>
  333 + public decimal MonthlyTransportSubsidy { get; set; }
  334 +
  335 + /// <summary>
  336 + /// 上月培训补贴
  337 + /// </summary>
  338 + public decimal LastMonthTrainingSubsidy { get; set; }
  339 +
  340 + /// <summary>
  341 + /// 上月交通补贴
  342 + /// </summary>
  343 + public decimal LastMonthTransportSubsidy { get; set; }
  344 +
  345 + /// <summary>
  346 + /// 员工确认时间
  347 + /// </summary>
  348 + public DateTime? EmployeeConfirmTime { get; set; }
  349 +
  350 + /// <summary>
  351 + /// 员工确认备注
  352 + /// </summary>
  353 + public string EmployeeConfirmRemark { get; set; }
239 354 }
240 355 }
241 356  
... ...
netcore/src/Modularity/Extend/NCC.Extend/LqDirectorSalaryService.cs
... ... @@ -80,12 +80,24 @@ namespace NCC.Extend
80 80 var list = await query.Select(x => new DirectorSalaryOutput
81 81 {
82 82 Id = x.Id,
  83 + StoreId = x.StoreId,
83 84 StoreName = x.StoreName,
  85 + EmployeeId = x.EmployeeId,
84 86 EmployeeName = x.EmployeeName,
85 87 Position = x.Position,
  88 + StatisticsMonth = x.StatisticsMonth,
  89 + StoreType = x.StoreType,
  90 + StoreCategory = x.StoreCategory,
  91 + IsNewStore = x.IsNewStore,
  92 + NewStoreProtectionStage = x.NewStoreProtectionStage,
86 93 StoreTotalPerformance = x.StoreTotalPerformance,
87 94 StoreBillingPerformance = x.StoreBillingPerformance,
88 95 StoreRefundPerformance = x.StoreRefundPerformance,
  96 + ProductMaterial = x.ProductMaterial,
  97 + CooperationCost = x.CooperationCost,
  98 + StoreExpense = x.StoreExpense,
  99 + LaundryCost = x.LaundryCost,
  100 + GrossProfit = x.GrossProfit,
89 101 StoreLifeline = x.StoreLifeline,
90 102 PerformanceCompletionRate = x.PerformanceCompletionRate,
91 103 PerformanceReached = x.PerformanceReached,
... ... @@ -108,7 +120,19 @@ namespace NCC.Extend
108 120 LeaveDays = x.LeaveDays,
109 121 GrossSalary = x.GrossSalary,
110 122 ActualSalary = x.ActualSalary,
  123 + MissingCard = x.MissingCard,
  124 + LateArrival = x.LateArrival,
  125 + LeaveDeduction = x.LeaveDeduction,
  126 + SocialInsuranceDeduction = x.SocialInsuranceDeduction,
  127 + RewardDeduction = x.RewardDeduction,
  128 + AccommodationDeduction = x.AccommodationDeduction,
  129 + StudyPeriodDeduction = x.StudyPeriodDeduction,
  130 + WorkClothesDeduction = x.WorkClothesDeduction,
111 131 TotalDeduction = x.TotalDeduction,
  132 + MonthlyTrainingSubsidy = x.MonthlyTrainingSubsidy,
  133 + MonthlyTransportSubsidy = x.MonthlyTransportSubsidy,
  134 + LastMonthTrainingSubsidy = x.LastMonthTrainingSubsidy,
  135 + LastMonthTransportSubsidy = x.LastMonthTransportSubsidy,
112 136 TotalSubsidy = x.TotalSubsidy,
113 137 Bonus = x.Bonus,
114 138 ReturnPhoneDeposit = x.ReturnPhoneDeposit,
... ... @@ -120,11 +144,9 @@ namespace NCC.Extend
120 144 MonthlyTotalPayment = x.MonthlyTotalPayment,
121 145 IsLocked = x.IsLocked,
122 146 EmployeeConfirmStatus = x.EmployeeConfirmStatus,
123   - UpdateTime = x.UpdateTime,
124   - StoreType = x.StoreType,
125   - StoreCategory = x.StoreCategory,
126   - IsNewStore = x.IsNewStore,
127   - NewStoreProtectionStage = x.NewStoreProtectionStage
  147 + EmployeeConfirmTime = x.EmployeeConfirmTime,
  148 + EmployeeConfirmRemark = x.EmployeeConfirmRemark,
  149 + UpdateTime = x.UpdateTime
128 150 })
129 151 .ToPagedListAsync(input.currentPage, input.pageSize);
130 152  
... ... @@ -147,36 +169,72 @@ namespace NCC.Extend
147 169 .Select(x => new DirectorSalaryOutput
148 170 {
149 171 Id = x.Id,
  172 + StoreId = x.StoreId,
150 173 StoreName = x.StoreName,
  174 + EmployeeId = x.EmployeeId,
151 175 EmployeeName = x.EmployeeName,
152 176 Position = x.Position,
  177 + StatisticsMonth = x.StatisticsMonth,
  178 + StoreType = x.StoreType,
  179 + StoreCategory = x.StoreCategory,
  180 + IsNewStore = x.IsNewStore,
  181 + NewStoreProtectionStage = x.NewStoreProtectionStage,
153 182 StoreTotalPerformance = x.StoreTotalPerformance,
154 183 StoreBillingPerformance = x.StoreBillingPerformance,
155 184 StoreRefundPerformance = x.StoreRefundPerformance,
  185 + ProductMaterial = x.ProductMaterial,
  186 + CooperationCost = x.CooperationCost,
  187 + StoreExpense = x.StoreExpense,
  188 + LaundryCost = x.LaundryCost,
  189 + GrossProfit = x.GrossProfit,
156 190 StoreLifeline = x.StoreLifeline,
157 191 PerformanceCompletionRate = x.PerformanceCompletionRate,
  192 + PerformanceReached = x.PerformanceReached,
  193 + HeadCountReached = x.HeadCountReached,
  194 + ConsumeReached = x.ConsumeReached,
  195 + AssessmentDeduction = x.AssessmentDeduction,
  196 + UnreachedIndicatorCount = x.UnreachedIndicatorCount,
  197 + HeadCount = x.HeadCount,
  198 + TargetHeadCount = x.TargetHeadCount,
  199 + StoreConsume = x.StoreConsume,
  200 + TargetConsume = x.TargetConsume,
158 201 CommissionRateBelowLifeline = x.CommissionRateBelowLifeline,
159 202 CommissionRateAboveLifeline = x.CommissionRateAboveLifeline,
160 203 CommissionAmountBelowLifeline = x.CommissionAmountBelowLifeline,
161 204 CommissionAmountAboveLifeline = x.CommissionAmountAboveLifeline,
162 205 TotalCommissionAmount = x.TotalCommissionAmount,
163 206 BaseSalary = x.BaseSalary,
  207 + ActualBaseSalary = x.ActualBaseSalary,
164 208 WorkingDays = x.WorkingDays,
165 209 LeaveDays = x.LeaveDays,
166 210 GrossSalary = x.GrossSalary,
167 211 ActualSalary = x.ActualSalary,
  212 + MissingCard = x.MissingCard,
  213 + LateArrival = x.LateArrival,
  214 + LeaveDeduction = x.LeaveDeduction,
  215 + SocialInsuranceDeduction = x.SocialInsuranceDeduction,
  216 + RewardDeduction = x.RewardDeduction,
  217 + AccommodationDeduction = x.AccommodationDeduction,
  218 + StudyPeriodDeduction = x.StudyPeriodDeduction,
  219 + WorkClothesDeduction = x.WorkClothesDeduction,
168 220 TotalDeduction = x.TotalDeduction,
  221 + MonthlyTrainingSubsidy = x.MonthlyTrainingSubsidy,
  222 + MonthlyTransportSubsidy = x.MonthlyTransportSubsidy,
  223 + LastMonthTrainingSubsidy = x.LastMonthTrainingSubsidy,
  224 + LastMonthTransportSubsidy = x.LastMonthTransportSubsidy,
169 225 TotalSubsidy = x.TotalSubsidy,
170 226 Bonus = x.Bonus,
  227 + ReturnPhoneDeposit = x.ReturnPhoneDeposit,
  228 + ReturnAccommodationDeposit = x.ReturnAccommodationDeposit,
171 229 MonthlyPaymentStatus = x.MonthlyPaymentStatus,
172 230 PaidAmount = x.PaidAmount,
173 231 PendingAmount = x.PendingAmount,
  232 + LastMonthSupplement = x.LastMonthSupplement,
  233 + MonthlyTotalPayment = x.MonthlyTotalPayment,
174 234 IsLocked = x.IsLocked,
175 235 EmployeeConfirmStatus = x.EmployeeConfirmStatus,
176   - StoreType = x.StoreType,
177   - StoreCategory = x.StoreCategory,
178   - IsNewStore = x.IsNewStore,
179   - NewStoreProtectionStage = x.NewStoreProtectionStage,
  236 + EmployeeConfirmTime = x.EmployeeConfirmTime,
  237 + EmployeeConfirmRemark = x.EmployeeConfirmRemark,
180 238 UpdateTime = x.UpdateTime
181 239 })
182 240 .FirstAsync();
... ... @@ -478,8 +536,8 @@ namespace NCC.Extend
478 536 // 毛利 = 销售业绩 - 产品物料 - 合作项目成本 - 店内支出 - 洗毛巾
479 537 salary.GrossProfit = salary.SalesPerformance - salary.ProductMaterial - salary.CooperationCost - salary.StoreExpense - salary.LaundryCost;
480 538  
481   - // 2.7 将毛利赋值给StoreTotalPerformance(用于提成计算)
482   - salary.StoreTotalPerformance = salary.GrossProfit;
  539 + // 2.7 StoreTotalPerformance保存开单业绩-退卡业绩(销售业绩),提成计算使用毛利(GrossProfit)
  540 + salary.StoreTotalPerformance = salary.SalesPerformance;
483 541  
484 542 // 2.8 计算业绩完成率(基于毛利与生命线比较)
485 543 if (salary.StoreLifeline > 0)
... ... @@ -670,9 +728,10 @@ namespace NCC.Extend
670 728 return;
671 729 }
672 730  
673   - // 提成计算基于毛利(StoreTotalPerformance存储的是毛利
  731 + // 提成计算基于毛利(GrossProfit
674 732 // 重要:提成基数使用毛利,不是销售业绩(开单-退卡)
675   - decimal grossProfit = salary.StoreTotalPerformance; // 这里已经是毛利了
  733 + // StoreTotalPerformance存储的是开单业绩-退卡业绩,而提成需要使用毛利
  734 + decimal grossProfit = salary.GrossProfit;
676 735 decimal lifeline = salary.StoreLifeline;
677 736  
678 737 // 确定提成比例(根据新店/老店和门店分类)
... ... @@ -1057,59 +1116,159 @@ namespace NCC.Extend
1057 1116 CreateUser = ""
1058 1117 };
1059 1118  
1060   - // Excel字段映射(主任工资43列,Excel顺序:门店名称,员工姓名,岗位,实发工资,补贴合计,扣款合计,是否锁定,是否新店,新店保护阶段,门店总业绩...)
  1119 + // Excel字段映射
  1120 + // 向后兼容:先按旧格式读取(43列格式)
  1121 + // 如果Excel列数更多,则按新格式读取(支持新增字段)
1061 1122 entity.StoreName = storeName;
1062 1123 entity.EmployeeName = employeeName;
1063 1124 entity.Position = GetColumnValue(2 + offset);
1064   - entity.ActualSalary = ParseDecimal(GetColumnValue(3 + offset));
1065   - entity.TotalSubsidy = ParseDecimal(GetColumnValue(4 + offset));
1066   - entity.TotalDeduction = ParseDecimal(GetColumnValue(5 + offset));
1067   - // 跳过"是否锁定"字段(第7列),在最后处理
1068   - entity.IsNewStore = GetColumnValue(7 + offset) == "是" ? "是" : "否";
1069   - entity.NewStoreProtectionStage = ParseInt(GetColumnValue(8 + offset));
1070   - entity.StoreTotalPerformance = ParseDecimal(GetColumnValue(9 + offset));
1071   - entity.StoreBillingPerformance = ParseDecimal(GetColumnValue(10 + offset));
1072   - entity.StoreRefundPerformance = ParseDecimal(GetColumnValue(11 + offset));
1073   - entity.StoreLifeline = ParseDecimal(GetColumnValue(12 + offset));
1074   - entity.PerformanceCompletionRate = ParseDecimal(GetColumnValue(13 + offset));
1075   - entity.PerformanceReached = GetColumnValue(14 + offset);
1076   - entity.HeadCountReached = GetColumnValue(15 + offset);
1077   - entity.ConsumeReached = GetColumnValue(16 + offset);
1078   - entity.AssessmentDeduction = ParseDecimal(GetColumnValue(17 + offset));
1079   - entity.UnreachedIndicatorCount = ParseInt(GetColumnValue(18 + offset));
1080   - entity.HeadCount = ParseInt(GetColumnValue(19 + offset));
1081   - entity.TargetHeadCount = ParseDecimal(GetColumnValue(20 + offset));
1082   - entity.StoreConsume = ParseDecimal(GetColumnValue(21 + offset));
1083   - entity.TargetConsume = ParseDecimal(GetColumnValue(22 + offset));
1084   - entity.CommissionRateBelowLifeline = ParseDecimal(GetColumnValue(23 + offset));
1085   - entity.CommissionRateAboveLifeline = ParseDecimal(GetColumnValue(24 + offset));
1086   - entity.CommissionAmountBelowLifeline = ParseDecimal(GetColumnValue(25 + offset));
1087   - entity.CommissionAmountAboveLifeline = ParseDecimal(GetColumnValue(26 + offset));
1088   - entity.TotalCommissionAmount = ParseDecimal(GetColumnValue(27 + offset));
1089   - entity.BaseSalary = ParseDecimal(GetColumnValue(28 + offset));
1090   - entity.ActualBaseSalary = ParseDecimal(GetColumnValue(29 + offset));
1091   - entity.WorkingDays = ParseInt(GetColumnValue(30 + offset));
1092   - entity.LeaveDays = ParseInt(GetColumnValue(31 + offset));
1093   - entity.GrossSalary = ParseDecimal(GetColumnValue(32 + offset));
1094   - entity.Bonus = ParseDecimal(GetColumnValue(33 + offset));
1095   - entity.ReturnPhoneDeposit = ParseDecimal(GetColumnValue(34 + offset));
1096   - entity.ReturnAccommodationDeposit = ParseDecimal(GetColumnValue(35 + offset));
1097   - entity.MonthlyPaymentStatus = GetColumnValue(36 + offset);
1098   - entity.PaidAmount = ParseDecimal(GetColumnValue(37 + offset));
1099   - entity.PendingAmount = ParseDecimal(GetColumnValue(38 + offset));
1100   - entity.LastMonthSupplement = ParseDecimal(GetColumnValue(39 + offset));
1101   - entity.MonthlyTotalPayment = ParseDecimal(GetColumnValue(40 + offset));
1102   - // 处理门店类型和类别
1103   - var storeTypeStr = GetColumnValue(41 + offset);
1104   - if (!string.IsNullOrWhiteSpace(storeTypeStr) && int.TryParse(storeTypeStr, out int storeType))
1105   - entity.StoreType = storeType;
1106   - var storeCategoryStr = GetColumnValue(42 + offset);
1107   - if (!string.IsNullOrWhiteSpace(storeCategoryStr) && int.TryParse(storeCategoryStr, out int storeCategory))
1108   - entity.StoreCategory = storeCategory;
1109   - // 处理锁定状态(第7列)
1110   - var isLockedStr = GetColumnValue(6 + offset);
1111   - if (isLockedStr == "已锁定" || isLockedStr == "1" || isLockedStr == "锁定") entity.IsLocked = 1;
1112   - else entity.IsLocked = 0;
  1125 +
  1126 + // 旧格式兼容(保持向后兼容,Excel顺序:门店名称,员工姓名,岗位,实发工资,补贴合计,扣款合计,是否锁定,是否新店,新店保护阶段,门店总业绩...)
  1127 + if (dataTable.Columns.Count <= 43 + offset)
  1128 + {
  1129 + // 旧格式(43列)
  1130 + entity.ActualSalary = ParseDecimal(GetColumnValue(3 + offset));
  1131 + entity.TotalSubsidy = ParseDecimal(GetColumnValue(4 + offset));
  1132 + entity.TotalDeduction = ParseDecimal(GetColumnValue(5 + offset));
  1133 + var isLockedStr = GetColumnValue(6 + offset);
  1134 + if (isLockedStr == "已锁定" || isLockedStr == "1" || isLockedStr == "锁定") entity.IsLocked = 1;
  1135 + else entity.IsLocked = 0;
  1136 + entity.IsNewStore = GetColumnValue(7 + offset) == "是" ? "是" : "否";
  1137 + entity.NewStoreProtectionStage = ParseInt(GetColumnValue(8 + offset));
  1138 + entity.StoreTotalPerformance = ParseDecimal(GetColumnValue(9 + offset));
  1139 + entity.StoreBillingPerformance = ParseDecimal(GetColumnValue(10 + offset));
  1140 + entity.StoreRefundPerformance = ParseDecimal(GetColumnValue(11 + offset));
  1141 + entity.StoreLifeline = ParseDecimal(GetColumnValue(12 + offset));
  1142 + entity.PerformanceCompletionRate = ParseDecimal(GetColumnValue(13 + offset));
  1143 + entity.PerformanceReached = GetColumnValue(14 + offset);
  1144 + entity.HeadCountReached = GetColumnValue(15 + offset);
  1145 + entity.ConsumeReached = GetColumnValue(16 + offset);
  1146 + entity.AssessmentDeduction = ParseDecimal(GetColumnValue(17 + offset));
  1147 + entity.UnreachedIndicatorCount = ParseInt(GetColumnValue(18 + offset));
  1148 + entity.HeadCount = ParseInt(GetColumnValue(19 + offset));
  1149 + entity.TargetHeadCount = ParseDecimal(GetColumnValue(20 + offset));
  1150 + entity.StoreConsume = ParseDecimal(GetColumnValue(21 + offset));
  1151 + entity.TargetConsume = ParseDecimal(GetColumnValue(22 + offset));
  1152 + entity.CommissionRateBelowLifeline = ParseDecimal(GetColumnValue(23 + offset));
  1153 + entity.CommissionRateAboveLifeline = ParseDecimal(GetColumnValue(24 + offset));
  1154 + entity.CommissionAmountBelowLifeline = ParseDecimal(GetColumnValue(25 + offset));
  1155 + entity.CommissionAmountAboveLifeline = ParseDecimal(GetColumnValue(26 + offset));
  1156 + entity.TotalCommissionAmount = ParseDecimal(GetColumnValue(27 + offset));
  1157 + entity.BaseSalary = ParseDecimal(GetColumnValue(28 + offset));
  1158 + entity.ActualBaseSalary = ParseDecimal(GetColumnValue(29 + offset));
  1159 + entity.WorkingDays = ParseInt(GetColumnValue(30 + offset));
  1160 + entity.LeaveDays = ParseInt(GetColumnValue(31 + offset));
  1161 + entity.GrossSalary = ParseDecimal(GetColumnValue(32 + offset));
  1162 + entity.Bonus = ParseDecimal(GetColumnValue(33 + offset));
  1163 + entity.ReturnPhoneDeposit = ParseDecimal(GetColumnValue(34 + offset));
  1164 + entity.ReturnAccommodationDeposit = ParseDecimal(GetColumnValue(35 + offset));
  1165 + entity.MonthlyPaymentStatus = GetColumnValue(36 + offset);
  1166 + entity.PaidAmount = ParseDecimal(GetColumnValue(37 + offset));
  1167 + entity.PendingAmount = ParseDecimal(GetColumnValue(38 + offset));
  1168 + entity.LastMonthSupplement = ParseDecimal(GetColumnValue(39 + offset));
  1169 + entity.MonthlyTotalPayment = ParseDecimal(GetColumnValue(40 + offset));
  1170 + var storeTypeStr = GetColumnValue(41 + offset);
  1171 + if (!string.IsNullOrWhiteSpace(storeTypeStr) && int.TryParse(storeTypeStr, out int storeType))
  1172 + entity.StoreType = storeType;
  1173 + var storeCategoryStr = GetColumnValue(42 + offset);
  1174 + if (!string.IsNullOrWhiteSpace(storeCategoryStr) && int.TryParse(storeCategoryStr, out int storeCategory))
  1175 + entity.StoreCategory = storeCategory;
  1176 + }
  1177 + else
  1178 + {
  1179 + // 新格式:按列名匹配(更灵活)
  1180 + // 由于前端导出是基于 tableColumns 动态生成,列顺序可能变化
  1181 + // 这里使用列名匹配方式,提高兼容性
  1182 + var columnNameMap = new Dictionary<string, int>();
  1183 + for (int colIdx = 0; colIdx < dataTable.Columns.Count; colIdx++)
  1184 + {
  1185 + var colName = dataTable.Columns[colIdx].ColumnName?.Trim() ?? "";
  1186 + if (!string.IsNullOrWhiteSpace(colName))
  1187 + {
  1188 + columnNameMap[colName] = colIdx;
  1189 + }
  1190 + }
  1191 +
  1192 + // 辅助方法:根据列名获取值
  1193 + Func<string, string> GetValueByColumnName = (columnName) =>
  1194 + {
  1195 + if (columnNameMap.ContainsKey(columnName))
  1196 + {
  1197 + var colIdx = columnNameMap[columnName];
  1198 + return GetColumnValue(colIdx);
  1199 + }
  1200 + return "";
  1201 + };
  1202 +
  1203 + // 按列名读取字段(支持新字段)
  1204 + entity.StoreTotalPerformance = ParseDecimal(GetValueByColumnName("门店总业绩"));
  1205 + entity.StoreBillingPerformance = ParseDecimal(GetValueByColumnName("门店开单业绩"));
  1206 + entity.StoreRefundPerformance = ParseDecimal(GetValueByColumnName("门店退卡业绩"));
  1207 + // 销售业绩字段不在导入中处理,仅在计算时使用
  1208 + entity.ProductMaterial = ParseDecimal(GetValueByColumnName("产品物料"));
  1209 + entity.CooperationCost = ParseDecimal(GetValueByColumnName("合作项目成本"));
  1210 + entity.StoreExpense = ParseDecimal(GetValueByColumnName("店内支出"));
  1211 + entity.LaundryCost = ParseDecimal(GetValueByColumnName("洗毛巾费用"));
  1212 + entity.GrossProfit = ParseDecimal(GetValueByColumnName("毛利"));
  1213 + entity.StoreLifeline = ParseDecimal(GetValueByColumnName("门店生命线"));
  1214 + entity.PerformanceCompletionRate = ParseDecimal(GetValueByColumnName("业绩完成率"));
  1215 + entity.PerformanceReached = GetValueByColumnName("业绩是否达标");
  1216 + entity.HeadCountReached = GetValueByColumnName("人头是否达标");
  1217 + entity.ConsumeReached = GetValueByColumnName("消耗是否达标");
  1218 + entity.AssessmentDeduction = ParseDecimal(GetValueByColumnName("考核扣款金额"));
  1219 + entity.UnreachedIndicatorCount = ParseInt(GetValueByColumnName("未达标指标数量"));
  1220 + entity.HeadCount = ParseInt(GetValueByColumnName("进店消耗人数"));
  1221 + entity.TargetHeadCount = ParseDecimal(GetValueByColumnName("目标人头数"));
  1222 + entity.StoreConsume = ParseDecimal(GetValueByColumnName("门店消耗金额"));
  1223 + entity.TargetConsume = ParseDecimal(GetValueByColumnName("目标消耗金额"));
  1224 + entity.CommissionRateBelowLifeline = ParseDecimal(GetValueByColumnName("≤生命线部分提成比例"));
  1225 + entity.CommissionRateAboveLifeline = ParseDecimal(GetValueByColumnName(">生命线部分提成比例"));
  1226 + entity.CommissionAmountBelowLifeline = ParseDecimal(GetValueByColumnName("≤生命线部分提成金额"));
  1227 + entity.CommissionAmountAboveLifeline = ParseDecimal(GetValueByColumnName(">生命线部分提成金额"));
  1228 + entity.TotalCommissionAmount = ParseDecimal(GetValueByColumnName("提成总金额"));
  1229 + entity.BaseSalary = ParseDecimal(GetValueByColumnName("底薪金额"));
  1230 + entity.ActualBaseSalary = ParseDecimal(GetValueByColumnName("实际底薪"));
  1231 + entity.WorkingDays = ParseInt(GetValueByColumnName("在店天数"));
  1232 + entity.LeaveDays = ParseInt(GetValueByColumnName("请假天数"));
  1233 + entity.GrossSalary = ParseDecimal(GetValueByColumnName("应发工资"));
  1234 + entity.ActualSalary = ParseDecimal(GetValueByColumnName("实发工资"));
  1235 + entity.MissingCard = ParseDecimal(GetValueByColumnName("缺卡扣款"));
  1236 + entity.LateArrival = ParseDecimal(GetValueByColumnName("迟到扣款"));
  1237 + entity.LeaveDeduction = ParseDecimal(GetValueByColumnName("请假扣款"));
  1238 + entity.SocialInsuranceDeduction = ParseDecimal(GetValueByColumnName("扣社保"));
  1239 + entity.RewardDeduction = ParseDecimal(GetValueByColumnName("扣除奖励"));
  1240 + entity.AccommodationDeduction = ParseDecimal(GetValueByColumnName("扣住宿费"));
  1241 + entity.StudyPeriodDeduction = ParseDecimal(GetValueByColumnName("扣学习期费用"));
  1242 + entity.WorkClothesDeduction = ParseDecimal(GetValueByColumnName("扣工作服费用"));
  1243 + entity.TotalDeduction = ParseDecimal(GetValueByColumnName("扣款合计"));
  1244 + entity.MonthlyTrainingSubsidy = ParseDecimal(GetValueByColumnName("当月培训补贴"));
  1245 + entity.MonthlyTransportSubsidy = ParseDecimal(GetValueByColumnName("当月交通补贴"));
  1246 + entity.LastMonthTrainingSubsidy = ParseDecimal(GetValueByColumnName("上月培训补贴"));
  1247 + entity.LastMonthTransportSubsidy = ParseDecimal(GetValueByColumnName("上月交通补贴"));
  1248 + entity.TotalSubsidy = ParseDecimal(GetValueByColumnName("补贴合计"));
  1249 + entity.Bonus = ParseDecimal(GetValueByColumnName("发奖金"));
  1250 + entity.ReturnPhoneDeposit = ParseDecimal(GetValueByColumnName("退手机押金"));
  1251 + entity.ReturnAccommodationDeposit = ParseDecimal(GetValueByColumnName("退住宿押金"));
  1252 + entity.MonthlyPaymentStatus = GetValueByColumnName("当月是否发放");
  1253 + entity.PaidAmount = ParseDecimal(GetValueByColumnName("支付金额"));
  1254 + entity.PendingAmount = ParseDecimal(GetValueByColumnName("待支付金额"));
  1255 + entity.LastMonthSupplement = ParseDecimal(GetValueByColumnName("补发上月"));
  1256 + entity.MonthlyTotalPayment = ParseDecimal(GetValueByColumnName("当月支付总额"));
  1257 + var isLockedStrNew = GetValueByColumnName("锁定状态");
  1258 + if (isLockedStrNew == "已锁定" || isLockedStrNew == "1" || isLockedStrNew == "锁定") entity.IsLocked = 1;
  1259 + else entity.IsLocked = 0;
  1260 + var confirmStatusStr = GetValueByColumnName("确认状态");
  1261 + if (confirmStatusStr == "已确认" || confirmStatusStr == "1") entity.EmployeeConfirmStatus = 1;
  1262 + else entity.EmployeeConfirmStatus = 0;
  1263 + var storeTypeStrNew = GetValueByColumnName("门店类型");
  1264 + if (!string.IsNullOrWhiteSpace(storeTypeStrNew) && int.TryParse(storeTypeStrNew, out int storeTypeNew))
  1265 + entity.StoreType = storeTypeNew;
  1266 + var storeCategoryStrNew = GetValueByColumnName("门店类别");
  1267 + if (!string.IsNullOrWhiteSpace(storeCategoryStrNew) && int.TryParse(storeCategoryStrNew, out int storeCategoryNew))
  1268 + entity.StoreCategory = storeCategoryNew;
  1269 + entity.IsNewStore = GetValueByColumnName("是否新店") == "是" ? "是" : "否";
  1270 + entity.NewStoreProtectionStage = ParseInt(GetValueByColumnName("新店保护阶段"));
  1271 + }
1113 1272  
1114 1273 if (existing != null)
1115 1274 {
... ... @@ -1159,10 +1318,12 @@ namespace NCC.Extend
1159 1318 if (recordsToInsert.Any()) await _db.Insertable(recordsToInsert).ExecuteCommandAsync();
1160 1319 if (recordsToUpdate.Any())
1161 1320 {
1162   - // 使用IgnoreColumns排除CreateTime和CreateUser,确保其他所有字段都被更新
  1321 + // 使用IgnoreColumns排除CreateTime、CreateUser、StoreId和EmployeeId,确保这些字段不会被更新
1163 1322 await _db.Updateable(recordsToUpdate)
1164 1323 .IgnoreColumns(x => x.CreateTime)
1165 1324 .IgnoreColumns(x => x.CreateUser)
  1325 + .IgnoreColumns(x => x.StoreId)
  1326 + .IgnoreColumns(x => x.EmployeeId)
1166 1327 .ExecuteCommandAsync();
1167 1328 }
1168 1329  
... ...
netcore/src/Modularity/Extend/NCC.Extend/LqKhxxService.cs
... ... @@ -2152,10 +2152,121 @@ namespace NCC.Extend.LqKhxx
2152 2152  
2153 2153 var itemDetailIds = itemDetails.Select(x => (string)x.Id).ToList();
2154 2154  
2155   - // 5. 批量查询消耗品项
  2155 + // 4.5. 双向查询:直接查询会员的所有耗卡记录,确保不遗漏数据
  2156 + // 通过耗卡记录反向查找可能遗漏的开单品项明细和开单记录
  2157 + var allConsumedItemIds = new HashSet<string>();
  2158 + var additionalBillingItemIds = new HashSet<string>();
  2159 +
  2160 + if (memberIdsList.Any())
  2161 + {
  2162 + // 查询会员的所有耗卡记录
  2163 + var allConsumedItemsFromConsume = await _db.Queryable<LqXhPxmxEntity, LqXhHyhkEntity>(
  2164 + (pxmx, hyhk) => new JoinQueryInfos(JoinType.Inner, pxmx.ConsumeInfoId == hyhk.Id))
  2165 + .Where((pxmx, hyhk) => memberIdsList.Contains(hyhk.Hy)
  2166 + && pxmx.IsEffective == StatusEnum.有效.GetHashCode()
  2167 + && hyhk.IsEffective == StatusEnum.有效.GetHashCode())
  2168 + .Where((pxmx, hyhk) => !string.IsNullOrEmpty(pxmx.BillingItemId))
  2169 + .Select((pxmx, hyhk) => new
  2170 + {
  2171 + pxmx.BillingItemId,
  2172 + pxmx.Px,
  2173 + pxmx.Pxmc,
  2174 + pxmx.Pxjg,
  2175 + pxmx.ProjectNumber,
  2176 + pxmx.TotalPrice
  2177 + })
  2178 + .ToListAsync();
  2179 +
  2180 + // 收集所有耗卡记录关联的开单品项明细ID
  2181 + foreach (var consumedItem in allConsumedItemsFromConsume)
  2182 + {
  2183 + if (!string.IsNullOrEmpty(consumedItem.BillingItemId))
  2184 + {
  2185 + allConsumedItemIds.Add(consumedItem.BillingItemId);
  2186 + // 如果这个BillingItemId不在当前的itemDetailIds中,需要额外查询
  2187 + if (!itemDetailIds.Contains(consumedItem.BillingItemId))
  2188 + {
  2189 + additionalBillingItemIds.Add(consumedItem.BillingItemId);
  2190 + }
  2191 + }
  2192 + }
  2193 +
  2194 + // 如果有额外的开单品项明细ID,查询这些品项明细对应的开单记录
  2195 + if (additionalBillingItemIds.Any())
  2196 + {
  2197 + // 查询这些开单品项明细
  2198 + var additionalItemDetailsData = await _db.Queryable<LqKdPxmxEntity>()
  2199 + .Where(x => additionalBillingItemIds.Contains(x.Id) && x.IsEffective == StatusEnum.有效.GetHashCode())
  2200 + .Select(x => new
  2201 + {
  2202 + x.Id,
  2203 + x.Glkdbh,
  2204 + x.Px,
  2205 + x.Pxmc,
  2206 + x.Pxjg,
  2207 + x.SourceType,
  2208 + x.ProjectNumber,
  2209 + x.TotalPrice
  2210 + })
  2211 + .ToListAsync();
  2212 +
  2213 + // 将额外的品项明细添加到itemDetails中
  2214 + itemDetails.AddRange(additionalItemDetailsData.Cast<dynamic>());
  2215 + itemDetailIds.AddRange(additionalItemDetailsData.Select(x => x.Id));
  2216 +
  2217 + // 查询这些品项明细对应的开单记录(如果不在当前的billingIds中)
  2218 + var additionalBillingIds = additionalItemDetailsData
  2219 + .Select(x => x.Glkdbh)
  2220 + .Where(x => !string.IsNullOrEmpty(x) && !billingIds.Contains(x))
  2221 + .Distinct()
  2222 + .ToList();
  2223 +
  2224 + if (additionalBillingIds.Any())
  2225 + {
  2226 + var additionalBillingRecords = await _db.Queryable<LqKdKdjlbEntity>()
  2227 + .Where(x => additionalBillingIds.Contains(x.Id) && x.IsEffective == StatusEnum.有效.GetHashCode())
  2228 + .Select(x => new
  2229 + {
  2230 + x.Id,
  2231 + x.Kdhy,
  2232 + x.Kdrq,
  2233 + x.Djmd,
  2234 + x.CreateUser
  2235 + })
  2236 + .ToListAsync();
  2237 +
  2238 + // 将额外的开单记录添加到billingRecords中(转换为dynamic以便后续使用)
  2239 + foreach (var record in additionalBillingRecords)
  2240 + {
  2241 + billingRecords.Add(record);
  2242 + }
  2243 + billingIds.AddRange(additionalBillingIds);
  2244 + }
  2245 + }
  2246 + }
  2247 +
  2248 + // 5. 批量查询消耗品项(包括所有通过耗卡找到的BillingItemId)
2156 2249 var consumedItems = new List<dynamic>();
2157   - if (itemDetailIds.Any())
  2250 + if (allConsumedItemIds.Any())
2158 2251 {
  2252 + // 使用allConsumedItemIds(来自耗卡记录的所有BillingItemId)查询消耗品项
  2253 + var consumedItemsData = await _db.Queryable<LqXhPxmxEntity>()
  2254 + .Where(x => allConsumedItemIds.Contains(x.BillingItemId) && x.IsEffective == StatusEnum.有效.GetHashCode())
  2255 + .Select(x => new
  2256 + {
  2257 + x.BillingItemId,
  2258 + x.Px,
  2259 + x.Pxmc,
  2260 + x.Pxjg,
  2261 + x.ProjectNumber,
  2262 + x.TotalPrice
  2263 + })
  2264 + .ToListAsync();
  2265 + consumedItems = consumedItemsData.Cast<dynamic>().ToList();
  2266 + }
  2267 + else if (itemDetailIds.Any())
  2268 + {
  2269 + // 兼容原有逻辑:如果没有耗卡记录,使用itemDetailIds查询
2159 2270 var consumedItemsData = await _db.Queryable<LqXhPxmxEntity>()
2160 2271 .Where(x => itemDetailIds.Contains(x.BillingItemId) && x.IsEffective == StatusEnum.有效.GetHashCode())
2161 2272 .Select(x => new
... ... @@ -2171,7 +2282,7 @@ namespace NCC.Extend.LqKhxx
2171 2282 consumedItems = consumedItemsData.Cast<dynamic>().ToList();
2172 2283 }
2173 2284  
2174   - // 6. 批量查询退卡品项
  2285 + // 6. 批量查询退卡品项(使用itemDetailIds,因为退卡也是关联开单品项明细)
2175 2286 var refundedItems = new List<dynamic>();
2176 2287 if (itemDetailIds.Any())
2177 2288 {
... ... @@ -2190,6 +2301,113 @@ namespace NCC.Extend.LqKhxx
2190 2301 refundedItems = refundedItemsData.Cast<dynamic>().ToList();
2191 2302 }
2192 2303  
  2304 + // 6.5. 双向查询退卡品项:直接从会员的退卡记录查询,确保不遗漏
  2305 + if (memberIdsList.Any())
  2306 + {
  2307 + var allRefundedItemsFromRefund = await _db.Queryable<LqHytkMxEntity, LqHytkHytkEntity>(
  2308 + (mx, hytk) => new JoinQueryInfos(JoinType.Inner, mx.RefundInfoId == hytk.Id))
  2309 + .Where((mx, hytk) => memberIdsList.Contains(hytk.Hy)
  2310 + && mx.IsEffective == StatusEnum.有效.GetHashCode()
  2311 + && hytk.IsEffective == StatusEnum.有效.GetHashCode())
  2312 + .Where((mx, hytk) => !string.IsNullOrEmpty(mx.BillingItemId))
  2313 + .Select((mx, hytk) => new
  2314 + {
  2315 + mx.BillingItemId,
  2316 + mx.Px,
  2317 + mx.Pxmc,
  2318 + mx.Pxjg,
  2319 + mx.ProjectNumber,
  2320 + mx.Tkje
  2321 + })
  2322 + .ToListAsync();
  2323 +
  2324 + // 收集退卡记录关联的开单品项明细ID
  2325 + var refundedBillingItemIds = allRefundedItemsFromRefund
  2326 + .Select(x => x.BillingItemId)
  2327 + .Where(x => !string.IsNullOrEmpty(x))
  2328 + .Distinct()
  2329 + .ToList();
  2330 +
  2331 + // 如果退卡关联的开单品项明细不在当前的itemDetailIds中,也需要查询对应的开单记录
  2332 + var missingRefundedBillingItemIds = refundedBillingItemIds
  2333 + .Where(x => !itemDetailIds.Contains(x))
  2334 + .ToList();
  2335 +
  2336 + if (missingRefundedBillingItemIds.Any())
  2337 + {
  2338 + // 查询这些开单品项明细
  2339 + var refundedAdditionalItemDetailsData = await _db.Queryable<LqKdPxmxEntity>()
  2340 + .Where(x => missingRefundedBillingItemIds.Contains(x.Id) && x.IsEffective == StatusEnum.有效.GetHashCode())
  2341 + .Select(x => new
  2342 + {
  2343 + x.Id,
  2344 + x.Glkdbh,
  2345 + x.Px,
  2346 + x.Pxmc,
  2347 + x.Pxjg,
  2348 + x.SourceType,
  2349 + x.ProjectNumber,
  2350 + x.TotalPrice
  2351 + })
  2352 + .ToListAsync();
  2353 +
  2354 + // 将额外的品项明细添加到itemDetails中
  2355 + itemDetails.AddRange(refundedAdditionalItemDetailsData.Cast<dynamic>());
  2356 + itemDetailIds.AddRange(refundedAdditionalItemDetailsData.Select(x => x.Id));
  2357 +
  2358 + // 查询这些品项明细对应的开单记录(如果不在当前的billingIds中)
  2359 + var refundedAdditionalBillingIds = refundedAdditionalItemDetailsData
  2360 + .Select(x => x.Glkdbh)
  2361 + .Where(x => !string.IsNullOrEmpty(x) && !billingIds.Contains(x))
  2362 + .Distinct()
  2363 + .ToList();
  2364 +
  2365 + if (refundedAdditionalBillingIds.Any())
  2366 + {
  2367 + var refundedAdditionalBillingRecords = await _db.Queryable<LqKdKdjlbEntity>()
  2368 + .Where(x => refundedAdditionalBillingIds.Contains(x.Id) && x.IsEffective == StatusEnum.有效.GetHashCode())
  2369 + .Select(x => new
  2370 + {
  2371 + x.Id,
  2372 + x.Kdhy,
  2373 + x.Kdrq,
  2374 + x.Djmd,
  2375 + x.CreateUser
  2376 + })
  2377 + .ToListAsync();
  2378 +
  2379 + // 将额外的开单记录添加到billingRecords中(转换为dynamic以便后续使用)
  2380 + foreach (var record in refundedAdditionalBillingRecords)
  2381 + {
  2382 + billingRecords.Add(record);
  2383 + }
  2384 + billingIds.AddRange(refundedAdditionalBillingIds);
  2385 + }
  2386 +
  2387 + // 将退卡数据添加到refundedItems中(去重)
  2388 + foreach (var refundedItem in allRefundedItemsFromRefund)
  2389 + {
  2390 + var refundedItemDynamic = refundedItem as dynamic;
  2391 + if (!refundedItems.Any(x => x.BillingItemId?.ToString() == refundedItemDynamic.BillingItemId?.ToString()))
  2392 + {
  2393 + refundedItems.Add(refundedItemDynamic);
  2394 + }
  2395 + }
  2396 + }
  2397 + else
  2398 + {
  2399 + // 如果所有退卡记录关联的开单品项明细都在itemDetailIds中,只需要合并退卡数据(去重)
  2400 + foreach (var refundedItem in allRefundedItemsFromRefund)
  2401 + {
  2402 + var refundedItemDynamic = refundedItem as dynamic;
  2403 + if (!refundedItems.Any(x => x.BillingItemId?.ToString() == refundedItemDynamic.BillingItemId?.ToString()))
  2404 + {
  2405 + refundedItems.Add(refundedItemDynamic);
  2406 + }
  2407 + }
  2408 + }
  2409 + }
  2410 +
2193 2411 // 7. 批量查询储扣品项
2194 2412 var deductedItems = new List<dynamic>();
2195 2413 if (billingIds.Any())
... ...
netcore/src/Modularity/Extend/NCC.Extend/LqReimbursementApplicationService.cs
... ... @@ -262,6 +262,44 @@ namespace NCC.Extend.LqReimbursementApplication
262 262 List<string> queryCompletionTime = input.completionTime != null ? input.completionTime.Split(',').ToObeject<List<string>>() : null;
263 263 DateTime? startCompletionTime = queryCompletionTime != null ? Ext.GetDateTime(queryCompletionTime.First()) : null;
264 264 DateTime? endCompletionTime = queryCompletionTime != null ? Ext.GetDateTime(queryCompletionTime.Last()) : null;
  265 + // 如果提供了完成时间筛选,需要先查询符合条件的申请ID,然后在主查询中过滤
  266 + List<string> filteredApplicationIdsByCompletionTime = null;
  267 + if (queryCompletionTime != null)
  268 + {
  269 + var startDate = new DateTime(startCompletionTime.ToDate().Year, startCompletionTime.ToDate().Month, startCompletionTime.ToDate().Day, 0, 0, 0);
  270 + var endDate = new DateTime(endCompletionTime.ToDate().Year, endCompletionTime.ToDate().Month, endCompletionTime.ToDate().Day, 23, 59, 59);
  271 +
  272 + // 先查询实体类中有CompletionTime字段且符合条件的申请ID
  273 + var entitiesWithCompletionTime = await _db.Queryable<LqReimbursementApplicationEntity>()
  274 + .Where(x => x.CompletionTime.HasValue
  275 + && x.CompletionTime.Value >= startDate
  276 + && x.CompletionTime.Value <= endDate)
  277 + .Select(x => x.Id)
  278 + .ToListAsync();
  279 +
  280 + // 查询实体类中没有CompletionTime字段的申请,需要从审批记录中获取完成时间
  281 + var entitiesWithoutCompletionTime = await _db.Queryable<LqReimbursementApplicationEntity>()
  282 + .Where(x => !x.CompletionTime.HasValue)
  283 + .Select(x => x.Id)
  284 + .ToListAsync();
  285 +
  286 + var applicationIdsFromRecords = new List<string>();
  287 + if (entitiesWithoutCompletionTime.Any())
  288 + {
  289 + // 从审批记录中查询符合条件的申请ID
  290 + applicationIdsFromRecords = await _db.Queryable<LqReimbursementApprovalRecordEntity>()
  291 + .Where(x => entitiesWithoutCompletionTime.Contains(x.ApplicationId) && x.ApprovalResult == "通过")
  292 + .GroupBy(x => x.ApplicationId)
  293 + .Having(x => SqlFunc.AggregateMax(x.ApprovalTime) >= startDate &&
  294 + SqlFunc.AggregateMax(x.ApprovalTime) <= endDate)
  295 + .Select(x => x.ApplicationId)
  296 + .ToListAsync();
  297 + }
  298 +
  299 + // 合并两种情况的申请ID
  300 + filteredApplicationIdsByCompletionTime = entitiesWithCompletionTime.Union(applicationIdsFromRecords).ToList();
  301 + }
  302 +
265 303 var query = _db.Queryable<LqReimbursementApplicationEntity>()
266 304 .WhereIF(!string.IsNullOrEmpty(input.id), p => p.Id.Contains(input.id))
267 305 .WhereIF(!string.IsNullOrEmpty(input.applicationUserId), p => p.ApplicationUserId.Contains(input.applicationUserId))
... ... @@ -274,7 +312,11 @@ namespace NCC.Extend.LqReimbursementApplication
274 312 .WhereIF(!string.IsNullOrEmpty(input.approveStatus), p => (p.ApprovalStatus ?? p.ApproveStatus).Contains(input.approveStatus))
275 313 // .WhereIF(queryApproveTime != null, p => p.ApproveTime >= new DateTime(startApproveTime.ToDate().Year, startApproveTime.ToDate().Month, startApproveTime.ToDate().Day, 0, 0, 0))
276 314 //.WhereIF(queryApproveTime != null, p => p.ApproveTime <= new DateTime(endApproveTime.ToDate().Year, endApproveTime.ToDate().Month, endApproveTime.ToDate().Day, 23, 59, 59))
277   - .WhereIF(!string.IsNullOrEmpty(input.purchaseRecordsId), p => p.PurchaseRecordsId.Contains(input.purchaseRecordsId));
  315 + .WhereIF(!string.IsNullOrEmpty(input.purchaseRecordsId), p => p.PurchaseRecordsId.Contains(input.purchaseRecordsId))
  316 + // 如果提供了完成时间筛选,在主查询中过滤
  317 + .WhereIF(filteredApplicationIdsByCompletionTime != null && filteredApplicationIdsByCompletionTime.Any(), p => filteredApplicationIdsByCompletionTime.Contains(p.Id))
  318 + // 如果没有符合条件的申请,返回空结果
  319 + .WhereIF(filteredApplicationIdsByCompletionTime != null && !filteredApplicationIdsByCompletionTime.Any(), p => false);
278 320  
279 321 // 处理排序(兼容前端传入的字段名)
280 322 if (string.IsNullOrEmpty(input.sidx))
... ... @@ -343,31 +385,6 @@ namespace NCC.Extend.LqReimbursementApplication
343 385 .GroupBy(x => (string)x.applicationId)
344 386 .ToDictionary(g => g.Key, g => string.Join(", ", g.Select(x => (string)x.approverName)));
345 387  
346   - // 如果提供了完成时间筛选,需要先查询完成时间,然后过滤
347   - if (queryCompletionTime != null && applicationIds.Any())
348   - {
349   - var completionTimeRecords = await _db.Queryable<LqReimbursementApprovalRecordEntity>()
350   - .Where(x => applicationIds.Contains(x.ApplicationId) && x.ApprovalResult == "通过")
351   - .GroupBy(x => x.ApplicationId)
352   - .Select(x => new
353   - {
354   - ApplicationId = x.ApplicationId,
355   - MaxApprovalTime = SqlFunc.AggregateMax(x.ApprovalTime)
356   - })
357   - .ToListAsync();
358   -
359   - var filteredApplicationIds = completionTimeRecords
360   - .Where(x => x.MaxApprovalTime.HasValue &&
361   - x.MaxApprovalTime.Value >= new DateTime(startCompletionTime.ToDate().Year, startCompletionTime.ToDate().Month, startCompletionTime.ToDate().Day, 0, 0, 0) &&
362   - x.MaxApprovalTime.Value <= new DateTime(endCompletionTime.ToDate().Year, endCompletionTime.ToDate().Month, endCompletionTime.ToDate().Day, 23, 59, 59))
363   - .Select(x => x.ApplicationId)
364   - .ToList();
365   -
366   - entities = entities.Where(x => filteredApplicationIds.Contains(x.Id)).ToList();
367   - total = entities.Count;
368   - applicationIds = entities.Select(x => x.Id).ToList();
369   - }
370   -
371 388 // 获取门店名称
372 389 var storeIds = entities.Where(x => !string.IsNullOrEmpty(x.ApplicationStoreId)).Select(x => x.ApplicationStoreId).Distinct().ToList();
373 390 var storeDict = new Dictionary<string, string>();
... ...
netcore/src/Modularity/Extend/NCC.Extend/LqTechGeneralManagerSalaryService.cs
... ... @@ -201,6 +201,24 @@ namespace NCC.Extend
201 201 /// <summary>
202 202 /// 计算科技部总经理工资
203 203 /// </summary>
  204 + /// <remarks>
  205 + /// 计算科技部总经理的工资,包括底薪、溯源金额提成、Cell金额提成等。
  206 + ///
  207 + /// 计算规则:
  208 + /// - 底薪:固定4000元
  209 + /// - 溯源金额提成:根据管理的所有门店的溯源金额总和分段累进计算
  210 + /// - Cell金额提成:根据管理的所有门店的Cell金额总和分段累进计算
  211 + ///
  212 + /// 数据统计说明:
  213 + /// - 时间范围:严格按照当月范围(startDate 到 endDate 23:59:59),不包含下个月的数据
  214 + /// - 数据查询:使用原生SQL在数据库层面转换和求和,确保与科技部驾驶舱接口的计算方式一致
  215 + /// - 门店范围:通过门店的kjb字段确定科技部总经理管理的门店
  216 + ///
  217 + /// 溯源金额和Cell金额的计算方式:
  218 + /// - 开单金额:从 lq_kd_jksyj 表统计(使用原生SQL CAST转换)
  219 + /// - 退卡金额:从 lq_hytk_jksyj 表统计(使用SqlSugar聚合查询)
  220 + /// - 净金额 = 开单金额 - 退卡金额
  221 + /// </remarks>
204 222 /// <param name="year">年份</param>
205 223 /// <param name="month">月份</param>
206 224 /// <returns></returns>
... ... @@ -210,6 +228,12 @@ namespace NCC.Extend
210 228 var startDate = new DateTime(year, month, 1);
211 229 var endDate = startDate.AddMonths(1).AddDays(-1);
212 230 var monthStr = $"{year}{month:D2}";
  231 + // 使用与科技部驾驶舱接口相同的时间范围处理方式
  232 + var endDateTime = monthStr == DateTime.Now.ToString("yyyyMM")
  233 + ? DateTime.Now
  234 + : endDate.Date.AddHours(23).AddMinutes(59).AddSeconds(59);
  235 +
  236 + _logger.LogInformation($"[科技部总经理工资计算] 开始计算,月份: {monthStr}, 时间范围: {startDate.ToString("yyyy-MM-dd HH:mm:ss")} 到 {endDateTime.ToString("yyyy-MM-dd HH:mm:ss")}");
213 237  
214 238 // 1. 获取基础数据
215 239  
... ... @@ -229,9 +253,9 @@ namespace NCC.Extend
229 253 var techOrganizeIds = techOrganizeList.Select(x => x.Id).ToList();
230 254 var techOrganizeDict = techOrganizeList.ToDictionary(x => x.Id, x => x.FullName);
231 255  
232   - // 1.2 从BASE_USER表查询岗位为"总经理"且组织ID在科技一部或科技二部的员工
  256 + // 1.2 从BASE_USER表查询岗位为"总经理"或"科技部总经理"且组织ID在科技一部或科技二部的员工
233 257 var techGeneralManagerUserList = await _db.Queryable<UserEntity>()
234   - .Where(x => x.Gw == "总经理"
  258 + .Where(x => (x.Gw == "总经理" || x.Gw == "科技部总经理")
235 259 && techOrganizeIds.Contains(x.OrganizeId)
236 260 && x.DeleteMark == null && x.EnabledMark == 1)
237 261 .Select(x => new { x.Id, x.RealName, x.Account, x.Gw, x.OrganizeId, x.IsOnJob })
... ... @@ -239,13 +263,8 @@ namespace NCC.Extend
239 263  
240 264 if (!techGeneralManagerUserList.Any())
241 265 {
242   - // 如果没有科技部总经理员工,直接返回
243   - return;
244   - }
245   -
246   - if (!techGeneralManagerUserList.Any())
247   - {
248   - // 如果没有科技部总经理员工,直接返回
  266 + // 如果没有科技部总经理员工,记录日志并返回
  267 + _logger.LogWarning($"[科技部总经理工资计算] 未找到科技部总经理员工,科技部组织ID: {string.Join(",", techOrganizeIds)}");
249 268 return;
250 269 }
251 270  
... ... @@ -301,51 +320,110 @@ namespace NCC.Extend
301 320 var storeDetailDict = new Dictionary<string, Dictionary<string, StoreDetailItem>>();
302 321  
303 322 // 按门店统计溯源和Cell金额(如果有管理的门店)
  323 + // 使用与科技部驾驶舱接口完全相同的查询方式:批量查询所有门店,然后按门店分组
304 324 if (allManagedStoreIds.Any())
305 325 {
  326 + // 时间格式化字符串(使用与科技部驾驶舱接口相同的格式化方式)
  327 + var startDateStr = startDate.ToString("yyyy-MM-dd HH:mm:ss");
  328 + var endDateTimeStr = endDateTime.ToString("yyyy-MM-dd HH:mm:ss");
  329 +
  330 + // 批量查询所有门店的开单溯源金额(与科技部驾驶舱接口保持一致)
  331 + var allStoreTraceabilityBillingSql = $@"
  332 + SELECT F_StoreId, COALESCE(SUM(CAST(jksyj AS DECIMAL(18,2))), 0) as Amount
  333 + FROM lq_kd_jksyj
  334 + WHERE F_IsEffective = 1
  335 + AND F_StoreId IN ('{string.Join("','", allManagedStoreIds)}')
  336 + AND (F_BeautyType = '溯源系统' OR F_BeautyType = '溯源')
  337 + AND yjsj >= '{startDateStr}'
  338 + AND yjsj <= '{endDateTimeStr}'
  339 + GROUP BY F_StoreId";
  340 + var allStoreTraceabilityBillingResult = await _db.Ado.SqlQueryAsync<dynamic>(allStoreTraceabilityBillingSql);
  341 + var storeTraceabilityBillingDict = new Dictionary<string, decimal>();
  342 + if (allStoreTraceabilityBillingResult != null)
  343 + {
  344 + foreach (var item in allStoreTraceabilityBillingResult)
  345 + {
  346 + var storeId = item.F_StoreId?.ToString() ?? "";
  347 + var amount = Convert.ToDecimal(item.Amount ?? 0);
  348 + storeTraceabilityBillingDict[storeId] = amount;
  349 + }
  350 + }
  351 +
  352 + // 批量查询所有门店的开单Cell金额(与科技部驾驶舱接口保持一致)
  353 + var allStoreCellBillingSql = $@"
  354 + SELECT F_StoreId, COALESCE(SUM(CAST(jksyj AS DECIMAL(18,2))), 0) as Amount
  355 + FROM lq_kd_jksyj
  356 + WHERE F_IsEffective = 1
  357 + AND F_StoreId IN ('{string.Join("','", allManagedStoreIds)}')
  358 + AND (F_BeautyType = 'cell' OR F_BeautyType = 'Cell')
  359 + AND yjsj >= '{startDateStr}'
  360 + AND yjsj <= '{endDateTimeStr}'
  361 + GROUP BY F_StoreId";
  362 + var allStoreCellBillingResult = await _db.Ado.SqlQueryAsync<dynamic>(allStoreCellBillingSql);
  363 + var storeCellBillingDict = new Dictionary<string, decimal>();
  364 + if (allStoreCellBillingResult != null)
  365 + {
  366 + foreach (var item in allStoreCellBillingResult)
  367 + {
  368 + var storeId = item.F_StoreId?.ToString() ?? "";
  369 + var amount = Convert.ToDecimal(item.Amount ?? 0);
  370 + storeCellBillingDict[storeId] = amount;
  371 + }
  372 + }
  373 +
  374 + // 批量查询所有门店的退卡溯源金额
  375 + var allStoreTraceabilityRefundList = await _db.Queryable<LqHytkJksyjEntity>()
  376 + .Where(x => x.IsEffective == 1)
  377 + .Where(x => allManagedStoreIds.Contains(x.StoreId))
  378 + .Where(x => (x.BeautyType == "溯源系统" || x.BeautyType == "溯源"))
  379 + .Where(x => x.Tksj.HasValue && x.Tksj.Value.Date >= startDate.Date && x.Tksj.Value.Date <= endDate.Date)
  380 + .GroupBy(x => x.StoreId)
  381 + .Select(x => new { StoreId = x.StoreId, Amount = SqlFunc.AggregateSum(x.Jksyj) })
  382 + .ToListAsync();
  383 + var storeTraceabilityRefundDict = new Dictionary<string, decimal>();
  384 + if (allStoreTraceabilityRefundList != null)
  385 + {
  386 + foreach (var item in allStoreTraceabilityRefundList)
  387 + {
  388 + var storeId = item.StoreId ?? "";
  389 + var amount = Convert.ToDecimal(item.Amount ?? 0);
  390 + storeTraceabilityRefundDict[storeId] = amount;
  391 + }
  392 + }
  393 +
  394 + // 批量查询所有门店的退卡Cell金额
  395 + var allStoreCellRefundList = await _db.Queryable<LqHytkJksyjEntity>()
  396 + .Where(x => x.IsEffective == 1)
  397 + .Where(x => allManagedStoreIds.Contains(x.StoreId))
  398 + .Where(x => (x.BeautyType == "cell" || x.BeautyType == "Cell"))
  399 + .Where(x => x.Tksj.HasValue && x.Tksj.Value.Date >= startDate.Date && x.Tksj.Value.Date <= endDate.Date)
  400 + .GroupBy(x => x.StoreId)
  401 + .Select(x => new { StoreId = x.StoreId, Amount = SqlFunc.AggregateSum(x.Jksyj) })
  402 + .ToListAsync();
  403 + var storeCellRefundDict = new Dictionary<string, decimal>();
  404 + if (allStoreCellRefundList != null)
  405 + {
  406 + foreach (var item in allStoreCellRefundList)
  407 + {
  408 + var storeId = item.StoreId ?? "";
  409 + var amount = Convert.ToDecimal(item.Amount ?? 0);
  410 + storeCellRefundDict[storeId] = amount;
  411 + }
  412 + }
  413 +
  414 + // 遍历所有门店,构建门店明细
306 415 foreach (var storeId in allManagedStoreIds)
307 416 {
308   - // 该门店的开单溯源金额(从健康师业绩表统计)
309   - var storeTraceabilityBillingList = await _db.Queryable<LqKdJksyjEntity>()
310   - .Where(x => x.IsEffective == 1
311   - && x.StoreId == storeId
312   - && (x.BeautyType == "溯源系统" || x.BeautyType == "溯源")
313   - && x.Yjsj >= startDate && x.Yjsj <= endDate.AddDays(1))
314   - .Select(x => x.Jksyj)
315   - .ToListAsync();
316   -
317   - var storeTraceabilityBilling = storeTraceabilityBillingList
318   - .Where(x => !string.IsNullOrEmpty(x))
319   - .Sum(x => decimal.TryParse(x, out var val) ? val : 0m);
320   -
321   - // 该门店的退卡溯源金额(从退卡健康师业绩表统计)
322   - var storeTraceabilityRefund = await _db.Queryable<LqHytkJksyjEntity>()
323   - .Where(x => x.IsEffective == 1
324   - && x.StoreId == storeId
325   - && (x.BeautyType == "溯源系统" || x.BeautyType == "溯源")
326   - && x.Tksj >= startDate && x.Tksj <= endDate.AddDays(1))
327   - .SumAsync(x => (decimal?)x.Jksyj) ?? 0m;
328   -
329   - // 该门店的开单Cell金额(从健康师业绩表统计)
330   - var storeCellBillingList = await _db.Queryable<LqKdJksyjEntity>()
331   - .Where(x => x.IsEffective == 1
332   - && x.StoreId == storeId
333   - && (x.BeautyType == "cell" || x.BeautyType == "Cell")
334   - && x.Yjsj >= startDate && x.Yjsj <= endDate.AddDays(1))
335   - .Select(x => x.Jksyj)
336   - .ToListAsync();
337   -
338   - var storeCellBilling = storeCellBillingList
339   - .Where(x => !string.IsNullOrEmpty(x))
340   - .Sum(x => decimal.TryParse(x, out var val) ? val : 0m);
341   -
342   - // 该门店的退卡Cell金额(从退卡健康师业绩表统计)
343   - var storeCellRefund = await _db.Queryable<LqHytkJksyjEntity>()
344   - .Where(x => x.IsEffective == 1
345   - && x.StoreId == storeId
346   - && (x.BeautyType == "cell" || x.BeautyType == "Cell")
347   - && x.Tksj >= startDate && x.Tksj <= endDate.AddDays(1))
348   - .SumAsync(x => (decimal?)x.Jksyj) ?? 0m;
  417 + var storeTraceabilityBilling = storeTraceabilityBillingDict.ContainsKey(storeId) ? storeTraceabilityBillingDict[storeId] : 0m;
  418 + var storeTraceabilityRefund = storeTraceabilityRefundDict.ContainsKey(storeId) ? storeTraceabilityRefundDict[storeId] : 0m;
  419 + var storeCellBilling = storeCellBillingDict.ContainsKey(storeId) ? storeCellBillingDict[storeId] : 0m;
  420 + var storeCellRefund = storeCellRefundDict.ContainsKey(storeId) ? storeCellRefundDict[storeId] : 0m;
  421 +
  422 + // 调试日志:记录关键门店的计算结果
  423 + if (storeId == "1649328471923847173" || storeId == "1649328471923847175" || storeId == "1649328471923847187")
  424 + {
  425 + _logger.LogInformation($"[科技部总经理工资计算] 门店ID: {storeId}, 开单Cell金额: {storeCellBilling}, 退卡Cell金额: {storeCellRefund}, Cell金额: {storeCellBilling - storeCellRefund}");
  426 + }
349 427  
350 428 // 获取该门店属于哪些科技部总经理
351 429 // 通过门店的kjb字段确定:如果门店的kjb等于科技一部的组织ID,则该门店属于科技一部总经理
... ... @@ -445,6 +523,8 @@ namespace NCC.Extend
445 523 salary.TraceabilityAmount = totalTraceabilityAmount;
446 524 salary.CellAmount = totalCellAmount;
447 525  
  526 + _logger.LogInformation($"[科技部总经理工资计算] 员工: {salary.EmployeeName}, 溯源金额: {totalTraceabilityAmount}, Cell金额: {totalCellAmount}, 门店数: {storeDetails.Count}");
  527 +
448 528 // 2.5 保存门店明细(JSON格式)
449 529 salary.StoreDetail = JsonConvert.SerializeObject(storeDetails);
450 530  
... ... @@ -493,6 +573,8 @@ namespace NCC.Extend
493 573 managerStats[managerId] = salary;
494 574 }
495 575  
  576 + _logger.LogInformation($"[科技部总经理工资计算] 共计算了 {managerStats.Count} 个科技部总经理的工资数据");
  577 +
496 578 // 3. 保存数据
497 579 if (managerStats.Any())
498 580 {
... ... @@ -508,7 +590,12 @@ namespace NCC.Extend
508 590 if (existingDict.ContainsKey(salary.EmployeeId))
509 591 {
510 592 var existing = existingDict[salary.EmployeeId];
511   - if (existing.IsLocked == 1 || existing.EmployeeConfirmStatus == 1) { skippedCount++; continue; }
  593 + if (existing.IsLocked == 1 || existing.EmployeeConfirmStatus == 1)
  594 + {
  595 + _logger.LogWarning($"[科技部总经理工资计算] 跳过更新,员工: {salary.EmployeeName}, IsLocked: {existing.IsLocked}, EmployeeConfirmStatus: {existing.EmployeeConfirmStatus}");
  596 + skippedCount++;
  597 + continue;
  598 + }
512 599 salary.Id = existing.Id;
513 600 salary.EmployeeConfirmStatus = existing.EmployeeConfirmStatus;
514 601 salary.EmployeeConfirmTime = existing.EmployeeConfirmTime;
... ... @@ -516,6 +603,8 @@ namespace NCC.Extend
516 603 salary.IsLocked = existing.IsLocked;
517 604 salary.CreateTime = existing.CreateTime;
518 605 salary.CreateUser = existing.CreateUser;
  606 + salary.UpdateTime = DateTime.Now; // 强制更新UpdateTime
  607 + _logger.LogInformation($"[科技部总经理工资计算] 准备更新,员工: {salary.EmployeeName}, 旧Cell金额: {existing.CellAmount}, 新Cell金额: {salary.CellAmount}");
519 608 recordsToUpdate.Add(salary);
520 609 }
521 610 else
... ... @@ -529,7 +618,15 @@ namespace NCC.Extend
529 618 }
530 619 }
531 620 if (recordsToInsert.Any()) await _db.Insertable(recordsToInsert).ExecuteCommandAsync();
532   - if (recordsToUpdate.Any()) await _db.Updateable(recordsToUpdate).ExecuteCommandAsync();
  621 + if (recordsToUpdate.Any())
  622 + {
  623 + // 使用IgnoreColumns排除CreateTime和CreateUser,确保其他所有字段都被更新
  624 + await _db.Updateable(recordsToUpdate)
  625 + .IgnoreColumns(x => x.CreateTime)
  626 + .IgnoreColumns(x => x.CreateUser)
  627 + .ExecuteCommandAsync();
  628 + _logger.LogInformation($"已更新 {recordsToUpdate.Count} 条科技部总经理工资记录(月份:{monthStr})");
  629 + }
533 630 if (skippedCount > 0) _logger.LogWarning($"计算工资时跳过了 {skippedCount} 条已锁定或已确认的记录(月份:{monthStr})");
534 631 }
535 632 }
... ... @@ -626,7 +723,7 @@ namespace NCC.Extend
626 723 public decimal TraceabilityAmount { get; set; }
627 724 public decimal CellBillingAmount { get; set; }
628 725 public decimal CellRefundAmount { get; set; }
629   - public decimal CellAmount { get; set; }
  726 + public decimal CellAmount { get; set; }
630 727 }
631 728  
632 729 #region 员工工资确认
... ... @@ -793,11 +890,11 @@ namespace NCC.Extend
793 890  
794 891 if (lockedCount > 0 || unlockedCount > 0)
795 892 {
796   - var salariesToUpdate = salaries.Where(s =>
797   - (input.IsLocked && s.IsLocked == 0) ||
  893 + var salariesToUpdate = salaries.Where(s =>
  894 + (input.IsLocked && s.IsLocked == 0) ||
798 895 (!input.IsLocked && s.IsLocked == 1 && s.EmployeeConfirmStatus != 1)
799 896 ).ToList();
800   -
  897 +
801 898 if (salariesToUpdate.Any())
802 899 {
803 900 await _db.Updateable(salariesToUpdate)
... ... @@ -809,10 +906,10 @@ namespace NCC.Extend
809 906 var action = input.IsLocked ? "锁定" : "解锁";
810 907 var count = input.IsLocked ? lockedCount : unlockedCount;
811 908 var message = $"{action}成功:{count}条";
812   -
  909 +
813 910 if (alreadyLockedCount > 0)
814 911 message += $",跳过{alreadyLockedCount}条(已是{action}状态)";
815   -
  912 +
816 913 if (skippedCount > 0)
817 914 message += $",跳过{skippedCount}条(已确认的记录不能解锁)";
818 915  
... ... @@ -851,7 +948,7 @@ namespace NCC.Extend
851 948 {
852 949 if (file == null || file.Length == 0)
853 950 throw NCCException.Oh("请选择要上传的Excel文件");
854   -
  951 +
855 952 var allowedExtensions = new[] { ".xlsx", ".xls" };
856 953 var fileExtension = Path.GetExtension(file.FileName).ToLowerInvariant();
857 954 if (!allowedExtensions.Contains(fileExtension))
... ... @@ -901,7 +998,7 @@ namespace NCC.Extend
901 998  
902 999 var firstColumnValue = GetColumnValue(0);
903 1000 bool isOldFormat = !string.IsNullOrWhiteSpace(firstColumnValue) && (firstColumnValue == "员工姓名" || (!long.TryParse(firstColumnValue, out _) && firstColumnValue.Length > 20));
904   -
  1001 +
905 1002 int employeeNameIndex = isOldFormat ? 0 : 1;
906 1003 int offset = isOldFormat ? 0 : 1;
907 1004  
... ... @@ -932,7 +1029,7 @@ namespace NCC.Extend
932 1029 {
933 1030 existing = await _db.Queryable<LqTechGeneralManagerSalaryStatisticsEntity>()
934 1031 .Where(x => x.Id == id).FirstAsync();
935   -
  1032 +
936 1033 if (existing != null && (existing.IsLocked == 1 || existing.EmployeeConfirmStatus == 1))
937 1034 {
938 1035 skippedCount++;
... ... @@ -1008,7 +1105,7 @@ namespace NCC.Extend
1008 1105 if (user != null) entity.EmployeeId = user.Id;
1009 1106 }
1010 1107 }
1011   -
  1108 +
1012 1109 entity.UpdateTime = DateTime.Now;
1013 1110 if (existing != null) recordsToUpdate.Add(entity);
1014 1111 else recordsToInsert.Add(entity);
... ...
netcore/src/Modularity/Extend/NCC.Extend/LqTechTeacherSalaryService.cs
... ... @@ -416,7 +416,7 @@ namespace NCC.Extend
416 416 // 总业绩 > 30,000元:只计算2%提成
417 417 salary.BaseSalary = 0;
418 418 salary.BaseSalaryLevel = 0;
419   - salary.PerformanceCommissionRate = 2m;
  419 + salary.PerformanceCommissionRate = 0.02m; // 保存为小数形式,0.02表示2%
420 420 salary.PerformanceCommissionAmount = salary.TotalPerformance * 0.02m;
421 421 salary.ConsumeCommissionRate = 0;
422 422 salary.ConsumeCommissionAmount = 0;
... ... @@ -438,23 +438,45 @@ namespace NCC.Extend
438 438 {
439 439 // 在职员工正常计算
440 440  
441   - // 3.1 计算底薪(根据项目数和总业绩)
442   - var baseSalaryResult = CalculateBaseSalary(salary.ProjectCount, salary.TotalPerformance);
443   - salary.BaseSalary = baseSalaryResult.BaseSalary;
444   - salary.BaseSalaryLevel = baseSalaryResult.Level;
  441 + // 判断是否为T区员工(员工姓名包含"T区")
  442 + bool isTZoneEmployee = !string.IsNullOrEmpty(salary.EmployeeName) && salary.EmployeeName.Contains("T区");
445 443  
446   - // 3.2 计算业绩提成(分段累进)
447   - var performanceCommissionResult = CalculatePerformanceCommission(salary.TotalPerformance);
448   - salary.PerformanceCommissionRate = performanceCommissionResult.Rate;
449   - salary.PerformanceCommissionAmount = performanceCommissionResult.Amount;
  444 + if (isTZoneEmployee)
  445 + {
  446 + // T区员工:按照开单减去退款之后的业绩统一按照2%提成
  447 + // 总业绩 = 开单业绩 - 退卡业绩(已在前面计算)
  448 + salary.PerformanceCommissionRate = 0.02m; // 保存为小数形式,0.02表示2%
  449 + salary.PerformanceCommissionAmount = salary.TotalPerformance * 0.02m;
  450 + salary.ConsumeCommissionRate = 0;
  451 + salary.ConsumeCommissionAmount = 0;
  452 + salary.TotalCommission = salary.PerformanceCommissionAmount;
  453 + // T区员工也计算底薪(根据项目数和总业绩)
  454 + var baseSalaryResult = CalculateBaseSalary(salary.ProjectCount, salary.TotalPerformance);
  455 + salary.BaseSalary = baseSalaryResult.BaseSalary;
  456 + salary.BaseSalaryLevel = baseSalaryResult.Level;
  457 + }
  458 + else
  459 + {
  460 + // 非T区员工正常计算
  461 +
  462 + // 3.1 计算底薪(根据项目数和总业绩)
  463 + var baseSalaryResult = CalculateBaseSalary(salary.ProjectCount, salary.TotalPerformance);
  464 + salary.BaseSalary = baseSalaryResult.BaseSalary;
  465 + salary.BaseSalaryLevel = baseSalaryResult.Level;
450 466  
451   - // 3.3 计算消耗提成(分段累进,可能为负数)
452   - var consumeCommissionResult = CalculateConsumeCommission(salary.ConsumeAchievement);
453   - salary.ConsumeCommissionRate = consumeCommissionResult.Rate;
454   - salary.ConsumeCommissionAmount = consumeCommissionResult.Amount;
  467 + // 3.2 计算业绩提成(分段累进,门槛改为3万)
  468 + var performanceCommissionResult = CalculatePerformanceCommission(salary.TotalPerformance);
  469 + salary.PerformanceCommissionRate = performanceCommissionResult.Rate;
  470 + salary.PerformanceCommissionAmount = performanceCommissionResult.Amount;
455 471  
456   - // 3.4 提成合计
457   - salary.TotalCommission = salary.PerformanceCommissionAmount + salary.ConsumeCommissionAmount;
  472 + // 3.3 计算消耗提成(新规则:10万门槛,阶梯式)
  473 + var consumeCommissionResult = CalculateConsumeCommission(salary.ConsumeAchievement);
  474 + salary.ConsumeCommissionRate = consumeCommissionResult.Rate;
  475 + salary.ConsumeCommissionAmount = consumeCommissionResult.Amount;
  476 +
  477 + // 3.4 提成合计
  478 + salary.TotalCommission = salary.PerformanceCommissionAmount + salary.ConsumeCommissionAmount;
  479 + }
458 480 }
459 481  
460 482 // 3.5 初始化其他字段(默认值为0)
... ... @@ -603,30 +625,30 @@ namespace NCC.Extend
603 625 /// <returns>提成比例和金额</returns>
604 626 /// <remarks>
605 627 /// 提成规则(分段累进式):
606   - /// 1. 前提条件:业绩必须大于1万才能进行提成
  628 + /// 1. 前提条件:整月业绩必须大于等于3万才能进行提成(门槛从1万提高到3万)
607 629 /// 2. 如果有提成资格后,分段计算:
608 630 /// - 0-7万部分:2%(整个0-7万部分都按2%计算)
609 631 /// - 7万-15万部分:2.5%
610 632 /// - 15万以上部分:3%
611 633 ///
612 634 /// 计算公式(分段累进):
613   - /// - 如果业绩 ≤ 1万:提成 = 0(无提成资格)
614   - /// - 如果 1万 < 业绩 ≤ 7万:提成 = 业绩 × 2%
  635 + /// - 如果业绩 < 3万:提成 = 0(无提成资格)
  636 + /// - 如果 3万 ≤ 业绩 ≤ 7万:提成 = 业绩 × 2%
615 637 /// - 如果 7万 < 业绩 ≤ 15万:提成 = 7万 × 2% + (业绩 - 7万) × 2.5%
616 638 /// - 如果业绩 > 15万:提成 = 7万 × 2% + (15万 - 7万) × 2.5% + (业绩 - 15万) × 3%
617 639 ///
618 640 /// 示例:
619   - /// - 总业绩 = 5,000元 → 提成 = 0(无提成资格
  641 + /// - 总业绩 = 25,000元 → 提成 = 0(无提成资格,未达到3万门槛
620 642 /// - 总业绩 = 50,000元 → 提成 = 50,000 × 2% = 1,000元
621 643 /// - 总业绩 = 100,000元 → 提成 = 70,000 × 2% + (100,000 - 70,000) × 2.5% = 1,400 + 750 = 2,150元
622 644 /// - 总业绩 = 200,000元 → 提成 = 70,000 × 2% + (150,000 - 70,000) × 2.5% + (200,000 - 150,000) × 3% = 1,400 + 2,000 + 1,500 = 4,900元
623 645 /// </remarks>
624 646 private (decimal Rate, decimal Amount) CalculatePerformanceCommission(decimal totalPerformance)
625 647 {
626   - // 提成前提:业绩必须大于1万才能进行提成
627   - if (totalPerformance <= 10000m)
  648 + // 提成前提:整月业绩必须大于等于3万才能进行提成
  649 + if (totalPerformance < 30000m)
628 650 {
629   - // ≤ 10,000元 → 0%(无提成资格)
  651 + // < 30,000元 → 0%(无提成资格)
630 652 return (0m, 0m);
631 653 }
632 654  
... ... @@ -655,43 +677,61 @@ namespace NCC.Extend
655 677 }
656 678 else
657 679 {
658   - // 业绩 > 1万 且 ≤ 7万:整个业绩按2%计算
  680 + // 业绩 ≥ 3万 且 ≤ 7万:整个业绩按2%计算
659 681 totalCommission = totalPerformance * 0.02m;
660 682 }
661 683  
662   - // 计算平均提成比例(用于显示)
663   - decimal averageRate = totalCommission > 0 && totalPerformance > 0 ? (totalCommission / totalPerformance) * 100m : 0m;
  684 + // 计算平均提成比例(保存为小数形式,如0.02表示2%,前端会乘以100显示)
  685 + decimal averageRate = totalCommission > 0 && totalPerformance > 0 ? (totalCommission / totalPerformance) : 0m;
664 686  
665 687 return (averageRate, totalCommission);
666 688 }
667 689  
668 690 /// <summary>
669   - /// 计算消耗提成(分段累进,可能为负数)
  691 + /// 计算消耗提成(阶梯式,可能为负数)
670 692 /// </summary>
671 693 /// <param name="consumeAchievement">消耗业绩</param>
672 694 /// <returns>提成比例和金额(金额可能为负数,比例用于显示)</returns>
  695 + /// <remarks>
  696 + /// 消耗提成规则:
  697 + /// 1. 未完成10万底标:负激励300元(扣除300元)
  698 + /// 2. 达到10万条件后,按照阶梯式提成:
  699 + /// - 1-20万部分:0.5%
  700 + /// - 超过20万部分:1%
  701 + ///
  702 + /// 计算公式(阶梯式):
  703 + /// - 如果消耗业绩 < 10万:提成 = -300元(扣除300元)
  704 + /// - 如果消耗业绩 ≥ 10万 且 ≤ 20万:提成 = 消耗业绩 × 0.5%
  705 + /// - 如果消耗业绩 > 20万:提成 = 20万 × 0.5% + (消耗业绩 - 20万) × 1%
  706 + ///
  707 + /// 示例:
  708 + /// - 消耗业绩 = 50,000元 → 提成 = -300元(未完成10万底标)
  709 + /// - 消耗业绩 = 100,000元 → 提成 = 100,000 × 0.5% = 500元
  710 + /// - 消耗业绩 = 150,000元 → 提成 = 150,000 × 0.5% = 750元
  711 + /// - 消耗业绩 = 250,000元 → 提成 = 200,000 × 0.5% + (250,000 - 200,000) × 1% = 1,000 + 500 = 1,500元
  712 + /// </remarks>
673 713 private (decimal Rate, decimal Amount) CalculateConsumeCommission(decimal consumeAchievement)
674 714 {
675   - if (consumeAchievement < 80000m)
  715 + if (consumeAchievement < 100000m)
676 716 {
677   - // < 80,000元 → 扣除300元(负数)
  717 + // < 100,000元(未完成10万底标)→ 扣除300元(负数)
678 718 // 比例显示为0,金额为-300
679 719 return (0m, -300m);
680 720 }
681   - else if (consumeAchievement < 100000m)
682   - {
683   - // 80,000-100,000元 → 0.5%
684   - return (0.5m, consumeAchievement * 0.005m);
685   - }
686   - else if (consumeAchievement < 200000m)
  721 + else if (consumeAchievement <= 200000m)
687 722 {
688   - // 100,000-200,000元 → 0.5%
689   - return (0.5m, consumeAchievement * 0.005m);
  723 + // ≥ 100,000元 且 ≤ 200,000元 → 1-20万部分按0.5%
  724 + return (0.005m, consumeAchievement * 0.005m); // 保存为小数形式,0.005表示0.5%
690 725 }
691 726 else
692 727 {
693   - // > 200,000元 → 1%
694   - return (1m, consumeAchievement * 0.01m);
  728 + // > 200,000元 → 阶梯式:1-20万部分0.5%,超过20万部分1%
  729 + decimal part1 = 200000m * 0.005m; // 20万 × 0.5% = 1,000元
  730 + decimal part2 = (consumeAchievement - 200000m) * 0.01m; // 超过20万部分 × 1%
  731 + decimal totalCommission = part1 + part2;
  732 + // 计算平均比例(保存为小数形式,前端会乘以100显示)
  733 + decimal averageRate = totalCommission > 0 && consumeAchievement > 0 ? (totalCommission / consumeAchievement) : 0m;
  734 + return (averageRate, totalCommission);
695 735 }
696 736 }
697 737  
... ...
netcore/src/Modularity/System/NCC.System.Entitys/Mapper/SystemMapper.cs
... ... @@ -2,6 +2,7 @@
2 2 using NCC.System.Entitys.Dto.System.DbBackup;
3 3 using NCC.System.Entitys.Dto.System.Province;
4 4 using NCC.System.Entitys.Model.System.DataBase;
  5 +using NCC.System.Entitys.Model.Permission.UsersCurrent;
5 6 using NCC.System.Entitys.System;
6 7 using Mapster;
7 8 using SqlSugar;
... ... @@ -51,6 +52,8 @@ namespace NCC.System.Entitys.Mapper
51 52 .Map(dest => dest.dataLength, src => src.Length.ToString())
52 53 .Map(dest => dest.primaryKey, src => src.IsPrimarykey ? 1 : 0)
53 54 .Map(dest => dest.allowNull, src => src.IsNullable ? 1 : 0);
  55 + config.ForType<ModuleEntity, UsersCurrentAuthorizeMoldel>()
  56 + .Map(dest => dest.description, src => src.Description);
54 57 }
55 58 }
56 59 }
... ...
netcore/src/Modularity/System/NCC.System.Entitys/Model/Permission/UsersCurrent/UsersCurrentAuthorizeMoldel.cs
... ... @@ -30,5 +30,10 @@ namespace NCC.System.Entitys.Model.Permission.UsersCurrent
30 30 /// </summary>
31 31 [JsonIgnore]
32 32 public string moduleId { get; set; }
  33 +
  34 + /// <summary>
  35 + /// 备注
  36 + /// </summary>
  37 + public string description { get; set; }
33 38 }
34 39 }
... ...