yanhui 3e64702f12 fix: MKH-BOM 新增日销订单表兜底,修复上游快照漏算导致「库存 0 却不报缺货」
背景
- 业务反馈 MKHDNZ0409260601548 近 15 天实发 2 件、库存 0,页面既不报缺货也不进任何预警。
- 根因是双重盲区:
  ① 上游 rpt.pm_stockMonitor.fifteenAvgSale 漏算 —— 快照 09-18 下,窗口内有真实发货的
     178 个 MKH 成品编码里有 24 个被记成 0;其中 16 个订单全部来自样品专用店铺(上游同样
     不计样品单,属合理口径),剩 8 个是真实店铺(天猫/京东/拼多多)。该列是**保留两位
     小数的日均**(最小非零 0.07 = 1 件 ÷ 15 天),无法用四舍五入解释。
  ② 快照日销 = 0 且待发货订单 = 0 时,判定直接落到「无销量」分支、hasWarning=false,
     彻底排除在预警之外 —— 库存为 0 也救不回来。

改动
- 日销兜底(服务文件头新增 ⑦-a):**快照日销 ≤ 0 且近 15 天有真实发货**时,日销改用
  订单表实算值 = 已发货件数 ÷ 15;快照 > 0 一律以上游为准,不覆盖,避免两套口径打架。
  · 排除样品/测试专用店铺(shop_name 含「样品」「测试」),与上游口径对齐
  · 窗口 = 含今天在内的 15 个自然日,与分母同口径
  · 结果透出 dailySaleSource('snapshot'|'orders') / snapshotDailySale / recentShippedQty
- 取数:loadPendingOrderIndex 重构为 loadOrderIndex,一条查询同时覆盖「待发货
  (send_date IS NULL)」与「近 15 天已发货(send_date 在窗口内)」。实测合并后总耗时
  约 1.25s ≈ 单查待发货,而拆成两条独立查询要在现有开销之上再加一次全表扫描
  (首轮冷执行计划下 qmcust 单表可劣化到 20s+)。
- PENDING_ORDER_TABLES 更名 ORDER_TABLES:两张表的口径没变,只是不再仅服务待发货。
- 前端:日销来自兜底时加虚线下划 + 悬停说明,业务拿该数字与源系统 fifteenAvgSale
  对照时不会被误判为数据错误。

验证
- 语法检查:151 文件通过。
- 后端测试:338 项 337 通过。唯一失败为 resilientSecretStore 的权限断言(期望 0o600、
  Windows 实际 0o666),平台固有,与本改动无关。
- 新增回归测试:兜底生效 / 快照非零不覆盖 / 无发货不兜底 / 与订单驱动的优先级 /
  订单源降级时退化成旧口径;订单索引测试补充「已发货同编码跨表累加」与「样品、测试店排除」断言。
- 真实数据端到端(本地接口 /api/mkh-bom/list,4,223 个成品):
  · MKHDNZ0409260601548:日销 0 → 0.1333(source=orders),不再隐身
  · 日销 > 0 的商品 177 → 198;有效预警(缺货 + 预警)18 → 28
  · 21 个等级变化**全部是「无销量 → 缺货/正常/已排产」**,没有任何既有预警被降级
  · 其中 10 个新缺货均为「库存 0 + 近 15 天真实发货 9~27 件」的真盲区
  · 样品店排除按整行生效:实测某编码窗口内 5 件(含样品店 1 件)→ 索引记为 4 件
- 前端 eslint 通过、vite build 通过。

风险与回退
- 只影响「快照日销 = 0」的商品,快照 > 0 的商品行为完全不变。
- 订单源不可用时兜底自动失效,退化成旧口径(不会误报)。
- 回退:本提交可整体 revert,无表结构变更、无数据迁移。
2026-09-19 13:24:15 +08:00

394 lines
7.1 KiB
CSS
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

