Merge pull request 'fix: 配件仓位仅使用箱及仓位库存表' (#11)

Use rpt.jst_packInventoryInfo as the sole BOM location source and document explicit owner merge authorization.
This commit is contained in:
lunanting 2026-09-17 11:15:47 +08:00
commit ecfd97787d
6 changed files with 26 additions and 81 deletions

View File

@ -6,7 +6,7 @@
- 保留他人的未提交修改和已有提交,仅暂存本任务内容;不得清理或覆盖未知工作。 - 保留他人的未提交修改和已有提交,仅暂存本任务内容;不得清理或覆盖未知工作。
- 推送前同步远端,逐项检查最终差异并验证;被拒绝后先查明并合并更新,禁止任何形式的强推。 - 推送前同步远端,逐项检查最终差异并验证;被拒绝后先查明并合并更新,禁止任何形式的强推。
- 不改写已发布历史,不批量选择冲突的某一方;不明确的行为取舍需要负责人确认。 - 不改写已发布历史,不批量选择冲突的某一方;不明确的行为取舍需要负责人确认。
- 所有变更通过 PR 进入 `main`,不得直接推送主分支;独立审核与最新内容的验证通过后由维护者合并。 - 所有变更通过 PR 进入 `main`,不得直接推送主分支;默认由非提交者审核。仓库所有者或管理员明确授权某个 PR 合并时,该书面授权可以代替独立审核,但仍必须同步最新主分支、验证最终提交并保留授权记录。
- 不提交凭据、私钥、环境秘密或真实业务数据;不把合并授权当成生产部署授权。 - 不提交凭据、私钥、环境秘密或真实业务数据;不把合并授权当成生产部署授权。
- 新建或修改数据表、映射表、查询和同步任务时,必须按实际过滤、关联、排序和唯一性设计索引,用执行计划和有代表性数据验证负载;禁止无边界全表扫描、无节制并发和重复全量同步。 - 新建或修改数据表、映射表、查询和同步任务时,必须按实际过滤、关联、排序和唯一性设计索引,用执行计划和有代表性数据验证负载;禁止无边界全表扫描、无节制并发和重复全量同步。
- 开发机器使用中央系统数据库和公网 TLS Redis,无需安装本地数据库;生产部署必须先将代码通过 PR 合入 `main`,并在 `/opt/cangku/deploy.lock` 锁内从服务器 Git 工作区快进拉取和验证。 - 开发机器使用中央系统数据库和公网 TLS Redis,无需安装本地数据库;生产部署必须先将代码通过 PR 合入 `main`,并在 `/opt/cangku/deploy.lock` 锁内从服务器 Git 工作区快进拉取和验证。

View File

@ -39,9 +39,8 @@
// //
// ⑤ 配件仓位 —— 业务库 rpt.jst_packInventoryInfo; // ⑤ 配件仓位 —— 业务库 rpt.jst_packInventoryInfo;
// sku_id 可能带“-包装序号”后缀,需按基础配件编码匹配。优先显示 bin_group / bin, // sku_id 可能带“-包装序号”后缀,需按基础配件编码匹配。优先显示 bin_group / bin,
// 仓位为空时显示净数量大于 0 的 pack_id(箱号)与实际仓库名称。 // 仓位为空时显示净数量大于 0 的 pack_id(箱号)与 wh_id(仓库)。
// 尚未分箱时使用 rpt.jst_inventoryInfo 的正库存仓库;连库存记录也没有时, // 该列只使用箱及仓位库存表,不用库存仓库或商品档案仓库冒充仓位。
// 回退到 jst_sku.other_3 的商品档案仓库,并明确标注没有箱号,避免误报具体仓位。
// //
// ⑥ 配件库存 —— 业务库 rpt.jst_inventoryInfo 按 sku_id 汇总。 // ⑥ 配件库存 —— 业务库 rpt.jst_inventoryInfo 按 sku_id 汇总。
@ -555,37 +554,6 @@ function buildPackLocationMap(rows, componentCodes) {
return result; return result;
} }
function buildInventoryWarehouseMap(rows, componentCodes) {
const positiveWarehouses = new Map(componentCodes.map((code) => [code, new Set()]));
const defaultWarehouses = new Map();
rows.forEach((row) => {
const componentCode = componentCodes.find((code) => normalizeCode(code) === normalizeCode(row.skuId));
if (!componentCode) return;
const defaultWarehouse = toText(row.defaultWarehouse);
if (defaultWarehouse) defaultWarehouses.set(componentCode, defaultWarehouse);
const warehouseName = toText(row.warehouseName);
if (warehouseName && (toNumber(row.availableQty) ?? 0) > 0) {
positiveWarehouses.get(componentCode).add(warehouseName);
}
});
const result = new Map();
componentCodes.forEach((code) => {
const warehouses = [...positiveWarehouses.get(code)];
if (warehouses.length > 0) {
result.set(code, `${warehouses.join('、')}(未分箱)`);
return;
}
const defaultWarehouse = defaultWarehouses.get(code);
if (defaultWarehouse) result.set(code, `${defaultWarehouse}(暂无箱号)`);
});
return result;
}
/** /**
* 列表:指定仓库的 MKH 模块化成品 + BOM 配件种类/数量。 * 列表:指定仓库的 MKH 模块化成品 + BOM 配件种类/数量。
* BOM 数据只做明细匹配,不向商品列表追加无仓库档案的编码。 * BOM 数据只做明细匹配,不向商品列表追加无仓库档案的编码。
@ -725,18 +693,10 @@ async function getMkhBomDetail(mkhSkuId) {
try { try {
const stockRows = await cloudDatabase.query( const stockRows = await cloudDatabase.query(
`SELECT s.sku_id AS skuId, `SELECT i.sku_id AS skuId, SUM(i.qty) AS qty
s.other_3 AS defaultWarehouse, FROM rpt.jst_inventoryInfo i WITH (NOLOCK)
w.name AS warehouseName, WHERE i.sku_id IN (${placeholders})
SUM(COALESCE(i.qty, 0)) AS qty, GROUP BY i.sku_id`,
SUM(COALESCE(i.qty, 0)
- COALESCE(i.order_lock, 0)
- COALESCE(i.pick_lock, 0)) AS availableQty
FROM dbo.jst_sku s WITH (NOLOCK)
LEFT JOIN rpt.jst_inventoryInfo i WITH (NOLOCK) ON i.sku_id = s.sku_id
LEFT JOIN rpt.jst_warehouseInfo w WITH (NOLOCK) ON w.wms_co_id = i.wms_co_id
WHERE s.sku_id IN (${placeholders})
GROUP BY s.sku_id, s.other_3, i.wms_co_id, w.name`,
codeParams, codeParams,
{ requestTimeout: 180000 } { requestTimeout: 180000 }
); );
@ -745,9 +705,6 @@ async function getMkhBomDetail(mkhSkuId) {
if (!code) return; if (!code) return;
stockMap.set(code, (stockMap.get(code) || 0) + (toNumber(row.qty) ?? 0)); stockMap.set(code, (stockMap.get(code) || 0) + (toNumber(row.qty) ?? 0));
}); });
buildInventoryWarehouseMap(stockRows, uncachedCodes).forEach((location, code) => {
locationMap.set(code, location);
});
stockQuerySucceeded = true; stockQuerySucceeded = true;
} catch (error) { } catch (error) {
console.warn('[MkhBom] 配件库存查询降级:', error.message); console.warn('[MkhBom] 配件库存查询降级:', error.message);
@ -763,15 +720,13 @@ async function getMkhBomDetail(mkhSkuId) {
const locRows = await cloudDatabase.query( const locRows = await cloudDatabase.query(
`SELECT p.sku_id AS skuId, `SELECT p.sku_id AS skuId,
p.pack_id AS packId, p.pack_id AS packId,
p.wms_co_id AS warehouseId, p.wh_id AS warehouseId,
w.name AS warehouseName,
p.bin AS bin, p.bin AS bin,
p.bin_group AS binGroup, p.bin_group AS binGroup,
SUM(p.qty) AS qty SUM(p.qty) AS qty
FROM rpt.jst_packInventoryInfo p WITH (NOLOCK) FROM rpt.jst_packInventoryInfo p WITH (NOLOCK)
LEFT JOIN rpt.jst_warehouseInfo w WITH (NOLOCK) ON w.wms_co_id = p.wms_co_id
WHERE ${locationConditions.join(' OR ')} WHERE ${locationConditions.join(' OR ')}
GROUP BY p.sku_id, p.pack_id, p.wms_co_id, w.name, p.bin, p.bin_group GROUP BY p.sku_id, p.pack_id, p.wh_id, p.bin, p.bin_group
HAVING SUM(p.qty) > 0`, HAVING SUM(p.qty) > 0`,
locationParams, locationParams,
{ requestTimeout: 120000 } { requestTimeout: 120000 }
@ -831,6 +786,5 @@ module.exports = {
matchesPackSku, matchesPackSku,
formatPackLocation, formatPackLocation,
buildPackLocationMap, buildPackLocationMap,
buildInventoryWarehouseMap,
}, },
}; };

View File

@ -155,27 +155,17 @@ test('pack inventory locations match a BOM component and its package suffixes',
test('pack inventory location prefers bins and falls back to positive-stock box ids', () => { test('pack inventory location prefers bins and falls back to positive-stock box ids', () => {
const locations = _private.buildPackLocationMap([ const locations = _private.buildPackLocationMap([
{ {
skuId: 'PART-1-1', warehouseId: 1, warehouseName: '半成品仓', binGroup: '成品区', bin: 'A-01', packId: 'BOX-1', qty: 2, skuId: 'PART-1-1', warehouseId: 1, binGroup: '成品区', bin: 'A-01', packId: 'BOX-1', qty: 2,
}, },
{ {
skuId: 'PART-1-2', warehouseId: 1, binGroup: null, bin: '', packId: 'BOX-2', qty: 1, skuId: 'PART-1-2', warehouseId: 1, binGroup: null, bin: '', packId: 'BOX-2', qty: 1,
}, },
], ['PART-1']); ], ['PART-1']);
assert.equal(locations.get('PART-1'), '半成品仓 · 成品区 / A-01、仓库 1 · 箱号 BOX-2'); assert.equal(locations.get('PART-1'), '仓库 1 · 成品区 / A-01、仓库 1 · 箱号 BOX-2');
}); });
test('inventory warehouse fallback distinguishes unboxed stock from a warehouse with no box record', () => { test('components without pack inventory rows do not receive a fallback warehouse location', () => {
const locations = _private.buildInventoryWarehouseMap([ const locations = _private.buildPackLocationMap([], ['PART-1']);
{ assert.equal(locations.has('PART-1'), false);
skuId: 'PART-1', defaultWarehouse: '默认半成品仓', warehouseName: '实际库存仓', availableQty: 12,
},
{
skuId: 'PART-2', defaultWarehouse: '默认半成品仓', warehouseName: '实际库存仓', availableQty: 0,
},
], ['PART-1', 'PART-2', 'PART-3']);
assert.equal(locations.get('PART-1'), '实际库存仓(未分箱)');
assert.equal(locations.get('PART-2'), '默认半成品仓(暂无箱号)');
assert.equal(locations.has('PART-3'), false);
}); });

View File

@ -84,7 +84,7 @@ const PrintPreview = ({ data, onClose }) => {
<div className="mkh-print-foot"> <div className="mkh-print-foot">
<span> <span>
MKH 商品编码取自指定仓库商品主档;配件优先取聚水潭商品 BOM,业务组合 BOM 与产品资料系统作为补充; MKH 商品编码取自指定仓库商品主档;配件优先取聚水潭商品 BOM,业务组合 BOM 与产品资料系统作为补充;
配件仓位优先取包装箱与具体仓位,尚未分箱时显示库存仓库或商品档案仓库。 配件仓位只取箱及仓位库存表;没有箱号或仓位记录时显示「未维护」。
</span> </span>
</div> </div>
</div> </div>
@ -317,7 +317,7 @@ const MkhBomData = () => {
dataIndex: 'location', dataIndex: 'location',
width: 140, width: 140,
render: (v) => (v ? text(v) : ( render: (v) => (v ? text(v) : (
<Tooltip title="上游未回传库位数据"> <Tooltip title="箱及仓位库存表中没有该配件的有效记录">
<span className="mkh-muted">未维护</span> <span className="mkh-muted">未维护</span>
</Tooltip> </Tooltip>
)), )),
@ -339,8 +339,8 @@ const MkhBomData = () => {
<Text type="secondary" className="mkh-note"> <Text type="secondary" className="mkh-note">
数据口径:MKH 商品编码仅取自业务库 jst_sku 中“简尚-崔桥工厂品牌仓”的成品(当前 4408 条); 数据口径:MKH 商品编码仅取自业务库 jst_sku 中“简尚-崔桥工厂品牌仓”的成品(当前 4408 条);
配件明细与数量优先取自聚水潭商品 BOM,业务中台组合商品 BOM 与产品资料系统数据库中维护的配件作为补充; 配件明细与数量优先取自聚水潭商品 BOM,业务中台组合商品 BOM 与产品资料系统数据库中维护的配件作为补充;
无指定仓库商品档案的 BOM 组合编码不会进入列表;配件仓位优先显示箱号或具体仓位, 无指定仓库商品档案的 BOM 组合编码不会进入列表;配件仓位只取 rpt.jst_packInventoryInfo,
尚未分箱时显示实际库存仓库,没有库存记录时显示商品档案仓库。 优先显示具体仓位,没有具体仓位时显示箱号,表中没有有效记录时显示「未维护」。
</Text> </Text>
</div> </div>
</header> </header>

View File

@ -168,12 +168,12 @@ npm --prefix frontend run check
合并要求: 合并要求:
1. 至少一名非提交者的维护者或开发者审核最终差异;AI 自我复查可以辅助,但不代替独立审核。提交 AI 不得用另一个会话或共享账号冒充独立审核者。 1. 默认由至少一名非提交者的维护者或开发者审核最终差异;AI 自我复查可以辅助,但不代替独立审核。仓库所有者或管理员对明确的 PR 作出书面合并指令时,该授权可以代替独立审核,但必须在 PR 中记录授权人、授权范围和当时的最终提交 SHA。提交 AI 不得伪造授权、冒用其他账号或把模糊的日常开发指令解释为合并授权。
2. 审核需关注是否撤销他人逻辑、丢失配置/依赖、改变接口与权限、破坏已上线功能,不能只看有无冲突或测试绿灯。 2. 审核需关注是否撤销他人逻辑、丢失配置/依赖、改变接口与权限、破坏已上线功能,不能只看有无冲突或测试绿灯。
3. 审核和检查必须对应最终待合并提交;后续新提交或冲突解决后需要重新确认,不能沿用失效的批准。 3. 审核、所有者/管理员授权和检查必须对应最终待合并提交;后续新增业务代码、依赖变更或冲突解决后需要重新确认,不能沿用失效的批准。授权后仅补充不改变行为的合并记录或文档时,应在 PR 中说明并重新检查最终差异。
4. 合并前再次确认最新 `main` 已被纳入任务分支,并验证组合后的行为。其他 PR 先合并导致主分支前进时,更新本 PR、重新检查并确认审核后再合并。 4. 合并前再次确认最新 `main` 已被纳入任务分支,并验证组合后的行为。其他 PR 先合并导致主分支前进时,更新本 PR、重新检查并确认审核或所有者/管理员授权仍适用于最终提交后再合并。
5. 由维护者通过仓库 PR 合并功能依次处理;同时有多个待合并 PR 时串行合并。有合并队列时使用针对最新组合提交的检查,避免两份旧验证同时放行。 5. 由维护者通过仓库 PR 合并功能依次处理;同时有多个待合并 PR 时串行合并。有合并队列时使用针对最新组合提交的检查,避免两份旧验证同时放行。
6. 未解决冲突、必需检查失败、尚未审核或同步状态不清楚时不得合并。缺少独立审核者时保持 PR 待审核,不自行批准。 6. 未解决冲突、必需检查失败、同步状态不清楚,或既无独立审核也无所有者/管理员明确授权时不得合并。授权例外不允许绕过失败检查、未解决冲突、数据库风险证据或分支保护,也不允许直接推送 `main`。
7. 合并后核对主分支结果。删除任务分支前确认已合并且无人继续使用,不删除其他开发者的分支。 7. 合并后核对主分支结果。删除任务分支前确认已合并且无人继续使用,不删除其他开发者的分支。
## 7. 发现误合并或代码被覆盖 ## 7. 发现误合并或代码被覆盖
@ -190,6 +190,7 @@ npm --prefix frontend run check
- [ ] 涉及数据表、索引、映射、查询或同步时,已验证执行计划、数据量边界和负载控制,并在 PR 记录真实证据。 - [ ] 涉及数据表、索引、映射、查询或同步时,已验证执行计划、数据量边界和负载控制,并在 PR 记录真实证据。
- [ ] 已同步最新主分支及远端任务分支,未强推、未改写已发布历史。 - [ ] 已同步最新主分支及远端任务分支,未强推、未改写已发布历史。
- [ ] 已逐项审核最终差异;冲突处理保留双方有效行为,疑点已确认。 - [ ] 已逐项审核最终差异;冲突处理保留双方有效行为,疑点已确认。
- [ ] 已取得独立审核,或已在 PR 中记录仓库所有者/管理员针对最终提交的明确合并授权。
- [ ] 已完成受影响的验证,记录准确结果和提交 SHA。 - [ ] 已完成受影响的验证,记录准确结果和提交 SHA。
- [ ] PR 已说明范围、兼容性、验证、风险和回退方法。 - [ ] PR 已说明范围、兼容性、验证、风险和回退方法。
- [ ] 已明确报告当前状态:本地提交 / 已推送 / PR 待审核 / 已合并;没有把推送成功说成合并或部署成功。 - [ ] 已明确报告当前状态:本地提交 / 已推送 / PR 待审核 / 已合并;没有把推送成功说成合并或部署成功。
@ -198,6 +199,6 @@ npm --prefix frontend run check
本文件规定协作流程,`AGENTS.md` 提供 AI 读取入口;它们不是权限控制,不能保证每个 AI 工具自动读取或遵守。使用不自动读取这些文件的工具时,开发者必须主动提供规则。 本文件规定协作流程,`AGENTS.md` 提供 AI 读取入口;它们不是权限控制,不能保证每个 AI 工具自动读取或遵守。使用不自动读取这些文件的工具时,开发者必须主动提供规则。
仓库管理员应在服务端落实以下保护,并以实际设置为准:保护 `main`、禁止直接及强制推送和删除、要求 PR 与独立审核、使过期审核失效、绑定真实运行的必需检查、限制合并权限、按个人发放最小权限凭据。没有真正运行的检查不能仅凭同名状态作为合并依据。 仓库管理员应在服务端落实以下保护,并以实际设置为准:保护 `main`、禁止直接及强制推送和删除、要求通过 PR 合并、使过期审核失效、绑定真实运行的必需检查、限制合并权限、按个人发放最小权限凭据。团队可默认要求独立审核;需要允许所有者/管理员授权例外时,应确保只有真实所有者或管理员能够执行并保留可审计记录。没有真正运行的检查不能仅凭同名状态作为合并依据。
**添加本文件不等于上述保护已经启用。** 此文档变更不修改仓库权限、分支保护、CI 或生产部署;如需启用服务端限制,应由管理员单独配置并验证。 **添加本文件不等于上述保护已经启用。** 此文档变更不修改仓库权限、分支保护、CI 或生产部署;如需启用服务端限制,应由管理员单独配置并验证。

View File

@ -59,8 +59,8 @@ git diff --stat origin/main...HEAD
2. 本地已有修改先按 `仓库提交与合并规范.md` 提交到唯一任务分支,再同步最新 `origin/main`。 2. 本地已有修改先按 `仓库提交与合并规范.md` 提交到唯一任务分支,再同步最新 `origin/main`。
3. 如果主仓库已有更新,先合并到任务分支,逐处处理冲突,重新检查最终差异并运行受影响测试。 3. 如果主仓库已有更新,先合并到任务分支,逐处处理冲突,重新检查最终差异并运行受影响测试。
4. 禁止使用强制推送、`git reset --hard`、批量选择 `ours/theirs` 或复制旧目录覆盖新代码来解决冲突。 4. 禁止使用强制推送、`git reset --hard`、批量选择 `ours/theirs` 或复制旧目录覆盖新代码来解决冲突。
5. 将任务分支推送到主仓库,创建 PR,经过审核和验证后合并到 `main`。这里的“先推送主仓库”指推送任务分支并通过 PR 合并,禁止直接推送 `main`。 5. 将任务分支推送到主仓库并创建 PR;经过独立审核,或取得仓库所有者/管理员针对该 PR 最终提交的明确授权,并完成验证后合并到 `main`。这里的“先推送主仓库”指推送任务分支并通过 PR 合并,禁止直接推送 `main`。
6. PR 合并后再次执行 `git fetch origin`,记录准备部署的 `origin/main` 完整 SHA。部署 SHA 必须与审核通过的最终提交一致。 6. PR 合并后再次执行 `git fetch origin`,记录准备部署的 `origin/main` 完整 SHA。部署 SHA 必须与审核或所有者/管理员授权覆盖的最终提交一致。
如果本次变更涉及数据表、索引、查询、数据映射或同步任务,部署者还必须确认 PR 已按《仓库提交与合并规范》记录索引与执行计划证据、分批/并发/超时边界、大表锁影响和回退方式。证据缺失、依赖全表扫描、可能对系统库或第三方云库造成不可控负载时,不得部署。 如果本次变更涉及数据表、索引、查询、数据映射或同步任务,部署者还必须确认 PR 已按《仓库提交与合并规范》记录索引与执行计划证据、分批/并发/超时边界、大表锁影响和回退方式。证据缺失、依赖全表扫描、可能对系统库或第三方云库造成不可控负载时,不得部署。