Commit 7200c7ad4a219ade20061f6455ade2f394727e89

Authored by “wangming”
1 parent 891ea9b6

feat: enhance welcome page and tech department dashboard

- Redesigned the welcome page layout with a new hero section and action cards for quick access to features.
- Added a feature list in the welcome component to dynamically render action cards.
- Updated the tech department dashboard to improve table display by reducing max height and ensuring proper pagination.
- Enhanced inventory service documentation for clarity on stock update processes.
- Introduced teacher performance metrics in the billing service for better reporting accuracy.
antis-ncc-admin/src/views/extend/techDepartmentDashboard/index.vue
... ... @@ -264,7 +264,8 @@
264 264 size="mini"
265 265 border
266 266 style="width: 100%"
267   - :max-height="500">
  267 + :max-height="440"
  268 + :row-key="(row, index) => `store-detail-${index}`">
268 269 <el-table-column prop="StoreName" label="门店名称" ></el-table-column>
269 270 <el-table-column prop="TraceabilityAmount" label="溯源金额">
270 271 <template slot-scope="scope">¥{{ formatMoney(scope.row.TraceabilityAmount) }}</template>
... ... @@ -304,7 +305,7 @@
304 305 size="mini"
305 306 border
306 307 style="width: 100%"
307   - :max-height="500">
  308 + :max-height="440">
308 309 <el-table-column prop="EmployeeName" label="老师姓名" ></el-table-column>
309 310 <el-table-column prop="StoreName" label="门店" ></el-table-column>
310 311 <el-table-column prop="OrderAchievement" label="开单业绩" >
... ... @@ -349,7 +350,7 @@
349 350 size="mini"
350 351 border
351 352 style="width: 100%"
352   - :max-height="500">
  353 + :max-height="440">
353 354 <el-table-column prop="BillingDate" label="开单日期" ></el-table-column>
354 355 <el-table-column prop="StoreName" label="门店" ></el-table-column>
355 356 <el-table-column prop="MemberName" label="会员" ></el-table-column>
... ... @@ -383,7 +384,7 @@
383 384 size="mini"
384 385 border
385 386 style="width: 100%"
386   - :max-height="500">
  387 + :max-height="440">
387 388 <el-table-column prop="ConsumeDate" label="消耗日期" ></el-table-column>
388 389 <el-table-column prop="StoreName" label="门店" ></el-table-column>
389 390 <el-table-column prop="MemberName" label="会员" ></el-table-column>
... ... @@ -1709,7 +1710,7 @@ export default {
1709 1710 }
1710 1711 }
1711 1712  
1712   - // 明细列表样式
  1713 + // 明细列表样式(表格 max-height 预留分页区域,避免最后一行被遮挡)
