Commit eb04731ec7152305ffeaf2021beafb394e5b1168

Authored by 李宇
2 parents 7d45b4d1 06da27f0

Merge branch 'master' of http://39.98.150.180/antissoft/lvqianmeiye_ERP

docs/加班系数逻辑说明及修改方案.md
... ... @@ -19,16 +19,19 @@
19 19  
20 20 #### 📊 **主表(lq_xh_hyhk)计算**
21 21  
  22 +**重要**:科技部老师不参与加班,主表加班手工费仅来自健康师。
  23 +
22 24 ```
23   -加班手工费(F_OvertimeSgfy)= 原始手工费(F_OriginalSgfy)× 加班系数(F_OvertimeCoefficient)
  25 +加班手工费(F_OvertimeSgfy)= 健康师原始手工费之和 × 加班系数(F_OvertimeCoefficient)
24 26 最终手工费(sgfy)= 原始手工费(F_OriginalSgfy)+ 加班手工费(F_OvertimeSgfy)
25 27 ```
26 28  
27   -**示例**:
28   -- 原始手工费 = 100元
  29 +**示例**(健康师12元 + 科技部40元 = 整单原始52元):
  30 +- 原始手工费 = 52元(健康师12 + 科技部40)
  31 +- 健康师原始手工费之和 = 12元
29 32 - 加班系数 = 0.5
30   -- 加班手工费 = 100 × 0.5 = 50元
31   -- 最终手工费 = 100 + 50 = 150元
  33 +- 加班手工费 = 12 × 0.5 = 6元(仅健康师参与)
  34 +- 最终手工费 = 52 + 6 = 58元(= 健康师18 + 科技部40)
32 35  
33 36 ---
34 37  
... ... @@ -94,8 +97,8 @@ LaborCost = ikjbs_tem.laborCost,
94 97 ┌─────────────────────────────────────────────────────────────┐
95 98 │ lq_xh_hyhk(耗卡主表) │
96 99 │ F_OvertimeCoefficient(加班系数) │
97   -│ F_OriginalSgfy(原始手工费) │
98   -│ F_OvertimeSgfy(加班手工费)= OriginalSgfy × Coefficient │
  100 +│ F_OriginalSgfy(原始手工费 = 健康师+科技部) │
  101 +│ F_OvertimeSgfy(加班手工费)= Σ健康师原始手工费 × 系数 │
99 102 │ sgfy(最终手工费)= OriginalSgfy + OvertimeSgfy │
100 103 └─────────────────────────────────────────────────────────────┘
101 104
... ... @@ -127,11 +130,12 @@ LaborCost = ikjbs_tem.laborCost,
127 130  
128 131 **流程**:
129 132 1. 接收 `LqXhHyhkCrInput` 参数,包含 `overtimeCoefficient`
130   -2. 计算主表加班字段
  133 +2. 计算主表加班字段(科技部不参与加班,主表加班手工费 = 健康师加班手工费之和)
