Commit 0e7e34606d9e15c1252a444547ef06afe308bfbd
1 parent
b16455bb
feat: 修改科技部老师工资提成为阶梯式,清洗流水接口返回送出/送回时间,主任工资合作成本查询使用YYYYMM格式
- 科技部老师工资:提成改为阶梯式(整个业绩按对应比例),业绩必须>1万才有提成 - 清洗流水:GetDifferenceList和GetInfo接口返回SendTime和ReturnTime字段 - 主任工资:合作成本查询使用monthStr(YYYYMM格式)匹配Month字段 - 更新科技部老师工资计算规则文档
Showing
11 changed files
with
569 additions
and
65 deletions
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 | ... | ... |