1713 1714 .detail-list-row {
1714 1715 margin-top: 16px;
1715 1716 margin-bottom: 16px;
... ... @@ -1729,6 +1730,7 @@ export default {
1729 1730  
1730 1731 .el-table {
1731 1732 flex: 1;
  1733 + min-height: 0;
1732 1734 }
1733 1735 }
1734 1736  
... ... @@ -1745,6 +1747,7 @@ export default {
1745 1747 }
1746 1748  
1747 1749 .detail-pagination {
  1750 + flex-shrink: 0;
1748 1751 margin-top: 16px;
1749 1752 text-align: right;
1750 1753 }
... ...
antis-ncc-admin/src/views/welcome.vue
1 1 <template>
2 2 <div class="welcome-container">
3   - <div class="welcome-content">
4   - <div class="welcome-header">
5   - <h1 class="welcome-title">欢迎使用绿纤美业ERP系统</h1>
6   - <p class="welcome-subtitle">Welcome to Lvqian Beauty ERP System</p>
  3 + <div class="welcome-bg" />
  4 +
  5 + <!-- 顶部大标题区:铺满宽度 -->
  6 + <section class="welcome-hero">
  7 + <div class="hero-inner">
  8 + <div class="hero-icon-wrap">
  9 + <i class="el-icon-office-building hero-icon" />
  10 + </div>
  11 + <h1 class="hero-title">欢迎使用绿纤美业ERP系统</h1>
  12 + <p class="hero-subtitle">Welcome to Lvqian Beauty ERP System</p>
7 13 </div>
  14 + </section>
8 15  
9   - <div class="welcome-footer">
10   - <p>如有问题,请联系系统管理员</p>
  16 + <!-- 快捷入口:全宽网格,多列 -->
  17 + <section class="welcome-actions">
  18 + <div class="actions-inner">
  19 + <div v-for="(item, index) in featureList" :key="index" class="action-card" @click="handleFeatureClick(item)">
  20 + <i :class="item.icon" class="action-icon" />
  21 + <span class="action-label">{{ item.label }}</span>
  22 + </div>
11 23 </div>
12   - </div>
  24 + </section>
  25 +
  26 + <!-- 页脚 -->
  27 + <footer class="welcome-footer">
  28 + <p>如有问题,请联系系统管理员</p>
  29 + </footer>
13 30 </div>
14 31 </template>
15 32  
... ... @@ -17,61 +34,200 @@
17 34 export default {
18 35 name: 'Welcome',
19 36 data() {
20   - return {}
21   - }
  37 + return {
  38 + featureList: [
  39 + { label: '美业仪表板', icon: 'el-icon-data-line', path: '/statisticsList/form9' },
  40 + { label: '开单管理', icon: 'el-icon-document-add', path: '/lqKdKdjlb' },
  41 + { label: '消耗管理', icon: 'el-icon-s-operation', path: '/lqXhHyhk' },
  42 + { label: '会员管理', icon: 'el-icon-user', path: '/lqKhxx' },
  43 + { label: '门店管理', icon: 'el-icon-office-building', path: '/lqMdxx' },
  44 + ],
  45 + }
  46 + },
  47 + methods: {
  48 + handleFeatureClick(item) {
  49 + if (item.path && this.$router) {
  50 + this.$router.push(item.path).catch(() => { })
  51 + }
  52 + },
  53 + },
22 54 }
23 55 </script>
24 56  
25 57 <style lang="scss" scoped>
26 58 .welcome-container {
27   - height: 100%;
  59 + min-height: 100%;
28 60 overflow: hidden;
29   - background: #fff;
  61 + position: relative;
  62 + display: flex;
  63 + flex-direction: column;
  64 + padding: 0;
  65 + box-sizing: border-box;
  66 +}
  67 +
  68 +.welcome-bg {
  69 + position: absolute;
  70 + inset: 0;
  71 + background: linear-gradient(180deg, #e8f4ff 0%, #f5f9ff 28%, #fff 55%, #f8fafc 100%);
  72 + pointer-events: none;
  73 +}
  74 +
  75 +/* 顶部大标题区:全宽、留白充足 */
  76 +.welcome-hero {
  77 + position: relative;
  78 + flex: 0 0 auto;
  79 + padding: 64px 24px 48px;
  80 + text-align: center;
30 81 }
31 82  
32   -.welcome-content {
33   - height: 100%;
34   - padding: 60px 20px;
  83 +.hero-inner {
  84 + max-width: 1200px;
  85 + margin: 0 auto;
  86 +}
  87 +
  88 +.hero-icon-wrap {
  89 + width: 88px;
  90 + height: 88px;
  91 + margin: 0 auto 24px;
  92 + border-radius: 20px;
  93 + background: linear-gradient(135deg, #409eff 0%, #66b1ff 100%);
35 94 display: flex;
36   - flex-direction: column;
37 95 align-items: center;
38 96 justify-content: center;
  97 + box-shadow: 0 12px 32px rgba(64, 158, 255, 0.35);
39 98 }
40 99  
41   -.welcome-header {
42   - text-align: center;
43   - margin-bottom: 60px;
  100 +.hero-icon {
  101 + font-size: 44px;
  102 + color: #fff;
  103 +}
  104 +
  105 +.hero-title {
  106 + font-size: 36px;
  107 + font-weight: 600;
  108 + margin: 0 0 12px 0;
44 109 color: #303133;
  110 + letter-spacing: 0.5px;
  111 + line-height: 1.3;
  112 +}
45 113  
46   - .welcome-title {
47   - font-size: 48px;
48   - font-weight: 600;
49   - margin: 0 0 16px 0;
50   - }
  114 +.hero-subtitle {
  115 + font-size: 16px;
  116 + margin: 0;
  117 + color: #909399;
  118 + font-weight: 400;
  119 +}
51 120  
52   - .welcome-subtitle {
53   - font-size: 20px;
54   - margin: 0;
55   - color: #909399;
  121 +/* 快捷入口:全宽网格,多列卡片 */
  122 +.welcome-actions {
  123 + position: relative;
  124 + flex: 1 1 auto;
  125 + padding: 0 24px 48px;
  126 + display: flex;
  127 + align-items: flex-start;
  128 + justify-content: center;
  129 +}
  130 +
  131 +.actions-inner {
  132 + width: 100%;
  133 + max-width: 960px;
  134 + margin: 0 auto;
  135 + display: grid;
  136 + grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
  137 + gap: 16px;
  138 +}
  139 +
  140 +.action-card {
  141 + display: flex;
  142 + flex-direction: column;
  143 + align-items: center;
  144 + justify-content: center;
  145 + min-height: 100px;
  146 + padding: 24px 20px;
  147 + border-radius: 12px;
  148 + background: #fff;
  149 + border: 1px solid #ebeef5;
  150 + color: #606266;
  151 + font-size: 15px;
  152 + cursor: pointer;
  153 + transition: color 0.2s ease, background 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
  154 +
  155 + &:hover {
  156 + background: #ecf5ff;
  157 + color: #409eff;
  158 + border-color: #d9ecff;
  159 + box-shadow: 0 4px 20px rgba(64, 158, 255, 0.15);
56 160 }
57 161 }
58 162  
  163 +.action-icon {
  164 + font-size: 28px;
  165 + color: #409eff;
  166 + margin-bottom: 12px;
  167 +}
  168 +
  169 +.action-label {
  170 + font-weight: 500;
  171 + text-align: center;
  172 +}
  173 +
  174 +/* 页脚 */
59 175 .welcome-footer {
  176 + position: relative;
  177 + flex: 0 0 auto;
60 178 text-align: center;
61   - color: #909399;
62   - font-size: 14px;
63   - margin-top: 40px;
  179 + padding: 24px 20px;
  180 +
  181 + p {
  182 + margin: 0;
  183 + color: #909399;
  184 + font-size: 13px;
  185 + }
64 186 }
65 187  
66 188 @media (max-width: 768px) {
67   - .welcome-header {
68   - .welcome-title {
69   - font-size: 32px;
70   - }
  189 + .welcome-hero {
  190 + padding: 48px 16px 32px;
  191 + }
71 192  
72   - .welcome-subtitle {
73   - font-size: 16px;
74   - }
  193 + .hero-icon-wrap {
  194 + width: 72px;
  195 + height: 72px;
  196 + margin-bottom: 20px;
  197 + }
  198 +
  199 + .hero-icon {
  200 + font-size: 36px;
  201 + }
  202 +
  203 + .hero-title {
  204 + font-size: 26px;
  205 + }
  206 +
  207 + .hero-subtitle {
  208 + font-size: 14px;
  209 + }
  210 +
  211 + .welcome-actions {
  212 + padding: 0 16px 32px;
  213 + }
  214 +
  215 + .actions-inner {
  216 + grid-template-columns: repeat(2, 1fr);
  217 + gap: 12px;
  218 + max-width: 400px;
  219 + margin: 0 auto;
  220 + }
  221 +
  222 + .action-card {
  223 + min-height: 88px;
  224 + padding: 20px 16px;
  225 + font-size: 14px;
  226 + }
  227 +
  228 + .action-icon {
  229 + font-size: 24px;
  230 + margin-bottom: 8px;
75 231 }
76 232 }
77 233 </style>
... ...
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqKdKdjlb/LqKdKdjlbListOutput.cs
1   -using System;
  1 +using System;
2 2 using System.Collections.Generic;
3 3 using NCC.Extend.Entitys.Dto.LqKdDeductinfo;
4 4  
... ... @@ -192,6 +192,11 @@ namespace NCC.Extend.Entitys.Dto.LqKdKdjlb
192 192 public DateTime? appointmentTime { get; set; }
193 193  
194 194 /// <summary>
  195 + /// 当前查询的科技部老师在本单的业绩合计(仅按科技部老师ID筛选开单列表时有值,用于明细汇总与工资/报表一致,避免用整单实付 sfyj 汇总产生差异)
  196 + /// </summary>
  197 + public decimal teacherOrderAchievement { get; set; }
  198 +
  199 + /// <summary>
195 200 /// 开单品项明细列表
196 201 /// </summary>
197 202 public List<LqKdPxmxInfoOutput> ItemDetails { get; set; }
... ...
netcore/src/Modularity/Extend/NCC.Extend/LqInventoryService.cs
... ... @@ -301,18 +301,16 @@ namespace NCC.Extend
301 301 /// 更新库存信息
302 302 /// </summary>
303 303 /// <remarks>
304   - /// 更新库存记录,支持普通入库和采购入库的金额更新
  304 + /// 更新库存记录,支持普通入库和采购入库的金额/单价更新。
  305 + /// 采购入库:可传 purchaseUnitPrice、finalAmount;普通入库也可传二者以变更该条库存的成本,参与加权平均计算。
305 306 ///
306 307 /// 示例请求(采购入库更新):
307 308 /// ```json
308   - /// {
309   - /// "id": "库存ID",
310   - /// "productId": "产品ID",
311   - /// "quantity": 100,
312   - /// "stockInType": 2,
313   - /// "purchaseUnitPrice": 50.00,
314   - /// "finalAmount": 5000.00
315   - /// }
  309 + /// { "id": "库存ID", "productId": "产品ID", "quantity": 100, "stockInType": 2, "purchaseUnitPrice": 50.00, "finalAmount": 5000.00 }
  310 + /// ```
  311 + /// 示例请求(普通入库变更价格):
  312 + /// ```json
  313 + /// { "id": "库存ID", "productId": "产品ID", "quantity": 100, "stockInType": 1, "purchaseUnitPrice": 30.00, "finalAmount": 3000.00 }
316 314 /// ```
317 315 /// </remarks>
318 316 /// <param name="input">更新输入</param>
... ... @@ -353,25 +351,17 @@ namespace NCC.Extend
353 351 // 入库类型,默认为普通入库
354 352 var stockInType = input.StockInType ?? 1;
355 353  
356   - // 如果是采购入库,验证和计算采购金额
  354 + // 采购入库:必填或选填单价/金额;普通入库:选填单价/金额(需要变更价格时传)
357 355 decimal? purchaseUnitPrice = null;
358 356 decimal? purchaseAmount = null;
359 357 decimal? finalAmount = null;
360 358  
361 359 if (stockInType == 2) // 采购入库
362 360 {
363   - // 验证采购单价
364   - // if (input.PurchaseUnitPrice == null || input.PurchaseUnitPrice <= 0)
365   - // {
366   - // throw NCCException.Oh("采购入库时,采购单价必须大于0");
367   - // }
368   -
369 361 purchaseUnitPrice = input.PurchaseUnitPrice;
370   -
371   - // 计算采购总金额(采购单价 × 数量)
372   - purchaseAmount = purchaseUnitPrice.Value * input.Quantity;
373   -
374   - // 产品最终金额:如果用户提供了,使用用户提供的;否则使用采购总金额
  362 + purchaseAmount = purchaseUnitPrice.HasValue && purchaseUnitPrice.Value > 0
  363 + ? purchaseUnitPrice.Value * input.Quantity
  364 + : null;
375 365 finalAmount = input.FinalAmount ?? purchaseAmount;
376 366  
377 367 // 如果原来没有采购单号,生成新的采购单号
... ... @@ -380,7 +370,6 @@ namespace NCC.Extend
380 370 try
381 371 {
382 372 existingInventory.PurchaseOrderNo = await _billRuleService.GetBillNumber("PurchaseOrder", false);
383   - // 如果返回的是错误信息,也使用fallback
384 373 if (string.IsNullOrEmpty(existingInventory.PurchaseOrderNo) || existingInventory.PurchaseOrderNo == "单据规则不存在")
385 374 {
386 375 _logger.LogWarning("采购单号生成失败(单据规则不存在),使用时间戳作为单号");
... ... @@ -394,6 +383,26 @@ namespace NCC.Extend
394 383 }
395 384 }
396 385 }
  386 + else // 普通入库(stockInType == 1):也支持变更单价/金额,用于成本核算与加权平均
  387 + {
  388 + purchaseUnitPrice = input.PurchaseUnitPrice;
  389 + if (purchaseUnitPrice.HasValue && purchaseUnitPrice.Value > 0)
  390 + {
  391 + purchaseAmount = purchaseUnitPrice.Value * input.Quantity;
  392 + finalAmount = input.FinalAmount ?? purchaseAmount;
  393 + }
  394 + else
  395 + {
  396 + finalAmount = input.FinalAmount;
  397 + purchaseAmount = null;
  398 + }
  399 + }
  400 +
  401 + // 记录修改前的金额和数量(必须在赋值前取,用于日志)
  402 + var oldFinalAmount = existingInventory.FinalAmount;
  403 + var oldQuantity = existingInventory.Quantity;
  404 + var oldPurchaseUnitPrice = existingInventory.PurchaseUnitPrice;
  405 + var oldAveragePrice = product.AveragePrice;
397 406  
398 407 // 更新库存记录
399 408 existingInventory.ProductId = input.ProductId;
... ... @@ -409,14 +418,6 @@ namespace NCC.Extend
409 418 existingInventory.UpdateUser = _userManager.UserId;
410 419 existingInventory.UpdateTime = DateTime.Now;
411 420  
412   - // 记录修改前的金额和数量,用于日志
413   - var oldFinalAmount = existingInventory.FinalAmount;
414   - var oldQuantity = existingInventory.Quantity;
415   - var oldPurchaseUnitPrice = existingInventory.PurchaseUnitPrice;
416   -
417   - // 获取修改前的平均单价(用于日志)
418   - var oldAveragePrice = product.AveragePrice;
419   -
420 421 _db.Ado.BeginTran();
421 422 try
422 423 {
... ... @@ -475,7 +476,9 @@ namespace NCC.Extend
475 476 }
476 477  
477 478 /// <summary>
478   - /// 重新计算产品的平均单价(基于所有有效库存)
  479 + /// 重新计算产品的平均单价(基于所有有效库存,加权平均)
  480 + /// 单价取值顺序:优先 FinalAmount/Quantity,其次 PurchaseUnitPrice,最后 Product.Price。
  481 + /// 采购入库与普通入库在更新时若设置了 FinalAmount 或 PurchaseUnitPrice,均会参与计算。
479 482 /// </summary>
480 483 /// <param name="productId">产品ID</param>
481 484 private async Task RecalculateProductAveragePriceAsync(string productId)
... ...
netcore/src/Modularity/Extend/NCC.Extend/LqKdKdjlbService.cs
... ... @@ -1165,12 +1165,26 @@ namespace NCC.Extend.LqKdKdjlb
1165 1165 var itemDetailsGrouped = itemDetails.GroupBy(x => x.glkdbh)
1166 1166 .ToDictionary(g => g.Key, g => g.ToList());
1167 1167  
1168   - // 为每个开单记录分配品项明细
  1168 + // 当前科技部老师在各开单的业绩合计(用于明细汇总与工资/报表一致,避免用整单实付 sfyj 汇总产生 118271 vs 115071 差异)
  1169 + var teacherAchievementByBilling = new Dictionary<string, decimal>();
  1170 + if (billingIds.Any())
  1171 + {
  1172 + var kjbsyjRows = await _db.Queryable<LqKdKjbsyjEntity>()
  1173 + .Where(x => billingIds.Contains(x.Glkdbh) && x.Kjbls == input.kjblsId && x.IsEffective == StatusEnum.有效.GetHashCode())
  1174 + .Select(x => new { x.Glkdbh, x.Kjblsyj })
  1175 + .ToListAsync();
  1176 + teacherAchievementByBilling = kjbsyjRows
  1177 + .GroupBy(x => x.Glkdbh)
  1178 + .ToDictionary(g => g.Key, g => g.Sum(x => decimal.TryParse(x.Kjblsyj, out var v) ? v : 0m));
  1179 + }
  1180 +
  1181 + // 为每个开单记录分配品项明细及该老师在本单业绩
1169 1182 foreach (var item in data.list)
1170 1183 {
1171 1184 item.ItemDetails = itemDetailsGrouped.ContainsKey(item.id)
1172 1185 ? itemDetailsGrouped[item.id]
1173 1186 : new List<LqKdPxmxInfoOutput>();
  1187 + item.teacherOrderAchievement = teacherAchievementByBilling.ContainsKey(item.id) ? teacherAchievementByBilling[item.id] : 0m;
1174 1188 }
1175 1189  
1176 1190 return PageResult<LqKdKdjlbListOutput>.SqlSugarPageResult(data);
... ...
netcore/src/Modularity/Extend/NCC.Extend/LqReportService.cs
... ... @@ -3264,9 +3264,9 @@ namespace NCC.Extend
3264 3264 BillingPerformance = billing?.Performance ?? 0,
3265 3265 ConsumePerformance = consume?.Performance ?? 0,
3266 3266 RefundPerformance = refund?.Performance ?? 0,
3267   - BillingProjectCount = billing?.ProjectCount ?? 0,
3268   - ConsumeProjectCount = consume?.ProjectCount ?? 0,
3269   - RefundProjectCount = refund?.ProjectCount ?? 0
  3267 + BillingProjectCount = billing != null ? Convert.ToInt32(Convert.ToDecimal(billing.ProjectCount ?? 0)) : 0,
  3268 + ConsumeProjectCount = consume != null ? Convert.ToInt32(Convert.ToDecimal(consume.ProjectCount ?? 0)) : 0,
  3269 + RefundProjectCount = refund != null ? Convert.ToInt32(Convert.ToDecimal(refund.ProjectCount ?? 0)) : 0
3270 3270 };
3271 3271 })
3272 3272 .ToList();
... ... @@ -3348,8 +3348,11 @@ namespace NCC.Extend
3348 3348 .Where(x => !string.IsNullOrEmpty(x.Id))
3349 3349 .ToList();
3350 3350  
3351   - var rankingIds = rankingData.Select(x => x.Id).ToList();
3352   - var rankingIdNameDict = rankingData.ToDictionary(x => x.Id, x => x.Name ?? "未知");
  3351 + // 同一健康师可能有多条记录(如 jksxm 不一致),按 Id 去重,避免 ToDictionary 重复键
  3352 + var rankingIds = rankingData.Select(x => x.Id).Distinct().ToList();
  3353 + var rankingIdNameDict = rankingData
  3354 + .GroupBy(x => x.Id)
  3355 + .ToDictionary(g => g.Key, g => g.First().Name ?? "未知");
3353 3356  
3354 3357 if (!rankingIds.Any())
3355 3358 {
... ... @@ -3383,16 +3386,26 @@ namespace NCC.Extend
3383 3386  
3384 3387 billingDataSql += " GROUP BY jks.jkszh, jks.jksxm";
3385 3388  
3386   - var billingData = (await _db.Ado.SqlQueryAsync<dynamic>(billingDataSql))
3387   - .ToDictionary(
3388   - item => item.health_coach_id?.ToString(),
3389   - item => new
3390   - {
3391   - Name = item.health_coach_name?.ToString() ?? "未知",
3392   - Performance = Convert.ToDecimal(item.billing_performance ?? 0),
3393   - ProjectCount = Convert.ToInt32(item.billing_project_count ?? 0)
3394   - }
3395   - );
  3389 + // 使用显式元组 (Name, Performance, ProjectCount) 避免匿名类型导致 decimal→int 推断错误(与 5753 行门店健康师分析一致)
  3390 + var billingRaw = await _db.Ado.SqlQueryAsync<dynamic>(billingDataSql);
  3391 + var billingData = new Dictionary<string, (string Name, decimal Performance, int ProjectCount)>();
  3392 + foreach (var item in billingRaw)
  3393 + {
  3394 + var id = item.health_coach_id?.ToString();
  3395 + if (string.IsNullOrEmpty(id)) continue;
  3396 + var name = item.health_coach_name?.ToString() ?? "未知";
  3397 + var perf = Convert.ToDecimal(item.billing_performance ?? 0);
  3398 + var count = Convert.ToInt32(Convert.ToDecimal(item.billing_project_count ?? 0));
  3399 + if (billingData.ContainsKey(id))
  3400 + {
  3401 + var existingBilling = billingData[id];
  3402 + billingData[id] = new ValueTuple<string, decimal, int>(existingBilling.Item1, existingBilling.Item2 + perf, existingBilling.Item3 + count);
  3403 + }
  3404 + else
  3405 + {
  3406 + billingData[id] = new ValueTuple<string, decimal, int>(name, perf, count);
  3407 + }
  3408 + }
3396 3409  
3397 3410 // 2.2 查询耗卡业绩数据(使用jkszh作为ID)
3398 3411 var consumeDataSql = $@"
... ... @@ -3417,16 +3430,25 @@ namespace NCC.Extend
3417 3430  
3418 3431 consumeDataSql += " GROUP BY jks.jkszh, jks.jksxm";
3419 3432  
3420   - var consumeData = (await _db.Ado.SqlQueryAsync<dynamic>(consumeDataSql))
3421   - .ToDictionary(
3422   - item => item.health_coach_id?.ToString(),
3423   - item => new
3424   - {
3425   - Name = item.health_coach_name?.ToString() ?? "未知",
3426   - Performance = Convert.ToDecimal(item.consume_performance ?? 0),
3427   - ProjectCount = Convert.ToInt32(item.consume_project_count ?? 0)
3428   - }
3429   - );
  3433 + var consumeRaw = await _db.Ado.SqlQueryAsync<dynamic>(consumeDataSql);
  3434 + var consumeData = new Dictionary<string, (string Name, decimal Performance, int ProjectCount)>();
  3435 + foreach (var item in consumeRaw)
  3436 + {
  3437 + var id = item.health_coach_id?.ToString();
  3438 + if (string.IsNullOrEmpty(id)) continue;
  3439 + var name = item.health_coach_name?.ToString() ?? "未知";
  3440 + var perf = Convert.ToDecimal(item.consume_performance ?? 0);
  3441 + var count = Convert.ToInt32(Convert.ToDecimal(item.consume_project_count ?? 0));
  3442 + if (consumeData.ContainsKey(id))
  3443 + {
  3444 + var existingConsume = consumeData[id];
  3445 + consumeData[id] = new ValueTuple<string, decimal, int>(existingConsume.Item1, existingConsume.Item2 + perf, existingConsume.Item3 + count);
  3446 + }
  3447 + else
  3448 + {
  3449 + consumeData[id] = new ValueTuple<string, decimal, int>(name, perf, count);
  3450 + }
  3451 + }
3430 3452  
3431 3453 // 2.3 查询退卡业绩数据(使用jkszh作为ID)
3432 3454 var refundDataSql = $@"
... ... @@ -3451,35 +3473,45 @@ namespace NCC.Extend
3451 3473  
3452 3474 refundDataSql += " GROUP BY jks.jkszh, jks.jksxm";
3453 3475  
3454   - var refundData = (await _db.Ado.SqlQueryAsync<dynamic>(refundDataSql))
3455   - .ToDictionary(
3456   - item => item.health_coach_id?.ToString(),
3457   - item => new
3458   - {
3459   - Name = item.health_coach_name?.ToString() ?? "未知",
3460   - Performance = Convert.ToDecimal(item.refund_performance ?? 0),
3461   - ProjectCount = Convert.ToInt32(item.refund_project_count ?? 0)
3462   - }
3463   - );
  3476 + var refundRaw = await _db.Ado.SqlQueryAsync<dynamic>(refundDataSql);
  3477 + var refundData = new Dictionary<string, (string Name, decimal Performance, int ProjectCount)>();
  3478 + foreach (var item in refundRaw)
  3479 + {
  3480 + var id = item.health_coach_id?.ToString();
  3481 + if (string.IsNullOrEmpty(id)) continue;
  3482 + var name = item.health_coach_name?.ToString() ?? "未知";
  3483 + var perf = Convert.ToDecimal(item.refund_performance ?? 0);
  3484 + var count = Convert.ToInt32(Convert.ToDecimal(item.refund_project_count ?? 0));
  3485 + if (refundData.ContainsKey(id))
  3486 + {
  3487 + var existingRefund = refundData[id];
  3488 + refundData[id] = new ValueTuple<string, decimal, int>(existingRefund.Item1, existingRefund.Item2 + perf, existingRefund.Item3 + count);
  3489 + }
  3490 + else
  3491 + {
  3492 + refundData[id] = new ValueTuple<string, decimal, int>(name, perf, count);
  3493 + }
  3494 + }
3464 3495  
3465   - // 第三步:合并数据,按原始排序构建排行榜
  3496 + // 第三步:合并数据,按原始排序构建排行榜(元组 ProjectCount 已为 int)
  3497 + (string Name, decimal Performance, int ProjectCount) emptyTuple = default;
3466 3498 var ranking = rankingIds
3467 3499 .Select(id =>
3468 3500 {
3469   - var billing = billingData.ContainsKey(id) ? billingData[id] : null;
3470   - var consume = consumeData.ContainsKey(id) ? consumeData[id] : null;
3471   - var refund = refundData.ContainsKey(id) ? refundData[id] : null;
  3501 + var billing = billingData.ContainsKey(id) ? billingData[id] : emptyTuple;
  3502 + var consume = consumeData.ContainsKey(id) ? consumeData[id] : emptyTuple;
  3503 + var refund = refundData.ContainsKey(id) ? refundData[id] : emptyTuple;
3472 3504  
3473 3505 return new HealthCoachStatisticsOutput
3474 3506 {
3475 3507 HealthCoachId = id,
3476   - HealthCoachName = rankingIdNameDict.ContainsKey(id) ? rankingIdNameDict[id] : (billing?.Name ?? consume?.Name ?? refund?.Name ?? "未知"),
3477   - BillingPerformance = billing?.Performance ?? 0,
3478   - ConsumePerformance = consume?.Performance ?? 0,
3479   - RefundPerformance = refund?.Performance ?? 0,
3480   - BillingProjectCount = billing?.ProjectCount ?? 0,
3481   - ConsumeProjectCount = consume?.ProjectCount ?? 0,
3482   - RefundProjectCount = refund?.ProjectCount ?? 0
  3508 + HealthCoachName = rankingIdNameDict.ContainsKey(id) ? rankingIdNameDict[id] : (billing.Item1 ?? consume.Item1 ?? refund.Item1 ?? "未知"),
  3509 + BillingPerformance = billing.Item2,
  3510 + ConsumePerformance = consume.Item2,
  3511 + RefundPerformance = refund.Item2,
  3512 + BillingProjectCount = Convert.ToInt32(billing.Item3),
  3513 + ConsumeProjectCount = Convert.ToInt32(consume.Item3),
  3514 + RefundProjectCount = Convert.ToInt32(refund.Item3)
3483 3515 };
3484 3516 })
3485 3517 .ToList();
... ... @@ -3695,9 +3727,9 @@ namespace NCC.Extend
3695 3727 BillingPerformance = billing?.Performance ?? 0,
3696 3728 ConsumePerformance = consume?.Performance ?? 0,
3697 3729 RefundPerformance = refund?.Performance ?? 0,
3698   - BillingProjectCount = billing?.ProjectCount ?? 0,
3699   - ConsumeProjectCount = consume?.ProjectCount ?? 0,
3700   - RefundProjectCount = refund?.ProjectCount ?? 0
  3730 + BillingProjectCount = billing != null ? Convert.ToInt32(Convert.ToDecimal(billing.ProjectCount ?? 0)) : 0,
  3731 + ConsumeProjectCount = consume != null ? Convert.ToInt32(Convert.ToDecimal(consume.ProjectCount ?? 0)) : 0,
  3732 + RefundProjectCount = refund != null ? Convert.ToInt32(Convert.ToDecimal(refund.ProjectCount ?? 0)) : 0
3701 3733 };
3702 3734 })
3703 3735 .ToList();
... ...
scripts/sh/test_health_coach_consume_ranking.sh 0 → 100755
  1 +#!/bin/bash
  2 +# 健康师耗卡业绩排行榜接口测试(get-health-coach-consume-ranking)
  3 +# 测试前请确保后端服务已启动(如 http://localhost:2011)
  4 +
  5 +set -e
  6 +BASE_URL="${BASE_URL:-http://localhost:2011}"
  7 +API_URL="${BASE_URL}/api/Extend/LqReport/get-health-coach-consume-ranking"
  8 +
  9 +echo "=========================================="
  10 +echo "健康师耗卡业绩排行榜接口测试"
  11 +echo "BASE_URL=$BASE_URL"
  12 +echo "=========================================="
  13 +
  14 +echo ""
  15 +echo "正在获取 Token..."
  16 +LOGIN_RESPONSE=$(curl -s -X POST "${BASE_URL}/api/oauth/Login" \
  17 + -H "Content-Type: application/x-www-form-urlencoded" \
  18 + -d "account=admin&password=e10adc3949ba59abbe56e057f20f883e")
  19 +TOKEN=$(echo "$LOGIN_RESPONSE" | python3 -c "import sys, json; print(json.load(sys.stdin).get('data',{}).get('token',''))" 2>/dev/null)
  20 +if [ -z "$TOKEN" ]; then
  21 + echo "❌ 获取 Token 失败"
  22 + echo "响应: $LOGIN_RESPONSE"
  23 + exit 1
  24 +fi
  25 +echo "✅ Token 获取成功"
  26 +
  27 +# 测试1:2月(之前报错的时间段)
  28 +echo ""
  29 +echo "=== 测试1: 2月 (2026-02-01 ~ 2026-02-28) ==="
  30 +RESP1=$(curl -s -w "\n%{http_code}" -X POST "$API_URL" \
  31 + -H "Authorization: $TOKEN" \
  32 + -H "Content-Type: application/json" \
  33 + -d '{
  34 + "startTime": "2026-02-01 00:00:00",
  35 + "endTime": "2026-02-28 23:59:59",
  36 + "storeIds": []
  37 + }')
  38 +HTTP1=$(echo "$RESP1" | tail -n1)
  39 +BODY1=$(echo "$RESP1" | sed '$d')
  40 +CODE1=$(echo "$BODY1" | python3 -c "import sys, json; d=json.load(sys.stdin); print(d.get('code', 0))" 2>/dev/null || echo "0")
  41 +echo "HTTP: $HTTP1, code: $CODE1"
  42 +echo "$BODY1" | python3 -m json.tool 2>/dev/null || echo "$BODY1"
  43 +if [ "$HTTP1" = "200" ] && [ "$CODE1" = "200" ]; then
  44 + echo "✅ 测试1 通过:2月接口返回成功"
  45 + # 校验返回结构
  46 + HAS_DATA=$(echo "$BODY1" | python3 -c "import sys, json; d=json.load(sys.stdin); print('data' in d and d.get('data') is not None)" 2>/dev/null || echo "False")
  47 + if [ "$HAS_DATA" = "True" ]; then
  48 + echo " 返回含 data 字段"
  49 + fi
  50 +else
  51 + echo "❌ 测试1 失败"
  52 + exit 1
  53 +fi
  54 +
  55 +# 测试2:1月(正常月份)
  56 +echo ""
  57 +echo "=== 测试2: 1月 (2026-01-01 ~ 2026-01-31) ==="
  58 +RESP2=$(curl -s -w "\n%{http_code}" -X POST "$API_URL" \
  59 + -H "Authorization: $TOKEN" \
  60 + -H "Content-Type: application/json" \
  61 + -d '{
  62 + "startTime": "2026-01-01 00:00:00",
  63 + "endTime": "2026-01-31 23:59:59",
  64 + "storeIds": []
  65 + }')
  66 +HTTP2=$(echo "$RESP2" | tail -n1)
  67 +BODY2=$(echo "$RESP2" | sed '$d')
  68 +CODE2=$(echo "$BODY2" | python3 -c "import sys, json; d=json.load(sys.stdin); print(d.get('code', 0))" 2>/dev/null || echo "0")
  69 +echo "HTTP: $HTTP2, code: $CODE2"
  70 +if [ "$HTTP2" = "200" ] && [ "$CODE2" = "200" ]; then
  71 + echo "✅ 测试2 通过:1月接口返回成功"
  72 +else
  73 + echo "❌ 测试2 失败"
  74 + exit 1
  75 +fi
  76 +
  77 +echo ""
  78 +echo "=========================================="
  79 +echo "健康师耗卡业绩排行榜接口测试完成"
  80 +echo "=========================================="
... ...
scripts/sh/test_lq_inventory_update.sh 0 → 100755
  1 +#!/bin/bash
  2 +# 库存更新接口(PUT /api/Extend/LqInventory/Update)测试脚本
  3 +# 覆盖:采购入库(stockInType=2) 与 普通入库(stockInType=1) 的价格变更逻辑
  4 +# 测试前请确保后端服务已启动(如 http://localhost:2011)
  5 +
  6 +set -e
  7 +BASE_URL="${BASE_URL:-http://localhost:2011}"
  8 +API_UPDATE="${BASE_URL}/api/Extend/LqInventory/Update"
  9 +API_GETLIST="${BASE_URL}/api/Extend/LqInventory/GetList"
  10 +
  11 +echo "=========================================="
  12 +echo "库存更新接口测试 (LqInventory/Update)"
  13 +echo "BASE_URL=$BASE_URL"
  14 +echo "=========================================="
  15 +
  16 +# 获取 Token
  17 +echo ""
  18 +echo "正在获取 Token..."
  19 +LOGIN_RESPONSE=$(curl -s -X POST "${BASE_URL}/api/oauth/Login" \
  20 + -H "Content-Type: application/x-www-form-urlencoded" \
  21 + -d "account=admin&password=e10adc3949ba59abbe56e057f20f883e")
  22 +TOKEN=$(echo "$LOGIN_RESPONSE" | python3 -c "import sys, json; print(json.load(sys.stdin).get('data',{}).get('token',''))" 2>/dev/null)
  23 +if [ -z "$TOKEN" ]; then
  24 + echo "❌ 获取 Token 失败"
  25 + echo "响应: $LOGIN_RESPONSE"
  26 + exit 1
  27 +fi
  28 +echo "✅ Token 获取成功"
  29 +
  30 +# 获取一条有效库存记录(用于更新测试)
  31 +echo ""
  32 +echo "正在获取一条库存记录(用于更新)..."
  33 +LIST_RESPONSE=$(curl -s -X GET "${API_GETLIST}?currentPage=1&pageSize=1" -H "Authorization: $TOKEN")
  34 +CODE=$(echo "$LIST_RESPONSE" | python3 -c "import sys, json; d=json.load(sys.stdin); print(d.get('code', 0))" 2>/dev/null)
  35 +if [ "$CODE" != "200" ]; then
  36 + echo "❌ GetList 失败 (code=$CODE)"
  37 + echo "$LIST_RESPONSE" | python3 -m json.tool 2>/dev/null || echo "$LIST_RESPONSE"
  38 + exit 1
  39 +fi
  40 +INVENTORY_ID=$(echo "$LIST_RESPONSE" | python3 -c "
  41 +import sys, json
  42 +d = json.load(sys.stdin)
  43 +lst = d.get('data',{}).get('list') or d.get('data',{}).get('data',[]) or []
  44 +if not lst:
  45 + sys.exit(1)
  46 +print(lst[0].get('id',''))
  47 +" 2>/dev/null)
  48 +PRODUCT_ID=$(echo "$LIST_RESPONSE" | python3 -c "
  49 +import sys, json
  50 +d = json.load(sys.stdin)
  51 +lst = d.get('data',{}).get('list') or d.get('data',{}).get('data',[]) or []
  52 +if not lst:
  53 + sys.exit(1)
  54 +print(lst[0].get('productId',''))
  55 +" 2>/dev/null)
  56 +
  57 +if [ -z "$INVENTORY_ID" ] || [ -z "$PRODUCT_ID" ]; then
  58 + echo "⚠️ 未获取到库存记录,将使用占位 ID 仅验证接口可达性与参数校验"
  59 + INVENTORY_ID="${INVENTORY_ID:-test-invalid-id}"
  60 + PRODUCT_ID="${PRODUCT_ID:-test-invalid-product}"
  61 +fi
  62 +
  63 +# 测试1:采购入库更新(stockInType=2,带单价与金额)
  64 +echo ""
  65 +echo "=== 测试1: 采购入库更新 (stockInType=2, purchaseUnitPrice=50, finalAmount=5000) ==="
  66 +BODY1=$(cat <<EOF
  67 +{
  68 + "id": "$INVENTORY_ID",
  69 + "productId": "$PRODUCT_ID",
  70 + "quantity": 100,
  71 + "stockInType": 2,
  72 + "purchaseUnitPrice": 50.00,
  73 + "finalAmount": 5000.00
  74 +}
  75 +EOF
  76 +)
  77 +RESP1=$(curl -s -w "\n%{http_code}" -X PUT "$API_UPDATE" \
  78 + -H "Authorization: $TOKEN" \
  79 + -H "Content-Type: application/json" \
  80 + -d "$BODY1")
  81 +HTTP1=$(echo "$RESP1" | tail -n1)
  82 +BODY1_RESP=$(echo "$RESP1" | sed '$d')
  83 +CODE1=$(echo "$BODY1_RESP" | python3 -c "import sys, json; d=json.load(sys.stdin); print(d.get('code', d.get('statusCode', 0)))" 2>/dev/null || echo "0")
  84 +echo "HTTP: $HTTP1, 业务 code: $CODE1"
  85 +echo "$BODY1_RESP" | python3 -m json.tool 2>/dev/null || echo "$BODY1_RESP"
  86 +if [ "$HTTP1" = "200" ] && [ "$CODE1" = "200" ]; then
  87 + echo "✅ 测试1 通过:采购入库更新成功"
  88 +else
  89 + if [ "$INVENTORY_ID" = "test-invalid-id" ]; then
  90 + echo "⚠️ 预期失败(无有效库存ID),接口与参数格式正常"
  91 + else
  92 + echo "❌ 测试1 失败"
  93 + exit 1
  94 + fi
  95 +fi
  96 +
  97 +# 测试2:普通入库更新(stockInType=1,带单价与金额)
  98 +echo ""
  99 +echo "=== 测试2: 普通入库更新 (stockInType=1, purchaseUnitPrice=30, finalAmount=3000) ==="
  100 +BODY2=$(cat <<EOF
  101 +{
  102 + "id": "$INVENTORY_ID",
  103 + "productId": "$PRODUCT_ID",
  104 + "quantity": 100,
  105 + "stockInType": 1,
  106 + "purchaseUnitPrice": 30.00,
  107 + "finalAmount": 3000.00
  108 +}
  109 +EOF
  110 +)
  111 +RESP2=$(curl -s -w "\n%{http_code}" -X PUT "$API_UPDATE" \
  112 + -H "Authorization: $TOKEN" \
  113 + -H "Content-Type: application/json" \
  114 + -d "$BODY2")
  115 +HTTP2=$(echo "$RESP2" | tail -n1)
  116 +BODY2_RESP=$(echo "$RESP2" | sed '$d')
  117 +CODE2=$(echo "$BODY2_RESP" | python3 -c "import sys, json; d=json.load(sys.stdin); print(d.get('code', d.get('statusCode', 0)))" 2>/dev/null || echo "0")
  118 +echo "HTTP: $HTTP2, 业务 code: $CODE2"
  119 +echo "$BODY2_RESP" | python3 -m json.tool 2>/dev/null || echo "$BODY2_RESP"
  120 +if [ "$HTTP2" = "200" ] && [ "$CODE2" = "200" ]; then
  121 + echo "✅ 测试2 通过:普通入库价格变更成功"
  122 +else
  123 + if [ "$INVENTORY_ID" = "test-invalid-id" ]; then
  124 + echo "⚠️ 预期失败(无有效库存ID),接口与参数格式正常"
  125 + else
  126 + echo "❌ 测试2 失败"
  127 + exit 1
  128 + fi
  129 +fi
  130 +
  131 +# 测试3:参数校验(数量<=0 应报错)
  132 +echo ""
  133 +echo "=== 测试3: 参数校验(quantity=0 应返回错误) ==="
  134 +BODY3=$(cat <<EOF
  135 +{
  136 + "id": "$INVENTORY_ID",
  137 + "productId": "$PRODUCT_ID",
  138 + "quantity": 0,
  139 + "stockInType": 1
  140 +}
  141 +EOF
  142 +)
  143 +RESP3=$(curl -s -X PUT "$API_UPDATE" \
  144 + -H "Authorization: $TOKEN" \
  145 + -H "Content-Type: application/json" \
  146 + -d "$BODY3")
  147 +CODE3=$(echo "$RESP3" | python3 -c "import sys, json; d=json.load(sys.stdin); print(d.get('code', 200))" 2>/dev/null || echo "200")
  148 +echo "业务 code: $CODE3 (预期非 200)"
  149 +if [ "$CODE3" != "200" ]; then
  150 + echo "✅ 测试3 通过:数量校验生效"
  151 +else
  152 + echo "⚠️ 测试3:接口未对 quantity<=0 返回错误(请确认业务是否允许)"
  153 +fi
  154 +
  155 +# 测试4:验证更新后加权平均单价计算正确(与 RecalculateProductAveragePriceAsync 一致)
  156 +if [ "$INVENTORY_ID" != "test-invalid-id" ] && [ -n "$PRODUCT_ID" ]; then
  157 + echo ""
  158 + echo "=== 测试4: 验证加权平均单价计算 ==="
  159 + API_GETINFO="${BASE_URL}/api/Extend/LqProduct/GetInfo"
  160 + INFO_RESP=$(curl -s -X GET "${API_GETINFO}?id=${PRODUCT_ID}" -H "Authorization: $TOKEN")
  161 + LIST_FULL=$(curl -s -X GET "${API_GETLIST}?productId=${PRODUCT_ID}&currentPage=1&pageSize=100" -H "Authorization: $TOKEN")
  162 + VERIFY=$(echo "$INFO_RESP
  163 +$LIST_FULL" | python3 -c "
  164 +import sys, json
  165 +lines = sys.stdin.read().strip().split('\n')
  166 +if len(lines) < 2:
  167 + print('FAIL: no data')
  168 + sys.exit(1)
  169 +info = json.loads(lines[0])
  170 +lst_data = json.loads(lines[1])
  171 +if info.get('code') != 200:
  172 + print('FAIL: GetInfo code', info.get('code'))
  173 + sys.exit(1)
  174 +lst = lst_data.get('data',{}).get('list') or lst_data.get('data',{}).get('data',[]) or []
  175 +# 产品基础价(无 FinalAmount/PurchaseUnitPrice 时用);产品当前平均单价(接口返回)
  176 +product_price = float(info.get('data',{}).get('price') or 0)
  177 +product_avg = float(info.get('data',{}).get('averagePrice') or 0)
  178 +# 与 RecalculateProductAveragePriceAsync 一致:优先 FinalAmount/Quantity,其次 PurchaseUnitPrice,最后 Product.Price
  179 +total_amount = 0
  180 +total_qty = 0
  181 +for row in lst:
  182 + qty = int(row.get('quantity') or 0)
  183 + if qty <= 0:
  184 + continue
  185 + fa = row.get('finalAmount')
  186 + pu = row.get('purchaseUnitPrice')
  187 + if fa is not None and float(fa or 0) > 0:
  188 + unit = float(fa) / qty
  189 + elif pu is not None and float(pu or 0) > 0:
  190 + unit = float(pu)
  191 + else:
  192 + unit = product_price
  193 + total_amount += unit * qty
  194 + total_qty += qty
  195 +expected_avg = (total_amount / total_qty) if total_qty > 0 else product_price
  196 +diff = abs(expected_avg - product_avg) if product_avg else abs(expected_avg)
  197 +# 允许四舍五入误差
  198 +ok = diff < 0.02
  199 +print('OK' if ok else 'FAIL')
  200 +print('expected_avg=%.4f product_avg=%.4f total_amount=%.2f total_qty=%d' % (expected_avg, product_avg, total_amount, total_qty))
  201 +" 2>/dev/null)
  202 + VERIFY_OK=$(echo "$VERIFY" | head -1)
  203 + VERIFY_DETAIL=$(echo "$VERIFY" | tail -1)
  204 + if [ "$VERIFY_OK" = "OK" ]; then
  205 + echo "✅ 测试4 通过:加权平均单价与接口返回一致 ($VERIFY_DETAIL)"
  206 + else
  207 + echo "❌ 测试4 失败:加权平均与产品平均单价不一致"
  208 + echo " $VERIFY_DETAIL"
  209 + exit 1
  210 + fi
  211 +else
  212 + echo ""
  213 + echo "=== 测试4: 跳过(无有效库存/产品ID) ==="
  214 +fi
  215 +
  216 +echo ""
  217 +echo "=========================================="
  218 +echo "库存更新接口测试完成"
  219 +echo "=========================================="
... ...
sql/修正品项直播-生命之波-预览影响行数.sql 0 → 100644
  1 +-- ============================================
  2 +-- 预览:品项「直播-生命之波」在各表中的影响行数(仅查询,不更新)
  3 +-- ============================================
  4 +-- 执行「修正品项直播-生命之波开单耗卡退卡分类-执行版.sql」前,可先执行本脚本确认影响范围。
  5 +-- 品项以 xmmc = '直播-生命之波' 匹配;若有多条请改用 F_Id。
  6 +-- ============================================
  7 +
  8 +-- 品项当前值(请确认 qt2、F_BeautyType 已是目标值)
  9 +SELECT xmzl.F_Id, xmzl.xmmc, xmzl.qt2 AS 品项分类, xmzl.F_BeautyType AS 科美类型
  10 +FROM lq_xmzl xmzl
  11 +WHERE xmzl.xmmc = '直播-生命之波';
  12 +
  13 +-- 各表将更新的行数
  14 +SELECT 'lq_kd_pxmx' AS 表名, COUNT(*) AS 影响行数 FROM lq_kd_pxmx pxmx INNER JOIN lq_xmzl xmzl ON pxmx.px = xmzl.F_Id WHERE xmzl.xmmc = '直播-生命之波'
  15 +UNION ALL
  16 +SELECT 'lq_kd_deductinfo' AS 表名, COUNT(*) AS 影响行数 FROM lq_kd_deductinfo d INNER JOIN lq_xmzl xmzl ON d.F_ItemId = xmzl.F_Id WHERE xmzl.xmmc = '直播-生命之波'
  17 +UNION ALL
  18 +SELECT 'lq_xh_pxmx' AS 表名, COUNT(*) AS 影响行数 FROM lq_xh_pxmx pxmx INNER JOIN lq_xmzl xmzl ON pxmx.px = xmzl.F_Id WHERE xmzl.xmmc = '直播-生命之波'
  19 +UNION ALL
  20 +SELECT 'lq_hytk_mx' AS 表名, COUNT(*) AS 影响行数 FROM lq_hytk_mx mx INNER JOIN lq_xmzl xmzl ON mx.px = xmzl.F_Id WHERE xmzl.xmmc = '直播-生命之波'
  21 +UNION ALL
  22 +SELECT 'lq_kd_jksyj' AS 表名, COUNT(*) AS 影响行数 FROM lq_kd_jksyj j INNER JOIN lq_xmzl xmzl ON j.F_ItemId = xmzl.F_Id WHERE xmzl.xmmc = '直播-生命之波'
  23 +UNION ALL
  24 +SELECT 'lq_kd_jksyj(pxmx)' AS 表名, COUNT(*) AS 影响行数 FROM lq_kd_jksyj j INNER JOIN lq_kd_pxmx px ON j.F_kdpxid = px.F_Id INNER JOIN lq_xmzl xmzl ON px.px = xmzl.F_Id WHERE xmzl.xmmc = '直播-生命之波'
  25 +UNION ALL
  26 +SELECT 'lq_xh_jksyj' AS 表名, COUNT(*) AS 影响行数 FROM lq_xh_jksyj j INNER JOIN lq_xmzl xmzl ON j.F_ItemId = xmzl.F_Id WHERE xmzl.xmmc = '直播-生命之波'
  27 +UNION ALL
  28 +SELECT 'lq_hytk_jksyj' AS 表名, COUNT(*) AS 影响行数 FROM lq_hytk_jksyj j INNER JOIN lq_xmzl xmzl ON j.F_ItemId = xmzl.F_Id WHERE xmzl.xmmc = '直播-生命之波'
  29 +UNION ALL
  30 +SELECT 'lq_kd_kjbsyj' AS 表名, COUNT(*) AS 影响行数 FROM lq_kd_kjbsyj j INNER JOIN lq_xmzl xmzl ON j.F_ItemId = xmzl.F_Id WHERE xmzl.xmmc = '直播-生命之波'
  31 +UNION ALL
  32 +SELECT 'lq_kd_kjbsyj(pxmx)'AS 表名, COUNT(*) AS 影响行数 FROM lq_kd_kjbsyj j INNER JOIN lq_kd_pxmx px ON j.F_kdpxid = px.F_Id INNER JOIN lq_xmzl xmzl ON px.px = xmzl.F_Id WHERE xmzl.xmmc = '直播-生命之波'
  33 +UNION ALL
  34 +SELECT 'lq_xh_kjbsyj' AS 表名, COUNT(*) AS 影响行数 FROM lq_xh_kjbsyj j INNER JOIN lq_xmzl xmzl ON j.F_ItemId = xmzl.F_Id WHERE xmzl.xmmc = '直播-生命之波'
  35 +UNION ALL
  36 +SELECT 'lq_hytk_kjbsyj' AS 表名, COUNT(*) AS 影响行数 FROM lq_hytk_kjbsyj j INNER JOIN lq_xmzl xmzl ON j.F_ItemId = xmzl.F_Id WHERE xmzl.xmmc = '直播-生命之波';
... ...
sql/修正品项直播-生命之波开单耗卡退卡分类-执行版.sql 0 → 100644
  1 +-- ============================================
  2 +-- 修正品项「直播-生命之波」历史数据的 F_BeautyType(仅置为 NULL)
  3 +-- ============================================
  4 +-- 背景:该品项之前被错误设置为「溯源系统」,实际不属于溯源系统和 cell。
  5 +-- F_ItemCategory 无需修改(已是正确值),本脚本仅将开单、耗卡、退卡等相关表中的
  6 +-- F_BeautyType 置为 NULL。
  7 +--
  8 +-- 涉及表:
  9 +-- 1. lq_kd_pxmx 开单品项明细
  10 +-- 2. lq_xh_pxmx 耗卡品项明细
  11 +-- 3. lq_hytk_mx 退卡品项明细
  12 +-- 4. lq_kd_jksyj 开单健康师业绩
  13 +-- 5. lq_xh_jksyj 耗卡健康师业绩
  14 +-- 6. lq_hytk_jksyj 退卡健康师业绩
  15 +-- 7. lq_kd_kjbsyj 开单科技部老师业绩
  16 +-- 8. lq_xh_kjbsyj 耗卡科技部老师业绩
  17 +-- 9. lq_hytk_kjbsyj 退卡科技部老师业绩
  18 +--
  19 +-- 建议先执行「预览版」查询确认影响行数,再执行本脚本。
  20 +-- ============================================
  21 +
  22 +-- 1. 开单品项明细表
  23 +UPDATE lq_kd_pxmx pxmx
  24 +INNER JOIN lq_xmzl xmzl ON pxmx.px = xmzl.F_Id
  25 +SET pxmx.F_BeautyType = NULL
  26 +WHERE xmzl.xmmc = '直播-生命之波';
  27 +
  28 +-- 2. 耗卡品项明细表
  29 +UPDATE lq_xh_pxmx pxmx
  30 +INNER JOIN lq_xmzl xmzl ON pxmx.px = xmzl.F_Id
  31 +SET pxmx.F_BeautyType = NULL
  32 +WHERE xmzl.xmmc = '直播-生命之波';
  33 +
  34 +-- 3. 退卡品项明细表
  35 +UPDATE lq_hytk_mx mx
  36 +INNER JOIN lq_xmzl xmzl ON mx.px = xmzl.F_Id
  37 +SET mx.F_BeautyType = NULL
  38 +WHERE xmzl.xmmc = '直播-生命之波';
  39 +
  40 +-- 4. 开单健康师业绩表(F_ItemId 关联 + F_kdpxid 经开单品项明细关联,双路径覆盖)
  41 +UPDATE lq_kd_jksyj j
  42 +INNER JOIN lq_xmzl xmzl ON j.F_ItemId = xmzl.F_Id
  43 +SET j.F_BeautyType = NULL
  44 +WHERE xmzl.xmmc = '直播-生命之波';
  45 +
  46 +UPDATE lq_kd_jksyj j
  47 +INNER JOIN lq_kd_pxmx px ON j.F_kdpxid = px.F_Id
  48 +INNER JOIN lq_xmzl xmzl ON px.px = xmzl.F_Id
  49 +SET j.F_BeautyType = NULL
  50 +WHERE xmzl.xmmc = '直播-生命之波';
  51 +
  52 +-- 5. 耗卡健康师业绩表
  53 +UPDATE lq_xh_jksyj j
  54 +INNER JOIN lq_xmzl xmzl ON j.F_ItemId = xmzl.F_Id
  55 +SET j.F_BeautyType = NULL
  56 +WHERE xmzl.xmmc = '直播-生命之波';
  57 +
  58 +-- 6. 退卡健康师业绩表
  59 +UPDATE lq_hytk_jksyj j
  60 +INNER JOIN lq_xmzl xmzl ON j.F_ItemId = xmzl.F_Id
  61 +SET j.F_BeautyType = NULL
  62 +WHERE xmzl.xmmc = '直播-生命之波';
  63 +
  64 +-- 7. 开单科技部老师业绩表(F_ItemId 关联 + F_kdpxid 经开单品项明细关联,双路径覆盖)
  65 +UPDATE lq_kd_kjbsyj j
  66 +INNER JOIN lq_xmzl xmzl ON j.F_ItemId = xmzl.F_Id
  67 +SET j.F_BeautyType = NULL
  68 +WHERE xmzl.xmmc = '直播-生命之波';
  69 +
  70 +UPDATE lq_kd_kjbsyj j
  71 +INNER JOIN lq_kd_pxmx px ON j.F_kdpxid = px.F_Id
  72 +INNER JOIN lq_xmzl xmzl ON px.px = xmzl.F_Id
  73 +SET j.F_BeautyType = NULL
  74 +WHERE xmzl.xmmc = '直播-生命之波';
  75 +
  76 +-- 8. 耗卡科技部老师业绩表
  77 +UPDATE lq_xh_kjbsyj j
  78 +INNER JOIN lq_xmzl xmzl ON j.F_ItemId = xmzl.F_Id
  79 +SET j.F_BeautyType = NULL
  80 +WHERE xmzl.xmmc = '直播-生命之波';
  81 +
  82 +-- 9. 退卡科技部老师业绩表
  83 +UPDATE lq_hytk_kjbsyj j
  84 +INNER JOIN lq_xmzl xmzl ON j.F_ItemId = xmzl.F_Id
  85 +SET j.F_BeautyType = NULL
  86 +WHERE xmzl.xmmc = '直播-生命之波';
  87 +
  88 +-- ============================================
  89 +-- 说明:
  90 +-- - 若 lq_xmzl 中该品项名称为「直播-生命之波」存在多条(不同 F_Id),
  91 +-- 请先确认唯一品项或改用 F_Id 限定,例如:WHERE xmzl.F_Id = '具体品项ID'。
  92 +-- ============================================
... ...
sql/科美业绩与科技部老师开单业绩差异核对.sql 0 → 100644
  1 +-- ============================================================
  2 +-- 科美业绩(1163803) vs 科技部老师开单业绩(1165702.63) 差异核对
  3 +-- 理论上两者应相等,差异约 1899.63。本脚本用于排查原因,不修改任何数据。
  4 +-- ============================================================
  5 +
  6 +-- 1) 口径说明
  7 +-- 科美业绩:一般指「开单品项明细」中 品项分类=科美 的 F_ActualPrice 之和(或来自台账选科美时的汇总)
  8 +-- 科技部老师开单业绩:lq_kd_kjbsyj 表的 kjblsyj 之和
  9 +
  10 +-- 2) 同一时间范围下两数分别多少(请按需修改时间)
  11 +SET @start = '2026-01-01 00:00:00';
  12 +SET @end = '2026-01-31 23:59:59';
  13 +
  14 +-- 2.1 科美业绩:按开单品项明细,品项分类=科美,有效开单+有效明细
  15 +SELECT
  16 + '科美业绩-按品项明细(pxmx.F_ItemCategory=科美)' AS 口径,
  17 + COALESCE(SUM(px.F_ActualPrice), 0) AS 金额
  18 +FROM lq_kd_pxmx px
  19 +INNER JOIN lq_kd_kdjlb kd ON px.glkdbh = kd.F_Id AND kd.F_IsEffective = 1
  20 +WHERE px.F_IsEffective = 1
  21 + AND (px.F_ItemCategory = '科美' OR EXISTS (SELECT 1 FROM lq_xmzl zl WHERE zl.F_Id = px.px AND zl.F_IsEffective = 1 AND zl.qt2 = '科美'))
  22 + AND kd.kdrq >= @start
  23 + AND kd.kdrq <= @end;
  24 +
  25 +-- 2.2 科技部老师开单业绩:lq_kd_kjbsyj 全表汇总(当前系统统计方式,未过滤科美)
  26 +SELECT
  27 + '科技部老师开单业绩-全表kjblsyj' AS 口径,
  28 + COALESCE(SUM(CAST(NULLIF(TRIM(k.kjblsyj), '') AS DECIMAL(18,2))), 0) AS 金额
  29 +FROM lq_kd_kjbsyj k
  30 +INNER JOIN lq_kd_kdjlb kd ON k.glkdbh = kd.F_Id AND kd.F_IsEffective = 1
  31 +WHERE k.F_IsEffective = 1
  32 + AND k.yjsj >= @start
  33 + AND k.yjsj <= @end
  34 + AND k.kjblsyj IS NOT NULL
  35 + AND TRIM(k.kjblsyj) != ''
  36 + AND TRIM(k.kjblsyj) != '0';
  37 +
  38 +-- 2.3 科技部老师开单业绩:仅 F_ItemCategory='科美'(若只统计科美,应与科美业绩一致)
  39 +SELECT
  40 + '科技部老师开单业绩-仅科美F_ItemCategory' AS 口径,
  41 + COALESCE(SUM(CAST(NULLIF(TRIM(k.kjblsyj), '') AS DECIMAL(18,2))), 0) AS 金额
  42 +FROM lq_kd_kjbsyj k
  43 +INNER JOIN lq_kd_kdjlb kd ON k.glkdbh = kd.F_Id AND kd.F_IsEffective = 1
  44 +WHERE k.F_IsEffective = 1
  45 + AND k.F_ItemCategory = '科美'
  46 + AND k.yjsj >= @start
  47 + AND k.yjsj <= @end
  48 + AND k.kjblsyj IS NOT NULL
  49 + AND TRIM(k.kjblsyj) != ''
  50 + AND TRIM(k.kjblsyj) != '0';
  51 +
  52 +-- 3) 差异来源:科技部表里「非科美」或「品项分类为空」的金额(多出来的部分)
  53 +SELECT
  54 + '科技部-非科美或空分类金额' AS 口径,
  55 + COALESCE(SUM(CAST(NULLIF(TRIM(k.kjblsyj), '') AS DECIMAL(18,2))), 0) AS 金额
  56 +FROM lq_kd_kjbsyj k
  57 +INNER JOIN lq_kd_kdjlb kd ON k.glkdbh = kd.F_Id AND kd.F_IsEffective = 1
  58 +WHERE k.F_IsEffective = 1
  59 + AND k.yjsj >= @start
  60 + AND k.yjsj <= @end
  61 + AND (k.F_ItemCategory IS NULL OR k.F_ItemCategory = '' OR k.F_ItemCategory != '科美')
  62 + AND k.kjblsyj IS NOT NULL
  63 + AND TRIM(k.kjblsyj) != ''
  64 + AND TRIM(k.kjblsyj) != '0';
  65 +
  66 +-- 4) 科美品项有明细但科技部无业绩 / 科技部有业绩但品项非科美(明细级抽查)
  67 +-- 4.1 科美品项明细对应的科技部业绩合计(按开单明细ID关联)
  68 +SELECT
  69 + '按开单明细关联-科美品项对应kjbsyj之和' AS 口径,
  70 + COALESCE(SUM(CAST(NULLIF(TRIM(k.kjblsyj), '') AS DECIMAL(18,2))), 0) AS 金额
  71 +FROM lq_kd_pxmx px
  72 +INNER JOIN lq_kd_kdjlb kd ON px.glkdbh = kd.F_Id AND kd.F_IsEffective = 1
  73 +LEFT JOIN lq_kd_kjbsyj k ON k.F_kdpxid = px.F_Id AND k.F_IsEffective = 1
  74 +WHERE px.F_IsEffective = 1
  75 + AND px.F_ItemCategory = '科美'
  76 + AND kd.kdrq >= @start
  77 + AND kd.kdrq <= @end
  78 + AND k.kjblsyj IS NOT NULL
  79 + AND TRIM(k.kjblsyj) != ''
  80 + AND TRIM(k.kjblsyj) != '0';
  81 +
  82 +-- 4.2 科美品项明细的 ActualPrice 之和(与 4.1 同范围,用于对比)
  83 +SELECT
  84 + '科美品项ActualPrice之和(同范围)' AS 口径,
  85 + COALESCE(SUM(px.F_ActualPrice), 0) AS 金额
  86 +FROM lq_kd_pxmx px
  87 +INNER JOIN lq_kd_kdjlb kd ON px.glkdbh = kd.F_Id AND kd.F_IsEffective = 1
  88 +WHERE px.F_IsEffective = 1
  89 + AND (px.F_ItemCategory = '科美' OR EXISTS (SELECT 1 FROM lq_xmzl zl WHERE zl.F_Id = px.px AND zl.F_IsEffective = 1 AND zl.qt2 = '科美'))
  90 + AND kd.kdrq >= @start
  91 + AND kd.kdrq <= @end;
  92 +
  93 +-- 5) 查看 lq_kd_kjbsyj 中 F_ItemCategory 的分布(是否有空或非科美)
  94 +SELECT
  95 + COALESCE(k.F_ItemCategory, '(空)') AS 品项分类,
  96 + COUNT(*) AS 条数,
  97 + COALESCE(SUM(CAST(NULLIF(TRIM(k.kjblsyj), '') AS DECIMAL(18,2))), 0) AS 金额合计
  98 +FROM lq_kd_kjbsyj k
  99 +INNER JOIN lq_kd_kdjlb kd ON k.glkdbh = kd.F_Id AND kd.F_IsEffective = 1
  100 +WHERE k.F_IsEffective = 1
  101 + AND k.yjsj >= @start
  102 + AND k.yjsj <= @end
  103 + AND k.kjblsyj IS NOT NULL
  104 + AND TRIM(k.kjblsyj) != ''
  105 + AND TRIM(k.kjblsyj) != '0'
  106 +GROUP BY COALESCE(k.F_ItemCategory, '(空)');
