Commit 0e7e34606d9e15c1252a444547ef06afe308bfbd

Authored by “wangming”
1 parent b16455bb

feat: 修改科技部老师工资提成为阶梯式,清洗流水接口返回送出/送回时间,主任工资合作成本查询使用YYYYMM格式

- 科技部老师工资:提成改为阶梯式(整个业绩按对应比例),业绩必须>1万才有提成
- 清洗流水:GetDifferenceList和GetInfo接口返回SendTime和ReturnTime字段
- 主任工资:合作成本查询使用monthStr(YYYYMM格式)匹配Month字段
- 更新科技部老师工资计算规则文档
excel/合作成本表.xlsx
No preview for this file type
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqInventoryUsage/StoreReceiveStatisticsInput.cs
... ... @@ -29,6 +29,13 @@ namespace NCC.Extend.Entitys.Dto.LqInventoryUsage
29 29 [StringLength(50, ErrorMessage = "门店ID长度不能超过50个字符")]
30 30 [Display(Name = "门店ID")]
31 31 public string StoreId { get; set; }
  32 +
  33 + /// <summary>
  34 + /// 仓库名称(可选,用于筛选特定仓库)
  35 + /// </summary>
  36 + [StringLength(100, ErrorMessage = "仓库名称长度不能超过100个字符")]
  37 + [Display(Name = "仓库名称")]
  38 + public string Warehouse { get; set; }
32 39 }
33 40 }
34 41  
... ...
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqLaundryFlow/LqLaundryFlowInfoOutput.cs
... ... @@ -109,6 +109,18 @@ namespace NCC.Extend.Entitys.Dto.LqLaundryFlow
109 109 /// </summary>
110 110 [Display(Name = "创建时间")]
111 111 public DateTime createTime { get; set; }
  112 +
  113 + /// <summary>
  114 + /// 送出时间(流水类型为0时使用)
  115 + /// </summary>
  116 + [Display(Name = "送出时间")]
  117 + public DateTime? sendTime { get; set; }
  118 +
  119 + /// <summary>
  120 + /// 送回时间(流水类型为1时使用)
  121 + /// </summary>
  122 + [Display(Name = "送回时间")]
  123 + public DateTime? returnTime { get; set; }
112 124 }
113 125 }
114 126  
... ...
netcore/src/Modularity/Extend/NCC.Extend/LqDirectorSalaryService.cs
... ... @@ -271,8 +271,10 @@ namespace NCC.Extend
271 271 .ToDictionary(x => x.StoreId.ToString(), x => Convert.ToDecimal(x.MaterialAmount ?? 0));
272 272  
273 273 // 1.10 合作项目成本统计
  274 + // Month字段格式为"11"(月份数字),不是"202511"(YYYYMM格式)
  275 + var cooperationCostMonth = $"{month:D2}"; // 格式化为"11"
274 276 var cooperationCostList = await _db.Queryable<LqCooperationCostEntity>()
275   - .Where(x => x.Year == year && x.Month == monthStr && x.IsEffective == StatusEnum.有效.GetHashCode())
  277 + .Where(x => x.Year == year && x.Month == cooperationCostMonth && x.IsEffective == StatusEnum.有效.GetHashCode())
276 278 .Select(x => new { x.StoreId, x.TotalAmount })
277 279 .ToListAsync();
278 280 var cooperationCostDict = cooperationCostList
... ... @@ -372,26 +374,31 @@ namespace NCC.Extend
372 374 // 2.3 获取门店目标信息(门店生命线、目标人头、目标消耗)
373 375 if (!storeTargetDict.ContainsKey(storeId))
374 376 {
375   - throw new Exception($"门店【{salary.StoreName ?? storeId}】在门店目标表中未配置{monthStr}月份的目标数据,无法计算主任工资");
  377 + // 如果没有配置目标数据,使用默认值0
  378 + salary.StoreLifeline = 0;
  379 + salary.TargetHeadCount = 0;
  380 + salary.TargetConsume = 0;
  381 + }
  382 + else
  383 + {
  384 + var storeTarget = storeTargetDict[storeId];
  385 + salary.StoreLifeline = storeTarget.StoreLifeline;
  386 + salary.TargetHeadCount = storeTarget.StoreHeadcountTarget;
  387 + salary.TargetConsume = storeTarget.StoreConsumeTarget;
376 388 }
377 389  
378   - var storeTarget = storeTargetDict[storeId];
379   - salary.StoreLifeline = storeTarget.StoreLifeline;
380   - salary.TargetHeadCount = storeTarget.StoreHeadcountTarget;
381   - salary.TargetConsume = storeTarget.StoreConsumeTarget;
382   -
383   - // 数据校验:门店生命线、目标人头、目标消耗必须设置
  390 + // 如果目标值未设置(<=0),使用默认值0,允许继续计算
