diff --git a/docs/加班系数逻辑说明及修改方案.md b/docs/加班系数逻辑说明及修改方案.md
index 152e458..a34f693 100644
--- a/docs/加班系数逻辑说明及修改方案.md
+++ b/docs/加班系数逻辑说明及修改方案.md
@@ -19,16 +19,19 @@
#### 📊 **主表(lq_xh_hyhk)计算**
+**重要**:科技部老师不参与加班,主表加班手工费仅来自健康师。
+
```
-加班手工费(F_OvertimeSgfy)= 原始手工费(F_OriginalSgfy)× 加班系数(F_OvertimeCoefficient)
+加班手工费(F_OvertimeSgfy)= 健康师原始手工费之和 × 加班系数(F_OvertimeCoefficient)
最终手工费(sgfy)= 原始手工费(F_OriginalSgfy)+ 加班手工费(F_OvertimeSgfy)
```
-**示例**:
-- 原始手工费 = 100元
+**示例**(健康师12元 + 科技部40元 = 整单原始52元):
+- 原始手工费 = 52元(健康师12 + 科技部40)
+- 健康师原始手工费之和 = 12元
- 加班系数 = 0.5
-- 加班手工费 = 100 × 0.5 = 50元
-- 最终手工费 = 100 + 50 = 150元
+- 加班手工费 = 12 × 0.5 = 6元(仅健康师参与)
+- 最终手工费 = 52 + 6 = 58元(= 健康师18 + 科技部40)
---
@@ -94,8 +97,8 @@ LaborCost = ikjbs_tem.laborCost,
┌─────────────────────────────────────────────────────────────┐
│ lq_xh_hyhk(耗卡主表) │
│ F_OvertimeCoefficient(加班系数) │
-│ F_OriginalSgfy(原始手工费) │
-│ F_OvertimeSgfy(加班手工费)= OriginalSgfy × Coefficient │
+│ F_OriginalSgfy(原始手工费 = 健康师+科技部) │
+│ F_OvertimeSgfy(加班手工费)= Σ健康师原始手工费 × 系数 │
│ sgfy(最终手工费)= OriginalSgfy + OvertimeSgfy │
└─────────────────────────────────────────────────────────────┘
│
@@ -127,11 +130,12 @@ LaborCost = ikjbs_tem.laborCost,
**流程**:
1. 接收 `LqXhHyhkCrInput` 参数,包含 `overtimeCoefficient`
-2. 计算主表加班字段:
+2. 计算主表加班字段(科技部不参与加班,主表加班手工费 = 健康师加班手工费之和):
```csharp
entity.OvertimeCoefficient = input.overtimeCoefficient ?? 0;
entity.OriginalSgfy = input.sgfy;
- entity.OvertimeSgfy = entity.OriginalSgfy * entity.OvertimeCoefficient;
+ var jksOriginalLaborCostSum = input.lqXhPxmxList?.SelectMany(p => p.lqXhJksyjList ?? ...).Sum(j => j.laborCost ?? 0) ?? 0;
+ entity.OvertimeSgfy = (decimal)(jksOriginalLaborCostSum * entity.OvertimeCoefficient);
entity.Sgfy = entity.OriginalSgfy + entity.OvertimeSgfy;
```
3. 遍历品项明细,计算每个品项的加班字段:
@@ -236,7 +240,7 @@ LaborCost = ikjbs_tem.laborCost,
```
3. **计算逻辑**:
- - 主表:重新计算 `F_OvertimeSgfy` 和 `sgfy`
+ - 主表:重新计算 `F_OvertimeSgfy`(= 健康师原始手工费之和 × 系数)和 `sgfy`(科技部不参与加班)
- 品项明细表:重新计算 `F_OvertimeProjectNumber` 和 `F_ProjectNumber`
- 健康师业绩表:重新计算 `F_OvertimeKdpxNumber`、`F_kdpxNumber`、`F_OvertimeLaborCost`、`F_LaborCost`
- 科技部老师业绩表:如果需要支持,重新计算相关字段
diff --git a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqDailyReport/TechTeacherDailyStatisticsOutput.cs b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqDailyReport/TechTeacherDailyStatisticsOutput.cs
index ac3290a..12685fc 100644
--- a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqDailyReport/TechTeacherDailyStatisticsOutput.cs
+++ b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqDailyReport/TechTeacherDailyStatisticsOutput.cs
@@ -47,6 +47,11 @@ namespace NCC.Extend.Entitys.Dto.LqDailyReport
/// 开单业绩
///
public decimal OrderAchievement { get; set; }
+
+ ///
+ /// 退卡业绩(用于计算净业绩:净业绩 = 耗卡业绩 - 退卡业绩)
+ ///
+ public decimal RefundAchievement { get; set; }
}
}
diff --git a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Mapper/LqReimbursementApplicationMapper.cs b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Mapper/LqReimbursementApplicationMapper.cs
index a4650cd..35c9758 100644
--- a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Mapper/LqReimbursementApplicationMapper.cs
+++ b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Mapper/LqReimbursementApplicationMapper.cs
@@ -1,4 +1,5 @@
-using NCC.Common.Helper;
+using NCC.Common.Helper;
+using NCC.Extend.Entitys;
using NCC.Extend.Entitys.Dto.LqReimbursementApplication;
using Mapster;
using System.Collections.Generic;
@@ -9,6 +10,9 @@ namespace NCC.Extend.Entitys.Mapper.LqReimbursementApplication
{
public void Register(TypeAdapterConfig config)
{
+ // 确保 camelCase 的 workflowConfigId 正确映射到 Entity 的 WorkflowConfigId
+ config.NewConfig()
+ .Map(d => d.WorkflowConfigId, s => s.workflowConfigId);
}
}
}
diff --git a/netcore/src/Modularity/Extend/NCC.Extend/LqDailyReportService.cs b/netcore/src/Modularity/Extend/NCC.Extend/LqDailyReportService.cs
index 6022566..a37a1f9 100644
--- a/netcore/src/Modularity/Extend/NCC.Extend/LqDailyReportService.cs
+++ b/netcore/src/Modularity/Extend/NCC.Extend/LqDailyReportService.cs
@@ -1159,6 +1159,7 @@ namespace NCC.Extend
/// - ConsumeProjectCount: 消耗项目数
/// - ConsumeAchievement: 消耗业绩
/// - OrderAchievement: 开单业绩
+ /// - RefundAchievement: 退卡业绩(净业绩 = 耗卡业绩 - 退卡业绩)
///
/// 查询参数
/// 科技部老师统计列表
@@ -1193,6 +1194,13 @@ namespace NCC.Extend
teacherFilterForOrder = $"AND ord.kjbls IN ('{teacherIdsStr}')";
}
+ var teacherFilterForRefund = "";
+ if (input.TeacherIds != null && input.TeacherIds.Any())
+ {
+ var teacherIdsStr = string.Join("','", input.TeacherIds);
+ teacherFilterForRefund = $"AND refund.kjbls IN ('{teacherIdsStr}')";
+ }
+
// SQL查询:统计科技部老师的消耗业绩、见客数、项目数
// 注意:GROUP BY 中移除了 user.F_RealName,避免同一老师ID因姓名不同产生重复记录
var consumeSql = $@"
@@ -1242,6 +1250,29 @@ namespace NCC.Extend
var orderResult = await _db.Ado.SqlQueryAsync(orderSql);
+ // 查询退卡业绩(与耗卡使用相同过滤条件:门店、时间、人员、科技部)
+ var refundSql = $@"
+ SELECT
+ techDept.F_Id as TechDepartmentId,
+ techDept.F_FullName as TechDepartmentName,
+ refund.kjbls as TeacherId,
+ MAX(user.F_RealName) as TeacherName,
+ SUM(refund.kjblsyj) as RefundAchievement
+ FROM lq_hytk_kjbsyj refund
+ INNER JOIN lq_hytk_hytk hytk ON refund.gltkbh = hytk.F_Id
+ INNER JOIN lq_mdxx store ON hytk.md = store.F_Id
+ LEFT JOIN base_organize techDept ON store.kjb = techDept.F_Id
+ LEFT JOIN BASE_USER user ON refund.kjbls = user.F_Id
+ WHERE refund.F_IsEffective = 1
+ AND hytk.F_IsEffective = 1
+ AND DATE(hytk.tksj) >= '{startDate:yyyy-MM-dd}'
+ AND DATE(hytk.tksj) <= '{endDate:yyyy-MM-dd}'
+ {techFilter}
+ {teacherFilterForRefund}
+ GROUP BY techDept.F_Id, techDept.F_FullName, refund.kjbls";
+
+ var refundResult = await _db.Ado.SqlQueryAsync(refundSql);
+
// 合并数据:按员工ID汇总,避免同一员工在多个科技部重复出现
// 使用 TeacherId 作为唯一键,汇总所有科技部的数据
var teacherDict = new Dictionary();
@@ -1287,7 +1318,8 @@ namespace NCC.Extend
CustomerCount = 0,
ConsumeProjectCount = 0,
ConsumeAchievement = 0,
- OrderAchievement = 0
+ OrderAchievement = 0,
+ RefundAchievement = 0
};
}
@@ -1334,7 +1366,8 @@ namespace NCC.Extend
CustomerCount = 0,
ConsumeProjectCount = 0,
ConsumeAchievement = 0,
- OrderAchievement = 0
+ OrderAchievement = 0,
+ RefundAchievement = 0
};
}
else
@@ -1349,7 +1382,38 @@ namespace NCC.Extend
teacherDict[teacherId].OrderAchievement += orderAchievement;
}
- // 第三步:确定每个员工的主要科技部
+ // 第三步:处理退卡业绩,按员工ID汇总
+ foreach (var item in refundResult ?? Enumerable.Empty())
+ {
+ var teacherId = item.TeacherId?.ToString();
+ var techDeptId = item.TechDepartmentId?.ToString();
+ var techDeptName = item.TechDepartmentName?.ToString();
+ var refundAchievement = Convert.ToDecimal(item.RefundAchievement);
+
+ if (string.IsNullOrEmpty(teacherId))
+ continue;
+
+ if (!teacherDict.ContainsKey(teacherId))
+ {
+ // 仅有退卡数据、无消耗和开单时,也创建记录
+ teacherDict[teacherId] = new TechTeacherDailyStatisticsOutput
+ {
+ TechDepartmentId = techDeptId,
+ TechDepartmentName = techDeptName,
+ TeacherId = teacherId,
+ TeacherName = item.TeacherName?.ToString(),
+ CustomerCount = 0,
+ ConsumeProjectCount = 0,
+ ConsumeAchievement = 0,
+ OrderAchievement = 0,
+ RefundAchievement = 0
+ };
+ }
+
+ teacherDict[teacherId].RefundAchievement += refundAchievement;
+ }
+
+ // 第四步:确定每个员工的主要科技部
// 优先按消耗业绩最多的科技部,如果消耗业绩为0,则按开单业绩最多的科技部
foreach (var teacherId in teacherDict.Keys.ToList())
{
@@ -1424,7 +1488,7 @@ namespace NCC.Extend
}
}
- // 第四步:重新计算见客数(去重,因为同一个客户可能在多个科技部被统计)
+ // 第五步:重新计算见客数(去重,因为同一个客户可能在多个科技部被统计)
// 由于已经汇总了数据,这里需要重新查询去重后的见客数
var teacherIds = teacherDict.Keys.ToList();
if (teacherIds.Any())
@@ -1453,7 +1517,7 @@ namespace NCC.Extend
}
}
- // 第五步:统一查询所有用户名称,确保所有用户都能获取到名称
+ // 第六步:统一查询所有用户名称,确保所有用户都能获取到名称
var teacherNamesSql = $@"
SELECT
F_Id as TeacherId,
diff --git a/netcore/src/Modularity/Extend/NCC.Extend/LqReimbursementApplicationService.cs b/netcore/src/Modularity/Extend/NCC.Extend/LqReimbursementApplicationService.cs
index 8fe1bc3..b19378f 100644
--- a/netcore/src/Modularity/Extend/NCC.Extend/LqReimbursementApplicationService.cs
+++ b/netcore/src/Modularity/Extend/NCC.Extend/LqReimbursementApplicationService.cs
@@ -282,8 +282,8 @@ namespace NCC.Extend.LqReimbursementApplication
// 先查询实体类中有CompletionTime字段且符合条件的申请ID
var entitiesWithCompletionTime = await _db.Queryable()
- .Where(x => x.CompletionTime.HasValue
- && x.CompletionTime.Value >= startDate
+ .Where(x => x.CompletionTime.HasValue
+ && x.CompletionTime.Value >= startDate
&& x.CompletionTime.Value <= endDate)
.Select(x => x.Id)
.ToListAsync();
@@ -528,11 +528,8 @@ namespace NCC.Extend.LqReimbursementApplication
}
}
- // 2. 设置报销申请初始状态
- if (!string.IsNullOrEmpty(input.workflowConfigId))
- {
- entity.WorkflowConfigId = input.workflowConfigId.Trim();
- }
+ // 2. 设置报销申请初始状态(含流程配置ID,确保入库)
+ entity.WorkflowConfigId = !string.IsNullOrEmpty(input.workflowConfigId) ? input.workflowConfigId.Trim() : null;
entity.NodeCount = input.nodes.Count;
entity.CurrentNodeOrder = 0;
entity.ApprovalStatus = "待审批";
@@ -546,8 +543,8 @@ namespace NCC.Extend.LqReimbursementApplication
entity.ApplicationUserName = userInfo.userName;
}
- // 3. 保存报销申请表(不使用IgnoreColumns,确保新字段被保存)
- var isOk = await _db.Insertable(entity).ExecuteCommandAsync();
+ // 3. 保存报销申请表(ignoreNullColumn: false 确保 WorkflowConfigId 等可选字段能正确入库)
+ var isOk = await _db.Insertable(entity).IgnoreColumns(ignoreNullColumn: false).ExecuteCommandAsync();
if (!(isOk > 0)) throw NCCException.Oh(ErrorCode.COM1000);
// 4. 创建节点配置
@@ -825,8 +822,10 @@ namespace NCC.Extend.LqReimbursementApplication
.ExecuteCommandAsync();
}
- // 更新报销申请表(确保 purchaseRecordsId 字段被正确更新)
+ // 更新报销申请表(确保 purchaseRecordsId、WorkflowConfigId 等字段被正确更新)
var entity = input.Adapt();
+ entity.Id = id; // 确保使用路由参数中的主键
+ entity.WorkflowConfigId = !string.IsNullOrEmpty(input.workflowConfigId) ? input.workflowConfigId.Trim() : oldEntity?.WorkflowConfigId;
var isOk = await _db.Updateable(entity).IgnoreColumns(ignoreAllNullColumns: true).ExecuteCommandAsync();
if (!(isOk > 0)) throw NCCException.Oh(ErrorCode.COM1001);
diff --git a/netcore/src/Modularity/Extend/NCC.Extend/LqXhHyhkService.cs b/netcore/src/Modularity/Extend/NCC.Extend/LqXhHyhkService.cs
index d17b4ce..af272f0 100644
--- a/netcore/src/Modularity/Extend/NCC.Extend/LqXhHyhkService.cs
+++ b/netcore/src/Modularity/Extend/NCC.Extend/LqXhHyhkService.cs
@@ -1104,9 +1104,12 @@ namespace NCC.Extend.LqXhHyhk
entity.OvertimeCoefficient = input.overtimeCoefficient ?? 0;
entity.OriginalSgfy = input.sgfy;
entity.AppointmentId = input.appointmentId;
- //加班手工费 = 原始手工费 * 加班系数
- entity.OvertimeSgfy = entity.OriginalSgfy * entity.OvertimeCoefficient;
- //最终手工费 = 原始手工费 + 加班手工费
+ // 加班手工费 = 健康师原始手工费之和 × 加班系数(科技部不参与加班)
+ var jksOriginalLaborCostSum = input.lqXhPxmxList?
+ .SelectMany(p => p.lqXhJksyjList ?? Enumerable.Empty())
+ .Sum(j => j.laborCost ?? 0) ?? 0;
+ entity.OvertimeSgfy = (decimal)(jksOriginalLaborCostSum * entity.OvertimeCoefficient);
+ // 最终手工费 = 原始手工费 + 加班手工费
entity.Sgfy = entity.OriginalSgfy + entity.OvertimeSgfy;
try
{
@@ -1489,9 +1492,13 @@ namespace NCC.Extend.LqXhHyhk
throw NCCException.Oh(ErrorCode.COM1005, "耗卡记录不存在或已作废");
}
entity.UpdateTime = DateTime.Now;
- entity.OvertimeCoefficient = input.overtimeCoefficient;
+ entity.OvertimeCoefficient = input.overtimeCoefficient ?? 0;
entity.OriginalSgfy = input.sgfy;
- entity.OvertimeSgfy = (decimal)(entity.OvertimeCoefficient * input.sgfy);
+ // 加班手工费 = 健康师原始手工费之和 × 加班系数(科技部不参与加班)
+ var jksOriginalLaborCostSum = input.lqXhPxmxList?
+ .SelectMany(p => p.lqXhJksyjList ?? Enumerable.Empty())
+ .Sum(j => j.laborCost ?? 0) ?? 0;
+ entity.OvertimeSgfy = (decimal)(jksOriginalLaborCostSum * (entity.OvertimeCoefficient ?? 0));
entity.Sgfy = entity.OriginalSgfy + entity.OvertimeSgfy;
entity.AppointmentId = input.appointmentId;
//更新会员耗卡记录
@@ -1823,7 +1830,7 @@ namespace NCC.Extend.LqXhHyhk
///
/// 计算逻辑:
/// 1. 主表(lq_xh_hyhk):
- /// - 加班手工费 = 原始手工费 × 新加班系数
+ /// - 加班手工费 = 健康师原始手工费之和 × 新加班系数(科技部不参与加班)
/// - 最终手工费 = 原始手工费 + 加班手工费
///
/// 2. 品项明细表(lq_xh_pxmx):
@@ -1881,11 +1888,26 @@ namespace NCC.Extend.LqXhHyhk
}
}
- // 2. 更新主表加班系数和相关字段
+ // 2. 查询健康师业绩,计算主表加班手工费(科技部不参与加班)
+ var jksyjList = await _db.Queryable()
+ .Where(x => x.Glkdbh == id && x.IsEffective == StatusEnum.有效.GetHashCode())
+ .ToListAsync();
+ var jksOriginalLaborCostSum = jksyjList.Sum(j =>
+ {
+ var original = j.OriginalLaborCost ?? 0;
+ if (original == 0 && (j.LaborCost ?? 0) > 0)
+ {
+ original = (j.LaborCost ?? 0) - (j.OvertimeLaborCost ?? 0);
+ if (original < 0) original = 0;
+ }
+ return original;
+ });
+
+ // 3. 更新主表加班系数和相关字段
var newCoefficient = input.overtimeCoefficient ?? 0;
var originalSgfy = entity.OriginalSgfy ?? 0;
entity.OvertimeCoefficient = newCoefficient;
- entity.OvertimeSgfy = (decimal)(originalSgfy * newCoefficient);
+ entity.OvertimeSgfy = (decimal)(jksOriginalLaborCostSum * newCoefficient);
entity.Sgfy = originalSgfy + (entity.OvertimeSgfy ?? 0);
entity.UpdateTime = DateTime.Now;
@@ -1893,7 +1915,7 @@ namespace NCC.Extend.LqXhHyhk
.UpdateColumns(x => new { x.OvertimeCoefficient, x.OvertimeSgfy, x.Sgfy, x.UpdateTime })
.ExecuteCommandAsync();
- // 3. 查询所有品项明细,更新加班字段
+ // 4. 查询所有品项明细,更新加班字段
var pxmxList = await _db.Queryable()
.Where(x => x.ConsumeInfoId == id && x.IsEffective == StatusEnum.有效.GetHashCode())
.ToListAsync();
@@ -1922,11 +1944,7 @@ namespace NCC.Extend.LqXhHyhk
.ExecuteCommandAsync();
}
- // 4. 查询所有健康师业绩,更新加班字段
- var jksyjList = await _db.Queryable()
- .Where(x => x.Glkdbh == id && x.IsEffective == StatusEnum.有效.GetHashCode())
- .ToListAsync();
-
+ // 5. 更新健康师业绩加班字段(jksyjList 已在步骤2查询)
foreach (var jksyj in jksyjList)
{
// 如果原始耗卡品项次数为空,使用当前值作为原始值
@@ -1969,7 +1987,7 @@ namespace NCC.Extend.LqXhHyhk
.ExecuteCommandAsync();
}
- // 5. 科技部老师业绩表:当前代码中不参与加班计算,保持原值不变
+ // 6. 科技部老师业绩表:当前代码中不参与加班计算,保持原值不变
// 如果需要支持,可以取消注释以下代码
/*
var kjbsyjList = await _db.Queryable()
diff --git a/sql/修复消耗单主表加班手工费历史数据.sql b/sql/修复消耗单主表加班手工费历史数据.sql
new file mode 100644
index 0000000..4fc92cd
--- /dev/null
+++ b/sql/修复消耗单主表加班手工费历史数据.sql
@@ -0,0 +1,129 @@
+-- ============================================
+-- 修复消耗单主表(lq_xh_hyhk)F_OvertimeSgfy 和 sgfy 历史数据
+-- ============================================
+-- 背景:主表加班手工费已改为「健康师原始手工费之和 × 加班系数」,科技部不参与加班。
+-- 本脚本修复历史数据,使 F_OvertimeSgfy 和 sgfy 符合新逻辑。
+--
+-- 修复逻辑:
+-- F_OvertimeSgfy = 健康师原始手工费之和 × F_OvertimeCoefficient
+-- sgfy = F_OriginalSgfy + F_OvertimeSgfy
+--
+-- 健康师原始手工费:优先用 F_OriginalLaborCost,为空时用 F_LaborCost - F_OvertimeLaborCost
+-- F_OriginalSgfy 为空时:用 健康师原始之和 + 科技部原始之和 补全
+--
+-- 执行前请先备份!建议在测试环境验证后再在生产环境执行。
+
+-- ============================================
+-- 1. 预览:查看需要修复的记录(只读,可先执行验证)
+-- ============================================
+/*
+SELECT
+ h.F_Id,
+ h.F_OriginalSgfy AS 当前原始手工费,
+ h.F_OvertimeSgfy AS 当前加班手工费,
+ h.sgfy AS 当前最终手工费,
+ h.F_OvertimeCoefficient AS 加班系数,
+ COALESCE(j.jks_sum, 0) AS 健康师原始手工费之和,
+ COALESCE(j.jks_sum, 0) * COALESCE(h.F_OvertimeCoefficient, 0) AS 新加班手工费,
+ COALESCE(h.F_OriginalSgfy, COALESCE(j.jks_sum, 0) + COALESCE(k.kjb_sum, 0)) + (COALESCE(j.jks_sum, 0) * COALESCE(h.F_OvertimeCoefficient, 0)) AS 新最终手工费
+FROM lq_xh_hyhk h
+LEFT JOIN (
+ SELECT glkdbh,
+ SUM(COALESCE(F_OriginalLaborCost, GREATEST(0, COALESCE(F_LaborCost, 0) - COALESCE(F_OvertimeLaborCost, 0)))) AS jks_sum
+ FROM lq_xh_jksyj
+ WHERE F_IsEffective = 1
+ GROUP BY glkdbh
+) j ON h.F_Id = j.glkdbh
+LEFT JOIN (
+ SELECT glkdbh,
+ SUM(COALESCE(F_OriginalLaborCost, F_LaborCost)) AS kjb_sum
+ FROM lq_xh_kjbsyj
+ WHERE F_IsEffective = 1
+ GROUP BY glkdbh
+) k ON h.F_Id = k.glkdbh
+WHERE h.F_IsEffective = 1
+ AND (
+ COALESCE(h.F_OvertimeSgfy, 0) != COALESCE(j.jks_sum, 0) * COALESCE(h.F_OvertimeCoefficient, 0)
+ OR COALESCE(h.sgfy, 0) != COALESCE(h.F_OriginalSgfy, COALESCE(j.jks_sum, 0) + COALESCE(k.kjb_sum, 0)) + (COALESCE(j.jks_sum, 0) * COALESCE(h.F_OvertimeCoefficient, 0))
+ )
+LIMIT 50;
+*/
+
+-- ============================================
+-- 2. 执行修复:更新 F_OvertimeSgfy 和 sgfy
+-- ============================================
+UPDATE lq_xh_hyhk h
+INNER JOIN (
+ SELECT glkdbh,
+ SUM(COALESCE(F_OriginalLaborCost, GREATEST(0, COALESCE(F_LaborCost, 0) - COALESCE(F_OvertimeLaborCost, 0)))) AS jks_sum
+ FROM lq_xh_jksyj
+ WHERE F_IsEffective = 1
+ GROUP BY glkdbh
+) j ON h.F_Id = j.glkdbh
+LEFT JOIN (
+ SELECT glkdbh,
+ SUM(COALESCE(F_OriginalLaborCost, F_LaborCost)) AS kjb_sum
+ FROM lq_xh_kjbsyj
+ WHERE F_IsEffective = 1
+ GROUP BY glkdbh
+) k ON h.F_Id = k.glkdbh
+SET
+ h.F_OvertimeSgfy = COALESCE(j.jks_sum, 0) * COALESCE(h.F_OvertimeCoefficient, 0),
+ h.sgfy = COALESCE(h.F_OriginalSgfy, COALESCE(j.jks_sum, 0) + COALESCE(k.kjb_sum, 0)) + (COALESCE(j.jks_sum, 0) * COALESCE(h.F_OvertimeCoefficient, 0))
+WHERE h.F_IsEffective = 1;
+
+-- 说明:上述 UPDATE 使用 INNER JOIN j,因此仅更新「存在健康师业绩」的耗卡记录。
+-- 若耗卡仅有科技部、无健康师,则 j 子查询无结果,该记录不会被更新。
+-- 对于「仅科技部」的耗卡,正确逻辑应为:F_OvertimeSgfy=0,sgfy=F_OriginalSgfy,需单独处理。
+
+-- ============================================
+-- 3. 补充修复:仅科技部、无健康师的耗卡(F_OvertimeSgfy 应为 0,sgfy = 原始手工费)
+-- ============================================
+UPDATE lq_xh_hyhk h
+LEFT JOIN (
+ SELECT glkdbh
+ FROM lq_xh_jksyj
+ WHERE F_IsEffective = 1
+ GROUP BY glkdbh
+) j ON h.F_Id = j.glkdbh
+LEFT JOIN (
+ SELECT glkdbh,
+ SUM(COALESCE(F_OriginalLaborCost, F_LaborCost)) AS kjb_sum
+ FROM lq_xh_kjbsyj
+ WHERE F_IsEffective = 1
+ GROUP BY glkdbh
+) k ON h.F_Id = k.glkdbh
+SET
+ h.F_OvertimeSgfy = 0,
+ h.sgfy = COALESCE(h.F_OriginalSgfy, COALESCE(k.kjb_sum, 0), h.sgfy)
+WHERE h.F_IsEffective = 1
+ AND j.glkdbh IS NULL
+ AND (COALESCE(h.F_OvertimeSgfy, 0) != 0);
+
+-- ============================================
+-- 4. 验证修复结果(可选)
+-- ============================================
+-- 检查是否仍有不一致记录(应返回 0)
+/*
+SELECT COUNT(*) AS 仍不一致记录数
+FROM lq_xh_hyhk h
+LEFT JOIN (
+ SELECT glkdbh,
+ SUM(COALESCE(F_OriginalLaborCost, GREATEST(0, COALESCE(F_LaborCost, 0) - COALESCE(F_OvertimeLaborCost, 0)))) AS jks_sum
+ FROM lq_xh_jksyj
+ WHERE F_IsEffective = 1
+ GROUP BY glkdbh
+) j ON h.F_Id = j.glkdbh
+LEFT JOIN (
+ SELECT glkdbh,
+ SUM(COALESCE(F_OriginalLaborCost, F_LaborCost)) AS kjb_sum
+ FROM lq_xh_kjbsyj
+ WHERE F_IsEffective = 1
+ GROUP BY glkdbh
+) k ON h.F_Id = k.glkdbh
+WHERE h.F_IsEffective = 1
+ AND (
+ COALESCE(h.F_OvertimeSgfy, 0) != COALESCE(j.jks_sum, 0) * COALESCE(h.F_OvertimeCoefficient, 0)
+ OR COALESCE(h.sgfy, 0) != COALESCE(h.F_OriginalSgfy, COALESCE(j.jks_sum, 0) + COALESCE(k.kjb_sum, 0)) + (COALESCE(j.jks_sum, 0) * COALESCE(h.F_OvertimeCoefficient, 0))
+ );
+*/
diff --git a/test-get-tech-teacher-daily-statistics.sh b/test-get-tech-teacher-daily-statistics.sh
new file mode 100755
index 0000000..9b64a03
--- /dev/null
+++ b/test-get-tech-teacher-daily-statistics.sh
@@ -0,0 +1,50 @@
+#!/bin/bash
+# GetTechTeacherDailyStatistics 接口测试脚本
+# 使用方式: ./test-get-tech-teacher-daily-statistics.sh [BASE_URL],默认 http://localhost:2011
+# 前置:API 需已启动且已重新编译(含 RefundAchievement 的 DTO 变更)
+
+BASE_URL="${1:-http://localhost:2011}"
+
+echo "=== 1. 获取 Token ==="
+TOKEN=$(curl -s -X POST "$BASE_URL/api/oauth/Login" \
+ -H "Content-Type: application/x-www-form-urlencoded" \
+ -d "account=admin&password=e10adc3949ba59abbe56e057f20f883e" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('data',{}).get('token','') if d.get('code')==200 else '')" 2>/dev/null)
+if [ -z "$TOKEN" ]; then
+ echo "获取 Token 失败,请检查 API 是否启动"
+ exit 1
+fi
+echo "Token 获取成功"
+
+echo ""
+echo "=== 2. 调用 GetTechTeacherDailyStatistics 接口 ==="
+echo "时间范围: 2026-03-01 ~ 2026-03-16"
+RESP=$(curl -s -X POST "$BASE_URL/api/Extend/LqDailyReport/get-tech-teacher-daily-statistics" \
+ -H "Authorization: $TOKEN" \
+ -H "Content-Type: application/json" \
+ -d '{"startTime":"2026-03-01T00:00:00","endTime":"2026-03-16T23:59:59"}')
+echo "$RESP" | python3 -m json.tool 2>/dev/null || echo "$RESP"
+
+CODE=$(echo "$RESP" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('code',-1))" 2>/dev/null)
+if [ "$CODE" != "200" ]; then
+ echo ""
+ echo "接口返回非 200,若为 500 且提示 set_RefundAchievement,请重启 API 服务后重试"
+ exit 1
+fi
+
+echo ""
+echo "=== 3. 数据库验证 SQL(MCP MySQL 或手动执行)==="
+echo "耗卡业绩汇总(lq_xh_kjbsyj + lq_xh_hyhk):"
+echo "SELECT consume.kjbls as TeacherId, SUM(consume.kjblsyj) as ConsumeAchievement"
+echo "FROM lq_xh_kjbsyj consume"
+echo "INNER JOIN lq_xh_hyhk hyhk ON consume.glkdbh = hyhk.F_Id"
+echo "WHERE consume.F_IsEffective = 1 AND hyhk.F_IsEffective = 1"
+echo " AND DATE(hyhk.hksj) >= '2026-03-01' AND DATE(hyhk.hksj) <= '2026-03-16'"
+echo "GROUP BY consume.kjbls;"
+echo ""
+echo "退卡业绩汇总(lq_hytk_kjbsyj + lq_hytk_hytk):"
+echo "SELECT refund.kjbls as TeacherId, SUM(refund.kjblsyj) as RefundAchievement"
+echo "FROM lq_hytk_kjbsyj refund"
+echo "INNER JOIN lq_hytk_hytk hytk ON refund.gltkbh = hytk.F_Id"
+echo "WHERE refund.F_IsEffective = 1 AND hytk.F_IsEffective = 1"
+echo " AND DATE(hytk.tksj) >= '2026-03-01' AND DATE(hytk.tksj) <= '2026-03-16'"
+echo "GROUP BY refund.kjbls;"
diff --git a/test-xh-overtime-apis.sh b/test-xh-overtime-apis.sh
new file mode 100644
index 0000000..3f670b2
--- /dev/null
+++ b/test-xh-overtime-apis.sh
@@ -0,0 +1,34 @@
+#!/bin/bash
+# 消耗单(会员耗卡)加班手工费接口测试脚本
+# 使用方式: ./test-xh-overtime-apis.sh [BASE_URL],默认 http://localhost:2011
+# 前置:API 需已启动
+
+BASE_URL="${1:-http://localhost:2011}"
+
+echo "=== 1. 获取 Token ==="
+TOKEN=$(curl -s -X POST "$BASE_URL/api/oauth/Login" \
+ -H "Content-Type: application/x-www-form-urlencoded" \
+ -d "account=admin&password=e10adc3949ba59abbe56e057f20f883e" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('data',{}).get('token','') if d.get('code')==200 else '')" 2>/dev/null)
+if [ -z "$TOKEN" ]; then
+ echo "获取 Token 失败,请检查 API 是否启动"
+ exit 1
+fi
+echo "Token 获取成功"
+
+echo ""
+echo "=== 2. Create 消耗单(健康师12+科技部40=52,系数0.5)==="
+echo "预期:F_OvertimeSgfy=6, sgfy=58"
+CREATE_BODY='{"hy":"742276000326354181","md":"1649328471923847174","mdbh":"001","mdmc":"绿纤优品道店","hyzh":"17882541236","hymc":"咕噜","xfje":100,"sgfy":52,"hksj":"2026-03-16T10:00:00","overtimeCoefficient":0.5,"lqXhPxmxList":[{"px":"1","pxmc":"活动陪同","pxjg":100,"projectNumber":1,"sourceType":"耗卡","lqXhJksyjList":[{"jks":"13032810387","jksxm":"范时依","jkszh":"13032810387","jksyj":50,"laborCost":12,"kdpxNumber":1}],"lqXhKjbsyjList":[{"kjbls":"13110190690","kjblsxm":"刘雨佳","kjblszh":"13110190690","kjblsyj":50,"laborCost":40,"hdpxNumber":1}]}]}'
+curl -s -X POST "$BASE_URL/api/Extend/LqXhHyhk" -H "Authorization: $TOKEN" -H "Content-Type: application/json" -d "$CREATE_BODY" | python3 -m json.tool 2>/dev/null || echo "请求失败"
+
+echo ""
+echo "=== 3. 创建后需查库验证主表 F_OvertimeSgfy、sgfy ==="
+echo "SQL: SELECT F_Id, sgfy, F_OriginalSgfy, F_OvertimeSgfy FROM lq_xh_hyhk WHERE hy='742276000326354181' ORDER BY F_CreateTime DESC LIMIT 1"
+
+echo ""
+echo "=== 4. Update 消耗单(需替换 {id} 为实际耗卡ID,body 必须包含 id 字段)==="
+echo "curl -X PUT \"$BASE_URL/api/Extend/LqXhHyhk/{id}\" -H \"Authorization: \$TOKEN\" -H \"Content-Type: application/json\" -d '{\"id\":\"{id}\",\"hy\":\"742276000326354181\",\"md\":\"1649328471923847174\",\"sgfy\":52,\"overtimeCoefficient\":0.5,\"lqXhPxmxList\":[...]}'"
+
+echo ""
+echo "=== 5. UpdateOvertimeCoefficient(需替换 {id} 为实际耗卡ID)==="
+echo "curl -X PUT \"$BASE_URL/api/Extend/LqXhHyhk/{id}/overtime-coefficient\" -H \"Authorization: \$TOKEN\" -H \"Content-Type: application/json\" -d '{\"overtimeCoefficient\":1.0}'"