... ...
sql/科美业绩差异-具体开单明细.sql 0 → 100644
  1 +-- 查出导致「科技部老师开单业绩」多于「科美业绩」的那些开单数据(科技部表里 非科美 或 品项分类为空 的明细)
  2 +-- 时间范围:与台账一致 2026-01-01 ~ 2026-01-22(若需整月可改为 2026-01-31 23:59:59)
  3 +SET @start = '2026-01-01 00:00:00';
  4 +SET @end = '2026-01-22 23:59:59';
  5 +
  6 +-- 差异来源:lq_kd_kjbsyj 中 F_ItemCategory 非科美或为空的记录(按开单号列出)
  7 +SELECT
  8 + kd.F_Id AS 开单ID,
  9 + kd.kdbh AS 开单编号,
  10 + kd.kdrq AS 开单日期,
  11 + k.F_Id AS 科技部业绩明细ID,
  12 + COALESCE(k.F_ItemCategory, '(空)') AS 品项分类_科技部,
  13 + CAST(NULLIF(TRIM(k.kjblsyj), '') AS DECIMAL(18,2)) AS 科技部业绩金额,
  14 + k.yjsj AS 业绩时间
  15 +FROM lq_kd_kjbsyj k
  16 +INNER JOIN lq_kd_kdjlb kd ON k.glkdbh = kd.F_Id AND kd.F_IsEffective = 1
  17 +WHERE k.F_IsEffective = 1
  18 + AND k.yjsj >= @start
  19 + AND k.yjsj <= @end
  20 + AND (k.F_ItemCategory IS NULL OR k.F_ItemCategory = '' OR k.F_ItemCategory != '科美')
  21 + AND k.kjblsyj IS NOT NULL
  22 + AND TRIM(k.kjblsyj) != ''
  23 + AND TRIM(k.kjblsyj) != '0'
  24 +ORDER BY kd.kdrq, kd.F_Id, k.yjsj;