384 391 if (salary.StoreLifeline <= 0)
385 392 {
386   - throw new Exception($"门店【{salary.StoreName ?? storeId}】的门店生命线未设置,无法计算主任工资");
  393 + salary.StoreLifeline = 0;
387 394 }
388 395 if (salary.TargetHeadCount <= 0)
389 396 {
390   - throw new Exception($"门店【{salary.StoreName ?? storeId}】的目标人头数未设置,无法计算主任工资");
  397 + salary.TargetHeadCount = 0;
391 398 }
392   - if (!isNewStore && salary.TargetConsume <= 0)
  399 + if (salary.TargetConsume <= 0)
393 400 {
394   - throw new Exception($"门店【{salary.StoreName ?? storeId}】的目标消耗未设置,无法计算主任工资(老店需要考核消耗)");
  401 + salary.TargetConsume = 0;
395 402 }
396 403  
397 404 // 2.4 计算销售业绩(开单业绩-退款业绩)
... ...
netcore/src/Modularity/Extend/NCC.Extend/LqInventoryUsageService.cs
... ... @@ -1590,7 +1590,8 @@ namespace NCC.Extend
1590 1590 /// {
1591 1591 /// "year": 2025,
1592 1592 /// "month": 11,
1593   - /// "storeId": "门店ID(可选)"
  1593 + /// "storeId": "门店ID(可选)",
  1594 + /// "warehouse": "仓库名称(可选)"
1594 1595 /// }
1595 1596 /// ```
1596 1597 ///
... ... @@ -1598,6 +1599,7 @@ namespace NCC.Extend
1598 1599 /// - year: 统计年份(必填,范围:2020-2100)
1599 1600 /// - month: 统计月份(必填,范围:1-12)
1600 1601 /// - storeId: 门店ID(可选,不传则统计所有门店)
  1602 + /// - warehouse: 仓库名称(可选,不传则统计所有仓库)
1601 1603 ///
1602 1604 /// 返回说明:
1603 1605 /// - 按门店分组统计
... ... @@ -1651,15 +1653,25 @@ namespace NCC.Extend
1651 1653 // 获取所有批次ID
1652 1654 var batchIds = applications.Select(x => x.UsageBatchId).Distinct().ToList();
1653 1655  
1654   - // 查询这些批次的使用记录,计算总金额
1655   - var usageRecords = await _db.Queryable<LqInventoryUsageEntity>()
1656   - .Where(x => batchIds.Contains(x.UsageBatchId))
1657   - .Where(x => x.IsEffective == StatusEnum.有效.GetHashCode())
1658   - .Select(x => new
  1656 + // 查询这些批次的使用记录,关联产品表获取仓库信息,计算总金额
  1657 + var usageRecordsQuery = _db.Queryable<LqInventoryUsageEntity, LqProductEntity>(
  1658 + (usage, product) => usage.ProductId == product.Id)
  1659 + .Where((usage, product) => batchIds.Contains(usage.UsageBatchId))
  1660 + .Where((usage, product) => usage.IsEffective == StatusEnum.有效.GetHashCode());
  1661 +
  1662 + // 如果指定了仓库筛选,添加仓库条件
  1663 + if (!string.IsNullOrWhiteSpace(input.Warehouse))
  1664 + {
  1665 + usageRecordsQuery = usageRecordsQuery.Where((usage, product) => product.Warehouse == input.Warehouse);
  1666 + }
  1667 +
  1668 + var usageRecords = await usageRecordsQuery
  1669 + .Select((usage, product) => new
1659 1670 {
1660   - x.UsageBatchId,
1661   - x.StoreId,
1662   - x.TotalAmount
  1671 + usage.UsageBatchId,
  1672 + usage.StoreId,
  1673 + usage.TotalAmount,
  1674 + product.Warehouse
1663 1675 })
1664 1676 .ToListAsync();
1665 1677  
... ...
netcore/src/Modularity/Extend/NCC.Extend/LqLaundryFlowService.cs
... ... @@ -473,7 +473,9 @@ namespace NCC.Extend
473 473 isEffective = flow.IsEffective,
474 474 createUser = flow.CreateUser,
475 475 createUserName = "",
476   - createTime = flow.CreateTime
  476 + createTime = flow.CreateTime,
  477 + sendTime = flow.SendTime,
  478 + returnTime = flow.ReturnTime
477 479 })
478 480 .FirstAsync();
479 481  
... ... @@ -533,7 +535,7 @@ namespace NCC.Extend
533 535 StoreName = store.Dm ?? "",
534 536 flow.ProductType,
535 537 SendQuantity = flow.Quantity,
536   - SendTime = flow.CreateTime
  538 + SendTime = flow.SendTime
537 539 })
538 540 .ToListAsync();
539 541  
... ... @@ -558,7 +560,7 @@ namespace NCC.Extend
558 560 {
559 561 x.BatchNumber,
560 562 ReturnQuantity = x.Quantity,
561   - ReturnTime = x.CreateTime,
  563 + ReturnTime = x.ReturnTime,
562 564 x.Remark
563 565 })
564 566 .ToListAsync();
... ...
netcore/src/Modularity/Extend/NCC.Extend/LqSalaryExtraCalculationService.cs
... ... @@ -318,27 +318,39 @@ namespace NCC.Extend
318 318 var entitiesToInsert = new List<LqSalaryExtraCalculationEntity>();
319 319 var entitiesToUpdate = new List<LqSalaryExtraCalculationEntity>();
320 320  
  321 + // 用于跟踪已处理的员工记录,避免重复(Key: EmployeeId_Year_Month)
  322 + var processedEmployeeRecords = new Dictionary<string, LqSalaryExtraCalculationEntity>();
  323 +
