Merge pull request 'feat: returns board outbound column -> supplier cooperation status' (#52) from codex/lunanting-supplier-coop-001 into main

This commit is contained in:
Xkh5201314 2026-09-28 15:36:14 +08:00
commit 23e1b365d6
3 changed files with 81 additions and 17 deletions

View File

@ -14,13 +14,20 @@
* · returnBinName = 用户口径的聚水潭仓位名,写进页面/导出的「数据来源」,便于核对;
* · returnSlot = 副本里真正取数的那一列,必须与上面的仓位名一一对应。
*
* 仓位名 → 库存位 对照(2026-09-25 复测,条件 ISNULL(col,0) > 0):
* 箱及仓位库存 · 简尚-崔桥工厂仓 · 崔桥工厂客退临时上架仓 customize_qty_1 2 编码 / 2 件 ⚠
* 仓位名 → 库存位 对照(2026-09-27 复测,条件 ISNULL(col,0) > 0):
* 箱及仓位库存 · 简尚-崔桥工厂仓 · 崔桥工厂客退临时上架仓 customize_qty_1 349 编码 / 507 件
* 箱及仓位库存 · 简尚-新苏北仓 · 新苏北客退上架仓 customize_qty_1 75 编码 / 362 件
* 箱及仓位库存 · 简尚-睢宁仓 · 睢宁客退上架仓 customize_qty_1 98 编码 / 243 件
* 箱及仓位库存 · 简尚-山东青岗集仓 · 青岗集客退上架仓 customize_qty_2 403 编码 / 912 件
* 箱及仓位库存 · 简尚-河南洛宁仓 · 河南洛宁客退上架仓 三位全为 0(无数据)
*
* ── 2026-09-27 重要更新:崔桥工厂仓的客退库存已经同步进副本了 ──
* 09-25 首次切换时该仓客退位只有 2 编码 / 2 件(当时判定「未同步进副本」),
* 09-26 起 IT 侧补上了同步:09-26 当天写入 317 编码 / 460 件,09-27 又补 31 编码 / 46 件,
* 09-27 10:11 实测 349 编码 / 507 件(= 副本 c1 合计,页面与副本完全对得上)。
* ⚠ 所以下面「崔桥必然近乎空白 / 只能靠导入」的旧结论已经作废,不要再拿它当依据。
* 现在这张看板能正常出数,**不需要**用导入功能兜底。
*
* ⚠ 崔桥工厂仓(15038426)是 2026-09-25 用户指定替换横林二仓的。复测结论:
* 副本里该仓客退位只有 2 编码 / 2 件,但退货单量很大(ERP 售后单 3,115 条 +
* 奇门退款单 1,812 条,warehouse='简尚-崔桥工厂仓销退仓')——即「退货有,客退上架库存
@ -58,10 +65,8 @@ const WAREHOUSE_LIST = [
returnBinName: '崔桥工厂客退临时上架仓',
// 客退临时上架仓占用的自定义库存位
returnSlot: 'customize_qty_1',
// ⚠ 2026-09-25 实测:该仓在副本 jst_inventoryInfo 里客退位只有 2 个编码 / 2 件
//(qty 总库存 92,818 件;return_qty / c2 / c3 / sale_refund_qty 全为 0)。
// 退货单是有的(ERP 售后单 3,115 条 + 奇门退款单 1,812 条),
// 说明是「客退上架仓」这一仓位没同步进副本,不是业务上没有退货。
// 2026-09-27 复测:客退位已同步进副本 —— 349 编码 / 507 件(c1 合计),
// 退货单口径:ERP 售后单 3,115 条 + 奇门退款单 1,812 条(warehouse='简尚-崔桥工厂仓销退仓')。
hasReturnStock: true,
},
{

View File

@ -367,10 +367,16 @@ const buildFullSql = (createdAtBasis = DEFAULT_CREATED_AT_BASIS, warehouse = DEF
];
if (useFirstInbound) selectColumns.push('f.first_inbound_time AS [firstInboundTime]');
selectColumns.push("ISNULL(sk.supplier_name, N'') AS [supplier]");
// 供应商是否合作:取自简尚供应商主数据 js_supplier.coopersgn(1=合作 / 2=不合作)
selectColumns.push('sp.coopersgn AS [supplierCoopSign]');
// 单价取自商品资料 jst_sku:成本价优先,缺失/为 0 时回退采购价(见 pickUnitPrice)
selectColumns.push('sk.cost_price AS [costPrice]');
selectColumns.push('sk.purchase_price AS [purchasePrice]');
const joins = ['LEFT JOIN dbo.jst_sku sk ON sk.sku_id = s.product_code'];
// ⚠ 关联键必须是 scmname(供应商简称),与 jst_sku.supplier_name 是同一套命名(2026-09-28 实测
// 匹配 100/109);tradebody(工商全称)只能匹配 1 条,别用。scmname 在 js_supplier 里唯一
// (1,497 行 / 1,497 个不同值),所以 LEFT JOIN 不会放大行数。
joins.push('LEFT JOIN dbo.js_supplier sp ON sp.scmname = sk.supplier_name');
if (useFirstInbound) joins.push('LEFT JOIN first_in f ON f.product_code = s.product_code');
return `
@ -392,7 +398,13 @@ const buildStockSyncedSql = (warehouse = DEFAULT_WAREHOUSE) => `
FROM dbo.jst_inventoryInfo
WHERE wms_co_id = ${warehouse.wmsCoId}`;
const CACHE_TTL_MS = 10 * 60 * 1000;
// 默认 5 分钟:副本侧 ETL 约 10 分钟一批增量(2026-09-27 实测),
// 这里再压一层缓存只是防止多人反复翻页把副本打爆;想更快用 env 调,或前端点「刷新数据」强制回源。
const DEFAULT_CACHE_TTL_MS = 5 * 60 * 1000;
const resolveCacheTtlMs = () => {
const raw = Number.parseInt(process.env.RETURN_STOCK_WARNING_CACHE_TTL_MS, 10);
return Number.isFinite(raw) && raw >= 0 ? raw : DEFAULT_CACHE_TTL_MS;
};
// 缓存键 = 仓库 + 创建时间口径(两者都会改变 SQL / 取到的行)
const buildCacheKey = (warehouse, basis) => `${warehouse.key}|${basis}`;
const rowCache = new Map();
@ -437,7 +449,7 @@ async function loadReplicaRows({
/** 副本库存表最近一次同步时间(YYYY-MM-DD HH:mm:ss),页面「库存更新时间」用 */
async function loadStockSyncedAt({ refresh = false, warehouse = DEFAULT_WAREHOUSE } = {}) {
const state = stockSyncedAtState.get(warehouse.key);
if (!refresh && state?.value && Date.now() - state.at < CACHE_TTL_MS) {
if (!refresh && state?.value && Date.now() - state.at < resolveCacheTtlMs()) {
return state.value;
}
try {
@ -567,6 +579,24 @@ const resolveReturnLimitDays = (value) => {
* 其余 → pending,returnRemainDays = 剩余天数(时限 − 在库天数)
* 拿不到创建时间(算不出在库天数)时按 pending 处理,剩余天数留空,页面提示「待补创建时间」。
*/
// ---------------------------------------------------------------------------
// 供应商是否合作(2026-09-28 新增)
// 依据:简尚供应商主数据 dbo.js_supplier.coopersgn —— 1 = 合作中,2 = 不合作。
// 语义已交叉验证:与 dbo.fys_supplier.IsCoop('合作'/'不合作')比对,
// coopersgn=2 ↔ 不合作 823 条一致、coopersgn=1 ↔ 合作 270 条一致。
// 两边冲突的 262 条以 js_supplier 为准 —— fys_supplier 是旧快照(DownloadTime 停在
// 2025-11,样本多为 2023/2024),而 js_supplier 的 local_updated 已到 2026-09。
// 匹配不上(供应商没进主数据、或该编码没有供应商)一律按「未知」展示,不要当「不合作」。
// ---------------------------------------------------------------------------
const SUPPLIER_COOP_LABELS = { cooperative: '合作', ended: '不合作', unknown: '未知' };
const resolveSupplierCoop = (sign) => {
if (sign === null || sign === undefined || sign === '') return 'unknown';
const n = Number(sign);
if (n === 1) return 'cooperative';
if (n === 2) return 'ended';
return 'unknown';
};
const resolveReturnDeadline = ({ unitPriceLevel, stockDays, isOutbound, returnLimitDays }) => {
if (unitPriceLevel !== 'high') return { status: 'none', remainDays: null };
if (isOutbound) return { status: 'done', remainDays: null };
@ -821,6 +851,8 @@ function composeRows({
const deadline = resolveReturnDeadline({
unitPriceLevel, stockDays, isOutbound, returnLimitDays,
});
// 供应商是否合作(js_supplier.coopersgn)
const supplierCoop = resolveSupplierCoop(row.supplierCoopSign);
if (scope === 'imported' && !override) return;
if (qty <= 0) return;
@ -851,6 +883,8 @@ function composeRows({
stockDays,
warningLevel: resolveWarningLevel(stockDays, focusDays, alertDays),
isOutbound,
supplierCoop,
supplierCoopLabel: SUPPLIER_COOP_LABELS[supplierCoop] || '',
source: override ? 'jst' : 'replica',
noInboundRecord: !createdAtRaw,
});
@ -870,6 +904,8 @@ function composeRows({
const deadline = resolveReturnDeadline({
unitPriceLevel, stockDays, isOutbound, returnLimitDays,
});
// 这条是「导入里有、副本里没有」补进来的编码,没走副本 join,拿不到 coopersgn → 标「未知」
const supplierCoop = 'unknown';
rows.push({
productCode: code,
createdAt: formatDateTime(override.createdAt),
@ -895,6 +931,8 @@ function composeRows({
stockDays,
warningLevel: resolveWarningLevel(stockDays, focusDays, alertDays),
isOutbound,
supplierCoop,
supplierCoopLabel: SUPPLIER_COOP_LABELS[supplierCoop] || '',
source: 'jst',
noInboundRecord: !override.createdAt,
});
@ -1148,7 +1186,7 @@ const buildExportColumns = (warehouse = DEFAULT_WAREHOUSE) => [
{ key: 'amount', title: '总金额' },
{ key: 'unitPriceLevelLabel', title: '单价预警' },
{ key: 'returnDeadlineLabel', title: '退厂时限' },
{ key: 'isOutboundLabel', title: '是否出库' },
{ key: 'supplierCoopLabel', title: '供应商是否合作' },
{ key: 'returnQty', title: '退货仓库存(return_qty)' },
{ key: 'returnSlotQty', title: `客退临时上架库存(${warehouse.returnSlot})` },
{ key: 'sourceLabel', title: '数据来源' },
@ -1208,7 +1246,7 @@ async function getReturnStockWarningExportRows(options = {}) {
warningLabel: WARNING_LABELS[row.warningLevel] || WARNING_LABELS.normal,
unitPriceLevelLabel: PRICE_WARNING_LABELS[row.unitPriceLevel] || '',
returnDeadlineLabel: formatReturnDeadlineLabel(row.returnDeadlineStatus, row.returnRemainDays),
isOutboundLabel: row.isOutbound ? '是' : '否',
supplierCoopLabel: row.supplierCoopLabel || SUPPLIER_COOP_LABELS.unknown,
sourceLabel: row.createdAtSourceLabel || '',
createdAtRaw: undefined,
}));

View File

@ -71,6 +71,19 @@ const RETURN_DEADLINE_TAGS = {
};
const RETURN_DEADLINE_RANK = { overdue: 3, pending: 2, done: 1, none: 0 };
// 供应商是否合作(后端取自简尚供应商主数据 js_supplier.coopersgn:1=合作 / 2=不合作)
const SUPPLIER_COOP_TAGS = {
cooperative: { color: 'green', label: '合作' },
ended: { color: 'red', label: '不合作' },
unknown: { color: 'default', label: '未知' },
};
const SUPPLIER_COOP_RANK = { ended: 2, cooperative: 1, unknown: 0 };
const SUPPLIER_COOP_TIPS = {
cooperative: '该商品编码的供应商在简尚供应商主数据里标记为「合作中」(js_supplier.coopersgn = 1)',
ended: '该商品编码的供应商在简尚供应商主数据里标记为「不合作」(js_supplier.coopersgn = 2),退货处理需另行商定对接人',
unknown: '供应商名在简尚供应商主数据(js_supplier)里没有匹配记录,或该编码没有供应商,无法判断合作状态',
};
// 预警等级过滤:主视图改成一排状态页签(全部 / 红色预警 / 关注 / 正常 / 未知),
// 因此不再需要下拉选项;导出时仍把 warningLevel 原样传给后端(服务端 filter 同口径)。
@ -348,11 +361,19 @@ const COLUMNS = [
},
},
{
title: '是否出库',
dataIndex: 'isOutbound',
width: 90,
title: '供应商是否合作',
dataIndex: 'supplierCoop',
width: 136,
align: 'center',
render: (value) => (value ? <Tag color="green">是</Tag> : <Tag>否</Tag>),
sorter: (a, b) => (SUPPLIER_COOP_RANK[a.supplierCoop] || 0) - (SUPPLIER_COOP_RANK[b.supplierCoop] || 0),
render: (value) => {
const tag = SUPPLIER_COOP_TAGS[value] || SUPPLIER_COOP_TAGS.unknown;
return (
<Tooltip title={SUPPLIER_COOP_TIPS[value] || SUPPLIER_COOP_TIPS.unknown}>
<Tag color={tag.color}>{tag.label}</Tag>
</Tooltip>
);
},
},
{
title: '创建时间来源',