Compare commits

...

254 Commits

Author SHA1 Message Date
居居
b1b23a0b5e ops(supply-chain): add production limit snapshot timer
Install a production systemd timer at 09:30 Asia/Shanghai, persist daily snapshots under the shared release directory, and validate required production data sources before enabling the schedule.
2026-10-30 16:53:19 +08:00
居居
2a1ae8640b feat(clearance): 完成率档位支持自定义区间 2026-10-30 16:14:21 +08:00
居居
b5fed760cf feat(clearance): 规则条件值改为动态多选 2026-10-30 16:14:20 +08:00
居居
1afd1f7ffa feat(supply-chain): add daily limit snapshot workflow
Generate and export an atomic daily limit snapshot from the full product archive, reconcile all limit-dependent archive fields before publication, and apply owner-adjusted pricing without the gross-margin floor when an owner limit exists.
2026-10-30 16:14:20 +08:00
WuYongJie
cdd3e8cfcb feat(diy): add shared-angle batch AI rendering and refine scheme dialogs 2026-10-30 15:22:32 +08:00
WuYongJie
c03a9b5e36 fix(product-archive): unify list and detail image sources 2026-10-30 13:05:57 +08:00
居居
9cc2aa9ea7 feat(freight): 增加物流快照正式站定时调度 2026-10-30 11:14:45 +08:00
居居
49e00b0f93 feat(freight): 每日九点生成聚水潭物流规则快照 2026-10-30 10:35:40 +08:00
居居
d590b5d6cd fix(freight): 拦截说明文字作为商品编码提交
原文 cherry-pick 自主线 cd094bc90;因 main 上 calculateByBarcodes 为索引化融合版(不走单条方法),补充同样的 isFreightBarcodeInput 格式拦截与改进后的“不在产品全量档案中”文案到批量循环,并更新批量测试(FAIL 新文案 + 说明文字拦截用例)。
2026-10-30 10:14:08 +08:00
WuYongJie
9ae4ffb44b fix(diy): correct mirrored door texture orientation 2026-10-30 10:10:49 +08:00
居居
8571b5f649 feat(freight): 支持条码固定价和体积运费测算 2026-10-29 17:44:03 +08:00
居居
2ea3f22349 feat(freight): 支持连续对话修正与部分报价计算 2026-10-29 14:48:55 +08:00
WuYongJie
60c933a14e fix(intake): reuse empty SKU placeholders and normalize imported dimensions
Preserve factory specification names; match equivalent size separators.

Task: sku-import-empty-placeholder-20261029-01a18026
Source-commit: afcede79a052c0d478866f71c59fd15f4c64d3ef
2026-10-29 14:43:43 +08:00
WuYongJie
f18a0f3f80 fix(store-account): support JD profiles in account editor 2026-10-29 14:36:55 +08:00
WuYongJie
ee410934ec fix(intake): update children furniture execution standard to GB 28007-2024 2026-10-29 13:52:20 +08:00
WuYongJie
b936295a7d feat(intake): preserve factory specification names in SKU imports 2026-10-29 11:40:50 +08:00
WuYongJie
9af1d9efda fix(limit-management): bind large style sets as a PostgreSQL array 2026-10-29 10:56:34 +08:00
WuYongJie
2be511f346 perf(freight): index batch barcode calculations with shared calculator
calculateByBarcodes previously called calculateByBarcode per item, re-reading rates, shipment share and courier rules and re-scanning the full rate catalogue for every barcode. Switch it to the shared createFreightCalculator snapshot: batch archive read via findProductArchiveSnapshots, courier rules fetched once per warehouse, rates and province shipment share fetched once, and the calculation loop yields to the event loop every 8ms like the archive page path. Per-item validation messages and result/error shapes are unchanged.
2026-10-29 10:46:16 +08:00
WuYongJie
2830836b92 test(freight): align release fixture with published region formula 2026-10-29 10:22:51 +08:00
WuYongJie
c169f9392c fix(limit-management): reuse locale comparator for archive sorting 2026-10-29 10:20:35 +08:00
居居
09128489fb feat(freight-maintenance): 完善批量测算与对话式比价 2026-10-29 10:13:38 +08:00
WuYongJie
4fd17c6677 fix(intake): convert WebP installation drawings and verify image delivery
Isolated release of 423d8bc7e663788d4bd2f3c7a41d6f3a1b1089d1.
2026-10-29 10:08:56 +08:00
WuYongJie
66db2a26f6 fix(freight): use framework-compatible event loop scheduling 2026-10-29 09:49:12 +08:00
WuYongJie
ec8cf4a819 feat(material-library): export all boards and normalize surface types 2026-10-29 09:19:36 +08:00
WuYongJie
416bcd5939 test(freight): preserve production formulas in scoped release 2026-10-28 20:18:20 +08:00
WuYongJie
e889d53cb6 fix(freight): index batch rates and yield the web event loop 2026-10-28 20:10:56 +08:00
WuYongJie
83a3138fe9 fix(intake): use configured vision model for reference cost estimates 2026-10-28 17:06:19 +08:00
居居
45387c9003 fix(limit-management): include freight in limit exports 2026-10-28 16:36:28 +08:00
WuYongJie
a9f2917c5b style(intake): align SKU toolbar actions by task 2026-10-28 16:16:29 +08:00
WuYongJie
77665c85b5 feat(intake): add product notification delivery history 2026-10-28 16:03:41 +08:00
居居
3f7710ef84 feat(limit-management): add regional and national freight averages 2026-10-28 15:56:14 +08:00
居居
281e52bb95 feat(supply-chain): 完善快递维护与AI运费比价 2026-10-28 15:56:14 +08:00
WuYongJie
2775b4ff83 feat(intake): import SKU templates with automatic coding 2026-10-28 14:36:34 +08:00
WuYongJie
e58f8ba96d fix(limit-management): restore mysql2 query call compatible with locked 3.15.3 typings
75adc3f7 imported QueryValues from mysql2/promise, which only exists in newer mysql2 typings than the package-lock version (3.15.3), breaking tsc on a clean install. Restore the previous equivalent call form (options object + values argument), preserving runtime behavior exactly.
2026-10-28 14:04:01 +08:00
居居
062666f530 feat(limit-management): consolidate limit and freight maintenance 2026-10-28 13:59:30 +08:00
居居
ce0aa8f387 fix(supply-chain): 修复运费配置权限并过滤过期费率 2026-10-28 13:59:30 +08:00
居居
5c5fef8b8e feat(supply-chain): 新增运费配置查询页面 2026-10-28 13:59:30 +08:00
WuYongJie
8f1cb2ac02 refactor(users): align product permissions with four navigation levels 2026-10-28 10:28:26 +08:00
WuYongJie
500da7be24 fix(market-analysis): add account and role access control 2026-10-28 09:42:24 +08:00
WuYongJie
1ddf0a34ce fix(intake): honor photo upload grants across products 2026-10-28 09:04:00 +08:00
居居
d3468b3756 feat(clearance): 排除福利品和空白分类 2026-10-27 17:25:40 +08:00
居居
11c91796e3 fix(clearance): 扣减虚拟云仓库存 2026-10-27 17:25:40 +08:00
WuYongJie
dcb61cc13f feat(intake): improve installation pushes and admin replacement 2026-10-27 17:13:37 +08:00
居居
6eb3ac0d53 feat(clearance): 简尚无库存时标记入库时间 2026-10-27 15:39:03 +08:00
居居
47fde040ee fix(clearance): 日销保留两位小数 2026-10-27 15:39:03 +08:00
居居
14b6157411 feat(clearance): 调整加权日销计算逻辑 2026-10-27 15:39:03 +08:00
居居
51e9ac3642 fix(clearance): 修正两清协议库存统计标记 2026-10-27 15:39:03 +08:00
居居
c7d1965df1 fix(clearance): 页面库存明细负数显示为零 2026-10-27 15:39:03 +08:00
居居
74613ce107 fix(clearance): 负库存参与最终库存汇总 2026-10-27 15:39:03 +08:00
居居
003b0e780c fix(clearance): 库存负数生成时归零 2026-10-27 15:39:03 +08:00
居居
638d8f591f fix(clearance): 生成时使用实时商品状态 2026-10-27 15:39:03 +08:00
WuYongJie
40debae10c fix(store-account): prevent accidental dismissal of create dialog 2026-10-27 15:24:13 +08:00
WuYongJie
cc208a400a fix(store-account): sync personnel profit centers and show save errors 2026-10-27 14:39:13 +08:00
WuYongJie
b026c8c840 fix(intake): 更新新增产品必填项 2026-10-27 14:05:21 +08:00
居居
6b85379ebb fix(clearance): 注册清货菜单权限节点
(cherry picked from commit 136350a02efa09bb4684da8cf6a6e442c198ed93)
2026-10-27 09:40:30 +08:00
居居
6d341c4196 fix(discontinuation): 将每日任务调整为十点三十分 2026-10-27 09:23:54 +08:00
WuYongJie
7fe6939daa Merge remote-tracking branch 'origin/main' of D:\ChanPing\furniture-3d-system-Wu into HEAD 2026-10-26 18:03:03 +08:00
WuYongJie
afb2d2b153 fix(clearance): restore applied migration content to keep checksum stable
5ea75eac added IF NOT EXISTS to migration 20261020090000 so dev databases can replay it. That migration is already applied on production with a recorded checksum, so main must keep the original file content or prisma migrate deploy fails with a checksum mismatch. The IF NOT EXISTS tweak is only needed in dev environments; production already has the table.
2026-10-26 17:55:47 +08:00
居居
af9381c2df feat(clearance): 接入采购入库与分利润中心发货统计
- 最近入库时间按采购单完结时间或预计完结时间取最新值
- 入库天数按清货池生成日展示整数
- 发货数量按任务利润中心匹配发货店铺后汇总
- 发货数据限定当前清货月份和有效确认出库单
2026-10-26 17:55:47 +08:00
居居
5375e21d92 feat(supply-chain): 上线清货管理与月度评分
- 新增清货池、任务审核确认、任务推送与复盘分数体系
- 新增优先级/固定任务/折扣限制/分数档位配置
- 新增清货数据迁移、同步脚本、权限和钉钉组合异常通知
- 补充清货页面、接口、服务、仓储及自动化测试
2026-10-26 17:55:47 +08:00
apple
64ab4a9065 fix(modular-processing): remove retired status rule column 2026-10-26 17:55:35 +08:00
WuYongJie
769e3d519c fix(intake): standardize category display and validate create prefill 2026-10-26 17:50:21 +08:00
WuYongJie
3ac409dd24 Merge remote-tracking branch 'origin/main' into release-discontinuation 2026-10-26 16:46:05 +08:00
WuYongJie
75ad922a5f fix(supply-chain): trigger ERP export dropdown on hover 2026-10-26 16:45:32 +08:00
apple
454ea9f564 feat(modular-processing): add modular processing order management 2026-10-26 16:41:43 +08:00
WuYongJie
3e7f2c4dd1 fix(supply-chain): click ERP goods controls inside their frame 2026-10-26 16:32:49 +08:00
WuYongJie
e0b99c7331 fix(supply-chain): recognize authenticated ERP with hidden goods menu 2026-10-26 16:24:26 +08:00
WuYongJie
5abc141b03 test(supply-chain): complete typed ERP page mocks 2026-10-26 16:24:26 +08:00
WuYongJie
17c10b0863 fix(intake): follow current product purchase owner permissions 2026-10-26 16:17:59 +08:00
WuYongJie
3e487baa0f fix(supply-chain): handle actual ERP agreement and QR login state 2026-10-26 16:07:07 +08:00
居居
5905923527 fix(deploy): 发布时安全写入聚水潭登录凭据 2026-10-26 16:06:28 +08:00
居居
c9cd224684 feat(supply-chain): 支持聚水潭登录态失效后自动登录 2026-10-26 16:06:28 +08:00
居居
5874d57f00 test(supply-chain): 锁定首次导出成功即停止重试 2026-10-26 16:06:28 +08:00
居居
b7cf1e25ae feat(supply-chain): 单次导出重试三次后再分批 2026-10-26 16:06:28 +08:00
WuYongJie
b65051cf8a fix(supply-chain): load Next env through native ESM default import 2026-10-26 14:32:32 +08:00
WuYongJie
f08ac565cd fix(supply-chain): continue historical monitoring on empty notice dates
(cherry picked from commit c137661049b733449be4b21983ed511d1e0773e5)
2026-10-26 14:19:43 +08:00
居居
bd44175734 fix(supply-chain): 加强每日同步异常与并发保护
(cherry picked from commit 2b985b657f1faa4d26fabbe746a47c0ad8d50923)
2026-10-26 14:19:43 +08:00
居居
4350394e81 fix(supply-chain): 恢复完整链接导出并阻止快照覆盖
(cherry picked from commit e4b15fa61527c5dbef753676274265f2b9e57d80)
2026-10-26 14:19:42 +08:00
居居
05df5da4e0 feat(supply-chain): 提供服务器每日调度与重跑保护
(cherry picked from commit 1eb6ebd387a84e2a57b51f9ed798fc67238e24bb)
2026-10-26 14:19:42 +08:00
WuYongJie
121e213fc1 fix(market-analysis): use unified workbench layout 2026-10-26 11:56:19 +08:00
WuYongJie
b152c821af fix(supply-chain): 恢复前置仓列设置拖拽排序 2026-10-26 11:34:51 +08:00
apple
bba34df1ab feat(supply-chain): 过滤下单表中的双零明细 2026-10-26 11:14:13 +08:00
apple
20b09c6b12 fix(supply-chain): 同步更新下单重量汇总 2026-10-26 11:14:13 +08:00
apple
35c226a783 feat(supply-chain): 新增前置仓明细列设置 2026-10-26 11:14:12 +08:00
apple
f232e1adab feat(supply-chain): 精简前置仓页面并新增下单重量 2026-10-26 11:14:12 +08:00
apple
733b98792e fix(supply-chain): 导入后立即覆盖并展示反馈数据 2026-10-26 11:14:12 +08:00
apple
572888f688 feat(supply-chain): 新增批量清空字段弹窗 2026-10-26 11:14:11 +08:00
apple
b6937fc9cf fix(supply-chain): 刷新数据时恢复最终下单量计算值 2026-10-26 11:14:11 +08:00
apple
461e6866d4 fix(supply-chain): 限制采购账号并锁定下单表最终下单量 2026-10-26 11:14:11 +08:00
apple
57c366b2a0 feat(supply-chain): 支持最终下单量人工编辑 2026-10-26 11:14:10 +08:00
apple
58d217cbc9 fix(supply-chain): 将前置仓下单表调整为进销存子页面 2026-10-26 11:14:10 +08:00
apple
ea76449f3c feat(supply-chain): 新增前置仓下单表快照推送 2026-10-26 11:14:10 +08:00
apple
d9f3b5ae1e feat(supply-chain): 支持前置仓批量筛选与反馈默认值 2026-10-26 11:14:09 +08:00
apple
5aa915f477 feat(supply-chain): 限制采购视角与导出字段 2026-10-26 11:14:09 +08:00
WuYongJie
6c874a67db feat(market-analysis): integrate and modularize selection dashboard 2026-10-26 10:59:31 +08:00
居居
3d5c23ace9 feat(supply-chain): 使用聚水潭最新替换规则快照 2026-10-26 09:50:02 +08:00
居居
2702a501f4 fix(supply-chain): 重复商品按最新PLM记录取剩余库存 2026-10-26 09:50:02 +08:00
居居
b57346a5ae feat(supply-chain): 接入聚水潭替换规则并展开条码 2026-10-26 09:50:02 +08:00
居居
ffabb6af88 fix(supply-chain): 替换规则支持同货号并展开条码 2026-10-26 09:50:02 +08:00
居居
5d541e92dc fix(supply-chain): 默认一次性查询聚水潭链接 2026-10-26 09:50:01 +08:00
居居
38d6642833 fix(supply-chain): 分批同步聚水潭链接并增强重试 2026-10-26 09:50:01 +08:00
居居
3b5560adb6 feat(supply-chain): 调整停产下架查看与负责人数据范围 2026-10-26 09:50:01 +08:00
居居
8781c40461 feat(supply-chain): 配置停产下架页签权限与运营数据范围 2026-10-26 09:50:01 +08:00
居居
00227f524d feat(supply-chain): 完成停产下架管理与监控 2026-10-26 09:50:01 +08:00
WuYongJie
c6310a9912 fix(product-archive): retire historical owner refresh and prioritize intake 2026-10-26 09:43:37 +08:00
apple
1b66b87d19 feat(production-stop-demands): 增加分页页大小并放宽原因分类权限 2026-10-24 15:03:08 +08:00
WuYongJie
ab408b90a0 feat(intake): 产品定厂仓库必填改为样包测量门禁
产品定厂确认不再强制仓库必填,允许空仓库确认并进入条码打印;样包测量确认前校验定厂仓库已填,未填提示「定厂仓库未填写」。产品开发待办规则口径同步调整。
2026-10-24 14:22:43 +08:00
WuYongJie
1646e6c4ee feat(supply-chain): 导出字段弹窗分组级全选/全不选切换 2026-10-24 14:03:13 +08:00
WuYongJie
5fc195d9fe feat(supply-chain): 产品全量档案导出字段勾选与图片URL导出修复 2026-10-24 13:40:30 +08:00
WuYongJie
c0e7a8d606 feat(supply-chain): add authenticated SPU return address push API 2026-10-24 13:30:41 +08:00
apple
5feb69feff feat(production-stop-demands): 完善流程校验、删除权限与分析视图 2026-10-24 13:14:38 +08:00
WuYongJie
83fb538d8c fix(product-archive): preserve existing images when sync source is empty 2026-10-24 11:31:23 +08:00
WuYongJie
26a6a28aa0 fix(supply-chain): include no-sales SPUs in all dropship issues
(cherry picked from commit 9d32b57e970905fdee7b3194ceb9df4894eb517e)
(cherry picked from commit ac885932d082a371ddb16d982a8acdea6dc28588)
2026-10-24 11:21:19 +08:00
WuYongJie
3d7c519b3c fix(intake): 产品开发待办节点确认回退批次级确认记录
待办规则此前只读产品级 documentWorkspaceConfirmations/documentWorkspaceApprovedAt,与产品开发明细页面读取批次级 skuBatchWorkflow* 的兜底口径不一致,历史数据两份记录不同步时(如 GBSFY-LRSF0091 样包测量批次级已确认、产品级为 false)会每天误报节点未完成。规则层 nodeApproved/approvedAt 增加批次级记录回退(任一批次已确认且带可解析确认时间即视为节点已确认,激活锚点同取批次级时间),取数组装传入批次级确认映射。生产真实数据对照:消除 14 条误报候选(样包测量 6、基础信息相关 8),恢复 8 条此前漏推的提醒。
2026-10-24 11:18:39 +08:00
WuYongJie
e5d4db5127 feat(ai-render): manage independent SPU SKU models and deletion 2026-10-24 10:25:24 +08:00
WuYongJie
005b8cf535 feat(navigation): rename AI tools and add Yiyidian link 2026-10-24 10:25:23 +08:00
WuYongJie
4f076c3b67 fix(distribution): separate daily job lock from archive writers 2026-10-24 10:05:48 +08:00
WuYongJie
6b221e0db4 perf(distribution): use indexed scope checks for dependency groups 2026-10-24 09:49:25 +08:00
WuYongJie
87cf63745d perf(distribution): publish incremental snapshots with durable checkpoints 2026-10-24 09:18:01 +08:00
WuYongJie
4b16861dd1 fix(diy): place dimension annotations outside the cabinet 2026-10-23 17:29:27 +08:00
WuYongJie
69599e1d60 feat(diy): allow barcode overrides with admin audit history 2026-10-23 16:03:32 +08:00
WuYongJie
3730b8cbd8 feat(mobile): simplify product and supply workspaces and unify navigation 2026-10-23 14:44:14 +08:00
WuYongJie
3c4706019c feat(diy): add brand barcode summaries and product dimensions 2026-10-23 14:08:55 +08:00
apple
fbcc5bf26a fix(supply-chain): 前置仓默认展示满足选品数据 2026-10-23 14:00:11 +08:00
WuYongJie
3b715e79b9 feat(production-stop-demands): 新增停产需求明细导出(全字段/自选字段)
- 导出使用停产需求管理的「下载」权限,未授权时不显示导出入口

- 支持全字段导出和点选字段导出当前筛选结果

- 新增导出接口与共享筛选参数解析,单次最多 20000 条
2026-10-23 11:51:24 +08:00
WuYongJie
530b9631fa fix(intake): keep date filter hints and values consistent across browser locales 2026-10-23 11:32:30 +08:00
WuYongJie
e1fba9a533 fix(intake): compact filters and unify workspace typography 2026-10-23 10:19:56 +08:00
WuYongJie
b04b196155 fix(print): stabilize Sofia carton label size and preview 2026-10-23 10:07:46 +08:00
WuYongJie
6c494c72e0 fix(supply-chain): complete front warehouse import reports and local layout 2026-10-23 09:31:42 +08:00
apple
a68d80e59e fix(supply-chain): 修复前置仓移仓与导入反馈 2026-10-23 09:31:42 +08:00
apple
902cb3613f feat(supply-chain): 新增前置仓进销存管理 2026-10-23 09:31:42 +08:00
WuYongJie
3321f0fcdd fix(supply-chain): refine distribution count display 2026-10-23 09:13:30 +08:00
WuYongJie
11c9ea93ba fix(workbench): unify account operation logs and show scoped records 2026-10-22 16:17:12 +08:00
WuYongJie
840ef0c319 feat(supply-chain): export all unselected supplier products
Isolated integration of c9370e6c53f64e0078d839d75f5a5ae7a0efb33a; approved task supplier-recommended-unselected-export-20261022.
2026-10-22 15:36:50 +08:00
WuYongJie
304514ee13 fix(supply-chain): remove excess inventory table bottom spacing 2026-10-22 14:10:32 +08:00
WuYongJie
2a94cebb14 perf(supply-chain): publish paged distribution summaries and online snapshot exports 2026-10-22 12:44:45 +08:00
WuYongJie
08e1a4764f test(workbench): align account tools with accepted product layout
(cherry picked from commit e665ed6c53543ca3447162ce1082f119dd680f46)
(cherry picked from commit b8792bd18857b0391c1fb6659f61cab59f2f2cc2)
2026-10-22 11:10:10 +08:00
WuYongJie
f5f27f44b4 feat(workbench): unify workspace navigation, surfaces and tab scrolling
(cherry picked from commit 8beecc67ea3df76bbb97cca01400bc67c5dbd08c)
(cherry picked from commit 65428ffa0df6a04d7997af46acd6600698e2dd28)
2026-10-22 11:03:33 +08:00
WuYongJie
dc6c3a74ee fix(product-archive): classify DF products as dropship without warehouse fallback 2026-10-22 10:42:47 +08:00
WuYongJie
142bafaa34 feat(intake): allow editing quality notification content 2026-10-22 09:09:21 +08:00
WuYongJie
bf6cdf8ef3 perf(product-archive): batch source reads and writes with resumable sync 2026-10-22 08:57:56 +08:00
WuYongJie
bc17baba42 fix(supply-chain): protect the 81 limit-file manual prices
Preserve manual prices only for the exact 586cf718 batch and the limit-file provenance marker. Keep other styles and sources on the existing upstream sync rules. Retain timestamp precision and explain manual precedence in sync exports. Add focused scope and preservation regression checks.
2026-10-21 18:22:16 +08:00
居居
975efc6dfb feat(supply-chain): 按限价文件更新大货和品牌商品限价
命中 81 个货号并统一人工指定为 49,组合关系与组合逻辑不变。
2026-10-21 18:18:43 +08:00
WuYongJie
a6f097da3d fix(supply-chain): restore original SJ0380 limit prices
Compensate the authorized 48186a9f / 925f5b2e price-49 release using its pre-release production snapshot. Restore 74 prices and approval sources, remove the added manual sync protection, retain original migration history and all later distribution features. No limit upload interface or API.
2026-10-21 18:01:53 +08:00
WuYongJie
7f190d3e77 feat(supply-chain): publish distribution snapshots, combo coverage and archive links 2026-10-21 17:21:59 +08:00
WuYongJie
925f5b2e6b fix(supply-chain): pin SJ0380 limit prices to 49
Apply the final authorized net change from 17a56eca and 48186a9f21fda9c0ddeb6baed2be3846ca0297ac. Includes the 74-code data migration and manual price sync protection. The temporary upload UI, API, schema and migration are excluded.
2026-10-21 17:07:05 +08:00
WuYongJie
772ee85cb4 fix(intake): include business roles in owner candidates
(cherry picked from commit 3c7d69250efe05da878cfbe7ae92c85b44fd8b50)
(cherry picked from commit eb560236a941f7a1426b638ccf17b149c1199716)
2026-10-21 16:10:19 +08:00
WuYongJie
7b142eb04a fix(supply-chain): normalize marketing sales barcode matching
Normalize case and surrounding spaces during source aggregation and archive matching, while preserving stored barcode values and existing daily sales calculations.
2026-10-21 15:19:01 +08:00
WuYongJie
599273a46e feat(home): default to centered brand and workspace capsules 2026-10-21 15:07:18 +08:00
WuYongJie
170ca52dac fix(supply-chain): retry transient product archive enrichment reads
Reconnect and retry failed source reads after 30 seconds, 2 minutes and 5 minutes. Keep the current batch cursor, avoid repeated archive and audit writes, wait for concurrent reads before cleanup, and preserve permanent errors and the existing failure notification.
2026-10-21 14:14:07 +08:00
WuYongJie
50c2d44571 fix(production-stop-demands): 恢复盘停建议配置主窗口操作反馈
- 查询、新增、删除、导出、导入的错误/成功提示恢复到配置主窗口

- 新增/删除/导出的成功提示改为刷新后写入,避免被查询清空

- 保留独立批量导入弹窗

(cherry picked from commit 6226a4076d6352323f6342754d539970e7282e24)
2026-10-21 11:50:22 +08:00
WuYongJie
244a9f8b85 fix(production-stop-demands): 修复盘停建议大批量导入并完善配置权限
- 盘停建议导入上限提升到 5MB / 20000 行,修复数千行被拒与静默截断

- 修复条码解析把「保留:」并入条码的问题,前后端口径一致

- 批量 upsert 改为单条 SQL,5867 行导入约 0.1 秒

- 批量导入界面改为独立弹窗

- 原因分类配置仅系统管理员可维护

(cherry picked from commit f314d7802090c18ccc2b94d2e23529a4ffb89bfa)
2026-10-21 11:50:21 +08:00
居居
1c1efebf27 fix(supply-chain): 清空停产退市商品限价
(cherry picked from commit 42b374801df5926aba8f302af518a538e2baede1)
2026-10-20 17:24:25 +08:00
WuYongJie
0509df6408 fix(installation-demo): synchronize clockwise screw and tool playback 2026-10-20 15:08:37 +08:00
居居
89eb66c6f6 feat(supply-chain): 限价筛选条件支持自定义 2026-10-20 14:51:20 +08:00
居居
7ce4b93706 feat(supply-chain): 限价顶部新增编码筛选 2026-10-20 14:51:20 +08:00
WuYongJie
75ba586ac8 feat(production-stop-demands): 完善档案字段、筛选、视图与通知中心联动
- 明细新增图片、产品标签、含大单近30日日销(条码/SKU 数值快照)

- 表头漏斗筛选(模糊/多选/数值比较/日期区间)、批量搜索、筛选设置

- 列设置拖动排序、快捷查询保存与套用

- 视图统计:日/周/月趋势、部门/需求人/原因/状态分布、饼图与明细下钻

- 通知中心:停产确认/待走流程/需求驳回通知、待办快照同步、钉钉全屏链接

- 新增迁移与历史数据回填脚本
2026-10-20 14:32:41 +08:00
居居
c34b72304d feat(supply-chain): 新增组合限价导出 2026-10-20 13:10:28 +08:00
居居
3cc85ed5d2 feat(supply-chain): 精简破价订单筛选条件 2026-10-20 13:10:28 +08:00
居居
8153f284e5 feat(supply-chain): 品牌页纳入三类国标前缀 2026-10-20 13:10:28 +08:00
WuYongJie
1614bd5e95 feat(diy): add text replacement and template URL import
Add image text detection/replacement with original preservation, compact result actions, and stored template imports. Keep image editor and template layout dialogs at consistent viewport sizes with internal scrolling.
2026-10-20 12:12:53 +08:00
WuYongJie
cadb917d90 feat(diy): add image adjustment, resizing and template layout tools 2026-10-20 09:43:40 +08:00
WuYongJie
09f36608de Use furniture Z-up axes in installation demo editing
Map X to width, Y to depth, and Z to height in numeric movement, drag and rotation controls, and feedback. Preserve existing Y-up model poses and saved data, and extract transform-anchor helpers.
2026-10-19 17:55:11 +08:00
WuYongJie
cba68a8cc7 fix(print): allow C-Lodop embedded preview frames 2026-10-19 17:07:48 +08:00
WuYongJie
414b86fd56 fix(home): open saved websites in a new tab 2026-10-19 16:46:53 +08:00
WuYongJie
df253a470d Optimize installation model preview and repair exploded layouts
Parse and cache OBJ geometry off the main thread, reuse the viewport, and adapt interaction rendering. Pull drawers out as complete groups, enforce panel and hardware clearance, and frame the applied layout. Add relative X/Y/Z movement in millimeters with existing persistence.
2026-10-19 16:44:30 +08:00
居居
4e949067d3 fix(supply-chain): 仅在品牌页按GBSFY归类 2026-10-19 16:22:34 +08:00
居居
8aa17b7a34 fix(supply-chain): 按GBSFY前缀纠正索菲亚品牌 2026-10-19 16:22:34 +08:00
居居
d5b5a60141 feat(supply-chain): 限价列表按负责人限价降序 2026-10-19 16:22:33 +08:00
WuYongJie
ef4e4e36ed feat(supply-chain): 商品库存快照落库并改读快照
- 新增 ProductInventorySnapshot / ProductInventorySnapshotRun 模型与正式迁移(条码×实际仓库)
- 同步任务复用现有库存/云仓/档案读取,临时表+事务原子替换,advisory lock 防重叠,失败保留上一份快照
- 新增 15 分钟 systemd timer 安装脚本与同步脚本(身份校验、凭据走环境变量)
- 列表/导出接口改读快照并展示 syncedAt,保留全零行过滤与导出字段选择
2026-10-19 15:40:57 +08:00
WuYongJie
5cf84a19c8 Merge latest main into accepted homepage release 2026-10-19 15:17:01 +08:00
WuYongJie
7210dd2111 fix(home): render homepage without the product sidebar 2026-10-19 15:12:29 +08:00
WuYongJie
071cd23ac9 feat(home): add fan carousel and double-click overview 2026-10-19 15:12:29 +08:00
居居
211aff2578 feat(supply-chain): 查询框直接支持批量筛选 2026-10-19 15:07:49 +08:00
居居
c691669101 feat(supply-chain): 限价筛选支持批量查询 2026-10-19 15:07:49 +08:00
居居
2b4eb17ec7 fix(supply-chain): 排除停产退市组合限价 2026-10-19 15:07:49 +08:00
居居
c62d951228 fix(supply-chain): 限价筛选候选值联动 2026-10-19 15:07:49 +08:00
居居
da4b77e6d6 style(supply-chain): 对齐表头列宽拖拽热区 2026-10-19 15:07:49 +08:00
居居
9360c553a2 fix(supply-chain): 修复组合单行表头遮挡明细 2026-10-19 15:07:49 +08:00
居居
a843b643ed fix(supply-chain): 放行组合运营负责人筛选 2026-10-19 15:07:49 +08:00
居居
05e1f94e46 fix(supply-chain): 修复组合限价筛选交互 2026-10-19 15:07:49 +08:00
居居
de26559e3e fix(supply-chain): 完善组合限价筛选与运营负责人 2026-10-19 15:07:49 +08:00
居居
c2f005712b feat(supply-chain): 新增组合限价页 2026-10-19 15:07:49 +08:00
居居
177d11dd47 style(supply-chain): 列名「非负责人限价(新)」改为「非负责人限价」
- 限价明细页与产品全量档案页的限价分组同步改名(导出列头随列定义一起生效)
2026-10-19 15:07:49 +08:00
居居
c104a029c9 fix(supply-chain): 限价明细「负责人限价」筛选补上候选值与筛选逻辑
- 负责人限价是 SupplyChainStyleLimitPrice.ownerLimitPrice 的派生列(按货号),此前 option 接口没有对应分支,
  下拉框显示「没有可选值」、选了也不生效
- 候选值:无其他筛选时取限价表的全部去重价格(两位小数,与界面一致);有筛选时只取当前结果集里货号的限价;
  无流程限价的商品提供「空值」选项
- 筛选:选中的价格先经 ROUND(price,2) 换回货号再按 sku 过滤;选中的价格无对应货号返回空结果;
  「空值」= 货号不在限价表里
- 补 3 条单测(价格→货号、无匹配、空值)
2026-10-19 15:07:49 +08:00
WuYongJie
ee983673d5 feat(supply-chain): 产品全量档案新增安装方式列(定厂优先+商品标签推导) 2026-10-19 12:06:41 +08:00
居居
132331733e feat(supply-chain): 限价流程同步改为服务器端可运行 + systemd 每日定时器
- sync-limit-workflows.mjs 路径参数化:新增 --out-dir/--initial-dir/--env-file/--skip-excel,
  环境变量 LIMIT_SYNC_OUT_DIR / LIMIT_SYNC_INITIAL_DIR / LIMIT_SYNC_ENV_FILE / LIMIT_SYNC_SKIP_EXCEL / LIMIT_SYNC_HISTORY_KEEP;
  默认目录改为应用根目录,不再写死 Mac 桌面
- 期初数据:优先显式目录,其次仓库自带 scripts/data/limit-sync/期初数据(首次部署可直接使用),
  服务器上业务更新只需把新的「限价期初数据*.xlsx」放进 shared 目录
- 写库安全:校验 DATABASE_URL、打印目标库(不含密码)、DELETE+INSERT 包在单事务里,
  失败自动回滚保留上一版限价/授权;Excel 导出失败不影响已提交的库数据
- 输出治理:输出目录自动创建;历史版本默认保留最近 20 份,避免服务器磁盘每天累积 30MB
- 新增 run-limit-workflow-sync-production.cjs:预检 shared/.env 变量(DATABASE_URL / JST_REPORT_DB_*)、
  打印库目标、透传参数、30 分钟硬超时、失败写 job 日志
- 新增 install-limit-workflow-sync-timer.sh:照 marketing-sales 样板安装 systemd service+timer,
  EnvironmentFile=$BASE/shared/.env,默认每天北京时间 07:30,输出/期初数据放 $BASE/shared/limit-workflow-sync
- 文档补充「正式站同步」章节;顺带修正 supply-chain-page-permissions 测试的菜单页数(新增限价管理后 27 页)
- 未改动 20261014/20261015 三个迁移目录(正式库已按现名建表)
2026-10-19 10:13:34 +08:00
WuYongJie
1b849b0209 feat(supply-chain): 新增商品库存管理(库存/云仓/筛选/导出/快捷查询)
- 页面:成品库存 × 实际仓库,按条码匹配产品档案与仓库配置
- 服务:聚水潭成品库存全量读取与缓存、筛选/排序/分页、小计、Excel 导出
- 云仓:链接服务器 OPENQUERY 汇总 order_able_qty,支持云仓明细下钻
- 接入:菜单、标签页、权限节点 productInventory
2026-10-19 09:54:57 +08:00
WuYongJie
d8b5006e44 feat(diy): add permanent deletion for saved schemes
(cherry picked from commit 2d1486b64c5f7dcd3a3a1e247ebe0c6d12e41d21)
2026-10-19 09:50:58 +08:00
居居
7fc43502fd fix(supply-chain): 修复限价管理合并入 mainline 后的 18 处类型错误(type-check 0 错误)
提交自身的错误:
- breach-orders/route.ts:filtersSchema 去掉重复的 platform / shopName 键(保留带长度上限的一份)
- supply-chain-breach-order.service.ts:BreachOrderListFilters 去掉重复声明的 platform / shopName
- BreachOrderTab.tsx:MultiFilterChip 的 field 联合类型补齐店铺款式编码/店铺商品编码/申诉原因/平台/店铺(与 filter-options 接口支持的字段一致)
- breach-orders/[id]/route.ts 与 breach-orders/route.ts:多结果分支的处理器统一声明为 handleRequest<unknown>(…): Promise<NextResponse<unknown>>,
  与既有 reference-image-estimate 路由同一写法,解决 ctx.user 可选态与返回形状不匹配

补齐缺失的配套改动:
- errors.ts:新增 ErrorCode.RESOURCE_NOT_FOUND(含 ERROR_MESSAGES 与 HTTP 404 映射)
- ProductArchiveFilters:补 materialLevel / inspectionReport 两个筛选项类型(查询逻辑此前已随功能提交)
- bom-costing-multimodal-gateway.ts:BomCostingMultimodalOperation 增加 IMAGE_EXTRACTION(破价申诉凭证识别用),
  breach-evidence.service 对可空的 providerConfig 加空值保护
- supply-chain-product-archive-select.ts:去掉重复的 sabcRating 选择项
- supply-chain-product-archive.repository.ts:storageType 候选值已在专用分支处理,去掉恒为 false 的重复判断

验证:git 干净检出后 npm run type-check(next typegen + tsc --noEmit)0 错误;
breach/supply-chain/permission 相关 230 个测试套件 1650 用例通过,失败 7 个套件均为 mainline 原有问题。
2026-10-19 09:23:04 +08:00
居居
62452b6343 fix(supply-chain): 检测报告按 SPU 文本里的编码匹配(平台 SPU 是自由文本)
- 平台 spu 形如「SJ0380书架 / 顾家穿衣镜DS5154 / BJ0056」,与档案 SPU 编码不一致;
  解析时从 spu 与报告标题里提取编码(SJ0380、GBGJ-SJZ0021、DS5154…)再匹配,纯编码仍原样收下
- 通用报告(「茶几通用」等)不产出 SPU;无文件记录继续忽略
- 单测补自由文本/标题提取用例
2026-10-19 09:23:04 +08:00
居居
2fde21f5da feat(supply-chain): 限价明细「是否有检测报告」接入素材平台检测报告接口
- 新增检测报告索引服务:GET {素材平台}/api/external/certificate-reports(X-API-Key),只认有报告文件的记录,按 SPU 汇总,进程内缓存 10 分钟
- 列取自 external 接口:限价页数据接口与「导出明细(含图片)」都带上 inspectionReport(有 / 无 / 空)
- 表头筛选接入:候选值「有 / 无」,筛选按报告 SPU 清单(接口未配置时不加条件、候选值为空)
- 配置:MATERIAL_LIBRARY_CERTIFICATE_REPORT_API_BASE_URL/API_KEY,不填复用 MATERIAL_LIBRARY_WHITE_BACKGROUND_API_*;未配置时该列显示为空
- 增补单测(解析/缓存/未配置/接口报错)与文档说明
2026-10-19 09:23:04 +08:00
居居
eca1d63842 feat(supply-chain): 限价管理的查看/导出/申诉权限统一由管理员配置
- 查看:页面与数据仍走「限价管理 · 可见」;导出改按「限价管理 · 下载」,申诉/审核/生成/导入改按「限价管理 · 操作」(原来是经营管理 edit)
- 服务端:下载中心 limit-export 任务与产品档案导出接口的 columnSet=limit 都按「限价管理 · 下载」鉴权
- 前端:无下载权限不展示导出入口(限价明细两个导出、破价订单导出当前筛选/乐捐明细);规则配置仍单独走 breachRuleConfig 节点
- 申诉保留兜底:破价单负责人本人即使无「操作」权限也可申诉本人订单,审核必须「操作」
- 测试:CustomTableView 补导出入口权限用例并修正过期的表头断言;文档补充权限章节
2026-10-19 09:23:04 +08:00
居居
f276164532 feat(supply-chain): 限价管理(限价明细 + 破价订单审核 + 规则配置 + 流程同步)
- 限价明细:抬头列/筛选/列宽/排序可配置,批量查询(商品编码/订单号/负责人等),筛选条件回显与一键清空
- 列取数口径:入库/代发按仓库名推导(含仓库配置覆盖)、产品标签取商品SABC打标、材质等级从「材质」识别 E0/E1/ENF
- 导出:导出明细 / 导出明细(含图片)改为全局下载中心后台生成,进度/取消/保存都在下载中心;页签名与文件名一致
- 破价订单审核:按天增量生成、链接级与订单级申诉、审核通过/驳回/二次申诉冲销乐捐、豁免(同价/活动)与证据凭证要求
- 规则配置页与权限:管理员开通后可编辑(权限节点 breachRuleConfig),无权限不可见
- 限价流程同步脚本:4 类钉钉限价流程 + 产品限价页面已确认限价 + 限价期初数据;流程/页面/期初优先级与「期初更新日期更新则以期初为准」
- 非负责人限价统一按最新口径派生(×1.05 且不低于 +10 元)
- 迁移:breach order review / breach rule config / limit flow sync
- 与 mainline 合并:保留字段权限(blockedFields)、负责人在职接管、限价目标 limitManagement 等新特性;
  规则配置权限并入新的权限节点结构,档案列去掉重复的 opsPriceLimit 列,相关测试断言按当前口径更新
2026-10-19 09:23:03 +08:00
WuYongJie
399fd8182d refactor(diy): simplify canvas controls and unify capsule styling
(cherry picked from commit 1429b870f08b34e37b02c3077053f17385e9738a)
2026-10-16 18:04:54 +08:00
WuYongJie
5c899b4705 revert: 限价管理两个提交(类型检查不通过,待功能线程修复后重新提交) 2026-10-16 17:55:56 +08:00
居居
fbe276d4f6 feat(supply-chain): 限价管理的查看/导出/申诉权限统一由管理员配置
- 查看:页面与数据仍走「限价管理 · 可见」;导出改按「限价管理 · 下载」,申诉/审核/生成/导入改按「限价管理 · 操作」(原来是经营管理 edit)
- 服务端:下载中心 limit-export 任务与产品档案导出接口的 columnSet=limit 都按「限价管理 · 下载」鉴权
- 前端:无下载权限不展示导出入口(限价明细两个导出、破价订单导出当前筛选/乐捐明细);规则配置仍单独走 breachRuleConfig 节点
- 申诉保留兜底:破价单负责人本人即使无「操作」权限也可申诉本人订单,审核必须「操作」
- 测试:CustomTableView 补导出入口权限用例并修正过期的表头断言;文档补充权限章节
2026-10-16 17:46:01 +08:00
居居
4be4bd1e09 feat(supply-chain): 限价管理(限价明细 + 破价订单审核 + 规则配置 + 流程同步)
- 限价明细:抬头列/筛选/列宽/排序可配置,批量查询(商品编码/订单号/负责人等),筛选条件回显与一键清空
- 列取数口径:入库/代发按仓库名推导(含仓库配置覆盖)、产品标签取商品SABC打标、材质等级从「材质」识别 E0/E1/ENF
- 导出:导出明细 / 导出明细(含图片)改为全局下载中心后台生成,进度/取消/保存都在下载中心;页签名与文件名一致
- 破价订单审核:按天增量生成、链接级与订单级申诉、审核通过/驳回/二次申诉冲销乐捐、豁免(同价/活动)与证据凭证要求
- 规则配置页与权限:管理员开通后可编辑(权限节点 breachRuleConfig),无权限不可见
- 限价流程同步脚本:4 类钉钉限价流程 + 产品限价页面已确认限价 + 限价期初数据;流程/页面/期初优先级与「期初更新日期更新则以期初为准」
- 非负责人限价统一按最新口径派生(×1.05 且不低于 +10 元)
- 迁移:breach order review / breach rule config / limit flow sync
- 与 mainline 合并:保留字段权限(blockedFields)、负责人在职接管、限价目标 limitManagement 等新特性;
  规则配置权限并入新的权限节点结构,档案列去掉重复的 opsPriceLimit 列,相关测试断言按当前口径更新
2026-10-16 17:46:01 +08:00
WuYongJie
bc8a4a15e9 feat(intake): notify quality inspectors and keep product photo uploads available 2026-10-16 17:28:45 +08:00
WuYongJie
77c91bd689 fix(supply-chain): 产品全量档案 DF 条码成本价改为统一取 jst_sku.cost_price
- enrichment 价格补丁不再区分 DF/非 DF:purchasePrice 与 costPrice 均取 rpt.jst_sku(purchase_price / cost_price)
- 删除 dws_sn_sales_full_info_di.goods_cost 取价链路(dws-sales-goods-cost.repository.ts 整删,DWS_SALES_GOODS_COST 来源常量移除)
- 单测与《供应链产品全量档案字段来源对照表》同步更新

预期效果(部署后下一轮同步):约 1874 条 DF 成本价由空变有值、约 230 条由 dws 金额口径变为 jst 单价口径、约 4516 条数值不变、0 条丢值。
2026-10-16 16:27:31 +08:00
WuYongJie
4bd48d8bc0 fix(diy): keep wood grain continuous and compact inspector
(cherry picked from commit 9af20a14b07f6da46691a7356c5fd1d08aa48887)
2026-10-16 14:47:27 +08:00
WuYongJie
d8e43e89ba fix(supply-chain): 仓库管理列表只取聚水潭仓库档案,档案历史名称不再补充展示 2026-10-16 14:00:51 +08:00
WuYongJie
12f04a2f96 feat(supply-chain): 产品全量档案移除「数据字典」占位入口
数据字典属内部开发口径,不对外暴露;「更多」菜单仅保留使用说明,清理占位提示状态;使用说明对应条目同步。
2026-10-16 13:56:48 +08:00
Codex
47eed75bca feat(supply-chain): 仓库管理模块全量源码(筛选排序/导入导出/属性覆盖/负责人对齐)
- 表头:除操作列外全列筛选下拉 + 升/降序排序(服务端排序,空值恒排末尾)
- 导入导出:模板含仓库名称/仓库属性/仓库归属/负责人/入库代发;导出支持字段勾选、跟随筛选与排序;导入弹窗(下载模板+选择文件)与进度面板,导入后可一键「仅查看导入的仓库」
- 仓库属性人工覆盖表(新增迁移 20261016103000)与单元格内联编辑
- 仓库负责人:工厂仓取产品档案采购,档案读取走只读库(回退主库)
- 菜单/页签/页面标题统一为「仓库管理」
2026-10-16 13:32:52 +08:00
WuYongJie
0ca4e4f8f1 fix(supply-chain): 产品全量档案导入入口补挂「操作」权限门控 2026-10-16 13:14:36 +08:00
WuYongJie
a54d32941f fix(diy): fit transparent basket patterns with five millimeter clearance 2026-10-16 12:31:53 +08:00
WuYongJie
559d025e6d feat(users): 字段权限集中配置——产品全量档案与店铺账号管理 2026-10-16 11:29:50 +08:00
居居
141ac4654c fix(supply-chain): 创建天数正好 90 天归入「创建60-90天」
- 新品区间分布边界修正:90 天整算 60-90 天,91 天起才是「创建90天以上」
- 读取已生成月份快照时按快照里的创建天数重算该字段,旧月份无需重新核算即可按新口径显示
2026-10-16 11:20:24 +08:00
WuYongJie
9465c98702 feat(product-archive): add batch code filters and compact sales controls 2026-10-15 17:51:10 +08:00
居居
11fd22f356 feat(supply-chain): 产品中心/采购中心不再依赖运营负责人
- 有设计师即计入产品中心、有采购即计入采购中心,即使该行没有运营负责人
- 运营中心仍只统计有运营负责人的行;「山东项目组」设计师/采购继续排除
- 快照增加口径版本号:读到旧口径快照时先按实时口径出数,并在后台自动重建该月快照(无需手动点保存配置)
2026-10-15 17:20:10 +08:00
居居
fc862c80fc feat(supply-chain): 各部门数据分类支持按事业部导出,事业部按一/二/三/四部排序
- 运营中心卡片「导出」改为先勾选事业部(默认全选;页面已筛事业部时默认只选它),只导出勾选范围
- 导出接口本已支持 department 过滤,前端补上多选入口(后端无需改动)
- 事业部/品类部名称排序改为按序号解析:事业一部→二部→三部→四部,品类九部→十部→十一部,不再按拼音乱序
- 新增部门名称排序工具与单测
2026-10-15 17:20:10 +08:00
WuYongJie
bcfd191657 fix(diy): add generated image deletion 2026-10-15 16:26:10 +08:00
WuYongJie
a9635163bc Merge shared mainline for Relay native image generation
Preserve the five existing production owner fixes and apply the reviewed AI task migration. Resolve DIY add/add conflicts only after checking that the feature baseline and production versions were identical. Merged AI tests and type-check passed; the retained production write guards were also checked in production mode.
2026-10-15 15:10:10 +08:00
WuYongJie
928a7b37a9 feat(ai): use durable Relay native image tasks
Persist image submissions with fixed idempotency keys, leased execution, provider
identity, reference snapshots and staged results. Integrate DIY, assisted assembly
and non-integrated white-background sessions without holding generation in a
browser request. Recover ambiguous submissions through owner-scoped reads and
stop a new eight-view batch when restoring an earlier request.

Add the NativeImageGenerationJob migration and a dedicated worker to both release
entry points. Protect job inputs/results from storage GC. Existing production
configuration and business data are not changed by this commit.

Validation: 256 focused Jest tests, isolated PostgreSQL migration/lease/transaction
checks (8), deployment guard tests (9), type-check, production build and desktop/
mobile browser component scenarios. Live native 2K reference-image request worked;
reusing its idempotency key returned the same task. Build uses local placeholders
for otherwise absent Drawing System configuration. Three unrelated material tests
also fail with the original mainline test file; they remain outside this change.

Architecture/visual/root-cause review: passed with warnings. Independent review
completed for the integration and shared input/storage paths; worker additionally
reviewed by the primary agent. Production merge/deployment requires authorization.
2026-10-15 14:55:46 +08:00
WuYongJie
8e14aa5fb1 feat(diy): add shared assets, uploaded styles and AI rendering 2026-10-15 11:36:32 +08:00
WuYongJie
dc2c86275e feat(diy): add shared assets, uploaded styles and AI rendering 2026-10-15 11:35:34 +08:00
WuYongJie
e0c324ce06 feat(supply-chain): 全量档案清除筛选移入筛选行并优化筛选交互(设置抽屉/回车确认)
- 「清除 N」从工具栏移到筛选行尾部(设置图标旁),收起细条保留清除入口
2026-10-15 11:11:47 +08:00
WuYongJie
67541f9208 feat(supply-chain): 全量档案清除筛选移入筛选行并优化筛选交互(设置抽屉/回车确认)
- 「清除 N」从工具栏移到筛选行尾部(设置图标旁),收起细条保留清除入口
2026-10-15 11:10:50 +08:00
WuYongJie
e2a237f43b fix(product-archive): 结算价公式修正为成本价÷0.9(模块化)
- 模块化分支由 ×0.9 改为 ÷0.9:聚水潭里模块化成本价按 9 折记,
  结算价需除以 0.9 还原(如成本 34.00 → 结算 37.78)
- 非模块化仍为成本价原样展示;先取整到分再除、四舍五入保留两位
- 同步更新测试期望值(76.20→84.67、123.45→137.17、85→94.44、34.00→37.78)
2026-10-15 09:18:26 +08:00
WuYongJie
d1496b0c11 fix(product-archive): 结算价公式修正为成本价÷0.9(模块化)
- 模块化分支由 ×0.9 改为 ÷0.9:聚水潭里模块化成本价按 9 折记,
  结算价需除以 0.9 还原(如成本 34.00 → 结算 37.78)
- 非模块化仍为成本价原样展示;先取整到分再除、四舍五入保留两位
- 同步更新测试期望值(76.20→84.67、123.45→137.17、85→94.44、34.00→37.78)
2026-10-15 09:17:57 +08:00
WuYongJie
46ca3bf786 feat(product-archive): 全量档案第二个成本价列改为结算价(模块化 ×0.9)
- 列与变更记录标签:成本价(新品)→ 结算价 / 结算价(新品),表头与导出同步生效
- 结算价在查询映射层实时计算:非模块化沿用成本价原样,模块化按 0.9 折
  (先取整到分再打折、四舍五入保留两位);模块化以商品标签含「模块化」判断,不落库
- 修复列设置合并时 localStorage 旧 label 覆盖默认列名的问题:内置列名
  一律以当前默认定义为准,改名对已保存列设置的用户才能生效
2026-10-15 08:48:16 +08:00
WuYongJie
67834358d2 fix(product-archive): allow trusted HTTPS image host on port 3333 2026-10-14 18:18:50 +08:00
WuYongJie
b6a326d996 fix(product-archive): allow trusted HTTPS image host on port 3333 2026-10-14 18:14:47 +08:00
WuYongJie
ec6067facc feat(supply-chain): 产品全量档案新增独立图文使用说明页(更多菜单新标签页打开)
- 新增 /product-archive-guide 独立路由:模块树目录(产品全量档案 → 1.1~1.10),无系统壳,登录+productArchive 查看权限
2026-10-14 17:45:18 +08:00
WuYongJie
e57f95c489 feat(supply-chain): 产品全量档案新增独立图文使用说明页(更多菜单新标签页打开)
- 新增 /product-archive-guide 独立路由:模块树目录(产品全量档案 → 1.1~1.10),无系统壳,登录+productArchive 查看权限
2026-10-14 17:43:53 +08:00
WuYongJie
9ef800001f feat(product-archive): 全量档案第二个成本价列改为结算价(模块化 ×0.9)
- 列与变更记录标签:成本价(新品)→ 结算价 / 结算价(新品),表头与导出同步生效
- 结算价在查询映射层实时计算:非模块化沿用成本价原样,模块化按 0.9 折
  (先取整到分再打折、四舍五入保留两位);模块化以商品标签含「模块化」判断,不落库
- 修复列设置合并时 localStorage 旧 label 覆盖默认列名的问题:内置列名
  一律以当前默认定义为准,改名对已保存列设置的用户才能生效
2026-10-14 17:09:30 +08:00
WuYongJie
89fbe9231b feat(supply-chain): 全量档案运营负责人切换 js_sku_info_manage.sku_manager 数据源 2026-10-14 14:28:05 +08:00
WuYongJie
289926294d Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/local-3001-integration 2026-10-14 14:27:19 +08:00
WuYongJie
9492eba22d feat(supply-chain): 全量档案运营负责人切换 js_sku_info_manage.sku_manager 数据源 2026-10-14 14:27:02 +08:00
WuYongJie
1e0a8decc1 feat(supply-chain): 日销任务改为每日 08:00 单次执行,未到数即告警并支持手动补跑 2026-10-14 12:28:40 +08:00
WuYongJie
2c4128e065 feat(supply-chain): 日销任务改为每日 08:00 单次执行,未到数即告警并支持手动补跑 2026-10-14 12:28:24 +08:00
WuYongJie
bcb0636625 fix(product-archive): display complete column headers and preserve readable widths 2026-10-14 11:55:26 +08:00
WuYongJie
822df76e19 fix(product-archive): display complete column headers and preserve readable widths 2026-10-14 11:55:00 +08:00
WuYongJie
b0d531e1af feat(owners): include growth center employees in business owner options 2026-10-14 11:33:58 +08:00
WuYongJie
bf2f114762 feat(owners): include growth center employees in business owner options 2026-10-14 11:33:22 +08:00
WuYongJie
cf11c4251e fix(supply-chain): 基础同步回退视图不存在的 item_type 列引用并修复失败告警原因为空 2026-10-14 10:38:37 +08:00
WuYongJie
7f1358aa68 fix(supply-chain): 基础同步回退视图不存在的 item_type 列引用并修复失败告警原因为空 2026-10-14 10:38:22 +08:00
WuYongJie
f84abed356 Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/local-3001-integration
# Conflicts:
#	furniture-3d-system/src/server/repositories/__tests__/product-archive-owner-workflow.repository.test.ts
#	furniture-3d-system/src/server/repositories/product-archive-owner-workflow.repository.ts
2026-10-14 10:04:53 +08:00
WuYongJie
42d2447481 feat(supply-chain): 产品全量档案负责人治理与聚水潭式表头改造 2026-10-14 10:04:06 +08:00
WuYongJie
20eaf3589c fix(diy): retry transient storage failures during catalog import 2026-10-14 09:20:00 +08:00
WuYongJie
d76d024d62 fix(storage): handle Qiniu CommonJS SDK default export for DIY import 2026-10-14 08:58:44 +08:00
WuYongJie
462fee6a46 feat(diy): persist shared catalog and assets in database and object storage 2026-10-13 09:48:09 +08:00
WuYongJie
96f0373e79 feat(diy): add cabinet workbench with saved versions and BOM exports 2026-10-12 18:12:57 +08:00
WuYongJie
f7811468f7 feat(supply-chain): 产品全量档案定时任务通知——业绩快照每日简报与四任务失败兜底 2026-10-12 14:05:06 +08:00
WuYongJie
c9c62f4b22 docs: 删除已废弃的苹果风格视觉标准(验收只按白曜石工坊口径) 2026-10-12 11:08:10 +08:00
WuYongJie
1dbcc0d84c chore(installation-demo): 移除安装演示V2试验链并保留成对迁移历史 2026-10-12 11:08:10 +08:00
WuYongJie
5ca82d1033 fix(supply-chain): keep daily sales notification fields in separate paragraphs 2026-10-12 10:46:24 +08:00
WuYongJie
81cffc020a fix(supply-chain): 日销通知改为纯文字
去掉新通知的卡片按钮字段,正文采用普通换行。
保留旧投递的幂等重试载荷,公共通知链路不变。
验证:相关25项测试、ESLint及diff检查通过。
2026-10-12 10:25:34 +08:00
WuYongJie
520706df0d feat(store-account): display exact source platforms and filter before pagination 2026-10-12 10:22:51 +08:00
WuYongJie
c4f3063e88 fix(supply-chain): 日销08点同步、失败补跑及管理员结果通知
北京时间08:00开始,未到数或失败按固定10分钟刻度补跑,成功当天停止源查询。
保留重启/PG失败记录和到数时间,正式任务定向徐霞客发送去重结果通知。
验证:31项定向测试、ESLint、全仓TypeScript、脚本语法与本地真实运行通过。
2026-10-12 10:03:13 +08:00
WuYongJie
4885e8fc75 fix(intake): calibrate Gujia 2026 certificate print positions 2026-10-12 09:21:26 +08:00
WuYongJie
0ca07f8c5e feat(product-archive): 全量档案图片自动增量索引、换图失效与失败重试 2026-10-12 08:49:10 +08:00
WuYongJie
9c2f5f6085 feat(product-archive): sync approved owner workflows daily 2026-10-10 16:13:25 +08:00
WuYongJie
a944d5eecd feat(product-archive): use marketing daily sales with daily sync and arrival monitor
Aggregate rpt.op_marketingDailySales by complete SKU and calculate weighted purchase sales.
Persist daily source fingerprints, skip unchanged archive writes and prevent old enrichment from overwriting sales.
Run daily at 09:30 Beijing time and observe source arrival every 10 minutes on 2026-10-11 08:00-09:30.
2026-10-10 11:07:23 +08:00
1469 changed files with 207867 additions and 9816 deletions

View File

@ -18,6 +18,39 @@ JST_REPORT_DB_USER=
JST_REPORT_DB_PASSWORD=
JST_REPORT_DB_ENCRYPT=true
JST_REPORT_DB_TRUST_SERVER_CERTIFICATE=true
SUPPLY_CHAIN_LIMIT_SNAPSHOT_DIR=
# 换货规则表 app.jst_sku_replace_rule 与 rpt 视图使用独立授权;留空时复用上面的主机、库和账号。
JST_SKU_REPLACE_RULE_SCHEMA=app
JST_SKU_REPLACE_RULE_DB_USER=
JST_SKU_REPLACE_RULE_DB_PASSWORD=
# 停产下架链接日快照最大导入延迟(小时);超时保留已有链接。
JST_DISCONTINUATION_LINK_MAX_AGE_HOURS=26
# 数据库链接快照仅用于诊断;低于已有链接该比例时拒绝覆盖。
JST_DM_MIN_EXISTING_COVERAGE_RATIO=0.5
# 停产下架服务器 Chrome / RPA。
JST_RPA_BROWSER_URL=http://127.0.0.1:9222
JST_RPA_PROFILE_DIR=
JST_RPA_CHROME_EXECUTABLE=
JST_RPA_HEADLESS=true
JST_RPA_DEBUG_PORT=9222
JST_RPA_PYTHON=/usr/bin/python3
# 聚水潭账号密码登录,仅配置在服务器私有环境文件,禁止提交真实值。
JST_RPA_LOGIN_ID=
JST_RPA_LOGIN_PASSWORD=
JST_RPA_ATTEMPTS=3
JST_RPA_FALLBACK_BATCH_SIZE=10000
# 限价流程历史快照首次构建的追溯起点;后续只按更新时间增量同步。
LIMIT_PRICE_HISTORY_SOURCE_START=2026-01-01T00:00:00
# 物流规则和省份占比快照目录;正式站建议指向 shared/freight-config,本地留空。
FREIGHT_CONFIG_DATA_DIR=
# 产品限价页面已确认记录来源库(限价历史抽屉展示来源记录时使用)。
PROD_ARCHIVE_DB_HOST=
PROD_ARCHIVE_DB_PORT=5432
PROD_ARCHIVE_DB_NAME=
PROD_ARCHIVE_DB_USER=
PROD_ARCHIVE_DB_PASSWORD=
PROD_ARCHIVE_DB_SSL=true
# OA 新品流程看板数据源。默认 mysql;切换到 JSReportReplica 时设为 mssql。
OA_BOARD_SOURCE_DRIVER=mysql
@ -95,6 +128,9 @@ DINGTALK_TODO_ESCALATION_ENABLED=false
DINGTALK_TODO_ESCALATION_DAYS=3
# DINGTALK_AGENT_ID=
# 清货池多包标记钉钉通知:填写在职员工目录中的姓名,默认推送卷帘大将。
SUPPLY_CHAIN_CLEARANCE_COMBO_NOTIFICATION_RECIPIENT=卷帘大将
# 由壹点通知中心(仅服务端使用,项目密钥禁止提交或暴露给浏览器)
YYD_NOTIFICATION_CENTER_URL=https://sc.jiansun.vip/notification-center
YYD_NOTIFICATION_PROJECT_CODE=furniture_platform
@ -176,6 +212,9 @@ ALIBABA_1688_IMAGE_SEARCH_TAGS=4306497
MATERIAL_LIBRARY_WHITE_BACKGROUND_API_ENABLED=false
MATERIAL_LIBRARY_WHITE_BACKGROUND_API_BASE_URL=https://example.com
MATERIAL_LIBRARY_WHITE_BACKGROUND_API_KEY=replace-with-material-library-api-key
# 检测报告查询(不填则复用上面素材平台的 BaseUrl / API Key;都没有时「是否有检测报告」显示为空)
MATERIAL_LIBRARY_CERTIFICATE_REPORT_API_BASE_URL=
MATERIAL_LIBRARY_CERTIFICATE_REPORT_API_KEY=
# 产品全量档案以图搜图(模型文件放在仓库外,不提交到 Git)
IMAGE_SEARCH_MODEL_PATH=

View File

@ -79,3 +79,9 @@ agents/script-monitor/config.json
# 下载中心本地运行时产物
.download-center/
# 限价每日快照(本机/生产运行产物,不入库)
/data/limit-price-snapshots/
# 产品档案对账诊断报告
/data/archive-reconcile-diagnostics/

View File

@ -0,0 +1,257 @@
{
"version": 1,
"generatedAt": "2026-10-26T08:52:26.453Z",
"sourceFile": "各省发货占比.xlsx",
"totalShipment": 10983200,
"rowCount": 31,
"rows": [
{
"province": "安徽",
"shipmentCount": 426181,
"totalShipment": 10983200,
"shipmentShare": 0.038803,
"region": "一区",
"regionShare": 0.214
},
{
"province": "江苏",
"shipmentCount": 800646,
"totalShipment": 10983200,
"shipmentShare": 0.072897,
"region": "一区",
"regionShare": 0.214
},
{
"province": "上海",
"shipmentCount": 325564,
"totalShipment": 10983200,
"shipmentShare": 0.029642,
"region": "一区",
"regionShare": 0.214
},
{
"province": "浙江",
"shipmentCount": 797004,
"totalShipment": 10983200,
"shipmentShare": 0.072566,
"region": "一区",
"regionShare": 0.214
},
{
"province": "北京",
"shipmentCount": 290861,
"totalShipment": 10983200,
"shipmentShare": 0.026482,
"region": "二区",
"regionShare": 0.485
},
{
"province": "福建",
"shipmentCount": 467880,
"totalShipment": 10983200,
"shipmentShare": 0.0426,
"region": "二区",
"regionShare": 0.485
},
{
"province": "广东",
"shipmentCount": 1820992,
"totalShipment": 10983200,
"shipmentShare": 0.165798,
"region": "二区",
"regionShare": 0.485
},
{
"province": "河北",
"shipmentCount": 424406,
"totalShipment": 10983200,
"shipmentShare": 0.038641,
"region": "二区",
"regionShare": 0.485
},
{
"province": "河南",
"shipmentCount": 546529,
"totalShipment": 10983200,
"shipmentShare": 0.04976,
"region": "二区",
"regionShare": 0.485
},
{
"province": "湖北",
"shipmentCount": 378469,
"totalShipment": 10983200,
"shipmentShare": 0.034459,
"region": "二区",
"regionShare": 0.485
},
{
"province": "湖南",
"shipmentCount": 386336,
"totalShipment": 10983200,
"shipmentShare": 0.035175,
"region": "二区",
"regionShare": 0.485
},
{
"province": "江西",
"shipmentCount": 271676,
"totalShipment": 10983200,
"shipmentShare": 0.024736,
"region": "二区",
"regionShare": 0.485
},
{
"province": "山东",
"shipmentCount": 626419,
"totalShipment": 10983200,
"shipmentShare": 0.057034,
"region": "二区",
"regionShare": 0.485
},
{
"province": "天津",
"shipmentCount": 115475,
"totalShipment": 10983200,
"shipmentShare": 0.010514,
"region": "二区",
"regionShare": 0.485
},
{
"province": "广西",
"shipmentCount": 397325,
"totalShipment": 10983200,
"shipmentShare": 0.036176,
"region": "三区",
"regionShare": 0.251
},
{
"province": "贵州",
"shipmentCount": 242963,
"totalShipment": 10983200,
"shipmentShare": 0.022121,
"region": "三区",
"regionShare": 0.251
},
{
"province": "黑龙",
"shipmentCount": 166557,
"totalShipment": 10983200,
"shipmentShare": 0.015165,
"region": "三区",
"regionShare": 0.251
},
{
"province": "吉林",
"shipmentCount": 135735,
"totalShipment": 10983200,
"shipmentShare": 0.012358,
"region": "三区",
"regionShare": 0.251
},
{
"province": "辽宁",
"shipmentCount": 257754,
"totalShipment": 10983200,
"shipmentShare": 0.023468,
"region": "三区",
"regionShare": 0.251
},
{
"province": "山西",
"shipmentCount": 177480,
"totalShipment": 10983200,
"shipmentShare": 0.016159,
"region": "三区",
"regionShare": 0.251
},
{
"province": "陕西",
"shipmentCount": 281956,
"totalShipment": 10983200,
"shipmentShare": 0.025672,
"region": "三区",
"regionShare": 0.251
},
{
"province": "四川",
"shipmentCount": 532179,
"totalShipment": 10983200,
"shipmentShare": 0.048454,
"region": "三区",
"regionShare": 0.251
},
{
"province": "云南",
"shipmentCount": 341852,
"totalShipment": 10983200,
"shipmentShare": 0.031125,
"region": "三区",
"regionShare": 0.251
},
{
"province": "重庆",
"shipmentCount": 222288,
"totalShipment": 10983200,
"shipmentShare": 0.020239,
"region": "三区",
"regionShare": 0.251
},
{
"province": "甘肃",
"shipmentCount": 144419,
"totalShipment": 10983200,
"shipmentShare": 0.013149,
"region": "四区",
"regionShare": 0.05
},
{
"province": "海南",
"shipmentCount": 148380,
"totalShipment": 10983200,
"shipmentShare": 0.01351,
"region": "四区",
"regionShare": 0.05
},
{
"province": "内蒙",
"shipmentCount": 166636,
"totalShipment": 10983200,
"shipmentShare": 0.015172,
"region": "四区",
"regionShare": 0.05
},
{
"province": "宁夏",
"shipmentCount": 47493,
"totalShipment": 10983200,
"shipmentShare": 0.004324,
"region": "四区",
"regionShare": 0.05
},
{
"province": "青海",
"shipmentCount": 39031,
"totalShipment": 10983200,
"shipmentShare": 0.003554,
"region": "四区",
"regionShare": 0.05
},
{
"province": "西藏",
"shipmentCount": 441,
"totalShipment": 10983200,
"shipmentShare": 0.00004,
"region": "未分区",
"regionShare": 0.0002471046689489402
},
{
"province": "新疆",
"shipmentCount": 2273,
"totalShipment": 10983200,
"shipmentShare": 0.000207,
"region": "未分区",
"regionShare": 0.0002471046689489402
}
]
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,18 @@
[Unit]
Description=Furniture JST logistics rule daily snapshot
Wants=network-online.target furniture-jst-rpa-browser.service
After=network-online.target furniture-jst-rpa-browser.service
Conflicts=furniture-discontinuation-daily.service plm-barcode-sync.service
[Service]
Type=oneshot
WorkingDirectory=/opt/furniture-3d-system-prod/current/furniture-3d-system
Environment=NODE_ENV=production
Environment=TZ=Asia/Shanghai
Environment=PATH=/usr/local/bin:/usr/bin:/bin
Environment=JST_RPA_BROWSER_URL=http://127.0.0.1:9222
Environment=FREIGHT_CONFIG_DATA_DIR=/opt/furniture-3d-system-prod/shared/freight-config
EnvironmentFile=/opt/furniture-3d-system-prod/shared/.env
ExecStart=/usr/bin/env node node_modules/tsx/dist/cli.mjs scripts/sync-jst-logistics-rules.ts
TimeoutStartSec=30min
Nice=5

View File

@ -0,0 +1,11 @@
[Unit]
Description=Run JST logistics rule snapshot daily
[Timer]
OnCalendar=*-*-* 09:30:00 Asia/Shanghai
Persistent=true
AccuracySec=30s
Unit=furniture-jst-logistics-snapshot.service
[Install]
WantedBy=timers.target

View File

@ -0,0 +1,19 @@
[Unit]
Description=Furniture supply-chain daily limit price snapshot
Wants=network-online.target furniture-jst-rpa-browser.service
After=network-online.target furniture-jst-rpa-browser.service furniture-jst-logistics-snapshot.service
[Service]
Type=oneshot
WorkingDirectory=/opt/furniture-3d-system-prod/current/furniture-3d-system
Environment=NODE_ENV=production
Environment=TZ=Asia/Shanghai
Environment=PATH=/usr/local/bin:/usr/bin:/bin
Environment=JST_RPA_BROWSER_URL=http://127.0.0.1:9222
Environment=FREIGHT_CONFIG_DATA_DIR=/opt/furniture-3d-system-prod/shared/freight-config
Environment=SUPPLY_CHAIN_LIMIT_SNAPSHOT_DIR=/opt/furniture-3d-system-prod/shared/limit-price-snapshots
Environment=LIMIT_SYNC_SKIP_EXCEL=1
EnvironmentFile=/opt/furniture-3d-system-prod/shared/.env
ExecStart=/usr/bin/env node scripts/run-limit-price-daily-snapshot.mjs
TimeoutStartSec=2h
Nice=10

View File

@ -0,0 +1,12 @@
[Unit]
Description=Run furniture supply-chain daily limit price snapshot
[Timer]
OnCalendar=*-*-* 09:30:00 Asia/Shanghai
Persistent=true
RandomizedDelaySec=60
AccuracySec=30s
Unit=furniture-limit-price-snapshot.service
[Install]
WantedBy=timers.target

View File

@ -0,0 +1,109 @@
# 1688代发异常事项:退货地址推送接口
## 交付与启用
本接口供外部系统按 SPU 推送退货地址。正式站发布包含该接口的版本并配置独立密钥后,对方即可按本文调用。
部署人员在生产运行环境配置 `DROPSHIP_RETURN_ADDRESS_API_KEY`,使用独立随机密钥(建议至少32个随机字节),由双方通过安全渠道交接;密钥不进入 Git、对接文档、URL 或普通日志。未配置时接口返回503。无需提供数据库账号,无需平台用户登录。
## 请求
- 方法:`POST`
- 正式地址:`https://cp.jiansun.vip/api/external/supply-chain/dropship-return-addresses`
- 本地联调:`http://localhost:3001/api/external/supply-chain/dropship-return-addresses`
- 请求头:`Content-Type: application/json`、`x-api-key: <对接密钥>`
- 每批1–100条;单条推送也使用 `items` 数组。
```json
{
"items": [
{
"spu": "DF-C1YMJ0009",
"returnAddress": "收件人:张三;电话:138xxxxxxxx;地址:江苏省……"
}
]
}
```
| 字段 | 必填 | 规则 |
| --- | --- | --- |
| `spu` | 是 | 1–120位字母、数字、下划线或连字符;自动去除首尾空格并转为大写 |
| `returnAddress` | 是 | 去除首尾空格后1–500字;收件人、电话、详细地址合并为一段文字 |
仅接收上述字段。额外传入发票类型、安装方式等字段,该条会失败,不会改动这些字段。
调用示例(密钥由调用方运行环境注入):
```bash
curl --request POST \
'https://cp.jiansun.vip/api/external/supply-chain/dropship-return-addresses' \
--header 'Content-Type: application/json' \
--header "x-api-key: ${DROPSHIP_RETURN_ADDRESS_API_KEY}" \
--data '{"items":[{"spu":"DF-C1YMJ0009","returnAddress":"收件人:张三;电话:138xxxxxxxx;地址:江苏省……"}]}'
```
## 写入规则
1. 一SPU一退货地址,同SPU下所有SKU共用;按档案中的DF代发范围核验,不受当月业绩筛选影响。
2. SPU必须已存在于产品档案。没有维护记录时自动创建维护记录;不会创建产品档案。
3. 新地址覆盖该SPU原有地址(包括人工填写的地址),以数据库实际处理顺序为准。
4. 只更新退货地址和维护人/时间,保留发票类型、整装/拆装;新增维护记录时其他字段使用原有空值默认值。
5. 空地址拒绝,不能通过这个接口清空地址。重复推送同一地址返回 `UNCHANGED`,不重复写入或变更时间。
6. 同批重复SPU(含大小写/首尾空格差异)的重复项全部失败,其余条目继续处理;每批按输入顺序返回结果。
7. 每条独立保存,某条失败不撤销已经成功的条目。实际地址变化写入1688代发异常事项操作日志,标记“外部退货地址同步”,包含前后值与请求编号。
8. 当前页面默认只显示有业绩SPU;无业绩产品推送成功后,切换“全部SPU”查看。
## 响应
HTTP200表示批次已处理,**仍须检查 `data.failedCount` 和每条 `status`**。
```json
{
"success": true,
"data": {
"total": 2,
"successCount": 1,
"failedCount": 1,
"updatedCount": 1,
"unchangedCount": 0,
"results": [
{
"index": 1,
"spu": "DF-C1YMJ0009",
"status": "UPDATED",
"updatedAt": "2026-10-24T04:00:00.000Z"
},
{
"index": 2,
"spu": "DF-MISSING",
"status": "FAILED",
"code": "SPU_NOT_FOUND",
"message": "产品档案中不存在该代发 SPU"
}
]
},
"meta": {
"requestId": "服务端请求编号",
"timestamp": "2026-10-24T04:00:00.000Z"
}
}
```
`index` 从1开始,对应请求数组顺序;`UPDATED` 表示新增或更新成功,`UNCHANGED` 表示地址已一致。两种均计入 `successCount`。时间使用UTC ISO8601格式。
| 每条失败码 | 含义与处理 |
| --- | --- |
| `INVALID_ITEM` | 字段格式、地址为空/过长或包含额外字段;修正数据后重推 |
| `DUPLICATE_SPU` | 同批存在重复SPU;去重后重推 |
| `SPU_NOT_FOUND` | 不存在对应DF代发档案;核对SPU或等待档案同步后重推 |
| `WRITE_FAILED` | 保存失败;重试该条,持续失败时附 `meta.requestId` 联系管理员 |
| HTTP状态 | 含义 |
| --- | --- |
| 200 | 批次已处理,检查逐条结果 |
| 400 | 非法JSON、缺少items、空批次、超过100条或多余顶层字段;整批不写入 |
| 401 | 缺少密钥或密钥错误;整批不写入 |
| 503 | 服务端未配置对接密钥;整批不写入 |
| 500 | 系统错误;提供请求编号排查 |
网络超时或500时可能已有部分条目保存。可重推相同地址;如果期间出现更新,应重新取得上游最新地址再推送,避免旧批次覆盖新地址。调用方不应并发推送同一SPU。

View File

@ -0,0 +1,78 @@
# 产品全量档案同步性能治理
## 范围与业务规则
本次治理覆盖源库集中取数、匹配索引、事务批量写入、来源快照、断点续跑与日销优先调度。保留字段来源、负责人优先级、只填空值、首次来源初始化不记变更日志等既有规则。
公司 SQL Server 只读,使用个人账号。验证写入只发生在本机 PostgreSQL 55432 的独立测试 schema;public 业务表只读。本轮没有生产配置、定时器或生产数据变更,没有提交、推送、main 合并或部署。
## 来源读取
- 一轮先固定档案 id、条码、SKU、供应商、仓库及初始图片状态,再集中读取价格、公共库存、清仓、库存监控、供应商、仓库、图片七类来源。最多两个来源同时读取,每个来源尝试独占一个连接池,池大小为 1。
- 请求通过 OPENJSON 传入,不受原每 200 条重复查询限制。库存监控仍按原始目标批次匹配和选最新记录,保留条码优先、SKU 唯一兜底的口径。
- 图片保留 DF 全量刷新、nonDF 仅补当前无图档案的范围。主视图 jst_skumap 按 shop_id、shop_i_id、shop_sku_id 三列主键每页 50,000 条读取编码,再按命中的主键精确读取图片;另外两张图片表分别以哈希匹配读取请求编码,并读取 DF 范围;重复命中由最新非空选择去重。使用 COMPRESS 与 UTF-16 JSON 无损传输,跨页、跨表选择最新非空图片,保留时间的亚毫秒顺序,不截断图片。
- 每个来源的成功结果独立原子保存,带 SHA-256、条数和行结构校验。重启复用已完成来源,失败来源重新读取;来源未读完不发布部分结果。图片三张表还会分别保存已完成结果,失败后只重读未完成的表;主表内部分页尚不持久化中间页。
- 每次请求有完整传输的墙钟限时:图片 600 秒,其他来源 300 秒。PRODUCT_ARCHIVE_SNAPSHOT_QUERY_TIMEOUT_MS 可统一覆盖,范围 1,000~600,000 毫秒。超时取消请求,以 ETIMEOUT 进入原有 30 秒、120 秒、300 秒退避重试;认证、权限、结构错误不重试。
- 每批写前重新读取当前人员与图片,保留人工负责人保护;条码、SKU、供应商或仓库改变时拒绝旧快照。原先已有图的 nonDF 档案中途被清空,也要求新的 run key,避免使用未请求对应图片的旧快照。
- 各来源在不同时间读取,不构成跨公司库与应用库的同一时刻事务快照。后续新 run key 会重新读取来源。
## 匹配、写入与调度
- 来源数组建立索引,共享图片索引跨批复用。保留原 find 的首条命中与原始来源顺序。
- 每批最多 200 档案,在一个事务中按 id 排序锁定当前行,更新变化字段、字段来源、来源记录和逐字段日志。日志失败时整批回滚。来源日期传值、明确 null、未传三种状态保持原行为。
- Service 计算计划,Repository 负责数据访问与事务;Repository 只通过参数接收 planner,不在运行时调用 Service。
- PostgreSQL advisory lock 命名空间 90710:3 为写入轮次,4 为日销意向,5 为全量任务排他。日销先登记意向,后台当前批完成后让行。同库重复全量任务返回 SKIPPED_RUNNING;连接中断后拒绝继续写。
- 主补全与 JST 基础档案按批让行。负责人流程与生产类型阶段以整阶段持锁。JST 某项写入失败时停止启动新项,等待同批已发出写入结束后再释放锁,不保存失败批次进度。
- 匹配 mapper 保留原映射职责;类型、索引、快照、编排和事务已按职责拆分。没有新增界面或视觉样式。
## 运行与续跑
入口:scripts/sync-supply-chain-product-archive-enrichment.ts。保留 --apply、固定确认词、目标库及来源身份门禁。
- 默认 run key 为北京时间日期;相同配置重启继续已提交批次,已完成阶段跳过。
- 跨天续跑使用原 run key,例如 --run-key=2026-10-21。
- 同日需要重新读取最新来源时使用新 key,例如 --run-key=2026-10-21-second,保留原断点。
- 目标库、来源身份、关键代码或批次参数变化会拒绝旧断点;损坏文件不会静默按空来源继续。
- 断点经临时文件、fsync、原子 rename 保存。批次提交后、保存前中断会重放该批,通过比较当前值避免重复记录未变化字段。
- 默认目录:本地 D:\ChanPing\.local-services\checkpoints\product-archive-sync;生产发布结构下 shared/product-archive-sync-checkpoints。可用 PRODUCT_ARCHIVE_SYNC_CHECKPOINT_DIR 指定。
- ARCHIVE_SOURCE_SNAPSHOT 输出来源读取与复用状态,ARCHIVE_PICTURE_PAGE 输出主表扫描及命中进度,ARCHIVE_SYNC_BATCH 与 ARCHIVE_SYNC_PHASE 输出批次和阶段计时。
- 本机 Node、Jest、TypeScript、ESLint 都通过 .local-services/scripts/invoke-node-hidden.ps1 启动。
## 验证记录(2026-10-21)
全部写入位于本机独立副本 archive_full_1792578871629。基础来源真实读取 81,070 条,副本从 75,527 档案增至 81,074 条。随后覆盖全部档案,对修改前 mapper/applyPatch 构建的独立 oracle 逐批核对档案业务字段、来源业务字段和逐字段日志。排除 id、自动更新时间、同步时间等每轮必然不同的元数据。
- 全部 81,074 档案通过;比较 1,092,416 条来源记录及 15,290 条新增逐字段日志,结果一致。
- 首、中、末三个 200 条批次的六类来源共 18 项新旧取数对比通过。
- 图片主表 24,581,427 行按主键读至表尾,和独立 COUNT_BIG 结果一致;命中并精确回查 64,382 行,三表完成后得到 32,944 个最新非空图片编码。
- 13 组、97 项相关测试通过;TypeScript、源码 ESLint、diff --check 通过;3001 健康接口 database=connected、api=running。
- 在真实本机 PostgreSQL 验证日志失败整批回滚、日销在后台批次间优先、失败释放锁及同库全量排他。测试也覆盖后表失败复用主表、来源快照损坏、断点恢复、超时取消和 JST 失败等待已发出写入结束。
### 分阶段计时
以下是同一独立副本的分阶段实测,包含真实公司库读取与本地写入。部分来源读取在前置阶段完成,并在完整比对时复用,故这是分阶段累计,不是一次连续正式站整轮耗时。为了便于评估,累计值单独加上已提前读取的六类来源时间,且未扣除生产并发取数可能节省的重叠时间;旧逻辑 oracle 比对开销不计入执行成绩。
| 阶段 | 实测 |
| --- | ---: |
| 基础商品来源 81,070 条 | 173.81 秒 |
| 基础档案写入(更新 75,523,新增 5,547) | 270.33 秒 |
| 负责人流程首次应用(变更 2,317) | 51.36 秒 |
| 提前读取的六类非图片来源,分别累计 | 204.30 秒 |
| 三表图片完整取数 | 806.17 秒 |
| 共享数据、补全匹配和写入(复用来源,扣除 oracle 比对) | 81.08 秒 |
| 产品类型实际同步 | 39.68 秒 |
| 分阶段累计 | 27.11 分钟 |
图片主表单独耗时 787.83 秒,三表总图片来源 806.17 秒。完整补全验证实际墙钟 5.77 分钟,其中旧逻辑比对 4.42 分钟。
另一个 1,000 档案旧新写入试验中,匹配加写入从 44.13 秒降至 0.92 秒,约 48 倍;该局部试验不含源库,不能作为整轮加速倍数。
### 适用限制与收口
公司库负载和网络有波动。先前相同来源的读取曾明显变慢,不能据本地累计值承诺正式站固定耗时。完整生产成绩需正式发布后实测。负责人和产品类型阶段以整阶段让行;图片恢复以单表完成为边界,未完成的主表仍需重读。增量镜像、公司库索引与 DDL 不在本次范围。
本轮未提交、推送、合并 main 或部署,生产写入为 0。提交状态:可提交;main 标记:可合并 main。源码、测试及本说明共 31 个文件,登记为 awaiting_review。
规范结论:passed with warnings,这是自审,不是独立复审。架构职责保持;无视觉改动;修复重复扫描、线性查找、逐条事务和失败后重读已完成来源的原因。警告为尚无连续生产整轮计时及上述阶段让行限制。
审计证据:D:\ChanPing\.local-services\audits\archive-sync-performance-20261021\complete-review-result.json。

View File

@ -24,6 +24,30 @@
只做生产/目标库地基核查时,优先在远端应用目录执行 `npm run db:foundation:audit:production-readonly`。该命令不事务回放治理迁移 SQL,只通过只读迁移状态和质量查询生成 `.master-data-audits/foundation-production-readonly-current/manifest.json`,适合把脱敏 summary 或 manifest 交回主线收口。拿到 manifest 后,用 `npm run db:foundation:verify-production-readonly -- --manifest <manifest.json>` 做机器验收;只有 `accepted: true` 才能把这份证据计入生产级地基收口。
## 聚水潭 RPA 登录凭据
- 本地可在 `furniture-3d-system/.env.jst-rpa-auth` 保存
`JST_RPA_LOGIN_ID` 和 `JST_RPA_LOGIN_PASSWORD` 两项。该文件被 `.env*`
忽略规则排除,禁止提交 Git。
- 正式部署脚本会自动发现该文件,上传到目标机 `incoming` 临时目录,再安全合并进
`/opt/furniture-3d-system-prod/shared/.env`;正式 `.env` 和临时文件均保持
`600`,临时文件在部署结束后删除。
- 使用其他安全位置时,可给部署入口显式传
`-JstRpaAuthFile <凭据文件绝对路径>`。未提供文件时会保留正式机已有值,不会被空值覆盖。
- 部署完成后可执行
`node_modules/.bin/tsx scripts/jst-shop-goods-rpa.ts --action=login`
验证登录态,不执行链接导出。
## 限价每日快照
- 生产安装入口:`furniture-3d-system/scripts/install-limit-price-snapshot-systemd-timer.sh`。
- systemd 名称:`furniture-limit-price-snapshot.service` / `furniture-limit-price-snapshot.timer`。
- 默认每天中国标准时间 `09:30` 执行,可在安装时通过 `LIMIT_PRICE_SNAPSHOT_CALENDAR` 覆盖。
- 快照持久目录默认 `/opt/furniture-3d-system-prod/shared/limit-price-snapshots`,安装器会把该路径写入生产 `.env` 的 `SUPPLY_CHAIN_LIMIT_SNAPSHOT_DIR`,避免部署切换版本后丢失。
- 任务先更新产品档案、限价、物流规则并完整对账正式产品全量档案,全部成功后才原子切换 `latest.json`。
- 安装器会检查数据库和聚水潭报表库环境变量;生产 `.env` 更新后需要重启正式应用进程。
- 安装、启用定时器和首次生产快照属于生产操作,需要单独授权。
## 花名册同步
- 花名册同步属于当前 `furniture-3d-system` 自有能力,不依赖 `shujuku2` 或旧入口机。

View File

@ -0,0 +1,23 @@
# 铺货与业绩快照定时更新
本机 3001 的 Windows 计划任务 `Furniture-DistributionSnapshots-3001` 每天北京时间 09:00 顺序更新铺货明细、真实业绩。任务使用 `+08:00` 开始边界;错过时间后在机器可运行时补执行,重复触发不并发。电脑需开机、用户登录且公司报表库与本地 PostgreSQL 可连接。此任务未配置到正式站。
本机入口在 `D:\ChanPing\.local-services\scripts\run-distribution-snapshots.vbs`,依次调用隐藏 PowerShell、统一 Node 无窗口入口和个人凭据注入器。凭据仅运行时读取 `LOCAL_SECRETS.md` 的公司数据库节,不进入源码或日志。所有公司库操作均为 SELECT。
仓库入口 `scripts/sync-distribution-snapshots.ts` 验证来源和目标身份、持有任务锁,串行执行两个同步 CLI。日志及每次前后计数、含索引物理字节数、业绩统计区间报告写到 `D:\ChanPing\.local-services\logs\distribution-snapshots`。失败返回非零退出码,可从计划任务 LastTaskResult 和阶段日志定位;不自动报告成功。
铺货先读取目标管理、人员、店铺映射,字段一次补齐后写入连接专属临时表。自然键去重后核对实际条数、三类来源,较上一版减少超过 20% 时拒绝发布,等待人工核查。通过后在一个事务中替换正式表,失败保留上一版。最终替换阶段持有表锁,页面查询可能等待;临时表和会话锁会在进程退出后自动释放。没有新增长期全量副本,没有反复 UPDATE 数百万行。
组合补齐:`rpt.jst_comboProductInfo` 与 `rpt.jst_comboProductItem` 只提供完整套装编码、组成条码和数量。同步从三张 skumap 中同时匹配档案条码及包含档案条码的组合编码,每个原始套装规格仅保存一次。轻量组成关系写入 `SupplyChainDistributionComboItem`,与铺货规格在同一事务发布;读取视图 `SupplyChainDistributionListingResolved` 将套装规格关联到组成条码。页面按条码及平台、店铺ID、铺货ID去重,直接铺货与多个套装共享同一链接时计一次,组件数量不乘链接数。链接状态保持 skumap/目标管理原口径,不使用组合头 enabled 替代。关系缺失、组成为空或发布校验失败时保留上一批完整快照。
页面支持搜索组合编码/名称,并在链接明细的条码列查看“直接铺货”“组合套装”或两者共有及具体套装数量。Excel 铺货页保留原始货号和规格,新增独立“组合关系”工作表,避免重复保存/导出整份宽数据。业绩匹配可识别通过套装覆盖的子条码链接;只使用业绩源已经提供的子条码金额,不将套装金额复制或按数量自动分摊。
首次补齐可在两个同步 CLI 上显式增加 `--reuse-direct-snapshot`:先在本地数据库中将已发布的档案直接规格复制到临时表,再从三张源表读取组合规格,整体校验后统一替换。直接规格原有同步时间与内容保留,便于精确比较组合净增量。此选项不用于每日任务;定时任务默认完整刷新直接与组合规格。
业绩沿用独立版本发布:当月截至昨天以内源库最新可用日,按条码、店铺、铺货 ID 汇总;成功后保留当前和上一成功版本。每月 1 日当月无完整统计日时保留前一版并报出失败,页头继续显示实际统计周期。铺货与业绩不能作为一个跨库事务提交:铺货成功但业绩失败时,铺货保留新版本,业绩保留上一成功版,任务整体显示失败。
手动运行和定时执行使用相同入口,不并发补数。此文档不授权部署。
正式站经独立授权后,先核实目标库、来源库身份和磁盘余量,再通过统一部署入口应用三个迁移。服务器从公司库只读生成自己的快照,不复制整个本地数据库。个人凭据沿用 shared/marketing-sales-personal.env,目标和来源身份门禁放在权限为 600 的 shared/distribution-snapshots.env,分别为 DISTRIBUTION_EXPECTED_DATABASE 和 DISTRIBUTION_EXPECTED_SOURCE(host:port/database 格式)。来源连接的其他 JST_REPORT_DB_* 配置沿用 shared/.env。
生产安装入口 scripts/install-distribution-snapshots-timer.sh 校验身份及依赖后,启用 furniture-distribution-snapshots.timer,每天 Asia/Shanghai 09:00 执行;错过的调度补执行。对应 service 通过 scripts/run-distribution-snapshots.sh 运行同一串行同步入口,日志在 journalctl -u furniture-distribution-snapshots,前后条数与空间报告在 shared/logs/distribution-snapshots。手动首次执行使用 systemctl start --no-block furniture-distribution-snapshots.service;任务锁防止并发,失败状态需核查报告和日志。本机任务与生产任务独立,安装生产任务不会停用本机任务。

View File

@ -0,0 +1,33 @@
# DIY 柜门图案方向修正:请核对并替换旧图片
状态:待发布的更新日志文案。后续正式发布时录入系统“更新日志管理”并发布,本文档本身不会向用户推送通知。
## 更新日志标题
DIY 柜门图案方向已修正,请核对并替换旧图片
## 更新日志摘要
DIY 柜门图案曾出现左右镜像,现已修正。如果此前使用过 DIY 截图、AI 白底图或场景效果图,请对照柜门原图核对方向;发现镜像的图片请重新出图,并替换已使用的旧图片。
## 已修正的内容
DIY 柜门图案在 3D 画面中曾左右镜像,部分图案、文字和动物的朝向可能与原图相反。现已修正柜门贴图方向,横竖切换后也按原图方向显示。
## 用过旧图片的同事需要处理
1. 核对修复前生成、下载或发布的 DIY 截图、AI 白底图和场景效果图,重点检查文字及图案的左右方向。
2. 如发现镜像,请打开原保存方案,刷新页面后对照柜门原图确认方向,重新截图或生成 AI 图片。
3. 将店铺商品图、详情页、方案资料及其他已使用位置中的错误图片替换为核对后的新图片。
## 已保存的方案和旧图片
已保存方案可以直接打开并重新出图,无需重新搭建,也不需要为这次显示方向修正重新上传柜门原图。
旧截图及已生成的 AI 图片不会自动改正,已下载或发布的文件也不会自动替换。请核对重新生成的图片后再使用;原本方向正确的图片可继续保留。
## 后续发布登记
- 这条说明需要作为用户可见更新日志发布,仅提交本文档不能代替发布通知。
- 发布时使用系统现有“更新日志管理”,覆盖使用 DIY 的同事;如不能准确筛选这批人员,按全员可见发布。
- 发布后确认普通使用者可看到标题、摘要及上述替换说明。不要把“修正 3D 预览”写成“旧图片已自动更新”。

View File

@ -0,0 +1,46 @@
# 2026-10-28 限价运费计算阻塞网页线程
## 故障与证据
生产版本 `83a3138fe9704d61f13de9f665779e783931c57d` 的网页进程长时间占用一个 CPU 核心,端口 3001 的监听队列达到 512,本机健康检查无法响应,公网返回 502。穿透进程持续正常,内存和磁盘有余量,故障时没有检出矿工。
重启前的 CPU 采样主要落在 Unicode 格式转换、字符串替换和去空格。入口访问日志显示,部署后 `/api/supply-chain/limit-management/products` 出现长耗时;17:13 至 19:29 之间有 338 次限价计算请求,其中一次成功请求耗时 397.882 秒。请求时间包含等待与其他请求争用的时间,不能当作单次函数的纯计算耗时。
故障链路:
`限价商品列表 / 限价导出 -> calculateRegionFreightsForArchiveRows -> buildFreightCalculation -> 每个快递的每个省份调用 selectLatestApplicableRate -> 重扫并规范化整批费率`
10 月 28 日新增的区域和全国运费计算,把原来用于单个商品的全表扫描放大到整页商品。算法的重复扫描量随「商品数 × 快递数 × 29 个省份 × 全部费率行数」增长,而且原整批循环没有让出网页线程。并发打开或刷新列表会进一步积压请求;入口 502 和数据库事务超时是下游症状。
## 隔离复现
只读取得费率、物流规则和商品字段快照,在本机离线运行,不向生产网页接口重放旧重计算。上游费率视图有 75,630 行;复现的 200 行限价页中有 159 行能进行运费计算,共需 4,465 行费率。
同一快照、同一计算时间、相同输入与输出顺序,本机 Node.js 24.22.1 的一次带规范化调用计数的对比(计数会影响耗时):
| 项目 | 修复前 | 修复后 |
| --- | ---: | ---: |
| 计算耗时 | 16,056 ms | 124 ms |
| 字符串规范化调用 | 64,631,978 | 16,030 |
| 完整结果 SHA-256 | `a34b18ccfd50ae6053b26d7ca2513fa101368cadfdbee3befaf7f752c360efc6` | 相同 |
这是离线计算对比,不代表生产端到端请求耗时,也不包含数据库和网络时间。原始商业数据与 CPU 采样只保存在私有排错目录,不进入 Git。
另用本机独立 HTTP 子进程调用实际批量服务,数据访问以同一只读快照代替,不插入字符串计数。旧版计算用时 5,012 ms,期间并发健康请求最长等待 5,019 ms;修复版计算用时 143 ms,8 次健康请求均为 200,最长等待 29 ms。两次完整服务返回值摘要均为 `ef8a73cec474260611eb74c073d22b32c721b1881644db7ceff22cc83e41d489`。
## 修复边界
- 纯计算层按规范化后的快递和省份建立费率索引,并按原来的生效时间和规则 ID 排序。保留有效期、重量/体积/最长边、计泡、取整、失效提示和财务计算规则。
- `createFreightCalculator` 在同一批次复用费率索引与发货占比;新的批次重新使用自己的费率快照和计算时间,不增加跨请求的陈旧缓存。
- 批量服务超过 8 ms 计算时间片后用 `setImmediate` 让出线程,让健康检查和其他请求有执行机会。
- 新增复杂度、报价一致性、有效期和批量让出线程的回归测试。本次生产修复保留现有运费计算公式及基线期望,不带入开发线上尚未发布的运费功能和公式变化。
不修改数据库结构或业务数据、页面视觉、SSH 密钥、密码、共享账号、全局 Node.js、穿透配置或其他项目服务。
## 发布核查
2026-10-29 的真实页面验证发现额外的运行时兼容问题:单商品运费能返回,大页触发让出线程时抛出 `RangeError: Maximum call stack size exceeded`。在生产私有 Node.js 22.23.4 的独立子进程中,仅加载当前 Next.js 16.1.6 的 `fast-set-immediate.external.js` 并调用 `node:timers/promises.setImmediate()` 即可复现;调用栈反复进入框架的 `patchedSetImmediatePromise`,与数据库记录无关。
统一的 `yieldToEventLoop` 改用 `node:timers` 的回调接口包装 Promise。它仍然把执行机会交还事件循环,不修改框架、全局计时器或其他项目;新增子进程测试加载实际 Next.js 补丁,覆盖生产运行环境,避免只用模拟计时器漏掉这类问题。
发布需按当前生产部署入口说明执行,保留上一版本,核对目标主机、生产提交、PM2、端口和日志。重点验证真实的限价列表和限价导出,同时并行请求健康检查;不能只验证健康检查或本地测试就认定生产修复完成。其他项目的进程 PID 和重启次数应保持不变。

View File

@ -0,0 +1,35 @@
# 2026-10-29 限价排序与 DNS 延迟
## 两种独立的等待
生产版本 `66db2a26` 已修复运费重复扫描和 Next.js 计时器兼容问题,继续检查时仍出现两类延迟,不能混为同一个业务故障。
- 本机网页进程:限价排序函数在价格、时间相同的商品之间,反复调用带 `zh-CN` 和 `numeric: true` 参数的 `String.localeCompare`。每次比较都重新准备排序规则,整页取数时需要给五万多条候选商品排序,阻塞网页线程约 3 秒。
- 公网检查:分阶段记录 DNS、TCP、TLS 和首字节耗时,慢请求中 DNS 本身耗时约 5.01 秒;同次请求后续连接和网页响应只占约 0.09 秒。保持域名、TLS 验证与目标服务不变,仅诊断时用 `curl --resolve` 指定当前地址后,30 次请求均返回 200,最长约 0.234 秒。该 DNS 等待发生在检查机器上,不能据此认为每位用户的浏览器都有相同延迟。
当前主机的两个上游 DNS 直接查询都观察到少量超时,本地 DNS stub 的 AAAA 查询也有超时。证据不足以仅凭这些现象断定双宽带负载均衡是唯一原因。本次不改共享主机的 DNS、路由器、网卡、FRP 或其他项目配置。
## 排序对比与修复范围
在生产私有 Node.js 22.23.4 的独立进程中,只读取得 54,381 条销售商品候选和 13,537 条货号限价,使用同一批内存数据对比:
| 指标 | 原比较方式 | 复用 Intl.Collator |
| --- | ---: | ---: |
| 排序耗时 | 3,318 ms | 212 ms |
| 完整有序商品 ID SHA-256 | `8d83f84e3252953e0729d320da3a43ede88fca7cffce6e2f906c9f857a62ff10` | 相同 |
这是隔离计算耗时,不是整页网络时间。取数约 700 ms,不计入上表。
打开真实限价页面的同时采样 100 次本机健康请求,全部返回 200,但最长等待 3,076 ms,其中 4 次超过 1 秒。这与隔离复现的同步排序耗时吻合;无用户操作时的 30 次本机检查最长约 9 ms。
产品档案 Repository 内复用同一个 `Intl.Collator('zh-CN', { numeric: true })`,用于限价列表及筛选候选值的中文数字比较。价格优先、时间优先、条码数字顺序、相同比较值的稳定顺序、空值位置、筛选范围及分页契约保持原样。不增加跨请求业务数据缓存,不修改 SQL、数据库结构、业务数据或页面视觉。
回归验证覆盖中文与数字/前导零/大小写/全半角的顺序一致性、大批次不重复初始化排序规则、筛选候选值去重与分页。发布后还需在真实页面翻页/刷新并并发检查本机健康响应,核对其他项目进程和凭据指纹。
## 继续定位查询参数准备
第一轮上线后,连续翻页和打开筛选仍出现过约 2.1 秒的等待。仅对本项目网页进程短时启用函数采样,发现主线程热点包含 Prisma 的查询模板参数展开函数 `td / Hn / ca / od`,并伴随对象分配和垃圾回收。采样完成后已恢复原 Node.js 启动参数。
隔离只读查询使用 54,395 条销售商品中的 54,291 个货号,对比 ORM 的逐项 `IN` 参数与 PostgreSQL 的单个 `text[]` 参数:原查询约 958 ms,数组参数约 202 ms;两者均返回 9,552 条,完整货号与限价结果摘要均为 `801e325b6d702880dc7d386f0903b1be073655407bfc43b08a7cdbbf7713df1e`。CPU 时间约为 1,657 ms / 285 ms,包含独立测试进程的各线程。
限价列表及负责人限价候选值改用参数化 `= ANY($1::text[])`,仍只查询已有筛选候选对应的货号;保留空集合短路、未定价商品、非空价格条件与原始小数精度。没有拼接货号进入 SQL,不扩大读取范围,不修改数据库结构或记录,不新增缓存。新增超过 PostgreSQL 标量参数上限的批次、数据范围、SQL 参数绑定及空价格候选值回归测试。

View File

@ -1,6 +1,6 @@
# 供应链产品全量档案字段来源对照表
更新时间:2026-08-10
更新时间:2026-10-16
## 1. 合并与追踪规则
@ -70,7 +70,7 @@
| `sales7d` | 7日日销 | | | `rpt.pm_stockMonitor.sevenAvgSale` | 按条码匹配最新记录 |
| `sales15d` | 15日日销 | | | `rpt.pm_stockMonitor.fifteenAvgSale` | 按条码匹配最新记录 |
| `purchasePrice` | 采购价 | `rpt.jst_sku.purchase_price` | | | 按档案条码精确匹配 `jst_sku.sku_id`;匹配不到时留空 |
| `costPrice` | 成本价(聚水潭) | `rpt.jst_sku.cost_price` | | | 按档案条码精确匹配 `jst_sku.sku_id`;匹配不到时留空 |
| `costPrice` | 成本价(聚水潭) | `rpt.jst_sku.cost_price` | | | 按档案条码精确匹配 `jst_sku.sku_id`;匹配不到时留空。DF 条码自 2026-10-16 起同样取本来源(原 `rpt.dws_sn_sales_full_info_di.goods_cost` 为金额口径,已停用) |
| `costPriceSecondary` | 成本价(新品) | | `external.product_catalog_wide_v1.cost_price` | | 按档案条码精确匹配 `barcode_business_key`,只读取 `catalog_source = INTAKE_READY` 的新品行;匹配不到或来源为空时留空 |
## 3. JST_SKU 第一批字段覆盖率

View File

@ -0,0 +1,180 @@
# 停产下架服务器运行与上游数据依赖
更新日期:2026-10-26
## 1. 服务器每日入口
服务器入口:
```bash
node scripts/run-supply-chain-discontinuation-daily.mjs
```
执行顺序:
1. 生成 Prisma Client。
2. 同步虚拟云仓库存快照。
3. 生成编码维度通知和库存预警。
4. 通过服务器 Chrome 从聚水潭“店铺商品管理”导出完整链接并生成链接维度快照。
5. 生成未下架监控与乐捐快照。
生产安装:
```bash
sudo bash scripts/install-supply-chain-discontinuation-linux-timer.sh
```
默认使用 systemd,每日北京时间 10:30 执行,`Persistent=true` 会在服务器恢复后补跑。
服务名默认为 `furniture-discontinuation-daily`。
RPA 使用的 Python 必须显式可执行:
- 正式机默认 `/usr/bin/python3`,安装器会检查可执行权限并写入
`JST_RPA_PYTHON=/usr/bin/python3`。
- 如果正式机使用虚拟环境或其他路径,安装前设置
`DISCONTINUATION_PYTHON_BIN=/明确路径/python3`。
- 每日同步在连接聚水潭页面、开始导出前先执行 `python3 --version` 预检;
路径不存在、不可执行或版本命令失败时立即终止,不会等到导出完成后才发现。
## 2. 数据源与生成责任
| 数据 | 服务器读取来源 | 应用侧生成方式 | 是否依赖个人电脑 |
| --- | --- | --- | --- |
| 商品状态、组合商品、库存 | `JianSun_Old.JianSun`、`JSReportReplica.rpt` | 服务端 SQL 只读,应用生成本地快照 | 否 |
| 虚拟云仓库存 | `JianSun_Old.JianSun.dbo.jst_virtualstockStocksInfo` | 服务端脚本读取,应用生成本地日快照 | 否 |
| 替换规则(PM) | `JSReportReplica.rpt.pm_replacementManage` | 服务端 SQL 只读 | 否 |
| 替换规则(聚水潭) | `JSReportReplica.rpt.jst_sku_replace_rule` | 读取最新 `snapshot_date` | 否 |
| 供应商剩余库存 | `JianSun_Old.JianSun.dbo.plm_productDiscontinuation` | 服务端 SQL 只读 | 否 |
| 链接完整快照 | 聚水潭“店铺商品管理”前台导出 | 服务器 Chrome + RPA,分批导出后落本地链接快照 | 否(需服务器浏览器登录态) |
| 链接数据库快照(诊断) | `JianSun_Old.JianSun.dbo.dm_jst_shop_goods_link_daily` | 只用于比对和排障,不作为每日全量写入源 | 否 |
| 订单与订单明细 | `JSReportReplica.rpt.jst_Orders`、`jst_orderitem`、`jst_qmcust_orderlist*` | 服务端 SQL 只读 | 否 |
| 产品档案、负责人、申诉审核状态 | 应用 PostgreSQL | 应用自身写入 | 否 |
## 3. 数据库链接快照仅用于诊断
`dm_jst_shop_goods_link_daily` 的真实字段包括:
- `run_date`
- `shop_i_id`
- `shop_sku_id`
- `sku_id`
- `online_status`
- `stock_sync_status`
- `imported_at`
该表缺少大量完整导出链接,不能作为链接维度最终数据源。2026-10-26 对比结果:
- 数据库快照:52,639 行,按条件过滤后匹配通知仅 1,868 条。
- 聚水潭完整导出:246,480 条,全部匹配通知。
- 数据库快照缺少 99% 以上完整链接,只能用于差异诊断。
诊断脚本带覆盖率保护:如果数据库快照结果不足已有链接的 50%,会拒绝覆盖。
可配置:
```text
JST_DISCONTINUATION_LINK_MAX_AGE_HOURS=26
```
手动核对:
```bash
node_modules/.bin/tsx scripts/sync-supply-chain-discontinuation-links-from-dm.ts \
--date=YYYY-MM-DD \
--dry-run
```
## 4. 上游任务归属
已确认此前完整链接由个人 Mac 的 launchd 每日 09:00 启动 RPA 生成,随后写入数据库。
数据库日快照不是完整当前链接。
新的服务器方案:
- Linux 安装器同时安装 `furniture-jst-rpa-browser.service`。
- Chrome 使用持久化 profile,并通过本机 CDP 端口供 RPA 使用。
- 每日入口从服务器 Chrome 导出完整聚水潭数据。
- 正常整单导出最多尝试 3 次,任一次成功立即结束;只有全部失败后才按每批
10,000 个编码降级导出,避免页面偶发失败时立即产生大量分批记录。
- 首次部署优先把已登录的聚水潭 profile 放到 `JST_RPA_PROFILE_DIR`,或在服务器图形/VNC 环境完成一次登录。
- 服务器私有环境文件可配置 `JST_RPA_LOGIN_ID` 和
`JST_RPA_LOGIN_PASSWORD`;持久登录态失效时 RPA 会自动填写账号密码登录页。
凭据只能保存在权限为 `600` 的服务器共享 `.env`,禁止提交到 Git。
- 自动登录遇到验证码、短信验证或密码错误时会明确失败并保留已有链接,不覆盖完整快照。
首次登录示例:
```bash
sudo systemctl stop furniture-jst-rpa-browser.service
sudo env JST_RPA_HEADLESS=false \
JST_RPA_PROFILE_DIR=/opt/furniture-3d-system-prod/shared/jst-rpa-profile \
node scripts/launch-jst-rpa-browser.mjs
```
在图形界面登录聚水潭后停止该进程,再启动 systemd 浏览器服务。
如需允许登录态失效后自动重新登录,在服务器私有环境文件
`/opt/furniture-3d-system-prod/shared/.env` 中配置以下两项后重启每日服务:
```text
JST_RPA_LOGIN_ID=<聚水潭登录账号>
JST_RPA_LOGIN_PASSWORD=<聚水潭登录密码>
```
不要把这些值写入仓库、聊天记录、普通日志或 systemd 命令行参数。
正式部署入口会自动读取本机被忽略的
`furniture-3d-system/.env.jst-rpa-auth`,并在发布时合并上述两项;也可通过
`-JstRpaAuthFile` 指定其他权限受限的凭据文件。部署结束后临时凭据会被删除。
配置后可只验证登录,不执行导出:
```bash
node_modules/.bin/tsx scripts/jst-shop-goods-rpa.ts --action=login
```
## 5. 重跑保护
通知:
- 按 `(barcode, statDate)` 原地 upsert,保持既有通知 ID 不变。
- 同日重跑成功后,当天通知集合必须与最新计算结果完全一致;不再命中的旧通知
会删除,其旧链接按外键级联删除。
- 通知、链接清理和库存预警写入位于同一数据库事务。计算或写入失败时整体
回滚,原有通知和链接保持不变。
- 当天停产/退市商品整体为 0 时也走同一清理事务,不保留上一轮通知。
乐捐:
- 同日重跑不再先删除全部当天记录。
- 已进入 `PENDING_REVIEW`、`APPROVED`、`REJECTED`、`RETURNED` 的记录保留。
- 仅清理不再命中当前结果且仍为 `PENDING_APPEAL` 或 `LEFT_EMPLOYEE` 的旧记录。
- 候选 ID 虽在事务外读取,最终 `DELETE` 会再次校验数据库中的最新状态;候选
读取后刚提交的申诉不会被误删。
- 清理和 upsert 位于同一数据库事务,失败时整体回滚。
链接导入:
- 旧链接删除、新链接解析和写入使用同一事务。
- 最终有效写入为 0 时默认回滚并保留旧链接快照。
- 只有业务明确确认当天正常零链接时,才可给导入命令追加
`--allow-zero-links`,允许清空当天链接。
## 6. 文件交付
- 每日入口:`scripts/run-supply-chain-discontinuation-daily.mjs`
- Linux 安装器:`scripts/install-supply-chain-discontinuation-linux-timer.sh`
- 完整链接 RPA:`scripts/jst-shop-goods-rpa.ts`
- 服务器浏览器:`scripts/launch-jst-rpa-browser.mjs`
- 链接数据库诊断:`scripts/sync-supply-chain-discontinuation-links-from-dm.ts`
- 通知重跑保护:`src/server/repositories/supply-chain-discontinuation-notice.repository.ts`
- 乐捐重跑保护:`src/server/services/supply-chain-discontinuation-penalty-sync.ts`
## 7. 本地验证结果
2026-10-26 使用完整聚水潭导出验证:
- 完整导出:246,480 条链接。
- 链接落库后共 246,480 条,涉及 304 个负责人。
- 同日重跑通知后链接数量和通知 ID 未变化。
- 未下架监控首次同步生成 516 条。
- 未下架监控第二次同步仍为 516 条,无重复。
- 自动化测试覆盖链接条件过滤、去重、新鲜度拒绝、通知/乐捐状态保护。

View File

@ -0,0 +1,256 @@
# 停产下架管理数据源
更新时间:2026-10-20
## 1. 当前状态
停产下架管理已按新口径重新接入数据源:
- 通知下架-编码维度已接入并生成每日快照。
- 通知下架-链接维度已接入并生成每日快照。
- 库存预警已接入,读取当日 `ProductInventorySnapshot`。
- 未下架监控与申诉已接入,读取聚水潭订单头及订单明细生成月度乐捐明细。
- 页面、页签、表头、筛选、分页和导出能力已保留。
- 四个页签均支持导出当前筛选结果:编码维度、链接维度、库存预警、未下架监控与申诉分别使用独立字段和文件名。
- 页面查询接口只读取本地快照,不再实时访问 SQL Server。
- 每日生成入口按北京时间生成当天快照;同日重跑只替换当天数据,历史日期不动。
## 2. 数据流边界
页面和查询接口只读本地落库结果,不直接访问外部库:
```text
外部权威数据源
-> 每日 10:30 同步/计算任务
-> 本地快照及业务表
-> 停产下架页面 API
-> 页面筛选、展示和导出
```
本地承接表:
| 数据类别 | 本地表 |
| --- | --- |
| 虚拟云仓库存 | `SupplyChainVirtualInventorySnapshot` |
| 库存预警(按商品编码 + 仓库) | `ProductInventorySnapshot` |
| 通知下架-编码维度 | `SupplyChainDiscontinuationNotice` |
| 通知下架-链接维度 | `SupplyChainDiscontinuationNoticeLink` |
| 未下架订单和乐捐 | `SupplyChainDiscontinuationPenalty` |
## 3. 权威数据源
### 3.1 简尚库存
来源:`JianSun.dbo.jst_inventoryInfo_view`
页面使用“公有可用数”口径:
```text
公有可用数
= 库存
- 虚拟云仓库存
- 订单锁定数(包含缺货仓订单占有数)
```
- 虚拟云仓库存来自 `JianSun.dbo.jst_virtualstockStocksInfo`,按商品编码汇总。
- 公有可用数小于 0 时按 0 处理。
- 仓库名称通过 `JianSun.dbo.jst_warehouseInfo` 映射。
- `三方-南昌樵舍仓` 作为京喜渠道仓单独汇总,不计入普通简尚库存。
- 渠道专用仓库存不计入简尚主库存,单独返回到“渠道专用仓库存”和“专用仓备注”。
- 库存预警的“库存所在仓库”只统计实际计入公有可用数且有正库存的普通仓。
- 同一商品在多个仓库有库存时拆成多条明细,不合并仓库名称。
- 虚拟云仓库存按各仓库存占比分摊到仓库明细,分摊后各行公有可用数合计与原商品汇总值一致。
- 虚拟云仓先落到本地 `SupplyChainVirtualInventorySnapshot`,日任务只读本地快照,避免链接服务器偶发超时。
- 日常通过 `scripts/sync-supply-chain-virtual-inventory-snapshot.ts` 从 `JianSun.dbo.jst_virtualstockStocksInfo` 同步;链接服务器不可用时,可用 `scripts/import-supply-chain-virtual-inventory-snapshot.ts` 导入页面导出文件作为兜底。
### 3.2 工厂库存
来源:`plm_productDiscontinuation`
- 使用 `remainingStock` 作为供应商剩余库存量。
- 按 `skucode` 去重,空条码忽略;同一商品编码存在多条记录时,以 `createTime DESC, Id DESC` 最新创建的一条为准。
### 3.3 商品状态
商品状态以聚水潭源表 `rpt.jst_sku.other_5` 为准,只纳入实时状态为停产、退市的编码。
- 本地产品全量档案只用于补充 SKU、成本、名称等资料。
- 本地档案同步较慢时,不能用档案中的旧状态决定当天停产/退市结果。
- 若源表有、档案暂时没有的新停产编码,使用源表条码、货号和成本兜底进入计算。
### 3.4 组合商品
来源:
- `rpt.jst_comboProductInfo`:组合商品主表。
- `rpt.jst_comboProductItem`:组合商品与子商品明细。
规则:
- 只检查 `enabled=1` 的组合商品。
- 子商品数量 `qty>0` 才参与判断。
- 组合中任一子商品为停产/退市,且该子商品的简尚库存 + 工厂库存 = 0,则组合商品进入通知下架。
- 支持组合嵌套:若子商品本身也是组合,继续向下递归检查,并对循环引用做保护。
- 组合商品自身库存仍按组合编码从简尚库存和工厂库存表读取,用于页面展示。
### 3.5 可替换商品
来源:`rpt.pm_replacementManage` 与 `rpt.jst_sku_replace_rule`,两张表按同一套货号继承和条码展开规则合并。
- `pm_replacementManage` 只读取 `delete=0` 且替换货号非空的数据;聚水潭表按启用/删除字段可用性自动过滤无效行。
- 聚水潭替换表使用 `rpt.jst_sku_replace_rule`,只读取最新 `snapshot_date`;原编码取 `sku_id`,替换编码取 `data`,`data` 中逗号分隔的多个候选按顺序保留。
- 同一原编码同时存在两张表规则时优先使用聚水潭替换表;聚水潭表不可读取时记录告警,并继续使用 `pm_replacementManage`。
- 优先按完整商品编码匹配;完整编码没有规则时,按商品档案货号继承同货号其他条码的替换规则。
- 兼容反向录入:若当前停产条码或其货号等于 `replaceSku`,则反向使用对应 `sku` 作为替换来源。
- 同一原编码存在多条规则时,取 `createTime` 最新的一条。
- 替换目标能在商品档案映射时,必须展开为具体 barcode;同货号有多个条码时优先销售、有库存、最近更新的条码。
- 替换目标完全无法映射时,仍展示规则中的替换编码,但是否换图、区别点和价格差允许为空。
- 是否换图取 `isSwapping`,1 为是。
- 区别点取 `differentPoints`。
- 价格差沿用现有页面口径:替换商品成本 - 原商品成本。
### 3.6 店铺链接
正式来源:聚水潭“店铺商品管理”完整前台导出(服务器 Chrome + RPA)。
- 每日编码维度生成后,服务器 Chrome 自动批量查询并导出完整链接。
- 默认先执行单次全量导出,连续尝试 3 次;第 1、2 次失败后重置页面再重试,
任一次成功立即结束;只有三次都失败后才切换为每批 10,000 个编码的分批导出。
- 只保留 `online_status='是'` 且 `stock_sync_status='开启同步'` 的导出记录。
- 全部批次成功后,在数据库事务中替换当天链接;失败时保留已有完整链接。
- 最终有效写入为 0 时默认回滚;确有业务依据的正常零链接必须显式使用
`--allow-zero-links`,避免异常空文件清空当天快照。
- 每个平台店铺商品编码独立展示,不合并多个平台店铺商品编码,不加总线上库存。
- 运营负责人取 `op_TargetManagement.ider`(ID负责人)。
- 部门负责人取 `op_TargetManagement.currCater`。
- 利润中心取 `op_TargetManagement.dept`。
- 同一商品ID在负责人表中存在多条时,按 `realTime DESC, Id DESC` 取最新一条。
- 最终结果落到本地 `SupplyChainDiscontinuationNoticeLink`,页面不直接读取 Excel 或大表。
链接维度页面字段顺序:
1. 统计时间
2. 平台
3. 店铺名称
4. 商品ID
5. 平台店铺商品编码
6. 线上商品编码
7. 线上库存
8. 运营负责人
9. 部门负责人
10. 利润中心
11. 处理说明
12. 可替换商品编码
13. 区别点
14. 是否换图
15. 价格差
16. 渠道专用仓库存
前 10 项来自聚水潭完整导出和负责人表;后 6 项关联同一系统商品编码的编码维度快照。
历史数据源差异留档:
- 聚水潭前台导出:234,656 行、234,641 个唯一链接。
- legacy 链接结果:779,412 行。
- 完全匹配:234,337 个链接。
- legacy 多余:545,075 个链接。
- 仅使用 `jst_skumap` 主表:447,558 行。
- 主表与前台精确匹配:182,276 个链接。
- 主表漏掉前台链接:52,380 个。
- 主表多出前台链接:265,282 个。
- `jst_skumap.enabled=1` 无法完整复刻前台“库存同步状态 + 店铺链接是否上架”的当前视图。
`dm_jst_shop_goods_link_daily` 仅用于差异诊断:
- 2026-10-26 快照共 52,639 行,`imported_at` 为当天 10:18。
- 同日聚水潭完整导出为 246,480 条;数据库快照匹配通知后只有 1,868 条,缺少 99% 以上完整链接。
- 因此该表不能作为链接维度最终数据源。
数据库快照诊断命令:
```bash
pnpm exec tsx scripts/sync-supply-chain-discontinuation-links-from-dm.ts --date=YYYY-MM-DD
```
只做只读核对时加 `--dry-run`。脚本默认在结果不足已有链接的 50% 时拒绝覆盖。
Linux 每日任务(北京时间 10:30):
```bash
sudo bash scripts/install-supply-chain-discontinuation-linux-timer.sh
```
任务顺序为“虚拟云仓库存快照 → 编码维度通知快照与库存预警快照 →
聚水潭完整链接导出 → 未下架监控与乐捐快照”。
每日任务启动时会先按当前 `prisma/schema.prisma` 重新生成 Prisma Client,
并输出各数据源阶段日志,避免加载到旧客户端或无法判断卡在某一张外部表。
系统不再依赖个人 Mac;服务器使用独立的 `furniture-jst-rpa-browser.service`
和持久化 Chrome 登录态。
RPA Python 路径由 `JST_RPA_PYTHON` 明确指定;Linux 安装器默认
`/usr/bin/python3`,可用 `DISCONTINUATION_PYTHON_BIN` 覆盖。同步动作会在连接
浏览器和开始导出前预检 Python,路径或执行权限有误时立即失败。
正常整单导出次数默认由 `JST_RPA_ATTEMPTS=3` 控制,分批兜底大小默认由
`JST_RPA_FALLBACK_BATCH_SIZE=10000` 控制。
聚水潭账号密码只从服务器私有 `.env` 的 `JST_RPA_LOGIN_ID` 和
`JST_RPA_LOGIN_PASSWORD` 读取;持久登录态有效时不会重复提交登录,
失效时才自动填写登录页。凭据不得写入仓库或日志。
只补库存预警、不重跑通知和链接时:
```bash
pnpm exec tsx scripts/generate-supply-chain-inventory-alert-snapshot.ts
```
服务与定时器日志通过 `journalctl -u furniture-discontinuation-daily` 查看。
> 预策不作为任何数据源或参考流程;其数据正确仅因为每天人工导入了聚水潭前台文件。
### 3.7 订单
- 其他平台订单头:`jst_Orders`
- 其他平台订单明细:`jst_orderitem`
- 淘系订单头:`jst_qmcust_orderlist`
- 淘系订单明细:`jst_qmcust_orderlist_item`
订单只用于“未下架监控与申诉”和乐捐计算,不参与通知编码或库存预警:
- 订单明细通过 `shop_sku_id` 与通知链接维度的平台店铺商品编码匹配。
- 仅统计当月有通知下架的链接。
- 排除订单状态 `Sent`(已发货)、`Delivering`(发货中)、`Split`(被拆分)、`Merged`(被合并)。
- 仅当订单付款日期达到该链接首次通知下架日期后的第 2 天(含),才进入未处理明细;不按具体时分判断。
- 页面默认统计日期为北京时间当天,可手动切换历史日期。
- 未下架监控按“任务生成日期”归档,并实时计算到当前时间;通知后第 2 天起的所有后续订单都计入,不只是第 2 天当天。
- 每日任务固定处理三天前的通知队列:23 日统计 20 日通知、22 日起流入的订单;24 日统计 21 日通知、23 日起流入的订单。
- 同一订单命中同一平台店铺商品编码只计一笔;同一订单命中两个链接计两笔。
- 乐捐唯一键为“内部订单号 + 平台店铺商品编码”,重复来源数据不会重复计费。
- 运营可对自己的待申诉明细上传凭证并发起申诉;有业务管理操作权限的人员可审核通过、驳回或打回。
- 申诉和审核均支持按链接批量处理:同一平台店铺商品编码下符合条件的订单一次覆盖。
- 审核通过后原乐捐记录保留,但页面展示与月度汇总按 0 元计算,实现自动减免。
- 负责人离职或无法匹配到员工目录在职记录时,状态显示“人员已离职”,乐捐金额按 0 元计算且不能申诉。
- “人员已离职”可作为申诉状态筛选;筛选后可批量向部门负责人的钉钉个人工作通知推送处理提醒。
- 未下架监控页顶部显示本月有效乐捐合计(¥10/单),审核通过的明细不计入合计。
- 未下架监控支持个人列显示/拖拽排序、紧凑展示和筛选字段配置。
## 4. 业务口径
- 通知下架:商品状态为停产或退市,且简尚库存 + 工厂库存 = 0。
- 京喜渠道判断:普通简尚库存、工厂库存和渠道专用仓库存均为 0,仅 `三方-南昌樵舍仓` 有库存时,处理说明为“仅京喜下架”;存在有效替换款时为“仅京喜替换”。
- 南昌樵舍仓无库存或其他仓库仍有库存时,是否通知及替换判断继续沿用原逻辑;需要进入通知时,处理说明显示“全渠道下架”或“全渠道替换”。
- 组合通知:组合商品任一子编码为停产/退市且合计库存为 0,整个组合进入通知下架。
- 库存预警:商品状态为停产或退市,且公有可用数 + 工厂库存为 1 至 5;0 库存只进入通知下架。
- 多仓商品先按商品编码判断是否进入预警,再拆成仓库明细;工厂库存没有仓库来源时单独展示为仓库为空的一条明细。
- 通知后订单监控:付款日期达到该链接首次通知下架日期后的第 2 天(含)。例如 20 日通知,22 日全天的订单都计入。
- 排除订单状态:已发货、发货中、被合并、被拆分。
- 乐捐:按“订单 + 链接”组合计算,每组 10 元;同一订单命中两个链接计两笔。
- 乐捐按月累计,次月 1 日开启新账期,历史明细保留。
- 运营人员只能查看自己的订单和申诉数据。
## 5. 接入约束
- 新同步任务负责写入本地快照,页面 API 不承担跨库实时计算。
- 编码维度、链接维度和库存预警必须使用同一统计日的库存口径。
- 历史通知数据一经生成不得覆盖,每日任务只新增当日结果。
- 同一统计日允许重跑并替换当天快照,其他日期保持不变。
- 统计日期统一使用北京时间。
- 页面查询不得直接访问任一外部数据源。

View File

@ -0,0 +1,212 @@
# 清货管理(2026-10-20)
## 1. 页面与入口
- 页面:`/supply-chain/management/clearance`
- 菜单:供应链中心 > 经营管理 > 清货管理
- 五个页签:清货池、清货任务、清货分数、清货复盘、清货配置
## 2. 数据与边界
页面和接口只读取本地 PostgreSQL 快照/业务表,不实时访问 SQL Server。
| 数据 | 本地表 | 说明 |
| --- | --- | --- |
| 清货池 | `SupplyChainClearancePoolSnapshot` | 产品全量档案的月度快照;包含 `periodKey`、`generatedAt`;只有经管重新生成时才会更新 |
| 清货池生成记录 | `SupplyChainClearancePoolGeneration` | 每个清货月份只有一条经管生成记录,保存生成时间、生成人和商品数 |
| 清货任务 | `SupplyChainClearanceTask` | 按完整月份保存任务量、折扣、原因、审核状态;同月调整记录独立保留 |
| 流转记录 | `SupplyChainClearanceTaskEvent` | 提交、通过、驳回、调整、运营确认移除 |
| 复盘快照 | `SupplyChainClearanceReviewSnapshot` | 月度整体/运营中心/部门负责人/优先级/折扣维度 |
| 销售出库进度 | `SupplyChainProductArchiveSalesPerformanceDaily` | 按条码汇总清货月份内 `shipQty`;页面对已确认任务实时覆盖进度,脚本持久化到任务 `clearedQty` |
| 时间窗口 | `SupplyChainClearanceApplicationWindow` | 每个清货月份包含次月任务制定窗口和次月折扣调整窗口;清货周期始终为一个完整自然月 |
| 清货池库存 | 聚水潭库存、PLM 清货表 | 经管生成时实时读取源表,计算结果落到清货池月度快照;页面不直接查询源表 |
清货池月度生成:
- 只有 `clearanceReview.edit` 权限的经管可以在「清货配置 > 时间窗口」中同时选择是否生成清货池。
- 清货池页面标题说明会读取当前月份的生成记录,展示生成时间、生成人和生成方式;未通过配置生成或没有生成记录时不追加该说明。
- 经管选择 `YYYY-MM` 清货月份后,系统按当前产品全量档案补充商品资料,并从聚水潭库存和 PLM 清货表重新计算库存。
- 简尚总库存 = 大货仓实体库存 + 前置仓库存 + 专用仓库存;库存直接读取上游最新视图 `JianSun_Old.JianSun.dbo.jst_inventoryInfo_view`,四类仓库统一使用公有可用数 `qty - order_lock`。大货、前置、专用仓保留负数参与汇总,例如缺货仓的负库存会冲减大货仓;代发仓负数按 0。
- 大货仓:仓库类型为 `成品专用仓-真实库存` 或 `成品发货仓-真实库存`,剔除新外贸仓、张小飞代发仓、济南外贸仓、南昌樵舍仓和 `简尚-运营` 开头的专用仓。
- 前置仓:仓库名称包含 `南昌樵舍仓`。
- 专用仓:仓库名称以 `简尚-运营` 开头。
- 代发仓:仓库类型为 `成品代发仓-虚拟库存`,负数参与汇总,且结果小于 `77777`。
- 简尚总库存 = 大货仓 + 前置仓 + 专用仓 - 最新虚拟云仓库存;云仓库存完全抵扣后为 0 的商品不进入清货池。
- 供应商剩余库存读取 `rpt.plm_productDiscontinuation.remainingStock`,同一商品取 `state=2/3` 中 `updateTime, Id` 最新的一条。
- 工厂总库存 = 供应商剩余库存 + 代发仓;已移除贴单发货仓。
- 简尚 + 工厂总库存 = 简尚总库存 + 工厂总库存;清货池只写入和展示总库存大于 0 的商品。
- 两清协议按 `rpt.plm_productDiscontinuation` 判断:存在任意状态记录、但没有 `state=2/3` 的有效库存记录时显示「工厂库存未统计」;PLM 完全无记录或已有有效库存记录时留空。
- 清货池「日销」= 产品全量档案 `sales3d × 0.5 + sales7d × 0.3 + sales15d × 0.2`,结果保留两位小数,不取采购日销 `dailySales`;旧数据仅存在 7 日销时回退到 `sales7d`。
- 清货池「最近入库时间」在简尚总库存为 0 时显示「简尚无库存」;工厂仍有库存时总库存和工厂库存照常保留,入库天数仍按真实采购入库日期计算。
- 清货池生成排除二级分类为空和「其他 - 福利品」的商品,并单独排除编码 `GBGJ-MPWL01N126V01`。
- 优先级规则和固定任务规则的条件值从当前清货池读取候选并支持多选;仅「包含 / 不包含」使用自由文本输入,「为空 / 不为空」无需填写值。
- 清货池「停产时间」按商品编码关联 `rpt.plm_productDiscontinuationDetail_flow` 与 `rpt.plm_productDiscontinuation_flow`:仅取已完成的停产流程(`flowtype=14`、`flowStatus=1`)、有效停产明细(`prostatus=1`、`state=0`),以最新流程的 `applyTime` 填入年月日;销售状态为「销售」的商品无论是否存在历史停产流程都清空停产时间。
- 清货池「利润中心」按产品档案的运营负责人 `opsHead`,在人员信息表 `SupplyChainManagementOwnerConfig.operationOwner` 中精确匹配后取 `profitCenter`;未匹配时留空。
- 清货组合匹配只处理产品全量档案的单双包字段原本为「双包」或「三包」且有库存的商品;「非双包」和空值即使出现在组合明细中,也不改写包型,清货商品编码直接填当前商品编码。
- 组合读取启用的 `jst_comboProductInfo` 和 `jst_comboProductItem`,并严格按档案包型匹配:双包只取 2 个不同商品编码的组合,三包只取 3 个不同商品编码的组合;1 个、4 个及以上或其他包型组合不参与。
- 同一商品编码命中多个组合时只选择一个,且组合编码必须以 `ZH-` 开头并直接包含当前商品编码;`运营组合套装xxx`、`MKH-运营组合套装xxx` 等无法从编码识别组成的组合不使用。具体组合中优先选择所有组成商品数量均为 1 的组合,其次选择总数量更小、存在 A/B 对应件、无异常且编码稳定的组合。例如同时存在 `ZH-...A-2-...B-2` 和 `ZH-...A-1-...B-1` 时,必须选择后者。
- 原有单商品明细行不新增、不拆分,`清货商品编码` 只填写选中的完整具体组合编码;不是 `ZH-` 开头或找不到具体组合时,填当前商品编码。
- 对选中的组合递归检查组成商品:任一组件状态为停产或退市且总库存小于等于 0 时,组合状态标记「多包异常」,并记录首个异常原因;同月二次折扣调整不清空或重算这些组合字段。
- 组合清货总量取所有组成商品「简尚+工厂总库存」的最小值;非组合商品取自身简尚+工厂总库存。组合所有组成商品的简尚库存都大于 0 但数值不一致时标记「多包库存不等」。
- 可用天数按 `清货总量 / 日销` 向下取整;日销为 0 时显示「无销量」,日销为空时留空。
- 清货池页面和导出中的日销最多保留两位小数;整数不补小数位,可用天数仍使用原始日销精度计算。
- 「多包异常」商品禁止填写任务、申请折扣、推送草稿或由经管调整后通过;「多包库存不等」允许填写任务和申请折扣,但任务量上限仍使用组合最小清货总量。
- 常规生成完成后,向钉钉个人通知发送一条汇总提醒,只说明「多包异常」和「多包库存不等」已生成及各自数量,不推送商品编码或组合明细;收件人从在职员工目录按姓名匹配,默认「卷帘大将」,可通过 `SUPPLY_CHAIN_CLEARANCE_COMBO_NOTIFICATION_RECIPIENT` 调整。二次折扣调整不重新打标,也不重复发送该通知。
- 同一清货月份允许经管不限次数重新生成;再次生成时事务内覆盖该月生成记录和商品快照,未再次生成时数据保持不变。
- 生成方式分为「常规生成」和「二次折扣调整」。常规生成重算完整商品池;二次折扣调整必须基于已生成月份,只更新现有商品的大货仓、前置仓、专用仓、代发仓、供应商剩余库存、简尚库存、工厂库存、总库存、日销(7 日销)和可用天数,商品集合、状态、负责人、利润中心、两清协议及其他字段保持不变。
- 「清货配置」是紧邻「清货复盘」的独立页面,不再以清货池工具栏弹窗展示,且仅对具备 `clearanceReview.edit` 权限的经管显示;页面包含「时间窗口」「优先级规则」「固定任务规则」「折扣限制」四个子页签。优先级和固定任务规则由经管人工维护并持久化到 `SupplyChainClearanceConfig`,每条规则填写目标值、多个条件以及条件关系(同时满足 `ALL` / 满足任一 `ANY`);条件关系通过点击条件之间的「且 / 或」切换。同一商品命中多条规则时采用序号最小的一条。
- 「折扣限制」按商品编码或 SPU 人工维护禁止申请折扣清单,匹配不区分大小写,重复配置自动合并;命中后清货池折扣输入禁用,保存草稿、提交、审核、申请人确认及经管强制推送都会在服务端再次校验,不能通过接口绕过。
- 优先级字段是人工自定义的优先级名称,不使用固定的高/中/低字典;规则值和兜底值都由经管自由命名。
- 优先级规则可判断销售状态、商品状态、分类、品牌、供应商、单双包、合作状态、负责人、商品编码、货号、各类库存、日销、可用天数、成本价、清货商品编码、清货总量、多包标记、利润中心、两清协议、停产时间和入库天数;支持等于、不等于、包含、不包含、数值比较以及为空/不为空。
- 有预设值的文本字段支持勾选多个值;等于和包含在任一选中值命中时成立,不等于和不包含在避开全部选中值时成立。数字字段及自由文本字段保持单值输入。
- 优先级规则只在常规生成清货池时计算并写入 `extra.priority`,同时保留命中的规则 ID 和序号;二次折扣调整不重算优先级。未命中任何规则时优先级留空。
- 固定任务规则使用安全算术公式,可引用简尚库存、工厂库存、总库存、日销、可用天数、清货总量、成本价、入库天数等数值字段,支持数字、括号和加减乘除;只解析受控字段和运算符,不执行任意代码。公式结果向下取整,字段无数值或计算无效时固定任务量留空。
- 优先级和固定任务配置都支持选填兜底值:当所有规则均未命中时,使用人工填写的兜底优先级或兜底公式;留空时保持为空。
- 常规生成清货池时把首个命中公式的结果写入 `extra.fixedTaskQty`,同时保留公式、规则 ID 和序号;二次折扣调整不重算。
- 页面可通过「清货月份」切换已经生成的月度清货池。
历史导入和诊断脚本:
- 当前全量映射:`npm run import:clearance-pool-archive`
- 把产品全量档案全部行写入清货池,销售状态取 `productStatus`;
- 《10月清货池》38 个业务抬头覆盖到对应字段,其余抬头保存在 `extra` JSONB;
- 商品名称和品牌作为商品档案列保留,可在列设置中显示;
- 已有快照中的库存值优先保留,其余使用档案库存字段。
- 停产/退市手工诊断同步:`npm run sync:clearance-pool:discontinued`。该命令会重写指定日期的清货池快照,只用于单独核对,不纳入清货池库存刷新流程。
- 使用 `rpt.jst_sku` 的实时停产/退市状态;
- 简尚库存取 `jst_inventoryInfo_view`,按现有规则排除专用仓并扣减虚拟云仓库存;
- 工厂库存取 `plm_productDiscontinuation` 中同一商品编码最新创建记录的 `remainingStock`;
- 只写入总库存 > 0 的款式。
- 本机导入(已有合并表时):`npm run import:clearance-pool -- --file=<csv> --date=YYYY-MM-DD`
- 复盘快照导入:`npm run import:clearance-review`
- 「清货配置 > 时间窗口」默认选中当前正在查看的清货月份,并提供「不生成」「常规生成或重新生成」「二次折扣调整生成」三个选项。
- 当前正式生成只保留产品全量档案字段,以及重新计算的库存、7 日销、可用天数、利润中心和两清协议;清货商品编码、清货批次、优先级、固定任务量等清货专有字段继续留空,等待后续逻辑接入。
- 档案基线重建:`npm run reset:clearance-pool:archive -- --date=YYYY-MM-DD --apply`;只更新现有快照字段,不新增或删除商品。
- 已处理库存实验命令 `sync:clearance-stock:stock-monitor` 当前暂停,不作为清货池正式库存来源。
## 3. 任务流转
清货任务默认列按经管提供抬头展示:统计月、spu、清货商品编码、清货价、成本价、折扣预警、价格类型、运营负责人、部门负责人、利润中心、期初任务量、真实任务量、发货数量、任务清货进度、退款数量、简尚库存、工厂库存、总库存、日销、可用天数、全国运费、清货批次、优先级。任务页只展示经管已推送的 `APPROVED` 明细,因此不再展示任务审核状态;统计月固定为第一列。
任务计算口径:
- 清货商品编码映射当前任务月份清货池的 `extra.clearanceCode`。
- 成本价取产品全量档案 `costPrice`;如果清货商品编码是组合款,则递归汇总组合明细中所有组成商品的产品档案成本,并按组成数量计算。
- 清货价 = 成本价 × 申请折扣;折扣为空时直接展示成本价。
- 折扣预警仅在存在折扣时计算:任务清货进度小于 50% 标记「异常」,大于等于 50% 标记「正常」。
- 期初任务量取同一申请人该条任务最早一次草稿/提交记录中的任务量,不随审核调整覆盖。
- 任务清货进度 = 发货数量 / 真实任务量。
- 简尚库存、工厂库存、7 日销取每日更新的产品全量档案;总库存 = 简尚库存 + 工厂库存;可用天数 = 总库存 / 7 日销并向下取整,日销为 0 时显示「无销量」。
- 运营负责人、部门负责人、利润中心、清货批次和优先级映射当月清货池数据;发货数量和退款数量来自销售出库日数据;清货价、价格类型和全国运费在接入各自数据源前按上述规则展示或保持为空。
- 运营在清货池填写并保存任务申请,可以查看清货池全部商品;只有清货经管可以审核申请,审核权限走 `clearanceReview.edit`。申请人确认和正式任务推送分别校验操作/审核权限。
- 同一商品、同一期次允许不同运营分别保存自己的任务申请;草稿按申请人隔离,互不覆盖。经管可查看该编码下全部申请;普通运营只能读取和看到本人申请、审核结果,不能看到其他人的申请内容、审核状态或状态计数。
- 审核状态流转为 `DRAFT/PENDING 待审核` → `READY 已审核` → `APPROVED 已确认`。经管审核通过后,申请人只看到自己的“已审核”结果;点击“确认无误”后自动写入清货任务明细并切换到清货任务页,无需经管再次推送。未确认的 `READY` 申请仅经管有权限强制推送。任务仍按申请人拆分,`spu负责人` 显示实际申请人。
- 清货任务页只做数据展示,不承载审核或调整操作。
- 清货池展示任务流程和「全部任务 / 待审核 / 已通过 / 按调整后通过 / 已驳回」审核结果快捷按钮;经管看到全部申请统计,普通运营仅看到本人申请统计。点击后筛选清货池明细,导出时沿用同一结果。清货任务页不展示利润中心汇总、状态分段按钮、重复搜索框或记录类型切换;任务周期、筛选项、刷新、导出和列设置合并为同一行。任务明细每页 50 条,表格下方使用紧凑分页控件展示总条数、本页条数和上一页/下一页。没有任务时仍保留完整表头,只在表体显示原有空态提示。
- 清货池任务流程和状态概览并入清货月份栏:任务流程压缩为单行步骤条,销售、退市、停产各自展示编码数量、简尚库存和总库存,长期滞销库存当前显示待配置;窄屏时状态卡片自动换行。指标按整个生成月份汇总,不随筛选条件变化,宽屏展示状态卡片后为筛选和明细释放纵向空间。
- 清货池明细最右侧为“审核结果”列。待审核、已通过、按调整后通过、已驳回分别标识;已驳回结果附带驳回原因。经管可点击结果查看申请详情;申请人端仅显示本人结果,并在审核通过时突出显示“确认审核结果”。申请人确认后自动跳转清货任务页;强制推送和自动确认都会刷新清货池、审核计数与任务明细。
- 审核弹窗顶部展示商品编码、清货商品编码、SPU、运营负责人、部门负责人、利润中心、清货批次、商品状态、优先级、简尚/工厂/总库存、清货总量、7 日销、可用天数、停产时间和入库天数,并展示同商品编码最近月份的任务量、折扣、完成量和完成率。每条申请可“通过原申请”“调整后通过”或“驳回”;调整后通过必须修改任务量或折扣,调整原因选填,驳回必须填写驳回原因。待审核弹窗不提供强制推送。
- 经管强制推送只处理 `READY`(已审核未确认)记录,入口保留在清货池明细下方,一次推送当前月份和任务期次的全部未确认记录;申请人已确认的记录会自动进入清货任务,不在强制推送范围内。
```text
经管选择清货月份并确认次月任务制定、次月折扣调整时间(起止时间)
-> 只有在对应时间窗口内,运营才能填写任务量 + 折扣 + 原因
-> 三个字段完整后离开输入框自动保存为 DRAFT 草稿
-> DRAFT / PENDING 待审核
-> 经管审核通过 -> READY 已审核
-> 申请本人确认无误 -> APPROVED 已确认,自动进入清货任务
-> 或由经管强制推送未确认的 READY 申请 -> APPROVED 已确认
-> 经管驳回 -> REJECTED(必须填写原因,仅原申请人可见)
-> 运营确认移除:REMOVED
-> 经管调整任务量/折扣后重新审核
```
同一月份、同一商品、同一种记录类型只允许一条非 REMOVED 任务。调整窗口开放后,即使已有月初确认任务,也可以为同款创建独立的调整记录,初始任务不覆盖、不删除。
分数和复盘按商品去重:同一商品存在初始任务和调整记录时,只取最新一期已确认记录参与任务量、完成量和维度汇总,历史记录继续保留用于追溯。
销售状态商品不允许申请折扣:清货池折扣和折扣原因都固定为空,只允许填写任务量;草稿保存、批量推送、经管调整接口均强制校验,不能通过接口绕过。
非销售状态商品申请折扣时必须先填写任务量,填写折扣时必须填写原因;没有折扣需求时折扣留空即可,可以只填写任务量。
草稿单元格在输入过程中只更新本地引用,失焦时立即提交保存,避免逐字重绘整张 50 行明细;保存接口并行校验时间窗口和库存快照,并并行查询本人草稿及有效任务。折扣输入保留 200ms 防抖,保存期间继续修改会在首次保存完成后自动补存最新内容。
时间窗口规则:
- 只有 `clearanceReview.edit` 可以配置和确认时间窗口(「清货配置」页签只对经管显示);选择生成时,窗口保存成功后再执行清货池生成。
- 未配置、未开始或已结束时,清货池进入只读,运营不能保存草稿,经管不能推送正式任务;后端会强制校验。经管审核和申请人确认审核结果不受该限制。
- 清货月份始终是完整自然月,任务和后续折扣调整都归属该清货月份。
- 次月任务制定:目标清货月前一个月的最后 7 天。
- 次月折扣调整:目标清货月第 8 天起的第一个工作日,自动跳过周末;法定节假日可由经管人工调整。
- 「清货配置」页面会按上述规则自动填入建议时间,经管可修改后确认。
执行进度:
- 清货任务页实时读取本地销售出库日数据,口径为清货月份内 `shipQty` 合计,仅对 `APPROVED` 任务生效。
- 每日销售出库同步完成后会自动回写上月和当月任务进度;也可单独执行 `npm run sync:clearance-progress -- --period=YYYY-MM`,不传月份时同步当月。
- 回写内容为 `clearedQty`、`lastProgressAt`;销售源未覆盖目标月份时不覆盖上次进度。
- 本地销售出库数据未覆盖目标月份时保留上次持久化进度,页面会显示覆盖到哪一天。
草稿接口:
- `POST /api/supply-chain/clearance/tasks/draft`:表内自动保存(同一商品同一记录类型重复保存会更新同一条草稿)。
- `POST /api/supply-chain/clearance/tasks/push`:把勾选的 DRAFT 批量转为 PENDING,并写 SUBMIT 流转记录。
- `GET/POST /api/supply-chain/clearance/window`:读取指定清货月份的次月任务制定/折扣调整时间;经管保存起止时间并确认。
- `POST /api/supply-chain/clearance/tasks/:id/review`:直接通过或驳回。
- `POST /api/supply-chain/clearance/tasks/:id/approve-adjust`:待审批任务调整任务量/折扣后通过。
- `POST /api/supply-chain/clearance/tasks/:id/adjust`:经管调整已驳回任务后确认。
- `POST /api/supply-chain/clearance/generate`:经管常规生成或重新生成清货池;二次折扣调整模式仅刷新库存、日销和可用天数。
## 4. 三张明细表的通用能力
清货池、清货任务、清货分数均支持:
1. 右上角齿轮「显示设置」:与限价管理一致,统一提供紧凑展示、列设置、筛选设置三个入口。
2. 个人列设置:搜索、勾选要显示的字段,上下移动字段顺序;列宽直接在明细表头右侧拖动手柄调整,松手后按用户保存到浏览器本地。
3. 顶部筛选:在「显示设置 → 筛选设置」中选择要展示的筛选项后,字段以横向下拉框显示;候选值可搜索并直接点击多选,也可粘贴或输入空格、逗号、分号、顿号、换行分隔的批量值后回车应用。点击或批量输入均使用精确等于匹配,避免选择「双包」时误包含「非双包」。同一字段多值按 OR 组合,不同字段按 AND 组合,并以标签展示已选条件、逐项清除或清空全部。清货池筛选在服务端执行,覆盖普通字段、日期、数值和 `extra` JSONB 抬头;任务量、折扣、原因按当前清货月份的动态任务数据筛选,任意人工配置字段的候选值通过 `GET /api/supply-chain/clearance/options` 加载。
4. 导出 Excel:清货池提供「导出明细」和「导出明细(含图片)」两个入口,统一放在筛选行右侧。普通明细移除图片列并生成 `清货池-明细` 文件;含图片导出会强制加入图片列、抓取商品图片,并生成 `清货池-含图片` 文件。两者都按当前列顺序、列宽和筛选结果导出,并按每页 5,000 条自动循环读取全部匹配记录,不再截断为首页。
5. 横向滚动:明细底部固定显示可拖动的横向滚动条,位于分页操作区上方;同时保留触控板横向滚动。
清货池默认展示《10月清货池》38 个抬头,任务量、折扣、原因、任务状态固定放在列顺序末尾;销售状态单独作为业务字段展示。
清货池顶部不再重复提供商品搜索框,统一通过下方人工配置筛选项查询。
## 5. 权限
页面可见:`businessManagement.view`
| 动作 | 权限节点 |
| --- | --- |
| 清货池定任务、提交折扣申请、运营确认移除 | `clearanceOperation.edit` |
| 经管通过、驳回、调整任务 | `clearanceReview.edit` |
| 经管生成或重新生成清货池 | `clearanceReview.edit` |
| 查看清货池、任务、分数、复盘 | `businessManagement.view` |
默认角色:
- `operation` 角色默认获得清货运营操作权限;
- `management` 角色默认获得清货经管审核权限;
- 管理员默认拥有全部权限。
## 6. 当前版本边界
- 清货任务的 `clearedQty` 已接入本地销售出库汇总;页面实时展示,持久化由 `sync:clearance-progress` 完成,生产定时任务仍需按部署环境配置。
- 所有旧导入、重置和库存同步脚本仍会拒绝直接修改已生成月份;更新必须通过经管「清货配置 > 时间窗口」中的生成选项执行。
- 清货分数先使用示例权重:完成度 60 + 毛利率 25 + 货值回收 15;历史周期使用复盘快照计算,当月任务只有完成度可计算。
- 清货分数档位按完成率上下限区间匹配,上下限均可配置为包含或不包含;区间不可重叠,旧“最低完成率”配置会自动转换为连续区间。
- 清货复盘优先使用 `2026年4月清货复盘.xlsx` 导入的历史快照;没有快照的月份由最新一期已确认任务 + 本地销售出库进度自动生成。
- 当月自动复盘暂不计算毛利率、结算毛利率和货值回收率,字段保持为空,待结算口径接入后再补齐。
- 清货池导出单次最多 5,000 条,按当前筛选范围导出;超出时在「导出说明」中提示实际总数。
- 本机已导入 2026-10-20 全量清货池快照(79,959 个产品,销售 51,619 / 停产 21,010 / 退市 3,548 / 未标记 3,782)。
## 7. 验证命令
```bash
npm run type-check
npm test -- src/server/services/__tests__/supply-chain-clearance.service.test.ts
npm test -- src/server/services/__tests__/supply-chain-clearance-review.builder.test.ts
npm test -- src/lib/clearance-filters.test.ts
npm test -- src/lib/supply-chain-permission-settings.test.ts
```
全仓 `type-check` 当前存在与本次改动无关的历史错误(`node_modules.pnpm-mixed`、`src/hooks/usePerformance.ts` 等);本次新增文件无 TypeScript 报错,ESLint 无 error。

View File

@ -0,0 +1,135 @@
# 限价与破价判定规则(业务确认版)
> 适用范围:限价管理 · 破价订单审核(`/supply-chain/management/custom-table` → 破价订单审核)
> 本文件为业务口径的唯一来源;规则变更请改本文件并注明生效时间。
## 一、限价与判定口径
1. **判定基准**:以「商家实收」为准;`商家实收 < 限价 × 数量` 即判定破价。
2. **实付款与平台承担**:实付款 + 平台承担(红包 / 消费券 / 平台券)= 商家实收;凭证必须同时包含「实付款」和「数量」。
3. **出资方字典**
- 商家承担:店铺优惠(官方立减、单品直降、店铺券)、政府补贴
- 平台承担:红包、消费券、平台券、平台优惠、新品礼金
- 买家承担:淘金币
- 待定:首单礼金(等与运营确认后补录)
4. **限价维度**:按**货号**限价——同一货号下所有规格/条码共用同一套限价。
5. **限价时间版本**:按订单付款时点生效的限价判定(限价调整前后分别取当时版本),不做"用最新限价回算历史单"。
6. **成本变动规则(限价维护侧)**:成本价上涨 5% 以内不动限价;成本价下调不变限价。
7. **非负责人限价**:2026-10-07 00:00 前的 ID 价格不判定破价(此后按规则判定)。
8. **老链接分界**:上架时间早于 2026-10-07 00:00 的链接(老链接)破价申诉通过;之后的按限价流程结束日期判定(上架时间需早于流程结束日期),并核对店铺款式编码 / 商品编码。
9. **负责人触发规则**:非运营发起的流程,前两个月全公司运营同价(不归属个人);两个月后按销量触发负责人(销量高者为负责人)。
10. **数据快照**:破价单在生成时保存金额快照;退款/售后状态单独记录,退款不等于免责(仍需判断是否已调价)。
11. **离职人员**:订单负责人已离职 → 自动标记「人员已离职」,不判定、不计乐捐。
12. **是否有检测报告**:取素材平台「检测报告」接口(`/api/external/certificate-reports`,按 **SPU** 精确匹配,只认有报告文件的记录),进程内缓存 10 分钟;
接口未配置或读取失败时该列显示为空、筛选候选值为空。配置项:`MATERIAL_LIBRARY_CERTIFICATE_REPORT_API_BASE_URL/API_KEY`,
不填则复用素材平台 `MATERIAL_LIBRARY_WHITE_BACKGROUND_API_*`。
13. **手工换货单**:聚水潭订单明细出现「被替换行(`item_status=Replaced`、数量归 0)」+「换入行(`item_ext_data` 含 `item_source=手工换货/手工添加`)」时按换货单处理:
- 判定按**换出行原编码**的限价执行(客户在平台上购买的那个编码);
- 成交数量与商家实收取**换入行**(换货不改变订单成交金额);换入行金额抓不到时回退订单主表商家实收 → 订单主表买家实付,都取不到才按 0(由运营申诉说明);
- 换货行配对成功后按原编码生成一条,不再按换入编码单独生成,避免同一订单重复生成两条破价单;
- 配对优先按 `sku_uuid` 精确匹配,剩余行仅在一对一时顺序配对;复杂结构(多对多)暂不强行配对,相关行按各自编码现状处理。
## 二、申诉与审核
1. **申诉类型**(4 组)
- 链接时间:老链接,上架时间早于限价流程(凭证须含链接 ID + 创建时间)/ 京喜换主体 · 老店转新店
- 优惠 / 活动:平台优惠 · 红包 · 券(非商家承担)/ 参与秒杀 · 百补 · 大促等平台活动(凭证须含活动开始与截止时间,且活动价未破价)/ 拼多多多件折扣(99折)
- 流程申请:已走新品限价及限价调整(必填流程号,系统到「新品限价及限价调整」流程校验,查不到不允许提交)
- 其他原因(须在备注中详细说明)
2. **申诉范围**:链接时间 / 流程申请类支持**链接级一次性申诉**(默认勾选,覆盖该链接下全部破价订单,后续新单自动继承);优惠 / 活动类按订单申诉。
3. **审核要点仅审核人可见**;申诉人只看到凭证要求与示例图。
4. **凭证校验**:必填校验 + 识别提示(识别结果仅提示,人工一键打回);后续增加图片查重(hash / 相似度)与关键字段(订单号、链接 ID)一致性校验。
5. **乐捐**:驳回计 ¥10/单(离职不计);配套审计链(数据快照 + 申诉材料 + 审核记录),扣款执行交财务确认,保留异议通道。
6. **举证期限**:待与运营确认后设置(避免事后无法追溯凭证)。
## 三、权限(由管理员统一管理)
路径:用户管理后台 → 权限配置 → 经营管理。限价管理相关的查看 / 导出 / 申诉都由管理员在这里开通。
| 能力 | 权限节点 | 动作 |
| --- | --- | --- |
| 页面与数据查看(限价明细、破价订单审核) | 限价管理 | 可见 |
| 导出(导出明细 / 导出明细(含图片)/ 破价订单导出 / 乐捐明细) | 限价管理 | 下载 |
| 申诉 · 审核 · 生成 · 导入 | 限价管理 | 操作 |
| 规则配置(页面可见 + 保存参数) | 限价管理 · 规则配置 | 可见 / 操作 |
- 没有「下载」权限时页面不展示导出入口,服务端导出接口同样鉴权(下载中心任务按「限价管理 · 下载」校验)。
- 没有「操作」权限时:不显示申诉/审核按钮,服务端也会拒绝申诉、审核、生成、导入。
- 申诉保留一条兜底:破价单的负责人本人,即使没有「操作」权限,也可以对本人负责的订单发起申诉(审核仍需「操作」)。
- 没有「规则配置」权限时,规则配置页签不显示、接口不可调用。
## 四、生成与运行
1. **按天增量**:限价逻辑上线后,破价订单**按天增量生成**(每天只生成最新一天的破价单并入库),**历史数据不再重复回算**;破价订单审核页每天展示最新破价单。
2. **数据来源**:聚水潭报表库(`jst_Orders` + `jst_OrderItem` / 天猫奇门 `jst_qmcust_orderlist` + `_item`),取 `seller_income_amount`、`buyer_paid_amount`、`qty`、`shop_i_id`、`shop_sku_id`、`pay_date`、退款状态,以及换货识别字段 `item_status`、`item_ext_data`(`item_source`、`sku_uuid`)。报表库查询较慢,逐日增量任务建议按天分片拉取。
3. **限价来源**:钉钉流程(仅取**已完结**),共 4 个模板,同步脚本 `scripts/sync-limit-workflows.mjs`:
- 「新品限价及限价调整」→ 货号取「货号(非商品编码)」,限价取「期望日常限价」;
其中的「调整限价明细表」子表(申请类型=调整原货号限价)只填「调整后限价」,该列同样作为限价取值
(2026-10-16 修复,此前这类调价会被漏掉);
- 「新品限价流程」→ 明细取系统产品限价页面(按流程链接 SPU 关联)。页面数据来自正式库
`external.product_catalog_wide_v1`(限价字段:`expected_daily_limit_price` / `non_spu_owner_limit_price`),
本地库该视图为空时退回本地 `IntakeNonIntegratedProduct(LimitRow)`;
- 「各中心特殊限价需求申请」→ 货号取「货号」,限价取「最低售价」;
- 「GB特殊限价需求申请」→ 货号取「货号」,限价取「期望日常限价」(2026-10-16 纳入)。
- 「产品限价页面」已确认(CONFIRMED)的限价全量纳入(2026-10-16):来源正式库
`external.product_catalog_wide_v1`,给没有限价流程的货号兜底(如 CY012801=268.6);
同货号与流程限价冲突时**以流程为准**,页面价只在流程没覆盖时生效。
- 「限价期初数据」(业务整理的存量限价 Excel,2026-10-16 纳入):放在期初数据目录
(`LIMIT_SYNC_INITIAL_DIR`,默认 `<app>/期初数据`;目录为空时用仓库自带
`scripts/data/limit-sync/期初数据/限价期初数据*.xlsx`),取目录里最新一份,
按「货号 + 负责人限价 + 更新日期」写入,作为**最低优先级**兜底(如 MKHDNZ0409260600468=279)。
优先级:**限价流程 > 产品限价页面(已确认)> 限价期初数据**;低优先级只在货号没有更高优先级限价时生效。
**例外(2026-10-16 确认)**:期初数据的「更新日期」**晚于**流程完成时间时,以期初数据为准(谁新用谁);
期初比流程旧则维持上面的优先级。
**非负责人限价一律按最新口径计算**(2026-10-16 确认):负责人限价 × 1.05,且不低于 +10 元;
期初数据表 / 产品限价页面里的历史「非负责人限价」(倍数 1%~15% 不统一)不采用,
限价明细页、导出文件、破价判定三处都用同一公式。
写入规则:同货号多流程取**审核通过时间最新**的一条;限价为 0 或空的明细剔除;「申请与SPU负责人同价 / 活动限价」不覆盖负责人限价,只作为价格授权;流程「货号」列若填的是商品编码,用商品编码匹配产品档案换算为货号;无货号时同样用商品编码匹配。页面限价的时间按**北京时间墙钟**存(与流程完成时间一致)。
4. **覆盖率**:暂不做未匹配清单与告警(后续优化)。
5. **正式站同步(服务器端)**:`scripts/sync-limit-workflows.mjs` 已改成服务器可运行,连接信息全部取
服务器 `shared/.env` 里的既有变量(`DATABASE_URL` / `JST_REPORT_DB_*` / `PROD_ARCHIVE_DB_*`),
不再依赖 Mac 本地路径:
```bash
# 手动跑一次(正式站目录下,root 或能读 shared/.env 的账号)
cd /opt/furniture-3d-system-prod/current/furniture-3d-system
sudo -E bash -lc 'set -a; . /opt/furniture-3d-system-prod/shared/.env; set +a; \
LIMIT_SYNC_OUT_DIR=/opt/furniture-3d-system-prod/shared/limit-workflow-sync/导出结果 \
LIMIT_SYNC_INITIAL_DIR=/opt/furniture-3d-system-prod/shared/limit-workflow-sync/期初数据 \
node scripts/run-limit-workflow-sync-production.cjs'
# 安装每日定时器(systemd,与营销日销同一套样板):每天北京时间 07:30
sudo bash scripts/install-limit-workflow-sync-timer.sh
systemctl list-timers --all 'furniture-limit-workflow-sync*' --no-pager
journalctl -u furniture-limit-workflow-sync.service -n 200 --no-pager
```
- 写库是**全量重建 + 单事务**:成功才提交;失败自动回滚,正式库保留上一版限价与授权。
- 输出目录 `LIMIT_SYNC_OUT_DIR`(默认 `<app>/导出结果`)里的 `限价流程_完整明细表.xlsx` 每日覆盖,
旧版本进 `历史版本/`,默认保留最近 20 份(`LIMIT_SYNC_HISTORY_KEEP`)。
- `--skip-excel` / `LIMIT_SYNC_SKIP_EXCEL=1` 可只写库不导 Excel。
- 期初数据更新:把新的 `限价期初数据*.xlsx` 放进 `shared/limit-workflow-sync/期初数据/` 即可,下次同步自动取最新。
- `SupplyChainBreachRuleConfig` 不需要灌数:接口读取时用代码内置默认值,首次在「规则配置」保存时落库。
### 每日聚水潭物流规则快照
- 正式站采用服务器端 systemd:`npm run install:jst-logistics-systemd`,默认每天 `09:30`(Asia/Shanghai)执行,避开 09:00 的数据库任务,并与 10:30 停产 RPA 任务错开。可通过 `JST_LOGISTICS_CALENDAR` 覆盖。
- 开发机示例使用 macOS launchd:`npm run install:jst-logistics-daily`,默认每天 `09:00`。
- 正式站快照目录固定为 `$BASE/shared/freight-config`,应用和定时任务共同读取 `FREIGHT_CONFIG_DATA_DIR`,发布切换 release 不会重置。
- 本地未配置 `FREIGHT_CONFIG_DATA_DIR` 时继续使用 `data/freight-config`。
- 每天最多成功更新一次;若快照的 `generatedAt` 已是北京时间当天,重跑会自动跳过。
- 当天确需人工覆盖时执行:在聚水潭 RPA 浏览器保持登录后运行 `npm run sync:jst-logistics-rules -- --force`。
- 同步失败时保留上一份可用快照,不写入半成品。
- 物流规则同步、停产下架同步和其他聚水潭 RPA 任务不得并发;正式站 unit 已与停产和条码同步任务建立冲突保护。
## 五、待办
- [ ] 限价流程数据接入 + 筛选逻辑(生效时间版本、货号维度)
- [ ] 非负责人限价 10-07 规则、负责人触发规则(前两个月同价 / 两个月后按销量)落到判定引擎
- [ ] 按天增量任务(cron / worker)与失败告警
- [ ] 凭证查重(hash / 相似度)、关键字段一致性校验
- [ ] 举证期限、乐捐财务对接流程
- [ ] 首单礼金 / 新品礼金出资方确认后补录出资方字典

View File

@ -74,6 +74,7 @@ const contentSecurityPolicy = [
"base-uri 'self'",
"object-src 'none'",
"frame-ancestors 'none'",
`frame-src 'self' ${lodopScriptSources.join(' ')}`,
"form-action 'self'",
"img-src 'self' data: blob: http: https:",
"font-src 'self' data:",
@ -85,6 +86,13 @@ const contentSecurityPolicy = [
...(isProduction ? ["upgrade-insecure-requests"] : []),
].join('; ')
// 市场选款看板静态页(public/modules/selection-market/)由宿主页 /market-research/selection-market
// 以同源 iframe 嵌入:在基础 CSP 上仅放开 frame-ancestors 为同源,其余指令不变。
const boardContentSecurityPolicy = contentSecurityPolicy.replace(
"frame-ancestors 'none'",
"frame-ancestors 'self'"
)
const baseSecurityHeaders = [
{
key: 'X-Frame-Options',
@ -229,6 +237,7 @@ function buildClientStorageEnv() {
const clientStorageEnv = buildClientStorageEnv()
const nextConfig = {
devIndicators: false,
distDir: process.env.NEXT_DIST_DIR || '.next',
// 启用standalone输出,用于Docker部署
output: 'standalone',
@ -239,7 +248,7 @@ const nextConfig = {
},
// 服务器外部包配置(Next.js 16新配置)
serverExternalPackages: ['sharp', 'three', 'gsap', '@huggingface/transformers', 'onnxruntime-node', 'mssql'],
serverExternalPackages: ['sharp', 'three', 'gsap', '@huggingface/transformers', 'onnxruntime-node', 'mssql', 'mysql2'],
// Turbopack配置(Next.js 16默认启用)
turbopack: {},
@ -382,6 +391,21 @@ const nextConfig = {
source: '/(.*)',
headers: baseSecurityHeaders,
},
{
// 市场选款看板静态页需要被本平台宿主页同源 iframe 嵌入:
// 覆盖基础安全头的 X-Frame-Options DENY / frame-ancestors 'none',仅此路径生效。
source: '/modules/selection-market/:path*',
headers: [
{
key: 'X-Frame-Options',
value: 'SAMEORIGIN'
},
{
key: 'Content-Security-Policy',
value: boardContentSecurityPolicy
}
],
},
{
// Next 静态资源在生产环境可长期缓存;开发环境必须禁用缓存,否则浏览器会继续复用旧 bundle。
source: '/_next/static/:path*',

View File

@ -3,6 +3,7 @@
"version": "0.1.0",
"private": true,
"scripts": {
"worker:ai-image": "node scripts/run-ai-image-worker.mjs",
"predev": "npm run prisma:check-client-sync",
"dev": "next dev --webpack",
"build": "prisma generate && next build",
@ -18,9 +19,15 @@
"prisma:studio": "prisma studio",
"sync:supply-chain-product-archive": "tsx scripts/sync-supply-chain-product-archive.ts",
"sync:supply-chain-product-archive-daily": "tsx scripts/sync-supply-chain-product-archive-sales-performance-daily.ts",
"generate:modular-processing-daily-snapshot": "tsx scripts/generate-modular-processing-daily-snapshot.ts",
"sync:supply-chain-product-archive-enrichment": "tsx scripts/sync-supply-chain-product-archive-enrichment.ts",
"sync:distribution-listings": "tsx scripts/sync-distribution-listings.ts",
"backfill:product-archive-designer": "tsx scripts/backfill-product-archive-designer-from-jst-sku-info-manage.ts",
"backfill:product-archive-designer:apply": "tsx scripts/backfill-product-archive-designer-from-jst-sku-info-manage.ts --apply",
"backfill:product-archive-opshead": "tsx scripts/backfill-product-archive-opshead-from-jst-sku-info-manage.ts --mode=regular",
"backfill:product-archive-opshead:apply": "tsx scripts/backfill-product-archive-opshead-from-jst-sku-info-manage.ts --mode=regular --apply",
"backfill:product-archive-opshead:regular": "tsx scripts/backfill-product-archive-opshead-from-jst-sku-info-manage.ts --mode=regular",
"backfill:product-archive-opshead:regular:apply": "tsx scripts/backfill-product-archive-opshead-from-jst-sku-info-manage.ts --mode=regular --apply",
"backfill:product-archive-production-type": "tsx scripts/backfill-product-archive-production-type.ts",
"backfill:product-archive-production-type:apply": "tsx scripts/backfill-product-archive-production-type.ts --apply",
"sync:supply-chain-product-archive-sales-performance": "tsx scripts/sync-supply-chain-product-archive-sales-performance.ts",
@ -28,6 +35,14 @@
"compare:product-sabc-xlsx": "tsx scripts/compare-product-sabc-xlsx.ts",
"seed:local-product-sabc-xlsx": "tsx scripts/seed-local-product-sabc-xlsx.ts",
"sync:jst-sku-product-archive": "tsx scripts/sync-jst-sku-product-archive.ts",
"snapshot:limit-price": "tsx scripts/generate-limit-price-snapshot.ts",
"snapshot:limit-price:daily": "node scripts/run-limit-price-daily-snapshot.mjs",
"install:limit-price-snapshot-systemd": "bash scripts/install-limit-price-snapshot-systemd-timer.sh",
"import:province-shipment-share": "node scripts/import-province-shipment-share.mjs",
"sync:jst-logistics-rules": "tsx scripts/sync-jst-logistics-rules.ts",
"snapshot:jst-logistics:daily": "tsx scripts/sync-jst-logistics-rules.ts",
"install:jst-logistics-daily": "bash scripts/install-jst-logistics-daily-sync.sh",
"install:jst-logistics-systemd": "bash scripts/install-jst-logistics-systemd-timer.sh",
"sync:bom-cost-reference": "tsx scripts/sync-bom-cost-reference.ts",
"backtest:bom-cost-reference": "tsx scripts/backtest-bom-cost-reference.ts",
"report:bom-cost-shadow-divergence": "tsx scripts/report-bom-cost-shadow-divergence.ts",
@ -70,6 +85,7 @@
"materials:sync-standard": "tsx scripts/sync-standard-finishes.ts",
"materials:sync-standard:apply": "tsx scripts/sync-standard-finishes.ts --apply",
"sync:jst-sku-stock": "tsx scripts/sync-jst-sku-stock-products.ts",
"sync:product-inventory-snapshot": "tsx scripts/sync-product-inventory-snapshot.ts",
"image-search:model:prepare": "tsx scripts/prepare-product-archive-image-search-model.ts",
"image-search:index": "tsx scripts/index-product-archive-images.ts",
"repair:barcode-print:historical": "tsx scripts/repair-barcode-print-source-rows.ts",
@ -81,6 +97,13 @@
"sync:oa-video-flow:apply": "tsx scripts/sync-oa-video-flow-board.ts --apply",
"sync:oa-art-flow-raw": "tsx scripts/sync-oa-art-flow-raw.ts",
"sync:oa-art-flow-raw:apply": "tsx scripts/sync-oa-art-flow-raw.ts --apply",
"import:clearance-pool": "tsx scripts/import-clearance-pool-snapshot.ts",
"import:clearance-pool-archive": "tsx scripts/import-clearance-pool-archive.ts",
"reset:clearance-pool:archive": "tsx scripts/reset-clearance-pool-to-archive.ts",
"sync:clearance-pool:discontinued": "tsx scripts/sync-clearance-pool-snapshot.ts",
"sync:clearance-stock:stock-monitor": "tsx scripts/sync-clearance-stock-from-stock-monitor.ts",
"sync:clearance-progress": "tsx scripts/sync-clearance-progress.ts",
"import:clearance-review": "tsx scripts/import-clearance-review-snapshots.ts",
"cache:oa-board:local": "tsx scripts/cache-oa-board-local-preview.ts",
"import:oa-board-activity-mappings": "tsx scripts/import-oa-board-activity-mappings.ts",
"import:oa-board-node-rules": "tsx scripts/import-oa-board-node-rules.ts",

View File

@ -0,0 +1,32 @@
CREATE TABLE "ModularProductProcessingRuleSet" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL DEFAULT '模块化商品备货规则',
"columns" JSONB NOT NULL DEFAULT '[]',
"updatedById" TEXT,
"updatedByName" TEXT NOT NULL DEFAULT '',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "ModularProductProcessingRuleSet_pkey" PRIMARY KEY ("id")
);
CREATE TABLE "ModularProductProcessingRuleRow" (
"id" TEXT NOT NULL,
"ruleSetId" TEXT NOT NULL,
"values" JSONB NOT NULL DEFAULT '{}',
"sortOrder" INTEGER NOT NULL DEFAULT 0,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "ModularProductProcessingRuleRow_pkey" PRIMARY KEY ("id")
);
CREATE INDEX "ModularProductProcessingRuleSet_updatedAt_idx"
ON "ModularProductProcessingRuleSet"("updatedAt");
CREATE INDEX "ModularProductProcessingRuleRow_ruleSetId_sortOrder_idx"
ON "ModularProductProcessingRuleRow"("ruleSetId", "sortOrder");
ALTER TABLE "ModularProductProcessingRuleRow"
ADD CONSTRAINT "ModularProductProcessingRuleRow_ruleSetId_fkey"
FOREIGN KEY ("ruleSetId") REFERENCES "ModularProductProcessingRuleSet"("id")
ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -0,0 +1,18 @@
ALTER TABLE "DiyCabinetStyle"
ADD COLUMN "modelFileId" TEXT,
ADD COLUMN "mainNodeNames" TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[],
ADD COLUMN "shelfNodeNames" TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[],
ADD COLUMN "modelRotationY" DOUBLE PRECISION NOT NULL DEFAULT 0;
ALTER TABLE "DiyCabinetStyle" ADD CONSTRAINT "DiyCabinetStyle_modelFileId_fkey"
FOREIGN KEY ("modelFileId") REFERENCES "DiyAssetFile"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
ALTER TABLE "DiyCabinetVariant"
ADD COLUMN "mainMaterialCode" TEXT,
ADD COLUMN "shelfMaterialCode" TEXT;
ALTER TABLE "DiyCabinetVariant" ADD CONSTRAINT "DiyCabinetVariant_mainMaterialCode_fkey"
FOREIGN KEY ("mainMaterialCode") REFERENCES "DiyMaterial"("itemCode") ON DELETE RESTRICT ON UPDATE CASCADE;
ALTER TABLE "DiyCabinetVariant" ADD CONSTRAINT "DiyCabinetVariant_shelfMaterialCode_fkey"
FOREIGN KEY ("shelfMaterialCode") REFERENCES "DiyMaterial"("itemCode") ON DELETE RESTRICT ON UPDATE CASCADE;
ALTER TABLE "DiyDoorPattern"
ADD COLUMN "width" DOUBLE PRECISION,
ADD COLUMN "height" DOUBLE PRECISION,
ADD COLUMN "thickness" DOUBLE PRECISION;

View File

@ -0,0 +1,85 @@
-- 限价管理 · 破价订单审核
CREATE TABLE "SupplyChainBreachOrder" (
"id" TEXT NOT NULL,
"platform" TEXT NOT NULL DEFAULT '',
"shopName" TEXT NOT NULL DEFAULT '',
"soId" TEXT NOT NULL DEFAULT '',
"oId" TEXT NOT NULL DEFAULT '',
"payDate" TIMESTAMP(3),
"itemCode" TEXT NOT NULL DEFAULT '',
"shopItemCode" TEXT NOT NULL DEFAULT '',
"shopStyleCode" TEXT NOT NULL DEFAULT '',
"linkGroupKey" TEXT NOT NULL DEFAULT '',
"qty" INTEGER NOT NULL DEFAULT 1,
"buyerPaidAmount" DECIMAL(18,4) NOT NULL DEFAULT 0,
"platformBearAmount" DECIMAL(18,4) NOT NULL DEFAULT 0,
"sellerIncomeAmount" DECIMAL(18,4) NOT NULL DEFAULT 0,
"limitUnitPrice" DECIMAL(18,4),
"limitTotal" DECIMAL(18,4),
"gapAmount" DECIMAL(18,4),
"ownerName" TEXT NOT NULL DEFAULT '',
"profitCenter" TEXT NOT NULL DEFAULT '',
"orderStatus" TEXT NOT NULL DEFAULT '',
"refundStatus" TEXT NOT NULL DEFAULT '',
"listedAt" TIMESTAMP(3),
"flowEndDate" TIMESTAMP(3),
"status" TEXT NOT NULL DEFAULT 'PENDING_APPEAL',
"appealType" TEXT NOT NULL DEFAULT '',
"appealScope" TEXT NOT NULL DEFAULT 'ORDER',
"processNo" TEXT NOT NULL DEFAULT '',
"appealRound" INTEGER NOT NULL DEFAULT 1,
"previousAppeal" JSON,
"appealDesc" TEXT NOT NULL DEFAULT '',
"appealImages" JSON NOT NULL DEFAULT '[]',
"appliedFromLink" BOOLEAN NOT NULL DEFAULT false,
"autoSuggested" TEXT NOT NULL DEFAULT '',
"autoSuggestedNote" TEXT NOT NULL DEFAULT '',
"reviewedBy" TEXT NOT NULL DEFAULT '',
"reviewedAt" TIMESTAMP(3),
"reviewDecision" TEXT NOT NULL DEFAULT '',
"reviewReason" TEXT NOT NULL DEFAULT '',
"reviewNote" TEXT NOT NULL DEFAULT '',
"fineAmount" DECIMAL(10,2) NOT NULL DEFAULT 0,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "SupplyChainBreachOrder_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "SupplyChainBreachOrder_oId_itemCode_key" ON "SupplyChainBreachOrder"("oId", "itemCode");
CREATE INDEX "SupplyChainBreachOrder_status_payDate_idx" ON "SupplyChainBreachOrder"("status", "payDate" DESC);
CREATE INDEX "SupplyChainBreachOrder_linkGroupKey_idx" ON "SupplyChainBreachOrder"("linkGroupKey");
CREATE INDEX "SupplyChainBreachOrder_ownerName_status_idx" ON "SupplyChainBreachOrder"("ownerName", "status");
CREATE INDEX "SupplyChainBreachOrder_shopName_itemCode_idx" ON "SupplyChainBreachOrder"("shopName", "itemCode");
CREATE TABLE "SupplyChainBreachOrderLog" (
"id" TEXT NOT NULL,
"orderId" TEXT NOT NULL,
"action" TEXT NOT NULL,
"operatorId" TEXT NOT NULL DEFAULT '',
"operatorName" TEXT NOT NULL DEFAULT '',
"detail" JSON NOT NULL DEFAULT '{}',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "SupplyChainBreachOrderLog_pkey" PRIMARY KEY ("id")
);
CREATE INDEX "SupplyChainBreachOrderLog_orderId_createdAt_idx" ON "SupplyChainBreachOrderLog"("orderId", "createdAt");
CREATE TABLE "SupplyChainLinkLimitPrice" (
"id" TEXT NOT NULL,
"shopName" TEXT NOT NULL,
"itemCode" TEXT NOT NULL,
"spu" TEXT NOT NULL DEFAULT '',
"limitUnitPrice" DECIMAL(18,4) NOT NULL,
"listedAt" TIMESTAMP(3),
"flowEndDate" TIMESTAMP(3),
"source" TEXT NOT NULL DEFAULT 'MANUAL',
"ownerName" TEXT NOT NULL DEFAULT '',
"updatedById" TEXT NOT NULL DEFAULT '',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "SupplyChainLinkLimitPrice_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "SupplyChainLinkLimitPrice_shopName_itemCode_key" ON "SupplyChainLinkLimitPrice"("shopName", "itemCode");
CREATE INDEX "SupplyChainLinkLimitPrice_itemCode_idx" ON "SupplyChainLinkLimitPrice"("itemCode");

View File

@ -0,0 +1,10 @@
-- Uploaded models have a file reference rather than generated panel geometry.
-- A cabinet without door openings is a valid model-only cabinet.
ALTER TABLE "DiyCabinetStyle"
DROP CONSTRAINT "DiyCabinetStyle_cellCount_check",
ADD CONSTRAINT "DiyCabinetStyle_cellCount_check"
CHECK ("cellCount" >= 0 AND jsonb_typeof(cells) = 'array' AND jsonb_array_length(cells) = "cellCount"),
DROP CONSTRAINT "DiyCabinetStyle_panelCount_check",
ADD CONSTRAINT "DiyCabinetStyle_panelCount_check"
CHECK ("panelCount" >= 0 AND jsonb_typeof(panels) = 'array' AND jsonb_array_length(panels) = "panelCount"
AND ("panelCount" > 0 OR "modelFileId" IS NOT NULL));

View File

@ -0,0 +1,9 @@
CREATE TABLE "SupplyChainBreachRuleConfig" (
"id" TEXT NOT NULL DEFAULT 'default',
"data" JSON NOT NULL DEFAULT '{}',
"updatedById" TEXT NOT NULL DEFAULT '',
"updatedByName" TEXT NOT NULL DEFAULT '',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "SupplyChainBreachRuleConfig_pkey" PRIMARY KEY ("id")
);

View File

@ -0,0 +1,17 @@
-- 加工单人工维护值:条码 → 最终加工量 / 加工单备注,支持单元格编辑与批量修改。
CREATE TABLE "ModularProcessingManualValue" (
"id" TEXT NOT NULL,
"barcode" TEXT NOT NULL,
"finalProcessingQuantity" INTEGER,
"processingNote" TEXT NOT NULL DEFAULT '',
"updatedById" TEXT,
"updatedByName" TEXT NOT NULL DEFAULT '',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "ModularProcessingManualValue_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "ModularProcessingManualValue_barcode_key" ON "ModularProcessingManualValue"("barcode");
CREATE INDEX "ModularProcessingManualValue_updatedAt_idx" ON "ModularProcessingManualValue"("updatedAt");

View File

@ -0,0 +1,2 @@
-- 人工维护值增加「叫货仓」:导入模板与单元格编辑都要能覆盖产品档案里的仓库。
ALTER TABLE "ModularProcessingManualValue" ADD COLUMN "orderWarehouse" TEXT;

View File

@ -0,0 +1,2 @@
-- 「图片」列:提单时按条码从产品全量档案库快照图片地址,列表在条码前展示缩略图。
ALTER TABLE "ProductionStopDemandItem" ADD COLUMN "image" TEXT NOT NULL DEFAULT '';

View File

@ -0,0 +1,10 @@
ALTER TABLE "DiyMaterial"
ADD COLUMN "deletedAt" TIMESTAMP(3),
ADD COLUMN "deletedBy" TEXT;
ALTER TABLE "DiyDoorPattern"
ADD COLUMN "deletedAt" TIMESTAMP(3),
ADD COLUMN "deletedBy" TEXT;
CREATE INDEX "DiyMaterial_deletedAt_idx" ON "DiyMaterial"("deletedAt");
CREATE INDEX "DiyDoorPattern_deletedAt_idx" ON "DiyDoorPattern"("deletedAt");

View File

@ -0,0 +1,25 @@
CREATE TABLE "NativeImageGenerationJob" (
"id" TEXT NOT NULL,
"createdBy" TEXT NOT NULL,
"providerConfigId" TEXT NOT NULL,
"providerFingerprint" TEXT NOT NULL,
"requestHash" TEXT NOT NULL,
"request" JSONB NOT NULL,
"completion" JSONB NOT NULL,
"progress" JSONB NOT NULL DEFAULT '[]',
"status" TEXT NOT NULL DEFAULT 'QUEUED',
"retryCount" INTEGER NOT NULL DEFAULT 0,
"nextRunAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"leaseOwner" TEXT,
"leaseUntil" TIMESTAMP(3),
"errorMessage" TEXT,
"finishedAt" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "NativeImageGenerationJob_pkey" PRIMARY KEY ("id"),
CONSTRAINT "NativeImageGenerationJob_status_valid" CHECK ("status" IN ('QUEUED', 'RUNNING', 'FINALIZING', 'SUCCEEDED', 'FAILED', 'BLOCKED')),
CONSTRAINT "NativeImageGenerationJob_retry_valid" CHECK ("retryCount" >= 0),
CONSTRAINT "NativeImageGenerationJob_identity_valid" CHECK (length(btrim("id")) > 0 AND length(btrim("createdBy")) > 0)
);
CREATE INDEX "NativeImageGenerationJob_status_nextRunAt_leaseUntil_idx" ON "NativeImageGenerationJob"("status", "nextRunAt", "leaseUntil");
CREATE INDEX "NativeImageGenerationJob_createdBy_createdAt_idx" ON "NativeImageGenerationJob"("createdBy", "createdAt");

View File

@ -0,0 +1,37 @@
-- 限价流程同步:货号负责人限价(来源:限价流程的期望日常限价,按货号维度)
CREATE TABLE "SupplyChainStyleLimitPrice" (
"id" TEXT NOT NULL,
"styleCode" TEXT NOT NULL,
"ownerLimitPrice" DECIMAL(18,4),
"processInstanceId" TEXT NOT NULL DEFAULT '',
"processNo" TEXT NOT NULL DEFAULT '',
"applyType" TEXT NOT NULL DEFAULT '',
"initiator" TEXT NOT NULL DEFAULT '',
"approvedAt" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "SupplyChainStyleLimitPrice_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "SupplyChainStyleLimitPrice_styleCode_key" ON "SupplyChainStyleLimitPrice"("styleCode");
-- 限价流程同步:价格授权(同价申请 / 活动限价;供破价判定豁免与超期判定)
CREATE TABLE "SupplyChainLimitFlowAuth" (
"id" TEXT NOT NULL,
"processInstanceId" TEXT NOT NULL,
"processNo" TEXT NOT NULL DEFAULT '',
"applyType" TEXT NOT NULL DEFAULT '',
"styleCode" TEXT NOT NULL DEFAULT '',
"spu" TEXT NOT NULL DEFAULT '',
"approvedAt" TIMESTAMP(3),
"startDate" TIMESTAMP(3),
"endDate" TIMESTAMP(3),
"adjustedLimitPrice" DECIMAL(18,4),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "SupplyChainLimitFlowAuth_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "SupplyChainLimitFlowAuth_processInstanceId_styleCode_applyType_key" ON "SupplyChainLimitFlowAuth"("processInstanceId", "styleCode", "applyType");
CREATE INDEX "SupplyChainLimitFlowAuth_styleCode_idx" ON "SupplyChainLimitFlowAuth"("styleCode");
CREATE INDEX "SupplyChainLimitFlowAuth_applyType_idx" ON "SupplyChainLimitFlowAuth"("applyType");

View File

@ -0,0 +1,19 @@
-- 产品全量档案字段权限(黑名单):主体对字段的屏蔽条目,只存被屏蔽的字段。
-- CreateTable
CREATE TABLE "ProductArchiveFieldPermissionBlock" (
"id" TEXT NOT NULL,
"subjectKey" TEXT NOT NULL,
"fieldKey" TEXT NOT NULL,
"createdBy" TEXT,
"updatedBy" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "ProductArchiveFieldPermissionBlock_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "ProductArchiveFieldPermissionBlock_subjectKey_fieldKey_key" ON "ProductArchiveFieldPermissionBlock"("subjectKey", "fieldKey");
-- CreateIndex
CREATE INDEX "ProductArchiveFieldPermissionBlock_subjectKey_idx" ON "ProductArchiveFieldPermissionBlock"("subjectKey");

View File

@ -0,0 +1,20 @@
-- Preserve explicit empty personal overrides separately from following a position.
CREATE TABLE "ProductArchiveFieldPermissionSubject" (
"subjectKey" TEXT NOT NULL,
"createdBy" TEXT,
"updatedBy" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "ProductArchiveFieldPermissionSubject_pkey" PRIMARY KEY ("subjectKey")
);
-- Keep every existing blocked field and its subject.
INSERT INTO "ProductArchiveFieldPermissionSubject" ("subjectKey", "createdAt", "updatedAt")
SELECT "subjectKey", MIN("createdAt"), MAX("updatedAt")
FROM "ProductArchiveFieldPermissionBlock"
GROUP BY "subjectKey";
ALTER TABLE "ProductArchiveFieldPermissionBlock"
ADD CONSTRAINT "ProductArchiveFieldPermissionBlock_subjectKey_fkey"
FOREIGN KEY ("subjectKey") REFERENCES "ProductArchiveFieldPermissionSubject"("subjectKey")
ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -0,0 +1,13 @@
-- 仓库属性人工覆盖:数据源 remark2 异常时在本系统修正展示值,不影响聚水潭原始数据。
CREATE TABLE "SupplyChainProductArchiveWarehouseAttributeOverride" (
"warehouseName" TEXT NOT NULL,
"attribute" TEXT NOT NULL DEFAULT '',
"updatedById" TEXT,
"updatedByName" TEXT NOT NULL DEFAULT '',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "SupplyChainProductArchiveWarehouseAttributeOverride_pkey" PRIMARY KEY ("warehouseName")
);
CREATE INDEX "SupplyChainProductArchiveWarehouseAttributeO_updatedAt_idx" ON "SupplyChainProductArchiveWarehouseAttributeOverride"("updatedAt");

View File

@ -0,0 +1,13 @@
CREATE TABLE "IntakeProductPhotoUploadGrant" (
"id" TEXT NOT NULL,
"productId" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"grantedBy" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "IntakeProductPhotoUploadGrant_pkey" PRIMARY KEY ("id"),
CONSTRAINT "IntakeProductPhotoUploadGrant_productId_fkey" FOREIGN KEY ("productId") REFERENCES "IntakeNonIntegratedProduct"("id") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "IntakeProductPhotoUploadGrant_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "IntakeProductPhotoUploadGrant_grantedBy_fkey" FOREIGN KEY ("grantedBy") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
CREATE UNIQUE INDEX "IntakeProductPhotoUploadGrant_productId_userId_key" ON "IntakeProductPhotoUploadGrant"("productId", "userId");
CREATE INDEX "IntakeProductPhotoUploadGrant_userId_idx" ON "IntakeProductPhotoUploadGrant"("userId");

View File

@ -0,0 +1,107 @@
-- 停产下架:每日通知快照(编码/链接)与乐捐明细
CREATE TABLE "SupplyChainDiscontinuationNotice" (
"id" TEXT NOT NULL,
"barcode" TEXT NOT NULL,
"statDate" DATE NOT NULL,
"statTime" TIMESTAMP(3) NOT NULL,
"productStatus" TEXT NOT NULL DEFAULT '',
"stockJianshang" INTEGER NOT NULL DEFAULT 0,
"stockFactory" INTEGER NOT NULL DEFAULT 0,
"handlingNote" TEXT NOT NULL DEFAULT '',
"replacementBarcode" TEXT NOT NULL DEFAULT '',
"changeImage" BOOLEAN NOT NULL DEFAULT false,
"difference" TEXT NOT NULL DEFAULT '',
"priceDiff" DECIMAL(18,2),
"channelStock" INTEGER NOT NULL DEFAULT 0,
"specialWarehouseNote" TEXT NOT NULL DEFAULT '',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "SupplyChainDiscontinuationNotice_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "SupplyChainDiscontinuationNotice_barcode_statDate_key"
ON "SupplyChainDiscontinuationNotice"("barcode", "statDate");
CREATE INDEX "SupplyChainDiscontinuationNotice_statDate_idx"
ON "SupplyChainDiscontinuationNotice"("statDate");
CREATE INDEX "SupplyChainDiscontinuationNotice_barcode_idx"
ON "SupplyChainDiscontinuationNotice"("barcode");
CREATE TABLE "SupplyChainDiscontinuationNoticeLink" (
"id" TEXT NOT NULL,
"noticeId" TEXT NOT NULL,
"itemId" TEXT NOT NULL DEFAULT '',
"shopName" TEXT NOT NULL DEFAULT '',
"platform" TEXT NOT NULL DEFAULT '',
"shopSkuCode" TEXT NOT NULL DEFAULT '',
"onlineItemCode" TEXT NOT NULL DEFAULT '',
"onlineStock" INTEGER NOT NULL DEFAULT 0,
"onlineStatus" TEXT NOT NULL DEFAULT '',
"ownerName" TEXT NOT NULL DEFAULT '',
"deptHead" TEXT NOT NULL DEFAULT '',
"profitCenter" TEXT NOT NULL DEFAULT '',
"statTime" TIMESTAMP(3) NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "SupplyChainDiscontinuationNoticeLink_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "SupplyChainDiscontinuationNoticeLink_noticeId_itemId_key"
ON "SupplyChainDiscontinuationNoticeLink"("noticeId", "itemId");
CREATE INDEX "SupplyChainDiscontinuationNoticeLink_itemId_idx"
ON "SupplyChainDiscontinuationNoticeLink"("itemId");
CREATE INDEX "SupplyChainDiscontinuationNoticeLink_ownerName_idx"
ON "SupplyChainDiscontinuationNoticeLink"("ownerName");
ALTER TABLE "SupplyChainDiscontinuationNoticeLink"
ADD CONSTRAINT "SupplyChainDiscontinuationNoticeLink_noticeId_fkey"
FOREIGN KEY ("noticeId") REFERENCES "SupplyChainDiscontinuationNotice"("id")
ON DELETE CASCADE ON UPDATE CASCADE;
CREATE TABLE "SupplyChainDiscontinuationPenalty" (
"id" TEXT NOT NULL,
"monthKey" TEXT NOT NULL,
"oId" TEXT NOT NULL,
"soId" TEXT NOT NULL DEFAULT '',
"barcode" TEXT NOT NULL DEFAULT '',
"itemId" TEXT NOT NULL DEFAULT '',
"shopName" TEXT NOT NULL DEFAULT '',
"platform" TEXT NOT NULL DEFAULT '',
"ownerName" TEXT NOT NULL DEFAULT '',
"payAt" TIMESTAMP(3) NOT NULL,
"noticeAt" TIMESTAMP(3) NOT NULL,
"statTime" TIMESTAMP(3) NOT NULL,
"orderStatus" TEXT NOT NULL DEFAULT '',
"penaltyAmount" DECIMAL(10,2) NOT NULL DEFAULT 10,
"status" TEXT NOT NULL DEFAULT 'PENDING_APPEAL',
"appealType" TEXT NOT NULL DEFAULT '',
"appealDesc" TEXT NOT NULL DEFAULT '',
"appealImages" JSONB NOT NULL DEFAULT '[]',
"reviewedBy" TEXT NOT NULL DEFAULT '',
"reviewedAt" TIMESTAMP(3),
"reviewNote" TEXT NOT NULL DEFAULT '',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "SupplyChainDiscontinuationPenalty_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "SupplyChainDiscontinuationPenalty_oId_itemId_key"
ON "SupplyChainDiscontinuationPenalty"("oId", "itemId");
CREATE INDEX "SupplyChainDiscontinuationPenalty_monthKey_ownerName_idx"
ON "SupplyChainDiscontinuationPenalty"("monthKey", "ownerName");
CREATE INDEX "SupplyChainDiscontinuationPenalty_status_idx"
ON "SupplyChainDiscontinuationPenalty"("status");
-- 停产下架 · 每日库存预警快照(页面只读取本地落库结果)
CREATE TABLE "SupplyChainDiscontinuationInventoryAlertSnapshot" (
"id" TEXT NOT NULL,
"statDate" DATE NOT NULL,
"statTime" TIMESTAMP(3) NOT NULL,
"barcode" TEXT NOT NULL,
"spu" TEXT NOT NULL DEFAULT '',
"sku" TEXT NOT NULL DEFAULT '',
"productName" TEXT NOT NULL DEFAULT '',
"productStatus" TEXT NOT NULL DEFAULT '',
"stockJianshang" INTEGER NOT NULL DEFAULT 0,
"stockFactory" INTEGER NOT NULL DEFAULT 0,
"warehouseName" TEXT NOT NULL DEFAULT '',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "SupplyChainDiscontinuationInventoryAlertSnapshot_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "SupplyChainDiscontinuationInventoryAlertSnapshot_barcode_statDate_key"
ON "SupplyChainDiscontinuationInventoryAlertSnapshot"("barcode", "statDate");
CREATE INDEX "SupplyChainDiscontinuationInventoryAlertSnapshot_statDate_idx"
ON "SupplyChainDiscontinuationInventoryAlertSnapshot"("statDate");

View File

@ -0,0 +1,2 @@
-- 产品全量档案新增「安装方式」列:取值来源产品开发(产品定厂 SKU installMethod,整装/拆装)。
ALTER TABLE "SupplyChainProductArchive" ADD COLUMN "installMethod" TEXT NOT NULL DEFAULT '';

View File

@ -0,0 +1,48 @@
-- 商品库存快照:条码 × 实际仓库粒度,由 15 分钟定时任务全量替换写入;失败保留上一份快照。
CREATE TABLE "ProductInventorySnapshot" (
"id" TEXT NOT NULL,
"barcode" TEXT NOT NULL,
"wmsCoId" INTEGER NOT NULL,
"productName" TEXT NOT NULL DEFAULT '',
"category" TEXT NOT NULL DEFAULT '',
"sku" TEXT NOT NULL DEFAULT '',
"productionStatus" TEXT NOT NULL DEFAULT '',
"supplier" TEXT NOT NULL DEFAULT '',
"purchaser" TEXT NOT NULL DEFAULT '',
"stockQty" INTEGER NOT NULL DEFAULT 0,
"orderLockQty" INTEGER NOT NULL DEFAULT 0,
"availableQty" INTEGER NOT NULL DEFAULT 0,
"cloudAvailableQty" INTEGER NOT NULL DEFAULT 0,
"warehouseName" TEXT NOT NULL DEFAULT '',
"warehouseAttribute" TEXT NOT NULL DEFAULT '',
"warehouseOwnership" TEXT NOT NULL DEFAULT '',
"warehouseHead" TEXT NOT NULL DEFAULT '',
"syncedAt" TIMESTAMP(3) NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "ProductInventorySnapshot_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "ProductInventorySnapshot_barcode_wmsCoId_key" ON "ProductInventorySnapshot"("barcode", "wmsCoId");
CREATE INDEX "ProductInventorySnapshot_warehouseName_idx" ON "ProductInventorySnapshot"("warehouseName");
CREATE INDEX "ProductInventorySnapshot_syncedAt_idx" ON "ProductInventorySnapshot"("syncedAt");
-- 同步运行状态:每次定时任务一行,记录 RUNNING / COMPLETED / FAILED。
CREATE TABLE "ProductInventorySnapshotRun" (
"id" TEXT NOT NULL,
"startedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"completedAt" TIMESTAMP(3),
"status" VARCHAR(24) NOT NULL,
"rowsRead" INTEGER NOT NULL DEFAULT 0,
"rowsWritten" INTEGER NOT NULL DEFAULT 0,
"syncedAt" TIMESTAMP(3),
"errorMessage" TEXT,
CONSTRAINT "ProductInventorySnapshotRun_pkey" PRIMARY KEY ("id")
);
CREATE INDEX "ProductInventorySnapshotRun_startedAt_idx" ON "ProductInventorySnapshotRun"("startedAt");
CREATE INDEX "ProductInventorySnapshotRun_status_startedAt_idx" ON "ProductInventorySnapshotRun"("status", "startedAt");

View File

@ -0,0 +1,15 @@
-- 虚拟云仓库存日快照:停产下架计算公有可用数时读取该表,避免实时跨链接服务器查询。
CREATE TABLE "SupplyChainVirtualInventorySnapshot" (
"id" TEXT NOT NULL,
"statDate" DATE NOT NULL,
"statTime" TIMESTAMP(3) NOT NULL,
"barcode" TEXT NOT NULL,
"cloudStock" INTEGER NOT NULL DEFAULT 0,
"sourceFileName" TEXT NOT NULL DEFAULT '',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "SupplyChainVirtualInventorySnapshot_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "SupplyChainVirtualInventorySnapshot_barcode_statDate_key"
ON "SupplyChainVirtualInventorySnapshot"("barcode", "statDate");
CREATE INDEX "SupplyChainVirtualInventorySnapshot_statDate_idx"
ON "SupplyChainVirtualInventorySnapshot"("statDate");

View File

@ -0,0 +1,17 @@
-- 通知中心待办同步状态(停产确认 / 待走流程两类)。
CREATE TABLE "ProductionStopNotificationSync" (
"id" TEXT NOT NULL,
"kind" TEXT NOT NULL,
"recipientKey" TEXT NOT NULL,
"recipientName" TEXT NOT NULL DEFAULT '',
"pendingCount" INTEGER NOT NULL DEFAULT 0,
"snapshot" TEXT NOT NULL DEFAULT '',
"lastSentAt" TIMESTAMP(3),
"completedAt" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "ProductionStopNotificationSync_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "ProductionStopNotificationSync_kind_recipientKey_key"
ON "ProductionStopNotificationSync"("kind", "recipientKey");

View File

@ -0,0 +1,18 @@
-- 模块化商品加工单每日生成快照:按日期保存当天最后一次生成的主表结果,供历史日期查询。
CREATE TABLE "ModularProcessingOrderDailySnapshot" (
"id" TEXT NOT NULL,
"snapshotDate" TEXT NOT NULL,
"recordKey" TEXT NOT NULL,
"barcode" TEXT NOT NULL DEFAULT '',
"rowJson" JSONB NOT NULL,
"generatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "ModularProcessingOrderDailySnapshot_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "ModularProcessingOrderDailySnapshot_snapshotDate_recordKey_key"
ON "ModularProcessingOrderDailySnapshot"("snapshotDate", "recordKey");
CREATE INDEX "ModularProcessingOrderDailySnapshot_snapshotDate_generatedAt_idx"
ON "ModularProcessingOrderDailySnapshot"("snapshotDate", "generatedAt");

View File

@ -0,0 +1,11 @@
-- 含大单近30日日销的数值快照列:用于 >、>=、<、<= 数值筛选与数值排序。
ALTER TABLE "ProductionStopDemandItem" ADD COLUMN "sales30dWithBigValue" DECIMAL(18,4);
ALTER TABLE "ProductionStopDemandItem" ADD COLUMN "sales30dWithBigSkuValue" DECIMAL(18,4);
UPDATE "ProductionStopDemandItem"
SET "sales30dWithBigValue" = "sales30dWithBig"::numeric
WHERE "sales30dWithBig" ~ '^-?[0-9]+(\.[0-9]+)?$';
UPDATE "ProductionStopDemandItem"
SET "sales30dWithBigSkuValue" = "sales30dWithBigSku"::numeric
WHERE "sales30dWithBigSku" ~ '^-?[0-9]+(\.[0-9]+)?$';

View File

@ -0,0 +1,38 @@
-- 铺货ID明细数据:一行 = 条码 × 铺货ID × 平台规格ID
CREATE TABLE "SupplyChainDistributionListing" (
"id" TEXT NOT NULL,
"barcode" TEXT NOT NULL,
"listingId" TEXT NOT NULL,
"platformSkuId" TEXT NOT NULL,
"source" TEXT NOT NULL,
"channel" TEXT NOT NULL DEFAULT '',
"platform" TEXT NOT NULL DEFAULT '',
"shopId" TEXT NOT NULL DEFAULT '',
"shopName" TEXT NOT NULL DEFAULT '',
"productName" TEXT NOT NULL DEFAULT '',
"propertiesValue" TEXT NOT NULL DEFAULT '',
"image" TEXT,
"linkCreatedAt" TIMESTAMP(3),
"listingStatus" TEXT NOT NULL DEFAULT '',
"enabledRaw" TEXT NOT NULL DEFAULT '',
"targetLinkStatus" TEXT NOT NULL DEFAULT '',
"opsOwner" TEXT NOT NULL DEFAULT '',
"opsDeptOwner" TEXT NOT NULL DEFAULT '',
"opsGeneralOwner" TEXT NOT NULL DEFAULT '',
"profitCenter" TEXT NOT NULL DEFAULT '',
"onSaleDays" INTEGER,
"realPerformance" TEXT NOT NULL DEFAULT '',
"syncedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "SupplyChainDistributionListing_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "SupplyChainDistributionListing_source_shopId_listingId_platformSkuId_barcode_key" ON "SupplyChainDistributionListing"("source", "shopId", "listingId", "platformSkuId", "barcode");
CREATE INDEX "SupplyChainDistributionListing_barcode_idx" ON "SupplyChainDistributionListing"("barcode");
CREATE INDEX "SupplyChainDistributionListing_listingId_idx" ON "SupplyChainDistributionListing"("listingId");
CREATE INDEX "SupplyChainDistributionListing_shopName_idx" ON "SupplyChainDistributionListing"("shopName");
CREATE INDEX "SupplyChainDistributionListing_opsOwner_idx" ON "SupplyChainDistributionListing"("opsOwner");
CREATE INDEX "SupplyChainDistributionListing_listingStatus_idx" ON "SupplyChainDistributionListing"("listingStatus");
CREATE INDEX "SupplyChainDistributionListing_platform_idx" ON "SupplyChainDistributionListing"("platform");

View File

@ -0,0 +1,12 @@
-- 链接维度改为真实平台链接:同一链接只保留一行,并保存命中的全部停产编码
ALTER TABLE "SupplyChainDiscontinuationNoticeLink"
ADD COLUMN IF NOT EXISTS "matchedBarcodes" TEXT NOT NULL DEFAULT '';
ALTER TABLE "SupplyChainDiscontinuationNoticeLink"
ADD COLUMN IF NOT EXISTS "shopId" TEXT NOT NULL DEFAULT '';
DROP INDEX IF EXISTS "SupplyChainDiscontinuationNoticeLink_noticeId_itemId_key";
DROP INDEX IF EXISTS "SupplyChainDiscontinuationNoticeLink_nps_item_key";
DROP INDEX IF EXISTS "SupplyChainDiscontinuationNoticeLink_nsi_item_key";
CREATE UNIQUE INDEX IF NOT EXISTS "SupplyChainDiscontinuationNoticeLink_nsis_key"
ON "SupplyChainDiscontinuationNoticeLink"("noticeId", "shopId", "itemId", "shopSkuCode");

View File

@ -0,0 +1,5 @@
-- 链接维度按聚水潭 SKU 维度展示:每个平台店铺商品编码保留独立明细
DROP INDEX IF EXISTS "SupplyChainDiscontinuationNoticeLink_nsi_item_key";
CREATE UNIQUE INDEX IF NOT EXISTS "SupplyChainDiscontinuationNoticeLink_nsis_key"
ON "SupplyChainDiscontinuationNoticeLink"("noticeId", "shopId", "itemId", "shopSkuCode");

View File

@ -0,0 +1,106 @@
-- 清货管理:清货池快照、月度任务与流转记录、复盘维度快照
CREATE TABLE "SupplyChainClearancePoolSnapshot" (
"id" TEXT NOT NULL,
"statDate" DATE NOT NULL,
"statTime" TIMESTAMP(3) NOT NULL,
"barcode" TEXT NOT NULL,
"sku" TEXT NOT NULL DEFAULT '',
"productName" TEXT NOT NULL DEFAULT '',
"brand" TEXT NOT NULL DEFAULT '',
"category" TEXT NOT NULL DEFAULT '',
"productStatus" TEXT NOT NULL DEFAULT '',
"ownerName" TEXT NOT NULL DEFAULT '',
"department" TEXT NOT NULL DEFAULT '',
"directorName" TEXT NOT NULL DEFAULT '',
"costPrice" DECIMAL(18,2),
"stockJianshang" INTEGER NOT NULL DEFAULT 0,
"stockFactory" INTEGER NOT NULL DEFAULT 0,
"totalStock" INTEGER NOT NULL DEFAULT 0,
"sourceFileName" TEXT NOT NULL DEFAULT '',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "SupplyChainClearancePoolSnapshot_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "SupplyChainClearancePoolSnapshot_barcode_statDate_key"
ON "SupplyChainClearancePoolSnapshot"("barcode", "statDate");
CREATE INDEX "SupplyChainClearancePoolSnapshot_statDate_idx"
ON "SupplyChainClearancePoolSnapshot"("statDate");
CREATE INDEX "SupplyChainClearancePoolSnapshot_ownerName_idx"
ON "SupplyChainClearancePoolSnapshot"("ownerName");
CREATE INDEX "SupplyChainClearancePoolSnapshot_productStatus_idx"
ON "SupplyChainClearancePoolSnapshot"("productStatus");
CREATE TABLE "SupplyChainClearanceTask" (
"id" TEXT NOT NULL,
"periodKey" VARCHAR(7) NOT NULL,
"barcode" TEXT NOT NULL,
"sku" TEXT NOT NULL DEFAULT '',
"productName" TEXT NOT NULL DEFAULT '',
"brand" TEXT NOT NULL DEFAULT '',
"productStatus" TEXT NOT NULL DEFAULT '',
"department" TEXT NOT NULL DEFAULT '',
"ownerName" TEXT NOT NULL DEFAULT '',
"taskQty" INTEGER NOT NULL,
"discount" DECIMAL(4,2) NOT NULL,
"reason" TEXT NOT NULL,
"status" VARCHAR(16) NOT NULL DEFAULT 'PENDING',
"submittedById" TEXT NOT NULL DEFAULT '',
"submittedByName" TEXT NOT NULL DEFAULT '',
"submittedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"reviewedById" TEXT NOT NULL DEFAULT '',
"reviewedByName" TEXT NOT NULL DEFAULT '',
"reviewedAt" TIMESTAMP(3),
"rejectReason" TEXT NOT NULL DEFAULT '',
"clearedQty" INTEGER NOT NULL DEFAULT 0,
"lastProgressAt" TIMESTAMP(3),
"revision" INTEGER NOT NULL DEFAULT 1,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "SupplyChainClearanceTask_pkey" PRIMARY KEY ("id")
);
CREATE INDEX "SupplyChainClearanceTask_periodKey_status_idx"
ON "SupplyChainClearanceTask"("periodKey", "status");
CREATE INDEX "SupplyChainClearanceTask_barcode_idx"
ON "SupplyChainClearanceTask"("barcode");
CREATE INDEX "SupplyChainClearanceTask_ownerName_idx"
ON "SupplyChainClearanceTask"("ownerName");
CREATE TABLE "SupplyChainClearanceTaskEvent" (
"id" TEXT NOT NULL,
"taskId" TEXT NOT NULL,
"action" VARCHAR(24) NOT NULL,
"actorId" TEXT NOT NULL DEFAULT '',
"actorName" TEXT NOT NULL DEFAULT '',
"note" TEXT NOT NULL DEFAULT '',
"before" JSONB,
"after" JSONB,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "SupplyChainClearanceTaskEvent_pkey" PRIMARY KEY ("id")
);
CREATE INDEX "SupplyChainClearanceTaskEvent_taskId_createdAt_idx"
ON "SupplyChainClearanceTaskEvent"("taskId", "createdAt");
ALTER TABLE "SupplyChainClearanceTaskEvent"
ADD CONSTRAINT "SupplyChainClearanceTaskEvent_taskId_fkey"
FOREIGN KEY ("taskId") REFERENCES "SupplyChainClearanceTask"("id")
ON DELETE CASCADE ON UPDATE CASCADE;
CREATE TABLE "SupplyChainClearanceReviewSnapshot" (
"id" TEXT NOT NULL,
"monthKey" VARCHAR(7) NOT NULL,
"dimension" VARCHAR(24) NOT NULL,
"dimensionKey" TEXT NOT NULL,
"dimensionLabel" TEXT NOT NULL,
"taskQty" INTEGER NOT NULL DEFAULT 0,
"clearedQty" INTEGER NOT NULL DEFAULT 0,
"completionRate" DECIMAL(6,2),
"actualMargin" DECIMAL(6,2),
"settleMargin" DECIMAL(6,2),
"recoveryRate" DECIMAL(6,2),
"sourceFile" TEXT NOT NULL DEFAULT '',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "SupplyChainClearanceReviewSnapshot_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "SupplyChainClearanceReviewSnapshot_month_dimension_key_key"
ON "SupplyChainClearanceReviewSnapshot"("monthKey", "dimension", "dimensionKey");
CREATE INDEX "SupplyChainClearanceReviewSnapshot_monthKey_dimension_idx"
ON "SupplyChainClearanceReviewSnapshot"("monthKey", "dimension");

View File

@ -0,0 +1,19 @@
-- 清货任务申报期:经管确认后,运营只能在窗口内申请任务和折扣
CREATE TABLE "SupplyChainClearanceApplicationWindow" (
"id" TEXT NOT NULL,
"periodKey" VARCHAR(7) NOT NULL,
"startAt" TIMESTAMP(3) NOT NULL,
"endAt" TIMESTAMP(3) NOT NULL,
"status" VARCHAR(16) NOT NULL DEFAULT 'CONFIRMED',
"confirmedById" TEXT NOT NULL DEFAULT '',
"confirmedByName" TEXT NOT NULL DEFAULT '',
"confirmedAt" TIMESTAMP(3),
"note" TEXT NOT NULL DEFAULT '',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "SupplyChainClearanceApplicationWindow_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "SupplyChainClearanceApplicationWindow_periodKey_key"
ON "SupplyChainClearanceApplicationWindow"("periodKey");
CREATE INDEX "SupplyChainClearanceApplicationWindow_status_startAt_endAt_idx"
ON "SupplyChainClearanceApplicationWindow"("status", "startAt", "endAt");

View File

@ -0,0 +1,3 @@
-- 清货池先全量映射产品全量档案,并单独保留销售状态
ALTER TABLE "SupplyChainClearancePoolSnapshot"
ADD COLUMN IF NOT EXISTS "salesStatus" TEXT NOT NULL DEFAULT '';

View File

@ -0,0 +1,20 @@
-- 清货池按 10 月清货池抬头扩展字段;申报期支持一期/二期
ALTER TABLE "SupplyChainClearancePoolSnapshot"
ADD COLUMN IF NOT EXISTS "spu" TEXT NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS "image" TEXT,
ADD COLUMN IF NOT EXISTS "supplierName" TEXT NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS "packType" TEXT NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS "cooperationStatus" TEXT NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS "dailySales" DECIMAL(18,4),
ADD COLUMN IF NOT EXISTS "availableDays" TEXT NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS "businessCreatedAt" TIMESTAMP(3),
ADD COLUMN IF NOT EXISTS "extra" JSONB;
ALTER TABLE "SupplyChainClearanceApplicationWindow"
ADD COLUMN IF NOT EXISTS "round" INTEGER NOT NULL DEFAULT 1;
DROP INDEX IF EXISTS "SupplyChainClearanceApplicationWindow_periodKey_key";
CREATE UNIQUE INDEX IF NOT EXISTS "SupplyChainClearanceApplicationWindow_periodKey_round_key"
ON "SupplyChainClearanceApplicationWindow"("periodKey", "round");
ALTER TABLE "SupplyChainClearanceTask"
ADD COLUMN IF NOT EXISTS "round" INTEGER NOT NULL DEFAULT 1;

View File

@ -0,0 +1,27 @@
CREATE TABLE "SupplyChainDistributionPerformanceRun" (
"id" TEXT NOT NULL,
"status" TEXT NOT NULL,
"periodStart" DATE NOT NULL,
"periodEnd" DATE NOT NULL,
"startedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"completedAt" TIMESTAMP(3),
"sourceRows" INTEGER NOT NULL DEFAULT 0,
"aggregateRows" INTEGER NOT NULL DEFAULT 0,
"matchedRows" INTEGER NOT NULL DEFAULT 0,
"unmatchedRows" INTEGER NOT NULL DEFAULT 0,
CONSTRAINT "SupplyChainDistributionPerformanceRun_pkey" PRIMARY KEY ("id")
);
CREATE INDEX "SupplyChainDistributionPerformanceRun_status_completedAt_idx"
ON "SupplyChainDistributionPerformanceRun"("status", "completedAt");
CREATE TABLE "SupplyChainDistributionPerformance" (
"runId" TEXT NOT NULL,
"barcode" TEXT NOT NULL,
"shopId" TEXT NOT NULL,
"listingId" TEXT NOT NULL,
"realPerformance" DECIMAL(24,4) NOT NULL,
"sourceRows" INTEGER NOT NULL,
CONSTRAINT "SupplyChainDistributionPerformance_pkey" PRIMARY KEY ("runId", "barcode", "shopId", "listingId"),
CONSTRAINT "SupplyChainDistributionPerformance_runId_fkey" FOREIGN KEY ("runId")
REFERENCES "SupplyChainDistributionPerformanceRun"("id") ON DELETE CASCADE ON UPDATE CASCADE
);

View File

@ -0,0 +1,22 @@
CREATE TABLE "SupplyChainManualLimitPriceBatch" (
"id" TEXT NOT NULL,
"fileName" TEXT NOT NULL,
"targetPrice" DECIMAL(18,4),
"rowCount" INTEGER NOT NULL,
"uniqueCodeCount" INTEGER NOT NULL,
"matchedBarcodeCount" INTEGER NOT NULL,
"matchedStyleCount" INTEGER NOT NULL,
"updatedLimitCount" INTEGER NOT NULL,
"codes" JSONB NOT NULL DEFAULT '[]',
"unmatchedCodes" JSONB NOT NULL DEFAULT '[]',
"invalidRows" JSONB NOT NULL DEFAULT '[]',
"conflictingStyles" JSONB NOT NULL DEFAULT '[]',
"uploadedById" TEXT NOT NULL DEFAULT '',
"uploadedByName" TEXT NOT NULL DEFAULT '',
"note" TEXT NOT NULL DEFAULT '',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "SupplyChainManualLimitPriceBatch_pkey" PRIMARY KEY ("id")
);
CREATE INDEX "SupplyChainManualLimitPriceBatch_createdAt_idx"
ON "SupplyChainManualLimitPriceBatch"("createdAt");

View File

@ -0,0 +1,24 @@
-- 原始套装规格只存一份;视图将原始货号映射到档案子条码,不复制宽表。
CREATE TABLE "SupplyChainDistributionComboItem" (
"comboCode" TEXT NOT NULL,
"barcode" TEXT NOT NULL,
"comboName" TEXT NOT NULL DEFAULT '',
"quantity" DECIMAL(24,6) NOT NULL,
CONSTRAINT "SupplyChainDistributionComboItem_pkey" PRIMARY KEY ("comboCode","barcode"),
CONSTRAINT "distribution_combo_positive_quantity" CHECK (quantity > 0)
);
CREATE INDEX "SupplyChainDistributionComboItem_barcode_idx" ON "SupplyChainDistributionComboItem" (barcode);
CREATE VIEW "SupplyChainDistributionListingResolved" AS
SELECT d.*, d.barcode AS "sourceSkuId", 'direct'::text AS "matchType",
''::text AS "comboName", NULL::numeric(24,6) AS "componentQuantity"
FROM "SupplyChainDistributionListing" d
WHERE EXISTS (SELECT 1 FROM "SupplyChainProductArchive" a WHERE a.barcode=d.barcode)
UNION ALL
SELECT d.id,c.barcode,d."listingId",d."platformSkuId",d.source,d.channel,d.platform,d."shopId",d."shopName",
d."productName",d."propertiesValue",d.image,d."linkCreatedAt",d."listingStatus",d."enabledRaw",
d."targetLinkStatus",d."opsOwner",d."opsDeptOwner",d."opsGeneralOwner",d."profitCenter",d."onSaleDays",
d."realPerformance",d."syncedAt",d."createdAt",d."updatedAt",d.barcode AS "sourceSkuId",
'combo'::text AS "matchType",c."comboName",c.quantity AS "componentQuantity"
FROM "SupplyChainDistributionListing" d
JOIN "SupplyChainDistributionComboItem" c ON c."comboCode"=d.barcode;

View File

@ -0,0 +1,67 @@
-- 清货池按月冻结:经管生成一次后不再变更,并通过生成记录区分清货月份。
ALTER TABLE "SupplyChainClearancePoolSnapshot"
ADD COLUMN IF NOT EXISTS "periodKey" VARCHAR(7),
ADD COLUMN IF NOT EXISTS "generatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;
UPDATE "SupplyChainClearancePoolSnapshot"
SET
"periodKey" = TO_CHAR("statDate", 'YYYY-MM'),
"generatedAt" = "statTime"
WHERE "periodKey" IS NULL;
DROP INDEX IF EXISTS "SupplyChainClearancePoolSnapshot_barcode_statDate_key";
CREATE UNIQUE INDEX "SupplyChainClearancePoolSnapshot_barcode_periodKey_statDate_key"
ON "SupplyChainClearancePoolSnapshot"("barcode", "periodKey", "statDate");
CREATE INDEX IF NOT EXISTS "SupplyChainClearancePoolSnapshot_periodKey_statDate_idx"
ON "SupplyChainClearancePoolSnapshot"("periodKey", "statDate");
CREATE TABLE "SupplyChainClearancePoolGeneration" (
"id" TEXT NOT NULL,
"periodKey" VARCHAR(7) NOT NULL,
"statDate" DATE NOT NULL,
"generatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"generatedById" TEXT NOT NULL DEFAULT '',
"generatedByName" TEXT NOT NULL DEFAULT '',
"rowCount" INTEGER NOT NULL DEFAULT 0,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "SupplyChainClearancePoolGeneration_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "SupplyChainClearancePoolGeneration_periodKey_key"
ON "SupplyChainClearancePoolGeneration"("periodKey");
CREATE INDEX "SupplyChainClearancePoolGeneration_generatedAt_idx"
ON "SupplyChainClearancePoolGeneration"("generatedAt");
WITH ranked_snapshots AS (
SELECT
TO_CHAR("statDate", 'YYYY-MM') AS "periodKey",
"statDate",
"statTime",
COUNT(*)::integer AS "rowCount",
ROW_NUMBER() OVER (
PARTITION BY TO_CHAR("statDate", 'YYYY-MM')
ORDER BY "statDate" DESC, "statTime" DESC
) AS "rowNumber"
FROM "SupplyChainClearancePoolSnapshot"
GROUP BY "statDate", "statTime"
)
INSERT INTO "SupplyChainClearancePoolGeneration" (
"id",
"periodKey",
"statDate",
"generatedAt",
"generatedById",
"generatedByName",
"rowCount"
)
SELECT
'clearance-pool-' || REPLACE("periodKey", '-', ''),
"periodKey",
"statDate",
"statTime",
'',
'历史快照',
"rowCount"
FROM ranked_snapshots
WHERE "rowNumber" = 1
ON CONFLICT ("periodKey") DO NOTHING;

View File

@ -0,0 +1,37 @@
-- SJ0380 资料表中的 74 个商品编码统一使用人工指定限价 49。
-- 使用 UPSERT,确保该迁移可在已有或尚无对应限价行的环境安全执行。
INSERT INTO "SupplyChainStyleLimitPrice" (
"id",
"styleCode",
"ownerLimitPrice",
"processInstanceId",
"processNo",
"applyType",
"initiator",
"approvedAt",
"createdAt",
"updatedAt"
)
SELECT
md5(random()::text || code."styleCode" || clock_timestamp()::text),
code."styleCode",
49,
'',
'',
'人工指定限价',
'SJ0380资料表',
CURRENT_TIMESTAMP,
CURRENT_TIMESTAMP,
CURRENT_TIMESTAMP
FROM (
SELECT 'SJ038' || lpad(sequence_no::text, 3, '0') AS "styleCode"
FROM generate_series(1, 74) AS sequence_no
) AS code
ON CONFLICT ("styleCode") DO UPDATE SET
"ownerLimitPrice" = EXCLUDED."ownerLimitPrice",
"processInstanceId" = EXCLUDED."processInstanceId",
"processNo" = EXCLUDED."processNo",
"applyType" = EXCLUDED."applyType",
"initiator" = EXCLUDED."initiator",
"approvedAt" = EXCLUDED."approvedAt",
"updatedAt" = CURRENT_TIMESTAMP;

View File

@ -0,0 +1,5 @@
-- 停产下架库存预警按商品编码 + 仓库拆分,不再将一个商品的多个仓库合并为一行。
DROP INDEX IF EXISTS "SupplyChainDiscontinuationInventoryAlertSnapshot_barcode_statDate_key";
CREATE UNIQUE INDEX "SupplyChainDiscontinuationInventoryAlertSnapshot_barcode_warehouseName_statDate_key"
ON "SupplyChainDiscontinuationInventoryAlertSnapshot"("barcode", "warehouseName", "statDate");

View File

@ -0,0 +1,142 @@
-- Compensates only the authorized SJ038001-SJ038074 price-49 release.
-- Original business values come from the production snapshot taken before 925f5b2e.
-- Keep existing IDs, creation times, applied migration history and later features.
-- Snapshot timestamps were serialized by node-postgres in Asia/Shanghai; restore
-- their original timestamp-without-time-zone wall clock explicitly.
BEGIN;
CREATE TEMP TABLE sj0380_original_limits ON COMMIT DROP AS
SELECT code, price::numeric(18,4) AS price, instance_id, process_no, apply_type, initiator,
approved_iso::timestamptz AT TIME ZONE 'Asia/Shanghai' AS approved_at
FROM (VALUES
('SJ038001', '91.5700', 'hN9meWbASsKroqK0iGJZpg08691772071804', '202602261010000047795', '', '黄庭坚', '2026-02-26T02:16:00.000Z'),
('SJ038002', '91.5700', 'hN9meWbASsKroqK0iGJZpg08691772071804', '202602261010000047795', '', '黄庭坚', '2026-02-26T02:16:00.000Z'),
('SJ038003', '91.5700', 'hN9meWbASsKroqK0iGJZpg08691772071804', '202602261010000047795', '', '黄庭坚', '2026-02-26T02:16:00.000Z'),
('SJ038004', '91.5700', 'hN9meWbASsKroqK0iGJZpg08691772071804', '202602261010000047795', '', '黄庭坚', '2026-02-26T02:16:00.000Z'),
('SJ038005', '138.4800', 'hN9meWbASsKroqK0iGJZpg08691772071804', '202602261010000047795', '', '黄庭坚', '2026-02-26T02:16:00.000Z'),
('SJ038006', '138.4800', 'hN9meWbASsKroqK0iGJZpg08691772071804', '202602261010000047795', '', '黄庭坚', '2026-02-26T02:16:00.000Z'),
('SJ038007', '138.4800', 'hN9meWbASsKroqK0iGJZpg08691772071804', '202602261010000047795', '', '黄庭坚', '2026-02-26T02:16:00.000Z'),
('SJ038008', '138.4800', 'hN9meWbASsKroqK0iGJZpg08691772071804', '202602261010000047795', '', '黄庭坚', '2026-02-26T02:16:00.000Z'),
('SJ038009', '185.6800', 'hN9meWbASsKroqK0iGJZpg08691772071804', '202602261010000047795', '', '黄庭坚', '2026-02-26T02:16:00.000Z'),
('SJ038010', '185.6800', 'hN9meWbASsKroqK0iGJZpg08691772071804', '202602261010000047795', '', '黄庭坚', '2026-02-26T02:16:00.000Z'),
('SJ038011', '185.6800', 'hN9meWbASsKroqK0iGJZpg08691772071804', '202602261010000047795', '', '黄庭坚', '2026-02-26T02:16:00.000Z'),
('SJ038012', '185.6800', 'hN9meWbASsKroqK0iGJZpg08691772071804', '202602261010000047795', '', '黄庭坚', '2026-02-26T02:16:00.000Z'),
('SJ038013', '210.8200', 'hN9meWbASsKroqK0iGJZpg08691772071804', '202602261010000047795', '', '黄庭坚', '2026-02-26T02:16:00.000Z'),
('SJ038014', '210.8200', 'hN9meWbASsKroqK0iGJZpg08691772071804', '202602261010000047795', '', '黄庭坚', '2026-02-26T02:16:00.000Z'),
('SJ038015', '210.8200', 'hN9meWbASsKroqK0iGJZpg08691772071804', '202602261010000047795', '', '黄庭坚', '2026-02-26T02:16:00.000Z'),
('SJ038016', '210.8200', 'hN9meWbASsKroqK0iGJZpg08691772071804', '202602261010000047795', '', '黄庭坚', '2026-02-26T02:16:00.000Z'),
('SJ038017', '269.1000', 'oS3EbzH0Rve2k7xU13diog08691774594560', '202603271456000007703', '', '玉衡', '2026-03-28T10:07:00.000Z'),
('SJ038018', '300.0000', 'KCeOJQpQRMu4pOtsy8v0xA08691782378849', '202606251714000090495', '调整原货号限价', '幻琪', '2026-06-25T09:15:00.000Z'),
('SJ038019', '269.1000', 'oS3EbzH0Rve2k7xU13diog08691774594560', '202603271456000007703', '', '玉衡', '2026-03-28T10:07:00.000Z'),
('SJ038020', '269.1000', 'oS3EbzH0Rve2k7xU13diog08691774594560', '202603271456000007703', '', '玉衡', '2026-03-28T10:07:00.000Z'),
('SJ038021', '339.0000', 'oS3EbzH0Rve2k7xU13diog08691774594560', '202603271456000007703', '', '玉衡', '2026-03-28T10:07:00.000Z'),
('SJ038022', '400.0000', 'KCeOJQpQRMu4pOtsy8v0xA08691782378849', '202606251714000090495', '调整原货号限价', '幻琪', '2026-06-25T09:15:00.000Z'),
('SJ038023', '339.0000', 'oS3EbzH0Rve2k7xU13diog08691774594560', '202603271456000007703', '', '玉衡', '2026-03-28T10:07:00.000Z'),
('SJ038024', '339.0000', 'oS3EbzH0Rve2k7xU13diog08691774594560', '202603271456000007703', '', '玉衡', '2026-03-28T10:07:00.000Z'),
('SJ038025', '354.0000', '7oN6tLEGTmGpaNURkt0Aug08691769226248', '202601241144000089818', '', '伯阳', '2026-01-26T01:24:00.000Z'),
('SJ038026', '354.0000', '7oN6tLEGTmGpaNURkt0Aug08691769226248', '202601241144000089818', '', '伯阳', '2026-01-26T01:24:00.000Z'),
('SJ038027', '354.0000', '7oN6tLEGTmGpaNURkt0Aug08691769226248', '202601241144000089818', '', '伯阳', '2026-01-26T01:24:00.000Z'),
('SJ038028', '354.0000', '7oN6tLEGTmGpaNURkt0Aug08691769226248', '202601241144000089818', '', '伯阳', '2026-01-26T01:24:00.000Z'),
('SJ038029', '374.3000', 'oS3EbzH0Rve2k7xU13diog08691774594560', '202603271456000007703', '', '玉衡', '2026-03-28T10:07:00.000Z'),
('SJ038030', '374.3000', 'oS3EbzH0Rve2k7xU13diog08691774594560', '202603271456000007703', '', '玉衡', '2026-03-28T10:07:00.000Z'),
('SJ038031', '374.3000', 'oS3EbzH0Rve2k7xU13diog08691774594560', '202603271456000007703', '', '玉衡', '2026-03-28T10:07:00.000Z'),
('SJ038032', '374.3000', 'oS3EbzH0Rve2k7xU13diog08691774594560', '202603271456000007703', '', '玉衡', '2026-03-28T10:07:00.000Z'),
('SJ038033', '21.0000', 'tXMYPcFQTPSRAYPVw3uqxQ08691773199660', '202603111127000406634', '', '伯阳', '2026-03-11T05:32:00.000Z'),
('SJ038034', '21.0000', 'tXMYPcFQTPSRAYPVw3uqxQ08691773199660', '202603111127000406634', '', '伯阳', '2026-03-11T05:32:00.000Z'),
('SJ038035', '21.0000', 'tXMYPcFQTPSRAYPVw3uqxQ08691773199660', '202603111127000406634', '', '伯阳', '2026-03-11T05:32:00.000Z'),
('SJ038036', '21.0000', 'tXMYPcFQTPSRAYPVw3uqxQ08691773199660', '202603111127000406634', '', '伯阳', '2026-03-11T05:32:00.000Z'),
('SJ038037', '21.0000', 'tXMYPcFQTPSRAYPVw3uqxQ08691773199660', '202603111127000406634', '', '伯阳', '2026-03-11T05:32:00.000Z'),
('SJ038038', '16.6300', 'hN9meWbASsKroqK0iGJZpg08691772071804', '202602261010000047795', '', '黄庭坚', '2026-02-26T02:16:00.000Z'),
('SJ038039', '21.0000', 'tXMYPcFQTPSRAYPVw3uqxQ08691773199660', '202603111127000406634', '', '伯阳', '2026-03-11T05:32:00.000Z'),
('SJ038040', '21.0000', 'tXMYPcFQTPSRAYPVw3uqxQ08691773199660', '202603111127000406634', '', '伯阳', '2026-03-11T05:32:00.000Z'),
('SJ038041', '24.6000', '1hqF4UwARuGIvXLEMCHt0A08691783661088', '202607101324000489426', '新增货号限价', '涂涂', '2026-07-10T05:44:00.000Z'),
('SJ038042', '24.6000', '1hqF4UwARuGIvXLEMCHt0A08691783661088', '202607101324000489426', '新增货号限价', '涂涂', '2026-07-10T05:44:00.000Z'),
('SJ038043', '25.6000', '1hqF4UwARuGIvXLEMCHt0A08691783661088', '202607101324000489426', '新增货号限价', '涂涂', '2026-07-10T05:44:00.000Z'),
('SJ038044', '25.6000', '1hqF4UwARuGIvXLEMCHt0A08691783661088', '202607101324000489426', '新增货号限价', '涂涂', '2026-07-10T05:44:00.000Z'),
('SJ038045', '25.6000', '1hqF4UwARuGIvXLEMCHt0A08691783661088', '202607101324000489426', '新增货号限价', '涂涂', '2026-07-10T05:44:00.000Z'),
('SJ038046', '25.6000', '1hqF4UwARuGIvXLEMCHt0A08691783661088', '202607101324000489426', '新增货号限价', '涂涂', '2026-07-10T05:44:00.000Z'),
('SJ038047', '25.6000', '1hqF4UwARuGIvXLEMCHt0A08691783661088', '202607101324000489426', '新增货号限价', '涂涂', '2026-07-10T05:44:00.000Z'),
('SJ038048', '25.6000', '1hqF4UwARuGIvXLEMCHt0A08691783661088', '202607101324000489426', '新增货号限价', '涂涂', '2026-07-10T05:44:00.000Z'),
('SJ038049', '25.6000', '1hqF4UwARuGIvXLEMCHt0A08691783661088', '202607101324000489426', '新增货号限价', '涂涂', '2026-07-10T05:44:00.000Z'),
('SJ038050', '25.6000', '1hqF4UwARuGIvXLEMCHt0A08691783661088', '202607101324000489426', '新增货号限价', '涂涂', '2026-07-10T05:44:00.000Z'),
('SJ038051', '25.6000', '1hqF4UwARuGIvXLEMCHt0A08691783661088', '202607101324000489426', '新增货号限价', '涂涂', '2026-07-10T05:44:00.000Z'),
('SJ038052', '25.6000', '1hqF4UwARuGIvXLEMCHt0A08691783661088', '202607101324000489426', '新增货号限价', '涂涂', '2026-07-10T05:44:00.000Z'),
('SJ038053', '25.6000', '1hqF4UwARuGIvXLEMCHt0A08691783661088', '202607101324000489426', '新增货号限价', '涂涂', '2026-07-10T05:44:00.000Z'),
('SJ038054', '25.6000', '1hqF4UwARuGIvXLEMCHt0A08691783661088', '202607101324000489426', '新增货号限价', '涂涂', '2026-07-10T05:44:00.000Z'),
('SJ038055', '25.6000', '1hqF4UwARuGIvXLEMCHt0A08691783661088', '202607101324000489426', '新增货号限价', '涂涂', '2026-07-10T05:44:00.000Z'),
('SJ038056', '25.6000', '1hqF4UwARuGIvXLEMCHt0A08691783661088', '202607101324000489426', '新增货号限价', '涂涂', '2026-07-10T05:44:00.000Z'),
('SJ038057', '25.6000', '1hqF4UwARuGIvXLEMCHt0A08691783661088', '202607101324000489426', '新增货号限价', '涂涂', '2026-07-10T05:44:00.000Z'),
('SJ038058', '25.6000', '1hqF4UwARuGIvXLEMCHt0A08691783661088', '202607101324000489426', '新增货号限价', '涂涂', '2026-07-10T05:44:00.000Z'),
('SJ038059', '25.6000', '1hqF4UwARuGIvXLEMCHt0A08691783661088', '202607101324000489426', '新增货号限价', '涂涂', '2026-07-10T05:44:00.000Z'),
('SJ038060', '25.6000', '1hqF4UwARuGIvXLEMCHt0A08691783661088', '202607101324000489426', '新增货号限价', '涂涂', '2026-07-10T05:44:00.000Z'),
('SJ038061', '25.6000', '1hqF4UwARuGIvXLEMCHt0A08691783661088', '202607101324000489426', '新增货号限价', '涂涂', '2026-07-10T05:44:00.000Z'),
('SJ038062', '25.6000', '1hqF4UwARuGIvXLEMCHt0A08691783661088', '202607101324000489426', '新增货号限价', '涂涂', '2026-07-10T05:44:00.000Z'),
('SJ038063', '25.6000', '1hqF4UwARuGIvXLEMCHt0A08691783661088', '202607101324000489426', '新增货号限价', '涂涂', '2026-07-10T05:44:00.000Z'),
('SJ038064', '25.6000', '1hqF4UwARuGIvXLEMCHt0A08691783661088', '202607101324000489426', '新增货号限价', '涂涂', '2026-07-10T05:44:00.000Z'),
('SJ038065', '25.6000', '1hqF4UwARuGIvXLEMCHt0A08691783661088', '202607101324000489426', '新增货号限价', '涂涂', '2026-07-10T05:44:00.000Z'),
('SJ038066', '25.6000', '1hqF4UwARuGIvXLEMCHt0A08691783661088', '202607101324000489426', '新增货号限价', '涂涂', '2026-07-10T05:44:00.000Z'),
('SJ038067', '25.6000', '1hqF4UwARuGIvXLEMCHt0A08691783661088', '202607101324000489426', '新增货号限价', '涂涂', '2026-07-10T05:44:00.000Z'),
('SJ038068', '25.6000', '1hqF4UwARuGIvXLEMCHt0A08691783661088', '202607101324000489426', '新增货号限价', '涂涂', '2026-07-10T05:44:00.000Z'),
('SJ038069', '25.6000', '1hqF4UwARuGIvXLEMCHt0A08691783661088', '202607101324000489426', '新增货号限价', '涂涂', '2026-07-10T05:44:00.000Z'),
('SJ038070', '25.6000', '1hqF4UwARuGIvXLEMCHt0A08691783661088', '202607101324000489426', '新增货号限价', '涂涂', '2026-07-10T05:44:00.000Z'),
('SJ038071', '25.6000', '1hqF4UwARuGIvXLEMCHt0A08691783661088', '202607101324000489426', '新增货号限价', '涂涂', '2026-07-10T05:44:00.000Z'),
('SJ038072', '25.6000', '1hqF4UwARuGIvXLEMCHt0A08691783661088', '202607101324000489426', '新增货号限价', '涂涂', '2026-07-10T05:44:00.000Z'),
('SJ038073', '25.6000', '1hqF4UwARuGIvXLEMCHt0A08691783661088', '202607101324000489426', '新增货号限价', '涂涂', '2026-07-10T05:44:00.000Z'),
('SJ038074', '25.6000', '1hqF4UwARuGIvXLEMCHt0A08691783661088', '202607101324000489426', '新增货号限价', '涂涂', '2026-07-10T05:44:00.000Z')
) AS originals(code,price,instance_id,process_no,apply_type,initiator,approved_iso);
DO $restore$
DECLARE invalid_codes text;
BEGIN
IF (SELECT count(*) FROM sj0380_original_limits) <> 74
OR (SELECT count(DISTINCT code) FROM sj0380_original_limits) <> 74
OR EXISTS (
SELECT 'SJ038'||lpad(n::text,3,'0') FROM generate_series(1,74) n
EXCEPT SELECT code FROM sj0380_original_limits
) THEN
RAISE EXCEPTION 'SJ0380 restoration snapshot must contain exactly the authorized 74 codes';
END IF;
PERFORM 1 FROM "SupplyChainStyleLimitPrice" t
JOIN sj0380_original_limits o ON o.code=t."styleCode"
ORDER BY t."styleCode" FOR UPDATE OF t;
SELECT string_agg(o.code,', ' ORDER BY o.code) INTO invalid_codes
FROM sj0380_original_limits o
LEFT JOIN "SupplyChainStyleLimitPrice" t ON t."styleCode"=o.code
WHERE t.id IS NULL OR NOT (
(ROW(t."ownerLimitPrice",t."processInstanceId",t."processNo",t."applyType",t."initiator",t."approvedAt")
IS NOT DISTINCT FROM ROW(o.price,o.instance_id,o.process_no,o.apply_type,o.initiator,o.approved_at))
OR (t."ownerLimitPrice" IS NOT DISTINCT FROM 49::numeric
AND t."applyType" IS NOT DISTINCT FROM '人工指定限价'
AND t.initiator IS NOT DISTINCT FROM 'SJ0380资料表'
AND t."processInstanceId" IS NOT DISTINCT FROM ''
AND t."processNo" IS NOT DISTINCT FROM '')
);
IF invalid_codes IS NOT NULL THEN
RAISE EXCEPTION 'SJ0380 rows changed or missing; refusing restoration: %',invalid_codes;
END IF;
UPDATE "SupplyChainStyleLimitPrice" t SET
"ownerLimitPrice"=o.price,
"processInstanceId"=o.instance_id,
"processNo"=o.process_no,
"applyType"=o.apply_type,
initiator=o.initiator,
"approvedAt"=o.approved_at,
"updatedAt"=CURRENT_TIMESTAMP
FROM sj0380_original_limits o
WHERE t."styleCode"=o.code AND NOT (ROW(t."ownerLimitPrice",t."processInstanceId",t."processNo",t."applyType",t."initiator",t."approvedAt")
IS NOT DISTINCT FROM ROW(o.price,o.instance_id,o.process_no,o.apply_type,o.initiator,o.approved_at));
IF EXISTS (
SELECT 1 FROM sj0380_original_limits o
LEFT JOIN "SupplyChainStyleLimitPrice" t ON t."styleCode"=o.code
WHERE t.id IS NULL OR NOT (ROW(t."ownerLimitPrice",t."processInstanceId",t."processNo",t."applyType",t."initiator",t."approvedAt")
IS NOT DISTINCT FROM ROW(o.price,o.instance_id,o.process_no,o.apply_type,o.initiator,o.approved_at))
) THEN
RAISE EXCEPTION 'SJ0380 restoration verification failed';
END IF;
END $restore$;
COMMIT;

View File

@ -0,0 +1,97 @@
-- 按「限价.xlsx」命中大货商品 42 个、品牌商品 39 个,共 81 个货号。
-- 仅更新货号负责人限价,不修改组合关系、组合编码或组合限价计算逻辑。
WITH target_styles ("styleCode") AS (
VALUES
('GBSFY-SJ038033'),
('GBSFY-SJ038034'),
('GBSFY-SJ038036'),
('GBSFY-SJ038039'),
('GBSFY-SJ038040'),
('GBSFY-SJ038041'),
('GBSFY-SJ038042'),
('GBSFY-SJ038043'),
('GBSFY-SJ038044'),
('GBSFY-SJ038045'),
('GBSFY-SJ038046'),
('GBSFY-SJ038047'),
('GBSFY-SJ038048'),
('GBSFY-SJ038049'),
('GBSFY-SJ038050'),
('GBSFY-SJ038051'),
('GBSFY-SJ038052'),
('GBSFY-SJ038053'),
('GBSFY-SJ038054'),
('GBSFY-SJ038055'),
('GBSFY-SJ038056'),
('GBSFY-SJ038057'),
('GBSFY-SJ038058'),
('GBSFY-SJ038059'),
('GBSFY-SJ038060'),
('GBSFY-SJ038061'),
('GBSFY-SJ038062'),
('GBSFY-SJ038063'),
('GBSFY-SJ038064'),
('GBSFY-SJ038065'),
('GBSFY-SJ038066'),
('GBSFY-SJ038067'),
('GBSFY-SJ038068'),
('GBSFY-SJ038069'),
('GBSFY-SJ038070'),
('GBSFY-SJ038071'),
('GBSFY-SJ038072'),
('GBSFY-SJ038073'),
('GBSFY-SJ038074'),
('SJ038033'),
('SJ038034'),
('SJ038035'),
('SJ038036'),
('SJ038037'),
('SJ038038'),
('SJ038039'),
('SJ038040'),
('SJ038041'),
('SJ038042'),
('SJ038043'),
('SJ038044'),
('SJ038045'),
('SJ038046'),
('SJ038047'),
('SJ038048'),
('SJ038049'),
('SJ038050'),
('SJ038051'),
('SJ038052'),
('SJ038053'),
('SJ038054'),
('SJ038055'),
('SJ038056'),
('SJ038057'),
('SJ038058'),
('SJ038059'),
('SJ038060'),
('SJ038061'),
('SJ038062'),
('SJ038063'),
('SJ038064'),
('SJ038065'),
('SJ038066'),
('SJ038067'),
('SJ038068'),
('SJ038069'),
('SJ038070'),
('SJ038071'),
('SJ038072'),
('SJ038073'),
('SJ038074')
)
UPDATE "SupplyChainStyleLimitPrice" AS limit_price
SET
"ownerLimitPrice" = 49,
"processInstanceId" = '',
"processNo" = '',
"applyType" = '人工指定限价',
"initiator" = '限价.xlsx',
"approvedAt" = CURRENT_TIMESTAMP,
"updatedAt" = CURRENT_TIMESTAMP
FROM target_styles
WHERE limit_price."styleCode" = target_styles."styleCode";

View File

@ -0,0 +1,3 @@
-- 清货池生成模式:全量生成或二次折扣调整时仅刷新库存与销售相关字段。
ALTER TABLE "SupplyChainClearancePoolGeneration"
ADD COLUMN IF NOT EXISTS "generationMode" VARCHAR(24) NOT NULL DEFAULT 'FULL';

View File

@ -0,0 +1,25 @@
-- 未下架监控按“订单 + 平台店铺商品编码”唯一,并补齐链接和责任人字段。
ALTER TABLE "SupplyChainDiscontinuationPenalty"
ADD COLUMN "oiId" TEXT NOT NULL DEFAULT '',
ADD COLUMN "shopId" TEXT NOT NULL DEFAULT '',
ADD COLUMN "shopSkuCode" TEXT NOT NULL DEFAULT '',
ADD COLUMN "deptHead" TEXT NOT NULL DEFAULT '',
ADD COLUMN "profitCenter" TEXT NOT NULL DEFAULT '',
ADD COLUMN "quantity" INTEGER NOT NULL DEFAULT 1,
ADD COLUMN "sourceType" TEXT NOT NULL DEFAULT 'OTHER';
DROP INDEX IF EXISTS "SupplyChainDiscontinuationPenalty_oId_itemId_key";
CREATE INDEX "SupplyChainDiscontinuationPenalty_shopSkuCode_idx"
ON "SupplyChainDiscontinuationPenalty"("shopSkuCode");
CREATE INDEX "SupplyChainDiscontinuationPenalty_payAt_idx"
ON "SupplyChainDiscontinuationPenalty"("payAt");
ALTER TABLE "SupplyChainDiscontinuationPenalty"
ALTER COLUMN "payAt" TYPE TIMESTAMPTZ(3) USING "payAt" AT TIME ZONE 'UTC',
ALTER COLUMN "noticeAt" TYPE TIMESTAMPTZ(3) USING "noticeAt" AT TIME ZONE 'UTC',
ALTER COLUMN "statTime" TYPE TIMESTAMPTZ(3) USING "statTime" AT TIME ZONE 'UTC',
ALTER COLUMN "reviewedAt" TYPE TIMESTAMPTZ(3) USING "reviewedAt" AT TIME ZONE 'UTC';
CREATE UNIQUE INDEX "SupplyChainDiscontinuationPenalty_oId_shopSkuCode_key"
ON "SupplyChainDiscontinuationPenalty"("oId", "shopSkuCode");

View File

@ -0,0 +1,21 @@
ALTER TYPE "OperationLogAction" ADD VALUE 'FRONT_WAREHOUSE_INVENTORY_UPDATE';
CREATE TABLE "FrontWarehouseInventoryAdjustment" (
"id" TEXT NOT NULL,
"barcode" TEXT NOT NULL,
"backupBarcode" TEXT NOT NULL DEFAULT '',
"backupSupplier" TEXT NOT NULL DEFAULT '',
"expectedDelivery" TEXT NOT NULL DEFAULT '',
"truckScheduledAt" TEXT NOT NULL DEFAULT '',
"truckQuantity" INTEGER,
"updatedById" TEXT,
"updatedByName" TEXT NOT NULL DEFAULT '',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "FrontWarehouseInventoryAdjustment_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "FrontWarehouseInventoryAdjustment_barcode_key"
ON "FrontWarehouseInventoryAdjustment"("barcode");
CREATE INDEX "FrontWarehouseInventoryAdjustment_updatedAt_idx"
ON "FrontWarehouseInventoryAdjustment"("updatedAt");

View File

@ -0,0 +1,43 @@
-- Small, replaceable read models. Detail rows and their counting rules stay unchanged.
CREATE TABLE "SupplyChainDistributionBarcodeSummary" (
barcode TEXT NOT NULL PRIMARY KEY,
"listingCount" INTEGER NOT NULL CHECK ("listingCount">0),
platforms TEXT[] NOT NULL,
"shopNames" TEXT[] NOT NULL,
"opsOwners" TEXT[] NOT NULL
);
CREATE INDEX "DistributionBarcodeSummary_count_barcode_idx"
ON "SupplyChainDistributionBarcodeSummary" ("listingCount" DESC,barcode ASC);
CREATE INDEX "DistributionBarcodeSummary_platforms_idx" ON "SupplyChainDistributionBarcodeSummary" USING GIN (platforms);
CREATE INDEX "DistributionBarcodeSummary_shopNames_idx" ON "SupplyChainDistributionBarcodeSummary" USING GIN ("shopNames");
CREATE INDEX "DistributionBarcodeSummary_opsOwners_idx" ON "SupplyChainDistributionBarcodeSummary" USING GIN ("opsOwners");
CREATE TABLE "SupplyChainDistributionFilterOption" (
field TEXT NOT NULL,
value TEXT NOT NULL,
count INTEGER NOT NULL,
PRIMARY KEY (field,value)
);
CREATE TABLE "SupplyChainDistributionSnapshotState" (
id TEXT NOT NULL PRIMARY KEY CHECK (id='current'),
version TEXT NOT NULL,
total INTEGER NOT NULL,
"linkCount" INTEGER NOT NULL,
"publishedAt" TIMESTAMPTZ NOT NULL
);
-- Backfill from the current published snapshot during this one-time migration.
INSERT INTO "SupplyChainDistributionBarcodeSummary"
SELECT barcode,COUNT(DISTINCT (platform,"shopId","listingId"))::int,
array_agg(DISTINCT platform),array_agg(DISTINCT "shopName"),array_agg(DISTINCT "opsOwner")
FROM "SupplyChainDistributionListingResolved" WHERE "listingStatus"='当前在线' GROUP BY barcode;
INSERT INTO "SupplyChainDistributionFilterOption"
SELECT v.field,v.value,COUNT(*)::int FROM "SupplyChainDistributionListing" d
CROSS JOIN LATERAL (VALUES ('platform',d.platform),('shopName',d."shopName"),
('listingStatus',d."listingStatus"),('opsOwner',d."opsOwner")) v(field,value)
WHERE v.value<>'' GROUP BY v.field,v.value;
INSERT INTO "SupplyChainDistributionSnapshotState"
SELECT 'current',gen_random_uuid()::text,COUNT(*)::int,COALESCE(SUM("listingCount"),0)::int,CURRENT_TIMESTAMP
FROM "SupplyChainDistributionBarcodeSummary";
ANALYZE "SupplyChainDistributionBarcodeSummary";
ANALYZE "SupplyChainDistributionFilterOption";

View File

@ -0,0 +1,37 @@
CREATE TABLE "DiyBarcodeOverride" (
"id" TEXT NOT NULL,
"brand" TEXT NOT NULL,
"spu" TEXT NOT NULL,
"itemCode" TEXT NOT NULL,
"barcode" TEXT NOT NULL,
"updatedById" TEXT,
"updatedByName" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "DiyBarcodeOverride_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "DiyBarcodeOverride_brand_spu_itemCode_key"
ON "DiyBarcodeOverride"("brand", "spu", "itemCode");
CREATE INDEX "DiyBarcodeOverride_spu_itemCode_idx"
ON "DiyBarcodeOverride"("spu", "itemCode");
CREATE TABLE "DiyBarcodeChangeLog" (
"id" TEXT NOT NULL,
"brand" TEXT NOT NULL,
"spu" TEXT NOT NULL,
"itemCode" TEXT NOT NULL,
"previousBarcode" TEXT NOT NULL,
"newBarcode" TEXT NOT NULL,
"actorId" TEXT,
"actorName" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "DiyBarcodeChangeLog_pkey" PRIMARY KEY ("id")
);
CREATE INDEX "DiyBarcodeChangeLog_spu_createdAt_idx"
ON "DiyBarcodeChangeLog"("spu", "createdAt");
CREATE INDEX "DiyBarcodeChangeLog_brand_spu_itemCode_createdAt_idx"
ON "DiyBarcodeChangeLog"("brand", "spu", "itemCode", "createdAt");

View File

@ -0,0 +1,49 @@
-- 前置仓下单表仅读取推送快照;进销存实时数据变化不会自动覆盖这里。
CREATE TABLE "FrontWarehouseInventoryOrderState" (
"id" TEXT NOT NULL,
"singletonKey" TEXT NOT NULL DEFAULT 'current',
"sourceStatus" JSONB NOT NULL,
"pushedById" TEXT,
"pushedByName" TEXT NOT NULL DEFAULT '',
"pushedAt" TIMESTAMP(3) NOT NULL,
"lastEditedById" TEXT,
"lastEditedByName" TEXT NOT NULL DEFAULT '',
"lastEditedAt" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "FrontWarehouseInventoryOrderState_pkey" PRIMARY KEY ("id")
);
CREATE TABLE "FrontWarehouseInventoryOrderRow" (
"id" TEXT NOT NULL,
"stateId" TEXT NOT NULL,
"barcode" TEXT NOT NULL,
"sortOrder" INTEGER NOT NULL,
"payload" JSONB NOT NULL,
"feedbackBackupBarcode" TEXT NOT NULL DEFAULT '',
"feedbackBackupSupplier" TEXT NOT NULL DEFAULT '',
"feedbackExpectedDelivery" TEXT NOT NULL DEFAULT '',
"truckScheduledAt" TEXT NOT NULL DEFAULT '',
"truckQuantity" INTEGER,
"feedbackUpdatedAt" TIMESTAMP(3),
"feedbackUpdatedByName" TEXT NOT NULL DEFAULT '',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "FrontWarehouseInventoryOrderRow_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "FrontWarehouseInventoryOrderState_singletonKey_key"
ON "FrontWarehouseInventoryOrderState"("singletonKey");
CREATE UNIQUE INDEX "FrontWarehouseInventoryOrderRow_stateId_barcode_key"
ON "FrontWarehouseInventoryOrderRow"("stateId", "barcode");
CREATE INDEX "FrontWarehouseInventoryOrderRow_stateId_sortOrder_idx"
ON "FrontWarehouseInventoryOrderRow"("stateId", "sortOrder");
ALTER TABLE "FrontWarehouseInventoryOrderRow"
ADD CONSTRAINT "FrontWarehouseInventoryOrderRow_stateId_fkey"
FOREIGN KEY ("stateId") REFERENCES "FrontWarehouseInventoryOrderState"("id")
ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -0,0 +1,9 @@
CREATE TABLE "SupplyChainClearanceConfig" (
"id" TEXT NOT NULL DEFAULT 'default',
"priorityRules" JSONB NOT NULL DEFAULT '[]',
"updatedById" TEXT NOT NULL DEFAULT '',
"updatedByName" TEXT NOT NULL DEFAULT '',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "SupplyChainClearanceConfig_pkey" PRIMARY KEY ("id")
);

View File

@ -0,0 +1,2 @@
ALTER TABLE "SupplyChainClearanceConfig"
ADD COLUMN IF NOT EXISTS "fixedTaskRules" JSONB NOT NULL DEFAULT '[]';

View File

@ -0,0 +1,3 @@
ALTER TABLE "SupplyChainClearanceConfig"
ADD COLUMN IF NOT EXISTS "priorityFallback" VARCHAR(50) NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS "fixedTaskFallbackFormula" VARCHAR(500) NOT NULL DEFAULT '';

View File

@ -0,0 +1,2 @@
ALTER TABLE "SupplyChainClearanceConfig"
ADD COLUMN IF NOT EXISTS "discountRestrictions" JSONB NOT NULL DEFAULT '[]';

View File

@ -0,0 +1,6 @@
-- 最终下单量允许人工覆盖;实时进销存与下单表快照分别保存自己的修改。
ALTER TABLE "FrontWarehouseInventoryAdjustment"
ADD COLUMN "finalOrderQty" INTEGER;
ALTER TABLE "FrontWarehouseInventoryOrderRow"
ADD COLUMN "finalOrderQty" INTEGER;

View File

@ -0,0 +1,146 @@
-- Market selection auxiliary data lives outside the platform public schema.
-- No demo data or upstream MySQL writes are part of this migration.
BEGIN;
CREATE SCHEMA IF NOT EXISTS app;
CREATE TABLE IF NOT EXISTS app.selection_market_items (
id bigserial PRIMARY KEY,
snapshot_date text NOT NULL,
cat_id text NOT NULL,
cat_name text,
source text DEFAULT 'item_rank',
rank integer,
item_id text NOT NULL,
item_title text,
shop_name text,
brand text,
price numeric,
pay_item_cnt numeric,
pay_amt numeric,
pay_buyer_cnt numeric,
flow_index numeric,
pay_conv_index numeric,
trade_index numeric,
pic_url text,
style text,
shape text,
material text,
color text,
scene text,
features text,
synced_at timestamptz DEFAULT now(),
UNIQUE (snapshot_date, cat_id, item_id, source)
);
CREATE TABLE IF NOT EXISTS app.selection_market_keywords (
id bigserial PRIMARY KEY,
snapshot_date text NOT NULL,
cat_id text NOT NULL,
cat_name text,
keyword text NOT NULL,
search_pv numeric,
click_rate numeric,
click_pv numeric,
pay_conv_rate numeric,
pay_item_cnt numeric,
mall_rate numeric,
supply_ratio numeric,
synced_at timestamptz DEFAULT now(),
UNIQUE (snapshot_date, cat_id, keyword)
);
CREATE TABLE IF NOT EXISTS app.selection_market_cat_demand (
id bigserial PRIMARY KEY,
snapshot_date text NOT NULL,
cat_id text NOT NULL,
page text DEFAULT 'catdemand',
rank integer,
cat2_name text,
cat3_name text,
demand_index numeric,
search_pv numeric,
pay_item_cnt numeric,
pay_amt numeric,
item_cnt numeric,
shop_cnt numeric,
click_rate numeric,
pay_conv_rate numeric,
synced_at timestamptz DEFAULT now(),
UNIQUE (snapshot_date, page, cat2_name, cat3_name)
);
CREATE TABLE IF NOT EXISTS app.selection_market_item_tags (
item_id text PRIMARY KEY,
style text,
shape text,
material text,
color text,
scene text,
features text,
model text,
confidence numeric,
cat2 text,
cat3 text,
updated_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_smit_cat ON app.selection_market_item_tags (cat2, cat3);
CREATE TABLE IF NOT EXISTS app.selection_market_style_clusters (
item_id text PRIMARY KEY,
cat2 text,
cat3 text,
cluster_id integer,
cluster_size integer,
rep_item_id text,
rep_title text,
rep_pic text,
dist_to_rep numeric,
threshold numeric,
sim numeric,
updated_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_smsc_cat ON app.selection_market_style_clusters (cat2, cat3);
CREATE TABLE IF NOT EXISTS app.selection_market_item_sig (
item_id text PRIMARY KEY,
cat2 text,
cat3 text,
ver text,
sig text NOT NULL,
updated_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_smsig_cat ON app.selection_market_item_sig (cat2, cat3);
CREATE TABLE IF NOT EXISTS app.selection_market_pool (
id serial PRIMARY KEY,
cat2 text,
cat3 text,
item_id text NOT NULL,
pic text,
title text,
brand text,
shop_name text,
price numeric,
price_band text,
band_volume numeric,
est_volume text,
selector text NOT NULL,
analysis text,
profit_calc jsonb,
profit_rate numeric,
sel_date date,
status text NOT NULL DEFAULT '意向',
created_by text NOT NULL,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
CREATE UNIQUE INDEX IF NOT EXISTS uidx_smp_item ON app.selection_market_pool (cat2, cat3, item_id);
COMMIT;

View File

@ -0,0 +1,24 @@
-- 模块化商品加工单:每日生成后自动生成;人工调整主表后可手动重新生成并替换当天明细。
CREATE TABLE "ModularProcessingOrder" (
"id" TEXT NOT NULL,
"generationDate" TEXT NOT NULL,
"orderNo" TEXT NOT NULL,
"factory" TEXT NOT NULL DEFAULT '',
"productCode" TEXT NOT NULL DEFAULT '',
"quantity" INTEGER NOT NULL DEFAULT 0,
"processMode" TEXT NOT NULL DEFAULT '标准模式(BOM)',
"expectedCompleteDate" TEXT NOT NULL DEFAULT '',
"note" TEXT NOT NULL DEFAULT '',
"processingWarehouse" TEXT NOT NULL DEFAULT '',
"receivingWarehouse" TEXT NOT NULL DEFAULT '',
"sourceBarcode" TEXT NOT NULL DEFAULT '',
"sortOrder" INTEGER NOT NULL DEFAULT 0,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "ModularProcessingOrder_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "ModularProcessingOrder_orderNo_key" ON "ModularProcessingOrder"("orderNo");
CREATE INDEX "ModularProcessingOrder_generationDate_sortOrder_idx" ON "ModularProcessingOrder"("generationDate", "sortOrder");
CREATE INDEX "ModularProcessingOrder_productCode_idx" ON "ModularProcessingOrder"("productCode");

View File

@ -0,0 +1,5 @@
ALTER TABLE "SupplyChainClearanceConfig"
ADD COLUMN IF NOT EXISTS "scoreConfig" JSONB NOT NULL DEFAULT '{}';
ALTER TABLE "SupplyChainClearanceConfig"
ADD COLUMN IF NOT EXISTS "priorityTaskTypes" JSONB NOT NULL DEFAULT '[]';

View File

@ -0,0 +1,22 @@
CREATE TABLE "SupplyChainLimitPriceHistorySnapshot" (
"id" TEXT NOT NULL,
"barcode" TEXT NOT NULL,
"sku" TEXT NOT NULL DEFAULT '',
"spu" TEXT NOT NULL DEFAULT '',
"flows" JSONB NOT NULL DEFAULT '[]',
"fields" JSONB NOT NULL DEFAULT '[]',
"syncedThrough" TEXT NOT NULL,
"lastSyncAttemptAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "SupplyChainLimitPriceHistorySnapshot_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "SupplyChainLimitPriceHistorySnapshot_barcode_sku_spu_key"
ON "SupplyChainLimitPriceHistorySnapshot"("barcode", "sku", "spu");
CREATE INDEX "SupplyChainLimitPriceHistorySnapshot_barcode_idx"
ON "SupplyChainLimitPriceHistorySnapshot"("barcode");
CREATE INDEX "SupplyChainLimitPriceHistorySnapshot_sku_idx"
ON "SupplyChainLimitPriceHistorySnapshot"("sku");

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

View File

@ -0,0 +1,83 @@
:root{--bg:#f5f6f8;--card:#fff;--line:#e5e7eb;--tx:#1f2937;--tx2:#6b7280;--blue:#2563eb;--green:#16a34a;--red:#dc2626;--amber:#d97706;--purple:#7c3aed}
*{margin:0;padding:0;box-sizing:border-box}
body{font-family:-apple-system,"PingFang SC","Microsoft YaHei",sans-serif;background:var(--bg);color:var(--tx);font-size:14px}
.wrap{max-width:1500px;margin:0 auto;padding:16px 20px 40px}
.head{display:flex;align-items:center;gap:14px;flex-wrap:wrap;margin-bottom:14px}
.head h1{font-size:19px;font-weight:600}
.head .sub{color:var(--tx2);font-size:12px}
.sel{padding:6px 10px;border:1px solid var(--line);border-radius:8px;background:#fff;font-size:13px;color:var(--tx)}
.vtab{display:inline-block;padding:4px 14px;font-size:13px;border:1px solid var(--line);background:#fff;color:var(--tx2);cursor:pointer;user-select:none}
.vtab:first-child{border-radius:8px 0 0 8px}
.vtab:last-child{border-radius:0 8px 8px 0}
.vtab.on{background:var(--blue);border-color:var(--blue);color:#fff;font-weight:600}
.tabs{display:flex;gap:4px;background:#eceef1;padding:4px;border-radius:10px;width:fit-content;margin-bottom:14px}
.tab{padding:7px 16px;border-radius:8px;cursor:pointer;font-size:13px;color:var(--tx2);border:none;background:transparent}
.tab.on{background:#fff;color:var(--tx);font-weight:600;box-shadow:0 1px 3px rgba(0,0,0,.08)}
.poolbtn{padding:3px 10px;font-size:12px;border:1px solid var(--line);border-radius:6px;background:#fff;color:var(--tx);cursor:pointer}
.poolbtn:hover{border-color:var(--blue);color:var(--blue)}
.card{background:var(--card);border:1px solid var(--line);border-radius:12px;padding:16px;margin-bottom:14px}
.card h3{font-size:14px;font-weight:600;margin-bottom:10px}
.card .hint{font-size:12px;color:var(--tx2);margin-bottom:8px}
.kpis{display:grid;grid-template-columns:repeat(auto-fit,minmax(170px,1fr));gap:12px;margin-bottom:14px}
.kpi{background:var(--card);border:1px solid var(--line);border-radius:12px;padding:14px 16px}
.kpi .v{font-size:22px;font-weight:700;margin-top:2px}
.kpi .l{font-size:12px;color:var(--tx2)}
.kpi .d{font-size:11px;color:var(--tx2);margin-top:2px}
table{width:100%;border-collapse:collapse;font-size:13px}
th,td{padding:8px 10px;border-bottom:1px solid var(--line);text-align:left;white-space:nowrap}
th{color:var(--tx2);font-weight:500;font-size:12px;background:#fafbfc;position:sticky;top:0}
td.num,th.num{text-align:right;font-variant-numeric:tabular-nums}
.tbl-scroll{overflow:auto;max-height:560px}
.bar{height:8px;background:#eef1f5;border-radius:4px;overflow:hidden;min-width:120px}
.bar i{display:block;height:100%;border-radius:4px}
.b-blue{background:var(--blue)}.b-green{background:var(--green)}.b-amber{background:var(--amber)}.b-purple{background:var(--purple)}.b-red{background:var(--red)}.b-gray{background:#d5dbe3}
.tag{display:inline-block;padding:2px 8px;border-radius:6px;font-size:12px;background:#eff6ff;color:var(--blue);margin:2px}
.tag.g{background:#f0fdf4;color:var(--green)}.tag.a{background:#fffbeb;color:var(--amber)}.tag.p{background:#f5f3ff;color:var(--purple)}
.tag.tcat{background:#eff6ff;color:#1d4ed8}.tag.tscene{background:#f0fdf4;color:#15803d}.tag.tattr{background:#fffbeb;color:#b45309}
.tag.tmat{background:#f5f3ff;color:#7c3aed}.tag.tstyle{background:#fef2f2;color:#b91c1c}.tag.tmkt{background:#ecfeff;color:#0e7490}
.heat{font-variant-numeric:tabular-nums;text-align:center;padding:7px 6px}
.empty{text-align:center;color:var(--tx2);padding:60px 0}
.pic{width:40px;height:40px;object-fit:cover;border-radius:6px;background:#f3f4f6}
.rowlink{color:var(--blue);text-decoration:none}
.legend{display:flex;gap:12px;flex-wrap:wrap;font-size:12px;color:var(--tx2);margin-bottom:8px}
.legend span i{display:inline-block;width:10px;height:10px;border-radius:3px;margin-right:4px;vertical-align:-1px}
.lbl{font-size:12px;color:var(--tx2)}
.modal-mask{position:fixed;inset:0;background:rgba(15,23,42,.45);display:none;align-items:flex-start;justify-content:center;z-index:99;padding:28px 14px}
.modal-mask.on{display:flex}
.modal-box{background:var(--card);border-radius:14px;max-width:96vw;max-height:88vh;overflow:auto;padding:14px 18px 18px;box-shadow:0 12px 40px rgba(0,0,0,.25)}
.modal-head{display:flex;justify-content:space-between;align-items:center;gap:16px;position:sticky;top:-14px;background:var(--card);padding:8px 0 6px;z-index:2;border-bottom:1px solid var(--line);margin-bottom:8px}
.modal-x{cursor:pointer;font-size:15px;color:var(--tx2);padding:2px 9px;border-radius:6px;user-select:none}
.modal-x:hover{background:#eef1f5;color:var(--tx)}
.drill-cell{cursor:pointer}
.drill-cell:hover .pic{outline:2px solid var(--blue)}
.drill-cell:hover .tag.p{outline:1.5px solid var(--blue)}
.pic{cursor:zoom-in}
.up{color:#dc2626}.down{color:#16a34a}
.imgzoom-mask{position:fixed;inset:0;background:rgba(15,23,42,.6);display:none;align-items:center;justify-content:center;z-index:120;cursor:zoom-out}
.imgzoom-mask.on{display:flex;animation:zf .12s ease-out}
@keyframes zf{from{opacity:0}to{opacity:1}}
.imgzoom-box{width:400px;height:400px;background:#fff;border-radius:12px;overflow:hidden;box-shadow:0 16px 50px rgba(0,0,0,.35);display:flex;align-items:center;justify-content:center}
.imgzoom-box img{max-width:400px;max-height:400px;width:auto;height:auto;object-fit:contain}
.two-col{display:grid;grid-template-columns:1fr 1fr;gap:14px}
@media(max-width:1100px){.two-col{grid-template-columns:1fr}}
/* ---- 识图判断(2026-10-22) ---- */
.imdrop{position:relative;border:2px dashed #cbd5e1;border-radius:12px;background:#fafbfc;padding:16px;display:flex;gap:16px;align-items:center;cursor:pointer;transition:.15s}
.imdrop:hover{border-color:var(--blue);background:#f8fbff}
.imdrop.on{border-color:#16a34a;background:#f0fdf4;border-style:solid}
.imdrop .ph{width:120px;height:120px;border-radius:10px;background:#fff;border:1px solid var(--line);display:flex;align-items:center;justify-content:center;color:var(--tx2);font-size:12px;text-align:center;flex:0 0 auto}
.imdrop .ph img{width:100%;height:100%;object-fit:contain;border-radius:9px}
.imgrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(124px,1fr));gap:10px;max-height:640px;overflow:auto;padding:2px}
.imcell{position:relative;border:2px solid #eef1f5;border-radius:10px;background:#fff;cursor:pointer;padding:6px;transition:.12s}
.imcell:hover{border-color:var(--blue);box-shadow:0 4px 14px rgba(0,0,0,.09);transform:translateY(-1px)}
.imcell.sel{border-color:#16a34a;background:#f0fdf4}
.imcell img{width:100%;height:100px;object-fit:contain;background:#f8fafc;border-radius:6px;display:block}
.imcell .s{position:absolute;top:6px;right:8px;background:rgba(15,23,42,.72);color:#fff;font-size:11px;padding:1px 6px;border-radius:6px;font-variant-numeric:tabular-nums}
.imcell .t{font-size:11px;color:var(--tx2);margin-top:5px;line-height:1.3;height:29px;overflow:hidden}
.imcell .p{font-size:12px;font-weight:700;margin-top:2px;font-variant-numeric:tabular-nums}
.imcell .c{font-size:10px;color:var(--tx2);margin-top:1px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.imchip{display:inline-flex;align-items:center;gap:8px;border:1px solid var(--line);border-radius:10px;padding:5px 10px 5px 5px;background:#fff;cursor:pointer;font-size:12px;margin:0 8px 8px 0}
.imchip:hover{border-color:var(--blue)}
.imchip.on{border-color:#16a34a;background:#f0fdf4;font-weight:600}
.imchip img{width:38px;height:38px;object-fit:cover;border-radius:6px;background:#f3f4f6}
.imcell .bg{position:absolute;top:6px;left:6px;font-size:11px;padding:1px 6px;border-radius:6px;color:#fff;z-index:2}
.bg-same{background:#16a34a}.bg-simi{background:#d97706}.bg-diff{background:#64748b}.bg-na{background:#94a3b8}

View File

@ -0,0 +1,40 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>市场选款看板</title>
<link rel="stylesheet" href="./css/board.css">
</head>
<body>
<div class="wrap">
<div class="head">
<h1>市场选款看板</h1>
<span class="sub" id="meta">生意参谋市场洞察 · 支付件数口径</span>
<span style="flex:1"></span>
<select class="sel" id="selDate"></select>
<span class="lbl">二级</span><select class="sel" id="selCat" title="二级类目"></select>
<span class="lbl">三级</span><select class="sel" id="selCat3" title="三级类目(随二级类目联动)"><option value="">全部</option></select>
<button class="sel" id="btnRefresh" style="cursor:pointer">刷新</button>
</div>
<div class="tabs">
<button class="tab on" data-t="capacity">容量分析</button>
<button class="tab" data-t="soar">飙升品榜</button>
<button class="tab" data-t="brand">品牌集中度</button>
<button class="tab" data-t="matrix">款式×价格带</button>
<button class="tab" data-t="catdemand">类目需求</button>
<button class="tab" data-t="kw">TOP关键词</button>
<button class="tab" data-t="items">TOP商品明细</button>
<button class="tab" data-t="pool">意向选品池</button>
<button class="tab" data-t="calc">容量测算</button>
<button class="tab" data-t="imatch">识图判断</button>
</div>
<div id="kpis" class="kpis"></div>
<div id="panels"></div>
</div>
<script type="module" src="./js/main.js"></script>
</body>
</html>

View File

@ -0,0 +1,124 @@
import { kpis, panels, meta, selDate, selCat, selCat3 } from './shared/dom.js';
import { esc, fmtN, fmtP } from './shared/format.js';
import { state } from './shared/state.js';
import { buildDemoData } from './shared/demo.js';
import { renderCapacity } from './tabs/capacity.js';
import { renderSoar } from './tabs/soaring.js';
import { renderBrand, renderKw, renderItems } from './tabs/product-lists.js';
import { renderMatrix } from './tabs/matrix.js';
import { renderCatDemand } from './tabs/category-demand.js';
import { renderPool } from './pool/list.js';
import { calcLoad, renderCalc } from './tabs/calculator.js';
import { renderImatch } from './image-match/view.js';
import { DEF_CAT, DEF_CAT3, DASH_URL } from './shared/config.js';
export function showEmpty(msg){
kpis.innerHTML='';
panels.innerHTML='<div class="card" style="max-width:680px;margin:40px auto;text-align:center;padding:26px">'
+'<h3 style="font-size:15px;margin-bottom:10px">'+esc(msg||'暂无数据')+'</h3>'
+'<div style="color:var(--tx2);font-size:13px;line-height:1.9;text-align:left;margin-bottom:14px">'
+'真实数据需在 Mac 抓取端执行(本交付包 <code>pipeline/</code> 目录,macOS + Python 3.11):<br>'
+'① <code>python3 login_once.py --force</code> —— 生意参谋授权(扫码,仅一次)<br>'
+'② <code>python3 fetch_market.py probe --page rank</code> —— 校准接口路径<br>'
+'③ <code>items / catdemand / soaring / keywords</code> 抓取后 <code>sync</code> 同步</div>'
+'<button class="sel" id="btnDemo" style="cursor:pointer;background:var(--blue);color:#fff;border:none;padding:8px 18px;border-radius:8px">加载演示数据(预览各页签效果)</button></div>';
const b=document.getElementById('btnDemo');
if(b)b.onclick=loadDemo;
}
export function showLocalHint(){
kpis.innerHTML='';
panels.innerHTML='<div class="card" style="max-width:640px;margin:40px auto;text-align:center;padding:28px">'
+'<h3 style="font-size:16px;margin-bottom:10px">这是静态页面预览,不承载数据</h3>'
+'<div style="color:var(--tx2);font-size:13px;line-height:1.9;text-align:left;margin-bottom:16px">'
+'市场选款看板需从数据中台访问(含钉钉登录态与后端 API):<br>'
+'① 打开 <a class="rowlink" href="'+DASH_URL+'" target="_blank">数据中台</a> → 「市场选款看板」板块;<br>'
+'② 看板有数据的前提:Mac 抓取端完成生意参谋授权 → 抓取 → sync 同步。</div>'
+'<button class="sel" id="btnDemo" style="cursor:pointer;background:var(--blue);color:#fff;border:none;padding:8px 18px;border-radius:8px">加载演示数据(预览五个页签效果)</button>'
+'</div>';
const b=document.getElementById('btnDemo');
if(b)b.onclick=loadDemo;
}
export function loadDemo(){
state.DATA=buildDemoData();
fillSelectors();renderKpis();render();
meta.textContent='【演示数据】生意参谋市场洞察 · 支付件数口径 · 非真实数据';
}
export function fillSelectors(){
if(selDate.options.length!==state.DATA.dates.length){
selDate.innerHTML=state.DATA.dates.map(d=>'<option value="'+d+'">'+d+'</option>').join('');
selDate.value=state.DATA.summary.snapshotDate;
}
const catOpts=state.DATA.cats.map(c=>'<option value="'+esc(c.catId)+'">'+esc(c.catName||c.catId)+'</option>').join('');
if(selCat.getAttribute('data-opts')!==catOpts){
selCat.innerHTML=catOpts;selCat.setAttribute('data-opts',catOpts);
}
selCat.value=state.DATA.summary.catId;
if(state.DATA.cats3){
const opts3='<option value="">全部三级类目</option>'
+state.DATA.cats3.map(c=>'<option value="'+esc(c.catId)+'">'+esc(c.catName||c.catId)+'</option>').join('');
if(selCat3.getAttribute('data-opts')!==opts3){
selCat3.innerHTML=opts3;selCat3.setAttribute('data-opts',opts3);
}
selCat3.value=state.DATA.summary.cat3||'';
}
const srcName=state.DATA.dbSource==='yuce_market'?'市场库直连(月榜商品·销量成交额口径)':'生意参谋市场洞察(支付件数口径)';
meta.textContent=srcName+' · '+state.DATA.summary.catName+' · 快照 '+state.DATA.summary.snapshotDate;
}
export function renderKpis(){
const s=state.DATA.summary;
if(state.DATA.dbSource==='yuce_market'){
const amt=s.demandAmount||0;
kpis.innerHTML=[
['类目市场容量',amt?('¥'+fmtN(amt)):'-','月榜商品销售额合计(市场库)'],
['需求赛道总容量',fmtN(s.demandTotal),'月榜商品销量合计(件)'],
['在榜商品数',s.itemCount||0,'个商品(当月榜)'],
['品牌数',s.brandCount||0,'含白牌/未标注'],
['成交均价','¥'+Math.round(s.avgPrice||0),'按销量加权'],
['CR5 集中度',fmtP(s.cr5),'TOP5 品牌销量占有率'],
['CR10 集中度',fmtP(s.cr10),'TOP10 品牌销量占有率'],
['飙升品数',s.soaringCount||0,'连续2月+排名上升(共'+(s.trendCovered||0)+'个有轨迹)'],
[s.styleMode==='cluster'?'外观同款':(s.imgTaggedCount>0?'主图识别覆盖':'款式识别覆盖'),
s.styleMode==='cluster'?(s.clusterCount||'-')+'款/'+(s.itemCount||0)+'品'
:(s.imgTaggedCount>0?s.imgTaggedCount:(s.taggedCount||0))+'/'+(s.itemCount||0),
s.styleMode==='cluster'?'外观相似度≥80%判同款(视觉距离聚类)'
:s.imgTaggedCount>0?'多模态主图打标(视觉口径)':'标题规则分类(非主图识别)'],
].map(k=>'<div class="kpi"><div class="l">'+k[0]+'</div><div class="v">'+k[1]+'</div><div class="d">'+k[2]+'</div></div>').join('');
return;
}
kpis.innerHTML=[
['需求赛道总容量',fmtN(s.demandTotal),'TOP300 支付件数合计'],
['在榜商品数',s.itemCount,'个商品'],
['品牌数',s.brandCount,'含白牌/未标注'],
['成交均价','¥'+(s.avgPrice||0).toFixed(0),'按支付件数加权'],
['CR5 集中度',fmtP(s.cr5),'TOP5 品牌支付件数占有率'],
['CR10 集中度',fmtP(s.cr10),'TOP10 品牌支付件数占有率'],
['已识别主图',s.taggedCount+'/'+s.itemCount,'主图识别分类覆盖'],
].map(k=>'<div class="kpi"><div class="l">'+k[0]+'</div><div class="v">'+k[1]+'</div><div class="d">'+k[2]+'</div></div>').join('');
}
export function render(){
if(!state.DATA||state.DATA.empty){
if(state.DATA&&state.DATA.empty)showEmpty(state.DATA.message);
else panels.innerHTML='<div class="empty">加载中…</div>';
return;
}
if(state.TAB==='capacity')renderCapacity();
else if(state.TAB==='soar')renderSoar();
else if(state.TAB==='brand')renderBrand();
else if(state.TAB==='matrix')renderMatrix();
else if(state.TAB==='catdemand')renderCatDemand();
else if(state.TAB==='kw')renderKw();
else if(state.TAB==='pool')renderPool();
else if(state.TAB==='calc'){calcLoad();renderCalc();}
else if(state.TAB==='imatch')renderImatch();
else renderItems();
}

View File

@ -0,0 +1,29 @@
import { showLocalHint, showEmpty, fillSelectors, renderKpis, render } from './board.js';
import { DEF_CAT, DEF_CAT3 } from './shared/config.js';
import { selCat, selCat3, selDate, panels } from './shared/dom.js';
import { getBoardData } from './shared/api.js';
import { state } from './shared/state.js';
import { esc } from './shared/format.js';
import { ensureMe, loadPool } from './pool/data.js';
export async function load(){
// HTTP(S) 宿主通过同源 API 和平台会话加载;直接打开本地文件时给出访问指引。
if(!['http:','https:'].includes(location.protocol)){
showLocalHint();return;
}
const p=new URLSearchParams();
const cat=selCat.value||DEF_CAT; // 首次打开(下拉尚未填充)→ 默认二级类目
p.set('cat',cat);
const cat3=selCat3.value||(cat===DEF_CAT?DEF_CAT3:''); // 默认三级类目(仅默认二级时生效)
if(cat3)p.set('cat3',cat3);
if(selDate.value)p.set('date',selDate.value);
panels.innerHTML='<div class="empty">加载中…</div>';
try{
const r=await getBoardData(p);
state.DATA=await r.json();
if(state.DATA.error){panels.innerHTML='<div class="empty">'+esc(state.DATA.error)+'</div>';return}
if(state.DATA.empty){showEmpty(state.DATA.message);return}
fillSelectors();renderKpis();render();
ensureMe();loadPool().then(()=>{if(state.TAB==='items')render();}); // 预取选品池 → 明细行显示「已登记」状态
}catch(e){panels.innerHTML='<div class="empty">加载失败:'+esc(e.message)+'<br><span style="font-size:12px">请确认从数据中台的「市场选款看板」板块访问(需同源 + 登录态)</span></div>'}
}

View File

@ -0,0 +1,155 @@
/* ==================== 识图判断(2026-10-22) ====================
流程:粘贴/拖拽/上传一张主图
→ 浏览器算「外观指纹」(与 market-scout/sig_images.py 逐字一致)
→ 和当前三级类目榜单 500 个 ID 的指纹逐一比对排序 → 相似款卡片墙
→ 点任意卡片 = 锁定它所属的款式簇 → 按簇展开算「同款」汇总
口径说明:指纹只负责「粗排」(实测同簇 Top20 召回 40%~55%),
同款结论仍以库内已校验的外观同款聚类(cluster_id)为准,
所以默认给一个推荐款,点一下卡片即可换成正确的那一款。 */
export const FP_VER='v1', FP_SIDE=24, FP_GRID=4, FP_MAX=400, FP_TRIM_TOL=54;
export function fpB64(u8){
let s='';
const CH=0x8000;
for(let i=0;i<u8.length;i+=CH) s+=String.fromCharCode.apply(null,u8.subarray(i,i+CH));
return btoa(s);
}
export function fpDecode(sig){
let bin;
try{bin=atob(sig);}catch(e){return null;}
const NP=FP_SIDE*FP_SIDE/2; // 288
if(bin.length<NP+48)return null;
const nb=new Uint8Array(NP);
for(let i=0;i<NP;i++)nb[i]=bin.charCodeAt(i);
const g=new Uint8Array(FP_SIDE*FP_SIDE), c=new Uint8Array(48);
const half=FP_SIDE/2;
for(let y=0;y<FP_SIDE;y++)for(let x=0;x<half;x++){
const b=nb[y*half+x];
g[y*FP_SIDE+x*2]=b>>4; g[y*FP_SIDE+x*2+1]=b&15;
}
for(let i=0;i<48;i++)c[i]=bin.charCodeAt(NP+i);
return {g:g,c:c};
}
export function fpSim(A,B){
let gd=0;for(let i=0;i<A.g.length;i++)gd+=Math.abs(A.g[i]-B.g[i]);
gd/=A.g.length*15;
let cd=0;for(let i=0;i<A.c.length;i++)cd+=Math.abs(A.c[i]-B.c[i]);
cd/=A.c.length*255;
return 0.72*(1-gd)+0.28*(1-cd);
}
/* 接近度校准(2026-10-22 实测 4 类目 500 品):
同一张图跨实现 ≈0.973;同款簇内对 p95≈0.904 / 中位≈0.852;随机对中位≈0.831 / p5≈0.769。
原始相似度全部挤在 0.83~0.91,直接显示百分数会让人误以为 90% 就是同款。
线性映射到 0~100「外观接近度」:≥85≈同一张图,65~85 很可能同款,45~65 相似,<45 仅供参考。 */
export const FP_CAL={lo:0.78,hi:0.975};
export function imPct(s){
let p=(s-FP_CAL.lo)/(FP_CAL.hi-FP_CAL.lo)*100;
if(!(p>0))p=0;
if(p>100)p=100;
return Math.round(p);
}
export function imGrade(p){
return p>=85?'几乎同一张图':(p>=65?'很可能同款':(p>=45?'外观相似':'仅供参考'));
}
export function fpCV(img){
const w=img.naturalWidth||img.width||0, h=img.naturalHeight||img.height||0;
if(!w||!h)return null;
const s=Math.min(1,FP_MAX/Math.max(w,h));
const cv=document.createElement('canvas');
cv.width=Math.max(1,Math.round(w*s)); cv.height=Math.max(1,Math.round(h*s));
const x=cv.getContext('2d',{willReadFrequently:true});
x.imageSmoothingEnabled=true; x.imageSmoothingQuality='high';
x.drawImage(img,0,0,cv.width,cv.height);
return cv;
}
// 用边框像素中位色估背景 → 取内容外接框(白底棚拍归一化,去掉留白对相似度的稀释)
export function fpTrimBox(cv){
const w=cv.width,h=cv.height,full={x:0,y:0,w:w,h:h};
if(w<8||h<8)return full;
const d=cv.getContext('2d',{willReadFrequently:true}).getImageData(0,0,w,h).data;
const R=[],G=[],B=[];
const add=function(i){R.push(d[i]);G.push(d[i+1]);B.push(d[i+2]);};
for(let x=0;x<w;x++){add(x*4);add(((h-1)*w+x)*4);}
for(let y=0;y<h;y++){add((y*w)*4);add((y*w+w-1)*4);}
const med=function(a){a.sort(function(p,q){return p-q;});return a[a.length>>1]||0;};
const br=med(R),bg=med(G),bb=med(B);
let x0=w,y0=h,x1=-1,y1=-1,n=0;
for(let y=0;y<h;y++){
for(let x=0;x<w;x++){
const i=(y*w+x)*4;
if(Math.abs(d[i]-br)+Math.abs(d[i+1]-bg)+Math.abs(d[i+2]-bb)>FP_TRIM_TOL){
n++; if(x<x0)x0=x; if(x>x1)x1=x; if(y<y0)y0=y; if(y>y1)y1=y;
}
}
}
const mr=n/(w*h);
if(mr<0.02||mr>0.97||x1<x0+5||y1<y0+5)return full;
return {x:x0,y:y0,w:x1-x0+1,h:y1-y0+1};
}
// 外接框居中画成 N×N 正方形(留 6% 边距,4 倍超采样再缩,避免浏览器缩小锯齿)
export function fpSquare(src,box,N){
const big=document.createElement('canvas');
big.width=N*4; big.height=N*4;
const x=big.getContext('2d',{willReadFrequently:true});
x.fillStyle='#fff'; x.fillRect(0,0,big.width,big.height);
x.imageSmoothingEnabled=true; x.imageSmoothingQuality='high';
const s=Math.min(big.width/box.w,big.height/box.h)*0.885;
const dw=box.w*s, dh=box.h*s;
x.drawImage(src,box.x,box.y,box.w,box.h,(big.width-dw)/2,(big.height-dh)/2,dw,dh);
const out=document.createElement('canvas');
out.width=N; out.height=N;
const y=out.getContext('2d',{willReadFrequently:true});
y.imageSmoothingEnabled=true; y.imageSmoothingQuality='high';
y.drawImage(big,0,0,N,N);
return out;
}
export function fpPack(core){
const N=FP_SIDE;
const gcv=document.createElement('canvas'); gcv.width=N; gcv.height=N;
const gx=gcv.getContext('2d',{willReadFrequently:true});
gx.imageSmoothingEnabled=true; gx.imageSmoothingQuality='high';
gx.drawImage(core,0,0,N,N);
const d=gx.getImageData(0,0,N,N).data;
const g=new Float32Array(N*N);
let m=0;
for(let i=0;i<N*N;i++){const v=0.299*d[i*4]+0.587*d[i*4+1]+0.114*d[i*4+2]; g[i]=v; m+=v;}
m/=N*N;
let vv=0;
for(let i=0;i<N*N;i++){const t=g[i]-m; vv+=t*t;}
const sd=Math.sqrt(vv/(N*N))||1;
const half=N/2;
const nb=new Uint8Array(N*half);
for(let y=0;y<N;y++){
for(let x=0;x<half;x++){
const z0=Math.round(((g[y*N+x*2]-m)/sd+2)/4*15);
const z1=Math.round(((g[y*N+x*2+1]-m)/sd+2)/4*15);
const a=z0<0?0:(z0>15?15:z0), b=z1<0?0:(z1>15?15:z1);
nb[y*half+x]=(a<<4)|b;
}
}
const ccv=document.createElement('canvas'); ccv.width=FP_GRID; ccv.height=FP_GRID;
const cx=ccv.getContext('2d',{willReadFrequently:true});
cx.imageSmoothingEnabled=true; cx.imageSmoothingQuality='high';
cx.drawImage(core,0,0,FP_GRID,FP_GRID);
const cd=cx.getImageData(0,0,FP_GRID,FP_GRID).data;
const out=new Uint8Array(nb.length+48);
out.set(nb,0);
for(let i=0;i<FP_GRID*FP_GRID;i++){
out[nb.length+i*3]=cd[i*4];
out[nb.length+i*3+1]=cd[i*4+1];
out[nb.length+i*3+2]=cd[i*4+2];
}
return fpB64(out);
}

View File

@ -0,0 +1,71 @@
import { imLoadFile, imReset, imKey } from './matching.js';
import { state } from '../shared/state.js';
import { renderImageMatch as renderImatch } from '../shared/renderers.js';
export function imRenderSrc(file){
imLoadFile(file);
}
export function imOnPick(ev){
const f=ev.target&&ev.target.files&&ev.target.files[0];
if(f)imLoadFile(f);
ev.target.value='';
}
export function imDrop(ev){
ev.preventDefault();
const box=document.getElementById('imDrop');
if(box)box.classList.remove('on');
const dt=ev.dataTransfer;
if(!dt)return;
let f=(dt.files&&dt.files[0])||null;
if(!f&&dt.items){
for(let i=0;i<dt.items.length;i++){
const it=dt.items[i];
if(it.kind==='file'&&String(it.type).indexOf('image/')===0){f=it.getAsFile();break;}
}
}
if(f)imLoadFile(f);
}
export function initImagePaste() {
// 全局粘贴:只在「识图判断」Tab 生效
window.addEventListener('paste',function(e){
if(typeof state.TAB==='undefined'||state.TAB!=='imatch')return;
const dt=e.clipboardData;
if(!dt)return;
let f=null;
if(dt.items){
for(let i=0;i<dt.items.length;i++){
const it=dt.items[i];
if(it.kind==='file'&&String(it.type).indexOf('image/')===0){f=it.getAsFile();break;}
}
}
if(!f&&dt.files&&dt.files.length)f=dt.files[0];
if(!f)return;
e.preventDefault();
imLoadFile(f);
});
}
export function imClear(){
if(state.IM.prev&&state.IM.prev.indexOf('blob:')===0){try{URL.revokeObjectURL(state.IM.prev);}catch(e){}}
const keep=state.IM.topN, key=state.IM.curKey, ready=state.IM.ready, sigs=state.IM.sigs, ver=state.IM.ver;
imReset();
state.IM.curKey=key; state.IM.topN=keep; state.IM.ready=ready; state.IM.sigs=sigs; state.IM.ver=ver;
renderImatch();
}
export function imReload(){
try{localStorage.removeItem('smIMSigV2_'+imKey());}catch(e){}
state.IM.ready=false; state.IM.busy=false; state.IM.err='';
renderImatch();
}
export function imMore(){
state.IM.topN=(state.IM.topN||48)+48;
renderImatch();
}

View File

@ -0,0 +1,126 @@
import { state } from '../shared/state.js';
import { renderImageMatch as renderImatch } from '../shared/renderers.js';
import { judgeStyles } from '../shared/api.js';
import { imLock } from './matching.js';
import { esc } from '../shared/format.js';
/* ---- AI 精判同款(2026-10-22):7 维度结构比对 ----
只比结构、忽略颜色/纹理/背景/拍摄差异;≥6 项一致=同款,4~5=相似款/改款,≤3=不同款。
候选按「每个款式簇取最像的 1 个」去重后取前 8,每对投 2 票取保守结论。 */
export async function imJudge(opt){
opt=opt||{};
if(!state.IM.sig||!state.IM.rows.length){state.IM.err='先粘贴一张主图';renderImatch();return;}
if(!state.IM.imgData){state.IM.err='这张图没能转成可上传格式,请重新粘贴一次';renderImatch();return;}
const recheck=!!opt.recheck;
const prev=(state.IM.judge&&state.IM.judge.res)||{};
let cand=[];
if(recheck){
// 复核:只对上一轮判为同款/相似款的进行(<=3 个,投 2 票)
for(let i=0;i<state.IM.rows.length&&cand.length<3;i++){
const r=state.IM.rows[i];
const v=prev[String(r.it.item_id)];
if(v&&v.verdict!=='不同款')cand.push(r);
}
if(!cand.length){state.IM.err='上一轮全是「不同款」,没有可复核的项';renderImatch();return;}
}else{
// 首轮:按款式簇去重(每簇取最像的 1 个),取前 6 个候选
const seen={};
for(let i=0;i<state.IM.rows.length&&cand.length<6;i++){
const r=state.IM.rows[i];
const c=+r.it.cluster_id||0;
const k=c?('c'+c):('i'+r.it.item_id);
if(seen[k])continue;
seen[k]=1;
cand.push(r);
}
}
const votes=recheck?2:1;
const chunks=[];
// 每批 3 个(短提示词后单批约 20s,远低于 nginx 120s 读超时)
for(let i=0;i<cand.length;i+=3)chunks.push(cand.slice(i,i+3));
const res=recheck?{}:prev; // 复核整批重来,首轮增量累积
state.IM.judge={busy:true,done:false,err:'',res:res,votes:votes,recheck:recheck,
cand:cand.map(function(r){return String(r.it.item_id);}),step:0,total:chunks.length};
renderImatch();
for(let ci=0;ci<chunks.length;ci++){
state.IM.judge.step=ci;
renderImatch();
let o=null;
try{
const r=await judgeStyles(state.IM.imgData,votes,chunks[ci].map(function(r){return {itemId:String(r.it.item_id),pic:r.it.pic_url};}));
const d=await r.json().catch(function(){return {};});
if(!r.ok)throw new Error((d&&d.error)||('HTTP '+r.status));
((d&&d.results)||[]).forEach(function(v){res[String(v.itemId)]=v;});
state.IM.judge.res=res;
}catch(e){
state.IM.judge.busy=false; state.IM.judge.done=true;
state.IM.judge.err='精判失败:'+((e&&e.message)||e);
renderImatch();
return;
}
}
state.IM.judge.busy=false; state.IM.judge.done=true; state.IM.judge.err=''; state.IM.judge.res=res;
renderImatch();
}
// 精判为「同款」的 ID(含其所属簇的全部同款展开)
export function imJudgeSet(){
const j=(state.IM.judge&&state.IM.judge.res)||{};
const items=(state.DATA&&state.DATA.items)||[];
const cs={};
items.forEach(function(it){
const v=j[String(it.item_id)];
if(v&&v.verdict==='同款'&&(+it.cluster_id||0))cs[+it.cluster_id]=1;
});
const out=[],seen={};
items.forEach(function(it){
const id=String(it.item_id);
if(seen[id])return;
const v=j[id];
const inC=(+it.cluster_id||0)&&cs[+it.cluster_id];
if((v&&v.verdict==='同款')||inC){seen[id]=1;out.push(it);}
});
return out;
}
export function imJudgeStats(){
const set=imJudgeSet();
const amt=set.reduce(function(s,it){return s+(+it.pay_amt||0);},0);
const vol=set.reduce(function(s,it){return s+(+it.pay_item_cnt||0);},0);
return {n:set.length,amt:amt,vol:vol,avg:set.length?amt/set.length:0,items:set};
}
export function imLockFromJudge(){
const j=(state.IM.judge&&state.IM.judge.res)||{};
let best=0;
(state.IM.rows||[]).forEach(function(r){
const v=j[String(r.it.item_id)];
if(!best&&v&&v.verdict==='同款'&&(+r.it.cluster_id||0))best=+r.it.cluster_id;
});
if(!best){state.IM.err='精判结果里没有可锁定的同款簇(候选都判为相似款/不同款)';renderImatch();return;}
imLock(best);
}
// 卡片徽标:优先用自身精判结果,其次用它所在簇的精判结论
export function imBadge(it){
const j=(state.IM.judge&&state.IM.judge.res)||{};
if(!Object.keys(j).length)return '';
const own=j[String(it.item_id)];
let v=own;
if(!v){
const c=+it.cluster_id||0;
if(!c)return '';
for(let i=0;i<state.IM.rows.length;i++){
const r=state.IM.rows[i];
if((+r.it.cluster_id||0)===c&&j[String(r.it.item_id)]){v=j[String(r.it.item_id)];break;}
}
}
if(!v)return '';
const cls=v.verdict==='同款'?'bg-same':(String(v.verdict).indexOf('相似')>=0?'bg-simi':(v.verdict==='不同款'?'bg-diff':'bg-na'));
return '<span class="bg '+cls+'" title="AI 精判:7 项中 '+v.same+' 项一致">'+esc(v.verdict)+'</span>';
}
export function imPickItems(){
if(!state.IM.pick)return [];
return ((state.DATA&&state.DATA.items)||[]).filter(function(it){return (+it.cluster_id||0)===state.IM.pick;});
}

View File

@ -0,0 +1,117 @@
import { selCat, selCat3 } from '../shared/dom.js';
import { DEF_CAT } from '../shared/config.js';
import { state } from '../shared/state.js';
import { getImageSignatures } from '../shared/api.js';
import { renderImageMatch as renderImatch } from '../shared/renderers.js';
import { fpCV, fpTrimBox, fpSquare, fpPack, fpDecode, fpSim } from './fingerprint.js';
export function imKey(){
const c=(typeof selCat!=='undefined'&&selCat&&selCat.value)||DEF_CAT;
const c3=(typeof selCat3!=='undefined'&&selCat3&&selCat3.value)||'';
return c+'|'+c3;
}
export function imReset(){
state.IM={sig:null,sigD:null,sigs:{},ver:'',rows:[],cands:[],pick:0,
err:'',busy:false,ready:false,prev:'',topN:state.IM.topN||48,hit:null,
imgData:'',judge:null};
}
export async function imLoadSigs(force){
const k=imKey();
if(!force){
try{
const raw=localStorage.getItem('smIMSigV2_'+k);
if(raw){
const o=JSON.parse(raw);
if(o&&o.sigs&&o.t>Date.now()-3*864e5){state.IM.sigs=o.sigs;state.IM.ver=o.ver||'';return;}
}
}catch(e){}
}
const cat=(selCat.value||DEF_CAT), cat3=(selCat3.value||'');
const r=await getImageSignatures(cat,cat3);
const d=await r.json().catch(function(){return {};});
if(!r.ok)throw new Error((d&&d.error)||('HTTP '+r.status));
state.IM.sigs=(d&&d.sig)||{};
state.IM.ver=(d&&d.ver)||'';
try{localStorage.setItem('smIMSigV2_'+k,JSON.stringify({t:Date.now(),ver:state.IM.ver,sigs:state.IM.sigs}));}catch(e){}
}
export function imLoadFile(file){
if(!file||String(file.type||'').indexOf('image/')!==0){state.IM.err='只支持图片文件(PNG/JPG/WebP)';renderImatch();return;}
if(file.size>12*1024*1024){state.IM.err='图片太大(>12MB),请压缩后再试';renderImatch();return;}
const url=URL.createObjectURL(file);
const img=new Image();
img.onload=function(){
try{
const cv=fpCV(img);
const box=fpTrimBox(cv);
const core=fpSquare(cv,box,96);
const sig=fpPack(core);
// 另存一份压缩后的 dataURL:AI 精判要把它上传给 VL 网关(原图动辄几 MB)
let up='';
try{
const MW=1000;
const iw=img.naturalWidth||img.width||1, ih=img.naturalHeight||img.height||1;
const s2=Math.min(1,MW/Math.max(iw,ih));
const cv2=document.createElement('canvas');
cv2.width=Math.max(1,Math.round(iw*s2)); cv2.height=Math.max(1,Math.round(ih*s2));
const x2=cv2.getContext('2d');
x2.imageSmoothingEnabled=true; x2.imageSmoothingQuality='high';
x2.drawImage(img,0,0,cv2.width,cv2.height);
up=cv2.toDataURL('image/jpeg',0.82);
}catch(e){up='';}
state.IM.prev=url; state.IM.sig=sig; state.IM.sigD=fpDecode(sig); state.IM.imgData=up; state.IM.err='';
imMatch();
renderImatch();
}catch(err){
state.IM.err='图片解析失败:'+(err&&err.message||err);renderImatch();
}
};
img.onerror=function(){state.IM.err='图片加载失败,请换一张图';renderImatch();};
img.src=url;
}
export function imMatch(){
const items=(state.DATA&&state.DATA.items)||[];
const rows=[];
let miss=0, bad=0;
items.forEach(function(it){
const s=state.IM.sigs[String(it.item_id)];
if(!s){miss++;return;}
const d=fpDecode(s);
if(!d){bad++;return;}
rows.push({it:it,s:fpSim(state.IM.sigD,d)});
});
rows.sort(function(a,b){return b.s-a.s;});
state.IM.rows=rows;
state.IM.miss=miss;
state.IM.badSig=bad;
// 款式簇聚合:取前 80 个相似 ID 覆盖到的簇,按「最像的那个成员」排序
const byC={};
rows.slice(0,80).forEach(function(r){
const c=+r.it.cluster_id||0;
if(!c)return;
(byC[c]=byC[c]||[]).push(r);
});
const cands=Object.keys(byC).map(function(c){
const arr=byC[c];
const mx=arr[0].s, n=arr.length;
const amt=arr.reduce(function(s,r){return s+(+r.it.pay_amt||0);},0);
return {cid:+c,mx:mx,n:n,amt:amt,name:arr[0].it.cluster_name||'',
score:mx+Math.min(0.05,0.008*(n-1))};
}).sort(function(a,b){return b.score-a.score;});
state.IM.cands=cands.slice(0,10);
state.IM.pick=state.IM.cands.length?state.IM.cands[0].cid:0;
state.IM.hit=(rows.length&&rows[0].s>=0.96)?rows[0]:null;
if(state.IM.hit&&+state.IM.hit.it.cluster_id)state.IM.pick=+state.IM.hit.it.cluster_id;
}
export function imLock(cid){
if(!cid){state.IM.err='这个 ID 还没归入款式簇,无法按同款统计';renderImatch();return;}
state.IM.pick=+cid; renderImatch();
}

View File

@ -0,0 +1,232 @@
import { selCat3, selCat, panels } from '../shared/dom.js';
import { DEF_CAT } from '../shared/config.js';
import { imKey, imReset, imLoadSigs, imMatch } from './matching.js';
import { state } from '../shared/state.js';
import { esc, fmtN, fmtP } from '../shared/format.js';
import { imPickItems, imJudgeStats, imBadge } from './judgement.js';
import { imPct, imGrade } from './fingerprint.js';
export function renderImatch(){
const cat3=(typeof selCat3!=='undefined'&&selCat3&&selCat3.value)||'';
const cat=(typeof selCat!=='undefined'&&selCat&&selCat.value)||DEF_CAT;
const key=imKey();
if(state.IM.curKey!==key){const keep=state.IM.topN;imReset();state.IM.curKey=key;state.IM.topN=keep;}
let h='<div class="card"><h3>识图判断 · 按外观找同款/相似款</h3>';
if(!cat3){
panels.innerHTML=h+'<div class="hint">请先在顶部选择<b>三级类目</b>——识图比对的范围是当前三级类目榜单的商品。</div></div>';
return;
}
if(!state.IM.ready&&!state.IM.busy){
state.IM.busy=true;
imLoadSigs(false).then(function(){
state.IM.ready=true;state.IM.busy=false;
if(state.IM.sig)imMatch();
renderImatch();
},function(e){
state.IM.busy=false;state.IM.err='指纹加载失败:'+(e&&e.message||e);renderImatch();
});
panels.innerHTML=h+'<div class="empty">正在加载「'+esc(cat)+' / '+esc(cat3)+'」的外观指纹…</div></div>';
return;
}
const nSig=Object.keys(state.IM.sigs||{}).length;
h+='<div class="hint">复制任意一张商品主图(<b>⌘/Ctrl + V</b> 粘贴)到下面这个框里,或直接拖拽图片进来 / 点框选文件。'
+'系统会按<b>外观指纹</b>在当前类目 <b>'+(nSig||500)+'</b> 个 ID 里找出最像的款式。'
+'<span style="color:var(--tx2)"> 口径:指纹只做粗排,点一下下面的卡片就能锁定「那一款」,汇总立刻按它的同款簇重算。</span>'
+(nSig?'':'<br><b style="color:#b45309">本类目暂无外观指纹</b>:需要在 Mac 端跑 <code>market-scout/sig_images.py</code> 再用 <code>push_signatures.py</code> 推送。')
+'</div>';
h+='<div class="imdrop'+(state.IM.prev?' on':'')+'" id="imDrop" onclick="document.getElementById(\'imFile\').click()"'
+' ondragover="event.preventDefault();this.classList.add(\'on\')" ondragleave="this.classList.remove(\'on\')" ondrop="imDrop(event)">'
+'<div class="ph">'+(state.IM.prev?'<img src="'+esc(state.IM.prev)+'">':'📋<br>粘贴主图<br><span style="font-size:11px">⌘/Ctrl+V</span>')+'</div>'
+'<div style="flex:1;min-width:220px">'
+(state.IM.prev
? '<div style="font-size:13px;font-weight:600;color:#15803d">✓ 已识图,下面是匹配结果</div>'
+'<div class="hint" style="margin:4px 0 8px">点框可换一张图;点结果卡片可切换锁定款式。</div>'
+'<button class="poolbtn" onclick="event.stopPropagation();imJudge()" style="border-color:#16a34a;color:#16a34a">AI 精判同款(7维结构比对)</button> '
+'<button class="poolbtn" onclick="event.stopPropagation();imClear()">清除重贴</button> '
+'<button class="poolbtn" onclick="event.stopPropagation();imReload()">重载指纹</button>'
: '<div style="font-size:13px;font-weight:600">点击选择图片,或直接把图片粘到这里</div>'
+'<div class="hint" style="margin:4px 0 0">支持截图/Ctrl+V 粘贴、拖拽、点选文件(PNG/JPG/WebP,≤12MB)</div>')
+'</div></div>';
h+='<input type="file" id="imFile" accept="image/*" style="display:none" onchange="imOnPick(event)">';
if(state.IM.err)h+='<div class="hint" style="color:#b91c1c;margin-top:8px">'+esc(state.IM.err)+'</div>';
if(state.IM.judge&&state.IM.judge.busy)h+='<div class="hint" style="margin-top:8px;color:var(--blue)">正在按 7 维度结构比对:第 '+((state.IM.judge.step||0)+1)+'/'+state.IM.judge.total+' 批,共 '+(state.IM.judge.cand||[]).length+' 个候选款('+(state.IM.judge.votes||1)+' 票/对,约 40~120 秒)…</div>';
if(state.IM.judge&&state.IM.judge.done&&state.IM.judge.err)h+='<div class="hint" style="color:#b91c1c;margin-top:8px">'+esc(state.IM.judge.err)+'</div>';
if(state.IM.sig&&!state.IM.rows.length){
const nS=Object.keys(state.IM.sigs||{}).length;
h+='<div class="hint" style="color:#b91c1c;margin-top:8px">没有可比对的结果:榜单 '+(((state.DATA&&state.DATA.items)||[]).length)
+' 个 ID 中,指纹库有 '+nS+' 条'
+(state.IM.badSig?(',其中 <b>'+state.IM.badSig+' 条解码失败</b>(指纹被截断或版本不一致)——请点上面「重载指纹」;仍失败则需在 Mac 端重跑 sig_images.py 并 push_signatures.py 重推'):'(指纹库与榜单 ID 对不上)')
+'。</div>';
}
h+='</div>';
if(state.IM.sig&&state.IM.rows.length){
const same=imPickItems();
const catAmt=((state.DATA&&state.DATA.items)||[]).reduce(function(s,it){return s+(+it.pay_amt||0);},0);
const amt=same.reduce(function(s,it){return s+(+it.pay_amt||0);},0);
const vol=same.reduce(function(s,it){return s+(+it.pay_item_cnt||0);},0);
const cnt=same.length;
const avg=cnt?amt/cnt:0;
const cur=state.IM.cands.filter(function(c){return c.cid===state.IM.pick;})[0]||null;
const pickName=(same[0]&&same[0].cluster_name)||(cur&&cur.name)||('款式簇#'+state.IM.pick);
const pickSim=cur?cur.mx:(state.IM.rows[0]?state.IM.rows[0].s:0);
// ---- 汇总 ----
h+='<div class="kpis">';
h+='<div class="kpi"><div class="l">同款市场总支付金额</div><div class="v">¥'+fmtN(amt)+'</div>'
+'<div class="d">锁定款式:'+esc(pickName)+'</div></div>';
h+='<div class="kpi"><div class="l">榜单 ID 数量</div><div class="v">'+cnt+'</div>'
+'<div class="d">该款在本类目榜单内的同款 ID 数</div></div>';
h+='<div class="kpi"><div class="l">榜单平均 ID 产出</div><div class="v">¥'+fmtN(avg)+'</div>'
+'<div class="d">总支付金额 ÷ ID 数量</div></div>';
h+='<div class="kpi"><div class="l">同款销量 / 类目占比</div><div class="v">'+fmtN(vol)+'</div>'
+'<div class="d">金额占类目 '+(catAmt>0?fmtP(amt/catAmt):'-')+'</div></div>';
h+='</div>';
// ---- 顶部提示 ----
if(state.IM.hit){
h+='<div class="card" style="border-color:#bbf7d0;background:#f0fdf4;padding:10px 14px">'
+'<b style="color:#15803d">✓ 这张图就是榜内商品</b> '
+'<span class="hint" style="margin:0">外观接近度 <b>'+imPct(state.IM.hit.s)+'</b>('+imGrade(imPct(state.IM.hit.s))+'),ID <b>'+esc(String(state.IM.hit.it.item_id))+'</b>'
+' '+(state.IM.hit.it.cluster_name?'已归入款式「'+esc(state.IM.hit.it.cluster_name)+'」':'暂未归簇')+'</span></div>';
}else{
h+='<div class="card" style="border-color:#fde68a;background:#fffbeb;padding:10px 14px">'
+'<b style="color:#b45309">最高外观接近度 '+imPct(state.IM.rows[0].s)+'('+imGrade(imPct(state.IM.rows[0].s))+')</b> '
+'<span class="hint" style="margin:0">这张图看起来不在榜内(或拍摄差异较大)。下面按外观相似度排序,'
+'<b>点任意一张卡片</b>即可把它所属的款式设为「同款基准」,汇总会立刻按该款重算。</span></div>';
}
// ---- 候选款式簇 ----
if(state.IM.cands.length){
h+='<div class="card"><h3>候选款式簇(按外观相似度排序,点一下切换同款基准)</h3>';
state.IM.cands.forEach(function(c){
const rep=(function(){
const it=((state.DATA&&state.DATA.items)||[]).filter(function(x){return (+x.cluster_id||0)===c.cid;})
.sort(function(a,b){return (+b.pay_amt||0)-(+a.pay_amt||0);})[0];
return it?it:null;
})();
const pu=rep&&rep.pic_url?String(rep.pic_url):'';
const img=pu?'<img src="'+esc(pu.indexOf('//')===0?'https:'+pu:pu)+'">':'<div style="width:38px;height:38px;border-radius:6px;background:#f3f4f6"></div>';
h+='<div class="imchip'+(c.cid===state.IM.pick?' on':'')+'" onclick="imLock('+c.cid+')">'+img
+'<span><b>'+imPct(c.mx)+'</b> · '+c.n+' 个ID · ¥'+fmtN(c.amt)+'<br>'
+'<span style="color:var(--tx2);font-size:11px">'+esc((c.name||'款式簇#'+c.cid).slice(0,16))+'</span></span></div>';
});
h+='<div class="hint" style="margin:6px 0 0">数字=<b>外观接近度</b>(校准分,不是同款判定):≥85≈同一张图,65~85 很可能同款,45~65 外观相似,&lt;45 仅供参考。'
+'同款 ID 数/金额按库内该款<b>全部</b> ID 统计(不只上前 80 的候选)。</div></div>';
}
// ---- AI 精判同款 · 7 维度结构比对 ----
if(state.IM.judge&&state.IM.judge.done&&!state.IM.judge.busy){
const res=state.IM.judge.res||{};
const st=imJudgeStats();
h+='<div class="card"><h3>AI 精判同款 · 7 维度结构比对</h3>';
h+='<div class="hint">口径:<b>只比结构</b>,忽略拍摄角度/光线/背景、颜色/木纹/饰面、尺寸清晰度水印、模特摆件;'
+'7 项中 <b>≥6 项一致=同款</b>,<b>4~5 项=相似款/改款</b>,<b>≤3 项=不同款</b>。'
+'首轮每对 1 票(约 40~120 秒),点「复核」可对同款/相似款再投一票取保守结论。</div>';
if(state.IM.judge.err){
h+='<div class="hint" style="color:#b91c1c">'+esc(state.IM.judge.err)+'</div>';
}else{
if(st.n){
h+='<div class="kpis">'
+'<div class="kpi"><div class="l">精判同款 · 市场总支付金额</div><div class="v">¥'+fmtN(st.amt)+'</div><div class="d">精判判为同款的 ID 汇总</div></div>'
+'<div class="kpi"><div class="l">精判同款 · 榜单 ID 数量</div><div class="v">'+st.n+'</div><div class="d">含同簇展开的全部 ID</div></div>'
+'<div class="kpi"><div class="l">精判同款 · 平均 ID 产出</div><div class="v">¥'+fmtN(st.avg)+'</div><div class="d">总支付金额 ÷ ID 数量</div></div>'
+'<div class="kpi"><div class="l">精判同款销量</div><div class="v">'+fmtN(st.vol)+'</div><div class="d">支付件数合计</div></div>'
+'</div>';
}else{
h+='<div class="hint" style="color:#b45309"><b>没有一个候选被判为同款</b>(都在相似款/不同款档位)——可以点下面的卡片手动锁定款式。</div>';
}
h+='<div class="tbl-scroll"><table><tr><th>#</th><th>主图</th><th>ID</th><th>商品标题</th>'
+'<th>7 维度逐项判定</th><th class="num">一致项</th><th>判定</th><th>所属款</th></tr>';
let k=0;
Object.keys(res).forEach(function(id){
const v=res[id];
const it=((state.DATA&&state.DATA.items)||[]).filter(function(x){return String(x.item_id)===String(id);})[0]||{};
k++;
const pu=it.pic_url?String(it.pic_url):'';
const url=pu?(pu.indexOf('//')===0?'https:'+pu:pu):'';
const dimNames=['轮廓','比例','支撑','分隔','门板','部件','设计'];
const dims=(v.dims||[]).map(function(d,n){
const mark=(d==='一致')?'<span style="color:#16a34a">✓</span>':((d==='有差异')?'<span style="color:#b91c1c">✗</span>':'?');
return '<span title="'+(n+1)+'.'+dimNames[n]+'">'+mark+dimNames[n]+'</span>';
}).join(' ');
const cls=v.verdict==='同款'?'bg-same':(String(v.verdict).indexOf('相似')>=0?'bg-simi':(v.verdict==='不同款'?'bg-diff':'bg-na'));
h+='<tr><td>'+k+'</td><td>'+(url?'<img class="pic" src="'+esc(url)+'">':'-')+'</td>'
+'<td style="font-variant-numeric:tabular-nums">'+esc(String(id))+'</td>'
+'<td style="max-width:300px;white-space:normal">'+esc(it.item_title||'')+'</td>'
+'<td style="white-space:normal">'+dims+'</td>'
+'<td class="num"><b>'+(v.same||0)+'</b>/7</td>'
+'<td><span class="bg '+cls+'">'+esc(v.verdict)+'</span></td>'
+'<td>'+esc(it.cluster_name||'未归簇')+'</td></tr>';
});
h+='</table></div>';
h+='<div style="margin-top:10px;display:flex;gap:10px;flex-wrap:wrap">'
+(st.n?'<button class="poolbtn" onclick="imLockFromJudge()">按精判结果锁定同款(上面汇总随之重算)</button>':'')
+'<button class="poolbtn" onclick="imJudge({recheck:true})">复核(对同款/相似款再投一票)</button>'
+'</div>';
}
h+='</div>';
}
// ---- 相似款卡片墙 ----
const top=state.IM.rows.slice(0,state.IM.topN);
h+='<div class="card"><h3>外观最像的 '+top.length+' 个 ID <span style="font-weight:400;color:var(--tx2);font-size:12px">(角标=外观接近度,非同款判定;点图片=锁定该款;绿框=当前锁定的同款款)</span></h3>';
h+='<div class="imgrid">';
top.forEach(function(r){
const it=r.it, c=+it.cluster_id||0;
const pu=it.pic_url?String(it.pic_url):'';
const url=pu?(pu.indexOf('//')===0?'https:'+pu:pu):'';
h+='<div class="imcell'+(c&&c===state.IM.pick?' sel':'')+'" title="'+esc(it.item_title||'')+'" onclick="imLock('+c+')">'
+imBadge(it)
+'<span class="s">'+imPct(r.s)+'</span>'
+(url?'<img loading="lazy" src="'+esc(url)+'">':'<div style="height:100px;background:#f3f4f6;border-radius:6px"></div>')
+'<div class="t">'+esc(String(it.item_title||'').slice(0,26))+'</div>'
+'<div class="p">¥'+fmtN(+it.pay_amt||0)+' <span style="font-weight:400;color:var(--tx2);font-size:11px">'+fmtN(+it.pay_item_cnt||0)+'件</span></div>'
+'<div class="c">'+esc(it.cluster_name||'未归簇')+'</div>'
+'</div>';
});
h+='</div>';
if(state.IM.rows.length>state.IM.topN){
h+='<div style="text-align:center;margin-top:10px"><button class="poolbtn" onclick="imMore()">再看 48 个(共 '+state.IM.rows.length+' 个可比对)</button></div>';
}
h+='</div>';
// ---- 同款明细 ----
h+='<div class="card"><h3>同款明细 · '+esc(pickName)+'('+cnt+' 个 ID)</h3>';
if(!cnt){
h+='<div class="empty">这个款还没有归入外观同款簇,换一张卡片试试</div>';
}else{
const list=same.slice().sort(function(a,b){return (+b.pay_amt||0)-(+a.pay_amt||0);});
h+='<div class="tbl-scroll"><table><tr><th>#</th><th>主图</th><th>ID</th><th>商品标题</th><th>店铺</th>'
+'<th class="num">价格</th><th class="num">销量</th><th class="num">支付金额</th><th class="num">外观接近度</th><th>操作</th></tr>';
const simOf={};
state.IM.rows.forEach(function(r){simOf[String(r.it.item_id)]=r.s;});
list.forEach(function(it,i){
const pu=it.pic_url?String(it.pic_url):'';
const url=pu?(pu.indexOf('//')===0?'https:'+pu:pu):'';
const sid=String(it.item_id);
h+='<tr><td>'+(i+1)+'</td>'
+'<td>'+(url?'<img class="pic" loading="lazy" src="'+esc(url)+'">':'-')+'</td>'
+'<td style="font-variant-numeric:tabular-nums">'+esc(sid)+'</td>'
+'<td style="max-width:320px;white-space:normal">'+esc(it.item_title||'')+'</td>'
+'<td>'+esc(it.shop_name||it.brand||'-')+'</td>'
+'<td class="num">¥'+(it.price!=null?(+it.price).toFixed(0):'-')+'</td>'
+'<td class="num">'+fmtN(+it.pay_item_cnt||0)+'</td>'
+'<td class="num"><b>¥'+fmtN(+it.pay_amt||0)+'</b></td>'
+'<td class="num">'+(simOf[sid]!=null?imPct(simOf[sid]):'-')+'</td>'
+'<td><button class="poolbtn" onclick="poolAdd(\''+esc(sid)+'\')">+ 池</button></td></tr>';
});
h+='<tr style="background:#f8fafc;font-weight:700"><td colspan="6">合计</td>'
+'<td class="num">'+fmtN(vol)+'</td><td class="num">¥'+fmtN(amt)+'</td>'
+'<td class="num" colspan="2">平均每 ID ¥'+fmtN(avg)+'</td></tr>';
h+='</table></div>';
}
h+='</div>';
}
panels.innerHTML=h;
state.IM.curKey=key;
}

View File

@ -0,0 +1,56 @@
import { state } from './shared/state.js';
import { render } from './board.js';
import { selDate, selCat, selCat3, btnRefresh } from './shared/dom.js';
import { load } from './data-loader.js';
import { initImageZoom } from './shared/modal.js';
import { initImagePaste } from './image-match/input.js';
import { registerRenderers } from './shared/renderers.js';
import { renderImatch } from './image-match/view.js';
import * as handlers0 from './shared/format.js';
import * as handlers1 from './shared/demo.js';
import * as handlers2 from './shared/modal.js';
import * as handlers3 from './board.js';
import * as handlers4 from './data-loader.js';
import * as handlers5 from './tabs/capacity.js';
import * as handlers6 from './tabs/soaring.js';
import * as handlers7 from './tabs/category-demand.js';
import * as handlers8 from './tabs/matrix.js';
import * as handlers9 from './tabs/product-lists.js';
import * as handlers10 from './pool/model.js';
import * as handlers11 from './pool/editor.js';
import * as handlers12 from './pool/data.js';
import * as handlers13 from './pool/list.js';
import * as handlers14 from './tabs/calculator.js';
import * as handlers15 from './image-match/fingerprint.js';
import * as handlers16 from './image-match/matching.js';
import * as handlers17 from './image-match/judgement.js';
import * as handlers18 from './image-match/input.js';
import * as handlers19 from './image-match/view.js';
/** 保留交付页 inline onclick 的入口;模块间通过 import 与统一 state 连接。 */
for (const handlers of [handlers0, handlers1, handlers2, handlers3, handlers4, handlers5, handlers6, handlers7, handlers8, handlers9, handlers10, handlers11, handlers12, handlers13, handlers14, handlers15, handlers16, handlers17, handlers18, handlers19]) {
for (const [name, value] of Object.entries(handlers)) if (typeof value === 'function') window[name] = value;
}
for (const name of Object.keys(state)) Object.defineProperty(window, name, {configurable:true,get:()=>state[name],set:value=>{state[name]=value;}});
registerRenderers({board:render,imageMatch:renderImatch});
initImageZoom();
initImagePaste();
document.querySelectorAll('.tab').forEach(b=>b.onclick=()=>{
document.querySelectorAll('.tab').forEach(x=>x.classList.remove('on'));
b.classList.add('on');state.TAB=b.dataset.t;render();
});
selDate.onchange=selCat.onchange=()=>{ // 二级类目变化 → 重置三级联动
selCat3.innerHTML='<option value="">全部</option>';selCat3.removeAttribute('data-opts');
selDate.querySelectorAll('option').length&&(state.DATA=null);state.ITEM_SRC='all';load();
};
selCat3.onchange=()=>{selDate.querySelectorAll('option').length&&(state.DATA=null);state.ITEM_SRC='all';load()};
btnRefresh.onclick=()=>{selDate.innerHTML='';selDate.removeAttribute('data-opts');load()};
load();

View File

@ -0,0 +1,45 @@
import { state } from '../shared/state.js';
import { getCurrentUser, getPoolData, removePoolRecord, savePoolRecord } from '../shared/api.js';
import { selCat, selCat3 } from '../shared/dom.js';
import { DEF_CAT, DEF_CAT3 } from '../shared/config.js';
import { renderBoard as render } from '../shared/renderers.js';
export async function ensureMe(){
if(state.ME_NAME)return;
try{const r=await getCurrentUser();const d=await r.json();
if(d&&d.user)state.ME_NAME=d.user.nickname||d.user.name||'';}catch(e){}
}
export async function loadPool(force){
if(state.POOL&&!force)return;
const p=new URLSearchParams();
const cat=selCat.value||DEF_CAT, cat3=selCat3.value||(cat===DEF_CAT?DEF_CAT3:'');
p.set('cat',cat);if(cat3)p.set('cat3',cat3);
try{
const r=await getPoolData(p);
const d=await r.json();
if(d.error){state.POOL={rows:[],stat:{},error:d.error};}
else{state.POOL=d;if(d.me)state.ME_NAME=d.me;}
}catch(e){state.POOL={rows:[],stat:{},error:e.message};}
}
export async function deletePool(id){
if(!confirm('确定从意向选品池删除这条登记吗?'))return;
try{
const r=await removePoolRecord(id);
const d=await r.json().catch(()=>({}));
if(!r.ok||d.error){alert(d.error||('删除失败 HTTP '+r.status));return;}
await loadPool(true);render();
}catch(e){alert('删除失败:'+e.message);}
}
export async function changeStatus(id,status){
const r=(state.POOL&&state.POOL.rows||[]).find(x=>Number(x.id)===Number(id));
if(!r)return;
try{
const resp=await savePoolRecord({id:id,item_id:r.item_id,status:status});
const d=await resp.json().catch(()=>({}));
if(!resp.ok||d.error){alert(d.error||('状态更新失败 HTTP '+resp.status));}
await loadPool(true);render();
}catch(e){alert('状态更新失败:'+e.message);}
}

View File

@ -0,0 +1,150 @@
import { POOL_STASH, poolMap, bandOfPrice, bandVolumeOf, bjToday, PROFIT_KEYS, POOL_STATUS, canEditPoolRecord } from './model.js';
import { state } from '../shared/state.js';
import { ensureMe, loadPool } from './data.js';
import { openModal } from '../shared/modal.js';
import { esc } from '../shared/format.js';
import { selCat, selCat3 } from '../shared/dom.js';
import { savePoolRecord } from '../shared/api.js';
import { renderBoard as render } from '../shared/renderers.js';
// 从 TOP商品明细 / 手动登记入口打开登记弹窗(rec=池内已有记录则转编辑)
export function poolAdd(itemId){
if(!canEditPoolRecord()){alert('没有市场选款看板编辑权限');return;}
const it=POOL_STASH[String(itemId)]||((state.DATA&&state.DATA.items)||[]).find(x=>String(x.item_id)===String(itemId));
const ex=poolMap()[String(itemId)];
if(ex){poolEdit(ex.id);return;}
if(!it){alert('未找到该商品(可能不在当前类目明细里),请在选品池内手动登记');return;}
ensureMe().then(()=>{
const band=bandOfPrice(it.price);
const vol=bandVolumeOf(band);
const head='<b>意向选品登记</b><span style="font-size:12px;color:var(--tx2)"> 商品将进入「意向选品池」,供后续落地跟进</span>';
const inner=poolFormHtml({
item_id:it.item_id,pic:it.pic_url,title:it.item_title,brand:it.brand,shop_name:it.shop_name,
price:it.price,price_band:band,band_volume:vol,selector:state.ME_NAME,sel_date:bjToday(),
status:'意向',est_volume:'',analysis:'',profit_calc:null,profit_rate:null
});
openModal(head,inner);
bindPoolForm(null);
});
}
export function poolEdit(id){
const r=(state.POOL&&state.POOL.rows||[]).find(x=>Number(x.id)===Number(id));
if(!r)return;
if(!canEditPoolRecord(r)){alert('没有这条选品的编辑权限');return;}
const head='<b>编辑意向选品</b><span style="font-size:12px;color:var(--tx2)"> 选品人 '+(r.selector||'-')
+' 登记于 '+String(r.created_at||'').slice(0,10)+'</span>';
openModal(head,poolFormHtml(r));
bindPoolForm(r);
}
// 手动登记:从当前类目 TOP500 明细里挑一个商品
export function poolPick(){
if(!canEditPoolRecord()){alert('没有市场选款看板编辑权限');return;}
const its=(state.DATA&&state.DATA.items)||[];
openModal('<b>手动登记 · 选择商品</b><span style="font-size:12px;color:var(--tx2)"> 当前类目在榜 '+its.length+' 个商品</span>',
'<div style="padding:10px 16px"><input class="sel" id="poolPickQ" placeholder="输入标题/品牌/ID 过滤…" style="width:100%">'
+'<div class="tbl-scroll" style="max-height:60vh;margin-top:10px"><table><tr><th>主图</th><th>商品</th><th class="num">价格</th><th></th></tr>'
+its.slice(0,80).map(it=>'<tr class="poolPickRow" data-q="'+esc(((it.item_title||'')+' '+(it.brand||'')+' '+(it.item_id||'')).toLowerCase())+'">'
+'<td>'+(it.pic_url?'<img class="pic" loading="lazy" style="width:44px;height:44px" src="'+(it.pic_url.startsWith('//')?'https:'+it.pic_url:esc(it.pic_url))+'">':'-')+'</td>'
+'<td style="max-width:360px;white-space:normal">'+esc(it.item_title)+'<br><span style="font-size:11px;color:var(--tx2)">'+esc(it.item_id||'')+'</span></td>'
+'<td class="num">¥'+(it.price!=null?(+it.price).toFixed(0):'-')+'</td>'
+'<td><button class="poolbtn" onclick="poolAdd(\''+esc(String(it.item_id))+'\')">选它</button></td></tr>').join('')
+'</table></div></div>');
const q=document.getElementById('poolPickQ');
if(q)q.oninput=()=>{
const k=q.value.trim().toLowerCase();
document.querySelectorAll('.poolPickRow').forEach(tr=>{tr.style.display=(!k||tr.dataset.q.indexOf(k)>=0)?'':'none';});
};
}
export function poolFormHtml(r){
const pc=(r&&r.profit_calc)||{};
const vals=PROFIT_KEYS.map(k=>pc[k]!=null?+pc[k]:(k==='售价'?(r&&r.price!=null?+r.price:''):''));
const pic=r&&r.pic?String(r.pic):'';
return '<div style="padding:12px 16px 18px">'
+'<div style="display:flex;gap:14px;align-items:flex-start">'
+(pic?'<img class="pic" src="'+(pic.startsWith('//')?'https:'+pic:esc(pic))+'" style="width:88px;height:88px;flex:none">':'')
+'<div style="font-size:13px;line-height:1.6"><b>'+esc(r&&r.title||'')+'</b><br>'
+'<span style="color:var(--tx2)">品牌/店铺:'+esc([r&&r.brand,r&&r.shop_name].filter(Boolean).join(' / ')||'-')
+' 商品ID:'+esc(r&&r.item_id||'')+'</span></div>'
+'</div>'
+'<div style="display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px;margin-top:12px">'
+'<label style="font-size:12px">选品人<input class="sel" id="pfSelector" value="'+esc(r&&r.selector||state.ME_NAME||'')+'" style="width:100%;margin-top:3px"></label>'
+'<label style="font-size:12px">选品价格带<select class="sel" id="pfBand" style="width:100%;margin-top:3px">'
+(((state.DATA&&state.DATA.priceBands)||[]).map(b=>'<option'+(b.band===(r&&r.price_band)?' selected':'')+'>'+esc(b.band)+'</option>').join('')||'<option>'+(esc(r&&r.price_band)||'')+'</option>')
+'</select></label>'
+'<label style="font-size:12px">价格带月销售额<input class="sel" id="pfBandVol" value="'+((r&&r.band_volume!=null)?+r.band_volume:'')+'" readonly style="width:100%;margin-top:3px;background:#f5f6f8"></label>'
+'<label style="font-size:12px">选款日期<input class="sel" type="date" id="pfDate" value="'+esc((r&&r.sel_date||bjToday()).slice(0,10))+'" style="width:100%;margin-top:3px"></label>'
+'<label style="font-size:12px">预估体量(如:月销300件 / 月销8万)<input class="sel" id="pfEst" value="'+esc(r&&r.est_volume||'')+'" placeholder="手工填写落地预期" style="width:100%;margin-top:3px"></label>'
+'<label style="font-size:12px">状态<select class="sel" id="pfStatus" style="width:100%;margin-top:3px">'
+POOL_STATUS.map(s=>'<option'+(s===(r&&r.status||'意向')?' selected':'')+'>'+s+'</option>').join('')+'</select></label>'
+'</div>'
+'<div style="margin-top:12px"><div style="font-size:12px;color:var(--tx2);margin-bottom:5px">利润率测算表(单位:元;利润率自动计算)</div>'
+'<table style="width:auto" id="pfProfit"><tr>'
+PROFIT_KEYS.map((k,i)=>'<th style="padding:4px 8px'+(i===0?';background:#eef4ff':'')+'">'+k+'</th>').join('')
+'<th style="padding:4px 8px;background:#eef9f1">利润</th><th style="padding:4px 8px;background:#eef9f1">利润率</th></tr><tr>'
+vals.map((v,i)=>'<td style="padding:3px"><input class="sel pfin" data-k="'+PROFIT_KEYS[i]+'" value="'+(v===''?'':v)+'" style="width:86px;padding:4px 6px"></td>').join('')
+'<td class="num" id="pfProfitVal" style="padding:4px 10px;font-weight:600">-</td>'
+'<td class="num" id="pfProfitRate" style="padding:4px 10px;font-weight:600">-</td></tr></table>'
+'<div style="font-size:11px;color:var(--tx2);margin-top:4px">平台扣点参考:天猫约 5.5%(可按 售价×0.055 填入);利润率 =(售价 − 各项成本合计)÷ 售价</div></div>'
+'<div style="margin-top:12px"><div style="font-size:12px;color:var(--tx2);margin-bottom:5px">选品分析(为什么选它:卖点 / 竞争度 / 差异化 / 风险)</div>'
+'<textarea class="sel" id="pfAnalysis" style="width:100%;min-height:88px;font-family:inherit;padding:8px">'+esc(r&&r.analysis||'')+'</textarea></div>'
+'<div style="margin-top:14px;display:flex;gap:10px;justify-content:flex-end">'
+'<button class="sel" style="cursor:pointer" onclick="document.getElementById(\'smModal\').classList.remove(\'on\')">取消</button>'
+'<button id="pfSave" style="padding:7px 22px;border:none;border-radius:8px;background:var(--blue);color:#fff;font-weight:600;cursor:pointer">保存登记</button></div>'
+'<input type="hidden" id="pfItemId" value="'+esc(r&&r.item_id||'')+'"><input type="hidden" id="pfEditId" value="'+(r&&r.id||'')+'">'
+'</div>';
}
export function poolCalc(){
const ins=[...document.querySelectorAll('#pfProfit .pfin')];
const get=k=>{const el=ins.find(x=>x.dataset.k===k);const v=el?parseFloat(el.value):NaN;return isNaN(v)?0:v;};
const price=get('售价');
const cost=PROFIT_KEYS.filter(k=>k!=='售价').reduce((s,k)=>s+get(k),0);
const profit=price-cost;
const rate=price>0?profit/price:null;
const pv=document.getElementById('pfProfitVal'),pr=document.getElementById('pfProfitRate');
if(pv)pv.textContent=price>0?profit.toFixed(1):'-';
if(pr){pr.textContent=rate==null?'-':(rate*100).toFixed(1)+'%';pr.style.color=rate==null?'':(rate>=0.18?'#16a34a':(rate>=0.1?'#b45309':'#dc2626'));}
return {price,cost,profit,rate,calc:ins.reduce((o,x)=>{o[x.dataset.k]=x.value===''?null:parseFloat(x.value);return o;},{})};
}
export function bindPoolForm(editing){
document.querySelectorAll('#pfProfit .pfin').forEach(el=>el.oninput=poolCalc);
const band=document.getElementById('pfBand');
if(band)band.onchange=()=>{const v=bandVolumeOf(band.value);const el=document.getElementById('pfBandVol');if(el)el.value=v==null?'':v;};
poolCalc();
const save=document.getElementById('pfSave');
if(save)save.onclick=()=>savePool(editing);
}
export async function savePool(editing){
const calc=poolCalc();
const body={
id:document.getElementById('pfEditId').value||undefined,
cat2:(state.DATA&&state.DATA.summary&&state.DATA.summary.cat2)||selCat.value||'',
cat3:selCat3.value||(state.DATA&&state.DATA.summary&&state.DATA.summary.cat3)||'',
item_id:document.getElementById('pfItemId').value,
pic:((state.DATA.items||[]).find(x=>String(x.item_id)===String(document.getElementById('pfItemId').value))||{}).pic_url||'',
title:((state.DATA.items||[]).find(x=>String(x.item_id)===String(document.getElementById('pfItemId').value))||{}).item_title||'',
price:calc.price||null,
price_band:(document.getElementById('pfBand')||{}).value||'',
band_volume:parseFloat((document.getElementById('pfBandVol')||{}).value)||null,
est_volume:(document.getElementById('pfEst')||{}).value||'',
selector:(document.getElementById('pfSelector')||{}).value||state.ME_NAME,
analysis:(document.getElementById('pfAnalysis')||{}).value||'',
profit_calc:calc.calc,profit_rate:calc.rate,
sel_date:(document.getElementById('pfDate')||{}).value||'',
status:(document.getElementById('pfStatus')||{}).value||'意向'
};
if(!body.item_id){alert('缺少商品ID');return;}
try{
const r=await savePoolRecord(body);
const d=await r.json().catch(()=>({}));
if(!r.ok||d.error){alert(d.error||('保存失败 HTTP '+r.status));return;}
document.getElementById('smModal').classList.remove('on');
await loadPool(true);render(); // 当前 Tab 重渲染:池列表刷新 / 明细行按钮变「已登记」
}catch(e){alert('保存失败:'+e.message);}
}

View File

@ -0,0 +1,68 @@
import { loadPool } from './data.js';
import { state } from '../shared/state.js';
import { fmtN, esc } from '../shared/format.js';
import { POOL_STATUS, canEditPoolRecord } from './model.js';
import { panels } from '../shared/dom.js';
export function renderPool(){
loadPool().then(()=>{
const rows=(state.POOL&&state.POOL.rows)||[], st=(state.POOL&&state.POOL.stat)||{};
const selectors=[...new Set(rows.map(r=>r.selector).filter(Boolean))];
let h='<div class="card"><h3>意向选品池'
+(canEditPoolRecord()?' <button class="sel" style="cursor:pointer;font-size:12px;padding:4px 12px" onclick="poolPick()">+ 登记商品</button>':'')
+' <button class="sel" style="cursor:pointer;font-size:12px;padding:4px 12px" onclick="loadPool(true).then(renderPool)">刷新</button></h3>'
+'<div class="hint">从「TOP商品明细」点 <b>+池</b> 或在此手动登记;唯一键=二级+三级类目+商品ID(同一商品不重复登记)。'
+'利润率绿≥18% / 橙≥10% / 红&lt;10%。选品池全公司共享,本人可编辑/删除自己的登记,無尘可管理全部。</div>'
+'<div style="display:flex;gap:18px;flex-wrap:wrap;margin:8px 0 4px">'
+'<span style="font-size:12px;color:var(--tx2)">在池 <b style="color:var(--tx);font-size:15px">'+(st.active!=null?st.active:rows.length)+'</b> 款</span>'
+'<span style="font-size:12px;color:var(--tx2)">累计登记 <b style="color:var(--tx);font-size:15px">'+(st.total||0)+'</b></span>'
+'<span style="font-size:12px;color:var(--tx2)">平均测算利润率 <b style="color:var(--tx);font-size:15px">'+(st.avgProfit?(st.avgProfit*100).toFixed(1)+'%':'-')+'</b></span>'
+'<span style="font-size:12px;color:var(--tx2)">覆盖价格带体量合计 <b style="color:var(--tx);font-size:15px">¥'+fmtN(st.bandVolume||0)+'</b></span>'
+'<span style="flex:1"></span>'
+'<select class="sel" id="poolFSel" style="font-size:12px;padding:3px 8px"><option value="">全部选品人</option>'
+selectors.map(s=>'<option'+(s===state.POOL_Q.selector?' selected':'')+'>'+esc(s)+'</option>').join('')+'</select>'
+'<select class="sel" id="poolFSt" style="font-size:12px;padding:3px 8px"><option value="">全部状态</option>'
+POOL_STATUS.map(s=>'<option'+(s===state.POOL_Q.status?' selected':'')+'>'+s+'</option>').join('')+'</select>'
+'</div>';
let list=rows;
if(state.POOL_Q.selector)list=list.filter(r=>r.selector===state.POOL_Q.selector);
if(state.POOL_Q.status)list=list.filter(r=>r.status===state.POOL_Q.status);
if(!list.length){
h+='<div class="empty">暂无登记。到「TOP商品明细」点商品行的 <b>+ 池</b>,或点上方「+ 登记商品」。</div></div>';
panels.innerHTML=h;bindPoolFilters();return;
}
h+='<div class="tbl-scroll"><table><tr><th>#</th><th>款图</th><th>商品</th><th>选品人</th><th>选品价格带</th>'
+'<th class="num">价格带月销售额</th><th>预估体量</th><th class="num">测算利润率</th><th>选款日期</th><th>状态</th><th>选品分析</th><th>操作</th></tr>';
list.forEach((r,i)=>{
const editable=canEditPoolRecord(r);
const pic=r.pic?String(r.pic):'';
const rate=r.profit_rate!=null&&!isNaN(+r.profit_rate)?+r.profit_rate:null;
const rateHtml=rate==null?'<span style="color:var(--tx2)">-</span>'
:'<b style="color:'+(rate>=0.18?'#16a34a':(rate>=0.1?'#b45309':'#dc2626'))+'">'+(rate*100).toFixed(1)+'%</b>';
const ana=r.analysis?esc(String(r.analysis).length>42?String(r.analysis).slice(0,42)+'…':r.analysis):'<span style="color:var(--tx2)">-</span>';
h+='<tr><td>'+(i+1)+'</td>'
+'<td>'+(pic?'<img class="pic" loading="lazy" src="'+(pic.startsWith('//')?'https:'+pic:esc(pic))+'">':'-')+'</td>'
+'<td style="max-width:300px;white-space:normal"><b>'+esc(r.title||'')+'</b><br><span style="font-size:11px;color:var(--tx2)">'
+esc([r.brand,r.shop_name].filter(Boolean).join(' / ')||'')+' ¥'+(r.price!=null?(+r.price).toFixed(0):'-')+'</span></td>'
+'<td>'+esc(r.selector||'-')+'</td>'
+'<td><span class="tag b">'+esc(r.price_band||'-')+'</span></td>'
+'<td class="num">'+(r.band_volume!=null?'¥'+fmtN(+r.band_volume):'-')+'</td>'
+'<td style="max-width:130px;white-space:normal">'+esc(r.est_volume||'-')+'</td>'
+'<td class="num">'+rateHtml+'</td>'
+'<td style="white-space:nowrap">'+esc(String(r.sel_date||'').slice(0,10))+'</td>'
+'<td><select '+(editable?'':'disabled ')+'class="sel" style="font-size:12px;padding:2px 6px" onchange="changeStatus('+Number(r.id)+',this.value)">'
+POOL_STATUS.map(s=>'<option'+(s===r.status?' selected':'')+'>'+s+'</option>').join('')+'</select></td>'
+'<td style="max-width:220px;white-space:normal" '+(editable?'title="点击查看/编辑" onclick="poolEdit('+Number(r.id)+')"':'')+'>'+ana+'</td>'
+'<td style="white-space:nowrap">'+(editable?'<button class="poolbtn" onclick="poolEdit('+Number(r.id)+')">编辑</button> '
+'<button class="poolbtn" style="color:#dc2626;border-color:#fecaca" onclick="deletePool('+Number(r.id)+')">删除</button>':'只读')+'</td></tr>';
});
h+='</table></div></div>';
panels.innerHTML=h;bindPoolFilters();
});
}
export function bindPoolFilters(){
const a=document.getElementById('poolFSel'),b=document.getElementById('poolFSt');
if(a)a.onchange=()=>{state.POOL_Q.selector=a.value;renderPool();};
if(b)b.onchange=()=>{state.POOL_Q.status=b.value;renderPool();};
}

View File

@ -0,0 +1,49 @@
import { esc } from '../shared/format.js';
import { state } from '../shared/state.js';
import { bandRange } from '../shared/price-bands.js';
/* ==================== 意向选品池 ====================
流程:TOP商品明细 / 手动登记 → 弹窗填 选品人/价格带/选品分析/利润率测算/预估体量
→ POST /api/selection-market/pool(PG app.selection_market_pool,唯一键 cat2+cat3+item_id)
→ 「意向选品池」Tab 列表维护(编辑/删除/状态流转),方便后续落地跟进 */
export const POOL_STATUS=['意向','已打样','已上架','放弃'];
export const PROFIT_KEYS=['售价','采购成本','快递费','包材','平台扣点','推广费','售后损耗'];
// 下钻弹窗里的行不在 DATA.items 现引用里时也能登记:先暂存商品对象
export const POOL_STASH={};
export function poolStash(it){if(it&&it.item_id)POOL_STASH[String(it.item_id)]=it;}
export function poolCellHtml(it){
poolStash(it);
const ex=poolMap()[String(it.item_id)];
if(ex)return '<span class="tag g" title="选品人:'+esc(ex.selector||'-')+'">已登记</span>';
if(!state.POOL?.permissions?.edit)return '';
return '<button class="poolbtn" title="登记到意向选品池" onclick="poolAdd(\''+esc(String(it.item_id))+'\')">+ 池</button>';
}
export function canEditPoolRecord(row){
return Boolean(state.POOL?.permissions?.edit &&
(!row || state.POOL.isOwner || row.created_by===state.ME_NAME || row.selector===state.ME_NAME));
}
export function poolMap(){const m={};(state.POOL&&state.POOL.rows||[]).forEach(r=>{m[String(r.item_id)]=r;});return m;}
// 价格 → 所在价格带标签(复用 bandRange 的分桶口径)
export function bandOfPrice(price){
const bands=((state.DATA&&state.DATA.priceBands)||[]).map(b=>b.band);
for(const b of bands){const r=bandRange(b);if(r&&+price>=r[0]&&+price<=r[1])return b;}
return bands[0]||'';
}
export function bandVolumeOf(band){
const pb=((state.DATA&&state.DATA.priceBands)||[]).find(b=>b.band===band);
return pb&&pb.payAmt!=null?+pb.payAmt:null;
}
export function bjToday(){
try{return new Intl.DateTimeFormat('sv-SE',{timeZone:'Asia/Shanghai'}).format(new Date());}
catch(e){return new Date().toISOString().slice(0,10);}
}

View File

@ -0,0 +1,10 @@
/** 维持原 URL、请求参数和缓存选项,响应处理仍由各业务模块负责。 */
export function getBoardData(params) { return fetch('/api/selection-market?' + params.toString(), {cache:'no-store'}); }
export function getCurrentUser() { return fetch('/api/auth/me', {cache:'no-store'}); }
export function getPoolData(params) { return fetch('/api/selection-market/pool?' + params.toString(), {cache:'no-store'}); }
export function savePoolRecord(body) { return fetch('/api/selection-market/pool', {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)}); }
export function removePoolRecord(id) { return fetch('/api/selection-market/pool?id=' + id, {method:'DELETE'}); }
export function getImageSignatures(cat, cat3) { return fetch('/api/selection-market/image-match?cat='+encodeURIComponent(cat)+'&cat3='+encodeURIComponent(cat3), {cache:'no-store'}); }
export function judgeStyles(img, votes, items) { return fetch('/api/selection-market/style-judge', {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({img:img,votes:votes,items:items})}); }

View File

@ -0,0 +1,5 @@
export const DEF_CAT='柜类', DEF_CAT3='鞋柜';
export const DASH_URL=location.origin;

View File

@ -0,0 +1,43 @@
export function buildDemoData(){
let seed=42;const rnd=()=>((seed=seed*1103515245+12345&0x7fffffff)/0x7fffffff);
const bands=['0-49','50-99','100-199','200-299','300-399','400-599','600-999'];
const styles=['塑料抽屉式收纳柜','金属布艺收纳架','木质开放式书架','翻盖收纳箱','简易钢管衣柜','床头小型柜','竹木置物架'];
const brands=['欧意朗','亿家达','卓禾','收纳大师','家逸','慧乐家','白牌/未标注'];
const priceBands=bands.map((b,i)=>({band:b,itemCount:20+Math.round(rnd()*40),
payItemCnt:Math.round((8000-i*900)*(0.7+rnd()*0.6)),
topStyles:styles.slice(0,3).map(s=>({style:s,count:3+Math.round(rnd()*10)}))}));
const totCnt=priceBands.reduce((s,b)=>s+b.payItemCnt,0);
const brandShare=brands.map((b,i)=>({brand:b,flowShare:0.22*Math.pow(0.62,i),
cntShare:0.24*Math.pow(0.6,i),productShare:0.2*Math.pow(0.7,i),
payItemCnt:Math.round(totCnt*0.24*Math.pow(0.6,i)),products:8+Math.round(rnd()*40)}));
const matrix=styles.map(st=>bands.map(()=>Math.round(rnd()*3000)));
const kws=['收纳柜','小衣柜','抽屉式收纳柜','床头柜小型','简易书架','布艺收纳','宿舍收纳柜','租房衣柜','桌面小书架','床边柜']
.map((k,i)=>({keyword:k,search_pv:90000-i*7000,click_rate:0.06-i*0.002,click_pv:5000-i*400,
pay_conv_rate:0.09-i*0.004,pay_item_cnt:8000-i*600,mall_rate:0.55+i*0.02,supply_ratio:1.2+i*0.3}));
const items=[];
for(let i=0;i<30;i++){
const st=styles[Math.floor(rnd()*styles.length)],br=brands[Math.floor(rnd()*brands.length)];
items.push({rank:i+1,item_id:'DEMO'+(1000+i),item_title:'【演示】'+st+' 小户型 '+br+' 简约现代',
shop_name:br+'旗舰店',brand:br,price:49+Math.round(rnd()*550),
pay_item_cnt:Math.round(4000*Math.pow(0.88,i)),flow_index:Math.round(20000*Math.pow(0.9,i)),
pic_url:'',style:st,shape:rnd()>0.5?'抽屉式':'层架式',material:st.includes('金属')?'金属':(st.includes('塑料')?'塑料':'木质'),
color:rnd()>0.5?'白':'灰',scene:'卧室、客厅',source:i>22?'soaring':'item_rank',cat_id:'50010788',cat_name:'收纳柜'});
}
return {dates:['2026-10-19'],cats:[{catId:'50010788',catName:'收纳柜'}],
cats3:[{catId:'收纳柜',catName:'收纳柜'},{catId:'收纳箱',catName:'收纳箱'},{catId:'床头柜',catName:'床头柜'}],
summary:{snapshotDate:'2026-10-19',catId:'50010788',cat2:'收纳整理',cat3:'',catName:'收纳整理',demandTotal:totCnt,
itemCount:30,soaringCount:7,trendCovered:30,brandCount:brands.length,avgPrice:236,cr5:0.52,cr10:0.71,taggedCount:30},
priceBands,brandShare,styleMatrix:{styles,bands,matrix},
catTrend:['2026-05','2026-06','2026-07','2026-08','2026-09','2026-10']
.map((m,i)=>({month:m,amount:5200000+i*430000+Math.round(rnd()*380000)})),
soarers:items.slice(0,7).map((it,i)=>({...it,orig_rank:8+i*3,rank:i+1,rise_months:2+(i%4),
rank_jump:14+i*11,trend6m:['2026-05','2026-06','2026-07','2026-08','2026-09','2026-10']
.map((d,j)=>({date:d,rank:42+i*6-j*(4+i%3)}))})),
keywords:kws,
catDemand:[{page:'catdemand',rank:1,cat2_name:'收纳整理',cat3_name:'收纳柜',demand_index:88230,search_pv:120000,pay_item_cnt:32000,pay_amt:7600000,item_cnt:8200,shop_cnt:2100,click_rate:0.32,pay_conv_rate:0.09},
{page:'catdemand',rank:2,cat2_name:'收纳整理',cat3_name:'收纳箱',demand_index:71200,search_pv:98000,pay_item_cnt:27000,pay_amt:4300000,item_cnt:6400,shop_cnt:1900,click_rate:0.35,pay_conv_rate:0.11},
{page:'soaring',rank:1,cat2_name:'收纳整理',cat3_name:'床边收纳柜',demand_index:45100,search_pv:51000,pay_item_cnt:15200,pay_amt:3100000,item_cnt:2100,shop_cnt:860,click_rate:0.41,pay_conv_rate:0.14}],
items};
}

View File

@ -0,0 +1,9 @@
export const panels = document.getElementById('panels');
export const kpis = document.getElementById('kpis');
export const meta = document.getElementById('meta');
export const selDate = document.getElementById('selDate');
export const selCat = document.getElementById('selCat');
export const selCat3 = document.getElementById('selCat3');
export const btnRefresh = document.getElementById('btnRefresh');

View File

@ -0,0 +1,32 @@
// 默认类目:首次打开定位到 柜类/鞋柜
export const fmtN = v => (v==null||isNaN(v)) ? '-' : (v>=10000 ? (v/10000).toFixed(1)+'万' : Math.round(v).toLocaleString());
export const fmtP = v => (v==null||isNaN(v)) ? '-' : (v*100).toFixed(1)+'%';
export const esc = s => String(s==null?'':s).replace(/[&<>"]/g, c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]));
export const COLORS=['b-blue','b-green','b-amber','b-purple','b-red'];
export function trendLine(t){
if(!t||!t.length)return '<span style="color:var(--tx2)">-</span>';
return t.map((p,i)=>{
const prev=i?t[i-1].rank:p.rank;
const up=p.rank<prev; // 排名数值变小=名次上升
const color=i===0?'var(--tx2)':(up?'#dc2626':'#16a34a');
return '<span style="color:'+color+';font-variant-numeric:tabular-nums">'+p.rank+'</span>'
+(i<t.length-1?'<span style="color:#cbd5e1">→</span>':'');
}).join('');
}
// 涨红跌绿(环比/同比通用;null 显示 -)
export function momCell(v){
if(v==null||isNaN(+v))return '<span style="color:var(--tx2)">-</span>';
const x=+v;
const cls=x>0?'up':(x<0?'down':'');
return '<span class="'+cls+'">'+(x>0?'+':'')+fmtP(x)+'</span>';
}

View File

@ -0,0 +1,148 @@
import { bandRange } from './price-bands.js';
import { state } from './state.js';
import { esc, fmtN, trendLine, fmtP, momCell } from './format.js';
import { poolCellHtml } from '../pool/model.js';
// ---- 通用弹层:款式下钻等全屏明细 ----
export function openModal(headHtml,innerHtml){
let m=document.getElementById('smModal');
if(!m){
m=document.createElement('div');m.id='smModal';m.className='modal-mask';
document.body.appendChild(m);
m.addEventListener('click',function(e){if(e.target===m)m.classList.remove('on');});
}
m.innerHTML='<div class="modal-box"><div class="modal-head"><div>'+headHtml
+'</div><span class="modal-x" onclick="document.getElementById(\'smModal\').classList.remove(\'on\')">✕ 关闭</span></div>'
+innerHtml+'</div>';
m.classList.add('on');
}
// ---- 点击商品主图放大显示(400×400)----
export function zoomImage(src,ri){
if(!src)return;
let m=document.getElementById('imgZoom');
if(!m){
m=document.createElement('div');m.id='imgZoom';m.className='imgzoom-mask';
m.innerHTML='<div class="imgzoom-box"><img alt="放大图"><div id="imgZoomAct" style="margin-top:12px;text-align:center"></div></div>';
document.body.appendChild(m);
m.addEventListener('click',function(){m.classList.remove('on');});
}
const img=m.querySelector('img');
const u=(String(src).indexOf('//')===0)?('https:'+src):src;
img.src=u;
// 款式名称列已去掉:下钻入口改为放大弹窗内的按钮(其他 Tab 的 .pic 无 data-ri 则不显示)
const act=document.getElementById('imgZoomAct');
if(act)act.innerHTML=(ri==null||ri==='')?''
:'<button style="padding:7px 16px;border:1px solid var(--line);border-radius:8px;background:#fff;'
+'font-size:13px;color:var(--tx);cursor:pointer" '
+'onclick="document.getElementById(\'imgZoom\').classList.remove(\'on\');showStyleDrill('+ri+')">下钻该款式全部商品 ▸</button>';
m.classList.add('on');
}
export function initImageZoom() {
// 事件委托:所有 .pic 主图点击放大(款式×价格带行内主图带 data-ri,可在放大弹窗内下钻)
document.addEventListener('click',function(e){
const el=e.target.closest&&e.target.closest('img.pic');
if(el){e.stopPropagation();zoomImage(el.getAttribute('src'),el.getAttribute('data-ri'));}
});
}
// ---- 款式下钻:展示该款式全部在榜商品(字段对齐生意参谋商品排行榜) ----
export function showStyleDrill(ri,band){
const row=((state.DATA.styleMatrix||{}).rows||[])[ri];
if(!row)return;
let range=null;
if(band)range=bandRange(band);
const its=(state.DATA.items||[]).filter(function(it){
if(it.style!==row.style)return false;
if(range)return +it.price>=range[0]&&+it.price<=range[1];
return true;
}).sort(function(a,b){return b.pay_amt-a.pay_amt;});
// 兜底:按价格带过滤后为空(口径/价格分档差异)→ 回落到该款式全部在榜商品,避免空弹窗
let fellBack=false;
let use=its;
if(!use.length&&band){
use=(state.DATA.items||[]).filter(function(it){return it.style===row.style;})
.sort(function(a,b){return b.pay_amt-a.pay_amt;});
fellBack=use.length>0;
}
if(!use.length){
openModal('<b>款式下钻</b>','<div class="empty">该款式暂无在榜商品明细</div>');
return;
}
const totAmt=use.reduce(function(s,it){return s+it.pay_amt;},0);
const totVol=use.reduce(function(s,it){return s+it.pay_item_cnt;},0);
const head='<b>款式下钻:'+esc(row.style)+(band&&!fellBack?' · 价格带 '+esc(band)+' 元':'')+'</b>'
+'<span style="font-size:12px;color:var(--tx2)"> '+use.length+' 个在榜商品 总销售额 ¥'+fmtN(totAmt)
+' 总成交量 '+fmtN(totVol)+'(数据源:'+esc(state.DATA.dbSource||'-')+' '+(state.DATA.summary&&state.DATA.summary.period||'')+')</span>';
let h='<div class="hint">按销售额降序'
+(band?(fellBack?';<b style="color:#b45309">该价格带内无商品,已回落到该款式全部在榜商品</b>':';仅含该价格带('+esc(band)+' 元)内的商品'):'')
+';排名=该商品在三级类目月榜名次;件单价=总成交额÷总成交量;环比/市占率为月榜口径;涨红跌绿。</div>'
+'<div class="tbl-scroll"><table>'
+'<tr><th>#</th><th>商品标题</th><th>店铺名称</th><th>品牌名称</th><th>近6个月排名趋势</th>'
+'<th class="num">总成交额</th><th class="num">市占率</th><th class="num">成交额月环比</th>'
+'<th class="num">总成交量</th><th class="num">成交量月环比</th><th class="num">件单价</th><th class="num">件单价月环比</th><th>商品</th><th>意向</th></tr>';
use.forEach(function(it,i){
// 件单价月环比 = (1+成交额环比)/(1+成交量环比)-1(件单价=额/量的恒等变形,免查上月快照)
let pm=null;
if(it.amt_mom!=null&&it.vol_mom!=null&&+it.vol_mom>-1&&+it.amt_mom>-1){
pm=(1+ +it.amt_mom)/(1+ +it.vol_mom)-1;
}
h+='<tr><td>'+(i+1)+'</td>'
+'<td style="max-width:330px;white-space:normal"><div style="display:flex;gap:8px;align-items:flex-start">'
+(it.pic_url?'<img class="pic" loading="lazy" src="'+(String(it.pic_url).startsWith('//')?'https:'+esc(it.pic_url):esc(it.pic_url))+'">':'')
+'<span>'+esc(it.item_title)+'</span></div></td>'
+'<td>'+esc(it.shop_name||'-')+'</td>'
+'<td>'+esc(it.brand||'-')+'</td>'
+'<td style="max-width:170px;white-space:normal">'+trendLine(it.trend6m)+'</td>'
+'<td class="num"><b>¥'+fmtN(it.pay_amt)+'</b></td>'
+'<td class="num">'+(it.market_share!=null?fmtP(+it.market_share):'-')+'</td>'
+'<td class="num">'+momCell(it.amt_mom)+'</td>'
+'<td class="num">'+fmtN(+it.pay_item_cnt)+'</td>'
+'<td class="num">'+momCell(it.vol_mom)+'</td>'
+'<td class="num">¥'+(it.price!=null?(+it.price).toFixed(0):'-')+'</td>'
+'<td class="num">'+momCell(pm)+'</td>'
+'<td>'+(it.detail_url?'<a class="rowlink" target="_blank" href="'+esc(String(it.detail_url).startsWith('//')?'https:'+it.detail_url:it.detail_url)+'">查看</a>':'-')+'</td>'
+'<td style="white-space:nowrap">'+poolCellHtml(it)+'</td>'
+'</tr>';
});
h+='</table></div>';
openModal(head,h);
}
export function showBandDrill(bandLabel){
const [lo,hi]=bandRange(bandLabel);
const its=(state.DATA.items||[]).filter(it=>+it.price>=lo&&+it.price<=hi)
.slice().sort((a,b)=>(+b.pay_amt||0)-(+a.pay_amt||0));
if(!its.length){openModal('<b>价格带下钻:'+esc(bandLabel)+' 元</b>','<div class="empty">该价格带暂无在榜商品明细</div>');return;}
let h='<div class="hint" style="margin-bottom:8px">共 '+its.length+' 个在榜商品 · 按销售额降序;'
+'市占率/环比为市场库月榜口径;件单价=总成交额÷总成交量,件单价月环比=(1+成交额环比)÷(1+成交量环比)−1;排名趋势仅飙升品有轨迹。红涨绿跌。</div>'
+'<div class="tbl-scroll"><table><tr><th>#</th><th>商品</th><th>店铺名称</th><th>品牌名称</th><th>近6个月排名趋势</th>'
+'<th class="num">总成交额</th><th class="num">市占率</th><th class="num">成交额月环比</th><th class="num">成交量</th>'
+'<th class="num">成交量月环比</th><th class="num">件单价</th><th class="num">件单价月环比</th><th>商品</th></tr>';
its.forEach((it,i)=>{
const pu=String(it.pic_url||'');
const pMom=(it.amt_mom!=null&&it.vol_mom!=null&&(1+(+it.vol_mom||0))!==0)
?((1+(+it.amt_mom||0))/(1+(+it.vol_mom||0))-1):null;
h+='<tr><td>'+(i+1)+'</td>'
+'<td style="max-width:300px;white-space:normal">'+(pu?'<img class="pic" loading="lazy" src="'+(pu.indexOf('//')===0?'https:'+esc(pu):esc(pu))+'" style="width:56px;height:56px;object-fit:cover;border-radius:5px;vertical-align:middle;margin-right:6px;cursor:zoom-in" onclick="zoomImage(this.getAttribute(\'src\'))">':'')
+'<b>'+esc(it.item_title)+'</b></td>'
+'<td>'+esc(it.shop_name||'-')+'</td>'
+'<td>'+esc(it.brand||'-')+'</td>'
+'<td style="white-space:nowrap">'+trendLine(it.trend6m)+'</td>'
+'<td class="num"><b>¥'+fmtN(+it.pay_amt||0)+'</b></td>'
+'<td class="num">'+(it.market_share!=null?fmtP(+it.market_share):'-')+'</td>'
+'<td class="num">'+momCell(it.amt_mom)+'</td>'
+'<td class="num">'+fmtN(+it.pay_item_cnt||0)+'</td>'
+'<td class="num">'+momCell(it.vol_mom)+'</td>'
+'<td class="num">¥'+(it.price!=null?(+it.price).toFixed(0):'-')+'</td>'
+'<td class="num">'+momCell(pMom)+'</td>'
+'<td>'+(it.detail_url?'<a href="'+esc(it.detail_url)+'" target="_blank" rel="noopener" style="color:var(--blue)">查看</a>':'-')+'</td></tr>';
});
h+='</table></div>';
openModal('<b>价格带下钻:'+esc(bandLabel)+' 元</b>('+its.length+' 个商品)',h);
}

View File

@ -0,0 +1,10 @@
export function bandRange(label){
const s=String(label||'');
let m=s.match(/^(\d+(?:\.\d+)?)\s*-\s*(\d+(?:\.\d+)?)$/);
if(m)return [parseFloat(m[1]),parseFloat(m[2])+0.99]; // 对齐服务端分桶上限(0-49 → ≤49.99)
m=s.match(/^(\d+(?:\.\d+)?)\s*\+$/);
if(m)return [parseFloat(m[1]),Infinity];
return [0,Infinity];
}

View File

@ -0,0 +1,7 @@
/** 入口注册实际视图,业务操作请求重绘,避免业务模块反向导入页面。 */
let views;
export function registerRenderers(renderers) { views = renderers; }
export function renderBoard() { return views.board(); }
export function renderImageMatch() { return views.imageMatch(); }

View File

@ -0,0 +1,19 @@
/** 单一状态源,页签与公共交互通过显式导入共享。 */
export const state = {
DATA: null,
TAB: 'capacity',
ITEM_SRC: 'all',
MATRIX_ID_RANGE: {min:'',max:''},
MATRIX_AMT_DIR: 'desc',
MATRIX_VIEW: 'style',
ME_NAME: '',
POOL: null,
POOL_Q: {selector:'',status:''},
SOAR_GROUPS: [],
CALC: null,
IM: {sig:null, sigD:null, sigs:{}, ver:'', rows:[], cands:[], pick:0,
err:'', busy:false, ready:false, prev:'', topN:48, hit:null,
imgData:'', judge:null}
};

Some files were not shown because too many files have changed in this diff Show More