321 324 foreach (var item in importData)
322 325 {
323 326 try
324 327 {
325   - // 1. 查找用户ID(优先使用ID,否则通过姓名和电话查找)
  328 + // 1. 查找用户ID(优先使用Excel中的id作为员工ID,否则通过姓名和电话查找)
326 329 UserEntity user = null;
327 330 LqSalaryExtraCalculationEntity existingRecord = null;
328 331  
329   - // 如果提供了ID,先尝试根据ID查找现有记录,获取EmployeeId
  332 + // 优先使用Excel中的id作为员工ID(EmployeeId)直接查找用户
330 333 if (!string.IsNullOrEmpty(item.Id))
331 334 {
332   - existingRecord = await _db.Queryable<LqSalaryExtraCalculationEntity>()
333   - .Where(x => x.Id == item.Id)
  335 + // 先尝试将id作为员工ID查找用户
  336 + user = await _db.Queryable<UserEntity>()
  337 + .Where(u => u.Id == item.Id)
334 338 .FirstAsync();
335 339  
336   - if (existingRecord != null)
  340 + // 如果找不到,尝试将id作为记录ID查找现有记录,获取EmployeeId
  341 + if (user == null)
337 342 {
338   - // 通过EmployeeId查找用户
339   - user = await _db.Queryable<UserEntity>()
340   - .Where(u => u.Id == existingRecord.EmployeeId)
  343 + existingRecord = await _db.Queryable<LqSalaryExtraCalculationEntity>()
  344 + .Where(x => x.Id == item.Id)
341 345 .FirstAsync();
  346 +
  347 + if (existingRecord != null)
  348 + {
  349 + // 通过EmployeeId查找用户
  350 + user = await _db.Queryable<UserEntity>()
  351 + .Where(u => u.Id == existingRecord.EmployeeId)
  352 + .FirstAsync();
  353 + }
342 354 }
343 355 }
344 356  
... ... @@ -382,7 +394,27 @@ namespace NCC.Extend
382 394 continue;
383 395 }
384 396  
385   - // 2. 如果还没有找到现有记录,则根据健康师ID、年份、月份查找
  397 + // 2. 检查是否已经在本次导入中处理过该员工(避免重复,使用员工ID而不是姓名)
  398 + var recordKey = $"{user.Id}_{item.Year}_{item.Month}";
  399 + if (processedEmployeeRecords.ContainsKey(recordKey))
  400 + {
  401 + // 如果已经处理过,更新记录而不是创建新记录
  402 + existingRecord = processedEmployeeRecords[recordKey];
  403 + existingRecord.BaseRewardPerformance = item.BaseRewardPerformance;
  404 + existingRecord.CooperationRewardPerformance = item.CooperationRewardPerformance;
  405 + existingRecord.NewCustomerPerformance = item.NewCustomerPerformance;
  406 + existingRecord.NewCustomerConversionRate = item.NewCustomerConversionRate;
  407 + existingRecord.UpgradePerformance = item.UpgradePerformance;
  408 + existingRecord.UpgradeConversionRate = item.UpgradeConversionRate;
  409 + existingRecord.UpgradeCustomerCount = item.UpgradeCustomerCount;
  410 + existingRecord.OtherPerformanceAdd = item.OtherPerformanceAdd;
  411 + existingRecord.OtherPerformanceSubtract = item.OtherPerformanceSubtract;
  412 + // 注意:这里不添加到entitiesToUpdate,因为已经在processedEmployeeRecords中,会在最后统一处理
  413 + successCount++;
  414 + continue;
  415 + }
  416 +
  417 + // 3. 如果还没有找到现有记录,则根据健康师ID、年份、月份查找数据库中的记录
386 418 if (existingRecord == null)
387 419 {
388 420 existingRecord = await _db.Queryable<LqSalaryExtraCalculationEntity>()
... ... @@ -403,6 +435,8 @@ namespace NCC.Extend
403 435 existingRecord.OtherPerformanceAdd = item.OtherPerformanceAdd;
404 436 existingRecord.OtherPerformanceSubtract = item.OtherPerformanceSubtract;
405 437 entitiesToUpdate.Add(existingRecord);
  438 + // 记录到已处理字典中
  439 + processedEmployeeRecords[recordKey] = existingRecord;
406 440 }
407 441 else
408 442 {
... ... @@ -424,6 +458,8 @@ namespace NCC.Extend
424 458 OtherPerformanceSubtract = item.OtherPerformanceSubtract
425 459 };
426 460 entitiesToInsert.Add(entity);
  461 + // 记录到已处理字典中(注意:这里需要创建一个临时对象,因为entity还没有ID)
  462 + processedEmployeeRecords[recordKey] = entity;
427 463 }
428 464  
429 465 successCount++;
... ... @@ -441,12 +477,18 @@ namespace NCC.Extend
441 477 await _db.Insertable(entitiesToInsert).ExecuteCommandAsync();
442 478 }
443 479  
444   - // 批量更新现有记录
  480 + // 批量更新现有记录(去重,确保每个员工只有一条记录被更新)