... ...
sql/科美业绩差异-剔除储扣后仍存在差异的开单.sql 0 → 100644
  1 +-- ============================================================
  2 +-- 科美业绩差异:剔除「储扣一致」后的真实差异开单
  3 +-- 时间范围:2026-01-01 ~ 2026-01-22(与台账一致)
  4 +-- ============================================================
  5 +--
  6 +-- 【剔除规则】
  7 +-- 若 品项明细金额 - 科技部业绩 ≈ 该开单的储扣金额(全部储扣,不限于科美,误差<=0.02),
  8 +-- 视为储扣导致,从结果中剔除;只列出「剔除后仍存在差异」的开单。
  9 +--
  10 +-- 【本脚本包含两个查询】
  11 +-- ① 有差异的开单列表:只输出 开单ID、开单日期,用于把有差异的开单列出来;
  12 +-- ② 有差异的开单明细:输出 开单ID、开单日期、品项明细金额、科技部业绩金额、储扣金额_全部、差异,用于分析。
  13 +--
  14 +-- 【查询②结果列】开单ID | 开单日期 | 品项明细金额 | 科技部业绩金额 | 储扣金额_全部 | 差异
  15 +--
  16 +-- ============================================================
  17 +
  18 +-- 一、有差异的开单列表(仅列开单ID与日期,便于核对)
  19 +SELECT
  20 + kd.F_Id AS 开单ID,
  21 + kd.kdrq AS 开单日期
  22 +FROM lq_kd_pxmx px
  23 +INNER JOIN lq_kd_kdjlb kd ON px.glkdbh = kd.F_Id AND kd.F_IsEffective = 1
  24 +WHERE px.F_IsEffective = 1
  25 + AND px.F_ItemCategory = '科美'
  26 + AND kd.kdrq >= '2026-01-01 00:00:00'
  27 + AND kd.kdrq <= '2026-01-22 23:59:59'
  28 +GROUP BY kd.F_Id, kd.kdrq
  29 +HAVING ABS(SUM(px.F_ActualPrice) - (SELECT COALESCE(SUM(CAST(NULLIF(TRIM(k2.kjblsyj), '') AS DECIMAL(18,2))), 0)
  30 + FROM lq_kd_kjbsyj k2
  31 + WHERE k2.glkdbh = kd.F_Id AND k2.F_IsEffective = 1 AND k2.F_ItemCategory = '科美'
  32 + AND k2.yjsj >= '2026-01-01 00:00:00' AND k2.yjsj <= '2026-01-22 23:59:59')) > 0.01
  33 + AND ABS((SUM(px.F_ActualPrice) - (SELECT COALESCE(SUM(CAST(NULLIF(TRIM(k2.kjblsyj), '') AS DECIMAL(18,2))), 0)
  34 + FROM lq_kd_kjbsyj k2
  35 + WHERE k2.glkdbh = kd.F_Id AND k2.F_IsEffective = 1 AND k2.F_ItemCategory = '科美'
  36 + AND k2.yjsj >= '2026-01-01 00:00:00' AND k2.yjsj <= '2026-01-22 23:59:59'))
  37 + - COALESCE((SELECT SUM(d.F_Amount) FROM lq_kd_deductinfo d
  38 + WHERE d.F_BillingId = kd.F_Id AND d.F_IsEffective = 1), 0)) > 0.02
  39 +ORDER BY kd.kdrq;
  40 +
  41 +-- 二、有差异的开单明细(含金额与储扣,便于分析)
  42 +SELECT
  43 + kd.F_Id AS 开单ID,
  44 + kd.kdrq AS 开单日期,
  45 + ROUND(SUM(px.F_ActualPrice), 2) AS 品项明细金额,
  46 + ROUND((SELECT COALESCE(SUM(CAST(NULLIF(TRIM(k2.kjblsyj), '') AS DECIMAL(18,2))), 0)
  47 + FROM lq_kd_kjbsyj k2
  48 + WHERE k2.glkdbh = kd.F_Id AND k2.F_IsEffective = 1 AND k2.F_ItemCategory = '科美'
  49 + AND k2.yjsj >= '2026-01-01 00:00:00' AND k2.yjsj <= '2026-01-22 23:59:59'), 2) AS 科技部业绩金额,
  50 + ROUND(COALESCE((SELECT SUM(d.F_Amount) FROM lq_kd_deductinfo d
  51 + WHERE d.F_BillingId = kd.F_Id AND d.F_IsEffective = 1), 0), 2) AS 储扣金额_全部,
  52 + ROUND(SUM(px.F_ActualPrice) - (SELECT COALESCE(SUM(CAST(NULLIF(TRIM(k2.kjblsyj), '') AS DECIMAL(18,2))), 0)
  53 + FROM lq_kd_kjbsyj k2
  54 + WHERE k2.glkdbh = kd.F_Id AND k2.F_IsEffective = 1 AND k2.F_ItemCategory = '科美'
  55 + AND k2.yjsj >= '2026-01-01 00:00:00' AND k2.yjsj <= '2026-01-22 23:59:59'), 2) AS 差异
  56 +FROM lq_kd_pxmx px
  57 +INNER JOIN lq_kd_kdjlb kd ON px.glkdbh = kd.F_Id AND kd.F_IsEffective = 1
  58 +WHERE px.F_IsEffective = 1
  59 + AND px.F_ItemCategory = '科美'
  60 + AND kd.kdrq >= '2026-01-01 00:00:00'
  61 + AND kd.kdrq <= '2026-01-22 23:59:59'
  62 +GROUP BY kd.F_Id, kd.kdrq
  63 +HAVING ABS(SUM(px.F_ActualPrice) - (SELECT COALESCE(SUM(CAST(NULLIF(TRIM(k2.kjblsyj), '') AS DECIMAL(18,2))), 0)
  64 + FROM lq_kd_kjbsyj k2
  65 + WHERE k2.glkdbh = kd.F_Id AND k2.F_IsEffective = 1 AND k2.F_ItemCategory = '科美'
  66 + AND k2.yjsj >= '2026-01-01 00:00:00' AND k2.yjsj <= '2026-01-22 23:59:59')) > 0.01
  67 + AND ABS((SUM(px.F_ActualPrice) - (SELECT COALESCE(SUM(CAST(NULLIF(TRIM(k2.kjblsyj), '') AS DECIMAL(18,2))), 0)
  68 + FROM lq_kd_kjbsyj k2
  69 + WHERE k2.glkdbh = kd.F_Id AND k2.F_IsEffective = 1 AND k2.F_ItemCategory = '科美'
  70 + AND k2.yjsj >= '2026-01-01 00:00:00' AND k2.yjsj <= '2026-01-22 23:59:59'))
  71 + - COALESCE((SELECT SUM(d.F_Amount) FROM lq_kd_deductinfo d
  72 + WHERE d.F_BillingId = kd.F_Id AND d.F_IsEffective = 1), 0)) > 0.02
  73 +ORDER BY kd.kdrq;
... ...