131 134 ```csharp
132 135 entity.OvertimeCoefficient = input.overtimeCoefficient ?? 0;
133 136 entity.OriginalSgfy = input.sgfy;
134   - entity.OvertimeSgfy = entity.OriginalSgfy * entity.OvertimeCoefficient;
  137 + var jksOriginalLaborCostSum = input.lqXhPxmxList?.SelectMany(p => p.lqXhJksyjList ?? ...).Sum(j => j.laborCost ?? 0) ?? 0;
  138 + entity.OvertimeSgfy = (decimal)(jksOriginalLaborCostSum * entity.OvertimeCoefficient);
135 139 entity.Sgfy = entity.OriginalSgfy + entity.OvertimeSgfy;
136 140 ```
137 141 3. 遍历品项明细,计算每个品项的加班字段:
... ... @@ -236,7 +240,7 @@ LaborCost = ikjbs_tem.laborCost,
236 240 ```
237 241  
238 242 3. **计算逻辑**:
239   - - 主表:重新计算 `F_OvertimeSgfy` 和 `sgfy`
  243 + - 主表:重新计算 `F_OvertimeSgfy`(= 健康师原始手工费之和 × 系数)和 `sgfy`(科技部不参与加班)
240 244 - 品项明细表:重新计算 `F_OvertimeProjectNumber` 和 `F_ProjectNumber`
241 245 - 健康师业绩表:重新计算 `F_OvertimeKdpxNumber`、`F_kdpxNumber`、`F_OvertimeLaborCost`、`F_LaborCost`
242 246 - 科技部老师业绩表:如果需要支持,重新计算相关字段
... ...
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqDailyReport/TechTeacherDailyStatisticsOutput.cs
... ... @@ -47,6 +47,11 @@ namespace NCC.Extend.Entitys.Dto.LqDailyReport
47 47 /// 开单业绩
48 48 /// </summary>
49 49 public decimal OrderAchievement { get; set; }
  50 +
  51 + /// <summary>
  52 + /// 退卡业绩(用于计算净业绩:净业绩 = 耗卡业绩 - 退卡业绩)
  53 + /// </summary>
  54 + public decimal RefundAchievement { get; set; }
50 55 }
51 56 }
52 57  
... ...
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Mapper/LqReimbursementApplicationMapper.cs
1   -using NCC.Common.Helper;
  1 +using NCC.Common.Helper;
  2 +using NCC.Extend.Entitys;
2 3 using NCC.Extend.Entitys.Dto.LqReimbursementApplication;
3 4 using Mapster;
4 5 using System.Collections.Generic;
... ... @@ -9,6 +10,9 @@ namespace NCC.Extend.Entitys.Mapper.LqReimbursementApplication
9 10 {
10 11 public void Register(TypeAdapterConfig config)
11 12 {
  13 + // 确保 camelCase 的 workflowConfigId 正确映射到 Entity 的 WorkflowConfigId
  14 + config.NewConfig<LqReimbursementApplicationCrInput, LqReimbursementApplicationEntity>()
  15 + .Map(d => d.WorkflowConfigId, s => s.workflowConfigId);
12 16 }
13 17 }
14 18 }
... ...
netcore/src/Modularity/Extend/NCC.Extend/LqDailyReportService.cs
... ... @@ -1159,6 +1159,7 @@ namespace NCC.Extend
1159 1159 /// - ConsumeProjectCount: 消耗项目数
1160 1160 /// - ConsumeAchievement: 消耗业绩
1161 1161 /// - OrderAchievement: 开单业绩
  1162 + /// - RefundAchievement: 退卡业绩(净业绩 = 耗卡业绩 - 退卡业绩)
1162 1163 /// </remarks>
1163 1164 /// <param name="input">查询参数</param>
1164 1165 /// <returns>科技部老师统计列表</returns>
... ... @@ -1193,6 +1194,13 @@ namespace NCC.Extend
1193 1194 teacherFilterForOrder = $"AND ord.kjbls IN ('{teacherIdsStr}')";
1194 1195 }
1195 1196  
  1197 + var teacherFilterForRefund = "";
  1198 + if (input.TeacherIds != null && input.TeacherIds.Any())
  1199 + {
  1200 + var teacherIdsStr = string.Join("','", input.TeacherIds);
  1201 + teacherFilterForRefund = $"AND refund.kjbls IN ('{teacherIdsStr}')";
  1202 + }
  1203 +
1196 1204 // SQL查询:统计科技部老师的消耗业绩、见客数、项目数
1197 1205 // 注意:GROUP BY 中移除了 user.F_RealName,避免同一老师ID因姓名不同产生重复记录
1198 1206 var consumeSql = $@"
... ... @@ -1242,6 +1250,29 @@ namespace NCC.Extend
1242 1250  
1243 1251 var orderResult = await _db.Ado.SqlQueryAsync<dynamic>(orderSql);
1244 1252  
  1253 + // 查询退卡业绩(与耗卡使用相同过滤条件:门店、时间、人员、科技部)
  1254 + var refundSql = $@"
  1255 + SELECT
  1256 + techDept.F_Id as TechDepartmentId,
  1257 + techDept.F_FullName as TechDepartmentName,
  1258 + refund.kjbls as TeacherId,
  1259 + MAX(user.F_RealName) as TeacherName,
  1260 + SUM(refund.kjblsyj) as RefundAchievement
  1261 + FROM lq_hytk_kjbsyj refund
  1262 + INNER JOIN lq_hytk_hytk hytk ON refund.gltkbh = hytk.F_Id
  1263 + INNER JOIN lq_mdxx store ON hytk.md = store.F_Id
  1264 + LEFT JOIN base_organize techDept ON store.kjb = techDept.F_Id
  1265 + LEFT JOIN BASE_USER user ON refund.kjbls = user.F_Id
  1266 + WHERE refund.F_IsEffective = 1
  1267 + AND hytk.F_IsEffective = 1
  1268 + AND DATE(hytk.tksj) >= '{startDate:yyyy-MM-dd}'
  1269 + AND DATE(hytk.tksj) <= '{endDate:yyyy-MM-dd}'
  1270 + {techFilter}
  1271 + {teacherFilterForRefund}
  1272 + GROUP BY techDept.F_Id, techDept.F_FullName, refund.kjbls";
  1273 +
  1274 + var refundResult = await _db.Ado.SqlQueryAsync<dynamic>(refundSql);
  1275 +
1245 1276 // 合并数据:按员工ID汇总,避免同一员工在多个科技部重复出现
1246 1277 // 使用 TeacherId 作为唯一键,汇总所有科技部的数据
1247 1278 var teacherDict = new Dictionary<string, TechTeacherDailyStatisticsOutput>();
... ... @@ -1287,7 +1318,8 @@ namespace NCC.Extend
1287 1318 CustomerCount = 0,
1288 1319 ConsumeProjectCount = 0,
1289 1320 ConsumeAchievement = 0,
1290   - OrderAchievement = 0
  1321 + OrderAchievement = 0,
  1322 + RefundAchievement = 0
1291 1323 };
1292 1324 }
1293 1325  
... ... @@ -1334,7 +1366,8 @@ namespace NCC.Extend
1334 1366 CustomerCount = 0,
1335 1367 ConsumeProjectCount = 0,
1336 1368 ConsumeAchievement = 0,
1337   - OrderAchievement = 0
  1369 + OrderAchievement = 0,
  1370 + RefundAchievement = 0
1338 1371 };
1339 1372 }
1340 1373 else
... ... @@ -1349,7 +1382,38 @@ namespace NCC.Extend
1349 1382 teacherDict[teacherId].OrderAchievement += orderAchievement;
1350 1383 }
1351 1384  
1352   - // 第三步:确定每个员工的主要科技部
  1385 + // 第三步:处理退卡业绩,按员工ID汇总
  1386 + foreach (var item in refundResult ?? Enumerable.Empty<dynamic>())
  1387 + {
  1388 + var teacherId = item.TeacherId?.ToString();
  1389 + var techDeptId = item.TechDepartmentId?.ToString();
  1390 + var techDeptName = item.TechDepartmentName?.ToString();
  1391 + var refundAchievement = Convert.ToDecimal(item.RefundAchievement);
  1392 +
  1393 + if (string.IsNullOrEmpty(teacherId))
  1394 + continue;
  1395 +
  1396 + if (!teacherDict.ContainsKey(teacherId))
  1397 + {
  1398 + // 仅有退卡数据、无消耗和开单时,也创建记录
  1399 + teacherDict[teacherId] = new TechTeacherDailyStatisticsOutput
  1400 + {
  1401 + TechDepartmentId = techDeptId,
  1402 + TechDepartmentName = techDeptName,
  1403 + TeacherId = teacherId,
  1404 + TeacherName = item.TeacherName?.ToString(),
  1405 + CustomerCount = 0,
  1406 + ConsumeProjectCount = 0,
  1407 + ConsumeAchievement = 0,
  1408 + OrderAchievement = 0,
  1409 + RefundAchievement = 0
  1410 + };
  1411 + }
  1412 +
  1413 + teacherDict[teacherId].RefundAchievement += refundAchievement;
  1414 + }
  1415 +
  1416 + // 第四步:确定每个员工的主要科技部
1353 1417 // 优先按消耗业绩最多的科技部,如果消耗业绩为0,则按开单业绩最多的科技部
1354 1418 foreach (var teacherId in teacherDict.Keys.ToList())
1355 1419 {
... ... @@ -1424,7 +1488,7 @@ namespace NCC.Extend
1424 1488 }
1425 1489 }
1426 1490  
1427   - // 第步:重新计算见客数(去重,因为同一个客户可能在多个科技部被统计)
  1491 + // 第步:重新计算见客数(去重,因为同一个客户可能在多个科技部被统计)
1428 1492 // 由于已经汇总了数据,这里需要重新查询去重后的见客数
1429 1493 var teacherIds = teacherDict.Keys.ToList();
1430 1494 if (teacherIds.Any())
... ... @@ -1453,7 +1517,7 @@ namespace NCC.Extend
1453 1517 }
1454 1518 }
1455 1519  
1456   - // 第步:统一查询所有用户名称,确保所有用户都能获取到名称
  1520 + // 第步:统一查询所有用户名称,确保所有用户都能获取到名称
1457 1521 var teacherNamesSql = $@"
1458 1522 SELECT
1459 1523 F_Id as TeacherId,
... ...
netcore/src/Modularity/Extend/NCC.Extend/LqReimbursementApplicationService.cs
... ... @@ -282,8 +282,8 @@ namespace NCC.Extend.LqReimbursementApplication
282 282  
283 283 // 先查询实体类中有CompletionTime字段且符合条件的申请ID
284 284 var entitiesWithCompletionTime = await _db.Queryable<LqReimbursementApplicationEntity>()
285   - .Where(x => x.CompletionTime.HasValue
286   - && x.CompletionTime.Value >= startDate
  285 + .Where(x => x.CompletionTime.HasValue
  286 + && x.CompletionTime.Value >= startDate
287 287 && x.CompletionTime.Value <= endDate)
288 288 .Select(x => x.Id)
289 289 .ToListAsync();
... ... @@ -528,11 +528,8 @@ namespace NCC.Extend.LqReimbursementApplication
528 528 }
529 529 }
530 530  
531   - // 2. 设置报销申请初始状态
532   - if (!string.IsNullOrEmpty(input.workflowConfigId))
533   - {
534   - entity.WorkflowConfigId = input.workflowConfigId.Trim();
535   - }
  531 + // 2. 设置报销申请初始状态(含流程配置ID,确保入库)
  532 + entity.WorkflowConfigId = !string.IsNullOrEmpty(input.workflowConfigId) ? input.workflowConfigId.Trim() : null;
536 533 entity.NodeCount = input.nodes.Count;
537 534 entity.CurrentNodeOrder = 0;
538 535 entity.ApprovalStatus = "待审批";
... ... @@ -546,8 +543,8 @@ namespace NCC.Extend.LqReimbursementApplication
546 543 entity.ApplicationUserName = userInfo.userName;
547 544 }
548 545  
549   - // 3. 保存报销申请表(不使用IgnoreColumns,确保新字段被保存)
550   - var isOk = await _db.Insertable(entity).ExecuteCommandAsync();
  546 + // 3. 保存报销申请表(ignoreNullColumn: false 确保 WorkflowConfigId 等可选字段能正确入库)
  547 + var isOk = await _db.Insertable(entity).IgnoreColumns(ignoreNullColumn: false).ExecuteCommandAsync();
551 548 if (!(isOk > 0)) throw NCCException.Oh(ErrorCode.COM1000);
552 549  
553 550 // 4. 创建节点配置
... ... @@ -825,8 +822,10 @@ namespace NCC.Extend.LqReimbursementApplication
825 822 .ExecuteCommandAsync();
826 823 }
827 824  
828   - // 更新报销申请表(确保 purchaseRecordsId 字段被正确更新)
  825 + // 更新报销申请表(确保 purchaseRecordsId、WorkflowConfigId 等字段被正确更新)
829 826 var entity = input.Adapt<LqReimbursementApplicationEntity>();
  827 + entity.Id = id; // 确保使用路由参数中的主键
  828 + entity.WorkflowConfigId = !string.IsNullOrEmpty(input.workflowConfigId) ? input.workflowConfigId.Trim() : oldEntity?.WorkflowConfigId;
830 829 var isOk = await _db.Updateable(entity).IgnoreColumns(ignoreAllNullColumns: true).ExecuteCommandAsync();
831 830 if (!(isOk > 0)) throw NCCException.Oh(ErrorCode.COM1001);
832 831  
... ...
netcore/src/Modularity/Extend/NCC.Extend/LqXhHyhkService.cs
... ... @@ -1104,9 +1104,12 @@ namespace NCC.Extend.LqXhHyhk
1104 1104 entity.OvertimeCoefficient = input.overtimeCoefficient ?? 0;
1105 1105 entity.OriginalSgfy = input.sgfy;
1106 1106 entity.AppointmentId = input.appointmentId;
1107   - //加班手工费 = 原始手工费 * 加班系数
1108   - entity.OvertimeSgfy = entity.OriginalSgfy * entity.OvertimeCoefficient;
1109   - //最终手工费 = 原始手工费 + 加班手工费
  1107 + // 加班手工费 = 健康师原始手工费之和 × 加班系数(科技部不参与加班)
  1108 + var jksOriginalLaborCostSum = input.lqXhPxmxList?
  1109 + .SelectMany(p => p.lqXhJksyjList ?? Enumerable.Empty<LqXhJksyjCrInput>())
  1110 + .Sum(j => j.laborCost ?? 0) ?? 0;
  1111 + entity.OvertimeSgfy = (decimal)(jksOriginalLaborCostSum * entity.OvertimeCoefficient);
  1112 + // 最终手工费 = 原始手工费 + 加班手工费
1110 1113 entity.Sgfy = entity.OriginalSgfy + entity.OvertimeSgfy;
1111 1114 try
1112 1115 {
... ... @@ -1489,9 +1492,13 @@ namespace NCC.Extend.LqXhHyhk
1489 1492 throw NCCException.Oh(ErrorCode.COM1005, "耗卡记录不存在或已作废");
1490 1493 }
1491 1494 entity.UpdateTime = DateTime.Now;
1492   - entity.OvertimeCoefficient = input.overtimeCoefficient;
  1495 + entity.OvertimeCoefficient = input.overtimeCoefficient ?? 0;
1493 1496 entity.OriginalSgfy = input.sgfy;
1494   - entity.OvertimeSgfy = (decimal)(entity.OvertimeCoefficient * input.sgfy);
  1497 + // 加班手工费 = 健康师原始手工费之和 × 加班系数(科技部不参与加班)
  1498 + var jksOriginalLaborCostSum = input.lqXhPxmxList?
  1499 + .SelectMany(p => p.lqXhJksyjList ?? Enumerable.Empty<LqXhJksyjCrInput>())
  1500 + .Sum(j => j.laborCost ?? 0) ?? 0;
  1501 + entity.OvertimeSgfy = (decimal)(jksOriginalLaborCostSum * (entity.OvertimeCoefficient ?? 0));
1495 1502 entity.Sgfy = entity.OriginalSgfy + entity.OvertimeSgfy;
1496 1503 entity.AppointmentId = input.appointmentId;
1497 1504 //更新会员耗卡记录
... ... @@ -1823,7 +1830,7 @@ namespace NCC.Extend.LqXhHyhk
1823 1830 ///
1824 1831 /// 计算逻辑:
1825 1832 /// 1. 主表(lq_xh_hyhk):
1826   - /// - 加班手工费 = 原始手工费 × 新加班系数
  1833 + /// - 加班手工费 = 健康师原始手工费之和 × 新加班系数(科技部不参与加班)
1827 1834 /// - 最终手工费 = 原始手工费 + 加班手工费
1828 1835 ///
1829 1836 /// 2. 品项明细表(lq_xh_pxmx):
... ... @@ -1881,11 +1888,26 @@ namespace NCC.Extend.LqXhHyhk
1881 1888 }
1882 1889 }
1883 1890  
1884   - // 2. 更新主表加班系数和相关字段
  1891 + // 2. 查询健康师业绩,计算主表加班手工费(科技部不参与加班)
  1892 + var jksyjList = await _db.Queryable<LqXhJksyjEntity>()
  1893 + .Where(x => x.Glkdbh == id && x.IsEffective == StatusEnum.有效.GetHashCode())
  1894 + .ToListAsync();
  1895 + var jksOriginalLaborCostSum = jksyjList.Sum(j =>
  1896 + {
  1897 + var original = j.OriginalLaborCost ?? 0;
  1898 + if (original == 0 && (j.LaborCost ?? 0) > 0)
  1899 + {
  1900 + original = (j.LaborCost ?? 0) - (j.OvertimeLaborCost ?? 0);
  1901 + if (original < 0) original = 0;
  1902 + }
  1903 + return original;
  1904 + });
  1905 +
  1906 + // 3. 更新主表加班系数和相关字段
1885 1907 var newCoefficient = input.overtimeCoefficient ?? 0;
1886 1908 var originalSgfy = entity.OriginalSgfy ?? 0;
1887 1909 entity.OvertimeCoefficient = newCoefficient;
1888   - entity.OvertimeSgfy = (decimal)(originalSgfy * newCoefficient);
  1910 + entity.OvertimeSgfy = (decimal)(jksOriginalLaborCostSum * newCoefficient);
1889 1911 entity.Sgfy = originalSgfy + (entity.OvertimeSgfy ?? 0);
1890 1912 entity.UpdateTime = DateTime.Now;
1891 1913  
... ... @@ -1893,7 +1915,7 @@ namespace NCC.Extend.LqXhHyhk
1893 1915 .UpdateColumns(x => new { x.OvertimeCoefficient, x.OvertimeSgfy, x.Sgfy, x.UpdateTime })
1894 1916 .ExecuteCommandAsync();
1895 1917  
1896   - // 3. 查询所有品项明细,更新加班字段
  1918 + // 4. 查询所有品项明细,更新加班字段
1897 1919 var pxmxList = await _db.Queryable<LqXhPxmxEntity>()
1898 1920 .Where(x => x.ConsumeInfoId == id && x.IsEffective == StatusEnum.有效.GetHashCode())
1899 1921 .ToListAsync();
... ... @@ -1922,11 +1944,7 @@ namespace NCC.Extend.LqXhHyhk
1922 1944 .ExecuteCommandAsync();
1923 1945 }
1924 1946  
1925   - // 4. 查询所有健康师业绩,更新加班字段
1926   - var jksyjList = await _db.Queryable<LqXhJksyjEntity>()
1927   - .Where(x => x.Glkdbh == id && x.IsEffective == StatusEnum.有效.GetHashCode())
1928   - .ToListAsync();
1929   -
  1947 + // 5. 更新健康师业绩加班字段(jksyjList 已在步骤2查询)
1930 1948 foreach (var jksyj in jksyjList)
1931 1949 {
1932 1950 // 如果原始耗卡品项次数为空,使用当前值作为原始值
... ... @@ -1969,7 +1987,7 @@ namespace NCC.Extend.LqXhHyhk
1969 1987 .ExecuteCommandAsync();
1970 1988 }
1971 1989  
1972   - // 5. 科技部老师业绩表:当前代码中不参与加班计算,保持原值不变
  1990 + // 6. 科技部老师业绩表:当前代码中不参与加班计算,保持原值不变
1973 1991 // 如果需要支持,可以取消注释以下代码
1974 1992 /*
1975 1993 var kjbsyjList = await _db.Queryable<LqXhKjbsyjEntity>()
... ...
sql/修复消耗单主表加班手工费历史数据.sql 0 → 100644
  1 +-- ============================================
  2 +-- 修复消耗单主表(lq_xh_hyhk)F_OvertimeSgfy 和 sgfy 历史数据
  3 +-- ============================================
  4 +-- 背景:主表加班手工费已改为「健康师原始手工费之和 × 加班系数」,科技部不参与加班。
  5 +-- 本脚本修复历史数据,使 F_OvertimeSgfy 和 sgfy 符合新逻辑。
  6 +--
  7 +-- 修复逻辑:
  8 +-- F_OvertimeSgfy = 健康师原始手工费之和 × F_OvertimeCoefficient
  9 +-- sgfy = F_OriginalSgfy + F_OvertimeSgfy
  10 +--
  11 +-- 健康师原始手工费:优先用 F_OriginalLaborCost,为空时用 F_LaborCost - F_OvertimeLaborCost
  12 +-- F_OriginalSgfy 为空时:用 健康师原始之和 + 科技部原始之和 补全
  13 +--
  14 +-- 执行前请先备份!建议在测试环境验证后再在生产环境执行。
  15 +
  16 +-- ============================================
  17 +-- 1. 预览:查看需要修复的记录(只读,可先执行验证)
  18 +-- ============================================
  19 +/*
  20 +SELECT
  21 + h.F_Id,
  22 + h.F_OriginalSgfy AS 当前原始手工费,
  23 + h.F_OvertimeSgfy AS 当前加班手工费,
  24 + h.sgfy AS 当前最终手工费,
  25 + h.F_OvertimeCoefficient AS 加班系数,
  26 + COALESCE(j.jks_sum, 0) AS 健康师原始手工费之和,
  27 + COALESCE(j.jks_sum, 0) * COALESCE(h.F_OvertimeCoefficient, 0) AS 新加班手工费,
  28 + 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 新最终手工费
  29 +FROM lq_xh_hyhk h
  30 +LEFT JOIN (
  31 + SELECT glkdbh,
  32 + SUM(COALESCE(F_OriginalLaborCost, GREATEST(0, COALESCE(F_LaborCost, 0) - COALESCE(F_OvertimeLaborCost, 0)))) AS jks_sum
  33 + FROM lq_xh_jksyj
  34 + WHERE F_IsEffective = 1
  35 + GROUP BY glkdbh
  36 +) j ON h.F_Id = j.glkdbh
  37 +LEFT JOIN (
  38 + SELECT glkdbh,
  39 + SUM(COALESCE(F_OriginalLaborCost, F_LaborCost)) AS kjb_sum
  40 + FROM lq_xh_kjbsyj
  41 + WHERE F_IsEffective = 1
  42 + GROUP BY glkdbh
  43 +) k ON h.F_Id = k.glkdbh
  44 +WHERE h.F_IsEffective = 1
  45 + AND (
  46 + COALESCE(h.F_OvertimeSgfy, 0) != COALESCE(j.jks_sum, 0) * COALESCE(h.F_OvertimeCoefficient, 0)
  47 + 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))
  48 + )
  49 +LIMIT 50;
  50 +*/
  51 +
  52 +-- ============================================
  53 +-- 2. 执行修复:更新 F_OvertimeSgfy 和 sgfy
  54 +-- ============================================
  55 +UPDATE lq_xh_hyhk h
  56 +INNER JOIN (
  57 + SELECT glkdbh,
  58 + SUM(COALESCE(F_OriginalLaborCost, GREATEST(0, COALESCE(F_LaborCost, 0) - COALESCE(F_OvertimeLaborCost, 0)))) AS jks_sum
  59 + FROM lq_xh_jksyj
  60 + WHERE F_IsEffective = 1
  61 + GROUP BY glkdbh
  62 +) j ON h.F_Id = j.glkdbh
  63 +LEFT JOIN (
  64 + SELECT glkdbh,
  65 + SUM(COALESCE(F_OriginalLaborCost, F_LaborCost)) AS kjb_sum
  66 + FROM lq_xh_kjbsyj
  67 + WHERE F_IsEffective = 1
  68 + GROUP BY glkdbh
  69 +) k ON h.F_Id = k.glkdbh
  70 +SET
  71 + h.F_OvertimeSgfy = COALESCE(j.jks_sum, 0) * COALESCE(h.F_OvertimeCoefficient, 0),
  72 + 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))
  73 +WHERE h.F_IsEffective = 1;
  74 +
  75 +-- 说明:上述 UPDATE 使用 INNER JOIN j,因此仅更新「存在健康师业绩」的耗卡记录。
  76 +-- 若耗卡仅有科技部、无健康师,则 j 子查询无结果,该记录不会被更新。
  77 +-- 对于「仅科技部」的耗卡,正确逻辑应为:F_OvertimeSgfy=0,sgfy=F_OriginalSgfy,需单独处理。
  78 +
  79 +-- ============================================
  80 +-- 3. 补充修复:仅科技部、无健康师的耗卡(F_OvertimeSgfy 应为 0,sgfy = 原始手工费)
  81 +-- ============================================
  82 +UPDATE lq_xh_hyhk h
  83 +LEFT JOIN (
  84 + SELECT glkdbh
  85 + FROM lq_xh_jksyj
  86 + WHERE F_IsEffective = 1
  87 + GROUP BY glkdbh
  88 +) j ON h.F_Id = j.glkdbh
  89 +LEFT JOIN (
  90 + SELECT glkdbh,
  91 + SUM(COALESCE(F_OriginalLaborCost, F_LaborCost)) AS kjb_sum
  92 + FROM lq_xh_kjbsyj
  93 + WHERE F_IsEffective = 1
  94 + GROUP BY glkdbh
  95 +) k ON h.F_Id = k.glkdbh
  96 +SET
  97 + h.F_OvertimeSgfy = 0,
  98 + h.sgfy = COALESCE(h.F_OriginalSgfy, COALESCE(k.kjb_sum, 0), h.sgfy)
  99 +WHERE h.F_IsEffective = 1
  100 + AND j.glkdbh IS NULL
  101 + AND (COALESCE(h.F_OvertimeSgfy, 0) != 0);
  102 +
  103 +-- ============================================
  104 +-- 4. 验证修复结果(可选)
  105 +-- ============================================
  106 +-- 检查是否仍有不一致记录(应返回 0)
  107 +/*
  108 +SELECT COUNT(*) AS 仍不一致记录数
  109 +FROM lq_xh_hyhk h
  110 +LEFT JOIN (
  111 + SELECT glkdbh,
  112 + SUM(COALESCE(F_OriginalLaborCost, GREATEST(0, COALESCE(F_LaborCost, 0) - COALESCE(F_OvertimeLaborCost, 0)))) AS jks_sum
  113 + FROM lq_xh_jksyj
  114 + WHERE F_IsEffective = 1
  115 + GROUP BY glkdbh
  116 +) j ON h.F_Id = j.glkdbh
  117 +LEFT JOIN (
  118 + SELECT glkdbh,
  119 + SUM(COALESCE(F_OriginalLaborCost, F_LaborCost)) AS kjb_sum
  120 + FROM lq_xh_kjbsyj
  121 + WHERE F_IsEffective = 1
  122 + GROUP BY glkdbh
  123 +) k ON h.F_Id = k.glkdbh
  124 +WHERE h.F_IsEffective = 1
  125 + AND (
  126 + COALESCE(h.F_OvertimeSgfy, 0) != COALESCE(j.jks_sum, 0) * COALESCE(h.F_OvertimeCoefficient, 0)
  127 + 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))
  128 + );
  129 +*/
... ...
test-get-tech-teacher-daily-statistics.sh 0 → 100755
  1 +#!/bin/bash
  2 +# GetTechTeacherDailyStatistics 接口测试脚本
  3 +# 使用方式: ./test-get-tech-teacher-daily-statistics.sh [BASE_URL],默认 http://localhost:2011
  4 +# 前置:API 需已启动且已重新编译(含 RefundAchievement 的 DTO 变更)
  5 +
  6 +BASE_URL="${1:-http://localhost:2011}"
  7 +
  8 +echo "=== 1. 获取 Token ==="
  9 +TOKEN=$(curl -s -X POST "$BASE_URL/api/oauth/Login" \
  10 + -H "Content-Type: application/x-www-form-urlencoded" \
  11 + -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)
  12 +if [ -z "$TOKEN" ]; then
  13 + echo "获取 Token 失败,请检查 API 是否启动"
  14 + exit 1
  15 +fi
  16 +echo "Token 获取成功"
  17 +
  18 +echo ""
  19 +echo "=== 2. 调用 GetTechTeacherDailyStatistics 接口 ==="
  20 +echo "时间范围: 2026-03-01 ~ 2026-03-16"
  21 +RESP=$(curl -s -X POST "$BASE_URL/api/Extend/LqDailyReport/get-tech-teacher-daily-statistics" \
  22 + -H "Authorization: $TOKEN" \
  23 + -H "Content-Type: application/json" \
  24 + -d '{"startTime":"2026-03-01T00:00:00","endTime":"2026-03-16T23:59:59"}')
  25 +echo "$RESP" | python3 -m json.tool 2>/dev/null || echo "$RESP"
  26 +
  27 +CODE=$(echo "$RESP" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('code',-1))" 2>/dev/null)
  28 +if [ "$CODE" != "200" ]; then
  29 + echo ""
  30 + echo "接口返回非 200,若为 500 且提示 set_RefundAchievement,请重启 API 服务后重试"
  31 + exit 1
  32 +fi
  33 +
  34 +echo ""
  35 +echo "=== 3. 数据库验证 SQL(MCP MySQL 或手动执行)==="
  36 +echo "耗卡业绩汇总(lq_xh_kjbsyj + lq_xh_hyhk):"
  37 +echo "SELECT consume.kjbls as TeacherId, SUM(consume.kjblsyj) as ConsumeAchievement"
  38 +echo "FROM lq_xh_kjbsyj consume"
  39 +echo "INNER JOIN lq_xh_hyhk hyhk ON consume.glkdbh = hyhk.F_Id"
  40 +echo "WHERE consume.F_IsEffective = 1 AND hyhk.F_IsEffective = 1"
  41 +echo " AND DATE(hyhk.hksj) >= '2026-03-01' AND DATE(hyhk.hksj) <= '2026-03-16'"
  42 +echo "GROUP BY consume.kjbls;"
  43 +echo ""
  44 +echo "退卡业绩汇总(lq_hytk_kjbsyj + lq_hytk_hytk):"
  45 +echo "SELECT refund.kjbls as TeacherId, SUM(refund.kjblsyj) as RefundAchievement"
  46 +echo "FROM lq_hytk_kjbsyj refund"
  47 +echo "INNER JOIN lq_hytk_hytk hytk ON refund.gltkbh = hytk.F_Id"
  48 +echo "WHERE refund.F_IsEffective = 1 AND hytk.F_IsEffective = 1"
  49 +echo " AND DATE(hytk.tksj) >= '2026-03-01' AND DATE(hytk.tksj) <= '2026-03-16'"
  50 +echo "GROUP BY refund.kjbls;"
... ...
test-xh-overtime-apis.sh 0 → 100644
  1 +#!/bin/bash
  2 +# 消耗单(会员耗卡)加班手工费接口测试脚本
  3 +# 使用方式: ./test-xh-overtime-apis.sh [BASE_URL],默认 http://localhost:2011
  4 +# 前置:API 需已启动
  5 +
  6 +BASE_URL="${1:-http://localhost:2011}"
  7 +
  8 +echo "=== 1. 获取 Token ==="
  9 +TOKEN=$(curl -s -X POST "$BASE_URL/api/oauth/Login" \
  10 + -H "Content-Type: application/x-www-form-urlencoded" \
  11 + -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)
  12 +if [ -z "$TOKEN" ]; then
  13 + echo "获取 Token 失败,请检查 API 是否启动"
  14 + exit 1
  15 +fi
  16 +echo "Token 获取成功"
  17 +
  18 +echo ""
  19 +echo "=== 2. Create 消耗单(健康师12+科技部40=52,系数0.5)==="
  20 +echo "预期:F_OvertimeSgfy=6, sgfy=58"
  21 +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}]}]}'
  22 +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 "请求失败"
  23 +
  24 +echo ""
  25 +echo "=== 3. 创建后需查库验证主表 F_OvertimeSgfy、sgfy ==="
  26 +echo "SQL: SELECT F_Id, sgfy, F_OriginalSgfy, F_OvertimeSgfy FROM lq_xh_hyhk WHERE hy='742276000326354181' ORDER BY F_CreateTime DESC LIMIT 1"
  27 +
  28 +echo ""
  29 +echo "=== 4. Update 消耗单(需替换 {id} 为实际耗卡ID,body 必须包含 id 字段)==="
  30 +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\":[...]}'"
  31 +
  32 +echo ""
  33 +echo "=== 5. UpdateOvertimeCoefficient(需替换 {id} 为实际耗卡ID)==="
  34 +echo "curl -X PUT \"$BASE_URL/api/Extend/LqXhHyhk/{id}/overtime-coefficient\" -H \"Authorization: \$TOKEN\" -H \"Content-Type: application/json\" -d '{\"overtimeCoefficient\":1.0}'"
... ...