diff --git a/excel/合作成本表.xlsx b/excel/合作成本表.xlsx
index 443d055..1ce4686 100644
--- a/excel/合作成本表.xlsx
+++ b/excel/合作成本表.xlsx
diff --git a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqInventoryUsage/StoreReceiveStatisticsInput.cs b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqInventoryUsage/StoreReceiveStatisticsInput.cs
index dc363eb..c19bc97 100644
--- a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqInventoryUsage/StoreReceiveStatisticsInput.cs
+++ b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqInventoryUsage/StoreReceiveStatisticsInput.cs
@@ -29,6 +29,13 @@ namespace NCC.Extend.Entitys.Dto.LqInventoryUsage
[StringLength(50, ErrorMessage = "门店ID长度不能超过50个字符")]
[Display(Name = "门店ID")]
public string StoreId { get; set; }
+
+ ///
+ /// 仓库名称(可选,用于筛选特定仓库)
+ ///
+ [StringLength(100, ErrorMessage = "仓库名称长度不能超过100个字符")]
+ [Display(Name = "仓库名称")]
+ public string Warehouse { get; set; }
}
}
diff --git a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqLaundryFlow/LqLaundryFlowInfoOutput.cs b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqLaundryFlow/LqLaundryFlowInfoOutput.cs
index 09071e6..17d03c6 100644
--- a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqLaundryFlow/LqLaundryFlowInfoOutput.cs
+++ b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqLaundryFlow/LqLaundryFlowInfoOutput.cs
@@ -109,6 +109,18 @@ namespace NCC.Extend.Entitys.Dto.LqLaundryFlow
///
[Display(Name = "创建时间")]
public DateTime createTime { get; set; }
+
+ ///
+ /// 送出时间(流水类型为0时使用)
+ ///
+ [Display(Name = "送出时间")]
+ public DateTime? sendTime { get; set; }
+
+ ///
+ /// 送回时间(流水类型为1时使用)
+ ///
+ [Display(Name = "送回时间")]
+ public DateTime? returnTime { get; set; }
}
}
diff --git a/netcore/src/Modularity/Extend/NCC.Extend/LqDirectorSalaryService.cs b/netcore/src/Modularity/Extend/NCC.Extend/LqDirectorSalaryService.cs
index 446ba3b..5e9aa84 100644
--- a/netcore/src/Modularity/Extend/NCC.Extend/LqDirectorSalaryService.cs
+++ b/netcore/src/Modularity/Extend/NCC.Extend/LqDirectorSalaryService.cs
@@ -271,8 +271,10 @@ namespace NCC.Extend
.ToDictionary(x => x.StoreId.ToString(), x => Convert.ToDecimal(x.MaterialAmount ?? 0));
// 1.10 合作项目成本统计
+ // Month字段格式为"11"(月份数字),不是"202511"(YYYYMM格式)
+ var cooperationCostMonth = $"{month:D2}"; // 格式化为"11"
var cooperationCostList = await _db.Queryable()
- .Where(x => x.Year == year && x.Month == monthStr && x.IsEffective == StatusEnum.有效.GetHashCode())
+ .Where(x => x.Year == year && x.Month == cooperationCostMonth && x.IsEffective == StatusEnum.有效.GetHashCode())
.Select(x => new { x.StoreId, x.TotalAmount })
.ToListAsync();
var cooperationCostDict = cooperationCostList
@@ -372,26 +374,31 @@ namespace NCC.Extend
// 2.3 获取门店目标信息(门店生命线、目标人头、目标消耗)
if (!storeTargetDict.ContainsKey(storeId))
{
- throw new Exception($"门店【{salary.StoreName ?? storeId}】在门店目标表中未配置{monthStr}月份的目标数据,无法计算主任工资");
+ // 如果没有配置目标数据,使用默认值0
+ salary.StoreLifeline = 0;
+ salary.TargetHeadCount = 0;
+ salary.TargetConsume = 0;
+ }
+ else
+ {
+ var storeTarget = storeTargetDict[storeId];
+ salary.StoreLifeline = storeTarget.StoreLifeline;
+ salary.TargetHeadCount = storeTarget.StoreHeadcountTarget;
+ salary.TargetConsume = storeTarget.StoreConsumeTarget;
}
- var storeTarget = storeTargetDict[storeId];
- salary.StoreLifeline = storeTarget.StoreLifeline;
- salary.TargetHeadCount = storeTarget.StoreHeadcountTarget;
- salary.TargetConsume = storeTarget.StoreConsumeTarget;
-
- // 数据校验:门店生命线、目标人头、目标消耗必须设置
+ // 如果目标值未设置(<=0),使用默认值0,允许继续计算
if (salary.StoreLifeline <= 0)
{
- throw new Exception($"门店【{salary.StoreName ?? storeId}】的门店生命线未设置,无法计算主任工资");
+ salary.StoreLifeline = 0;
}
if (salary.TargetHeadCount <= 0)
{
- throw new Exception($"门店【{salary.StoreName ?? storeId}】的目标人头数未设置,无法计算主任工资");
+ salary.TargetHeadCount = 0;
}
- if (!isNewStore && salary.TargetConsume <= 0)
+ if (salary.TargetConsume <= 0)
{
- throw new Exception($"门店【{salary.StoreName ?? storeId}】的目标消耗未设置,无法计算主任工资(老店需要考核消耗)");
+ salary.TargetConsume = 0;
}
// 2.4 计算销售业绩(开单业绩-退款业绩)
diff --git a/netcore/src/Modularity/Extend/NCC.Extend/LqInventoryUsageService.cs b/netcore/src/Modularity/Extend/NCC.Extend/LqInventoryUsageService.cs
index ebc1b01..0d5d1e3 100644
--- a/netcore/src/Modularity/Extend/NCC.Extend/LqInventoryUsageService.cs
+++ b/netcore/src/Modularity/Extend/NCC.Extend/LqInventoryUsageService.cs
@@ -1590,7 +1590,8 @@ namespace NCC.Extend
/// {
/// "year": 2025,
/// "month": 11,
- /// "storeId": "门店ID(可选)"
+ /// "storeId": "门店ID(可选)",
+ /// "warehouse": "仓库名称(可选)"
/// }
/// ```
///
@@ -1598,6 +1599,7 @@ namespace NCC.Extend
/// - year: 统计年份(必填,范围:2020-2100)
/// - month: 统计月份(必填,范围:1-12)
/// - storeId: 门店ID(可选,不传则统计所有门店)
+ /// - warehouse: 仓库名称(可选,不传则统计所有仓库)
///
/// 返回说明:
/// - 按门店分组统计
@@ -1651,15 +1653,25 @@ namespace NCC.Extend
// 获取所有批次ID
var batchIds = applications.Select(x => x.UsageBatchId).Distinct().ToList();
- // 查询这些批次的使用记录,计算总金额
- var usageRecords = await _db.Queryable()
- .Where(x => batchIds.Contains(x.UsageBatchId))
- .Where(x => x.IsEffective == StatusEnum.有效.GetHashCode())
- .Select(x => new
+ // 查询这些批次的使用记录,关联产品表获取仓库信息,计算总金额
+ var usageRecordsQuery = _db.Queryable(
+ (usage, product) => usage.ProductId == product.Id)
+ .Where((usage, product) => batchIds.Contains(usage.UsageBatchId))
+ .Where((usage, product) => usage.IsEffective == StatusEnum.有效.GetHashCode());
+
+ // 如果指定了仓库筛选,添加仓库条件
+ if (!string.IsNullOrWhiteSpace(input.Warehouse))
+ {
+ usageRecordsQuery = usageRecordsQuery.Where((usage, product) => product.Warehouse == input.Warehouse);
+ }
+
+ var usageRecords = await usageRecordsQuery
+ .Select((usage, product) => new
{
- x.UsageBatchId,
- x.StoreId,
- x.TotalAmount
+ usage.UsageBatchId,
+ usage.StoreId,
+ usage.TotalAmount,
+ product.Warehouse
})
.ToListAsync();
diff --git a/netcore/src/Modularity/Extend/NCC.Extend/LqLaundryFlowService.cs b/netcore/src/Modularity/Extend/NCC.Extend/LqLaundryFlowService.cs
index ad9c813..509d82d 100644
--- a/netcore/src/Modularity/Extend/NCC.Extend/LqLaundryFlowService.cs
+++ b/netcore/src/Modularity/Extend/NCC.Extend/LqLaundryFlowService.cs
@@ -473,7 +473,9 @@ namespace NCC.Extend
isEffective = flow.IsEffective,
createUser = flow.CreateUser,
createUserName = "",
- createTime = flow.CreateTime
+ createTime = flow.CreateTime,
+ sendTime = flow.SendTime,
+ returnTime = flow.ReturnTime
})
.FirstAsync();
@@ -533,7 +535,7 @@ namespace NCC.Extend
StoreName = store.Dm ?? "",
flow.ProductType,
SendQuantity = flow.Quantity,
- SendTime = flow.CreateTime
+ SendTime = flow.SendTime
})
.ToListAsync();
@@ -558,7 +560,7 @@ namespace NCC.Extend
{
x.BatchNumber,
ReturnQuantity = x.Quantity,
- ReturnTime = x.CreateTime,
+ ReturnTime = x.ReturnTime,
x.Remark
})
.ToListAsync();
diff --git a/netcore/src/Modularity/Extend/NCC.Extend/LqSalaryExtraCalculationService.cs b/netcore/src/Modularity/Extend/NCC.Extend/LqSalaryExtraCalculationService.cs
index 4a7b3a2..919bd31 100644
--- a/netcore/src/Modularity/Extend/NCC.Extend/LqSalaryExtraCalculationService.cs
+++ b/netcore/src/Modularity/Extend/NCC.Extend/LqSalaryExtraCalculationService.cs
@@ -318,27 +318,39 @@ namespace NCC.Extend
var entitiesToInsert = new List();
var entitiesToUpdate = new List();
+ // 用于跟踪已处理的员工记录,避免重复(Key: EmployeeId_Year_Month)
+ var processedEmployeeRecords = new Dictionary();
+
foreach (var item in importData)
{
try
{
- // 1. 查找用户ID(优先使用ID,否则通过姓名和电话查找)
+ // 1. 查找用户ID(优先使用Excel中的id作为员工ID,否则通过姓名和电话查找)
UserEntity user = null;
LqSalaryExtraCalculationEntity existingRecord = null;
- // 如果提供了ID,先尝试根据ID查找现有记录,获取EmployeeId
+ // 优先使用Excel中的id作为员工ID(EmployeeId)直接查找用户
if (!string.IsNullOrEmpty(item.Id))
{
- existingRecord = await _db.Queryable()
- .Where(x => x.Id == item.Id)
+ // 先尝试将id作为员工ID查找用户
+ user = await _db.Queryable()
+ .Where(u => u.Id == item.Id)
.FirstAsync();
- if (existingRecord != null)
+ // 如果找不到,尝试将id作为记录ID查找现有记录,获取EmployeeId
+ if (user == null)
{
- // 通过EmployeeId查找用户
- user = await _db.Queryable()
- .Where(u => u.Id == existingRecord.EmployeeId)
+ existingRecord = await _db.Queryable()
+ .Where(x => x.Id == item.Id)
.FirstAsync();
+
+ if (existingRecord != null)
+ {
+ // 通过EmployeeId查找用户
+ user = await _db.Queryable()
+ .Where(u => u.Id == existingRecord.EmployeeId)
+ .FirstAsync();
+ }
}
}
@@ -382,7 +394,27 @@ namespace NCC.Extend
continue;
}
- // 2. 如果还没有找到现有记录,则根据健康师ID、年份、月份查找
+ // 2. 检查是否已经在本次导入中处理过该员工(避免重复,使用员工ID而不是姓名)
+ var recordKey = $"{user.Id}_{item.Year}_{item.Month}";
+ if (processedEmployeeRecords.ContainsKey(recordKey))
+ {
+ // 如果已经处理过,更新记录而不是创建新记录
+ existingRecord = processedEmployeeRecords[recordKey];
+ existingRecord.BaseRewardPerformance = item.BaseRewardPerformance;
+ existingRecord.CooperationRewardPerformance = item.CooperationRewardPerformance;
+ existingRecord.NewCustomerPerformance = item.NewCustomerPerformance;
+ existingRecord.NewCustomerConversionRate = item.NewCustomerConversionRate;
+ existingRecord.UpgradePerformance = item.UpgradePerformance;
+ existingRecord.UpgradeConversionRate = item.UpgradeConversionRate;
+ existingRecord.UpgradeCustomerCount = item.UpgradeCustomerCount;
+ existingRecord.OtherPerformanceAdd = item.OtherPerformanceAdd;
+ existingRecord.OtherPerformanceSubtract = item.OtherPerformanceSubtract;
+ // 注意:这里不添加到entitiesToUpdate,因为已经在processedEmployeeRecords中,会在最后统一处理
+ successCount++;
+ continue;
+ }
+
+ // 3. 如果还没有找到现有记录,则根据健康师ID、年份、月份查找数据库中的记录
if (existingRecord == null)
{
existingRecord = await _db.Queryable()
@@ -403,6 +435,8 @@ namespace NCC.Extend
existingRecord.OtherPerformanceAdd = item.OtherPerformanceAdd;
existingRecord.OtherPerformanceSubtract = item.OtherPerformanceSubtract;
entitiesToUpdate.Add(existingRecord);
+ // 记录到已处理字典中
+ processedEmployeeRecords[recordKey] = existingRecord;
}
else
{
@@ -424,6 +458,8 @@ namespace NCC.Extend
OtherPerformanceSubtract = item.OtherPerformanceSubtract
};
entitiesToInsert.Add(entity);
+ // 记录到已处理字典中(注意:这里需要创建一个临时对象,因为entity还没有ID)
+ processedEmployeeRecords[recordKey] = entity;
}
successCount++;
@@ -441,12 +477,18 @@ namespace NCC.Extend
await _db.Insertable(entitiesToInsert).ExecuteCommandAsync();
}
- // 批量更新现有记录
+ // 批量更新现有记录(去重,确保每个员工只有一条记录被更新)
if (entitiesToUpdate.Any())
{
+ // 根据主键ID去重,确保每个记录只更新一次
+ var uniqueEntitiesToUpdate = entitiesToUpdate
+ .GroupBy(x => x.Id)
+ .Select(g => g.First())
+ .ToList();
+
// 明确指定要更新的字段,确保所有字段都被更新(包括升单业绩)
// 注意:Updateable接收实体列表时,会自动根据主键更新,不需要Where条件
- await _db.Updateable(entitiesToUpdate)
+ await _db.Updateable(uniqueEntitiesToUpdate)
.UpdateColumns(it => new
{
it.BaseRewardPerformance,
diff --git a/netcore/src/Modularity/Extend/NCC.Extend/LqStoreManagerSalaryService.cs b/netcore/src/Modularity/Extend/NCC.Extend/LqStoreManagerSalaryService.cs
index c2cd884..5763631 100644
--- a/netcore/src/Modularity/Extend/NCC.Extend/LqStoreManagerSalaryService.cs
+++ b/netcore/src/Modularity/Extend/NCC.Extend/LqStoreManagerSalaryService.cs
@@ -384,26 +384,31 @@ namespace NCC.Extend
// 2.3 获取门店目标信息(门店生命线、目标人头、目标消耗)
if (!storeTargetDict.ContainsKey(storeId))
{
- throw new Exception($"门店【{salary.StoreName ?? storeId}】在门店目标表中未配置{monthStr}月份的目标数据,无法计算店长工资");
+ // 如果没有配置目标数据,使用默认值0
+ salary.StoreLifeline = 0;
+ salary.TargetHeadCount = 0;
+ salary.TargetConsume = 0;
+ }
+ else
+ {
+ var storeTarget = storeTargetDict[storeId];
+ salary.StoreLifeline = storeTarget.StoreLifeline;
+ salary.TargetHeadCount = storeTarget.StoreHeadcountTarget;
+ salary.TargetConsume = storeTarget.StoreConsumeTarget;
}
- var storeTarget = storeTargetDict[storeId];
- salary.StoreLifeline = storeTarget.StoreLifeline;
- salary.TargetHeadCount = storeTarget.StoreHeadcountTarget;
- salary.TargetConsume = storeTarget.StoreConsumeTarget;
-
- // 数据校验:门店生命线、目标人头必须设置
+ // 如果目标值未设置(<=0),使用默认值0,允许继续计算
if (salary.StoreLifeline <= 0)
{
- throw new Exception($"门店【{salary.StoreName ?? storeId}】的门店生命线未设置,无法计算店长工资");
+ salary.StoreLifeline = 0;
}
if (salary.TargetHeadCount <= 0)
{
- throw new Exception($"门店【{salary.StoreName ?? storeId}】的目标人头数未设置,无法计算店长工资");
+ salary.TargetHeadCount = 0;
}
- if (!isNewStore && salary.TargetConsume <= 0)
+ if (salary.TargetConsume <= 0)
{
- throw new Exception($"门店【{salary.StoreName ?? storeId}】的目标消耗未设置,无法计算店长工资(老店需要考核消耗)");
+ salary.TargetConsume = 0;
}
// 2.4 计算门店业绩
diff --git a/netcore/src/Modularity/Extend/NCC.Extend/LqTechTeacherSalaryService.cs b/netcore/src/Modularity/Extend/NCC.Extend/LqTechTeacherSalaryService.cs
index 3118f50..f19ab56 100644
--- a/netcore/src/Modularity/Extend/NCC.Extend/LqTechTeacherSalaryService.cs
+++ b/netcore/src/Modularity/Extend/NCC.Extend/LqTechTeacherSalaryService.cs
@@ -453,32 +453,43 @@ namespace NCC.Extend
}
///
- /// 计算业绩提成(分段累进)
+ /// 计算业绩提成(阶梯式)
///
/// 总业绩
/// 提成比例和金额
private (decimal Rate, decimal Amount) CalculatePerformanceCommission(decimal totalPerformance)
{
- if (totalPerformance < 10000m)
+ // 提成前提:业绩必须大于1万才能进行提成
+ if (totalPerformance <= 10000m)
{
- // < 10,000元 → 0%
+ // ≤ 10,000元 → 0%(无提成)
return (0m, 0m);
}
- else if (totalPerformance < 70000m)
+
+ decimal rate;
+ decimal amount;
+
+ // 阶梯式提成计算(整个业绩按对应比例)
+ if (totalPerformance > 150000m)
{
- // 10,000-70,000元 → 2%
- return (2m, totalPerformance * 0.02m);
+ // > 15万 → 3%
+ rate = 3m;
+ amount = totalPerformance * 0.03m;
}
- else if (totalPerformance < 150000m)
+ else if (totalPerformance > 70000m)
{
- // 70,000-150,000元 → 2.5%
- return (2.5m, totalPerformance * 0.025m);
+ // > 7万 且 ≤ 15万 → 2.5%
+ rate = 2.5m;
+ amount = totalPerformance * 0.025m;
}
else
{
- // > 150,000元 → 3%
- return (3m, totalPerformance * 0.03m);
+ // > 1万 且 ≤ 7万 → 2%
+ rate = 2m;
+ amount = totalPerformance * 0.02m;
}
+
+ return (rate, amount);
}
///
diff --git a/主任工资计算逻辑完整梳理.md b/主任工资计算逻辑完整梳理.md
new file mode 100644
index 0000000..22e665f
--- /dev/null
+++ b/主任工资计算逻辑完整梳理.md
@@ -0,0 +1,397 @@
+# 主任工资计算逻辑完整梳理
+
+## 📋 概述
+
+主任工资由以下几个部分组成:
+1. **底薪**:固定3500元,根据考核指标扣款
+2. **提成**:基于**毛利**计算,使用阶梯提成模式
+
+**重要说明**:
+- **业绩(StoreTotalPerformance)就是毛利**
+- **提成计算基于毛利**,而不是销售业绩(开单-退卡)
+
+---
+
+## 💰 核心计算公式
+
+### 1. 销售业绩
+
+```
+销售业绩 = 开单业绩 - 退款业绩
+```
+
+### 2. 毛利(核心指标)
+
+```
+毛利 = 销售业绩 - - 合作项目成本 - 店内支出 - 洗毛巾费用
+```
+
+### 3. 业绩(用于提成计算)
+
+```
+业绩(StoreTotalPerformance)= 毛利(GrossProfit)
+```
+
+**关键点**:`StoreTotalPerformance` 字段存储的是**毛利**,用于提成计算。
+
+---
+
+## 📊 数据来源详解
+
+### 1. 开单业绩
+
+**数据来源**:
+- 表:`lq_kd_kdjlb`(开单记录表)
+- 字段:`sfyj`(实付业绩)
+- 条件:
+ - `F_IsEffective = 1`(有效记录)
+ - `Djmd = @StoreId`(门店ID)
+ - `Kdrq >= @StartDate AND Kdrq <= @EndDate`(时间范围)
+
+**代码位置**:第185-192行
+
+### 2. 退款业绩
+
+**数据来源**:
+- 表:`lq_hytk_hytk`(退卡记录表)
+- 字段:`F_ActualRefundAmount`(实际退款金额,如果没有则使用 `tkje`)
+- 条件:
+ - `F_IsEffective = 1`(有效记录)
+ - `md = @StoreId`(门店ID)
+ - `Tksj >= @StartDate AND Tksj <= @EndDate`(时间范围)
+
+**代码位置**:第194-202行
+
+### 3. 产品物料
+
+**数据来源**:
+- 表:`lq_inventory_usage`(库存使用记录表)
+- 字段:`F_TotalAmount`(合计金额)
+- 条件:
+ - `F_IsEffective = 1`(有效记录)
+ - `F_StoreId = @StoreId`(门店ID)
+ - **特殊规则**:11月工资算10月数据
+ - 如果计算月份是11月,则查询10月的数据
+ - 其他月份正常查询当月数据
+
+**代码位置**:第252-271行
+
+### 4. 合作项目成本
+
+**数据来源**:
+- 表:`lq_cooperation_cost`(合作成本表)
+- 字段:`F_TotalAmount`(合计金额)
+- 条件:
+ - `F_Year = @Year`(年份)
+ - `F_Month = @MonthStr`(月份,YYYYMM格式)
+ - `F_StoreId = @StoreId`(门店ID)
+ - `F_IsEffective = 1`(有效记录)
+
+**代码位置**:第273-281行
+
+### 5. 店内支出
+
+**数据来源**:
+- 表:`lq_store_expense`(店内支出表)
+- 字段:`F_Amount`(金额)
+- 条件:
+ - `F_IsEffective = 1`(有效记录)
+ - `F_StoreId = @StoreId`(门店ID)
+ - `DATE_FORMAT(F_ExpenseDate, '%Y%m') = @MonthStr`(月份,YYYYMM格式)
+
+**代码位置**:第283-296行
+
+### 6. 洗毛巾费用
+
+**数据来源**:
+- 表:`lq_laundry_flow`(清洗流水表)
+- 字段:`F_TotalPrice`(总费用)
+- 条件:
+ - `F_IsEffective = 1`(有效记录)
+ - `F_FlowType = 0`(只统计送出的记录)
+ - `F_StoreId = @StoreId`(门店ID)
+ - 优先使用 `F_SendTime`,如果为空则使用 `F_CreateTime`
+ - `DATE_FORMAT(COALESCE(F_SendTime, F_CreateTime), '%Y%m') = @MonthStr`(月份,YYYYMM格式)
+
+**代码位置**:第298-313行
+
+---
+
+## 🔄 计算流程
+
+### 步骤1:计算销售业绩
+
+```csharp
+// 2.4 计算销售业绩(开单业绩-退款业绩)
+decimal billing = storeBillingDict.ContainsKey(storeId) ? storeBillingDict[storeId] : 0;
+decimal refund = storeRefundDict.ContainsKey(storeId) ? storeRefundDict[storeId] : 0;
+salary.StoreBillingPerformance = billing;
+salary.StoreRefundPerformance = refund;
+salary.SalesPerformance = billing - refund;
+```
+
+**代码位置**:第402-407行
+
+### 步骤2:统计各项成本
+
+```csharp
+// 2.5 统计各项成本
+salary.ProductMaterial = productMaterialDict.ContainsKey(storeId) ? productMaterialDict[storeId] : 0;
+salary.CooperationCost = cooperationCostDict.ContainsKey(storeId) ? cooperationCostDict[storeId] : 0;
+salary.StoreExpense = storeExpenseDict.ContainsKey(storeId) ? storeExpenseDict[storeId] : 0;
+salary.LaundryCost = laundryCostDict.ContainsKey(storeId) ? laundryCostDict[storeId] : 0;
+```
+
+**代码位置**:第409-420行
+
+### 步骤3:计算毛利
+
+```csharp
+// 2.6 计算毛利
+// 毛利 = 销售业绩 - 产品物料 - 合作项目成本 - 店内支出 - 洗毛巾
+salary.GrossProfit = salary.SalesPerformance - salary.ProductMaterial - salary.CooperationCost - salary.StoreExpense - salary.LaundryCost;
+```
+
+**代码位置**:第422-424行
+
+### 步骤4:将毛利赋值给StoreTotalPerformance(用于提成计算)
+
+```csharp
+// 2.7 将毛利赋值给StoreTotalPerformance(用于提成计算)
+salary.StoreTotalPerformance = salary.GrossProfit;
+```
+
+**代码位置**:第426-427行
+
+**关键点**:`StoreTotalPerformance` 存储的是**毛利**,不是销售业绩。
+
+### 步骤5:计算业绩完成率(基于毛利)
+
+```csharp
+// 2.8 计算业绩完成率(基于毛利与生命线比较)
+if (salary.StoreLifeline > 0)
+{
+ salary.PerformanceCompletionRate = salary.GrossProfit / salary.StoreLifeline;
+}
+```
+
+**代码位置**:第429-437行
+
+### 步骤6:判断业绩是否达标(基于毛利)
+
+```csharp
+// 2.11 计算考核指标(业绩、人头、消耗是否达标)
+// 业绩达标判断基于毛利
+bool performanceReached = salary.GrossProfit >= salary.StoreLifeline;
+```
+
+**代码位置**:第445-447行
+
+### 步骤7:计算提成(基于毛利)
+
+```csharp
+// 2.14 计算阶梯提成(先计算门店总提成,基于毛利)
+CalculateCommission(salary, isNewStore);
+```
+
+**代码位置**:第495-496行
+
+**提成计算方法**:
+
+```csharp
+// 提成计算基于毛利(StoreTotalPerformance存储的是毛利)
+decimal performance = salary.StoreTotalPerformance; // 这里已经是毛利了
+decimal lifeline = salary.StoreLifeline;
+
+// 计算阶梯提成
+if (performance <= lifeline)
+{
+ // 业绩 ≤ 生命线:只计算≤生命线部分的提成
+ salary.CommissionAmountBelowLifeline = performance * rateBelowLifeline;
+ salary.CommissionAmountAboveLifeline = 0;
+}
+else
+{
+ // 业绩 > 生命线:分别计算≤生命线部分和>生命线部分的提成
+ salary.CommissionAmountBelowLifeline = lifeline * rateBelowLifeline;
+ salary.CommissionAmountAboveLifeline = (performance - lifeline) * rateAboveLifeline;
+}
+```
+
+**代码位置**:第567-640行
+
+---
+
+## 💰 提成规则详解
+
+### 提成计算方式
+
+**阶梯提成模式**:根据业绩是否超过生命线,使用不同的提成比例。
+
+### 老店主任提成规则
+
+根据门店分类(A、B、C类)和业绩是否超过生命线,使用不同的阶梯提成比例:
+
+| 门店分类 | 业绩 ≤ 生命线部分 | 业绩 > 生命线部分 |
+|---------|----------------|-----------------|
+| A类门店 | 2% | 2.5% |
+| B类门店 | 2.5% | 3% |
+| C类门店 | 3% | 3.5% |
+
+**计算公式**:
+```
+如果 业绩(毛利)≤ 生命线:
+ 提成 = 业绩(毛利) × 对应提成比例(≤生命线部分)
+
+如果 业绩(毛利)> 生命线:
+ 提成 = 生命线 × 对应提成比例(≤生命线部分) + (业绩(毛利) - 生命线) × 对应提成比例(>生命线部分)
+```
+
+### 新店主任提成规则
+
+**统一标准**,不区分门店分类:
+
+| 业绩范围 | 提成比例 |
+|---------|---------|
+| 业绩 ≤ 生命线部分 | 2% |
+| 业绩 > 生命线部分 | 2.5% |
+
+**计算公式**:
+```
+如果 业绩(毛利)≤ 生命线:
+ 提成 = 业绩(毛利) × 2%
+
+如果 业绩(毛利)> 生命线:
+ 提成 = 生命线 × 2% + (业绩(毛利) - 生命线) × 2.5%
+```
+
+### 提成按在店天数比例计算
+
+```csharp
+// 2.15 按在店天数比例计算提成金额
+if (daysInMonth > 0 && workingDays > 0)
+{
+ // 提成金额按在店天数比例计算
+ salary.CommissionAmountBelowLifeline = salary.CommissionAmountBelowLifeline / daysInMonth * workingDays;
+ salary.CommissionAmountAboveLifeline = salary.CommissionAmountAboveLifeline / daysInMonth * workingDays;
+ salary.TotalCommissionAmount = salary.CommissionAmountBelowLifeline + salary.CommissionAmountAboveLifeline;
+}
+```
+
+**代码位置**:第498-512行
+
+---
+
+## 📝 关键字段说明
+
+### StoreTotalPerformance(门店总业绩)
+
+**字段含义**:**毛利**
+
+**计算公式**:
+```
+StoreTotalPerformance = GrossProfit = 销售业绩 - 产品物料 - 合作项目成本 - 店内支出 - 洗毛巾费用
+```
+
+**用途**:
+- 用于提成计算
+- 用于业绩完成率计算
+- 用于业绩达标判断
+
+**重要说明**:此字段存储的是**毛利**,不是销售业绩(开单-退卡)。
+
+### GrossProfit(毛利)
+
+**字段含义**:毛利
+
+**计算公式**:
+```
+GrossProfit = SalesPerformance - ProductMaterial - CooperationCost - StoreExpense - LaundryCost
+```
+
+### SalesPerformance(销售业绩)
+
+**字段含义**:销售业绩(开单-退卡)
+
+**计算公式**:
+```
+SalesPerformance = StoreBillingPerformance - StoreRefundPerformance
+```
+
+---
+
+## ✅ 验证要点
+
+### 1. 业绩就是毛利
+
+- ✅ `StoreTotalPerformance = GrossProfit`(第427行)
+- ✅ 提成计算使用 `StoreTotalPerformance`(第581行)
+- ✅ 业绩完成率使用 `GrossProfit`(第432行)
+- ✅ 业绩达标判断使用 `GrossProfit`(第447行)
+
+### 2. 提成计算基于毛利
+
+- ✅ `CalculateCommission` 方法注释明确说明"基于毛利"(第563行)
+- ✅ 方法内部使用 `StoreTotalPerformance`(即毛利)进行计算(第581行)
+- ✅ 所有提成计算都基于 `performance`(即毛利)
+
+---
+
+## 📋 总结
+
+### 核心结论
+
+1. **业绩(StoreTotalPerformance)就是毛利**
+ - `StoreTotalPerformance = GrossProfit`
+ - 不是销售业绩(开单-退卡)
+
+2. **提成计算基于毛利**
+ - 提成计算使用 `StoreTotalPerformance`(即毛利)
+ - 不是基于销售业绩
+
+3. **毛利计算公式**
+ ```
+ 毛利 = 销售业绩 - 产品物料 - 合作项目成本 - 店内支出 - 洗毛巾费用
+ ```
+
+4. **所有业绩相关的判断都基于毛利**
+ - 业绩完成率 = 毛利 / 生命线
+ - 业绩达标判断 = 毛利 >= 生命线
+ - 提成计算 = 基于毛利使用阶梯提成
+
+### 数据流向
+
+```
+开单业绩 - 退款业绩
+ ↓
+销售业绩(SalesPerformance)
+ ↓
+销售业绩 - 产品物料 - 合作项目成本 - 店内支出 - 洗毛巾费用
+ ↓
+毛利(GrossProfit)
+ ↓
+StoreTotalPerformance(用于提成计算)
+ ↓
+阶梯提成计算
+```
+
+---
+
+## 🔍 代码关键位置
+
+| 功能 | 代码位置 | 说明 |
+|-----|---------|------|
+| 计算销售业绩 | 第402-407行 | 开单-退卡 |
+| 统计各项成本 | 第409-420行 | 产品物料、合作成本、店内支出、洗毛巾 |
+| 计算毛利 | 第422-424行 | 销售业绩 - 各项成本 |
+| 将毛利赋值给StoreTotalPerformance | 第426-427行 | **关键:业绩就是毛利** |
+| 计算业绩完成率 | 第429-437行 | 基于毛利 |
+| 判断业绩达标 | 第445-447行 | 基于毛利 |
+| 计算提成 | 第495-496行 | 调用CalculateCommission,基于毛利 |
+| 提成计算方法 | 第567-640行 | 使用StoreTotalPerformance(即毛利)计算 |
+
+---
+
+**文档版本**:2025-01-20
+**最后更新**:确认业绩就是毛利,提成计算基于毛利
+
diff --git a/科技部老师工资计算规则.md b/科技部老师工资计算规则.md
index 8e943fa..6530051 100644
--- a/科技部老师工资计算规则.md
+++ b/科技部老师工资计算规则.md
@@ -29,18 +29,27 @@
### 2. 业绩提成规则
-业绩提成基于**总业绩**计算,采用分段累进方式:
+业绩提成基于**总业绩**计算,采用阶梯式方式:
-| 总业绩范围 | 提成比例 |
-|-----------|---------|
-| < 10,000元 | 0% (无提成) |
-| 10,000元 - 70,000元 | 2% |
-| 70,000元 - 150,000元 | 2.5% |
-| > 150,000元 | 3% |
+**提成前提**:业绩必须大于1万才能进行提成
+
+| 总业绩范围 | 提成比例 | 说明 |
+|-----------|---------|------|
+| ≤ 10,000元 | 0% | 无提成 |
+| > 10,000元 且 ≤ 70,000元 | 2% | 整个业绩按2%计算 |
+| > 70,000元 且 ≤ 150,000元 | 2.5% | 整个业绩按2.5%计算 |
+| > 150,000元 | 3% | 整个业绩按3%计算 |
**计算说明**:
- 提成金额 = 总业绩 × 对应提成比例
-- 采用分段计算,不同区间按不同比例计算
+- 采用阶梯式计算,整个业绩按对应区间的比例计算(不是分段累进)
+- 业绩必须大于1万才有提成资格
+
+**示例**:
+- 总业绩 = 5,000元 → 提成 = 0(无提成,未达到1万门槛)
+- 总业绩 = 50,000元 → 提成 = 50,000 × 2% = 1,000元
+- 总业绩 = 100,000元 → 提成 = 100,000 × 2.5% = 2,500元
+- 总业绩 = 200,000元 → 提成 = 200,000 × 3% = 6,000元
---