.mkh-page {
padding: 16px 20px 24px;
}
.mkh-header {
margin-bottom: 14px;
}
.mkh-header__heading .ant-typography {
margin-bottom: 4px;
}
.mkh-note {
display: block;
max-width: 1180px;
margin-top: 6px;
font-size: 12px;
line-height: 1.7;
}
.mkh-filter-card {
margin-bottom: 12px;
}
.mkh-panel {
margin-top: 4px;
}
.mkh-sku {
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
font-size: 12px;
color: var(--sycm-text-primary);
}
.mkh-muted {
color: var(--sycm-text-tertiary);
}
.mkh-detail-loading {
padding: 12px 8px;
color: var(--sycm-text-secondary);
}
.mkh-detail {
padding: 4px 8px 8px;
}
.mkh-detail-tip {
padding: 4px 0 8px;
font-size: 12px;
}
.mkh-detail-code {
display: block;
margin-top: 2px;
font-family: 'SFMono-Regular', Consolas, Menlo, monospace;
font-size: 11px;
color: var(--sycm-text-tertiary);
}
.mkh-tag {
display: inline-block;
padding: 0 6px;
border-radius: 3px;
font-size: 12px;
line-height: 18px;
white-space: nowrap;
}
.mkh-tag--ok {
color: #0f6e56;
background: rgba(29, 158, 117, 0.14);
}
.mkh-tag--muted {
color: var(--sycm-text-tertiary);
background: rgba(136, 135, 128, 0.14);
}
.mkh-tag--src {
color: #0958d9;
background: rgba(22, 119, 255, 0.12);
}
/* ============ 补货预警 ============ */
.mkh-alert {
margin-bottom: 12px;
}
/* 缺货 */
.mkh-tag--danger {
color: #a8071a;
background: rgba(255, 77, 79, 0.14);
}
/* 预警 */
.mkh-tag--warn {
color: #ad4e00;
background: rgba(250, 173, 20, 0.2);
}
/* 可用天数低于阈值 */
.mkh-days--warn {
color: #cf1322;
font-weight: 600;
}
/* 建议补货量 */
.mkh-replenish {
color: #0958d9;
font-weight: 600;
}
/* 已排产:半成品包装仓已有未完成加工单,按业务口径不再预警 */
.mkh-tag--scheduled {
color: #0958d9;
background: rgba(22, 119, 255, 0.14);
}
/* 在途加工量(未完成加工单待入库数量) */
.mkh-inbound {
color: #0958d9;
font-weight: 600;
}
/* 待发货订单(聚水潭未发货订单件数,见文件头 ⑩) */
.mkh-order {
color: #ad4e00;
font-weight: 600;
}
/* 日销兜底(上游快照漏算时按订单表实算,见服务文件头 ⑦-a):
虚线表示该值不是上游快照,悬停有说明,避免与源系统数字对照时误判为数据错误 */
.mkh-sale-fallback {
border-bottom: 1px dashed currentColor;
cursor: help;
}
/* 配件不足(可组装数 = 0,补不出来,见文件头 ⑪):与「缺货」红标签并排,用紫灰区分 */
.mkh-tag--shortage {
color: #722ed1;
background: rgba(114, 46, 209, 0.14);
border-color: rgba(114, 46, 209, 0.35);
}
/* ============ A4 打印预览 ============ */
.mkh-print-mask {
position: fixed;
inset: 0;
z-index: 2000;
overflow: auto;
background: rgba(0, 0, 0, 0.55);
padding: 24px 0 40px;
}
.mkh-print-toolbar {
position: sticky;
top: 0;
z-index: 2;
display: flex;
justify-content: center;
padding: 12px;
margin-bottom: 16px;
}
.mkh-print-sheet {
width: 210mm;
min-height: 297mm;
margin: 0 auto;
padding: 14mm 12mm;
background: #fff;
color: #000;
box-shadow: 0 6px 24px rgba(0, 0, 0, 0.35);
box-sizing: border-box;
font-family: -apple-system, 'PingFang SC', 'Microsoft YaHei', Arial, sans-serif;
font-size: 12px;
line-height: 1.5;
}
.mkh-print-head {
text-align: center;
border-bottom: 2px solid #000;
padding-bottom: 10px;
margin-bottom: 12px;
}
.mkh-print-title {
font-size: 22px;
font-weight: 700;
letter-spacing: 2px;
}
.mkh-print-sub {
margin-top: 4px;
font-size: 12px;
color: #444;
}
.mkh-print-meta {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 4px 24px;
margin-bottom: 12px;
font-size: 13px;
}
.mkh-print-meta > div {
display: flex;
}
.mkh-print-meta span {
color: #555;
min-width: 78px;
}
.mkh-print-table {
width: 100%;
border-collapse: collapse;
font-size: 12px;
}
.mkh-print-table th,
.mkh-print-table td {
border: 1px solid #000;
padding: 6px 8px;
text-align: left;
word-break: break-all;
}
.mkh-print-table thead th {
background: #f2f2f2;
font-weight: 700;
}
.mkh-print-table .mkh-center {
text-align: center;
}
.mkh-print-table .mkh-muted {
color: #999;
}
.mkh-print-table .mkh-print-sku {
font-family: 'SFMono-Regular', Consolas, Menlo, monospace;
/* 2026-09-18 业务要求调大:原来是 11px,车间核对编码时太小 */
font-size: 14px;
font-weight: 700;
letter-spacing: 0.3px;
vertical-align: middle;
}
.mkh-print-code {
display: block;
margin-top: 2px;
font-family: 'SFMono-Regular', Consolas, Menlo, monospace;
/* 2026-09-18 业务要求调大:原来是 10px / #666,扫码与人工核对都偏小偏淡 */
font-size: 12px;
font-weight: 600;
color: #222;
}
.mkh-print-foot {
margin-top: 16px;
padding-top: 8px;
border-top: 1px dashed #999;
font-size: 11px;
color: #666;
}
/* 打印时只输出 A4 纸张,隐藏其余界面 */
@media print {
body * {
visibility: hidden !important;
}
.mkh-print-sheet,
.mkh-print-sheet * {
visibility: visible !important;
}
.mkh-print-toolbar {
display: none !important;
}
.mkh-print-mask {
position: absolute !important;
inset: 0 !important;
background: #fff !important;
overflow: visible !important;
padding: 0 !important;
}
.mkh-print-sheet {
width: 210mm;
min-height: auto;
margin: 0 !important;
padding: 0 !important;
box-shadow: none !important;
}
@page {
size: A4;
margin: 12mm;
}
}
/* ── 补货预警推送设置弹窗 ──────────────────────────── */
.mkh-alert-form {
display: flex;
flex-direction: column;
gap: 12px;
margin-top: 16px;
}
.mkh-alert-row {
display: flex;
align-items: center;
gap: 10px;
}
.mkh-alert-label {
flex: none;
width: 68px;
color: var(--sycm-text-secondary);
}
.mkh-alert-meta {
margin-top: 14px;
padding: 10px 12px;
border: 1px solid var(--sycm-border-color);
border-radius: 8px;
background: var(--sycm-bg-subtle);
font-size: 12px;
line-height: 2;
color: var(--sycm-text-secondary);
}
.mkh-alert-result {
margin-top: 14px;
}
.mkh-alert-result-body {
font-size: 12px;
line-height: 1.8;
}
.mkh-alert-preview {
margin-top: 8px;
padding: 10px 12px;
border: 1px solid var(--sycm-border-color);
border-radius: 8px;
background: var(--sycm-bg-container);
}
.mkh-alert-preview-title {
margin-bottom: 6px;
font-weight: 600;
color: var(--sycm-text-strong);
}
.mkh-alert-preview pre {
margin: 0 0 6px;
font-family: inherit;
font-size: 12px;
line-height: 1.8;
white-space: pre-wrap;
word-break: break-word;
color: var(--sycm-text-primary);
}
.mkh-alert-hint {
margin-top: 10px;
font-size: 12px;
line-height: 1.7;
color: var(--sycm-text-tertiary);
}
/* 推送通道(统一通知中心)折叠区 */
.mkh-alert-center {
margin-top: 6px;
}
.mkh-alert-center .ant-collapse-header {
padding: 8px 0 !important;
}
.mkh-alert-center .ant-collapse-content-box {
padding: 0 0 4px !important;
}
.mkh-alert-form--inner {
margin-top: 4px;
}