445 481 if (entitiesToUpdate.Any())
446 482 {
  483 + // 根据主键ID去重,确保每个记录只更新一次
  484 + var uniqueEntitiesToUpdate = entitiesToUpdate
  485 + .GroupBy(x => x.Id)
  486 + .Select(g => g.First())
  487 + .ToList();
  488 +
447 489 // 明确指定要更新的字段,确保所有字段都被更新(包括升单业绩)
448 490 // 注意:Updateable接收实体列表时,会自动根据主键更新,不需要Where条件
449   - await _db.Updateable(entitiesToUpdate)
  491 + await _db.Updateable(uniqueEntitiesToUpdate)
450 492 .UpdateColumns(it => new
451 493 {
452 494 it.BaseRewardPerformance,
... ...
netcore/src/Modularity/Extend/NCC.Extend/LqStoreManagerSalaryService.cs
... ... @@ -384,26 +384,31 @@ namespace NCC.Extend
384 384 // 2.3 获取门店目标信息(门店生命线、目标人头、目标消耗)
385 385 if (!storeTargetDict.ContainsKey(storeId))
386 386 {
387   - throw new Exception($"门店【{salary.StoreName ?? storeId}】在门店目标表中未配置{monthStr}月份的目标数据,无法计算店长工资");
  387 + // 如果没有配置目标数据,使用默认值0
  388 + salary.StoreLifeline = 0;
  389 + salary.TargetHeadCount = 0;
  390 + salary.TargetConsume = 0;
  391 + }
  392 + else
  393 + {
  394 + var storeTarget = storeTargetDict[storeId];
  395 + salary.StoreLifeline = storeTarget.StoreLifeline;
  396 + salary.TargetHeadCount = storeTarget.StoreHeadcountTarget;
  397 + salary.TargetConsume = storeTarget.StoreConsumeTarget;
388 398 }
389 399  
390   - var storeTarget = storeTargetDict[storeId];
391   - salary.StoreLifeline = storeTarget.StoreLifeline;
392   - salary.TargetHeadCount = storeTarget.StoreHeadcountTarget;
393   - salary.TargetConsume = storeTarget.StoreConsumeTarget;
394   -
395   - // 数据校验:门店生命线、目标人头必须设置
  400 + // 如果目标值未设置(<=0),使用默认值0,允许继续计算
396 401 if (salary.StoreLifeline <= 0)
397 402 {
398   - throw new Exception($"门店【{salary.StoreName ?? storeId}】的门店生命线未设置,无法计算店长工资");
  403 + salary.StoreLifeline = 0;
399 404 }
400 405 if (salary.TargetHeadCount <= 0)
401 406 {
402   - throw new Exception($"门店【{salary.StoreName ?? storeId}】的目标人头数未设置,无法计算店长工资");
  407 + salary.TargetHeadCount = 0;
403 408 }
404   - if (!isNewStore && salary.TargetConsume <= 0)
  409 + if (salary.TargetConsume <= 0)
405 410 {
406   - throw new Exception($"门店【{salary.StoreName ?? storeId}】的目标消耗未设置,无法计算店长工资(老店需要考核消耗)");
  411 + salary.TargetConsume = 0;
407 412 }
408 413  
409 414 // 2.4 计算门店业绩
... ...
netcore/src/Modularity/Extend/NCC.Extend/LqTechTeacherSalaryService.cs
... ... @@ -453,32 +453,43 @@ namespace NCC.Extend
453 453 }
454 454  
455 455 /// <summary>
456   - /// 计算业绩提成(分段累进
  456 + /// 计算业绩提成(阶梯式
457 457 /// </summary>
458 458 /// <param name="totalPerformance">总业绩</param>
459 459 /// <returns>提成比例和金额</returns>
460 460 private (decimal Rate, decimal Amount) CalculatePerformanceCommission(decimal totalPerformance)
461 461 {
462   - if (totalPerformance < 10000m)
  462 + // 提成前提:业绩必须大于1万才能进行提成
  463 + if (totalPerformance <= 10000m)
463 464 {
464   - // < 10,000元 → 0%
  465 + // ≤ 10,000元 → 0%(无提成)
465 466 return (0m, 0m);
466 467 }
467   - else if (totalPerformance < 70000m)
  468 +
  469 + decimal rate;
  470 + decimal amount;
  471 +
  472 + // 阶梯式提成计算(整个业绩按对应比例)
  473 + if (totalPerformance > 150000m)
468 474 {
469   - // 10,000-70,000元 → 2%
470   - return (2m, totalPerformance * 0.02m);
  475 + // > 15万 → 3%
  476 + rate = 3m;
  477 + amount = totalPerformance * 0.03m;
471 478 }
472   - else if (totalPerformance < 150000m)
  479 + else if (totalPerformance > 70000m)
473 480 {
474   - // 70,000-150,000元 → 2.5%
475   - return (2.5m, totalPerformance * 0.025m);
  481 + // > 7万 且 ≤ 15万 → 2.5%
  482 + rate = 2.5m;
  483 + amount = totalPerformance * 0.025m;
476 484 }
477 485 else
478 486 {
479   - // > 150,000元 → 3%
480   - return (3m, totalPerformance * 0.03m);
  487 + // > 1万 且 ≤ 7万 → 2%
  488 + rate = 2m;
  489 + amount = totalPerformance * 0.02m;
481 490 }
  491 +
  492 + return (rate, amount);
482 493 }
483 494  
484 495 /// <summary>
... ...
主任工资计算逻辑完整梳理.md 0 → 100644
  1 +# 主任工资计算逻辑完整梳理
  2 +
  3 +## 📋 概述
  4 +
  5 +主任工资由以下几个部分组成:
  6 +1. **底薪**:固定3500元,根据考核指标扣款
  7 +2. **提成**:基于**毛利**计算,使用阶梯提成模式
  8 +
  9 +**重要说明**:
  10 +- **业绩(StoreTotalPerformance)就是毛利**
  11 +- **提成计算基于毛利**,而不是销售业绩(开单-退卡)
  12 +
  13 +---
  14 +
  15 +## 💰 核心计算公式
  16 +
  17 +### 1. 销售业绩
  18 +
  19 +```
  20 +销售业绩 = 开单业绩 - 退款业绩
  21 +```
  22 +
  23 +### 2. 毛利(核心指标)
  24 +
  25 +```
  26 +毛利 = 销售业绩 - - 合作项目成本 - 店内支出 - 洗毛巾费用
  27 +```
  28 +
  29 +### 3. 业绩(用于提成计算)
  30 +
  31 +```
  32 +业绩(StoreTotalPerformance)= 毛利(GrossProfit)
  33 +```
  34 +
  35 +**关键点**:`StoreTotalPerformance` 字段存储的是**毛利**,用于提成计算。
  36 +
  37 +---
  38 +
  39 +## 📊 数据来源详解
  40 +
  41 +### 1. 开单业绩
  42 +
  43 +**数据来源**:
  44 +- 表:`lq_kd_kdjlb`(开单记录表)
  45 +- 字段:`sfyj`(实付业绩)
  46 +- 条件:
  47 + - `F_IsEffective = 1`(有效记录)
  48 + - `Djmd = @StoreId`(门店ID)
  49 + - `Kdrq >= @StartDate AND Kdrq <= @EndDate`(时间范围)
  50 +
  51 +**代码位置**:第185-192行
  52 +
  53 +### 2. 退款业绩
  54 +
  55 +**数据来源**:
  56 +- 表:`lq_hytk_hytk`(退卡记录表)
  57 +- 字段:`F_ActualRefundAmount`(实际退款金额,如果没有则使用 `tkje`)
  58 +- 条件:
  59 + - `F_IsEffective = 1`(有效记录)
  60 + - `md = @StoreId`(门店ID)
  61 + - `Tksj >= @StartDate AND Tksj <= @EndDate`(时间范围)
  62 +
  63 +**代码位置**:第194-202行
  64 +
  65 +### 3. 产品物料
  66 +
  67 +**数据来源**:
  68 +- 表:`lq_inventory_usage`(库存使用记录表)
  69 +- 字段:`F_TotalAmount`(合计金额)
  70 +- 条件:
  71 + - `F_IsEffective = 1`(有效记录)
  72 + - `F_StoreId = @StoreId`(门店ID)
  73 + - **特殊规则**:11月工资算10月数据
  74 + - 如果计算月份是11月,则查询10月的数据
  75 + - 其他月份正常查询当月数据
  76 +
  77 +**代码位置**:第252-271行
  78 +
  79 +### 4. 合作项目成本
  80 +
  81 +**数据来源**:
  82 +- 表:`lq_cooperation_cost`(合作成本表)
  83 +- 字段:`F_TotalAmount`(合计金额)
  84 +- 条件:
  85 + - `F_Year = @Year`(年份)
  86 + - `F_Month = @MonthStr`(月份,YYYYMM格式)
  87 + - `F_StoreId = @StoreId`(门店ID)
  88 + - `F_IsEffective = 1`(有效记录)
  89 +
  90 +**代码位置**:第273-281行
  91 +
  92 +### 5. 店内支出
  93 +
  94 +**数据来源**:
  95 +- 表:`lq_store_expense`(店内支出表)
  96 +- 字段:`F_Amount`(金额)
  97 +- 条件:
  98 + - `F_IsEffective = 1`(有效记录)
  99 + - `F_StoreId = @StoreId`(门店ID)
  100 + - `DATE_FORMAT(F_ExpenseDate, '%Y%m') = @MonthStr`(月份,YYYYMM格式)
  101 +
  102 +**代码位置**:第283-296行
  103 +
  104 +### 6. 洗毛巾费用
  105 +
  106 +**数据来源**:
  107 +- 表:`lq_laundry_flow`(清洗流水表)
  108 +- 字段:`F_TotalPrice`(总费用)
  109 +- 条件:
  110 + - `F_IsEffective = 1`(有效记录)
  111 + - `F_FlowType = 0`(只统计送出的记录)
  112 + - `F_StoreId = @StoreId`(门店ID)
  113 + - 优先使用 `F_SendTime`,如果为空则使用 `F_CreateTime`
  114 + - `DATE_FORMAT(COALESCE(F_SendTime, F_CreateTime), '%Y%m') = @MonthStr`(月份,YYYYMM格式)
  115 +
  116 +**代码位置**:第298-313行
  117 +
  118 +---
  119 +
  120 +## 🔄 计算流程
  121 +
  122 +### 步骤1:计算销售业绩
  123 +
  124 +```csharp
  125 +// 2.4 计算销售业绩(开单业绩-退款业绩)
  126 +decimal billing = storeBillingDict.ContainsKey(storeId) ? storeBillingDict[storeId] : 0;
  127 +decimal refund = storeRefundDict.ContainsKey(storeId) ? storeRefundDict[storeId] : 0;
  128 +salary.StoreBillingPerformance = billing;
  129 +salary.StoreRefundPerformance = refund;
  130 +salary.SalesPerformance = billing - refund;
  131 +```
  132 +
  133 +**代码位置**:第402-407行
  134 +
  135 +### 步骤2:统计各项成本
  136 +
  137 +```csharp
  138 +// 2.5 统计各项成本
  139 +salary.ProductMaterial = productMaterialDict.ContainsKey(storeId) ? productMaterialDict[storeId] : 0;
  140 +salary.CooperationCost = cooperationCostDict.ContainsKey(storeId) ? cooperationCostDict[storeId] : 0;
  141 +salary.StoreExpense = storeExpenseDict.ContainsKey(storeId) ? storeExpenseDict[storeId] : 0;
  142 +salary.LaundryCost = laundryCostDict.ContainsKey(storeId) ? laundryCostDict[storeId] : 0;
  143 +```
  144 +
  145 +**代码位置**:第409-420行
  146 +
  147 +### 步骤3:计算毛利
  148 +
  149 +```csharp
  150 +// 2.6 计算毛利
  151 +// 毛利 = 销售业绩 - 产品物料 - 合作项目成本 - 店内支出 - 洗毛巾
  152 +salary.GrossProfit = salary.SalesPerformance - salary.ProductMaterial - salary.CooperationCost - salary.StoreExpense - salary.LaundryCost;
  153 +```
  154 +
  155 +**代码位置**:第422-424行
  156 +
  157 +### 步骤4:将毛利赋值给StoreTotalPerformance(用于提成计算)
  158 +
  159 +```csharp
  160 +// 2.7 将毛利赋值给StoreTotalPerformance(用于提成计算)
  161 +salary.StoreTotalPerformance = salary.GrossProfit;
  162 +```
  163 +
  164 +**代码位置**:第426-427行
  165 +
  166 +**关键点**:`StoreTotalPerformance` 存储的是**毛利**,不是销售业绩。
  167 +
  168 +### 步骤5:计算业绩完成率(基于毛利)
  169 +
  170 +```csharp
  171 +// 2.8 计算业绩完成率(基于毛利与生命线比较)
  172 +if (salary.StoreLifeline > 0)
  173 +{
  174 + salary.PerformanceCompletionRate = salary.GrossProfit / salary.StoreLifeline;
  175 +}
  176 +```
  177 +
  178 +**代码位置**:第429-437行
  179 +
  180 +### 步骤6:判断业绩是否达标(基于毛利)
  181 +
  182 +```csharp
  183 +// 2.11 计算考核指标(业绩、人头、消耗是否达标)
  184 +// 业绩达标判断基于毛利
  185 +bool performanceReached = salary.GrossProfit >= salary.StoreLifeline;
  186 +```
  187 +
  188 +**代码位置**:第445-447行
  189 +
  190 +### 步骤7:计算提成(基于毛利)
  191 +
  192 +```csharp
  193 +// 2.14 计算阶梯提成(先计算门店总提成,基于毛利)
  194 +CalculateCommission(salary, isNewStore);
  195 +```
  196 +
  197 +**代码位置**:第495-496行
  198 +
  199 +**提成计算方法**:
  200 +
  201 +```csharp
  202 +// 提成计算基于毛利(StoreTotalPerformance存储的是毛利)
  203 +decimal performance = salary.StoreTotalPerformance; // 这里已经是毛利了
  204 +decimal lifeline = salary.StoreLifeline;
  205 +
  206 +// 计算阶梯提成
  207 +if (performance <= lifeline)
  208 +{
  209 + // 业绩 ≤ 生命线:只计算≤生命线部分的提成
  210 + salary.CommissionAmountBelowLifeline = performance * rateBelowLifeline;
  211 + salary.CommissionAmountAboveLifeline = 0;
  212 +}
  213 +else
  214 +{
  215 + // 业绩 > 生命线:分别计算≤生命线部分和>生命线部分的提成
  216 + salary.CommissionAmountBelowLifeline = lifeline * rateBelowLifeline;
  217 + salary.CommissionAmountAboveLifeline = (performance - lifeline) * rateAboveLifeline;
  218 +}
  219 +```
  220 +
  221 +**代码位置**:第567-640行
  222 +
  223 +---
  224 +
  225 +## 💰 提成规则详解
  226 +
  227 +### 提成计算方式
  228 +
  229 +**阶梯提成模式**:根据业绩是否超过生命线,使用不同的提成比例。
  230 +
  231 +### 老店主任提成规则
  232 +
  233 +根据门店分类(A、B、C类)和业绩是否超过生命线,使用不同的阶梯提成比例:
  234 +
  235 +| 门店分类 | 业绩 ≤ 生命线部分 | 业绩 > 生命线部分 |
  236 +|---------|----------------|-----------------|
  237 +| A类门店 | 2% | 2.5% |
  238 +| B类门店 | 2.5% | 3% |
  239 +| C类门店 | 3% | 3.5% |
  240 +
  241 +**计算公式**:
  242 +```
  243 +如果 业绩(毛利)≤ 生命线:
  244 + 提成 = 业绩(毛利) × 对应提成比例(≤生命线部分)
  245 +
  246 +如果 业绩(毛利)> 生命线:
  247 + 提成 = 生命线 × 对应提成比例(≤生命线部分) + (业绩(毛利) - 生命线) × 对应提成比例(>生命线部分)
  248 +```
  249 +
  250 +### 新店主任提成规则
  251 +
  252 +**统一标准**,不区分门店分类:
  253 +
  254 +| 业绩范围 | 提成比例 |
  255 +|---------|---------|
  256 +| 业绩 ≤ 生命线部分 | 2% |
  257 +| 业绩 > 生命线部分 | 2.5% |
  258 +
  259 +**计算公式**:
  260 +```
  261 +如果 业绩(毛利)≤ 生命线:
  262 + 提成 = 业绩(毛利) × 2%
  263 +
  264 +如果 业绩(毛利)> 生命线:
  265 + 提成 = 生命线 × 2% + (业绩(毛利) - 生命线) × 2.5%
  266 +```
  267 +
  268 +### 提成按在店天数比例计算
  269 +
  270 +```csharp
  271 +// 2.15 按在店天数比例计算提成金额
  272 +if (daysInMonth > 0 && workingDays > 0)
  273 +{
  274 + // 提成金额按在店天数比例计算
  275 + salary.CommissionAmountBelowLifeline = salary.CommissionAmountBelowLifeline / daysInMonth * workingDays;
  276 + salary.CommissionAmountAboveLifeline = salary.CommissionAmountAboveLifeline / daysInMonth * workingDays;
  277 + salary.TotalCommissionAmount = salary.CommissionAmountBelowLifeline + salary.CommissionAmountAboveLifeline;
  278 +}
  279 +```
  280 +
  281 +**代码位置**:第498-512行
  282 +
  283 +---
  284 +
  285 +## 📝 关键字段说明
  286 +
  287 +### StoreTotalPerformance(门店总业绩)
  288 +
  289 +**字段含义**:**毛利**
  290 +
  291 +**计算公式**:
  292 +```
  293 +StoreTotalPerformance = GrossProfit = 销售业绩 - 产品物料 - 合作项目成本 - 店内支出 - 洗毛巾费用
  294 +```
  295 +
  296 +**用途**:
  297 +- 用于提成计算
  298 +- 用于业绩完成率计算
  299 +- 用于业绩达标判断
  300 +
  301 +**重要说明**:此字段存储的是**毛利**,不是销售业绩(开单-退卡)。
  302 +
  303 +### GrossProfit(毛利)
  304 +
  305 +**字段含义**:毛利
  306 +
  307 +**计算公式**:
  308 +```
  309 +GrossProfit = SalesPerformance - ProductMaterial - CooperationCost - StoreExpense - LaundryCost
  310 +```
  311 +
  312 +### SalesPerformance(销售业绩)
  313 +
  314 +**字段含义**:销售业绩(开单-退卡)
  315 +
  316 +**计算公式**:
  317 +```
  318 +SalesPerformance = StoreBillingPerformance - StoreRefundPerformance
  319 +```
  320 +
  321 +---
  322 +
  323 +## ✅ 验证要点
  324 +
  325 +### 1. 业绩就是毛利
  326 +
  327 +- ✅ `StoreTotalPerformance = GrossProfit`(第427行)
  328 +- ✅ 提成计算使用 `StoreTotalPerformance`(第581行)
  329 +- ✅ 业绩完成率使用 `GrossProfit`(第432行)
  330 +- ✅ 业绩达标判断使用 `GrossProfit`(第447行)
  331 +
  332 +### 2. 提成计算基于毛利
  333 +
  334 +- ✅ `CalculateCommission` 方法注释明确说明"基于毛利"(第563行)
  335 +- ✅ 方法内部使用 `StoreTotalPerformance`(即毛利)进行计算(第581行)
  336 +- ✅ 所有提成计算都基于 `performance`(即毛利)
  337 +
  338 +---
  339 +
  340 +## 📋 总结
  341 +
  342 +### 核心结论
  343 +
  344 +1. **业绩(StoreTotalPerformance)就是毛利**
  345 + - `StoreTotalPerformance = GrossProfit`
  346 + - 不是销售业绩(开单-退卡)
  347 +
  348 +2. **提成计算基于毛利**
  349 + - 提成计算使用 `StoreTotalPerformance`(即毛利)
  350 + - 不是基于销售业绩
  351 +
  352 +3. **毛利计算公式**
  353 + ```
  354 + 毛利 = 销售业绩 - 产品物料 - 合作项目成本 - 店内支出 - 洗毛巾费用
  355 + ```
  356 +
  357 +4. **所有业绩相关的判断都基于毛利**
  358 + - 业绩完成率 = 毛利 / 生命线
  359 + - 业绩达标判断 = 毛利 >= 生命线
  360 + - 提成计算 = 基于毛利使用阶梯提成
  361 +
  362 +### 数据流向
  363 +
  364 +```
  365 +开单业绩 - 退款业绩
  366 + ↓
  367 +销售业绩(SalesPerformance)
  368 + ↓
  369 +销售业绩 - 产品物料 - 合作项目成本 - 店内支出 - 洗毛巾费用
  370 + ↓
  371 +毛利(GrossProfit)
  372 + ↓
  373 +StoreTotalPerformance(用于提成计算)
  374 + ↓
  375 +阶梯提成计算
  376 +```
  377 +
  378 +---
  379 +
  380 +## 🔍 代码关键位置
  381 +
  382 +| 功能 | 代码位置 | 说明 |
  383 +|-----|---------|------|
  384 +| 计算销售业绩 | 第402-407行 | 开单-退卡 |
  385 +| 统计各项成本 | 第409-420行 | 产品物料、合作成本、店内支出、洗毛巾 |
  386 +| 计算毛利 | 第422-424行 | 销售业绩 - 各项成本 |
  387 +| 将毛利赋值给StoreTotalPerformance | 第426-427行 | **关键:业绩就是毛利** |
  388 +| 计算业绩完成率 | 第429-437行 | 基于毛利 |
  389 +| 判断业绩达标 | 第445-447行 | 基于毛利 |
  390 +| 计算提成 | 第495-496行 | 调用CalculateCommission,基于毛利 |
  391 +| 提成计算方法 | 第567-640行 | 使用StoreTotalPerformance(即毛利)计算 |
  392 +
  393 +---
  394 +
  395 +**文档版本**:2025-01-20
  396 +**最后更新**:确认业绩就是毛利,提成计算基于毛利
  397 +
... ...
科技部老师工资计算规则.md
... ... @@ -29,18 +29,27 @@
29 29  
30 30 ### 2. 业绩提成规则
31 31  
32   -业绩提成基于**总业绩**计算,采用分段累进方式:
  32 +业绩提成基于**总业绩**计算,采用阶梯式方式:
33 33  
34   -| 总业绩范围 | 提成比例 |
35   -|-----------|---------|
36   -| < 10,000元 | 0% (无提成) |
37   -| 10,000元 - 70,000元 | 2% |
38   -| 70,000元 - 150,000元 | 2.5% |
39   -| > 150,000元 | 3% |
  34 +**提成前提**:业绩必须大于1万才能进行提成
  35 +
  36 +| 总业绩范围 | 提成比例 | 说明 |
  37 +|-----------|---------|------|
  38 +| ≤ 10,000元 | 0% | 无提成 |
  39 +| > 10,000元 且 ≤ 70,000元 | 2% | 整个业绩按2%计算 |
  40 +| > 70,000元 且 ≤ 150,000元 | 2.5% | 整个业绩按2.5%计算 |
  41 +| > 150,000元 | 3% | 整个业绩按3%计算 |
40 42  
41 43 **计算说明**:
42 44 - 提成金额 = 总业绩 × 对应提成比例
43   -- 采用分段计算,不同区间按不同比例计算
  45 +- 采用阶梯式计算,整个业绩按对应区间的比例计算(不是分段累进)
  46 +- 业绩必须大于1万才有提成资格
  47 +
  48 +**示例**:
  49 +- 总业绩 = 5,000元 → 提成 = 0(无提成,未达到1万门槛)
  50 +- 总业绩 = 50,000元 → 提成 = 50,000 × 2% = 1,000元
  51 +- 总业绩 = 100,000元 → 提成 = 100,000 × 2.5% = 2,500元
  52 +- 总业绩 = 200,000元 → 提成 = 200,000 × 3% = 6,000元
44 53  
45 54 ---
46 55  
... ...