diff --git a/antis-ncc-admin/src/views/extend/techDepartmentDashboard/index.vue b/antis-ncc-admin/src/views/extend/techDepartmentDashboard/index.vue index 5b81aed..388af77 100644 --- a/antis-ncc-admin/src/views/extend/techDepartmentDashboard/index.vue +++ b/antis-ncc-admin/src/views/extend/techDepartmentDashboard/index.vue @@ -264,7 +264,8 @@ size="mini" border style="width: 100%" - :max-height="500"> + :max-height="440" + :row-key="(row, index) => `store-detail-${index}`"> @@ -304,7 +305,7 @@ size="mini" border style="width: 100%" - :max-height="500"> + :max-height="440"> @@ -349,7 +350,7 @@ size="mini" border style="width: 100%" - :max-height="500"> + :max-height="440"> @@ -383,7 +384,7 @@ size="mini" border style="width: 100%" - :max-height="500"> + :max-height="440"> @@ -1709,7 +1710,7 @@ export default { } } - // 明细列表样式 + // 明细列表样式(表格 max-height 预留分页区域,避免最后一行被遮挡) .detail-list-row { margin-top: 16px; margin-bottom: 16px; @@ -1729,6 +1730,7 @@ export default { .el-table { flex: 1; + min-height: 0; } } @@ -1745,6 +1747,7 @@ export default { } .detail-pagination { + flex-shrink: 0; margin-top: 16px; text-align: right; } diff --git a/antis-ncc-admin/src/views/welcome.vue b/antis-ncc-admin/src/views/welcome.vue index 3c66c7d..d22052d 100644 --- a/antis-ncc-admin/src/views/welcome.vue +++ b/antis-ncc-admin/src/views/welcome.vue @@ -1,15 +1,32 @@ @@ -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;