Commit 7200c7ad4a219ade20061f6455ade2f394727e89
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.
Showing
13 changed files
with
967 additions
and
124 deletions
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}¤tPage=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; | ... | ... |