-
@@ -17,61 +34,200 @@
export default {
name: 'Welcome',
data() {
- return {}
- }
+ return {
+ featureList: [
+ { label: '美业仪表板', icon: 'el-icon-data-line', path: '/statisticsList/form9' },
+ { label: '开单管理', icon: 'el-icon-document-add', path: '/lqKdKdjlb' },
+ { label: '消耗管理', icon: 'el-icon-s-operation', path: '/lqXhHyhk' },
+ { label: '会员管理', icon: 'el-icon-user', path: '/lqKhxx' },
+ { label: '门店管理', icon: 'el-icon-office-building', path: '/lqMdxx' },
+ ],
+ }
+ },
+ methods: {
+ handleFeatureClick(item) {
+ if (item.path && this.$router) {
+ this.$router.push(item.path).catch(() => { })
+ }
+ },
+ },
}
diff --git a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqKdKdjlb/LqKdKdjlbListOutput.cs b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqKdKdjlb/LqKdKdjlbListOutput.cs
index 7ad3605..bb15298 100644
--- a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqKdKdjlb/LqKdKdjlbListOutput.cs
+++ b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqKdKdjlb/LqKdKdjlbListOutput.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using NCC.Extend.Entitys.Dto.LqKdDeductinfo;
@@ -192,6 +192,11 @@ namespace NCC.Extend.Entitys.Dto.LqKdKdjlb
public DateTime? appointmentTime { get; set; }
///
+ /// 当前查询的科技部老师在本单的业绩合计(仅按科技部老师ID筛选开单列表时有值,用于明细汇总与工资/报表一致,避免用整单实付 sfyj 汇总产生差异)
+ ///
+ public decimal teacherOrderAchievement { get; set; }
+
+ ///
/// 开单品项明细列表
///
public List
ItemDetails { get; set; }
diff --git a/netcore/src/Modularity/Extend/NCC.Extend/LqInventoryService.cs b/netcore/src/Modularity/Extend/NCC.Extend/LqInventoryService.cs
index 9fe6761..737cdd1 100644
--- a/netcore/src/Modularity/Extend/NCC.Extend/LqInventoryService.cs
+++ b/netcore/src/Modularity/Extend/NCC.Extend/LqInventoryService.cs
@@ -301,18 +301,16 @@ namespace NCC.Extend
/// 更新库存信息
///
///
- /// 更新库存记录,支持普通入库和采购入库的金额更新
+ /// 更新库存记录,支持普通入库和采购入库的金额/单价更新。
+ /// 采购入库:可传 purchaseUnitPrice、finalAmount;普通入库也可传二者以变更该条库存的成本,参与加权平均计算。
///
/// 示例请求(采购入库更新):
/// ```json
- /// {
- /// "id": "库存ID",
- /// "productId": "产品ID",
- /// "quantity": 100,
- /// "stockInType": 2,
- /// "purchaseUnitPrice": 50.00,
- /// "finalAmount": 5000.00
- /// }
+ /// { "id": "库存ID", "productId": "产品ID", "quantity": 100, "stockInType": 2, "purchaseUnitPrice": 50.00, "finalAmount": 5000.00 }
+ /// ```
+ /// 示例请求(普通入库变更价格):
+ /// ```json
+ /// { "id": "库存ID", "productId": "产品ID", "quantity": 100, "stockInType": 1, "purchaseUnitPrice": 30.00, "finalAmount": 3000.00 }
/// ```
///
/// 更新输入
@@ -353,25 +351,17 @@ namespace NCC.Extend
// 入库类型,默认为普通入库
var stockInType = input.StockInType ?? 1;
- // 如果是采购入库,验证和计算采购金额
+ // 采购入库:必填或选填单价/金额;普通入库:选填单价/金额(需要变更价格时传)
decimal? purchaseUnitPrice = null;
decimal? purchaseAmount = null;
decimal? finalAmount = null;
if (stockInType == 2) // 采购入库
{
- // 验证采购单价
- // if (input.PurchaseUnitPrice == null || input.PurchaseUnitPrice <= 0)
- // {
- // throw NCCException.Oh("采购入库时,采购单价必须大于0");
- // }
-
purchaseUnitPrice = input.PurchaseUnitPrice;
-
- // 计算采购总金额(采购单价 × 数量)
- purchaseAmount = purchaseUnitPrice.Value * input.Quantity;
-
- // 产品最终金额:如果用户提供了,使用用户提供的;否则使用采购总金额
+ purchaseAmount = purchaseUnitPrice.HasValue && purchaseUnitPrice.Value > 0
+ ? purchaseUnitPrice.Value * input.Quantity
+ : null;
finalAmount = input.FinalAmount ?? purchaseAmount;
// 如果原来没有采购单号,生成新的采购单号
@@ -380,7 +370,6 @@ namespace NCC.Extend
try
{
existingInventory.PurchaseOrderNo = await _billRuleService.GetBillNumber("PurchaseOrder", false);
- // 如果返回的是错误信息,也使用fallback
if (string.IsNullOrEmpty(existingInventory.PurchaseOrderNo) || existingInventory.PurchaseOrderNo == "单据规则不存在")
{
_logger.LogWarning("采购单号生成失败(单据规则不存在),使用时间戳作为单号");
@@ -394,6 +383,26 @@ namespace NCC.Extend
}
}
}
+ else // 普通入库(stockInType == 1):也支持变更单价/金额,用于成本核算与加权平均
+ {
+ purchaseUnitPrice = input.PurchaseUnitPrice;
+ if (purchaseUnitPrice.HasValue && purchaseUnitPrice.Value > 0)
+ {
+ purchaseAmount = purchaseUnitPrice.Value * input.Quantity;
+ finalAmount = input.FinalAmount ?? purchaseAmount;
+ }
+ else
+ {
+ finalAmount = input.FinalAmount;
+ purchaseAmount = null;
+ }
+ }
+
+ // 记录修改前的金额和数量(必须在赋值前取,用于日志)
+ var oldFinalAmount = existingInventory.FinalAmount;
+ var oldQuantity = existingInventory.Quantity;
+ var oldPurchaseUnitPrice = existingInventory.PurchaseUnitPrice;
+ var oldAveragePrice = product.AveragePrice;
// 更新库存记录
existingInventory.ProductId = input.ProductId;
@@ -409,14 +418,6 @@ namespace NCC.Extend
existingInventory.UpdateUser = _userManager.UserId;
existingInventory.UpdateTime = DateTime.Now;
- // 记录修改前的金额和数量,用于日志
- var oldFinalAmount = existingInventory.FinalAmount;
- var oldQuantity = existingInventory.Quantity;
- var oldPurchaseUnitPrice = existingInventory.PurchaseUnitPrice;
-
- // 获取修改前的平均单价(用于日志)
- var oldAveragePrice = product.AveragePrice;
-
_db.Ado.BeginTran();
try
{
@@ -475,7 +476,9 @@ namespace NCC.Extend
}
///
- /// 重新计算产品的平均单价(基于所有有效库存)
+ /// 重新计算产品的平均单价(基于所有有效库存,加权平均)
+ /// 单价取值顺序:优先 FinalAmount/Quantity,其次 PurchaseUnitPrice,最后 Product.Price。
+ /// 采购入库与普通入库在更新时若设置了 FinalAmount 或 PurchaseUnitPrice,均会参与计算。
///
/// 产品ID
private async Task RecalculateProductAveragePriceAsync(string productId)
diff --git a/netcore/src/Modularity/Extend/NCC.Extend/LqKdKdjlbService.cs b/netcore/src/Modularity/Extend/NCC.Extend/LqKdKdjlbService.cs
index 537b1cb..5dbc9aa 100644
--- a/netcore/src/Modularity/Extend/NCC.Extend/LqKdKdjlbService.cs
+++ b/netcore/src/Modularity/Extend/NCC.Extend/LqKdKdjlbService.cs
@@ -1165,12 +1165,26 @@ namespace NCC.Extend.LqKdKdjlb
var itemDetailsGrouped = itemDetails.GroupBy(x => x.glkdbh)
.ToDictionary(g => g.Key, g => g.ToList());
- // 为每个开单记录分配品项明细
+ // 当前科技部老师在各开单的业绩合计(用于明细汇总与工资/报表一致,避免用整单实付 sfyj 汇总产生 118271 vs 115071 差异)
+ var teacherAchievementByBilling = new Dictionary();
+ if (billingIds.Any())
+ {
+ var kjbsyjRows = await _db.Queryable()
+ .Where(x => billingIds.Contains(x.Glkdbh) && x.Kjbls == input.kjblsId && x.IsEffective == StatusEnum.有效.GetHashCode())
+ .Select(x => new { x.Glkdbh, x.Kjblsyj })
+ .ToListAsync();
+ teacherAchievementByBilling = kjbsyjRows
+ .GroupBy(x => x.Glkdbh)
+ .ToDictionary(g => g.Key, g => g.Sum(x => decimal.TryParse(x.Kjblsyj, out var v) ? v : 0m));
+ }
+
+ // 为每个开单记录分配品项明细及该老师在本单业绩
foreach (var item in data.list)
{
item.ItemDetails = itemDetailsGrouped.ContainsKey(item.id)
? itemDetailsGrouped[item.id]
: new List();
+ item.teacherOrderAchievement = teacherAchievementByBilling.ContainsKey(item.id) ? teacherAchievementByBilling[item.id] : 0m;
}
return PageResult.SqlSugarPageResult(data);
diff --git a/netcore/src/Modularity/Extend/NCC.Extend/LqReportService.cs b/netcore/src/Modularity/Extend/NCC.Extend/LqReportService.cs
index 07dff10..d8e8954 100644
--- a/netcore/src/Modularity/Extend/NCC.Extend/LqReportService.cs
+++ b/netcore/src/Modularity/Extend/NCC.Extend/LqReportService.cs
@@ -3264,9 +3264,9 @@ namespace NCC.Extend
BillingPerformance = billing?.Performance ?? 0,
ConsumePerformance = consume?.Performance ?? 0,
RefundPerformance = refund?.Performance ?? 0,
- BillingProjectCount = billing?.ProjectCount ?? 0,
- ConsumeProjectCount = consume?.ProjectCount ?? 0,
- RefundProjectCount = refund?.ProjectCount ?? 0
+ BillingProjectCount = billing != null ? Convert.ToInt32(Convert.ToDecimal(billing.ProjectCount ?? 0)) : 0,
+ ConsumeProjectCount = consume != null ? Convert.ToInt32(Convert.ToDecimal(consume.ProjectCount ?? 0)) : 0,
+ RefundProjectCount = refund != null ? Convert.ToInt32(Convert.ToDecimal(refund.ProjectCount ?? 0)) : 0
};
})
.ToList();
@@ -3348,8 +3348,11 @@ namespace NCC.Extend
.Where(x => !string.IsNullOrEmpty(x.Id))
.ToList();
- var rankingIds = rankingData.Select(x => x.Id).ToList();
- var rankingIdNameDict = rankingData.ToDictionary(x => x.Id, x => x.Name ?? "未知");
+ // 同一健康师可能有多条记录(如 jksxm 不一致),按 Id 去重,避免 ToDictionary 重复键
+ var rankingIds = rankingData.Select(x => x.Id).Distinct().ToList();
+ var rankingIdNameDict = rankingData
+ .GroupBy(x => x.Id)
+ .ToDictionary(g => g.Key, g => g.First().Name ?? "未知");
if (!rankingIds.Any())
{
@@ -3383,16 +3386,26 @@ namespace NCC.Extend
billingDataSql += " GROUP BY jks.jkszh, jks.jksxm";
- var billingData = (await _db.Ado.SqlQueryAsync(billingDataSql))
- .ToDictionary(
- item => item.health_coach_id?.ToString(),
- item => new
- {
- Name = item.health_coach_name?.ToString() ?? "未知",
- Performance = Convert.ToDecimal(item.billing_performance ?? 0),
- ProjectCount = Convert.ToInt32(item.billing_project_count ?? 0)
- }
- );
+ // 使用显式元组 (Name, Performance, ProjectCount) 避免匿名类型导致 decimal→int 推断错误(与 5753 行门店健康师分析一致)
+ var billingRaw = await _db.Ado.SqlQueryAsync(billingDataSql);
+ var billingData = new Dictionary();
+ foreach (var item in billingRaw)
+ {
+ var id = item.health_coach_id?.ToString();
+ if (string.IsNullOrEmpty(id)) continue;
+ var name = item.health_coach_name?.ToString() ?? "未知";
+ var perf = Convert.ToDecimal(item.billing_performance ?? 0);
+ var count = Convert.ToInt32(Convert.ToDecimal(item.billing_project_count ?? 0));
+ if (billingData.ContainsKey(id))
+ {
+ var existingBilling = billingData[id];
+ billingData[id] = new ValueTuple(existingBilling.Item1, existingBilling.Item2 + perf, existingBilling.Item3 + count);
+ }
+ else
+ {
+ billingData[id] = new ValueTuple(name, perf, count);
+ }
+ }
// 2.2 查询耗卡业绩数据(使用jkszh作为ID)
var consumeDataSql = $@"
@@ -3417,16 +3430,25 @@ namespace NCC.Extend
consumeDataSql += " GROUP BY jks.jkszh, jks.jksxm";
- var consumeData = (await _db.Ado.SqlQueryAsync(consumeDataSql))
- .ToDictionary(
- item => item.health_coach_id?.ToString(),
- item => new
- {
- Name = item.health_coach_name?.ToString() ?? "未知",
- Performance = Convert.ToDecimal(item.consume_performance ?? 0),
- ProjectCount = Convert.ToInt32(item.consume_project_count ?? 0)
- }
- );
+ var consumeRaw = await _db.Ado.SqlQueryAsync(consumeDataSql);
+ var consumeData = new Dictionary();
+ foreach (var item in consumeRaw)
+ {
+ var id = item.health_coach_id?.ToString();
+ if (string.IsNullOrEmpty(id)) continue;
+ var name = item.health_coach_name?.ToString() ?? "未知";
+ var perf = Convert.ToDecimal(item.consume_performance ?? 0);
+ var count = Convert.ToInt32(Convert.ToDecimal(item.consume_project_count ?? 0));
+ if (consumeData.ContainsKey(id))
+ {
+ var existingConsume = consumeData[id];
+ consumeData[id] = new ValueTuple(existingConsume.Item1, existingConsume.Item2 + perf, existingConsume.Item3 + count);
+ }
+ else
+ {
+ consumeData[id] = new ValueTuple(name, perf, count);
+ }
+ }
// 2.3 查询退卡业绩数据(使用jkszh作为ID)
var refundDataSql = $@"
@@ -3451,35 +3473,45 @@ namespace NCC.Extend
refundDataSql += " GROUP BY jks.jkszh, jks.jksxm";
- var refundData = (await _db.Ado.SqlQueryAsync(refundDataSql))
- .ToDictionary(
- item => item.health_coach_id?.ToString(),
- item => new
- {
- Name = item.health_coach_name?.ToString() ?? "未知",
- Performance = Convert.ToDecimal(item.refund_performance ?? 0),
- ProjectCount = Convert.ToInt32(item.refund_project_count ?? 0)
- }
- );
+ var refundRaw = await _db.Ado.SqlQueryAsync(refundDataSql);
+ var refundData = new Dictionary();
+ foreach (var item in refundRaw)
+ {
+ var id = item.health_coach_id?.ToString();
+ if (string.IsNullOrEmpty(id)) continue;
+ var name = item.health_coach_name?.ToString() ?? "未知";
+ var perf = Convert.ToDecimal(item.refund_performance ?? 0);
+ var count = Convert.ToInt32(Convert.ToDecimal(item.refund_project_count ?? 0));
+ if (refundData.ContainsKey(id))
+ {
+ var existingRefund = refundData[id];
+ refundData[id] = new ValueTuple(existingRefund.Item1, existingRefund.Item2 + perf, existingRefund.Item3 + count);
+ }
+ else
+ {
+ refundData[id] = new ValueTuple(name, perf, count);
+ }
+ }
- // 第三步:合并数据,按原始排序构建排行榜
+ // 第三步:合并数据,按原始排序构建排行榜(元组 ProjectCount 已为 int)
+ (string Name, decimal Performance, int ProjectCount) emptyTuple = default;
var ranking = rankingIds
.Select(id =>
{
- var billing = billingData.ContainsKey(id) ? billingData[id] : null;
- var consume = consumeData.ContainsKey(id) ? consumeData[id] : null;
- var refund = refundData.ContainsKey(id) ? refundData[id] : null;
+ var billing = billingData.ContainsKey(id) ? billingData[id] : emptyTuple;
+ var consume = consumeData.ContainsKey(id) ? consumeData[id] : emptyTuple;
+ var refund = refundData.ContainsKey(id) ? refundData[id] : emptyTuple;
return new HealthCoachStatisticsOutput
{
HealthCoachId = id,
- HealthCoachName = rankingIdNameDict.ContainsKey(id) ? rankingIdNameDict[id] : (billing?.Name ?? consume?.Name ?? refund?.Name ?? "未知"),
- BillingPerformance = billing?.Performance ?? 0,
- ConsumePerformance = consume?.Performance ?? 0,
- RefundPerformance = refund?.Performance ?? 0,
- BillingProjectCount = billing?.ProjectCount ?? 0,
- ConsumeProjectCount = consume?.ProjectCount ?? 0,
- RefundProjectCount = refund?.ProjectCount ?? 0
+ HealthCoachName = rankingIdNameDict.ContainsKey(id) ? rankingIdNameDict[id] : (billing.Item1 ?? consume.Item1 ?? refund.Item1 ?? "未知"),
+ BillingPerformance = billing.Item2,
+ ConsumePerformance = consume.Item2,
+ RefundPerformance = refund.Item2,
+ BillingProjectCount = Convert.ToInt32(billing.Item3),
+ ConsumeProjectCount = Convert.ToInt32(consume.Item3),
+ RefundProjectCount = Convert.ToInt32(refund.Item3)
};
})
.ToList();
@@ -3695,9 +3727,9 @@ namespace NCC.Extend
BillingPerformance = billing?.Performance ?? 0,
ConsumePerformance = consume?.Performance ?? 0,
RefundPerformance = refund?.Performance ?? 0,
- BillingProjectCount = billing?.ProjectCount ?? 0,
- ConsumeProjectCount = consume?.ProjectCount ?? 0,
- RefundProjectCount = refund?.ProjectCount ?? 0
+ BillingProjectCount = billing != null ? Convert.ToInt32(Convert.ToDecimal(billing.ProjectCount ?? 0)) : 0,
+ ConsumeProjectCount = consume != null ? Convert.ToInt32(Convert.ToDecimal(consume.ProjectCount ?? 0)) : 0,
+ RefundProjectCount = refund != null ? Convert.ToInt32(Convert.ToDecimal(refund.ProjectCount ?? 0)) : 0
};
})
.ToList();
diff --git a/scripts/sh/test_health_coach_consume_ranking.sh b/scripts/sh/test_health_coach_consume_ranking.sh
new file mode 100755
index 0000000..e9d4d05
--- /dev/null
+++ b/scripts/sh/test_health_coach_consume_ranking.sh
@@ -0,0 +1,80 @@
+#!/bin/bash
+# 健康师耗卡业绩排行榜接口测试(get-health-coach-consume-ranking)
+# 测试前请确保后端服务已启动(如 http://localhost:2011)
+
+set -e
+BASE_URL="${BASE_URL:-http://localhost:2011}"
+API_URL="${BASE_URL}/api/Extend/LqReport/get-health-coach-consume-ranking"
+
+echo "=========================================="
+echo "健康师耗卡业绩排行榜接口测试"
+echo "BASE_URL=$BASE_URL"
+echo "=========================================="
+
+echo ""
+echo "正在获取 Token..."
+LOGIN_RESPONSE=$(curl -s -X POST "${BASE_URL}/api/oauth/Login" \
+ -H "Content-Type: application/x-www-form-urlencoded" \
+ -d "account=admin&password=e10adc3949ba59abbe56e057f20f883e")
+TOKEN=$(echo "$LOGIN_RESPONSE" | python3 -c "import sys, json; print(json.load(sys.stdin).get('data',{}).get('token',''))" 2>/dev/null)
+if [ -z "$TOKEN" ]; then
+ echo "❌ 获取 Token 失败"
+ echo "响应: $LOGIN_RESPONSE"
+ exit 1
+fi
+echo "✅ Token 获取成功"
+
+# 测试1:2月(之前报错的时间段)
+echo ""
+echo "=== 测试1: 2月 (2026-02-01 ~ 2026-02-28) ==="
+RESP1=$(curl -s -w "\n%{http_code}" -X POST "$API_URL" \
+ -H "Authorization: $TOKEN" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "startTime": "2026-02-01 00:00:00",
+ "endTime": "2026-02-28 23:59:59",
+ "storeIds": []
+ }')
+HTTP1=$(echo "$RESP1" | tail -n1)
+BODY1=$(echo "$RESP1" | sed '$d')
+CODE1=$(echo "$BODY1" | python3 -c "import sys, json; d=json.load(sys.stdin); print(d.get('code', 0))" 2>/dev/null || echo "0")
+echo "HTTP: $HTTP1, code: $CODE1"
+echo "$BODY1" | python3 -m json.tool 2>/dev/null || echo "$BODY1"
+if [ "$HTTP1" = "200" ] && [ "$CODE1" = "200" ]; then
+ echo "✅ 测试1 通过:2月接口返回成功"
+ # 校验返回结构
+ 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")
+ if [ "$HAS_DATA" = "True" ]; then
+ echo " 返回含 data 字段"
+ fi
+else
+ echo "❌ 测试1 失败"
+ exit 1
+fi
+
+# 测试2:1月(正常月份)
+echo ""
+echo "=== 测试2: 1月 (2026-01-01 ~ 2026-01-31) ==="
+RESP2=$(curl -s -w "\n%{http_code}" -X POST "$API_URL" \
+ -H "Authorization: $TOKEN" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "startTime": "2026-01-01 00:00:00",
+ "endTime": "2026-01-31 23:59:59",
+ "storeIds": []
+ }')
+HTTP2=$(echo "$RESP2" | tail -n1)
+BODY2=$(echo "$RESP2" | sed '$d')
+CODE2=$(echo "$BODY2" | python3 -c "import sys, json; d=json.load(sys.stdin); print(d.get('code', 0))" 2>/dev/null || echo "0")
+echo "HTTP: $HTTP2, code: $CODE2"
+if [ "$HTTP2" = "200" ] && [ "$CODE2" = "200" ]; then
+ echo "✅ 测试2 通过:1月接口返回成功"
+else
+ echo "❌ 测试2 失败"
+ exit 1
+fi
+
+echo ""
+echo "=========================================="
+echo "健康师耗卡业绩排行榜接口测试完成"
+echo "=========================================="
diff --git a/scripts/sh/test_lq_inventory_update.sh b/scripts/sh/test_lq_inventory_update.sh
new file mode 100755
index 0000000..fd55c41
--- /dev/null
+++ b/scripts/sh/test_lq_inventory_update.sh
@@ -0,0 +1,219 @@
+#!/bin/bash
+# 库存更新接口(PUT /api/Extend/LqInventory/Update)测试脚本
+# 覆盖:采购入库(stockInType=2) 与 普通入库(stockInType=1) 的价格变更逻辑
+# 测试前请确保后端服务已启动(如 http://localhost:2011)
+
+set -e
+BASE_URL="${BASE_URL:-http://localhost:2011}"
+API_UPDATE="${BASE_URL}/api/Extend/LqInventory/Update"
+API_GETLIST="${BASE_URL}/api/Extend/LqInventory/GetList"
+
+echo "=========================================="
+echo "库存更新接口测试 (LqInventory/Update)"
+echo "BASE_URL=$BASE_URL"
+echo "=========================================="
+
+# 获取 Token
+echo ""
+echo "正在获取 Token..."
+LOGIN_RESPONSE=$(curl -s -X POST "${BASE_URL}/api/oauth/Login" \
+ -H "Content-Type: application/x-www-form-urlencoded" \
+ -d "account=admin&password=e10adc3949ba59abbe56e057f20f883e")
+TOKEN=$(echo "$LOGIN_RESPONSE" | python3 -c "import sys, json; print(json.load(sys.stdin).get('data',{}).get('token',''))" 2>/dev/null)
+if [ -z "$TOKEN" ]; then
+ echo "❌ 获取 Token 失败"
+ echo "响应: $LOGIN_RESPONSE"
+ exit 1
+fi
+echo "✅ Token 获取成功"
+
+# 获取一条有效库存记录(用于更新测试)
+echo ""
+echo "正在获取一条库存记录(用于更新)..."
+LIST_RESPONSE=$(curl -s -X GET "${API_GETLIST}?currentPage=1&pageSize=1" -H "Authorization: $TOKEN")
+CODE=$(echo "$LIST_RESPONSE" | python3 -c "import sys, json; d=json.load(sys.stdin); print(d.get('code', 0))" 2>/dev/null)
+if [ "$CODE" != "200" ]; then
+ echo "❌ GetList 失败 (code=$CODE)"
+ echo "$LIST_RESPONSE" | python3 -m json.tool 2>/dev/null || echo "$LIST_RESPONSE"
+ exit 1
+fi
+INVENTORY_ID=$(echo "$LIST_RESPONSE" | python3 -c "
+import sys, json
+d = json.load(sys.stdin)
+lst = d.get('data',{}).get('list') or d.get('data',{}).get('data',[]) or []
+if not lst:
+ sys.exit(1)
+print(lst[0].get('id',''))
+" 2>/dev/null)
+PRODUCT_ID=$(echo "$LIST_RESPONSE" | python3 -c "
+import sys, json
+d = json.load(sys.stdin)
+lst = d.get('data',{}).get('list') or d.get('data',{}).get('data',[]) or []
+if not lst:
+ sys.exit(1)
+print(lst[0].get('productId',''))
+" 2>/dev/null)
+
+if [ -z "$INVENTORY_ID" ] || [ -z "$PRODUCT_ID" ]; then
+ echo "⚠️ 未获取到库存记录,将使用占位 ID 仅验证接口可达性与参数校验"
+ INVENTORY_ID="${INVENTORY_ID:-test-invalid-id}"
+ PRODUCT_ID="${PRODUCT_ID:-test-invalid-product}"
+fi
+
+# 测试1:采购入库更新(stockInType=2,带单价与金额)
+echo ""
+echo "=== 测试1: 采购入库更新 (stockInType=2, purchaseUnitPrice=50, finalAmount=5000) ==="
+BODY1=$(cat </dev/null || echo "0")
+echo "HTTP: $HTTP1, 业务 code: $CODE1"
+echo "$BODY1_RESP" | python3 -m json.tool 2>/dev/null || echo "$BODY1_RESP"
+if [ "$HTTP1" = "200" ] && [ "$CODE1" = "200" ]; then
+ echo "✅ 测试1 通过:采购入库更新成功"
+else
+ if [ "$INVENTORY_ID" = "test-invalid-id" ]; then
+ echo "⚠️ 预期失败(无有效库存ID),接口与参数格式正常"
+ else
+ echo "❌ 测试1 失败"
+ exit 1
+ fi
+fi
+
+# 测试2:普通入库更新(stockInType=1,带单价与金额)
+echo ""
+echo "=== 测试2: 普通入库更新 (stockInType=1, purchaseUnitPrice=30, finalAmount=3000) ==="
+BODY2=$(cat </dev/null || echo "0")
+echo "HTTP: $HTTP2, 业务 code: $CODE2"
+echo "$BODY2_RESP" | python3 -m json.tool 2>/dev/null || echo "$BODY2_RESP"
+if [ "$HTTP2" = "200" ] && [ "$CODE2" = "200" ]; then
+ echo "✅ 测试2 通过:普通入库价格变更成功"
+else
+ if [ "$INVENTORY_ID" = "test-invalid-id" ]; then
+ echo "⚠️ 预期失败(无有效库存ID),接口与参数格式正常"
+ else
+ echo "❌ 测试2 失败"
+ exit 1
+ fi
+fi
+
+# 测试3:参数校验(数量<=0 应报错)
+echo ""
+echo "=== 测试3: 参数校验(quantity=0 应返回错误) ==="
+BODY3=$(cat </dev/null || echo "200")
+echo "业务 code: $CODE3 (预期非 200)"
+if [ "$CODE3" != "200" ]; then
+ echo "✅ 测试3 通过:数量校验生效"
+else
+ echo "⚠️ 测试3:接口未对 quantity<=0 返回错误(请确认业务是否允许)"
+fi
+
+# 测试4:验证更新后加权平均单价计算正确(与 RecalculateProductAveragePriceAsync 一致)
+if [ "$INVENTORY_ID" != "test-invalid-id" ] && [ -n "$PRODUCT_ID" ]; then
+ echo ""
+ echo "=== 测试4: 验证加权平均单价计算 ==="
+ API_GETINFO="${BASE_URL}/api/Extend/LqProduct/GetInfo"
+ INFO_RESP=$(curl -s -X GET "${API_GETINFO}?id=${PRODUCT_ID}" -H "Authorization: $TOKEN")
+ LIST_FULL=$(curl -s -X GET "${API_GETLIST}?productId=${PRODUCT_ID}¤tPage=1&pageSize=100" -H "Authorization: $TOKEN")
+ VERIFY=$(echo "$INFO_RESP
+$LIST_FULL" | python3 -c "
+import sys, json
+lines = sys.stdin.read().strip().split('\n')
+if len(lines) < 2:
+ print('FAIL: no data')
+ sys.exit(1)
+info = json.loads(lines[0])
+lst_data = json.loads(lines[1])
+if info.get('code') != 200:
+ print('FAIL: GetInfo code', info.get('code'))
+ sys.exit(1)
+lst = lst_data.get('data',{}).get('list') or lst_data.get('data',{}).get('data',[]) or []
+# 产品基础价(无 FinalAmount/PurchaseUnitPrice 时用);产品当前平均单价(接口返回)
+product_price = float(info.get('data',{}).get('price') or 0)
+product_avg = float(info.get('data',{}).get('averagePrice') or 0)
+# 与 RecalculateProductAveragePriceAsync 一致:优先 FinalAmount/Quantity,其次 PurchaseUnitPrice,最后 Product.Price
+total_amount = 0
+total_qty = 0
+for row in lst:
+ qty = int(row.get('quantity') or 0)
+ if qty <= 0:
+ continue
+ fa = row.get('finalAmount')
+ pu = row.get('purchaseUnitPrice')
+ if fa is not None and float(fa or 0) > 0:
+ unit = float(fa) / qty
+ elif pu is not None and float(pu or 0) > 0:
+ unit = float(pu)
+ else:
+ unit = product_price
+ total_amount += unit * qty
+ total_qty += qty
+expected_avg = (total_amount / total_qty) if total_qty > 0 else product_price
+diff = abs(expected_avg - product_avg) if product_avg else abs(expected_avg)
+# 允许四舍五入误差
+ok = diff < 0.02
+print('OK' if ok else 'FAIL')
+print('expected_avg=%.4f product_avg=%.4f total_amount=%.2f total_qty=%d' % (expected_avg, product_avg, total_amount, total_qty))
+" 2>/dev/null)
+ VERIFY_OK=$(echo "$VERIFY" | head -1)
+ VERIFY_DETAIL=$(echo "$VERIFY" | tail -1)
+ if [ "$VERIFY_OK" = "OK" ]; then
+ echo "✅ 测试4 通过:加权平均单价与接口返回一致 ($VERIFY_DETAIL)"
+ else
+ echo "❌ 测试4 失败:加权平均与产品平均单价不一致"
+ echo " $VERIFY_DETAIL"
+ exit 1
+ fi
+else
+ echo ""
+ echo "=== 测试4: 跳过(无有效库存/产品ID) ==="
+fi
+
+echo ""
+echo "=========================================="
+echo "库存更新接口测试完成"
+echo "=========================================="
diff --git a/sql/修正品项直播-生命之波-预览影响行数.sql b/sql/修正品项直播-生命之波-预览影响行数.sql
new file mode 100644
index 0000000..9cec43d
--- /dev/null
+++ b/sql/修正品项直播-生命之波-预览影响行数.sql
@@ -0,0 +1,36 @@
+-- ============================================
+-- 预览:品项「直播-生命之波」在各表中的影响行数(仅查询,不更新)
+-- ============================================
+-- 执行「修正品项直播-生命之波开单耗卡退卡分类-执行版.sql」前,可先执行本脚本确认影响范围。
+-- 品项以 xmmc = '直播-生命之波' 匹配;若有多条请改用 F_Id。
+-- ============================================
+
+-- 品项当前值(请确认 qt2、F_BeautyType 已是目标值)
+SELECT xmzl.F_Id, xmzl.xmmc, xmzl.qt2 AS 品项分类, xmzl.F_BeautyType AS 科美类型
+FROM lq_xmzl xmzl
+WHERE xmzl.xmmc = '直播-生命之波';
+
+-- 各表将更新的行数
+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 = '直播-生命之波'
+UNION ALL
+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 = '直播-生命之波'
+UNION ALL
+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 = '直播-生命之波'
+UNION ALL
+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 = '直播-生命之波'
+UNION ALL
+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 = '直播-生命之波'
+UNION ALL
+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 = '直播-生命之波'
+UNION ALL
+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 = '直播-生命之波'
+UNION ALL
+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 = '直播-生命之波'
+UNION ALL
+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 = '直播-生命之波'
+UNION ALL
+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 = '直播-生命之波'
+UNION ALL
+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 = '直播-生命之波'
+UNION ALL
+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 = '直播-生命之波';
diff --git a/sql/修正品项直播-生命之波开单耗卡退卡分类-执行版.sql b/sql/修正品项直播-生命之波开单耗卡退卡分类-执行版.sql
new file mode 100644
index 0000000..6f2045c
--- /dev/null
+++ b/sql/修正品项直播-生命之波开单耗卡退卡分类-执行版.sql
@@ -0,0 +1,92 @@
+-- ============================================
+-- 修正品项「直播-生命之波」历史数据的 F_BeautyType(仅置为 NULL)
+-- ============================================
+-- 背景:该品项之前被错误设置为「溯源系统」,实际不属于溯源系统和 cell。
+-- F_ItemCategory 无需修改(已是正确值),本脚本仅将开单、耗卡、退卡等相关表中的
+-- F_BeautyType 置为 NULL。
+--
+-- 涉及表:
+-- 1. lq_kd_pxmx 开单品项明细
+-- 2. lq_xh_pxmx 耗卡品项明细
+-- 3. lq_hytk_mx 退卡品项明细
+-- 4. lq_kd_jksyj 开单健康师业绩
+-- 5. lq_xh_jksyj 耗卡健康师业绩
+-- 6. lq_hytk_jksyj 退卡健康师业绩
+-- 7. lq_kd_kjbsyj 开单科技部老师业绩
+-- 8. lq_xh_kjbsyj 耗卡科技部老师业绩
+-- 9. lq_hytk_kjbsyj 退卡科技部老师业绩
+--
+-- 建议先执行「预览版」查询确认影响行数,再执行本脚本。
+-- ============================================
+
+-- 1. 开单品项明细表
+UPDATE lq_kd_pxmx pxmx
+INNER JOIN lq_xmzl xmzl ON pxmx.px = xmzl.F_Id
+SET pxmx.F_BeautyType = NULL
+WHERE xmzl.xmmc = '直播-生命之波';
+
+-- 2. 耗卡品项明细表
+UPDATE lq_xh_pxmx pxmx
+INNER JOIN lq_xmzl xmzl ON pxmx.px = xmzl.F_Id
+SET pxmx.F_BeautyType = NULL
+WHERE xmzl.xmmc = '直播-生命之波';
+
+-- 3. 退卡品项明细表
+UPDATE lq_hytk_mx mx
+INNER JOIN lq_xmzl xmzl ON mx.px = xmzl.F_Id
+SET mx.F_BeautyType = NULL
+WHERE xmzl.xmmc = '直播-生命之波';
+
+-- 4. 开单健康师业绩表(F_ItemId 关联 + F_kdpxid 经开单品项明细关联,双路径覆盖)
+UPDATE lq_kd_jksyj j
+INNER JOIN lq_xmzl xmzl ON j.F_ItemId = xmzl.F_Id
+SET j.F_BeautyType = NULL
+WHERE xmzl.xmmc = '直播-生命之波';
+
+UPDATE 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
+SET j.F_BeautyType = NULL
+WHERE xmzl.xmmc = '直播-生命之波';
+
+-- 5. 耗卡健康师业绩表
+UPDATE lq_xh_jksyj j
+INNER JOIN lq_xmzl xmzl ON j.F_ItemId = xmzl.F_Id
+SET j.F_BeautyType = NULL
+WHERE xmzl.xmmc = '直播-生命之波';
+
+-- 6. 退卡健康师业绩表
+UPDATE lq_hytk_jksyj j
+INNER JOIN lq_xmzl xmzl ON j.F_ItemId = xmzl.F_Id
+SET j.F_BeautyType = NULL
+WHERE xmzl.xmmc = '直播-生命之波';
+
+-- 7. 开单科技部老师业绩表(F_ItemId 关联 + F_kdpxid 经开单品项明细关联,双路径覆盖)
+UPDATE lq_kd_kjbsyj j
+INNER JOIN lq_xmzl xmzl ON j.F_ItemId = xmzl.F_Id
+SET j.F_BeautyType = NULL
+WHERE xmzl.xmmc = '直播-生命之波';
+
+UPDATE 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
+SET j.F_BeautyType = NULL
+WHERE xmzl.xmmc = '直播-生命之波';
+
+-- 8. 耗卡科技部老师业绩表
+UPDATE lq_xh_kjbsyj j
+INNER JOIN lq_xmzl xmzl ON j.F_ItemId = xmzl.F_Id
+SET j.F_BeautyType = NULL
+WHERE xmzl.xmmc = '直播-生命之波';
+
+-- 9. 退卡科技部老师业绩表
+UPDATE lq_hytk_kjbsyj j
+INNER JOIN lq_xmzl xmzl ON j.F_ItemId = xmzl.F_Id
+SET j.F_BeautyType = NULL
+WHERE xmzl.xmmc = '直播-生命之波';
+
+-- ============================================
+-- 说明:
+-- - 若 lq_xmzl 中该品项名称为「直播-生命之波」存在多条(不同 F_Id),
+-- 请先确认唯一品项或改用 F_Id 限定,例如:WHERE xmzl.F_Id = '具体品项ID'。
+-- ============================================
diff --git a/sql/科美业绩与科技部老师开单业绩差异核对.sql b/sql/科美业绩与科技部老师开单业绩差异核对.sql
new file mode 100644
index 0000000..f08dfab
--- /dev/null
+++ b/sql/科美业绩与科技部老师开单业绩差异核对.sql
@@ -0,0 +1,106 @@
+-- ============================================================
+-- 科美业绩(1163803) vs 科技部老师开单业绩(1165702.63) 差异核对
+-- 理论上两者应相等,差异约 1899.63。本脚本用于排查原因,不修改任何数据。
+-- ============================================================
+
+-- 1) 口径说明
+-- 科美业绩:一般指「开单品项明细」中 品项分类=科美 的 F_ActualPrice 之和(或来自台账选科美时的汇总)
+-- 科技部老师开单业绩:lq_kd_kjbsyj 表的 kjblsyj 之和
+
+-- 2) 同一时间范围下两数分别多少(请按需修改时间)
+SET @start = '2026-01-01 00:00:00';
+SET @end = '2026-01-31 23:59:59';
+
+-- 2.1 科美业绩:按开单品项明细,品项分类=科美,有效开单+有效明细
+SELECT
+ '科美业绩-按品项明细(pxmx.F_ItemCategory=科美)' AS 口径,
+ COALESCE(SUM(px.F_ActualPrice), 0) AS 金额
+FROM lq_kd_pxmx px
+INNER JOIN lq_kd_kdjlb kd ON px.glkdbh = kd.F_Id AND kd.F_IsEffective = 1
+WHERE px.F_IsEffective = 1
+ 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 = '科美'))
+ AND kd.kdrq >= @start
+ AND kd.kdrq <= @end;
+
+-- 2.2 科技部老师开单业绩:lq_kd_kjbsyj 全表汇总(当前系统统计方式,未过滤科美)
+SELECT
+ '科技部老师开单业绩-全表kjblsyj' AS 口径,
+ COALESCE(SUM(CAST(NULLIF(TRIM(k.kjblsyj), '') AS DECIMAL(18,2))), 0) AS 金额
+FROM lq_kd_kjbsyj k
+INNER JOIN lq_kd_kdjlb kd ON k.glkdbh = kd.F_Id AND kd.F_IsEffective = 1
+WHERE k.F_IsEffective = 1
+ AND k.yjsj >= @start
+ AND k.yjsj <= @end
+ AND k.kjblsyj IS NOT NULL
+ AND TRIM(k.kjblsyj) != ''
+ AND TRIM(k.kjblsyj) != '0';
+
+-- 2.3 科技部老师开单业绩:仅 F_ItemCategory='科美'(若只统计科美,应与科美业绩一致)
+SELECT
+ '科技部老师开单业绩-仅科美F_ItemCategory' AS 口径,
+ COALESCE(SUM(CAST(NULLIF(TRIM(k.kjblsyj), '') AS DECIMAL(18,2))), 0) AS 金额
+FROM lq_kd_kjbsyj k
+INNER JOIN lq_kd_kdjlb kd ON k.glkdbh = kd.F_Id AND kd.F_IsEffective = 1
+WHERE k.F_IsEffective = 1
+ AND k.F_ItemCategory = '科美'
+ AND k.yjsj >= @start
+ AND k.yjsj <= @end
+ AND k.kjblsyj IS NOT NULL
+ AND TRIM(k.kjblsyj) != ''
+ AND TRIM(k.kjblsyj) != '0';
+
+-- 3) 差异来源:科技部表里「非科美」或「品项分类为空」的金额(多出来的部分)
+SELECT
+ '科技部-非科美或空分类金额' AS 口径,
+ COALESCE(SUM(CAST(NULLIF(TRIM(k.kjblsyj), '') AS DECIMAL(18,2))), 0) AS 金额
+FROM lq_kd_kjbsyj k
+INNER JOIN lq_kd_kdjlb kd ON k.glkdbh = kd.F_Id AND kd.F_IsEffective = 1
+WHERE k.F_IsEffective = 1
+ AND k.yjsj >= @start
+ AND k.yjsj <= @end
+ AND (k.F_ItemCategory IS NULL OR k.F_ItemCategory = '' OR k.F_ItemCategory != '科美')
+ AND k.kjblsyj IS NOT NULL
+ AND TRIM(k.kjblsyj) != ''
+ AND TRIM(k.kjblsyj) != '0';
+
+-- 4) 科美品项有明细但科技部无业绩 / 科技部有业绩但品项非科美(明细级抽查)
+-- 4.1 科美品项明细对应的科技部业绩合计(按开单明细ID关联)
+SELECT
+ '按开单明细关联-科美品项对应kjbsyj之和' AS 口径,
+ COALESCE(SUM(CAST(NULLIF(TRIM(k.kjblsyj), '') AS DECIMAL(18,2))), 0) AS 金额
+FROM lq_kd_pxmx px
+INNER JOIN lq_kd_kdjlb kd ON px.glkdbh = kd.F_Id AND kd.F_IsEffective = 1
+LEFT JOIN lq_kd_kjbsyj k ON k.F_kdpxid = px.F_Id AND k.F_IsEffective = 1
+WHERE px.F_IsEffective = 1
+ AND px.F_ItemCategory = '科美'
+ AND kd.kdrq >= @start
+ AND kd.kdrq <= @end
+ AND k.kjblsyj IS NOT NULL
+ AND TRIM(k.kjblsyj) != ''
+ AND TRIM(k.kjblsyj) != '0';
+
+-- 4.2 科美品项明细的 ActualPrice 之和(与 4.1 同范围,用于对比)
+SELECT
+ '科美品项ActualPrice之和(同范围)' AS 口径,
+ COALESCE(SUM(px.F_ActualPrice), 0) AS 金额
+FROM lq_kd_pxmx px
+INNER JOIN lq_kd_kdjlb kd ON px.glkdbh = kd.F_Id AND kd.F_IsEffective = 1
+WHERE px.F_IsEffective = 1
+ 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 = '科美'))
+ AND kd.kdrq >= @start
+ AND kd.kdrq <= @end;
+
+-- 5) 查看 lq_kd_kjbsyj 中 F_ItemCategory 的分布(是否有空或非科美)
+SELECT
+ COALESCE(k.F_ItemCategory, '(空)') AS 品项分类,
+ COUNT(*) AS 条数,
+ COALESCE(SUM(CAST(NULLIF(TRIM(k.kjblsyj), '') AS DECIMAL(18,2))), 0) AS 金额合计
+FROM lq_kd_kjbsyj k
+INNER JOIN lq_kd_kdjlb kd ON k.glkdbh = kd.F_Id AND kd.F_IsEffective = 1
+WHERE k.F_IsEffective = 1
+ AND k.yjsj >= @start
+ AND k.yjsj <= @end
+ AND k.kjblsyj IS NOT NULL
+ AND TRIM(k.kjblsyj) != ''
+ AND TRIM(k.kjblsyj) != '0'
+GROUP BY COALESCE(k.F_ItemCategory, '(空)');
diff --git a/sql/科美业绩差异-具体开单明细.sql b/sql/科美业绩差异-具体开单明细.sql
new file mode 100644
index 0000000..88e7795
--- /dev/null
+++ b/sql/科美业绩差异-具体开单明细.sql
@@ -0,0 +1,24 @@
+-- 查出导致「科技部老师开单业绩」多于「科美业绩」的那些开单数据(科技部表里 非科美 或 品项分类为空 的明细)
+-- 时间范围:与台账一致 2026-01-01 ~ 2026-01-22(若需整月可改为 2026-01-31 23:59:59)
+SET @start = '2026-01-01 00:00:00';
+SET @end = '2026-01-22 23:59:59';
+
+-- 差异来源:lq_kd_kjbsyj 中 F_ItemCategory 非科美或为空的记录(按开单号列出)
+SELECT
+ kd.F_Id AS 开单ID,
+ kd.kdbh AS 开单编号,
+ kd.kdrq AS 开单日期,
+ k.F_Id AS 科技部业绩明细ID,
+ COALESCE(k.F_ItemCategory, '(空)') AS 品项分类_科技部,
+ CAST(NULLIF(TRIM(k.kjblsyj), '') AS DECIMAL(18,2)) AS 科技部业绩金额,
+ k.yjsj AS 业绩时间
+FROM lq_kd_kjbsyj k
+INNER JOIN lq_kd_kdjlb kd ON k.glkdbh = kd.F_Id AND kd.F_IsEffective = 1
+WHERE k.F_IsEffective = 1
+ AND k.yjsj >= @start
+ AND k.yjsj <= @end
+ AND (k.F_ItemCategory IS NULL OR k.F_ItemCategory = '' OR k.F_ItemCategory != '科美')
+ AND k.kjblsyj IS NOT NULL
+ AND TRIM(k.kjblsyj) != ''
+ AND TRIM(k.kjblsyj) != '0'
+ORDER BY kd.kdrq, kd.F_Id, k.yjsj;
diff --git a/sql/科美业绩差异-剔除储扣后仍存在差异的开单.sql b/sql/科美业绩差异-剔除储扣后仍存在差异的开单.sql
new file mode 100644
index 0000000..8ba2710
--- /dev/null
+++ b/sql/科美业绩差异-剔除储扣后仍存在差异的开单.sql
@@ -0,0 +1,73 @@
+-- ============================================================
+-- 科美业绩差异:剔除「储扣一致」后的真实差异开单
+-- 时间范围:2026-01-01 ~ 2026-01-22(与台账一致)
+-- ============================================================
+--
+-- 【剔除规则】
+-- 若 品项明细金额 - 科技部业绩 ≈ 该开单的储扣金额(全部储扣,不限于科美,误差<=0.02),
+-- 视为储扣导致,从结果中剔除;只列出「剔除后仍存在差异」的开单。
+--
+-- 【本脚本包含两个查询】
+-- ① 有差异的开单列表:只输出 开单ID、开单日期,用于把有差异的开单列出来;
+-- ② 有差异的开单明细:输出 开单ID、开单日期、品项明细金额、科技部业绩金额、储扣金额_全部、差异,用于分析。
+--
+-- 【查询②结果列】开单ID | 开单日期 | 品项明细金额 | 科技部业绩金额 | 储扣金额_全部 | 差异
+--
+-- ============================================================
+
+-- 一、有差异的开单列表(仅列开单ID与日期,便于核对)
+SELECT
+ kd.F_Id AS 开单ID,
+ kd.kdrq AS 开单日期
+FROM lq_kd_pxmx px
+INNER JOIN lq_kd_kdjlb kd ON px.glkdbh = kd.F_Id AND kd.F_IsEffective = 1
+WHERE px.F_IsEffective = 1
+ AND px.F_ItemCategory = '科美'
+ AND kd.kdrq >= '2026-01-01 00:00:00'
+ AND kd.kdrq <= '2026-01-22 23:59:59'
+GROUP BY kd.F_Id, kd.kdrq
+HAVING ABS(SUM(px.F_ActualPrice) - (SELECT COALESCE(SUM(CAST(NULLIF(TRIM(k2.kjblsyj), '') AS DECIMAL(18,2))), 0)
+ FROM lq_kd_kjbsyj k2
+ WHERE k2.glkdbh = kd.F_Id AND k2.F_IsEffective = 1 AND k2.F_ItemCategory = '科美'
+ AND k2.yjsj >= '2026-01-01 00:00:00' AND k2.yjsj <= '2026-01-22 23:59:59')) > 0.01
+ AND ABS((SUM(px.F_ActualPrice) - (SELECT COALESCE(SUM(CAST(NULLIF(TRIM(k2.kjblsyj), '') AS DECIMAL(18,2))), 0)
+ FROM lq_kd_kjbsyj k2
+ WHERE k2.glkdbh = kd.F_Id AND k2.F_IsEffective = 1 AND k2.F_ItemCategory = '科美'
+ AND k2.yjsj >= '2026-01-01 00:00:00' AND k2.yjsj <= '2026-01-22 23:59:59'))
+ - COALESCE((SELECT SUM(d.F_Amount) FROM lq_kd_deductinfo d
+ WHERE d.F_BillingId = kd.F_Id AND d.F_IsEffective = 1), 0)) > 0.02
+ORDER BY kd.kdrq;
+
+-- 二、有差异的开单明细(含金额与储扣,便于分析)
+SELECT
+ kd.F_Id AS 开单ID,
+ kd.kdrq AS 开单日期,
+ ROUND(SUM(px.F_ActualPrice), 2) AS 品项明细金额,
+ ROUND((SELECT COALESCE(SUM(CAST(NULLIF(TRIM(k2.kjblsyj), '') AS DECIMAL(18,2))), 0)
+ FROM lq_kd_kjbsyj k2
+ WHERE k2.glkdbh = kd.F_Id AND k2.F_IsEffective = 1 AND k2.F_ItemCategory = '科美'
+ AND k2.yjsj >= '2026-01-01 00:00:00' AND k2.yjsj <= '2026-01-22 23:59:59'), 2) AS 科技部业绩金额,
+ ROUND(COALESCE((SELECT SUM(d.F_Amount) FROM lq_kd_deductinfo d
+ WHERE d.F_BillingId = kd.F_Id AND d.F_IsEffective = 1), 0), 2) AS 储扣金额_全部,
+ ROUND(SUM(px.F_ActualPrice) - (SELECT COALESCE(SUM(CAST(NULLIF(TRIM(k2.kjblsyj), '') AS DECIMAL(18,2))), 0)
+ FROM lq_kd_kjbsyj k2
+ WHERE k2.glkdbh = kd.F_Id AND k2.F_IsEffective = 1 AND k2.F_ItemCategory = '科美'
+ AND k2.yjsj >= '2026-01-01 00:00:00' AND k2.yjsj <= '2026-01-22 23:59:59'), 2) AS 差异
+FROM lq_kd_pxmx px
+INNER JOIN lq_kd_kdjlb kd ON px.glkdbh = kd.F_Id AND kd.F_IsEffective = 1
+WHERE px.F_IsEffective = 1
+ AND px.F_ItemCategory = '科美'
+ AND kd.kdrq >= '2026-01-01 00:00:00'
+ AND kd.kdrq <= '2026-01-22 23:59:59'
+GROUP BY kd.F_Id, kd.kdrq
+HAVING ABS(SUM(px.F_ActualPrice) - (SELECT COALESCE(SUM(CAST(NULLIF(TRIM(k2.kjblsyj), '') AS DECIMAL(18,2))), 0)
+ FROM lq_kd_kjbsyj k2
+ WHERE k2.glkdbh = kd.F_Id AND k2.F_IsEffective = 1 AND k2.F_ItemCategory = '科美'
+ AND k2.yjsj >= '2026-01-01 00:00:00' AND k2.yjsj <= '2026-01-22 23:59:59')) > 0.01
+ AND ABS((SUM(px.F_ActualPrice) - (SELECT COALESCE(SUM(CAST(NULLIF(TRIM(k2.kjblsyj), '') AS DECIMAL(18,2))), 0)
+ FROM lq_kd_kjbsyj k2
+ WHERE k2.glkdbh = kd.F_Id AND k2.F_IsEffective = 1 AND k2.F_ItemCategory = '科美'
+ AND k2.yjsj >= '2026-01-01 00:00:00' AND k2.yjsj <= '2026-01-22 23:59:59'))
+ - COALESCE((SELECT SUM(d.F_Amount) FROM lq_kd_deductinfo d
+ WHERE d.F_BillingId = kd.F_Id AND d.F_IsEffective = 1), 0)) > 0.02
+ORDER BY kd.kdrq;