## 业务问题 10-28 18:00 轮页面核验出 546 单超时未发,业务核对「基本都发了,怎么会核验出未发」。 ## 根因(不是判定写错,是数据源停更) `jst_Orders` 当时**从 14:40 就停止同步了**(停了 4 小时),停更之后发出的单 `send_date` 一直是空,因此全被判成「未发货」: | 来源表 | 超时单数 | 该表状态 | |---|---|---| | `jst_Orders` | 540 | 最后更新 14:40(停 4 小时) | | `jst_qmcust_orderlist` | 6 | 17:30 正常 | 两个订单表**各自独立同步**,同一天完全可能一个正常一个停更。原来只拿候选行的 `MAX(updated_at)` 判快照滞后,会被「碰巧新」的行掩盖 —— 必须直接问源表。 ## 改动 后端 `warehouseShipTimelinessService.js`: - 新增 `loadSourceFreshness()`:分别直接查 `jst_Orders.MAX(updateTime)` 与 `jst_qmcust_orderlist.MAX(local_updated)`(限轮次窗口起 30 天,避免全表扫描), 两源并行;按 `CANGKU_WST_SOURCE_STALE_MINUTES`(默认 60 分钟)判停更 - 查询失败或取不到更新时间 → **不判停更**(宁可漏报,不谎报为停更) - 明细行新增 `src` / `srcLabel` / `sourceStale`;`summary` 新增 `sourceStaleUnshipped` - `freshness.stale` 改为「候选行滞后 **OR** 任一源停更」;`hint` 说清停更源、 停更时刻、以及各源各贡献多少单(不能只写「这张表」) - 导出数据带 `dataSources`,Excel 脱离页面也有上下文 - 响应新增 `dataSources` 数组(每源 label / lastUpdate / lagMinutes / stale) 前端 `WarehouseShipTimeliness/index.jsx`: - 红条 Alert:「N 单所在订单表已停止同步,这些单很可能已经发货」(error 级), 描述里逐源列出「最后同步 14:40:05(已 3.9 小时)」 - 超时统计卡下方小字:「其中 N 单数据未同步」(悬停有说明) - 明细「内部单号」旁「未同步」红标签 + tooltip,一眼看出哪几行不可信 - 导出新增「数据源 / 数据源是否停更」两列(`是(该单可能已发)`) ## 验证 - 后端 `npm run check`:**494/496**,2 个失败是既有环境问题(负缓存 TTL 计时、 Windows 文件权限 0o600),改动前同样失败 - `warehouseShipTimeliness.test.js` **23/23** 通过(含新增「停更表超时单必须打标 并计入 sourceStaleUnshipped」用例) - 本地真渲染(Playwright + 手签 token,localhost:5173): - 真数据:13:00 前 `notStarted` 正常展示「—」,无 pageerror - mock 停更数据:红条 / 逐源行 / 统计小字 / 3 行「未同步」标签全部渲染,无 pageerror - 真实接口 10-28 13:00 轮:`dataSources` 正确识别 `jst_Orders` 停更(滞后 96 分钟) 而 qmcust 正常;`sourceStaleUnshipped=1146` 与明细逐行打标同源同口径 ## 风险与回退 - 只新增字段与展示,**不改动分轮、起算点、候选池口径** —— 超时单量本身不变 - 阈值可用 `CANGKU_WST_SOURCE_STALE_MINUTES` 调;不配默认 60 分钟 - 低峰期(凌晨)可能出现「1 小时无新单」被判停更,但页面只在 13:00 / 18:00 核验时刻展示超时数据,这两个时点 1 小时不同步即属异常 - 回退:revert 本提交即可,无数据迁移、无新表、无新依赖
362 lines
6.7 KiB
CSS
362 lines
6.7 KiB
CSS
.wst-page {
|
||
padding: 16px 20px 24px;
|
||
}
|
||
|
||
.wst-header {
|
||
margin-bottom: 14px;
|
||
}
|
||
|
||
.wst-header .ant-typography {
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.wst-note {
|
||
display: block;
|
||
max-width: 1180px;
|
||
margin-top: 8px;
|
||
padding: 10px 14px;
|
||
/* 页面顶部是深色渐变,直接放浅色小字会看不清,给口径说明一块浅底 */
|
||
background: var(--sycm-bg-container);
|
||
border: 1px solid var(--sycm-border-color);
|
||
border-radius: 8px;
|
||
font-size: 12px;
|
||
line-height: 1.75;
|
||
}
|
||
|
||
/* ============ 轮次状态条 ============ */
|
||
.wst-status {
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.wst-status__row {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 12px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.wst-status__round {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.wst-status__badge {
|
||
padding: 1px 8px;
|
||
border-radius: 4px;
|
||
background: rgba(22, 119, 255, 0.12);
|
||
color: #0958d9;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
line-height: 20px;
|
||
}
|
||
|
||
.wst-status__badge--idle {
|
||
background: rgba(136, 135, 128, 0.14);
|
||
color: var(--sycm-text-tertiary);
|
||
}
|
||
|
||
.wst-status__label {
|
||
font-size: 13px;
|
||
color: var(--sycm-text-primary);
|
||
}
|
||
|
||
.wst-status__window,
|
||
.wst-status__time {
|
||
font-size: 12px;
|
||
color: var(--sycm-text-tertiary);
|
||
}
|
||
|
||
.wst-alert {
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
/* ============ 统计卡 ============ */
|
||
/* 5 张卡(超时未出库 / 业务挂起 / 涉及仓库 / 最久未出库 / 合计金额)
|
||
用 grid 而不是 Row/Col:24 栅格除不尽 5,Row/Col 会把第 5 张挤到下一行。 */
|
||
.wst-stats {
|
||
display: grid;
|
||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||
gap: 12px;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
@media (max-width: 1500px) {
|
||
.wst-stats { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||
}
|
||
|
||
@media (max-width: 900px) {
|
||
.wst-stats { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||
}
|
||
|
||
.wst-stat {
|
||
height: 100%;
|
||
}
|
||
|
||
/* 「业务挂起」卡可点开明细 */
|
||
.wst-stat--held {
|
||
cursor: pointer;
|
||
transition: box-shadow 0.2s;
|
||
}
|
||
|
||
.wst-stat--held:hover {
|
||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.wst-stat__more {
|
||
font-size: 12px;
|
||
font-weight: 400;
|
||
color: #1677ff;
|
||
}
|
||
|
||
/* 超时卡下面的「其中 N 单数据未同步」—— 订单表停更时的警示小字(2026-10-28) */
|
||
.wst-stat__warn {
|
||
margin-top: 6px;
|
||
font-size: 12px;
|
||
line-height: 1.4;
|
||
color: #cf1322;
|
||
cursor: help;
|
||
}
|
||
|
||
/* 停更告警里逐个源列出「最后同步时间 / 滞后多久」 */
|
||
.wst-alert__sources {
|
||
margin-top: 6px;
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 4px 16px;
|
||
}
|
||
|
||
.wst-alert__source {
|
||
font-size: 12px;
|
||
color: rgba(0, 0, 0, 0.65);
|
||
font-variant-numeric: tabular-nums;
|
||
}
|
||
|
||
/* 明细里的「内部单号 + 未同步标记」 */
|
||
.wst-detail__oid {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
font-variant-numeric: tabular-nums;
|
||
}
|
||
|
||
.wst-detail__stale {
|
||
margin-inline-end: 0;
|
||
font-size: 11px;
|
||
line-height: 16px;
|
||
padding-inline: 5px;
|
||
cursor: help;
|
||
}
|
||
|
||
/* ============ 主从两栏 ============ */
|
||
.wst-body {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 14px;
|
||
}
|
||
|
||
.wst-centers {
|
||
flex: none;
|
||
width: 264px;
|
||
}
|
||
|
||
.wst-centers__hint {
|
||
font-size: 11px;
|
||
}
|
||
|
||
.wst-centers__list {
|
||
margin: 0;
|
||
padding: 0;
|
||
list-style: none;
|
||
}
|
||
|
||
.wst-center {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 8px;
|
||
width: 100%;
|
||
padding: 6px 8px;
|
||
margin-bottom: 2px;
|
||
border: 1px solid transparent;
|
||
border-radius: 6px;
|
||
background: transparent;
|
||
font-size: 13px;
|
||
text-align: left;
|
||
color: var(--sycm-text-primary);
|
||
cursor: pointer;
|
||
transition: background 0.12s ease;
|
||
}
|
||
|
||
.wst-center:hover {
|
||
background: var(--sycm-bg-subtle);
|
||
}
|
||
|
||
.wst-center.is-active {
|
||
border-color: rgba(22, 119, 255, 0.45);
|
||
background: rgba(22, 119, 255, 0.08);
|
||
}
|
||
|
||
.wst-center.is-empty .wst-center__name {
|
||
color: var(--sycm-text-tertiary);
|
||
}
|
||
|
||
.wst-center.is-disabled {
|
||
cursor: not-allowed;
|
||
opacity: 0.6;
|
||
}
|
||
|
||
.wst-center__name {
|
||
overflow: hidden;
|
||
white-space: nowrap;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
.wst-center__badge {
|
||
flex: none;
|
||
min-width: 24px;
|
||
padding: 0 6px;
|
||
border-radius: 9px;
|
||
background: rgba(136, 135, 128, 0.12);
|
||
font-size: 11px;
|
||
font-weight: 600;
|
||
line-height: 17px;
|
||
text-align: center;
|
||
color: var(--sycm-text-tertiary);
|
||
font-variant-numeric: tabular-nums;
|
||
}
|
||
|
||
.wst-center__badge.is-warn {
|
||
background: rgba(255, 77, 79, 0.16);
|
||
color: #cf1322;
|
||
}
|
||
|
||
.wst-center__badge--none {
|
||
font-weight: 400;
|
||
}
|
||
|
||
.wst-detail {
|
||
flex: 1 1 auto;
|
||
min-width: 0;
|
||
}
|
||
|
||
.wst-detail__scope {
|
||
margin-left: 8px;
|
||
font-size: 12px;
|
||
font-weight: 400;
|
||
color: var(--sycm-text-tertiary);
|
||
}
|
||
|
||
.wst-detail__clear {
|
||
padding: 0;
|
||
border: none;
|
||
background: transparent;
|
||
font-size: 12px;
|
||
color: #1677ff;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.wst-detail__overdue {
|
||
color: #cf1322;
|
||
font-variant-numeric: tabular-nums;
|
||
}
|
||
|
||
/* 创建时间没补到、回退成付款时间时的提示色(带 * 号,tooltip 有说明) */
|
||
.wst-detail__fallback {
|
||
color: var(--sycm-text-tertiary);
|
||
font-style: italic;
|
||
}
|
||
|
||
.wst-foot {
|
||
display: block;
|
||
margin-top: 12px;
|
||
font-size: 12px;
|
||
color: var(--sycm-text-tertiary);
|
||
}
|
||
|
||
/* 窄屏:仓库列表改为两列网格,明细整行下移 */
|
||
@media (max-width: 1200px) {
|
||
.wst-body {
|
||
flex-direction: column;
|
||
}
|
||
|
||
.wst-centers {
|
||
width: 100%;
|
||
}
|
||
|
||
.wst-centers__list {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
gap: 2px 10px;
|
||
}
|
||
}
|
||
|
||
/* ── 钉钉通知(两轮核验完成后各推一次)───────────────────────────── */
|
||
.wst-notify__row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.wst-notify__label {
|
||
flex: 0 0 88px;
|
||
font-size: 13px;
|
||
color: var(--sycm-text-secondary);
|
||
}
|
||
|
||
.wst-notify__last {
|
||
padding: 8px 10px;
|
||
border-radius: 6px;
|
||
background: var(--sycm-fill-quaternary, rgba(0, 0, 0, 0.02));
|
||
font-size: 12px;
|
||
}
|
||
|
||
.wst-notify__error {
|
||
margin-top: 4px;
|
||
}
|
||
|
||
.wst-notify__help {
|
||
font-size: 12px;
|
||
line-height: 1.8;
|
||
color: var(--sycm-text-secondary);
|
||
}
|
||
|
||
.wst-notify__help code {
|
||
padding: 0 4px;
|
||
border-radius: 3px;
|
||
background: var(--sycm-fill-quaternary, rgba(0, 0, 0, 0.04));
|
||
}
|
||
|
||
.wst-notify__preview {
|
||
padding: 10px 12px;
|
||
border: 1px solid var(--sycm-border, rgba(0, 0, 0, 0.08));
|
||
border-radius: 8px;
|
||
background: var(--sycm-fill-quaternary, rgba(0, 0, 0, 0.02));
|
||
font-size: 12px;
|
||
line-height: 1.8;
|
||
}
|
||
|
||
.wst-notify__preview-head {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 8px;
|
||
}
|
||
|
||
.wst-notify__preview pre {
|
||
margin: 8px 0 0;
|
||
padding: 8px 10px;
|
||
border-radius: 6px;
|
||
background: var(--sycm-bg-container, #fff);
|
||
white-space: pre-wrap;
|
||
word-break: break-all;
|
||
font-size: 12px;
|
||
line-height: 1.7;
|
||
/* 30 条明细大概 40 行,别把整个弹窗撑爆;内部滚动看全 */
|
||
max-height: 280px;
|
||
overflow: auto;
|
||
}
|