Compare commits

...

2355 Commits

Author SHA1 Message Date
居居
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
居居
bfe3073aa1 feat(supply-chain): 人员信息总负责人支持多中心总监并列显示(如 子龙/妙玉) 2026-10-14 13:03:08 +08:00
WuYongJie
49ec0be34f feat(supply-chain): 日销任务改为每日 08:00 单次执行,未到数即告警并支持手动补跑 2026-10-14 12:28:45 +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
d5443e9f77 fix(product-archive): display complete column headers and preserve readable widths 2026-10-14 11:56:12 +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
fc50035d65 feat(owners): include growth center employees in business owner options 2026-10-14 11:35:12 +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
a917949a5e fix(supply-chain): 基础同步回退视图不存在的 item_type 列引用并修复失败告警原因为空 2026-10-14 10:39:04 +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
83f0fbee86 feat(supply-chain): 产品全量档案负责人治理与聚水潭式表头改造 2026-10-14 10:08:08 +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
fb1c74ae67 fix(diy): retry transient storage failures during catalog import 2026-10-14 09:20:36 +08:00
WuYongJie
20eaf3589c fix(diy): retry transient storage failures during catalog import 2026-10-14 09:20:00 +08:00
WuYongJie
ce66215764 fix(storage): handle Qiniu CommonJS SDK default export for DIY import 2026-10-14 08:59:22 +08:00
WuYongJie
d76d024d62 fix(storage): handle Qiniu CommonJS SDK default export for DIY import 2026-10-14 08:58:44 +08:00
WuYongJie
56548a731e Merge production main into shared baseline for SABC release
Preserve SABC storage-type and batch-search changes, logistics column resizing, and production-main employee-hours and route export compatibility. Verified 54 focused tests and type-check; no pending production migrations.
2026-10-13 16:24:15 +08:00
居居
252624708d Merge branch 'codex/xuxiake-sabc-sync-20260929' into codex/xuxiake-mainline 2026-10-13 10:15:47 +08:00
居居
7ae628f9f1 feat(supply-chain): SABC 明细新增「入库/代发」列,货号/商品编码/款式编码支持空格批量查询
- 明细与导出新增「入库/代发」,取值来源产品全量档案 storageType(入仓/代发)
- 已生成月份仍读快照;旧快照缺该字段时按 barcode 从档案补齐,无需重新核算
- 货号/商品编码/款式编码额外支持空格/制表符分隔(可直接粘贴 Excel 多行或多列),服务端最多取 200 个
- 入库/代发取值兼容字典值(INBOUND/DROPSHIP)与中文直填(入仓/入库/代发),产品全量档案列表同步显示
2026-10-13 10:15:34 +08:00
WuYongJie
f8b8959347 feat(diy): persist shared catalog and assets in database and object storage 2026-10-13 09:56:39 +08:00
WuYongJie
409e35e232 feat(diy): persist shared catalog and assets in database and object storage 2026-10-13 09:55:55 +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
ee033a1e08 feat(diy): add cabinet workbench with saved versions and BOM exports 2026-10-12 18:16:14 +08:00
WuYongJie
12de551328 feat(diy): add cabinet workbench with saved versions and BOM exports 2026-10-12 18:14:15 +08:00
WuYongJie
96f0373e79 feat(diy): add cabinet workbench with saved versions and BOM exports 2026-10-12 18:12:57 +08:00
WuYongJie
ecd11db3af docs: 删除已废弃的苹果风格视觉标准(验收只按白曜石工坊口径) 2026-10-12 14:05:53 +08:00
WuYongJie
b45b584871 chore(installation-demo): 移除安装演示V2试验链并保留成对迁移历史 2026-10-12 14:05:53 +08:00
WuYongJie
b237bbed7c feat(supply-chain): 产品全量档案定时任务通知——业绩快照每日简报与四任务失败兜底 2026-10-12 14:05:53 +08:00
WuYongJie
b8093f899e feat(supply-chain): 产品全量档案定时任务通知——业绩快照每日简报与四任务失败兜底 2026-10-12 14:05:18 +08:00
WuYongJie
f7811468f7 feat(supply-chain): 产品全量档案定时任务通知——业绩快照每日简报与四任务失败兜底 2026-10-12 14:05:06 +08:00
WuYongJie
92cd5f2903 docs: 删除已废弃的苹果风格视觉标准(验收只按白曜石工坊口径) 2026-10-12 11:08:32 +08:00
WuYongJie
e612936125 chore(installation-demo): 移除安装演示V2试验链并保留成对迁移历史 2026-10-12 11:08:32 +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
1de64d05f6 fix(supply-chain): keep daily sales notification fields in separate paragraphs 2026-10-12 10:47:05 +08:00
WuYongJie
553c237268 fix(supply-chain): keep daily sales notification fields in separate paragraphs 2026-10-12 10:47:05 +08:00
WuYongJie
5ca82d1033 fix(supply-chain): keep daily sales notification fields in separate paragraphs 2026-10-12 10:46:24 +08:00
WuYongJie
00dd1b2f89 fix(supply-chain): 日销通知改为纯文字
去掉新通知的卡片按钮字段,正文采用普通换行。
保留旧投递的幂等重试载荷,公共通知链路不变。
验证:相关25项测试、ESLint及diff检查通过。
2026-10-12 10:26:40 +08:00
WuYongJie
75dd248046 fix(supply-chain): 日销通知改为纯文字
去掉新通知的卡片按钮字段,正文采用普通换行。
保留旧投递的幂等重试载荷,公共通知链路不变。
验证:相关25项测试、ESLint及diff检查通过。
2026-10-12 10:26:40 +08:00
WuYongJie
81cffc020a fix(supply-chain): 日销通知改为纯文字
去掉新通知的卡片按钮字段,正文采用普通换行。
保留旧投递的幂等重试载荷,公共通知链路不变。
验证:相关25项测试、ESLint及diff检查通过。
2026-10-12 10:25:34 +08:00
WuYongJie
34e8426bb6 feat(store-account): display exact source platforms and filter before pagination 2026-10-12 10:24:17 +08:00
WuYongJie
a7554bebad feat(store-account): display exact source platforms and filter before pagination 2026-10-12 10:23:48 +08:00
WuYongJie
520706df0d feat(store-account): display exact source platforms and filter before pagination 2026-10-12 10:22:51 +08:00
WuYongJie
6a94f1b215 fix(supply-chain): 日销08点同步、失败补跑及管理员结果通知
北京时间08:00开始,未到数或失败按固定10分钟刻度补跑,成功当天停止源查询。
保留重启/PG失败记录和到数时间,正式任务定向徐霞客发送去重结果通知。
验证:31项定向测试、ESLint、全仓TypeScript、脚本语法与本地真实运行通过。
2026-10-12 10:05:44 +08:00
WuYongJie
9ca2f2d382 fix(supply-chain): 日销08点同步、失败补跑及管理员结果通知
北京时间08:00开始,未到数或失败按固定10分钟刻度补跑,成功当天停止源查询。
保留重启/PG失败记录和到数时间,正式任务定向徐霞客发送去重结果通知。
验证:31项定向测试、ESLint、全仓TypeScript、脚本语法与本地真实运行通过。
2026-10-12 10:04:42 +08:00
WuYongJie
c4f3063e88 fix(supply-chain): 日销08点同步、失败补跑及管理员结果通知
北京时间08:00开始,未到数或失败按固定10分钟刻度补跑,成功当天停止源查询。
保留重启/PG失败记录和到数时间,正式任务定向徐霞客发送去重结果通知。
验证:31项定向测试、ESLint、全仓TypeScript、脚本语法与本地真实运行通过。
2026-10-12 10:03:13 +08:00
WuYongJie
cb91d7a42f fix(intake): calibrate Gujia 2026 certificate print positions 2026-10-12 09:22:56 +08:00
WuYongJie
6edf13e974 fix(intake): calibrate Gujia 2026 certificate print positions 2026-10-12 09:22:20 +08:00
WuYongJie
4885e8fc75 fix(intake): calibrate Gujia 2026 certificate print positions 2026-10-12 09:21:26 +08:00
WuYongJie
ffa4290c4a feat(product-archive): 全量档案图片自动增量索引、换图失效与失败重试 2026-10-12 08:51:02 +08:00
WuYongJie
7b9fd60f6e feat(product-archive): 全量档案图片自动增量索引、换图失效与失败重试 2026-10-12 08:50:39 +08:00
WuYongJie
0ca07f8c5e feat(product-archive): 全量档案图片自动增量索引、换图失效与失败重试 2026-10-12 08:49:10 +08:00
WuYongJie
393f149c05 feat(product-archive): sync approved owner workflows daily 2026-10-10 16:14:24 +08:00
WuYongJie
50062a4c3d feat(product-archive): sync approved owner workflows daily 2026-10-10 16:13:53 +08:00
WuYongJie
9c2f5f6085 feat(product-archive): sync approved owner workflows daily 2026-10-10 16:13:25 +08:00
WuYongJie
abcffc424a fix(supply-chain): isolate permissions for every menu page 2026-10-10 14:31:38 +08:00
WuYongJie
9b7ab9e532 fix(supply-chain): isolate permissions for every menu page 2026-10-10 14:31:21 +08:00
apple
729eaf492d fix(logistics-arrival): 换货单不再把已换货的 0 数量行算成条码
内部单号 9627695(赔偿换货单)看板显示 X00005、SJ0061100N430V01 两个条码,
但聚水潭里 SJ0061100N430V01 是 qty=0 / item_status=Replaced 的"已换货"原始行,
实际发出的是手工换货替换品 X00005,看板应只显示 X00005。

- 条码匹配时跳过 qty<=0 的明细行(已换货/取消的原始行不产生条码);
- 明细查询补上 oi.qty 字段;
- 补单测:换货单只保留实际发出条码、0/负数行不计入,并把原有两条条码用例补上 qty。

实测(10-01~10-09 窗口):jst_OrderItem 2436 行、jst_qmcust_orderlist_item 3007 行
数量为 0 且全部是 Replaced;过滤后没有任何订单会变成无条码。

注:快照里的条码是生成时固化的,快照区间需重跑当天批次后才会更新。
任务名:物流到货签收时长-deepseek
2026-10-10 14:18:40 +08:00
apple
253c803be6 fix(logistics-arrival): 换货单不再把已换货的 0 数量行算成条码
内部单号 9627695(赔偿换货单)看板显示 X00005、SJ0061100N430V01 两个条码,
但聚水潭里 SJ0061100N430V01 是 qty=0 / item_status=Replaced 的"已换货"原始行,
实际发出的是手工换货替换品 X00005,看板应只显示 X00005。

- 条码匹配时跳过 qty<=0 的明细行(已换货/取消的原始行不产生条码);
- 明细查询补上 oi.qty 字段;
- 补单测:换货单只保留实际发出条码、0/负数行不计入,并把原有两条条码用例补上 qty。

实测(10-01~10-09 窗口):jst_OrderItem 2436 行、jst_qmcust_orderlist_item 3007 行
数量为 0 且全部是 Replaced;过滤后没有任何订单会变成无条码。

注:快照里的条码是生成时固化的,快照区间需重跑当天批次后才会更新。
任务名:物流到货签收时长-deepseek
2026-10-10 13:48:03 +08:00
apple
91eeb6402a fix(docs): 恢复生产部署说明中被覆盖的 SABC 章节并重放停产收口章节
上一次提交把 deploy 文档替换成旧版本,误删了「商品 SABC 月度核算」等主线内容;本次恢复主线版本并仅追加「停产需求确认超时收口」章节。
2026-10-10 13:21:18 +08:00
apple
f2480700ee chore(production-stop-demands): 补齐超时自动收口定时器与部署说明
- 新增 scripts/install-production-stop-confirmation-finalize-timer.sh:systemd service+timer,每 10 分钟检查一次(Asia/Shanghai),失败 5 分钟后自动重试,日志进 journald
- 生产部署入口说明新增「停产需求确认超时收口」章节(脚本、systemd 名称、调度、日志、启用步骤与授权边界)
2026-10-10 13:21:18 +08:00
apple
158995b71b fix(docs): 恢复生产部署说明中被覆盖的 SABC 章节并重放停产收口章节
上一次提交把 deploy 文档替换成旧版本,误删了「商品 SABC 月度核算」等主线内容;本次恢复主线版本并仅追加「停产需求确认超时收口」章节。
2026-10-10 12:14:38 +08:00
apple
26ebec4c40 chore(production-stop-demands): 补齐超时自动收口定时器与部署说明
- 新增 scripts/install-production-stop-confirmation-finalize-timer.sh:systemd service+timer,每 10 分钟检查一次(Asia/Shanghai),失败 5 分钟后自动重试,日志进 journald
- 生产部署入口说明新增「停产需求确认超时收口」章节(脚本、systemd 名称、调度、日志、启用步骤与授权边界)
2026-10-10 12:13:28 +08:00
apple
8270ae9b38 Merge branch 'codex/wuyongjie-production-stop-demands' into codex/xuxiake-mainline
feat(production-stop-demands): 停产需求管理全流程(提单→需求确认→走停产流程→流程结果同步)
- 按条码拆单提交;批量同意/不同意、批量走流程、批量修改停产原因
- 需求确认人与流程负责人分开配置(批量新增/删除/查询)
- 确认截止时间(15:00/16:00 规则)与超时默认同意;任一人不同意即整单驳回
- PLM 流程结果同步(拒绝→需求驳回;商品停产/退市→流程审批通过)与盘停建议配置

(cherry picked from commit 33555c7f6bc0881452a074b06c89b6caf911ca1d)
2026-10-10 11:41:51 +08:00
apple
33555c7f6b Merge branch 'codex/wuyongjie-production-stop-demands' into codex/xuxiake-mainline
feat(production-stop-demands): 停产需求管理全流程(提单→需求确认→走停产流程→流程结果同步)
- 按条码拆单提交;批量同意/不同意、批量走流程、批量修改停产原因
- 需求确认人与流程负责人分开配置(批量新增/删除/查询)
- 确认截止时间(15:00/16:00 规则)与超时默认同意;任一人不同意即整单驳回
- PLM 流程结果同步(拒绝→需求驳回;商品停产/退市→流程审批通过)与盘停建议配置
2026-10-10 11:28:48 +08:00
apple
737926de7e feat(production-stop-demands): 停产需求管理全流程(提单/确认/走流程/结果同步)
- 按条码拆单提交,支持批量同意/不同意、批量走流程、批量修改停产原因
- 需求确认人与流程负责人分别配置,支持批量新增/删除/查询
- 确认截止时间规则(当天15:00前当天16:00截止,否则次日16:00)与超时默认同意;任一人不同意即整单驳回
- PLM 流程结果同步:流程拒绝→需求驳回;商品状态变为停产/退市→流程审批通过
- 盘停建议配置:批量导入(模板/文件/三种模式)、表格式维护、提单按条码自动带出
- 列表支持列宽拖拽、表头排序、日期区间/需求人/采购筛选、需求状态流程步骤条与批量操作
2026-10-10 11:17:18 +08:00
WuYongJie
de7518bb16 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:09:51 +08:00
WuYongJie
cd8d5f2799 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:08:29 +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
apple
ce3658a33d fix(logistics-arrival): 条码不再并集同一物流单号上的其它订单
内部单号 9627695(线上单 351207604012)实际只有 X00005 和 SJ0061100N430V01
两项,看板却显示 4 个条码——多出来的 SJ0061100N430V01-5/-7 属于同一物流单号
75627908174893 上的另一张订单 9593499(合并发货)。

根因:buildBarcodeMap 把"物流单号"键的条码无条件并入,而一个物流单号可能合并
发货多张订单,于是一张单会串上别单的条码;同一张单还会因为分页里恰好有没有
那张邻单而显示不同条码。

- 条码匹配规则抽成纯函数 matchLogisticsArrivalSigningDurationBarcodes:
  先用内部单号/线上订单号/物流单号+付款时间匹配本单自己的明细,
  物流单号只在精确键一个都匹配不到时兜底;
- 补单测覆盖"合并发货不串单"和"精确键为空才兜底且去重"。

注:快照里的条码是生成时固化的,快照区间需重跑当天批次后才会更新。
任务名:物流到货签收时长-deepseek
2026-10-10 10:52:42 +08:00
apple
3679c247bf feat(logistics-arrival): 订单明细支持拖动调整各字段列宽
20 个字段原来只有统一的 max-width: 260px + 省略号,长文本(备注/留言/地址)
看不全也没法放大。现在每列表头右侧都有拖拽把手:

- 拖动调整列宽(72~640px),键盘 ←/→ 微调,双击把手恢复该列默认宽度;
- 列宽写进 localStorage,刷新/切页后保留;改过列宽后表头出现"恢复默认列宽"按钮;
- colgroup + table-layout: fixed,列宽所见即所得,表格总宽随列宽变化并横向滚动。

任务名:物流到货签收时长-deepseek
2026-10-10 10:12:36 +08:00
apple
7e1ca79dc0 fix(logistics-arrival): 变体条码回退基准条码,商品维度不再是未填写
聚水潭同款不同规格会给基准条码加 `-数字`(如 SJ0061100N430V01-5),
产品档案只登记基准条码,于是变体条码查不到档案、供应商/采购为空;
而订单明细行是按整单所有明细聚合的,只要单里有基准条码就有值——
同一张订单因此出现"商品维度未填写、订单明细有值"。

- 档案查询两步走:先精确匹配,未命中再用基准条码(去掉结尾 `-数字`)兜底;
- 基准条码可能与本批一起查询,两轮结果都参与回退;
- 新增 logisticsArrivalSigningDurationArchiveFallbackBarcodes 纯函数 + 单测。

注:快照里的 productItems 是生成时固化的,需重跑当天批次后分析看板才会带上兜底结果。
任务名:物流到货签收时长-deepseek
2026-10-10 10:06:10 +08:00
apple
f665a6d271 fix(logistics-arrival): 快照覆盖判定改用实际数据覆盖,修当天少算/导出缺单
看板/导出的快照覆盖判定此前只比对批次"声明窗口"(windowTo = 跑批当天),
但每天 03:00/05:30 跑批时当天还没签收完,数据只到跑批那一刻,于是把
"还没签收完的当天"误判成已覆盖:10-08 生成的批次只会算到 10-07(约 3.8 万),
当天签收的单(用户清单里那 5,820 条)整批缺失。

- 快照批次新增 dataThrough(该批真实最大签收时间),覆盖上限改为
  min(windowTo - 1, dataThrough 当天):跑批当天永远不完整,只信任到前一天;
  数据源本身滞后时再由 dataThrough 收窄。
- 生成任务逐片记录 dataThrough 并写回批次;旧批次缺值时按 windowTo - 1 兜底。
- 定时器默认 05:30 → 03:00;文档同步更新覆盖规则。
- migration: LogisticsArrivalSigningDurationSnapshotRun.dataThrough。

任务名:物流到货签收时长-deepseek

(cherry picked from commit eca96b3067d73bc01e54f72dd11dd7253b324b97)
2026-10-09 18:01:32 +08:00
居居
1a53aa31a3 feat(supply-chain): 各部门数据分类去掉「数据权限例外」,权限统一以用户管理后台为准
- 配置弹窗移除「数据权限例外」输入项,保存时不再写入该字段(旧配置下次保存即清空)
- 数据范围不再按人收窄:部门负责人/总负责人/普通账号一律看全部利润中心
- 设置结构、settings 接口返回与权限节点描述同步移除 dataScopeGrants
2026-10-09 17:47:34 +08:00
apple
eca96b3067 fix(logistics-arrival): 快照覆盖判定改用实际数据覆盖,修当天少算/导出缺单
看板/导出的快照覆盖判定此前只比对批次"声明窗口"(windowTo = 跑批当天),
但每天 03:00/05:30 跑批时当天还没签收完,数据只到跑批那一刻,于是把
"还没签收完的当天"误判成已覆盖:10-08 生成的批次只会算到 10-07(约 3.8 万),
当天签收的单(用户清单里那 5,820 条)整批缺失。

- 快照批次新增 dataThrough(该批真实最大签收时间),覆盖上限改为
  min(windowTo - 1, dataThrough 当天):跑批当天永远不完整,只信任到前一天;
  数据源本身滞后时再由 dataThrough 收窄。
- 生成任务逐片记录 dataThrough 并写回批次;旧批次缺值时按 windowTo - 1 兜底。
- 定时器默认 05:30 → 03:00;文档同步更新覆盖规则。
- migration: LogisticsArrivalSigningDurationSnapshotRun.dataThrough。

任务名:物流到货签收时长-deepseek
2026-10-09 17:16:12 +08:00
居居
1779d8a697 style(supply-chain): 各部门数据分类配置弹窗标题去掉「(仅管理员)」 2026-10-09 16:42:01 +08:00
居居
e95748dee9 Merge branch 'codex/xuxiake-sabc-sync-20260929' into codex/xuxiake-mainline 2026-10-09 16:30:54 +08:00
居居
530a35711f style(supply-chain): 各部门数据分类配置弹窗标题去掉「(仅管理员)」 2026-10-09 16:30:45 +08:00
居居
810f3a8a07 Merge branch 'codex/xuxiake-sabc-sync-20260929' into codex/xuxiake-mainline 2026-10-09 16:19:12 +08:00
居居
c4c370df2c feat(supply-chain): 各部门数据分类去掉「数据权限例外」,权限统一以用户管理后台为准
- 配置弹窗移除「数据权限例外」输入项,保存时不再写入该字段(旧配置下次保存即清空)
- 数据范围不再按人收窄:部门负责人/总负责人/普通账号一律看全部利润中心
- 设置结构、settings 接口返回与权限节点描述同步移除 dataScopeGrants
2026-10-09 16:18:56 +08:00
codex
1187a7ec56 fix(logistics-arrival): 经营维度点选不再自动滚到 09 订单明细
之前任何下钻(经营对象维度点选、经营总览异常卡片)都会把 09 订单明细 scrollIntoView 到视口顶部,
点平台/店铺等维度时会直接把页面翻到底,用户没法接着看其他维度、还得手动往上拉。

- 自动滚动只保留给经营总览的异常卡片(卡片写着"点击看明细")
- 经营对象维度(平台/店铺/发货仓/物流/采购/供应商/商品/签收日期)点选不再抢滚动条,
  下钻筛选照常生效,09 明细在原位置更新
- 测试拆成两条:维度点选不滚动(且仍触发 /drilldown)、异常卡片仍滚动到明细锚点
2026-10-09 15:48:00 +08:00
codex
e4fa4a2880 fix(logistics-arrival): 下钻/导出接口接受按字段分组的排除关键词
下钻和导出两个接口的 zod schema 还停留在 string | array,
而前端下发的是新的分组对象 { 字段: [词] },
于是点商品/店铺等对象下钻时 09 明细直接报"下钻条件格式不正确"(导出报"导出筛选条件格式不正确")。

- 两个接口的 excludeKeywords 都接受 对象 / `字段:词,词` 字符串 / 旧平铺数组
- 字符串形态改用 parseExcludeKeywords 解析(老链接按配置归位),不会把字段名当成关键词
- 新增两条路由单测:分组对象下钻/导出、字符串与平铺兼容、非法形态返回 400
2026-10-09 15:47:59 +08:00
居居
9b7d691ca9 feat(supply-chain): 商品SABC打标查看/导出默认全员开放,限制只在用户管理后台收紧
- 新增「商品SABC打标 · 商品明细」权限节点:默认全员可见+可导出,后台可单独关闭
- 侧边栏「商品SABC打标」入口与明细/经营总览/趋势/筛选候选/导出接口统一改用该节点
- 「各部门数据分类」同样默认全员可见+导出(移除商品企划的默认下载限制)
- 默认开放节点支持「显式关闭优先」:后台关掉某业务类型或个人例外,立即生效
- 「执行核算」仍由「商品SABC打标 · 执行核算」节点控制,未授权账号返回 403
- 补充权限映射与默认开放语义单测
2026-10-09 15:00:37 +08:00
居居
8d08176ab2 feat(supply-chain): 数据权限支持「全部」授权写法 2026-10-09 15:00:37 +08:00
居居
18eaaa4c5f feat(supply-chain): 部门数据配置改为独立权限节点,默认商品企划与管理员可用 2026-10-09 15:00:37 +08:00
居居
fb1d25b373 feat(supply-chain): 各部门数据分类改为独立权限模块(可见/下载分开配置) 2026-10-09 15:00:37 +08:00
居居
b2ca7bf7f1 Merge branch 'codex/xuxiake-sabc-sync-20260929' into codex/xuxiake-mainline 2026-10-09 14:42:53 +08:00
居居
e2254c50f5 feat(supply-chain): 各部门数据分类默认全员可见全部利润中心,只有「数据权限例外」才收窄
- 未配置数据权限例外时不再默认只看本人:所有人可见全部利润中心数据(查看/导出仍由用户管理后台节点控制,默认开放)
- 移除按人员信息表(部门负责人/总负责人)自动收窄到本事业部的旧规则
- 显式配置了范围的人仍按配置收窄,写「全部/所有/*/全公司」表示不受限
- 配置弹窗提示语同步说明「默认不需要配置」
2026-10-09 14:42:38 +08:00
居居
93293258d1 Merge branch 'codex/xuxiake-sabc-sync-20260929' into codex/xuxiake-mainline 2026-10-09 14:24:53 +08:00
居居
f86d27cae1 feat(supply-chain): 商品SABC打标查看/导出默认全员开放,限制只在用户管理后台收紧
- 新增「商品SABC打标 · 商品明细」权限节点:默认全员可见+可导出,后台可单独关闭
- 侧边栏「商品SABC打标」入口与明细/经营总览/趋势/筛选候选/导出接口统一改用该节点
- 「各部门数据分类」同样默认全员可见+导出(移除商品企划的默认下载限制)
- 默认开放节点支持「显式关闭优先」:后台关掉某业务类型或个人例外,立即生效
- 「执行核算」仍由「商品SABC打标 · 执行核算」节点控制,未授权账号返回 403
- 补充权限映射与默认开放语义单测
2026-10-09 14:24:45 +08:00
codex
d727421dca fix(logistics-arrival): 经营维度点选不再自动滚到 09 订单明细
之前任何下钻(经营对象维度点选、经营总览异常卡片)都会把 09 订单明细 scrollIntoView 到视口顶部,
点平台/店铺等维度时会直接把页面翻到底,用户没法接着看其他维度、还得手动往上拉。

- 自动滚动只保留给经营总览的异常卡片(卡片写着"点击看明细")
- 经营对象维度(平台/店铺/发货仓/物流/采购/供应商/商品/签收日期)点选不再抢滚动条,
  下钻筛选照常生效,09 明细在原位置更新
- 测试拆成两条:维度点选不滚动(且仍触发 /drilldown)、异常卡片仍滚动到明细锚点
2026-10-09 14:21:08 +08:00
居居
f94c86bcc8 Merge branch 'codex/xuxiake-sabc-sync-20260929' into codex/xuxiake-mainline
部门数据分类数据权限支持「全部」授权写法。
2026-10-09 13:08:24 +08:00
居居
5fdeea9a51 feat(supply-chain): 数据权限支持「全部」授权写法 2026-10-09 13:07:36 +08:00
居居
5a00ad317d Merge branch 'codex/xuxiake-sabc-sync-20260929' into codex/xuxiake-mainline
部门数据配置改为独立权限节点(默认商品企划 + 管理员)。
2026-10-09 11:27:32 +08:00
居居
cf5f3b2d49 feat(supply-chain): 部门数据配置改为独立权限节点,默认商品企划与管理员可用 2026-10-09 11:27:01 +08:00
居居
28e972f663 Merge branch 'codex/xuxiake-sabc-sync-20260929' into codex/xuxiake-mainline
各部门数据分类改为独立权限模块(可见/下载分开配置),无权限自动隐藏页签。
2026-10-09 10:26:28 +08:00
居居
ca08d89946 feat(supply-chain): 各部门数据分类改为独立权限模块(可见/下载分开配置) 2026-10-09 10:25:36 +08:00
WuYongJie
bffa3ec1a6 fix(intake): enable Gujia 2026 certificate template 2026-10-09 10:09:37 +08:00
WuYongJie
3fd4c0fac0 Merge Gujia 2026 certificate enablement into shared mainline 2026-10-09 10:09:06 +08:00
WuYongJie
ee95abd0b0 fix(intake): enable Gujia 2026 certificate template 2026-10-09 10:03:34 +08:00
WuYongJie
6989354a42 feat(supply-chain): 档案设计师绑定新品流程结构设计师(只填空值,以新品流程表为准) 2026-10-09 10:02:26 +08:00
WuYongJie
8a4e0ac797 feat(supply-chain): 档案设计师绑定新品流程结构设计师(只填空值,以新品流程表为准) 2026-10-09 10:02:18 +08:00
WuYongJie
afe9b806d5 feat(supply-chain): 档案设计师绑定新品流程结构设计师(只填空值,以新品流程表为准) 2026-10-09 10:01:58 +08:00
WuYongJie
6d3891fc44 style(layout): 产品开发侧栏对齐供应链 224px,修复系统切换器「产品开发」截断
侧栏变量 --layout-app-sidebar-width 11rem→14rem(浅色/深色两处),切换器簇/按钮随之与供应链逐项一致(224/192/144);切换器簇改 min-width、触发器 flex-1→flex-auto 作为防截断兜底。已实测四主页面无挤压无横向溢出。
2026-10-09 09:39:26 +08:00
WuYongJie
4cd07bdda5 Merge origin/codex/xuxiake-mainline into codex/local-3001-integration (sync before sidebar alignment push) 2026-10-09 09:39:06 +08:00
WuYongJie
8617493f8a style(layout): 产品开发侧栏对齐供应链 224px,修复系统切换器「产品开发」截断
侧栏变量 --layout-app-sidebar-width 11rem→14rem(浅色/深色两处),切换器簇/按钮随之与供应链逐项一致(224/192/144);切换器簇改 min-width、触发器 flex-1→flex-auto 作为防截断兜底。已实测四主页面无挤压无横向溢出。
2026-10-09 09:38:44 +08:00
WuYongJie
c3b20aace6 fix(supply-chain): 运营负责人改为一次性定标冻结(摘除跟随最新销售的每日翻转) 2026-10-09 09:00:53 +08:00
WuYongJie
5e842eeeb8 fix(supply-chain): 运营负责人改为一次性定标冻结(摘除跟随最新销售的每日翻转) 2026-10-09 09:00:46 +08:00
WuYongJie
b5dc4a1b1b fix(supply-chain): 运营负责人改为一次性定标冻结(摘除跟随最新销售的每日翻转) 2026-10-09 09:00:12 +08:00
WuYongJie
139aec0e70 feat(oa-board): 出图进度页改为美术设计申请源数据视图
一行=一条流程实例,列=流程实例ID/编号/标题/状态/时间+128 个表单原字段(按全量填写率排序,与源库导出 xlsx 同口径)。新快照表 oa_art_flow_raw_snapshot(关键筛选列+fields_json 全字段清洗值)+ oa_art_flow_raw_meta 字段顺序;同步脚本入 timer 链;API /api/intake/oa-board/art-flow-raw。旧 SKU 维度视图从页面移除,后端服务与导出消费方保留。
2026-10-08 17:24:06 +08:00
WuYongJie
1ba16cbc6b Merge origin/codex/xuxiake-mainline into codex/local-3001-integration (sync before art-flow raw view push) 2026-10-08 17:23:44 +08:00
WuYongJie
eaf11faea1 feat(oa-board): 出图进度页改为美术设计申请源数据视图
一行=一条流程实例,列=流程实例ID/编号/标题/状态/时间+128 个表单原字段(按全量填写率排序,与源库导出 xlsx 同口径)。新快照表 oa_art_flow_raw_snapshot(关键筛选列+fields_json 全字段清洗值)+ oa_art_flow_raw_meta 字段顺序;同步脚本入 timer 链;API /api/intake/oa-board/art-flow-raw。旧 SKU 维度视图从页面移除,后端服务与导出消费方保留。
2026-10-08 17:23:18 +08:00
WuYongJie
13b81e5822 feat(supply-chain): 新增替换款规则每日同步定时器安装脚本
SupplyChainProductArchiveReplacementRule 的上游是 rpt.pm_replacementManage,
此前只有 local-sync-archive.mjs 会调用 sync-product-replacement-rules.ts,
正式机没有任何定时任务,表建好后仍然是空的,SABC 的「同款商品编码」也就一直是空。

新增 install-product-replacement-rule-sync-timer.sh,按仓库既有 install-*-timer.sh
同款写法安装 furniture-product-replacement-rule-sync.{service,timer},
默认每天 07:40(Asia/Shanghai)同步一次,早于 09:00 的 SABC 打标日结。
脚本仅安装定时任务,改动生产数据由 sync-product-replacement-rules.ts 自己完成。
2026-10-08 16:43:58 +08:00
WuYongJie
bb777d4880 feat(supply-chain): 新增替换款规则每日同步定时器安装脚本
SupplyChainProductArchiveReplacementRule 的上游是 rpt.pm_replacementManage,
此前只有 local-sync-archive.mjs 会调用 sync-product-replacement-rules.ts,
正式机没有任何定时任务,表建好后仍然是空的,SABC 的「同款商品编码」也就一直是空。

新增 install-product-replacement-rule-sync-timer.sh,按仓库既有 install-*-timer.sh
同款写法安装 furniture-product-replacement-rule-sync.{service,timer},
默认每天 07:40(Asia/Shanghai)同步一次,早于 09:00 的 SABC 打标日结。
脚本仅安装定时任务,改动生产数据由 sync-product-replacement-rules.ts 自己完成。
2026-10-08 16:43:37 +08:00
居居
1bea953315 fix(supply-chain): 补齐替换规则表迁移(正式环境缺表导致同款商品编码全为空) 2026-10-08 16:30:35 +08:00
WuYongJie
03741083fa feat(supply-chain): 产品全量档案产品标签每日跟随SABC打标结果(有变化才更新) 2026-10-08 16:28:31 +08:00
WuYongJie
c23f616003 feat(supply-chain): 产品全量档案产品标签每日跟随SABC打标结果(有变化才更新) 2026-10-08 16:28:14 +08:00
WuYongJie
a3e236e986 feat(supply-chain): 产品全量档案产品标签每日跟随SABC打标结果(有变化才更新) 2026-10-08 16:26:54 +08:00
居居
9baee61769 Merge branch 'codex/xuxiake-sabc-sync-20260929' into codex/xuxiake-mainline
补齐 SupplyChainProductArchiveReplacementRule 迁移(正式环境缺表 → 同款商品编码全为空)。
2026-10-08 16:20:35 +08:00
居居
64a820a998 fix(supply-chain): 补齐替换规则表迁移(正式环境缺表导致同款商品编码全为空) 2026-10-08 16:20:27 +08:00
WuYongJie
c81b696a62 fix(supply-chain): 店铺账号列筛选支持全选后排除个别项(exclude 换算正面清单+筛选值截断上限提升) 2026-10-08 14:23:57 +08:00
WuYongJie
f13ed3d3fb fix(supply-chain): 店铺账号列筛选支持全选后排除个别项(exclude 换算正面清单+筛选值截断上限提升) 2026-10-08 14:23:53 +08:00
WuYongJie
ad0b094af1 fix(supply-chain): 店铺账号列筛选支持全选后排除个别项(exclude 换算正面清单+筛选值截断上限提升) 2026-10-08 14:23:02 +08:00
WuYongJie
2925818a75 feat(supply-chain): 店铺账号三级负责人离职改为文字后缀(已离职)并支持任一列命中筛选 2026-10-08 14:02:06 +08:00
WuYongJie
b7dd8ac490 feat(supply-chain): 店铺账号三级负责人离职改为文字后缀(已离职)并支持任一列命中筛选 2026-10-08 14:02:03 +08:00
WuYongJie
b295e128eb feat(supply-chain): 店铺账号三级负责人离职改为文字后缀(已离职)并支持任一列命中筛选 2026-10-08 14:01:43 +08:00
居居
8a3f32c5a2 fix(supply-chain): 修正月末创建时间边界,避免最后一天16点后创建的商品整月漏收录 2026-10-08 13:42:53 +08:00
WuYongJie
da1872c59d feat(supply-chain): 店铺账号离职负责人红色标注与负责人状态筛选(自定义下拉) 2026-10-08 12:59:32 +08:00
WuYongJie
4e594a4eee feat(supply-chain): 店铺账号离职负责人红色标注与负责人状态筛选(自定义下拉) 2026-10-08 12:59:23 +08:00
WuYongJie
ba1209deb3 feat(supply-chain): 店铺账号离职负责人红色标注与负责人状态筛选(自定义下拉) 2026-10-08 12:59:04 +08:00
WuYongJie
8e38894a2b fix(supply-chain): 全量档案运营层级匹配加固(不可见字符清洗+档案已有运营负责人兜底) 2026-10-08 12:01:59 +08:00
WuYongJie
605fb1347b fix(supply-chain): 全量档案运营层级匹配加固(不可见字符清洗+档案已有运营负责人兜底) 2026-10-08 12:01:38 +08:00
WuYongJie
bcfadc1e3f fix(supply-chain): 全量档案运营层级匹配加固(不可见字符清洗+档案已有运营负责人兜底) 2026-10-08 12:01:11 +08:00
WuYongJie
867b5bf897 feat(supply-chain): 店铺授权提醒每日定时推送(11:00)+ 负责人不在内部整店跳过 2026-10-08 11:36:30 +08:00
WuYongJie
cde4d59afb feat(supply-chain): 店铺授权提醒每日定时推送(11:00)+ 负责人不在内部整店跳过 2026-10-08 11:36:17 +08:00
WuYongJie
7979671a3d feat(supply-chain): 店铺授权提醒每日定时推送(11:00)+ 负责人不在内部整店跳过 2026-10-08 11:36:02 +08:00
WuYongJie
3a10aa3d9f feat(oa-board): 新品-视频进度(内容部)看板,钉钉内容视频流程独立接入
独立快照表 oa_video_flow_board_snapshot + 双驱动同步脚本 + 分页筛选接口 + 新 Tab;不进新品主链项目聚合/总状态/时效。阶段推进按表单字段推断(编导→摄像→剪辑→封面→验收/评分),节点名清单到位后可升级 activityId 映射。附数据字典 docs/数据字典/13-内容视频流程看板.md 供下游只读接入。
2026-10-08 11:22:46 +08:00
WuYongJie
9996ee83b2 Merge origin/codex/xuxiake-mainline into codex/local-3001-integration (sync before video-flow board push) 2026-10-08 11:22:14 +08:00
WuYongJie
af378a9d00 feat(oa-board): 新品-视频进度(内容部)看板,钉钉内容视频流程独立接入
独立快照表 oa_video_flow_board_snapshot + 双驱动同步脚本 + 分页筛选接口 + 新 Tab;不进新品主链项目聚合/总状态/时效。阶段推进按表单字段推断(编导→摄像→剪辑→封面→验收/评分),节点名清单到位后可升级 activityId 映射。附数据字典 docs/数据字典/13-内容视频流程看板.md 供下游只读接入。
2026-10-08 11:20:48 +08:00
codex
e6241481aa fix(logistics-arrival): 下钻/导出接口接受按字段分组的排除关键词
下钻和导出两个接口的 zod schema 还停留在 string | array,
而前端下发的是新的分组对象 { 字段: [词] },
于是点商品/店铺等对象下钻时 09 明细直接报"下钻条件格式不正确"(导出报"导出筛选条件格式不正确")。

- 两个接口的 excludeKeywords 都接受 对象 / `字段:词,词` 字符串 / 旧平铺数组
- 字符串形态改用 parseExcludeKeywords 解析(老链接按配置归位),不会把字段名当成关键词
- 新增两条路由单测:分组对象下钻/导出、字符串与平铺兼容、非法形态返回 400
2026-10-08 11:16:57 +08:00
WuYongJie
12feabd17f feat(supply-chain): 店铺账号授权提醒推送(正常+未授权店铺推送给运营负责人,离职顺延) 2026-10-08 11:05:56 +08:00
WuYongJie
24e403b183 feat(supply-chain): 店铺账号授权提醒推送(正常+未授权店铺推送给运营负责人,离职顺延) 2026-10-08 11:05:35 +08:00
WuYongJie
2342c53704 feat(supply-chain): 店铺账号授权提醒推送(正常+未授权店铺推送给运营负责人,离职顺延) 2026-10-08 10:59:54 +08:00
居居
246313c45a Merge branch 'codex/xuxiake-sabc-sync-20260929' into codex/xuxiake-mainline
修正商品SABC打标月末创建时间边界:最后一天 16:00 之后创建的商品不再被整月漏收录。
2026-10-08 09:27:37 +08:00
居居
838dcce410 fix(supply-chain): 修正月末创建时间边界,避免最后一天16点后创建的商品整月漏收录 2026-10-08 09:27:27 +08:00
WuYongJie
2528229c08 fix(supply-chain): 全量档案表宽固定为列宽之和,修复滚动时横向滚动条跳动 2026-10-08 09:27:05 +08:00
WuYongJie
91af9c5284 fix(supply-chain): 全量档案表宽固定为列宽之和,修复滚动时横向滚动条跳动 2026-10-08 09:26:56 +08:00
WuYongJie
f3f3bfa08b fix(supply-chain): 全量档案表宽固定为列宽之和,修复滚动时横向滚动条跳动 2026-10-08 09:26:38 +08:00
WuYongJie
685f3cc756 perf(supply-chain): 产品全量档案表体行虚拟化(视口窗口渲染+占位行) 2026-10-08 08:50:19 +08:00
WuYongJie
17474f0d85 perf(supply-chain): 产品全量档案表体行虚拟化(视口窗口渲染+占位行) 2026-10-08 08:50:10 +08:00
WuYongJie
174b1eec25 perf(supply-chain): 产品全量档案表体行虚拟化(视口窗口渲染+占位行) 2026-10-08 08:49:52 +08:00
WuYongJie
6363ed87b7 style(layout): 系统切换器灰底与下拉面板右边缘对齐左侧栏 2026-10-07 17:58:59 +08:00
WuYongJie
173c594c24 style(supply-chain): 物流到货签收时长看板移除顶部表头区块 2026-10-07 17:58:59 +08:00
WuYongJie
62f693223f style(supply-chain): 移除多处页面标题/说明块与冗余计数文本(工具条保持右对齐) 2026-10-07 17:58:59 +08:00
WuYongJie
bc79cd6321 style(layout): 系统切换器灰底与下拉面板右边缘对齐左侧栏 2026-10-07 17:58:56 +08:00
WuYongJie
48a8de5d77 style(supply-chain): 物流到货签收时长看板移除顶部表头区块 2026-10-07 17:58:56 +08:00
WuYongJie
bd0a5c81f3 style(supply-chain): 移除多处页面标题/说明块与冗余计数文本(工具条保持右对齐) 2026-10-07 17:58:56 +08:00
WuYongJie
5394319205 style(layout): 系统切换器灰底与下拉面板右边缘对齐左侧栏 2026-10-07 17:58:43 +08:00
WuYongJie
5779dcf5be style(supply-chain): 物流到货签收时长看板移除顶部表头区块 2026-10-07 17:58:42 +08:00
WuYongJie
97fffc4f3d style(supply-chain): 移除多处页面标题/说明块与冗余计数文本(工具条保持右对齐) 2026-10-07 17:58:42 +08:00
WuYongJie
5b8e1945af perf(supply-chain): 列设置关闭面板改为非阻塞过渡,修复整表同步重渲染卡顿 2026-10-07 17:33:36 +08:00
WuYongJie
45ba5c1005 perf(supply-chain): 列设置关闭面板改为非阻塞过渡,修复整表同步重渲染卡顿 2026-10-07 17:33:29 +08:00
WuYongJie
70fb47f529 perf(supply-chain): 列设置关闭面板改为非阻塞过渡,修复整表同步重渲染卡顿 2026-10-07 17:33:15 +08:00
WuYongJie
d5762b3e12 style(supply-chain): 产品全量档案工具栏移除标题与字段统计文案 2026-10-07 17:19:28 +08:00
WuYongJie
e54976df03 perf(supply-chain): 产品全量档案按页聚合+缓存+候选项SQL化(含SWR后台刷新与全选提速) 2026-10-07 17:19:27 +08:00
WuYongJie
f14d796056 style(supply-chain): 产品全量档案工具栏移除标题与字段统计文案 2026-10-07 17:19:03 +08:00
WuYongJie
0a0b925d99 perf(supply-chain): 产品全量档案按页聚合+缓存+候选项SQL化(含SWR后台刷新与全选提速) 2026-10-07 17:18:44 +08:00
WuYongJie
1f8e8bab74 style(supply-chain): 产品全量档案工具栏移除标题与字段统计文案 2026-10-07 17:18:26 +08:00
WuYongJie
1b3498e785 perf(supply-chain): 产品全量档案按页聚合+缓存+候选项SQL化(含SWR后台刷新与全选提速) 2026-10-07 17:18:25 +08:00
codex
ed865a3c2d fix(logistics-arrival): 修复订单明细"每页条数"选了没反应
BoardPagination 的下拉框在 onChange 里同时调了 onPageSizeChange(新值) 和 onPageChange(1)。
服务端分页(09 订单明细未下钻)这两次都会 router.push,而后一次是拿调用方当前的 filters
(pageSize 还是旧值)拼的 URL,把前一次覆盖掉——所以选 100/200 后又被拉回 50 条/页。

- BoardPagination 只通知"每页条数变了",回到第 1 页由调用方负责
- 本地分页(经营对象各维度表、发货仓/物流公司排名)在 setState 里带 page: 1
- 服务端分页继续用 href(1, { ...filters, pageSize })
- OrderDetailsBoard 测试补回归断言:切换每页条数只触发一次跳转
2026-10-07 15:26:12 +08:00
WuYongJie
1aef696ae6 chore(deploy): 生产 heap 上限 768MB 提升到 2048MB(缓解看板等重查询导致的 OOM 重启) 2026-10-07 15:26:11 +08:00
WuYongJie
b9a9d4b426 chore(deploy): 生产 heap 上限 768MB 提升到 2048MB(缓解看板等重查询导致的 OOM 重启) 2026-10-07 15:26:09 +08:00
WuYongJie
4fc857f1bd Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/local-3001-integration 2026-10-07 15:25:56 +08:00
WuYongJie
c1dbfaa145 chore(deploy): 生产 heap 上限 768MB 提升到 2048MB(缓解看板等重查询导致的 OOM 重启) 2026-10-07 15:25:55 +08:00
codex
debdfb454b fix(logistics-arrival): 修复订单明细"每页条数"选了没反应
BoardPagination 的下拉框在 onChange 里同时调了 onPageSizeChange(新值) 和 onPageChange(1)。
服务端分页(09 订单明细未下钻)这两次都会 router.push,而后一次是拿调用方当前的 filters
(pageSize 还是旧值)拼的 URL,把前一次覆盖掉——所以选 100/200 后又被拉回 50 条/页。

- BoardPagination 只通知"每页条数变了",回到第 1 页由调用方负责
- 本地分页(经营对象各维度表、发货仓/物流公司排名)在 setState 里带 page: 1
- 服务端分页继续用 href(1, { ...filters, pageSize })
- OrderDetailsBoard 测试补回归断言:切换每页条数只触发一次跳转
2026-10-07 14:35:46 +08:00
居居
255130b31f fix(supply-chain): 修正导出 dense 选项的类型声明(xlsx 0.18 类型未含 dense) 2026-10-07 14:28:52 +08:00
居居
5367bc4d27 perf(supply-chain): SABC 导出改 dense 表格+结果缓存+串行生成,降低内存峰值并支持秒级重试 2026-10-07 14:28:52 +08:00
WuYongJie
dfdfe019f2 Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/local-3001-integration 2026-10-07 14:27:58 +08:00
WuYongJie
5f31eca7ac fix(build): 外置 mssql,修复正式构建下 mssql 参数类型校验失败(物流看板等 mssql 功能) 2026-10-07 14:10:08 +08:00
WuYongJie
cb3e973bf6 fix(build): 外置 mssql,修复正式构建下 mssql 参数类型校验失败(物流看板等 mssql 功能) 2026-10-07 14:09:58 +08:00
WuYongJie
855cce7034 fix(build): 外置 mssql,修复正式构建下 mssql 参数类型校验失败(物流看板等 mssql 功能) 2026-10-07 14:09:47 +08:00
居居
ed893bd598 Merge branch 'codex/xuxiake-sabc-sync-20260929' into codex/xuxiake-mainline
修正 SABC 导出 dense 选项类型声明。
2026-10-07 14:08:24 +08:00
居居
3d64e3e332 fix(supply-chain): 修正导出 dense 选项的类型声明(xlsx 0.18 类型未含 dense) 2026-10-07 14:08:23 +08:00
居居
60ea2f923b Merge branch 'codex/xuxiake-sabc-sync-20260929' into codex/xuxiake-mainline
商品SABC打标导出:dense 表格 + 压缩输出 + 结果缓存 + 串行生成,降低 502/超时概率。
2026-10-07 14:00:21 +08:00
居居
faa60655f7 perf(supply-chain): SABC 导出改 dense 表格+结果缓存+串行生成,降低内存峰值并支持秒级重试 2026-10-07 14:00:13 +08:00
WuYongJie
0498a8f2e6 Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/local-3001-integration 2026-10-07 13:33:45 +08:00
居居
47f446bf62 perf(supply-chain): 导出压缩输出(8月明细 64MB→17MB)并补充导出耗时日志与错误详情 2026-10-07 13:04:59 +08:00
居居
14cb9939ad Merge branch 'codex/xuxiake-sabc-sync-20260929' into codex/xuxiake-mainline
商品SABC打标导出改为压缩输出(8 月明细 64MB → 17MB),并补充导出耗时日志与失败原因提示。
2026-10-07 12:00:26 +08:00
居居
9d8b75ca4d perf(supply-chain): 导出压缩输出(8月明细 64MB→17MB)并补充导出耗时日志与错误详情 2026-10-07 12:00:19 +08:00
codex
1b49125ffa feat(logistics-arrival): 重置 / 恢复默认 / 清空排除 三个按钮语义拆开
过去三个按钮都只是"改输入、等用户点查询",而且重置只清日期+关键词、另两个只改排除词,
最终看起来结果一样。现在各自一件事,点下去立即查询:

- 重置:签收日期、关键词、排除关键词全部清空 → 没有任何筛选条件
  (空日期仍由服务端兜底成本月 1 号~昨日;新增 dateRangeCleared 标记,
   让输入框保持空白而不是被兜底值回填)
- 恢复默认:签收日期回到本月 1 号~昨日(显式填回输入框)+ 排除关键词恢复系统默认配置
- 清空排除:签收日期回到本月 1 号~昨日 + 排除关键词置空

配套:`logisticsArrivalSigningDurationDefaultRange()` 从过滤器规范化里抽出来共用(单一来源)、
按钮 hover 说明 + 筛选栏底部图例、去掉旧的"填好后点查询"提示、补视图层测试。
2026-10-07 11:46:00 +08:00
codex
787fcb67bb perf(logistics-arrival): 看板冷启动从 7 秒降到 2 秒出头
整区间总览/环节目标原来 SELECT 全列,PG 只能对 900MB 堆表做 Seq Scan
(28 天区间 read=106409 页、6.3s);percentile_cont 又把 14 列宽的输入行
整行送进排序(两个分位数 4.3s,环节分位数叠加到分钟级);CTE 里的
CASE+EXTRACT 还会在每处引用被重复内联。

- 按用途收窄列集合:时间戳 / 时间戳+维度列,并新增覆盖索引
  (snapshotDate, signTime) INCLUDE (payTime, sendTime, fetchTime,
  warehouseName, platformName, shopName, logisticCompanyName) → Index Only Scan
- 分位数按列拆成独立语句、只投影一个浮点列,与计数语句并行下发
- measured / durations 改 AS MATERIALIZED,时长表达式只算一次
- 环节目标(上月平均 × 98%)接入 10 分钟内存缓存

实测(本机快照 648,689 行、冷启动):
28 天 7.3s→2.3s、10 天 4.4s→0.4s、2 天 1.7s→0.3s;
浏览器首屏出数 ~7-10s→2.7s。2 天区间与 JS 现算逐项对账一致。
2026-10-07 11:46:00 +08:00
codex
83d10ec9e7 fix(logistics-arrival): 修复排除关键词弹窗透明与保存报格式错误
- globals.css:在 :root 与 [data-theme='dark'] 补齐 --sc-* 主题别名。
  弹窗通过 createPortal 挂在 document.body 上,位于 .sc-layout 之外,
  之前取不到 --sc-surface/--sc-border 等变量,正式环境里表现为面板透明、
  边框与分隔线丢失(本地开发因为另一分支已临时补过这段而看不出来)。
- settings 接口:接受按字段分组的排除关键词对象(同时兼容历史平铺数组与字符串),
  修复管理员在选择器里新增关键词时报"排除关键词格式不正确"。
- 补充 settings 路由单测:分组对象落库、平铺数组兼容、非法格式返回 400。
2026-10-07 11:46:00 +08:00
WuYongJie
9a2bff07ab Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/local-3001-integration 2026-10-07 11:39:58 +08:00
WuYongJie
ff722ab12e chore(installation-demo-v2): 合并主线前落盘本地在途 V2 模型(仅 schema.prisma) 2026-10-07 11:39:55 +08:00
codex
7620e4bcb6 feat(logistics-arrival): 重置 / 恢复默认 / 清空排除 三个按钮语义拆开
过去三个按钮都只是"改输入、等用户点查询",而且重置只清日期+关键词、另两个只改排除词,
最终看起来结果一样。现在各自一件事,点下去立即查询:

- 重置:签收日期、关键词、排除关键词全部清空 → 没有任何筛选条件
  (空日期仍由服务端兜底成本月 1 号~昨日;新增 dateRangeCleared 标记,
   让输入框保持空白而不是被兜底值回填)
- 恢复默认:签收日期回到本月 1 号~昨日(显式填回输入框)+ 排除关键词恢复系统默认配置
- 清空排除:签收日期回到本月 1 号~昨日 + 排除关键词置空

配套:`logisticsArrivalSigningDurationDefaultRange()` 从过滤器规范化里抽出来共用(单一来源)、
按钮 hover 说明 + 筛选栏底部图例、去掉旧的"填好后点查询"提示、补视图层测试。
2026-10-07 11:05:35 +08:00
居居
bc10af7b62 feat(supply-chain): 商品SABC打标执行核算改由权限配置节点控制 2026-10-07 10:58:34 +08:00
居居
9f63e51702 Merge branch 'codex/xuxiake-sabc-sync-20260929' into codex/xuxiake-mainline
商品SABC打标「执行核算」改由权限配置节点(商品SABC打标 · 执行核算)控制,前端按钮与接口同步;清理未随提交带上的 WorkflowMonitorView 引用。
2026-10-07 10:01:28 +08:00
居居
e57604ac81 feat(supply-chain): 商品SABC打标执行核算改由权限配置节点控制 2026-10-07 10:01:19 +08:00
WuYongJie
c981ff3bb0 以图搜图粘贴与放大体验修复;部署脚本补齐 onnxruntime 原生库
- ProductArchiveImageSearchDialog:弹窗打开期间监听 document 级 paste,页面任意位置 Ctrl+V 直接粘贴图片(点击上传区仍是文件选择);搜索结果小图可点击,复用 ProductArchiveImagePreview 放大核对
- yydd-prod-release-guards.sh:prepare_standalone_runtime 补齐 Turbopack standalone 产物遗漏的 onnxruntime-node 原生二进制(libonnxruntime.so.*),根治部署后本地图索层加载失败;符号链接场景自动跳过
2026-10-07 09:57:43 +08:00
WuYongJie
8370ef2ddc 以图搜图粘贴与放大体验修复;部署脚本补齐 onnxruntime 原生库
- ProductArchiveImageSearchDialog:弹窗打开期间监听 document 级 paste,页面任意位置 Ctrl+V 直接粘贴图片(点击上传区仍是文件选择);搜索结果小图可点击,复用 ProductArchiveImagePreview 放大核对
- yydd-prod-release-guards.sh:prepare_standalone_runtime 补齐 Turbopack standalone 产物遗漏的 onnxruntime-node 原生二进制(libonnxruntime.so.*),根治部署后本地图索层加载失败;符号链接场景自动跳过
2026-10-07 09:57:23 +08:00
WuYongJie
a308eb6c81 以图搜图粘贴与放大体验修复;部署脚本补齐 onnxruntime 原生库
- ProductArchiveImageSearchDialog:弹窗打开期间监听 document 级 paste,页面任意位置 Ctrl+V 直接粘贴图片(点击上传区仍是文件选择);搜索结果小图可点击,复用 ProductArchiveImagePreview 放大核对
- yydd-prod-release-guards.sh:prepare_standalone_runtime 补齐 Turbopack standalone 产物遗漏的 onnxruntime-node 原生二进制(libonnxruntime.so.*),根治部署后本地图索层加载失败;符号链接场景自动跳过
2026-10-07 09:57:08 +08:00
WuYongJie
dbb2d4965a fix(layout): 浏览器标签页标题统一为工作区名,不再随子页面跳变
供应链中心 layout 新增统一标题「由壹点「供应链中心」」并删除其下 25 个页面的页级 metadata title;产品开发侧产品开发日志、BOM核算、AI出图工作台、安装演示工作台(V1)、通知记录、更新日志、回收站 8 处删除旧标题覆盖,统一显示「由壹点「产品开发」」。纯 metadata 变更,无 UI 与逻辑改动。3001 逐页 SSR title 实测通过。
2026-10-07 09:51:18 +08:00
codex
eac3344e2d perf(logistics-arrival): 看板冷启动从 7 秒降到 2 秒出头
整区间总览/环节目标原来 SELECT 全列,PG 只能对 900MB 堆表做 Seq Scan
(28 天区间 read=106409 页、6.3s);percentile_cont 又把 14 列宽的输入行
整行送进排序(两个分位数 4.3s,环节分位数叠加到分钟级);CTE 里的
CASE+EXTRACT 还会在每处引用被重复内联。

- 按用途收窄列集合:时间戳 / 时间戳+维度列,并新增覆盖索引
  (snapshotDate, signTime) INCLUDE (payTime, sendTime, fetchTime,
  warehouseName, platformName, shopName, logisticCompanyName) → Index Only Scan
- 分位数按列拆成独立语句、只投影一个浮点列,与计数语句并行下发
- measured / durations 改 AS MATERIALIZED,时长表达式只算一次
- 环节目标(上月平均 × 98%)接入 10 分钟内存缓存

实测(本机快照 648,689 行、冷启动):
28 天 7.3s→2.3s、10 天 4.4s→0.4s、2 天 1.7s→0.3s;
浏览器首屏出数 ~7-10s→2.7s。2 天区间与 JS 现算逐项对账一致。
2026-10-07 09:45:35 +08:00
WuYongJie
1da917bb01 Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/local-3001-integration 2026-10-07 09:40:10 +08:00
WuYongJie
08bb5a96a4 fix(layout): 浏览器标签页标题统一为工作区名,不再随子页面跳变
供应链中心 layout 新增统一标题「由壹点「供应链中心」」并删除其下 25 个页面的页级 metadata title;产品开发侧产品开发日志、BOM核算、AI出图工作台、安装演示工作台(V1)、通知记录、更新日志、回收站 8 处删除旧标题覆盖,统一显示「由壹点「产品开发」」。纯 metadata 变更,无 UI 与逻辑改动。3001 逐页 SSR title 实测通过。
2026-10-07 09:37:21 +08:00
WuYongJie
b0a6cdd1e7 feat(installation-demo): 工作台顶栏改为目录/制作/分享并新增工具抽屉与制作流程卡
- 标签行:模型→目录、详情→制作;工具从跳转链接改为面板内抽屉,勾选即保存,完整工具箱保留次级入口
2026-10-07 08:50:28 +08:00
WuYongJie
69618965c7 feat(installation-demo): 工作台顶栏改为目录/制作/分享并新增工具抽屉与制作流程卡
- 标签行:模型→目录、详情→制作;工具从跳转链接改为面板内抽屉,勾选即保存,完整工具箱保留次级入口
2026-10-06 18:21:35 +08:00
WuYongJie
f6e4d7f2e1 feat(installation-demo): 工作台顶栏改为目录/制作/分享并新增工具抽屉与制作流程卡
- 标签行:模型→目录、详情→制作;工具从跳转链接改为面板内抽屉,勾选即保存,完整工具箱保留次级入口
2026-10-06 18:20:36 +08:00
WuYongJie
614e3dabe2 feat(supply-chain): 产品全量档案「产品标签」接入 SABC 打标与未打标兜底 2026-10-06 18:19:40 +08:00
WuYongJie
ce2a6ebbaf feat(supply-chain): 产品全量档案「产品标签」接入 SABC 打标与未打标兜底 2026-10-06 18:19:34 +08:00
WuYongJie
b1f55faebb feat(supply-chain): 产品全量档案「产品标签」接入 SABC 打标与未打标兜底 2026-10-06 18:19:10 +08:00
codex
c5cae01ecd fix(logistics-arrival): 修复排除关键词弹窗透明与保存报格式错误
- globals.css:在 :root 与 [data-theme='dark'] 补齐 --sc-* 主题别名。
  弹窗通过 createPortal 挂在 document.body 上,位于 .sc-layout 之外,
  之前取不到 --sc-surface/--sc-border 等变量,正式环境里表现为面板透明、
  边框与分隔线丢失(本地开发因为另一分支已临时补过这段而看不出来)。
- settings 接口:接受按字段分组的排除关键词对象(同时兼容历史平铺数组与字符串),
  修复管理员在选择器里新增关键词时报"排除关键词格式不正确"。
- 补充 settings 路由单测:分组对象落库、平铺数组兼容、非法格式返回 400。
2026-10-06 18:15:57 +08:00
WuYongJie
7e6cc35199 fix(intake): 搜索下拉支持清空已选值 2026-10-06 17:56:20 +08:00
WuYongJie
57b2b405cf fix(intake): 商品企划候选人排除供应商账号 2026-10-06 17:56:20 +08:00
WuYongJie
e1b8989621 fix(intake): 搜索下拉支持清空已选值 2026-10-06 17:56:07 +08:00
WuYongJie
f5f20a8b18 fix(intake): 商品企划候选人排除供应商账号 2026-10-06 17:56:06 +08:00
WuYongJie
a110714305 fix(intake): 搜索下拉支持清空已选值 2026-10-06 17:55:36 +08:00
WuYongJie
f430b3e18d fix(intake): 商品企划候选人排除供应商账号 2026-10-06 17:55:24 +08:00
居居
5cd69fb9db Merge branch 'codex/xuxiake-sabc-sync-20260929' into codex/xuxiake-mainline 2026-10-06 17:24:22 +08:00
WuYongJie
a4e61b44a1 Merge origin/codex/xuxiake-mainline (SABC 模块等 74 个提交) into codex/local-3001-integration 2026-10-06 17:21:50 +08:00
居居
2be45f7a74 Merge branch 'codex/xuxiake-sabc-sync-20260929' into codex/xuxiake-mainline 2026-10-06 17:02:56 +08:00
居居
14a5e1c5f7 feat(supply-chain): 人员信息同步支持自动标记离职(方案B:抓取比对+钉钉确认) 2026-10-06 16:53:13 +08:00
WuYongJie
6d5a8a65d3 fix(intake): 商品企划候选人改为全部内部人员(商品企划部解散后搜不到人) 2026-10-06 16:29:59 +08:00
WuYongJie
e95a2365f5 fix(intake): 商品企划候选人改为全部内部人员(商品企划部解散后搜不到人) 2026-10-06 16:29:36 +08:00
WuYongJie
294d6d3dfa fix(intake): 商品企划候选人改为全部内部人员(商品企划部解散后搜不到人) 2026-10-06 16:29:20 +08:00
居居
cda264dd30 Merge branch 'codex/xuxiake-sabc-sync-20260929' into codex/xuxiake-mainline
商品SABC打标:月度核算生成后冻结、各部门数据分类子页面(按利润中心分组 + 部门数据权限 +
可配置品牌范围/兜底/目标 + 月度快照)、导出明细与经营总览、同款商品编码匹配修复;
并移除未随提交带上的 WorkflowMonitorView 引用、修正行快照类型。
2026-10-06 16:12:42 +08:00
居居
cfe8cf950c Merge branch 'codex/xuxiake-sabc-sync-20260929' into codex/xuxiake-mainline
商品SABC打标:月度核算生成后冻结、各部门数据分类子页面(按利润中心分组 + 部门数据权限 +
可配置品牌范围/兜底/目标 + 月度快照)、导出明细与经营总览、同款商品编码匹配修复;
并移除未随提交带上的 WorkflowMonitorView 引用、修正行快照类型。
2026-10-06 16:07:22 +08:00
居居
3ef36c7648 fix(supply-chain): 移除未随提交带上的 WorkflowMonitorView 引用并修正 SABC 行快照类型 2026-10-06 16:05:54 +08:00
codex
83c10719ca feat(logistics-arrival): 物流到货签收时长看板
- 看板:经营总览(平均到货时长 + 本月目标 + 目标达成率、异常单点击下钻)、
  履约环节拆解(平均/中位/P90/目标/目标达成率,分栏可拖动)、发货→签收时效分布
- 经营对象问题排名:签收日期→平台→店铺→发货仓→物流→采购→供应商→商品→订单明细
  逐级联动 + 独立翻页;经营对象带「目标 = 该对象上月平均 × 98%」与目标达成率
- 09 订单明细:看板内翻页、高度可拖动、下钻后自动滚动定位
- 排除关键词按字段分组(发货仓/物流公司/订单标签/买家留言/卖家备注),
  选择器支持搜索、全部/已选项、全选/反选/清空、紧凑模式与按字段新增
- 每日订单明细快照:Prisma 模型与迁移、同步脚本、定时器安装脚本;
  命中快照时整区间精确统计,超过规模上限回落均匀样本并如实标注
- 接入侧边菜单、标签页标题与权限映射(businessManagement),补充架构文档
2026-10-06 15:37:48 +08:00
居居
ac572e2c4d Merge branch 'codex/xuxiake-sabc-sync-20260929' into codex/xuxiake-mainline
商品SABC打标:月度核算生成后冻结(每月3日自动生成上月)、各部门数据分类子页面
(按利润中心分组 + 部门数据权限 + 可配置品牌范围/兜底/目标)、导出明细与经营总览、
同款商品编码匹配修复等。
2026-10-06 15:37:48 +08:00
codex
77eff1744a feat(employee-hours): 工时记录列表支持按列排序
- 查询接口与管理端筛选条件新增 sortBy / sortDirection,服务端校验取值合法性
- 仓储层按列名映射排序字段,approvedScore / attachment 使用 nulls last,并保持 id 升序兜底
- 列表列头改为可点击排序(显示升序/降序箭头与 aria-sort),保留原有列宽拖拽
- 补齐仓储、服务、视图层单测(排序参数校验与列头交互)
2026-10-06 15:37:47 +08:00
codex
05007d3ae2 feat(logistics-arrival): 物流到货签收时长看板
- 看板:经营总览(平均到货时长 + 本月目标 + 目标达成率、异常单点击下钻)、
  履约环节拆解(平均/中位/P90/目标/目标达成率,分栏可拖动)、发货→签收时效分布
- 经营对象问题排名:签收日期→平台→店铺→发货仓→物流→采购→供应商→商品→订单明细
  逐级联动 + 独立翻页;经营对象带「目标 = 该对象上月平均 × 98%」与目标达成率
- 09 订单明细:看板内翻页、高度可拖动、下钻后自动滚动定位
- 排除关键词按字段分组(发货仓/物流公司/订单标签/买家留言/卖家备注),
  选择器支持搜索、全部/已选项、全选/反选/清空、紧凑模式与按字段新增
- 每日订单明细快照:Prisma 模型与迁移、同步脚本、定时器安装脚本;
  命中快照时整区间精确统计,超过规模上限回落均匀样本并如实标注
- 接入侧边菜单、标签页标题与权限映射(businessManagement),补充架构文档
2026-09-30 18:18:51 +08:00
居居
a771a9a5dc Merge branch 'codex/xuxiake-sabc-sync-20260929' into codex/xuxiake-mainline
商品SABC打标:月度核算生成后冻结(每月3日自动生成上月)、各部门数据分类子页面
(按利润中心分组 + 部门数据权限 + 可配置品牌范围/兜底/目标)、导出明细与经营总览、
同款商品编码匹配修复等。
2026-09-30 18:15:07 +08:00
居居
3f1de4e431 style(supply-chain): 导出下拉框与查询/重置按钮对齐 2026-09-30 18:14:09 +08:00
居居
155c93a0c5 feat(supply-chain): 产品中心/采购中心卡片新增导出按钮 2026-09-30 18:11:09 +08:00
居居
471b5b373e perf(supply-chain): 各部门数据分类改为核算时落库月度快照,页面直接读结果 2026-09-30 18:08:01 +08:00
codex
df206dbfe3 feat(employee-hours): 工时记录列表支持按列排序
- 查询接口与管理端筛选条件新增 sortBy / sortDirection,服务端校验取值合法性
- 仓储层按列名映射排序字段,approvedScore / attachment 使用 nulls last,并保持 id 升序兜底
- 列表列头改为可点击排序(显示升序/降序箭头与 aria-sort),保留原有列宽拖拽
- 补齐仓储、服务、视图层单测(排序参数校验与列头交互)
2026-09-30 18:04:41 +08:00
居居
dc0a9e740a style(supply-chain): 各部门数据分类按 A/B/C/G 优先排序 2026-09-30 17:56:43 +08:00
居居
398bc73450 feat(supply-chain): 产品中心/采购中心剔除山东项目组,导出补合计行 2026-09-30 17:51:18 +08:00
居居
4369e2f050 perf(supply-chain): 各部门数据分类增加结果/快照/花名册缓存,加载从秒级降到毫秒级 2026-09-30 17:11:52 +08:00
居居
e61772c2ce feat(supply-chain): 导出菜单去掉列表项,明细=仅商品表,明细(含经营总览)=商品表+汇总 2026-09-30 15:51:36 +08:00
居居
5bc45f4e93 fix(supply-chain): 品牌兜底行的部门负责人/总负责人改按人员信息逻辑取 2026-09-30 15:41:07 +08:00
居居
4c4807c72e feat(supply-chain): SABC 导出改为导出列表/导出明细/导出经营总览三档菜单 2026-09-30 15:39:14 +08:00
居居
baa2017106 feat(supply-chain): 山东专用/分销专用品牌运营负责人兜底(刘景刚/宋兵乙) 2026-09-30 15:34:17 +08:00
居居
38120d3f73 feat(supply-chain): 其他(未分配)分组核实花名册离职状态并置底展示 2026-09-30 14:37:55 +08:00
居居
9d2633420d style(supply-chain): 产品中心/采购中心改为与运营中心一致的卡片与合计行 2026-09-30 14:25:33 +08:00
居居
de6d9014b3 feat(supply-chain): 各部门数据分类产品中心/采购中心改为完整指标列,去掉重复的未匹配清单 2026-09-30 14:22:09 +08:00
居居
0a2a9c2f89 feat(supply-chain): 各部门数据分类筛选占位/单中心导出/部门回落利润中心/离职人员分组/运营中心E并入G 2026-09-30 14:10:38 +08:00
居居
6a36cae707 style(supply-chain): 各部门数据分类业绩按万元展示 2026-09-30 13:54:03 +08:00
居居
d47d550409 fix(supply-chain): 各部门数据分类配置改为弹窗,点击配置立即可见 2026-09-30 13:53:28 +08:00
居居
303694784c feat(supply-chain): SABC 导出新增「明细」/「明细(含经营总览)」两种模式 2026-09-30 13:49:38 +08:00
居居
da46e84526 feat(supply-chain): 新增「各部门数据分类」子页面(按利润中心分组 + 部门数据权限 + 可配置品牌范围) 2026-09-30 13:02:00 +08:00
居居
6eebb56ba0 feat(supply-chain): SABC 每月3日生成上月并冻结,执行核算仅管理员 2026-09-30 09:51:41 +08:00
居居
ad08dccf1c Merge branch 'codex/xuxiake-sabc-sync-20260929' into codex/xuxiake-mainline
商品SABC打标全部源码:页面/筛选/经营总览、服务端核算规则、同款商品编码匹配、
每日同步(聚水潭档案/销售日表/替换规则/类目档位/产品类型回填)、API 与导出、测试用例。
2026-09-29 18:04:53 +08:00
居居
6da6b0df76 Merge branch 'codex/xuxiake-sabc-sync-20260929' into codex/xuxiake-mainline
商品SABC打标全部源码:页面/筛选/经营总览、服务端核算规则、同款商品编码匹配、
每日同步(聚水潭档案/销售日表/替换规则/类目档位/产品类型回填)、API 与导出、测试用例。
2026-09-29 17:59:16 +08:00
WuYongJie
b9dc0bddb1 产品全量档案以图搜图改用壹点图搜接口并增加本地向量双层检索
- A 层:调壹点图搜上游(ownProductMatches 我司 SKU 命中),上游调用收口为共享 client,供应商推荐搜索市场同款同步复用
- B 层:本地 DINOv2 向量索引覆盖全量档案图(宽松排序、同图哈希置顶、模型未就绪自动降级)
- 上游编码与档案条码/SKU 大小写不敏感匹配,两路结果按条码去重
- 弹窗分「我司 SKU 命中」「全量档案相似」两区展示,本地索引通过 scripts/index-product-archive-images.ts 增量维护
2026-09-29 17:55:30 +08:00
WuYongJie
4008bcf061 产品全量档案以图搜图改用壹点图搜接口并增加本地向量双层检索
- A 层:调壹点图搜上游(ownProductMatches 我司 SKU 命中),上游调用收口为共享 client,供应商推荐搜索市场同款同步复用
- B 层:本地 DINOv2 向量索引覆盖全量档案图(宽松排序、同图哈希置顶、模型未就绪自动降级)
- 上游编码与档案条码/SKU 大小写不敏感匹配,两路结果按条码去重
- 弹窗分「我司 SKU 命中」「全量档案相似」两区展示,本地索引通过 scripts/index-product-archive-images.ts 增量维护
2026-09-29 17:54:54 +08:00
居居
39f603c091 fix(supply-chain): 同款编码改为直接正反向匹配,不再顺延替换链到末端 2026-09-29 17:21:24 +08:00
居居
34abc63edc feat(supply-chain): 同款编码正反向匹配并顺延替换链到最终编码 2026-09-29 17:05:49 +08:00
居居
0a8231d93c feat(supply-chain): 总览刷新与趋势支持强制取最新数据(绕过缓存) 2026-09-29 16:49:52 +08:00
居居
7838b82655 feat(supply-chain): 同款编码重复映射取创建时间最新的一条 2026-09-29 16:47:06 +08:00
居居
9f89ccbb7e feat(supply-chain): 不考核原因只保留 ODM,其余为空并同步是否考核 2026-09-29 16:35:12 +08:00
WuYongJie
1dcaa48baf fix(intake): 产品核价工作区接入受控列宽并修复长规格名称挤成多排 2026-09-29 16:19:38 +08:00
WuYongJie
51ff817dde fix(intake): 产品核价工作区接入受控列宽并修复长规格名称挤成多排 2026-09-29 16:18:19 +08:00
居居
791fcf083b style(supply-chain): 总览标签顺序恢复 S/A/B/C/D 2026-09-29 16:16:22 +08:00
居居
4cb5f9192a feat(supply-chain): 筛选栏新增运营部门负责人(含下拉候选) 2026-09-29 16:13:58 +08:00
居居
0f9bade034 feat(supply-chain): 总览维度改为运营部门负责人并支持联动筛选 2026-09-29 16:12:30 +08:00
居居
eedda35a29 fix(supply-chain): 补回被误删的 percent 格式化函数 2026-09-29 16:06:51 +08:00
居居
55d77fd989 style(supply-chain): 总览真实业绩统一按万元展示 2026-09-29 16:05:29 +08:00
居居
c45ec60635 style(supply-chain): 总览标签卡片与对比条按真实业绩降序 2026-09-29 16:04:55 +08:00
居居
efc437f0de feat(supply-chain): 维度汇总表展示全部分组,不再合并其他行 2026-09-29 16:04:23 +08:00
居居
3b1a629854 style(supply-chain): 维度汇总表去掉标签配色,与其他列保持一致 2026-09-29 16:03:08 +08:00
居居
a38aaef5d2 feat(supply-chain): 总览维度模块改为 S/A/B/C/D 数据汇总表 2026-09-29 16:01:06 +08:00
居居
7caa307042 style(supply-chain): 总览卡片保留数量与占比,对比条改为真实业绩(万)+业绩占比 2026-09-29 15:53:44 +08:00
居居
e0bad60b31 fix(supply-chain): 页面支持整体滚动,明细区不再被压扁 2026-09-29 15:48:53 +08:00
居居
8e0fc1d347 perf(supply-chain): 经营总览一次计算+短时缓存,首页刷新提速 2026-09-29 15:47:34 +08:00
居居
574f42c4ed fix(supply-chain): 修复筛选区与总览重叠,总览可点击联动明细 2026-09-29 15:42:45 +08:00
居居
5102516b10 feat(supply-chain): 经营总览新增维度对比与近6个月趋势 2026-09-29 15:37:14 +08:00
居居
189344626e feat(supply-chain): 经营总览改为 SABC 页内可折叠区块,移除独立页面与菜单 2026-09-29 15:35:24 +08:00
居居
fe6389a2a0 feat(supply-chain): 新增 SABC 经营总览(商品数 vs 业绩对比) 2026-09-29 15:31:48 +08:00
居居
9d53362466 style(supply-chain): SABC 筛选框固定单行排列,标签归位到行尾 2026-09-29 15:28:29 +08:00
居居
005cc37a65 style(supply-chain): SABC 筛选框按内容宽度排成一行 2026-09-29 15:26:57 +08:00
居居
2f081dd1f5 style(supply-chain): SABC 筛选改为圆角输入框样式 2026-09-29 15:25:10 +08:00
居居
b0701d8ae4 feat(supply-chain): 筛选候选取消服务端上限 2026-09-29 15:22:11 +08:00
居居
f0af29c62b fix(supply-chain): 筛选候选上限提高到 200,运营负责人等人员全部可选 2026-09-29 15:21:14 +08:00
居居
2a61ed2b34 style(supply-chain): 核算按钮移至核算月份后,查询/重置/导出统一按钮样式 2026-09-29 15:17:06 +08:00
WuYongJie
143dfe019d fix(layout): raise sidebar peek z-index above page content layers 2026-09-29 15:14:27 +08:00
WuYongJie
f5f4e2ec90 fix(layout): raise sidebar peek z-index above page content layers 2026-09-29 15:13:52 +08:00
居居
fefb1319c0 style(supply-chain): SABC 筛选头部与字段网格分离,消除空格 2026-09-29 15:05:35 +08:00
居居
cde8d617ad style(supply-chain): SABC 筛选区统一网格排版 2026-09-29 15:03:55 +08:00
居居
85c69041fe feat(supply-chain): SABC 新增运营负责人/采购/设计师筛选 2026-09-29 15:01:55 +08:00
居居
1762ba8387 feat(supply-chain): SABC 筛选字段支持下拉候选选择 2026-09-29 15:01:04 +08:00
居居
84515e895c feat(supply-chain): SABC 筛选改为人员信息页同款样式(字段网格 + 搜索/重置) 2026-09-29 14:57:58 +08:00
居居
852d42bf3a feat(supply-chain): SABC 筛选改为按字段独立输入框(支持批量、多字段取交集) 2026-09-29 14:53:59 +08:00
居居
8cd0f71ff7 feat(supply-chain): SABC 筛选支持批量查询,筛选区拆分为两行 2026-09-29 14:50:27 +08:00
WuYongJie
8c1c4bd938 feat(barcode-print): PLM 产品开发视图每日同步正式站条码打印脚本 2026-09-29 14:44:04 +08:00
居居
b1c3ba3bda feat(supply-chain): 新老编码两侧均按自身+同款编码合并计算 2026-09-29 14:43:26 +08:00
居居
ea7c5a953c fix(supply-chain): 老编码按自身数据照常打标(与人工表一致) 2026-09-29 14:40:41 +08:00
居居
117a5b2947 feat(supply-chain): 人员字段以正式库档案为准同步 2026-09-29 14:02:09 +08:00
居居
2dbc455b86 feat(supply-chain): 每日任务新增运营总负责人映射步骤 2026-09-29 14:00:19 +08:00
居居
c59647ac9f feat(supply-chain): SABC 页面新增运营/供应商/采购/设计师字段(置于创建时间前) 2026-09-29 13:44:40 +08:00
居居
7b3a7acb12 feat(supply-chain): 是否考核动销按产品类型判定(OEM 考核,其余不考核) 2026-09-29 13:40:36 +08:00
居居
95f5915c6a fix(supply-chain): 同款编码只展示单个编码,自映射规则不重复计算 2026-09-29 13:34:47 +08:00
居居
ff1ea6ce24 feat(supply-chain): 同款商品编码按替换款管理合并业绩与费比 2026-09-29 13:21:45 +08:00
WuYongJie
5eb7d4b1aa fix(layout): cast keydown target to HTMLElement for isContentEditable check 2026-09-29 13:18:15 +08:00
WuYongJie
d40cad8eed fix(layout): cast keydown target to HTMLElement for isContentEditable check 2026-09-29 13:18:06 +08:00
WuYongJie
110de56e7f feat(layout): add collapsible workbench sidebar with hover peek and space shortcut 2026-09-29 13:03:51 +08:00
WuYongJie
d7e348b236 merge origin/codex/xuxiake-mainline (employee-hours sync) 2026-09-29 13:03:09 +08:00
WuYongJie
b2f4242526 feat(layout): add collapsible workbench sidebar with hover peek and space shortcut 2026-09-29 13:03:04 +08:00
apple
4c771e0a82 feat(employee-hours): sync row copy and time entry improvements 2026-09-29 11:49:49 +08:00
apple
f6b8065580 feat(employee-hours): sync row copy and time entry improvements 2026-09-29 11:37:15 +08:00
WuYongJie
db01d9e445 fix(intake): 样包测量/产品限价/首批质检列宽整列联动(受控列宽) 2026-09-29 11:27:18 +08:00
WuYongJie
eb6fa9d672 fix(intake): 样包测量/产品限价/首批质检列宽整列联动(受控列宽) 2026-09-29 11:25:55 +08:00
居居
4c518b054e feat(supply-chain): SABC 不考核原因改用档案产品类型,并纳入每日回填 2026-09-29 11:19:17 +08:00
居居
9ae9423742 feat(supply-chain): 每日同步新增正式库类目档位配置步骤 2026-09-29 10:20:18 +08:00
居居
b976f075ff Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/xuxiake-sabc-sync-20260929
# Conflicts:
#	furniture-3d-system/prisma/schema.prisma
#	furniture-3d-system/src/server/repositories/supply-chain-product-archive-category-scale-row.repository.ts
#	furniture-3d-system/src/server/repositories/supply-chain-product-archive-sales-period.repository.ts
2026-09-29 10:14:29 +08:00
居居
648b1eee8d feat(supply-chain): 商品SABC打标(同步开发线前的本地快照) 2026-09-29 10:10:16 +08:00
WuYongJie
749c28452c Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/local-3001-integration 2026-09-29 09:44:02 +08:00
WuYongJie
f2dfcf1419 feat(supply-chain): 产品全量档案新增「售后金额」列(after_sale_amount 按日/月/年周期聚合) 2026-09-28 18:13:15 +08:00
WuYongJie
0e85656279 feat(supply-chain): 产品全量档案新增「售后金额」列(after_sale_amount 按日/月/年周期聚合) 2026-09-28 18:12:31 +08:00
WuYongJie
fe99b46eef feat(supply-chain): 产品全量档案新增「售后金额」列(after_sale_amount 按日/月/年周期聚合) 2026-09-28 18:11:43 +08:00
WuYongJie
afbb7b3aad feat(supply-chain): refresh product type in daily archive sync
在每日产品档案同步链路中追加「产品类型」刷新步骤(定厂优先、JST vc_name 兜底),复用既有 timer 与库身份校验,无需单独回填或新增定时任务。
2026-09-28 17:00:23 +08:00
WuYongJie
7384163a2c feat(supply-chain): refresh product type in daily archive sync
在每日产品档案同步链路中追加「产品类型」刷新步骤(定厂优先、JST vc_name 兜底),复用既有 timer 与库身份校验,无需单独回填或新增定时任务。
2026-09-28 17:00:17 +08:00
WuYongJie
c473825b3d feat(supply-chain): refresh product type in daily archive sync
在每日产品档案同步链路中追加「产品类型」刷新步骤(定厂优先、JST vc_name 兜底),复用既有 timer 与库身份校验,无需单独回填或新增定时任务。
2026-09-28 17:00:08 +08:00
WuYongJie
5664958dfb feat(supply-chain): add product type column to product archive
产品全量档案在「商品状态」后新增「产品类型」:定厂「产品生产类型」(IntakeNonIntegratedProduct.payload.factoryRows[].productionType) 优先,rpt.jst_sku.vc_name 兜底,按条码对应,都没有留空;值原样写入。附迁移、筛选与回填脚本。
2026-09-28 16:31:09 +08:00
WuYongJie
199be5d7cb feat(supply-chain): add product type column to product archive
产品全量档案在「商品状态」后新增「产品类型」:定厂「产品生产类型」(IntakeNonIntegratedProduct.payload.factoryRows[].productionType) 优先,rpt.jst_sku.vc_name 兜底,按条码对应,都没有留空;值原样写入。附迁移、筛选与回填脚本。
2026-09-28 16:30:55 +08:00
WuYongJie
51c6c9f20b feat(supply-chain): add product type column to product archive
产品全量档案在「商品状态」后新增「产品类型」:定厂「产品生产类型」(IntakeNonIntegratedProduct.payload.factoryRows[].productionType) 优先,rpt.jst_sku.vc_name 兜底,按条码对应,都没有留空;值原样写入。附迁移、筛选与回填脚本。
2026-09-28 16:30:14 +08:00
WuYongJie
8b76cd57db feat(product-archive): 新增「销售金额」表头字段(取每日快照 sales_amount)
- 列表「数据信息」分组新增「销售金额」,位置固定在「真实业绩」前一列
- 取值来自每日快照表 SupplyChainProductArchiveSalesPerformanceDaily.sales_amount,
  与发货金额/退货金额/真实业绩走同一链路,随页面 日/月/年 周期聚合
- 表头漏斗支持按销售金额筛选(可搜索多选标准弹层),列设置/详情弹窗/资料导出随列配置自动生效,
  字段总数 89 → 90
- 新增一次性 localStorage 列序迁移,把上线前保存过的列设置里的新列挪回「真实业绩」前
- 不改数据库结构、不改每日同步脚本;旧的全周期汇总表不加列、不回填
2026-09-28 16:02:45 +08:00
WuYongJie
3f36d2b51e feat(product-archive): 新增「销售金额」表头字段(取每日快照 sales_amount)
- 列表「数据信息」分组新增「销售金额」,位置固定在「真实业绩」前一列
- 取值来自每日快照表 SupplyChainProductArchiveSalesPerformanceDaily.sales_amount,
  与发货金额/退货金额/真实业绩走同一链路,随页面 日/月/年 周期聚合
- 表头漏斗支持按销售金额筛选(可搜索多选标准弹层),列设置/详情弹窗/资料导出随列配置自动生效,
  字段总数 89 → 90
- 新增一次性 localStorage 列序迁移,把上线前保存过的列设置里的新列挪回「真实业绩」前
- 不改数据库结构、不改每日同步脚本;旧的全周期汇总表不加列、不回填
2026-09-28 16:02:22 +08:00
apple
0fde0bfe4b feat(employee-hours): add resizable columns and category import 2026-09-28 15:39:40 +08:00
codex
61a4d81658 feat(store-account): 新增账号按人员信息带出负责人层级,修复自动填充残留并明文显示密码 2026-09-28 14:58:24 +08:00
codex
2c4949f1cc feat(store-account): 新增账号按人员信息带出负责人层级,修复自动填充残留并明文显示密码 2026-09-28 14:58:13 +08:00
codex
c89e4248f0 feat(store-account): 新增账号按人员信息带出负责人层级,修复自动填充残留并明文显示密码 2026-09-28 14:57:53 +08:00
WuYongJie
2b14e8863e feat(supply-chain): sync product archive designer from js_sku_info_manage
按 barcode=sku_id 从 rpt.js_sku_info_manage 回填产品全量档案 designer:空值补来源值,冲突行按用户口径取辛环;写入 fieldSourceMap=JST_SKU_INFO_MANAGE 并保留来源记录。附正式站 systemd timer 安装脚本(每天 09:00 Asia/Shanghai)。
2026-09-28 14:45:30 +08:00
WuYongJie
97d618a25d feat(supply-chain): sync product archive designer from js_sku_info_manage
按 barcode=sku_id 从 rpt.js_sku_info_manage 回填产品全量档案 designer:空值补来源值,冲突行按用户口径取辛环;写入 fieldSourceMap=JST_SKU_INFO_MANAGE 并保留来源记录。附正式站 systemd timer 安装脚本(每天 09:00 Asia/Shanghai)。
2026-09-28 14:45:11 +08:00
WuYongJie
5f4170919d feat(supply-chain): sync product archive designer from js_sku_info_manage
按 barcode=sku_id 从 rpt.js_sku_info_manage 回填产品全量档案 designer:空值补来源值,冲突行按用户口径取辛环;写入 fieldSourceMap=JST_SKU_INFO_MANAGE 并保留来源记录。附正式站 systemd timer 安装脚本(每天 09:00 Asia/Shanghai)。
2026-09-28 14:44:54 +08:00
WuYongJie
b01a5bb574 fix(supplier-recommended): use upstream image search defaults 2026-09-28 14:22:40 +08:00
WuYongJie
da3a49f03f fix(supplier-recommended): use upstream image search defaults 2026-09-28 14:19:21 +08:00
apple
a2dab2cd49 feat(employee-hours): add resizable columns and category import 2026-09-28 14:15:33 +08:00
WuYongJie
a4e577e376 fix(supplier-recommended): use upstream image search defaults 2026-09-28 14:09:50 +08:00
WuYongJie
4f12fdfcac fix(intake): 分销部纳入运营负责人候选名单并默认归运营
1. intake-owner-options.service.ts: operation 口径在路径含运营中心之外,增加 江苏简尚品牌管理有限公司>分销部,SPU负责人与运营负责人共用该名单,运营中心原口径不变。

2. business-role-inference.ts: 分销部从 EXCLUDED_ROOTS 移出,改为默认归 OPERATIONS,避免每日花名册同步把分销部在职员工打回普通用户。
2026-09-28 12:35:36 +08:00
WuYongJie
4fb7802eb4 fix(intake): 分销部纳入运营负责人候选名单并默认归运营
1. intake-owner-options.service.ts: operation 口径在路径含运营中心之外,增加 江苏简尚品牌管理有限公司>分销部,SPU负责人与运营负责人共用该名单,运营中心原口径不变。

2. business-role-inference.ts: 分销部从 EXCLUDED_ROOTS 移出,改为默认归 OPERATIONS,避免每日花名册同步把分销部在职员工打回普通用户。
2026-09-28 12:35:21 +08:00
WuYongJie
d1aa93e17f fix(intake): 分销部纳入运营负责人候选名单并默认归运营
1. intake-owner-options.service.ts: operation 口径在路径含运营中心之外,增加 江苏简尚品牌管理有限公司>分销部,SPU负责人与运营负责人共用该名单,运营中心原口径不变。

2. business-role-inference.ts: 分销部从 EXCLUDED_ROOTS 移出,改为默认归 OPERATIONS,避免每日花名册同步把分销部在职员工打回普通用户。
2026-09-28 12:35:10 +08:00
apple
6a52c0aa4b feat(employee-hours): make business selector resizable 2026-09-28 09:27:59 +08:00
apple
daa0abbf03 feat(employee-hours): make business selector resizable 2026-09-28 09:00:22 +08:00
WuYongJie
48fcd34608 Revert "fix(deploy): support interactive password authentication"
This reverts commit d2f965611c15838a5b3e3e1f32cf1a5dbe7091bb.
2026-09-27 17:18:29 +08:00
WuYongJie
80f82d8ff0 fix(employee-hours): normalize attachment URLs before save 2026-09-27 17:14:02 +08:00
WuYongJie
9d7023df4a fix(employee-hours): normalize attachment URLs before save 2026-09-27 17:11:37 +08:00
WuYongJie
d2f965611c fix(deploy): support interactive password authentication 2026-09-27 17:10:21 +08:00
WuYongJie
a87a8b50b3 fix(employee-hours): normalize attachment URLs before save 2026-09-27 17:09:52 +08:00
WuYongJie
13e1dcb7b2 feat(barcode-print): 顾家模板改名「顾家(6x4)」并修复默认选中乱码匹配
- 下拉选项「顾家」改为「顾家(6x4)」(模板本身为 60mm×40mm 纸张)
2026-09-27 16:12:52 +08:00
WuYongJie
68b310f646 feat(store-account): add authorization status and header filters 2026-09-27 16:12:35 +08:00
WuYongJie
f9c32930da feat(barcode-print): 顾家模板改名「顾家(6x4)」并修复默认选中乱码匹配
- 下拉选项「顾家」改为「顾家(6x4)」(模板本身为 60mm×40mm 纸张)
2026-09-27 16:09:42 +08:00
WuYongJie
2ba2a37232 feat(store-account): add authorization status and header filters 2026-09-27 16:08:43 +08:00
apple
5a59e727c0 feat(employee-hours): complete statistics and ranking workflow 2026-09-27 15:40:53 +08:00
WuYongJie
52ab8578e5 feat(barcode-print): 顾家模板改名「顾家(6x4)」并修复默认选中乱码匹配
- 下拉选项「顾家」改为「顾家(6x4)」(模板本身为 60mm×40mm 纸张)
2026-09-27 15:32:06 +08:00
WuYongJie
4e52997d64 feat(layout): 侧栏分组归类与首页、操作日志体验统一
1) 首页快捷网址子项去图标改4px圆点(产品开发/供应链双侧栏共用生效)
2) 工作台选择卡片只留图标与名称,去掉描述小字
3) 产品开发侧栏按 新品流程/AI生图/模块化/仓库管理 分组(默认展开可折叠,一级带图标统一14px,二级圆点13px,BOM核算归新品流程),手机端下拉同步
4) /intake/operation-logs 去左侧栏,固定返回链接改为返回上一步按钮(无历史兜底/intake)
2026-09-27 15:20:20 +08:00
WuYongJie
ccf8b6480f feat(layout): 侧栏分组归类与首页、操作日志体验统一
1) 首页快捷网址子项去图标改4px圆点(产品开发/供应链双侧栏共用生效)
2) 工作台选择卡片只留图标与名称,去掉描述小字
3) 产品开发侧栏按 新品流程/AI生图/模块化/仓库管理 分组(默认展开可折叠,一级带图标统一14px,二级圆点13px,BOM核算归新品流程),手机端下拉同步
4) /intake/operation-logs 去左侧栏,固定返回链接改为返回上一步按钮(无历史兜底/intake)
2026-09-27 15:20:03 +08:00
apple
b96dfe9780 feat(employee-hours): complete statistics and ranking workflow 2026-09-27 14:30:00 +08:00
WuYongJie
aee6dc6c8f feat(store-account): add batch delete 2026-09-27 13:44:31 +08:00
WuYongJie
010cd87601 feat(store-account): add batch delete 2026-09-27 13:42:59 +08:00
WuYongJie
5013d709e4 feat(store-account): add batch delete 2026-09-27 13:41:04 +08:00
WuYongJie
ca0e2d884c fix(intake): persist AB package grouping and set counts 2026-09-27 13:14:50 +08:00
WuYongJie
02843032d2 fix(intake): persist AB package grouping and set counts 2026-09-27 13:13:28 +08:00
WuYongJie
90eebbc74d fix(intake): persist AB package grouping and set counts 2026-09-27 13:10:02 +08:00
WuYongJie
f988334005 style(warehouse-config): 表头靠左,筛选图标后置对齐产品全量档案 2026-09-27 12:33:04 +08:00
WuYongJie
e6545b6eae style(warehouse-config): 表头靠左,筛选图标后置对齐产品全量档案 2026-09-27 12:32:48 +08:00
WuYongJie
0cb88569d8 style(warehouse-config): 表头靠左,筛选图标后置对齐产品全量档案 2026-09-27 12:32:45 +08:00
WuYongJie
f9ff4af8c5 refactor(employee-hours): 工时独立业务配置表,与工作记录解耦
- 新增 EmployeeHoursBusinessOption/EmployeeHoursScoreRule 独立配置表与迁移
2026-09-27 11:37:39 +08:00
WuYongJie
3bd5384699 refactor(employee-hours): 工时独立业务配置表,与工作记录解耦
- 新增 EmployeeHoursBusinessOption/EmployeeHoursScoreRule 独立配置表与迁移
2026-09-27 11:36:16 +08:00
WuYongJie
5af8c6c539 refactor(employee-hours): 工时独立业务配置表,与工作记录解耦
- 新增 EmployeeHoursBusinessOption/EmployeeHoursScoreRule 独立配置表与迁移
2026-09-27 11:36:01 +08:00
WuYongJie
6f63f3936e Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/local-3001-integration 2026-09-27 10:59:34 +08:00
WuYongJie
7fa65e3309 feat(warehouse-config): 导入导出+聚水潭字典候选+采购负责人回填+表头筛选改造
- 导入导出:模板下载/NDJSON流式导入(名称upsert+逐行报错)/ExcelJS导出,均跟随筛选
2026-09-27 10:40:24 +08:00
WuYongJie
997efc1084 feat(warehouse-config): 导入导出+聚水潭字典候选+采购负责人回填+表头筛选改造
- 导入导出:模板下载/NDJSON流式导入(名称upsert+逐行报错)/ExcelJS导出,均跟随筛选
2026-09-27 10:40:01 +08:00
WuYongJie
36493b75f8 feat(warehouse-config): 导入导出+聚水潭字典候选+采购负责人回填+表头筛选改造
- 导入导出:模板下载/NDJSON流式导入(名称upsert+逐行报错)/ExcelJS导出,均跟随筛选
2026-09-27 10:39:45 +08:00
WuYongJie
001a1e919e fix(intake): skip unconfigured jushuitan virtual inventory 2026-09-27 10:22:31 +08:00
WuYongJie
1422c2f5fa fix(intake): skip unconfigured jushuitan virtual inventory 2026-09-27 10:21:02 +08:00
WuYongJie
427448f48e fix(intake): skip unconfigured jushuitan virtual inventory 2026-09-27 10:19:53 +08:00
WuYongJie
218517d5af fix(build): keep upload route constants private 2026-09-27 10:01:21 +08:00
apple
3e31bcf29e feat(supply-chain): add employee hours statistics 2026-09-27 09:22:12 +08:00
apple
2e3a36b25d feat(supply-chain): add employee hours statistics 2026-09-24 19:49:35 +08:00
WuYongJie
842bdd3967 style(category-config): 重置按钮去图标纯文字 2026-09-24 18:04:58 +08:00
WuYongJie
0b7cb58418 style(category-config): 重置按钮去图标纯文字 2026-09-24 18:04:45 +08:00
WuYongJie
e3e572657a feat(category-config): 筛选栏增加重置按钮(清空搜索/分类/规模标签并回第一页) 2026-09-24 18:03:12 +08:00
WuYongJie
ab817eddfa feat(category-config): 筛选栏增加重置按钮(清空搜索/分类/规模标签并回第一页) 2026-09-24 18:02:59 +08:00
WuYongJie
3dcdeb5cf1 docs(数据字典): 类目配置更新为单表合并结构(两表口径+取数方式+消费方) 2026-09-24 17:34:49 +08:00
WuYongJie
0a2e0484b3 fix(category-config): buildRowInput 分类入参改可选(生产构建类型对齐) 2026-09-24 17:27:12 +08:00
WuYongJie
01cf89cd01 fix(category-config): buildRowInput 分类入参改可选(生产构建类型对齐) 2026-09-24 17:26:59 +08:00
WuYongJie
5984179dc9 fix(category-config): 规模行服务入参分类改为可选以匹配路由校验 2026-09-24 17:21:21 +08:00
WuYongJie
f75b05bda1 fix(category-config): 规模行服务入参分类改为可选以匹配路由校验 2026-09-24 17:20:59 +08:00
WuYongJie
9e7f8914f0 fix(category-config): 移除对已删除规模行生成方法的残留调用 2026-09-24 17:13:21 +08:00
WuYongJie
a45c691428 fix(category-config): 移除对已删除规模行生成方法的残留调用 2026-09-24 17:12:27 +08:00
WuYongJie
fff6675c7a refactor(category-config): 类目规模合并为单表展示结构(一级类目/分类并入规模行,移除分类档位模板表) 2026-09-24 17:07:00 +08:00
WuYongJie
9e91f18dfd refactor(category-config): 类目规模合并为单表展示结构(一级类目/分类并入规模行,移除分类档位模板表) 2026-09-24 17:06:41 +08:00
WuYongJie
2af84fd9ce feat(intake): allow manual installation asset ordering 2026-09-24 16:36:24 +08:00
WuYongJie
9808224718 fix(category-config): 规模行导入同步一级类目与分类到类目配置;档位导入表头兼容带单位列名 2026-09-24 16:32:53 +08:00
WuYongJie
cb3efaa50c fix(category-config): 规模行导入同步一级类目与分类到类目配置;档位导入表头兼容带单位列名 2026-09-24 16:31:44 +08:00
WuYongJie
d52fd19c49 fix(category-config): 规模行导入同步一级类目与分类到类目配置(留空保留原值) 2026-09-24 15:47:04 +08:00
WuYongJie
eb6a2e6571 fix(category-config): 规模行导入同步一级类目与分类到类目配置(留空保留原值) 2026-09-24 15:46:30 +08:00
WuYongJie
ebf9e6a6c4 fix(category-config): 规模行导入表头对齐导出格式(含一级类目/分类/带单位列名)
导入、导出、下载模板三处表头统一为导出格式;导入兼容带单位列名(业绩起点(元)/日销起点(件/天)),一级类目与分类列接受但忽略
2026-09-24 15:02:12 +08:00
WuYongJie
d42d500071 fix(category-config): 规模行导入表头对齐导出格式(含一级类目/分类/带单位列名)
导入、导出、下载模板三处表头统一为导出格式;导入兼容带单位列名(业绩起点(元)/日销起点(件/天)),一级类目与分类列接受但忽略
2026-09-24 15:00:27 +08:00
WuYongJie
e8a374e93c feat(category-config): 类目配置增加分类、规模标签与业绩/日销阈值(含导入导出与删除)
- 类目配置单表新增:分类、规模标签、业绩起点/终点、日销起点/终点,操作列含编辑/删除
2026-09-24 14:36:43 +08:00
WuYongJie
fddf877d9e feat(category-config): 类目配置增加分类、规模标签与业绩/日销阈值(含导入导出与删除)
- 类目配置单表新增:分类、规模标签、业绩起点/终点、日销起点/终点,操作列含编辑/删除
2026-09-24 14:35:46 +08:00
WuYongJie
cc7219a24e feat(intake): sync installation assets to drawing system by ID
(cherry picked from commit c066c60494a9103e3d33655cbe09a0b7e03ffb18)
2026-09-24 13:37:30 +08:00
WuYongJie
c066c60494 feat(intake): sync installation assets to drawing system by ID 2026-09-24 13:35:04 +08:00
WuYongJie
9875a28716 feat(product-archive): include intake products in base sync and add 08:20 timer 2026-09-24 11:50:09 +08:00
WuYongJie
97a173eb9c Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/local-3001-integration 2026-09-24 11:48:37 +08:00
WuYongJie
a8b6d31c5e feat(product-archive): include intake products in base sync and add 08:20 timer 2026-09-24 10:56:21 +08:00
WuYongJie
46d0521105 merge: release DingTalk login resilience 2026-09-24 09:56:24 +08:00
WuYongJie
44be8f58b8 fix(auth): harden DingTalk login network failures 2026-09-24 09:50:38 +08:00
WuYongJie
02a25b1e75 修复:补齐创建人解析服务缺失的 ResolutionCache 类型声明(服务器构建失败根因) 2026-09-23 17:29:38 +08:00
WuYongJie
2d4965dfc7 修复:补齐创建人解析服务缺失的 ResolutionCache 类型声明(服务器构建失败根因) 2026-09-23 17:29:09 +08:00
WuYongJie
272219a4eb 1688代发异常事项:创建人列(纳廷创建人+人员信息在职校验+离职/兜底解析链)与业绩范围切换 2026-09-23 17:20:32 +08:00
WuYongJie
da4be64bfe 1688代发异常事项:创建人列(纳廷创建人+人员信息在职校验+离职/兜底解析链)与业绩范围切换 2026-09-23 17:20:05 +08:00
WuYongJie
c9431b7e26 feat(supply-chain): 店铺状态落库(StoreAccount.shopState 列 + 每日同步任务) 2026-09-23 14:05:25 +08:00
WuYongJie
a93159dddc feat(supply-chain): 店铺状态落库(StoreAccount.shopState 列 + 每日同步任务) 2026-09-23 14:03:13 +08:00
WuYongJie
1b293985d6 1688代发异常事项:上传图纸改为页内直传图纸系统(条码分组绑定V01/V02)并接入每日图纸对账 2026-09-23 13:16:56 +08:00
WuYongJie
291485dbda 1688代发异常事项:上传图纸改为页内直传图纸系统(条码分组绑定V01/V02)并接入每日图纸对账 2026-09-23 13:16:08 +08:00
WuYongJie
aff07c4375 feat(intake): support batch lead time with row overrides 2026-09-23 13:15:43 +08:00
WuYongJie
84ac3ed6a4 feat(intake): support batch lead time with row overrides 2026-09-23 13:14:21 +08:00
WuYongJie
791f6bfa40 feat(supply-chain): 店铺账号管理新增店铺状态列与筛选(实时取公司报表库 rpt.fys_shopInfo) 2026-09-23 12:45:04 +08:00
WuYongJie
eabc26a044 feat(supply-chain): 店铺账号管理新增店铺状态列与筛选(实时取公司报表库 rpt.fys_shopInfo) 2026-09-23 12:40:33 +08:00
WuYongJie
4b800d0b3b Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/wuyongjie-main-merge
# Conflicts:
#	furniture-3d-system/docs/workflow/生产部署入口说明.md
#	furniture-3d-system/scripts/install-dingtalk-directory-sync-timer.sh
#	furniture-3d-system/src/server/services/download-center/download-center.service.ts
2026-09-23 10:48:04 +08:00
WuYongJie
3d22d627fb fix(auth): separate DingTalk login credentials from API credentials 2026-09-23 10:43:16 +08:00
WuYongJie
fa17ffa9a8 fix(download-center): 生成中删除任务时丢弃产物,并忽略本地运行目录 2026-09-23 09:48:21 +08:00
WuYongJie
159035f98a fix(download-center): correct error code enum and failAllActive return shape 2026-09-22 17:40:30 +08:00
WuYongJie
8a95dbcbf0 fix(download-center): correct error code enum and failAllActive return shape 2026-09-22 17:40:21 +08:00
WuYongJie
71a7f0a09a feat(supply-chain): global download center with background export tasks 2026-09-22 17:32:53 +08:00
WuYongJie
9bfc3e826f feat(product-archive): factory stock column and ops-owner hierarchy rework 2026-09-22 17:32:53 +08:00
WuYongJie
89ed5ee355 chore(sync): schedule product archive source sync daily at 08:10 2026-09-22 17:32:53 +08:00
WuYongJie
24c668e1c8 feat(supply-chain): global download center with background export tasks 2026-09-22 17:32:32 +08:00
WuYongJie
870632bd5e feat(product-archive): factory stock column and ops-owner hierarchy rework 2026-09-22 17:32:23 +08:00
WuYongJie
152df22a1a chore(sync): schedule product archive source sync daily at 08:10 2026-09-22 17:32:10 +08:00
WuYongJie
6e35bb42ee chore(product-archive): 补提交发货方式历史数据回填脚本 2026-09-22 15:53:29 +08:00
WuYongJie
6f55f490f6 fix(supply-chain): complete personnel information source sync
(content-sync from origin/codex/xuxiake-mainline 0f52aca9)
2026-09-22 12:48:12 +08:00
居居
0f52aca923 Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/xuxiake-mainline 2026-09-22 12:01:43 +08:00
居居
c0cc39a565 fix(supply-chain): complete personnel information source sync 2026-09-22 11:59:28 +08:00
WuYongJie
41030ec7f2 feat(oa-board): 流程状态筛选新增「全部」口径
流程状态下拉新增「全部」:overallStatus=__all__ 时服务端与客户端过滤均跳过状态限制,包含已驳回、已终止、已取消及无状态标记数据;默认口径(进行中+已完成)不变,导出明细同口径生效。
2026-09-22 11:29:19 +08:00
WuYongJie
687e80b6cd feat(oa-board): 流程状态筛选新增「全部」口径
流程状态下拉新增「全部」:overallStatus=__all__ 时服务端与客户端过滤均跳过状态限制,包含已驳回、已终止、已取消及无状态标记数据;默认口径(进行中+已完成)不变,导出明细同口径生效。
2026-09-22 11:24:16 +08:00
居居
5512be5371 fix(supply-chain): 固化人员信息界面配置 2026-09-22 11:08:10 +08:00
居居
7b5f66dfcf fix(supply-chain): 固化人员信息界面配置 2026-09-22 10:33:18 +08:00
Codex
271c7b24b4 feat(supplier-recommended): add image similarity search 2026-09-22 10:20:05 +08:00
Codex
f94286464b feat(supplier-recommended): add image similarity search 2026-09-22 10:17:35 +08:00
WuYongJie
e3dcc2463a Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into HEAD 2026-09-21 21:27:48 +08:00
WuYongJie
4b54f3e531 fix(sync): guard against null skuoid rows in dws ops owners listing 2026-09-21 21:27:19 +08:00
WuYongJie
669fbd63fd Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into HEAD 2026-09-21 20:58:35 +08:00
WuYongJie
191e050374 fix(sync): drop server-only marker imports from the archive sync dependency chain 2026-09-21 20:58:07 +08:00
WuYongJie
9e0081724e Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into HEAD 2026-09-21 20:35:51 +08:00
WuYongJie
fd3b451af9 chore(sync): log full stack on enrichment failures 2026-09-21 20:34:25 +08:00
WuYongJie
616f985dee Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into HEAD 2026-09-21 19:28:31 +08:00
WuYongJie
16587f62af fix(sync): alias server-only to an empty stub for plain-node tooling runs 2026-09-21 19:28:06 +08:00
WuYongJie
f57fd6d354 Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into HEAD 2026-09-21 19:15:48 +08:00
WuYongJie
407946d5d2 chore(sync): record server-only and uuid in the deploy lock file 2026-09-21 19:15:24 +08:00
WuYongJie
37d09f0ca1 Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into HEAD 2026-09-21 19:11:37 +08:00
WuYongJie
3269f4409f fix(sync): declare server-only and uuid deps and drop the marker import so node-side scripts can run 2026-09-21 19:11:12 +08:00
WuYongJie
65b32ce405 Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into HEAD 2026-09-21 17:36:54 +08:00
WuYongJie
c4943e6b04 fix(product-archive): include stat_date in ops owners group by to unblock daily sync 2026-09-21 17:35:36 +08:00
居居
475478b273 feat(supply-chain): 新增人员信息与钉钉组织同步 2026-09-21 16:38:13 +08:00
WuYongJie
ec807b3217 Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/local-3001-integration
# Conflicts:
#	furniture-3d-system/prisma/schema.prisma
#	furniture-3d-system/src/app/api/supply-chain/new-products/dropship-issues/route.test.ts
#	furniture-3d-system/src/app/api/supply-chain/new-products/dropship-issues/route.ts
#	furniture-3d-system/src/app/api/supply-chain/new-products/dropship-issues/update/route.test.ts
#	furniture-3d-system/src/app/api/supply-chain/new-products/dropship-issues/update/route.ts
#	furniture-3d-system/src/app/supply-chain/config/supply-chain-menu.test.ts
#	furniture-3d-system/src/app/supply-chain/new-products/dropship-issues/DropshipIssueEditDialog.tsx
#	furniture-3d-system/src/app/supply-chain/new-products/dropship-issues/DropshipIssuesPagination.tsx
#	furniture-3d-system/src/app/supply-chain/new-products/dropship-issues/DropshipIssuesTable.tsx
#	furniture-3d-system/src/app/supply-chain/new-products/dropship-issues/DropshipOpsFilter.tsx
#	furniture-3d-system/src/app/supply-chain/new-products/dropship-issues/DropshipSkuPanel.tsx
#	furniture-3d-system/src/app/supply-chain/new-products/dropship-issues/dropship-issues.module.css
#	furniture-3d-system/src/app/supply-chain/new-products/dropship-issues/page.tsx
#	furniture-3d-system/src/app/supply-chain/new-products/dropship-issues/types.ts
#	furniture-3d-system/src/lib/supply-chain-permission-settings.test.ts
#	furniture-3d-system/src/lib/supply-chain-permission-settings.ts
#	furniture-3d-system/src/server/repositories/supply-chain-dropship-issue.repository.ts
#	furniture-3d-system/src/server/services/__tests__/supply-chain-dropship-issue.service.test.ts
#	furniture-3d-system/src/server/services/supply-chain-dropship-issue.service.ts
2026-09-21 15:58:02 +08:00
居居
e9cb61e18c Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/xuxiake-mainline 2026-09-21 14:25:22 +08:00
居居
1cf72e3c81 feat(supply-chain): 新增人员信息与钉钉组织同步 2026-09-21 14:25:05 +08:00
WuYongJie
4df5e800fe fix(1688): show zero for missing cost values
(cherry picked from commit ce48ab7944bed1a81c29eb36145bcd9d76bf01f8)
2026-09-21 11:50:13 +08:00
WuYongJie
ce48ab7944 fix(1688): show zero for missing cost values 2026-09-21 11:42:05 +08:00
WuYongJie
bb730dd533 安装演示工作台 5 项 bug 根因修复接手
(cherry picked from commit 073437c8bfe809f3992bb1a5288aa2c57f3c3567)
2026-09-21 11:33:53 +08:00
WuYongJie
073437c8bf 安装演示工作台 5 项 bug 根因修复接手 2026-09-21 11:02:32 +08:00
WuYongJie
b92d56c090 feat(intake): extend barcode import permissions
(cherry picked from commit d4778d29e9f618745f0777335336dd8986614d9f)
2026-09-21 11:01:48 +08:00
WuYongJie
d4778d29e9 feat(intake): extend barcode import permissions 2026-09-21 10:54:14 +08:00
WuYongJie
ba5eeaea33 feat(supply-chain): add estimated freight to 1688 dropship data
Task: supply-chain-1688-freight-inclusive-unit-cost-20260918
(cherry picked from commit 4450ac5ce610a45c5c06f7cced6bfa7a2f4af82a)
2026-09-21 10:00:40 +08:00
WuYongJie
4450ac5ce6 feat(supply-chain): add estimated freight to 1688 dropship data
Task: supply-chain-1688-freight-inclusive-unit-cost-20260918
2026-09-21 09:43:43 +08:00
WuYongJie
87a28cc5f7 fix(supply-chain): restore 1688 data menu order 2026-09-18 15:53:51 +08:00
WuYongJie
7d8f6d3848 fix(supply-chain): restore 1688 data menu order 2026-09-18 15:53:36 +08:00
WuYongJie
76420b8ecd fix(supply-chain): merge rapid 1688 field filters 2026-09-18 15:19:10 +08:00
伯阳
5a6f09b0df feat(supply-chain): complete 1688 dropship sourcing workflow 2026-09-18 15:18:56 +08:00
WuYongJie
009685cdd7 fix(supply-chain): merge rapid 1688 field filters 2026-09-18 15:16:55 +08:00
伯阳
9244b9d570 merge: sync xuxiake mainline for 1688 sourcing 2026-09-18 13:25:12 +08:00
伯阳
a22657e28e feat(supply-chain): complete 1688 dropship sourcing workflow 2026-09-18 11:17:47 +08:00
WuYongJie
446c550320 feat(operation-logs): 失败操作日志实时推送徐霞客
- 失败日志(FAILED/RETRY)写入后经壹点通知中心个人推送徐霞客
- 聚水潭固定虚拟库存错误码按 1 小时折叠成一条,其余按 动作+错误码+SPU+SKU 10 分钟合并
- 正文空行分段(钉钉 Markdown 会吞单个换行),纯文字无按钮
- record 挂载异步通知不阻塞业务,NODE_ENV=test 静默
- 新增带 worker 密钥的手动测试入口 /api/internal/operation-logs/failure-notification/run
2026-09-18 10:10:36 +08:00
WuYongJie
8ea81f886a feat(product-archive): 定厂「发货方式」同步进全量档案的入库/代发列
全量档案的「入库/代发」列(storageType)此前 73,278 行 100% 为空:产品定厂时
采购填的「发货方式」(入仓/代发)从来没有回流到档案表,列上没有任何来源映射。

规则(按用户口径):全量的入库/代发只有 代发/入仓 两个值——
- 新品(产品开发)定厂带了发货方式的,直接带进档案
- 没填的,等仓库配置同步兜底填空
- 无法识别的值(如 海运)不写,保持空

实现要点:
- sync.repository:产品开发来源的 SELECT/类型加 shipping_method;
  buildArchiveUpdate 白名单补 storageType(此前该列不在白名单里,值写不进档案表)
- sync.service:宽表 shipping_method 经 storageTypeFromShippingMethodLabel
  映射(入仓/入库→INBOUND、代发→DROPSHIP、无法识别→空串不动),
  仅非空时写入;fieldSourceMap 记 storageType ← shipping_method
- enrichment mapper:仓库配置来源拆成两条——warehouseHead 照常无条件更新;
  storageType 改 fillEmptyOnly(只填空),定厂带回的值不被仓库配置覆盖
- lib/values:新增 storageTypeFromShippingMethodLabel(入仓/入库→INBOUND、
  代发→DROPSHIP,兼容「入库」写法)
- 页面显示「入库」→「入仓」(与定厂填值一致)
- 测试:sync.service 加 3 用例;新增 warehouse-config-fill 套件 3 用例
  (已有值不覆盖 / 为空才填 / 初始化过不再重复应用);
  enrichment.service 计数断言 8→9 并补仓库配置两条的断言

验证:type-check EXIT=0;相关套件 sync 11 / enrichment 9 / warehouse-config-fill 3
全过;真库端到端——跑一次 XG0012 的产品开发同步,档案 storageType 从空变 INBOUND、
来源记 PRODUCT_DEVELOPMENT/shipping_method,再跑仓库配置 enrichment 后仍为 INBOUND
(不被覆盖);3001 真机按「入仓」筛选出 1 行、页面列显示「入仓」。
2026-09-18 10:10:36 +08:00
WuYongJie
f3f59adcbc 供应商新品推荐:页面默认视图改为明细数据 2026-09-18 10:10:36 +08:00
WuYongJie
b0bfd3d65a feat(operation-logs): 失败操作日志实时推送徐霞客
- 失败日志(FAILED/RETRY)写入后经壹点通知中心个人推送徐霞客
- 聚水潭固定虚拟库存错误码按 1 小时折叠成一条,其余按 动作+错误码+SPU+SKU 10 分钟合并
- 正文空行分段(钉钉 Markdown 会吞单个换行),纯文字无按钮
- record 挂载异步通知不阻塞业务,NODE_ENV=test 静默
- 新增带 worker 密钥的手动测试入口 /api/internal/operation-logs/failure-notification/run
2026-09-18 09:46:16 +08:00
WuYongJie
fc12269838 fix(jushuitan): batch fixed virtual inventory pushes 2026-09-18 09:39:26 +08:00
WuYongJie
9647a1f49f feat(operation-logs): 失败操作日志实时推送徐霞客
- 失败日志(FAILED/RETRY)写入后经壹点通知中心个人推送徐霞客
- 聚水潭固定虚拟库存错误码按 1 小时折叠成一条,其余按 动作+错误码+SPU+SKU 10 分钟合并
- 正文空行分段(钉钉 Markdown 会吞单个换行),纯文字无按钮
- record 挂载异步通知不阻塞业务,NODE_ENV=test 静默
- 新增带 worker 密钥的手动测试入口 /api/internal/operation-logs/failure-notification/run
2026-09-18 09:21:41 +08:00
WuYongJie
dc07310cda fix(jushuitan): batch fixed virtual inventory pushes 2026-09-18 09:20:17 +08:00
WuYongJie
e8f8087f78 fix(intake): 保存实时供应商目录映射 2026-09-17 17:28:08 +08:00
WuYongJie
be9f844c5a fix(intake): 保存实时供应商目录映射 2026-09-17 17:25:52 +08:00
WuYongJie
ca6e22ac66 feat(product-archive): 定厂「发货方式」同步进全量档案的入库/代发列
全量档案的「入库/代发」列(storageType)此前 73,278 行 100% 为空:产品定厂时
采购填的「发货方式」(入仓/代发)从来没有回流到档案表,列上没有任何来源映射。

规则(按用户口径):全量的入库/代发只有 代发/入仓 两个值——
- 新品(产品开发)定厂带了发货方式的,直接带进档案
- 没填的,等仓库配置同步兜底填空
- 无法识别的值(如 海运)不写,保持空

实现要点:
- sync.repository:产品开发来源的 SELECT/类型加 shipping_method;
  buildArchiveUpdate 白名单补 storageType(此前该列不在白名单里,值写不进档案表)
- sync.service:宽表 shipping_method 经 storageTypeFromShippingMethodLabel
  映射(入仓/入库→INBOUND、代发→DROPSHIP、无法识别→空串不动),
  仅非空时写入;fieldSourceMap 记 storageType ← shipping_method
- enrichment mapper:仓库配置来源拆成两条——warehouseHead 照常无条件更新;
  storageType 改 fillEmptyOnly(只填空),定厂带回的值不被仓库配置覆盖
- lib/values:新增 storageTypeFromShippingMethodLabel(入仓/入库→INBOUND、
  代发→DROPSHIP,兼容「入库」写法)
- 页面显示「入库」→「入仓」(与定厂填值一致)
- 测试:sync.service 加 3 用例;新增 warehouse-config-fill 套件 3 用例
  (已有值不覆盖 / 为空才填 / 初始化过不再重复应用);
  enrichment.service 计数断言 8→9 并补仓库配置两条的断言

验证:type-check EXIT=0;相关套件 sync 11 / enrichment 9 / warehouse-config-fill 3
全过;真库端到端——跑一次 XG0012 的产品开发同步,档案 storageType 从空变 INBOUND、
来源记 PRODUCT_DEVELOPMENT/shipping_method,再跑仓库配置 enrichment 后仍为 INBOUND
(不被覆盖);3001 真机按「入仓」筛选出 1 行、页面列显示「入仓」。
2026-09-17 16:58:54 +08:00
WuYongJie
8d38f07626 fix(intake): 修复大产品保存事务超时与聚水潭分批推送
- 非集成产品保存事务(formal SPU 主事务 / saveActiveWithLock / createRecord /
  updateRecord)统一加 { maxWait: 10s, timeout: 60s }:修复 224 SKU 大产品保存时
  Prisma 交互式事务超过默认 5s 触发 P2028 整笔回滚、页面提示保存成功但数据未落库
  的问题(正式站 LRSF0095 实际案例,单日 11 次保存失败,供应商数据全部丢失)
- 聚水潭商品资料推送分批:itemsku/upload 与 combinesku/upload 按每批 100 个 items
  推送(单次上限 200,LRSF0095 一次推 224 个被 code=130 拒绝),逐批成功落锁,
  中间批次失败抛 PartialFailureError 保护已成功 SKU
- 新增回归用例:250 SKU 断言 itemsku/upload 分 3 批(100/100/50)且总数无遗漏

验证:type-check 通过;intake-non-integrated-product.service.test.ts 108/108;
jushuitan-product-pricing-push.service.test.ts 33/34(唯一失败为既有环境问题)
2026-09-17 15:42:40 +08:00
WuYongJie
a310362839 fix(intake): 修复大产品保存事务超时与聚水潭分批推送
- 非集成产品保存事务(formal SPU 主事务 / saveActiveWithLock / createRecord /
  updateRecord)统一加 { maxWait: 10s, timeout: 60s }:修复 224 SKU 大产品保存时
  Prisma 交互式事务超过默认 5s 触发 P2028 整笔回滚、页面提示保存成功但数据未落库
  的问题(正式站 LRSF0095 实际案例,单日 11 次保存失败,供应商数据全部丢失)
- 聚水潭商品资料推送分批:itemsku/upload 与 combinesku/upload 按每批 100 个 items
  推送(单次上限 200,LRSF0095 一次推 224 个被 code=130 拒绝),逐批成功落锁,
  中间批次失败抛 PartialFailureError 保护已成功 SKU
- 新增回归用例:250 SKU 断言 itemsku/upload 分 3 批(100/100/50)且总数无遗漏

验证:type-check 通过;intake-non-integrated-product.service.test.ts 108/108;
jushuitan-product-pricing-push.service.test.ts 33/34(唯一失败为既有环境问题)
2026-09-17 15:37:45 +08:00
WuYongJie
14974d20ef fix(intake): 修复大产品保存事务超时与聚水潭分批推送
- 非集成产品保存事务(formal SPU 主事务 / saveActiveWithLock / createRecord /
  updateRecord)统一加 { maxWait: 10s, timeout: 60s }:修复 224 SKU 大产品保存时
  Prisma 交互式事务超过默认 5s 触发 P2028 整笔回滚、页面提示保存成功但数据未落库
  的问题(正式站 LRSF0095 实际案例,单日 11 次保存失败,供应商数据全部丢失)
- 聚水潭商品资料推送分批:itemsku/upload 与 combinesku/upload 按每批 100 个 items
  推送(单次上限 200,LRSF0095 一次推 224 个被 code=130 拒绝),逐批成功落锁,
  中间批次失败抛 PartialFailureError 保护已成功 SKU
- 新增回归用例:250 SKU 断言 itemsku/upload 分 3 批(100/100/50)且总数无遗漏

验证:type-check 通过;intake-non-integrated-product.service.test.ts 108/108;
jushuitan-product-pricing-push.service.test.ts 33/34(唯一失败为既有环境问题)
2026-09-17 15:36:22 +08:00
WuYongJie
26754ae266 供应商新品推荐:页面默认视图改为明细数据 2026-09-17 14:56:22 +08:00
WuYongJie
98482bccec feat(product-archive): 定厂「发货方式」同步进全量档案的入库/代发列
全量档案的「入库/代发」列(storageType)此前 73,278 行 100% 为空:产品定厂时
采购填的「发货方式」(入仓/代发)从来没有回流到档案表,列上没有任何来源映射。

规则(按用户口径):全量的入库/代发只有 代发/入仓 两个值——
- 新品(产品开发)定厂带了发货方式的,直接带进档案
- 没填的,等仓库配置同步兜底填空
- 无法识别的值(如 海运)不写,保持空

实现要点:
- sync.repository:产品开发来源的 SELECT/类型加 shipping_method;
  buildArchiveUpdate 白名单补 storageType(此前该列不在白名单里,值写不进档案表)
- sync.service:宽表 shipping_method 经 storageTypeFromShippingMethodLabel
  映射(入仓/入库→INBOUND、代发→DROPSHIP、无法识别→空串不动),
  仅非空时写入;fieldSourceMap 记 storageType ← shipping_method
- enrichment mapper:仓库配置来源拆成两条——warehouseHead 照常无条件更新;
  storageType 改 fillEmptyOnly(只填空),定厂带回的值不被仓库配置覆盖
- lib/values:新增 storageTypeFromShippingMethodLabel(入仓/入库→INBOUND、
  代发→DROPSHIP,兼容「入库」写法)
- 页面显示「入库」→「入仓」(与定厂填值一致)
- 测试:sync.service 加 3 用例;新增 warehouse-config-fill 套件 3 用例
  (已有值不覆盖 / 为空才填 / 初始化过不再重复应用);
  enrichment.service 计数断言 8→9 并补仓库配置两条的断言

验证:type-check EXIT=0;相关套件 sync 11 / enrichment 9 / warehouse-config-fill 3
全过;真库端到端——跑一次 XG0012 的产品开发同步,档案 storageType 从空变 INBOUND、
来源记 PRODUCT_DEVELOPMENT/shipping_method,再跑仓库配置 enrichment 后仍为 INBOUND
(不被覆盖);3001 真机按「入仓」筛选出 1 行、页面列显示「入仓」。
2026-09-17 14:55:11 +08:00
WuYongJie
ba930d1df2 feat(product-archive): SPU 列支持「只看正常编码」与可累加排除规则
产品全量档案的 SPU 列筛选原来只有「勾选具体值 = 包含」一种语义,没法表达
「不要 DF- 开头的 / 不要纯数字的」这类排除需求,只能靠不断放宽正选来绕。

新增两类能力(原有「勾选值包含」语义一字未改):
- 「只看正常编码」:字母前缀(2~6位) + 可选「-字母段」 + 数字编号(3~8位),
  一次收掉纯数字 / 1643A 型 / 含中文的商品名 / 32 位哈希等非正常形态
- 「排除以…开头」:可累加的前缀黑名单(如 WM- 外贸、DF- 代发、MKH),
  对应「还有不想看就再填一条」

实现要点:
- lib:新增 ProductArchiveTextFilterKey / ProductArchiveExcludeRule 与
  PRODUCT_ARCHIVE_NORMAL_CODE_PATTERN;ProductArchiveFilters 只追加
  normalCodeOnly、excludeStartsWith 两个可选字段,老字段类型与语义不动
- 校验:zod schema 是 .strict(),同步放行两个新字段(否则整包筛选会被 400 拒掉)
- 服务端:buildProductArchiveWhere 保持同步,只追加 NOT startsWith;
  需要查库的「正常编码」放在新增的异步 buildProductArchiveWhereWithTextRules
  (Prisma 7.3 的字符串过滤不支持 regex,改为原生 SQL 取符合形态的取值清单拼 IN,
  带 5 分钟缓存)。preparePeriodArchiveQuery 是全站唯一取数入口,
  列表、候选项、导出、图片导出自动生效
- 文本规则故意不参与 omitFilter:若参与,候选值列表会冒出已被排除的取值,
  用户勾了却得到 0 行
- UI:文本列弹层改左右双排(左=形态与排除规则,右=搜索与候选值),
  弹层高度与横向偏移按触发按钮所在位置动态计算,保证清除/应用在任何视口都可见
- 偏好:两个新字段进入 sanitizeFilters 白名单,规则可持久化,点清除即清空

验证:本地真实库 66033 行——只勾正常编码 27645 行、再接 WM- 26852 行、
再接 DF- 26788 行,与 SQL 基准逐一致;3001 HTTP 对账五个口径全对、
非法列返回 400、候选项接口在勾了正常编码后返回的 SPU 全部符合形态;
真机 UI 12/12(应用 26788、刷新记住、回显、清除回 66033);
布局专项 36/36(1600x900 / 1440x720 / 1366x640 / 1280x600 下弹层均在视口内、
按钮可见、不超左右边界);新增单测 15/15;type-check EXIT=0。
2026-09-17 11:46:18 +08:00
WuYongJie
3cd14c4f0d feat(product-archive): SPU 列支持「只看正常编码」与可累加排除规则
产品全量档案的 SPU 列筛选原来只有「勾选具体值 = 包含」一种语义,没法表达
「不要 DF- 开头的 / 不要纯数字的」这类排除需求,只能靠不断放宽正选来绕。

新增两类能力(原有「勾选值包含」语义一字未改):
- 「只看正常编码」:字母前缀(2~6位) + 可选「-字母段」 + 数字编号(3~8位),
  一次收掉纯数字 / 1643A 型 / 含中文的商品名 / 32 位哈希等非正常形态
- 「排除以…开头」:可累加的前缀黑名单(如 WM- 外贸、DF- 代发、MKH),
  对应「还有不想看就再填一条」

实现要点:
- lib:新增 ProductArchiveTextFilterKey / ProductArchiveExcludeRule 与
  PRODUCT_ARCHIVE_NORMAL_CODE_PATTERN;ProductArchiveFilters 只追加
  normalCodeOnly、excludeStartsWith 两个可选字段,老字段类型与语义不动
- 校验:zod schema 是 .strict(),同步放行两个新字段(否则整包筛选会被 400 拒掉)
- 服务端:buildProductArchiveWhere 保持同步,只追加 NOT startsWith;
  需要查库的「正常编码」放在新增的异步 buildProductArchiveWhereWithTextRules
  (Prisma 7.3 的字符串过滤不支持 regex,改为原生 SQL 取符合形态的取值清单拼 IN,
  带 5 分钟缓存)。preparePeriodArchiveQuery 是全站唯一取数入口,
  列表、候选项、导出、图片导出自动生效
- 文本规则故意不参与 omitFilter:若参与,候选值列表会冒出已被排除的取值,
  用户勾了却得到 0 行
- UI:文本列弹层改左右双排(左=形态与排除规则,右=搜索与候选值),
  弹层高度与横向偏移按触发按钮所在位置动态计算,保证清除/应用在任何视口都可见
- 偏好:两个新字段进入 sanitizeFilters 白名单,规则可持久化,点清除即清空

验证:本地真实库 66033 行——只勾正常编码 27645 行、再接 WM- 26852 行、
再接 DF- 26788 行,与 SQL 基准逐一致;3001 HTTP 对账五个口径全对、
非法列返回 400、候选项接口在勾了正常编码后返回的 SPU 全部符合形态;
真机 UI 12/12(应用 26788、刷新记住、回显、清除回 66033);
布局专项 36/36(1600x900 / 1440x720 / 1366x640 / 1280x600 下弹层均在视口内、
按钮可见、不超左右边界);新增单测 15/15;type-check EXIT=0。
2026-09-17 11:36:12 +08:00
WuYongJie
57e422308b feat(product-archive): SPU 列支持「只看正常编码」与可累加排除规则
产品全量档案的 SPU 列筛选原来只有「勾选具体值 = 包含」一种语义,没法表达
「不要 DF- 开头的 / 不要纯数字的」这类排除需求,只能靠不断放宽正选来绕。

新增两类能力(原有「勾选值包含」语义一字未改):
- 「只看正常编码」:字母前缀(2~6位) + 可选「-字母段」 + 数字编号(3~8位),
  一次收掉纯数字 / 1643A 型 / 含中文的商品名 / 32 位哈希等非正常形态
- 「排除以…开头」:可累加的前缀黑名单(如 WM- 外贸、DF- 代发、MKH),
  对应「还有不想看就再填一条」

实现要点:
- lib:新增 ProductArchiveTextFilterKey / ProductArchiveExcludeRule 与
  PRODUCT_ARCHIVE_NORMAL_CODE_PATTERN;ProductArchiveFilters 只追加
  normalCodeOnly、excludeStartsWith 两个可选字段,老字段类型与语义不动
- 校验:zod schema 是 .strict(),同步放行两个新字段(否则整包筛选会被 400 拒掉)
- 服务端:buildProductArchiveWhere 保持同步,只追加 NOT startsWith;
  需要查库的「正常编码」放在新增的异步 buildProductArchiveWhereWithTextRules
  (Prisma 7.3 的字符串过滤不支持 regex,改为原生 SQL 取符合形态的取值清单拼 IN,
  带 5 分钟缓存)。preparePeriodArchiveQuery 是全站唯一取数入口,
  列表、候选项、导出、图片导出自动生效
- 文本规则故意不参与 omitFilter:若参与,候选值列表会冒出已被排除的取值,
  用户勾了却得到 0 行
- UI:文本列弹层改左右双排(左=形态与排除规则,右=搜索与候选值),
  弹层高度与横向偏移按触发按钮所在位置动态计算,保证清除/应用在任何视口都可见
- 偏好:两个新字段进入 sanitizeFilters 白名单,规则可持久化,点清除即清空

验证:本地真实库 66033 行——只勾正常编码 27645 行、再接 WM- 26852 行、
再接 DF- 26788 行,与 SQL 基准逐一致;3001 HTTP 对账五个口径全对、
非法列返回 400、候选项接口在勾了正常编码后返回的 SPU 全部符合形态;
真机 UI 12/12(应用 26788、刷新记住、回显、清除回 66033);
布局专项 36/36(1600x900 / 1440x720 / 1366x640 / 1280x600 下弹层均在视口内、
按钮可见、不超左右边界);新增单测 15/15;type-check EXIT=0。
2026-09-17 11:32:53 +08:00
WuYongJie
cffa8b64f8 merge: release BOM 预估 v2(价格回退工具/case 级预估卡/预览页/缓存表) 2026-09-16 17:37:42 +08:00
WuYongJie
b0036bcc48 merge: BOM 预估 v2 落地——价格回退工具、case 级预估卡编排、预估卡预览页、结果缓存表 2026-09-16 17:37:26 +08:00
WuYongJie
f30866530a Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/wuyongjie-bom-estimate-agent-phase0 2026-09-16 17:37:23 +08:00
WuYongJie
48871454e9 feat(bom-costing): BOM 预估 v2 —— 价格回退工具、case 级预估卡编排与预览页
- agent 工具:尺寸感知镜像检索 + 分类画像三级级联(包装 v1 回退链,导出纯函数复用)
- case 级编排:首批代表 SKU 自动精算,其余按需点算;结果持久缓存(新表 BomCostingAgentSkuEstimate,幂等迁移)
- EstimateCard 契约:代码侧聚合五行构成/区间包络/最小置信度/不确定点并集
- API:/api/bom-costing/cases/[id]/agent-estimate(GET 缓存卡 + POST 运行,复用模块鉴权)
- 预览页:/bom-costing/estimate(预估卡 + SKU 列表按需点算 + 竞品图片浏览,旁路不过闸门不设默认入口)
- Select 组件:透传 id/aria 到触发按钮;修复 skill 知识未注入循环的缺陷
- 基线工具:report:bom-agent-baseline(定价覆盖率 + shadow 聚合,含隧道重试)
- 设计文档:docs/架构设计/BOM成本预估v2-agent方案设计-20260916.md
2026-09-16 17:37:06 +08:00
WuYongJie
a3b9fa0fbb merge: release 产品全量档案流式导出 2026-09-16 16:21:14 +08:00
WuYongJie
6f023e3f0d fix(product-archive): 导出改为流式写出,修复全量导出 OOM 崩溃
正式站导出「产品全量档案资料」必然崩溃:整包写法在 89 列下约 60MB/千行,
生产 Node 堆被 PM2 限到 768MB,实际只能撑约 5000 行,而档案表已有 7 万余行。
PM2 记录 exited with code [0] via signal [SIGABRT],error log 19 次
FATAL ERROR: Reached heap limit,最后一次 GC dump 763.5MB(= 堆上限)。

改成「先冻结行 id 快照 → 每 2000 行分批取数 → ExcelJS 流式写出 → 边生成边下发」,
服务端堆占用与导出总行数解耦;并加同机并发上限 3(超限 429)兜底。

- repository:listRows 全量取数改为 openRowSource。先冻结本次要导的有序 id 快照,
  再按 id 分批取完整行并按冻结顺序重排,避免导出期间上游同步导致行集合错位。
- service:createWorkbook 整包 writeBuffer 改为 openWorkbook 流式写出。
  useSharedStrings:false(共享字符串表会累积全部唯一文本且无法回收);
  冻结首行走 addWorksheet 的 views 选项(流式 worksheet 的 views 只有 getter,
  直接赋值会被静默忽略);并发额度在「正常写完」与「客户端断开」两条路径都归还。
- route:改为流式响应,不再发 Content-Length(改 chunked);监听 request.signal,
  客户端断开时销毁生成流;响应头发出后生成失败只能掐断流,故在路由层记日志。
- 前端:下载态文案改为「正在生成并下载文件...」;无 Content-Length 时显示「已接收 X」;
  收流中断统一提示为可读文案,不再只报网络错误。
- 测试:5 个用例,含冻结首行回归守护与「客户端中途断开不泄漏并发额度」守护。

验证:本地真实库 66033 行 × 89 列,NODE_OPTIONS=--max-old-space-size=768 下
首字节 0.5s / 总耗时 26.7s / 产物 25.8MB / 峰值 heapUsed 152.5MB(对照:整包写法
同规模在 4GB 堆直接 OOM)。3001 真实路由 13951 行:status 200、无 Content-Length、
chunked、首字节 0.2s、5.5s / 5.6MB。产物特性 10/10(表名、冻结首行、autoFilter
A1:CK1、89 列宽度、表头加粗与文本、行数),并用 openpyxl 独立交叉复核一致。
jest 5/5 通过、type-check EXIT=0。

已知边界:只在本机 next dev 验证,未跑 next build / next start。
2026-09-16 16:07:31 +08:00
WuYongJie
c5e1c50418 fix(product-archive): 导出改为流式写出,修复全量导出 OOM 崩溃
正式站导出「产品全量档案资料」必然崩溃:整包写法在 89 列下约 60MB/千行,
生产 Node 堆被 PM2 限到 768MB,实际只能撑约 5000 行,而档案表已有 7 万余行。
PM2 记录 exited with code [0] via signal [SIGABRT],error log 19 次
FATAL ERROR: Reached heap limit,最后一次 GC dump 763.5MB(= 堆上限)。

改成「先冻结行 id 快照 → 每 2000 行分批取数 → ExcelJS 流式写出 → 边生成边下发」,
服务端堆占用与导出总行数解耦;并加同机并发上限 3(超限 429)兜底。

- repository:listRows 全量取数改为 openRowSource。先冻结本次要导的有序 id 快照,
  再按 id 分批取完整行并按冻结顺序重排,避免导出期间上游同步导致行集合错位。
- service:createWorkbook 整包 writeBuffer 改为 openWorkbook 流式写出。
  useSharedStrings:false(共享字符串表会累积全部唯一文本且无法回收);
  冻结首行走 addWorksheet 的 views 选项(流式 worksheet 的 views 只有 getter,
  直接赋值会被静默忽略);并发额度在「正常写完」与「客户端断开」两条路径都归还。
- route:改为流式响应,不再发 Content-Length(改 chunked);监听 request.signal,
  客户端断开时销毁生成流;响应头发出后生成失败只能掐断流,故在路由层记日志。
- 前端:下载态文案改为「正在生成并下载文件...」;无 Content-Length 时显示「已接收 X」;
  收流中断统一提示为可读文案,不再只报网络错误。
- 测试:5 个用例,含冻结首行回归守护与「客户端中途断开不泄漏并发额度」守护。

验证:本地真实库 66033 行 × 89 列,NODE_OPTIONS=--max-old-space-size=768 下
首字节 0.5s / 总耗时 26.7s / 产物 25.8MB / 峰值 heapUsed 152.5MB(对照:整包写法
同规模在 4GB 堆直接 OOM)。3001 真实路由 13951 行:status 200、无 Content-Length、
chunked、首字节 0.2s、5.5s / 5.6MB。产物特性 10/10(表名、冻结首行、autoFilter
A1:CK1、89 列宽度、表头加粗与文本、行数),并用 openpyxl 独立交叉复核一致。
jest 5/5 通过、type-check EXIT=0。

已知边界:只在本机 next dev 验证,未跑 next build / next start。
2026-09-16 16:02:44 +08:00
WuYongJie
990a4bfeb1 merge: release 商品抠图模型目录合并与选择器升级 + BOM agent 旁路引擎
# Conflicts:
#	furniture-3d-system/src/app/supply-chain/config/supply-chain-menu.test.ts
#	furniture-3d-system/src/app/supply-chain/config/supply-chain-menu.ts
2026-09-16 15:19:50 +08:00
WuYongJie
9533c2e4ec merge: 商品抠图配置合并上游专用抠图模型目录 + BOM 核算 agent 旁路引擎与文本脱敏契约
- ai-config: 任一协议模式合并网关与异步两份模型目录,抠图模型置顶标记,选中自动对齐协议
- bom-costing/agent: 估算引擎(契约/工具/循环/网关适配/组装器,27 测试)旁路试验版
- lib: 预估文本对外脱敏契约共享模块(public-estimate-text)
- scripts: shadow 偏差报表与 agent 试跑工具(工程域专用)
2026-09-16 15:17:03 +08:00
WuYongJie
6492c2e400 Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/wuyongjie-bom-estimate-agent-phase0 2026-09-16 15:14:00 +08:00
WuYongJie
20755dfdd1 feat(ai-config): 商品抠图配置合并上游专用抠图模型目录并升级选择器
- 目录服务:商品抠图任一协议模式都合并通用网关与异步媒体任务两份目录,
  模型带 source 来源标记,抠图模型置顶;提取器按端点路径选择
- 配置页:原生 select 换平台 Select(可搜索、暗色协调),抠图模型带
  【抠图】标记,选中后运行协议自动对齐异步媒体任务
- Select 组件:rest props 透传到触发按钮,支持 aria-label/id
2026-09-16 15:13:52 +08:00
WuYongJie
f8d74a5543 fix(intake): 补齐按SKU锁定与导入保护 2026-09-16 14:21:57 +08:00
WuYongJie
d73508bae6 Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/wuyongjie-bom-estimate-agent-phase0 2026-09-16 14:18:51 +08:00
WuYongJie
2d93a22721 feat(bom-costing): agent 估算引擎(旁路试验版)
- agent/ 契约+工具+接线+循环+skill知识+网关适配+端到端组装器(27 测试)
- agent 只读仓储(归档素材/镜像价格/材质目录/案例与SKU简报)
- shadow 偏差报表与 trial 试跑脚本(工程域专用)
- 纪律:单价只来自工具结果、总额代码汇总、硬预算+trace、1688 仅中性摘要
2026-09-16 14:18:39 +08:00
WuYongJie
9983ab3a1a fix(product-archive): 固定列不再透字,操作列改图标按钮
问题:产品全量档案的「序号」「操作」是两个 position: sticky 固定列。
主题样式把半透明的 --sc-hover(rgba(17,19,21,0.035))用 !important 打到了
这两个固定列上,覆盖了组件自带的不透明底色;横向滚动时压在固定列下方的
单元格文字就会透出来,污染序号列与操作列。

修复:
- .archive-td-index / .archive-td-actions 从半透明 hover 规则中摘出,
  改为「不透明底色 var(--sc-surface) + linear-gradient(var(--sc-hover)) 叠加色层」,
  视觉与原来一致,但底色恒不透明(浅色与深色主题均适用)
- 操作列「查看 / 更多」文字按钮改为图标按钮(Eye / MoreHorizontal),
  补 title 与 aria-label,菜单项加图标并改用 --sc-* 主题变量配色
- 操作列宽度由 138px 收窄到 96px(colgroup / th / td 同步)

验证:3001 真机 puppeteer 复验 9/9 通过——hover 与横向滚动后两个固定列
背景均为不透明 rgb;操作列仅 2 个图标、无文字、宽度 96px;「更多」菜单与
「商品修改日志」入口正常,图标「查看」仍能打开条码档案详情。
type-check EXIT=0。jest 的 3 个失败用例均与本改动无关(主体文件属其他线程
未提交改动,或与 HEAD 逐字节一致属既有失败)。
2026-09-16 14:08:17 +08:00
WuYongJie
9c9a6a9a08 fix(product-archive): 固定列不再透字,操作列改图标按钮
问题:产品全量档案的「序号」「操作」是两个 position: sticky 固定列。
主题样式把半透明的 --sc-hover(rgba(17,19,21,0.035))用 !important 打到了
这两个固定列上,覆盖了组件自带的不透明底色;横向滚动时压在固定列下方的
单元格文字就会透出来,污染序号列与操作列。

修复:
- .archive-td-index / .archive-td-actions 从半透明 hover 规则中摘出,
  改为「不透明底色 var(--sc-surface) + linear-gradient(var(--sc-hover)) 叠加色层」,
  视觉与原来一致,但底色恒不透明(浅色与深色主题均适用)
- 操作列「查看 / 更多」文字按钮改为图标按钮(Eye / MoreHorizontal),
  补 title 与 aria-label,菜单项加图标并改用 --sc-* 主题变量配色
- 操作列宽度由 138px 收窄到 96px(colgroup / th / td 同步)

验证:3001 真机 puppeteer 复验 9/9 通过——hover 与横向滚动后两个固定列
背景均为不透明 rgb;操作列仅 2 个图标、无文字、宽度 96px;「更多」菜单与
「商品修改日志」入口正常,图标「查看」仍能打开条码档案详情。
type-check EXIT=0。jest 的 3 个失败用例均与本改动无关(主体文件属其他线程
未提交改动,或与 HEAD 逐字节一致属既有失败)。
2026-09-16 13:54:43 +08:00
WuYongJie
570dcacdc9 fix(product-archive): 固定列不再透字,操作列改图标按钮
问题:产品全量档案的「序号」「操作」是两个 position: sticky 固定列。
主题样式把半透明的 --sc-hover(rgba(17,19,21,0.035))用 !important 打到了
这两个固定列上,覆盖了组件自带的不透明底色;横向滚动时压在固定列下方的
单元格文字就会透出来,污染序号列与操作列。

修复:
- .archive-td-index / .archive-td-actions 从半透明 hover 规则中摘出,
  改为「不透明底色 var(--sc-surface) + linear-gradient(var(--sc-hover)) 叠加色层」,
  视觉与原来一致,但底色恒不透明(浅色与深色主题均适用)
- 操作列「查看 / 更多」文字按钮改为图标按钮(Eye / MoreHorizontal),
  补 title 与 aria-label,菜单项加图标并改用 --sc-* 主题变量配色
- 操作列宽度由 138px 收窄到 96px(colgroup / th / td 同步)

验证:3001 真机 puppeteer 复验 9/9 通过——hover 与横向滚动后两个固定列
背景均为不透明 rgb;操作列仅 2 个图标、无文字、宽度 96px;「更多」菜单与
「商品修改日志」入口正常,图标「查看」仍能打开条码档案详情。
type-check EXIT=0。jest 的 3 个失败用例均与本改动无关(主体文件属其他线程
未提交改动,或与 HEAD 逐字节一致属既有失败)。
2026-09-16 13:52:12 +08:00
WuYongJie
adac0dfc3a fix(intake): 补齐按SKU锁定与导入保护 2026-09-16 13:47:39 +08:00
WuYongJie
374a39b6dc feat(intake): 成品寄样支持一键填是/填否批量设置是否寄出
在产品开发明细-成品寄样工作区,「请寄样」旁新增「一键填否」「一键填是」,
一键把当前批次显示的 SKU 行的「是否寄出」填好,之后仍可单独改个别行
(例:30 个 SKU 先全填否,再把第 1 个改成是)。

实现要点:
- 只填空行,不覆盖已选过的行;目标为当前批次显示行的 skuId 集合。
- 单行默认值计算抽成 buildFinishedSampleShipmentRowWithShipped 复用,
  批量走 applyFinishedSampleShipmentDecisionToRows(无变化时返回原引用)。
- 权限沿用 canEditFinishedSampleShipmentDecision;无权限或无待填写行时按钮置灰。
- HeaderActions 经 DocumentWorkspaceHeaderActionsSlot 透传动作、权限与待填写行数。

验证:
- type-check EXIT=0
- 新增单测 8 例 + 组件测试 5 例 + 集成测试 3 例 全通过
- 既有回归 buildNonIntegratedDetailChrome / buildNonIntegratedDocumentWorkspaceHeaderActions 通过
- 3001 真机(puppeteer)确认按钮组与交互元素就位,用户目视验收通过
2026-09-16 13:12:28 +08:00
WuYongJie
60034dfff0 test(intake): 样包测量只读断言随锁定口径对齐
非集成权限工作流用例中原断言期望样包测量「已确认」行仍可编辑,
与本次落地的口径(已确认即不可改、取消确认后方可改)冲突,
随主提交一并校正为期望 readonly,并加注释说明规则来源。

补 d3dc8821 遗漏的这一个测试文件(主提交已含对应实现)。
2026-09-16 13:12:28 +08:00
WuYongJie
44c69bef3e feat(intake): 产品定厂与样包测量按SKU取消确认
为采购、跟单、管理员增加按当前批次取消样包测量 / 产品定厂确认的能力,
取消只退回未推送聚水潭的 SKU,已推送 SKU 保持锁定,且不清空任何业务字段。

主要改动:
- 取消确认:新增 workflow-confirmation/cancel 接口与服务端按结构化
  systemEnteredAt 逐 SKU 重算逻辑;严格要求先取消样包测量再取消产品定厂;
  「全部」页签不开放取消入口,仅具体批次页签可取消
- 编辑锁逐 SKU 化:基础信息、产品定厂、样包测量、产品核价改为逐 SKU 判定,
  旧页面保存、整件删除、并发聚水潭推送均增加已推送 SKU 保护
- 样包测量锁定口径对齐:已确认即不可改,取消确认后可改;
  界面只读、写入拦截与 Excel 导入共用同一判定
- 基础信息修订签名纳入产品公共字段,公共字段变更自动退回当前批次基础信息确认
- 修复产品定厂回写误退回基础信息确认:手动同步侧与派生侧统一使用
  buildBasicInfoRevisionSignature,避免两侧签名形态错位导致比对恒不等
- 修复 advisory lock 被 Prisma 按 void 结果反序列化导致的保存报错
  (无返回值加锁改用 executeRaw,产品行 FOR UPDATE 仍走 queryRaw)
- 并发保护:普通保存、正式 SPU 分配、资产中心系列部件写入、AI白底图乐观保存
  统一接入产品级锁与锁内最新数据重读,避免旧快照覆盖取消结果
2026-09-16 13:12:28 +08:00
WuYongJie
2a7d2b04a2 feat(intake): 成品寄样支持一键填是/填否批量设置是否寄出
在产品开发明细-成品寄样工作区,「请寄样」旁新增「一键填否」「一键填是」,
一键把当前批次显示的 SKU 行的「是否寄出」填好,之后仍可单独改个别行
(例:30 个 SKU 先全填否,再把第 1 个改成是)。

实现要点:
- 只填空行,不覆盖已选过的行;目标为当前批次显示行的 skuId 集合。
- 单行默认值计算抽成 buildFinishedSampleShipmentRowWithShipped 复用,
  批量走 applyFinishedSampleShipmentDecisionToRows(无变化时返回原引用)。
- 权限沿用 canEditFinishedSampleShipmentDecision;无权限或无待填写行时按钮置灰。
- HeaderActions 经 DocumentWorkspaceHeaderActionsSlot 透传动作、权限与待填写行数。

验证:
- type-check EXIT=0
- 新增单测 8 例 + 组件测试 5 例 + 集成测试 3 例 全通过
- 既有回归 buildNonIntegratedDetailChrome / buildNonIntegratedDocumentWorkspaceHeaderActions 通过
- 3001 真机(puppeteer)确认按钮组与交互元素就位,用户目视验收通过
2026-09-16 12:10:53 +08:00
WuYongJie
f9d29ba92f feat(intake): 成品寄样支持一键填是/填否批量设置是否寄出
在产品开发明细-成品寄样工作区,「请寄样」旁新增「一键填否」「一键填是」,
一键把当前批次显示的 SKU 行的「是否寄出」填好,之后仍可单独改个别行
(例:30 个 SKU 先全填否,再把第 1 个改成是)。

实现要点:
- 只填空行,不覆盖已选过的行;目标为当前批次显示行的 skuId 集合。
- 单行默认值计算抽成 buildFinishedSampleShipmentRowWithShipped 复用,
  批量走 applyFinishedSampleShipmentDecisionToRows(无变化时返回原引用)。
- 权限沿用 canEditFinishedSampleShipmentDecision;无权限或无待填写行时按钮置灰。
- HeaderActions 经 DocumentWorkspaceHeaderActionsSlot 透传动作、权限与待填写行数。

验证:
- type-check EXIT=0
- 新增单测 8 例 + 组件测试 5 例 + 集成测试 3 例 全通过
- 既有回归 buildNonIntegratedDetailChrome / buildNonIntegratedDocumentWorkspaceHeaderActions 通过
- 3001 真机(puppeteer)确认按钮组与交互元素就位,用户目视验收通过
2026-09-16 12:06:09 +08:00
WuYongJie
495ff9f30b test(intake): 样包测量只读断言随锁定口径对齐
非集成权限工作流用例中原断言期望样包测量「已确认」行仍可编辑,
与本次落地的口径(已确认即不可改、取消确认后方可改)冲突,
随主提交一并校正为期望 readonly,并加注释说明规则来源。

补 d3dc8821 遗漏的这一个测试文件(主提交已含对应实现)。
2026-09-16 12:01:21 +08:00
WuYongJie
3ae72e800f feat(intake): 产品定厂与样包测量按SKU取消确认
为采购、跟单、管理员增加按当前批次取消样包测量 / 产品定厂确认的能力,
取消只退回未推送聚水潭的 SKU,已推送 SKU 保持锁定,且不清空任何业务字段。

主要改动:
- 取消确认:新增 workflow-confirmation/cancel 接口与服务端按结构化
  systemEnteredAt 逐 SKU 重算逻辑;严格要求先取消样包测量再取消产品定厂;
  「全部」页签不开放取消入口,仅具体批次页签可取消
- 编辑锁逐 SKU 化:基础信息、产品定厂、样包测量、产品核价改为逐 SKU 判定,
  旧页面保存、整件删除、并发聚水潭推送均增加已推送 SKU 保护
- 样包测量锁定口径对齐:已确认即不可改,取消确认后可改;
  界面只读、写入拦截与 Excel 导入共用同一判定
- 基础信息修订签名纳入产品公共字段,公共字段变更自动退回当前批次基础信息确认
- 修复产品定厂回写误退回基础信息确认:手动同步侧与派生侧统一使用
  buildBasicInfoRevisionSignature,避免两侧签名形态错位导致比对恒不等
- 修复 advisory lock 被 Prisma 按 void 结果反序列化导致的保存报错
  (无返回值加锁改用 executeRaw,产品行 FOR UPDATE 仍走 queryRaw)
- 并发保护:普通保存、正式 SPU 分配、资产中心系列部件写入、AI白底图乐观保存
  统一接入产品级锁与锁内最新数据重读,避免旧快照覆盖取消结果
2026-09-16 12:01:21 +08:00
WuYongJie
a4bff5dd59 test(intake): 样包测量只读断言随锁定口径对齐
非集成权限工作流用例中原断言期望样包测量「已确认」行仍可编辑,
与本次落地的口径(已确认即不可改、取消确认后方可改)冲突,
随主提交一并校正为期望 readonly,并加注释说明规则来源。

补 d3dc8821 遗漏的这一个测试文件(主提交已含对应实现)。
2026-09-16 11:52:18 +08:00
WuYongJie
d3dc8821c2 feat(intake): 产品定厂与样包测量按SKU取消确认
为采购、跟单、管理员增加按当前批次取消样包测量 / 产品定厂确认的能力,
取消只退回未推送聚水潭的 SKU,已推送 SKU 保持锁定,且不清空任何业务字段。

主要改动:
- 取消确认:新增 workflow-confirmation/cancel 接口与服务端按结构化
  systemEnteredAt 逐 SKU 重算逻辑;严格要求先取消样包测量再取消产品定厂;
  「全部」页签不开放取消入口,仅具体批次页签可取消
- 编辑锁逐 SKU 化:基础信息、产品定厂、样包测量、产品核价改为逐 SKU 判定,
  旧页面保存、整件删除、并发聚水潭推送均增加已推送 SKU 保护
- 样包测量锁定口径对齐:已确认即不可改,取消确认后可改;
  界面只读、写入拦截与 Excel 导入共用同一判定
- 基础信息修订签名纳入产品公共字段,公共字段变更自动退回当前批次基础信息确认
- 修复产品定厂回写误退回基础信息确认:手动同步侧与派生侧统一使用
  buildBasicInfoRevisionSignature,避免两侧签名形态错位导致比对恒不等
- 修复 advisory lock 被 Prisma 按 void 结果反序列化导致的保存报错
  (无返回值加锁改用 executeRaw,产品行 FOR UPDATE 仍走 queryRaw)
- 并发保护:普通保存、正式 SPU 分配、资产中心系列部件写入、AI白底图乐观保存
  统一接入产品级锁与锁内最新数据重读,避免旧快照覆盖取消结果
2026-09-16 11:41:32 +08:00
WuYongJie
31dfacab23 1688代发异常事项:当月业绩列/供应商列/整装拆装维护 + Excel式列头筛选
- 列表新增当月真实业绩列(SPU 汇总条码当月值,条码展开行显示自身),当月无业绩整行过滤
- 新增供应商列(取 SPU 内非空 supplierName),列顺序调整为 SPU|业绩|供应商|运营
- 维护抽屉新增整装/拆装二选一,标记整装的 SPU 在"是否有图纸"列显示"整装不需要图纸"
- 顶部有无筛选与 Excel 式列头筛选(含 filter-options 接口)
- 新增迁移 20260915180000_add_dropship_issue_assembly_type(SupplyChainDropshipIssue.assemblyType)
2026-09-16 08:50:57 +08:00
WuYongJie
9e50804b2e 1688代发异常事项:当月业绩列/供应商列/整装拆装维护 + Excel式列头筛选
- 列表新增当月真实业绩列(SPU 汇总条码当月值,条码展开行显示自身),当月无业绩整行过滤
- 新增供应商列(取 SPU 内非空 supplierName),列顺序调整为 SPU|业绩|供应商|运营
- 维护抽屉新增整装/拆装二选一,标记整装的 SPU 在"是否有图纸"列显示"整装不需要图纸"
- 顶部有无筛选与 Excel 式列头筛选(含 filter-options 接口)
- 新增迁移 20260915180000_add_dropship_issue_assembly_type(SupplyChainDropshipIssue.assemblyType)
2026-09-16 08:43:27 +08:00
WuYongJie
b0fd9a8800 1688代发异常事项:SPU级维护表+运营筛选+图纸/视频筛选+条码搜索 2026-09-16 08:43:06 +08:00
WuYongJie
55ee1b421b fix(barcode-print): restore legacy split-code text layout
Full-Page scaling mis-measured the 3096x416 single-line PNG by its intrinsic size and shrank the whole layout. Templates two and three now share the restored legacy ADD_PRINT_TEXT segments.
2026-09-15 18:16:15 +08:00
WuYongJie
8a7d6cae5a fix(barcode-print): restore legacy split-code text layout
Full-Page scaling mis-measured the 3096x416 single-line PNG by its intrinsic size and shrank the whole layout. Templates two and three now share the restored legacy ADD_PRINT_TEXT segments.
2026-09-15 18:08:49 +08:00
WuYongJie
260fbc8428 feat(progress): add column header filters 2026-09-15 17:02:03 +08:00
WuYongJie
36616821cf feat(progress): add column header filters 2026-09-15 16:57:42 +08:00
WuYongJie
53febe8eb7 feat(supplier-recommendations): add guided editor options 2026-09-15 14:45:25 +08:00
WuYongJie
170d776aa6 feat(supplier-recommendations): add guided editor options 2026-09-15 14:44:48 +08:00
WuYongJie
64e3378125 fix(finished-progress): backfill sample merchandiser owner 2026-09-15 13:49:26 +08:00
WuYongJie
f6dd04eeee fix(finished-progress): backfill sample merchandiser owner 2026-09-15 13:45:40 +08:00
WuYongJie
1b301af15e feat(finished-progress): link automatic rows to sample details 2026-09-15 11:38:40 +08:00
WuYongJie
499cbf66d1 feat(finished-progress): link automatic rows to sample details 2026-09-15 11:26:52 +08:00
WuYongJie
78dcdae880 fix(finished-progress): exclude historical JST entries 2026-09-15 11:01:33 +08:00
WuYongJie
79f34971d6 fix(finished-progress): exclude historical JST entries 2026-09-15 10:58:04 +08:00
WuYongJie
dac31e736a fix(1688): accept opaque CDN image responses 2026-09-15 10:16:12 +08:00
WuYongJie
b63b2fe974 fix(1688): accept opaque CDN image responses 2026-09-15 10:05:15 +08:00
WuYongJie
200790b6fc feat(finished-progress): require completed samples and improve automatic purchase intake
Require a real completed sample before finished-product intake. Carry factory, brand, merchandiser, delivery date and minimum order quantity into editable records; leave order date blank and support bulk maintenance.

Generate one factory contract per source group, place new automatic orders first and enlarge the horizontal scrollbar. Include source identity migration and regressions.

Validation: TypeScript noEmit; 18 Jest suites / 164 tests; local 3001 browser drag, wheel, and bottom-row checks.
2026-09-14 18:13:13 +08:00
WuYongJie
ed15789a81 feat(finished-progress): require completed samples and improve automatic purchase intake
Require a real completed sample before finished-product intake. Carry factory, brand, merchandiser, delivery date and minimum order quantity into editable records; leave order date blank and support bulk maintenance.

Generate one factory contract per source group, place new automatic orders first and enlarge the horizontal scrollbar. Include source identity migration and regressions.

Validation: TypeScript noEmit; 18 Jest suites / 164 tests; local 3001 browser drag, wheel, and bottom-row checks.
2026-09-14 18:07:47 +08:00
WuYongJie
73be6c9019 1688代发异常事项:SPU级维护表+运营筛选+图纸/视频筛选+条码搜索
- 新表 SupplyChainDropshipIssue:SPU 级普票/专票、退货地址运营维护,带操作日志
- 页面重构:删除 header,SPU 汇总行+展开条码明细(一码一行),操作列图标化
- 运营独立筛选项:可搜索/单选/多选(DropshipOpsFilter),默认按近7天销量合计降序
- 新增 是否有图纸/是否有安装视频 三态筛选(下拉一行排布),搜索支持条码维度
- 菜单排序:新品经营总览→新品终止任务反馈→1688代发数据明细→1688代发异常事项
- 平台标签标题注册 dropship-issues,修复顶部标签空白
2026-09-14 17:03:29 +08:00
WuYongJie
027d17dd4a 1688代发异常事项:SPU级维护表+运营筛选+图纸/视频筛选+条码搜索
- 新表 SupplyChainDropshipIssue:SPU 级普票/专票、退货地址运营维护,带操作日志
- 页面重构:删除 header,SPU 汇总行+展开条码明细(一码一行),操作列图标化
- 运营独立筛选项:可搜索/单选/多选(DropshipOpsFilter),默认按近7天销量合计降序
- 新增 是否有图纸/是否有安装视频 三态筛选(下拉一行排布),搜索支持条码维度
- 菜单排序:新品经营总览→新品终止任务反馈→1688代发数据明细→1688代发异常事项
- 平台标签标题注册 dropship-issues,修复顶部标签空白
2026-09-14 16:00:55 +08:00
WuYongJie
01fd83d28b feat(supply-chain): 并入1688代发数据明细与代发异常事项占位
内容来源: codex/xuxiake-mainline 2aa85619(伯阳 1688 dropship analytics + 晓晓占位与图片预览),按文件集从 origin/main 干净并入。

- 新增 /supply-chain/new-products/1688-dropship-data 全链路(页面/筛选/趋势/API/repository/service)
- 新增 /supply-chain/new-products/dropship-issues 占位入口
- 权限 targetKey: newProduct1688DropshipData / newProductDropshipIssues
- 迁移 20260912120000_add_daily_sales_dropship_fields(salesAmount/skuOperator/unitCost)
- 产品图片点击弹窗预览
2026-09-14 10:23:47 +08:00
WuYongJie
2aa856197b feat(supply-chain): 1688代发数据明细产品图片点击弹窗预览
- 新增 DropshipImageCell:缩略图可点击,平台 Modal 弹窗放大(等比铺满,最高680px)
- 缩略图样式收口到 .imageButton,避免与就地渲染的 Modal 大图互相污染
- 大图复用 source-image 认证代理 URL,浏览器缓存零额外请求
2026-09-14 10:02:55 +08:00
伯阳
bc2104b169 test(supply-chain): verify merged 1688 entries and record integration checks 2026-09-12 18:32:24 +08:00
伯阳
e271d1b941 Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/wuyongjie-1688-xuxiake-integration 2026-09-12 18:29:30 +08:00
伯阳
dfdc3c3626 feat(supply-chain): integrate 1688 dropship analytics with shared daily sales
Preserve shared barcode/day facts and atomic synchronization; add DF sales, cost and operator enrichment, filters, trends, and module-scoped image access. Include additive migration and deployment/backfill handoff. Independent review: passed with warnings; existing master-data audit failures match baseline.
2026-09-12 18:29:16 +08:00
WuYongJie
5156ead4c1 供应链新品管理新增「1688代发异常事项」占位入口
- 新增 /supply-chain/new-products/dropship-issues 占位页(功能建设中)
- supply-chain-menu 新品管理分组加菜单项,targetKey=newProductDropshipIssues
- 权限体系 targetKey 类型/列表/路由映射同步
- 侧栏清理未使用的 Home 图标导入
2026-09-12 18:05:22 +08:00
WuYongJie
090c6b39c5 运营负责人SPU级兜底:同SPU最新销售条码的负责人下放给未匹配条码 2026-09-12 17:10:50 +08:00
WuYongJie
38259cc40c 运营负责人SPU级兜底:同SPU最新销售条码的负责人下放给未匹配条码 2026-09-12 17:10:28 +08:00
WuYongJie
d20d7e4dd6 运营负责人SPU级兜底:同SPU最新销售条码的负责人下放给未匹配条码
- dws listLatestOpsOwners 带回 lastSaleDate;新增 listBarcodeSpuPairs(全档案条码→spu)
- service 构建按 spu 聚合的运营负责人映射(逐字段取最新销售日期的非空值)
- mapper 兜底:条码无 dws 直配时按 spu 继承(精确匹配优先,来源追溯 spu: 前缀)
- 用户口径:能匹配到的都放上去,不留空
2026-09-12 17:07:29 +08:00
WuYongJie
687ef50bb5 fix(home): workbench entry inside quick-links fold with unified styles
- expand list now starts with a fixed workbench entry (router.push to
  the workspace picker) followed by configured links
- replace next/link with button + router.push inside the nav item so
  styled-jsx scoped styles apply; fixes icon/label wrapping and keeps
  font size and spacing consistent across entries
- tests updated for the new push-based navigation and roles
2026-09-12 17:03:28 +08:00
WuYongJie
f6dc344b71 fix(home): workbench entry inside quick-links fold with unified styles
- expand list now starts with a fixed workbench entry (router.push to
  the workspace picker) followed by configured links
- replace next/link with button + router.push inside the nav item so
  styled-jsx scoped styles apply; fixes icon/label wrapping and keeps
  font size and spacing consistent across entries
- tests updated for the new push-based navigation and roles
2026-09-12 17:02:38 +08:00
WuYongJie
2b192afd86 fix(installation-demo): allow customer service model upload with progress toast
- /api/upload/model 去掉 requireDesignWorkspaceWrite 硬性门槛:改为安装演示
  canEdit 或设计工作区权限二者其一即可上传(客服等已配置安装演示编辑权限
  的角色可上传模型,其他模块设计权限语义不变)
- 上传进度右下角浮层按系统既有导入进度样式实现(文件名+百分比+进度条,
  完成绿色停留 3 秒自动收起,失败红色保留可关闭)
- installationModelUpload 改用 XHR 以获得真实上传字节进度,工具箱共用不受影响
2026-09-12 15:51:36 +08:00
WuYongJie
6aba6e077c fix(installation-demo): allow customer service model upload with progress toast
- /api/upload/model 去掉 requireDesignWorkspaceWrite 硬性门槛:改为安装演示
  canEdit 或设计工作区权限二者其一即可上传(客服等已配置安装演示编辑权限
  的角色可上传模型,其他模块设计权限语义不变)
- 上传进度右下角浮层按系统既有导入进度样式实现(文件名+百分比+进度条,
  完成绿色停留 3 秒自动收起,失败红色保留可关闭)
- installationModelUpload 改用 XHR 以获得真实上传字节进度,工具箱共用不受影响
2026-09-12 15:51:11 +08:00
WuYongJie
9892fead34 fix(installation-demo): allow customer service model upload with progress toast
- /api/upload/model 去掉 requireDesignWorkspaceWrite 硬性门槛:改为安装演示
  canEdit 或设计工作区权限二者其一即可上传(客服等已配置安装演示编辑权限
  的角色可上传模型,其他模块设计权限语义不变)
- 上传进度右下角浮层按系统既有导入进度样式实现(文件名+百分比+进度条,
  完成绿色停留 3 秒自动收起,失败红色保留可关闭)
- installationModelUpload 改用 XHR 以获得真实上传字节进度,工具箱共用不受影响
2026-09-12 15:50:47 +08:00
WuYongJie
9e9d92306e feat(home): admin-configured quick links with sidebar home fold
- HomeQuickLink table + migration; CRUD API with admin-only writes and
  internal-only visibility (suppliers/external intake excluded)
- sidebar home item folds open configured links in both product and
  supply-chain sidebars, entries open in a new tab
- home portal top-right manage entry with sort/enable/delete sheet
- permission matrix default: home overview open to all internal subjects
  so switching to supply chain no longer lands on an empty page
2026-09-12 15:04:54 +08:00
WuYongJie
0ea1c3ecdd feat(home): admin-configured quick links with sidebar home fold
- HomeQuickLink table + migration; CRUD API with admin-only writes and
  internal-only visibility (suppliers/external intake excluded)
- sidebar home item folds open configured links in both product and
  supply-chain sidebars, entries open in a new tab
- home portal top-right manage entry with sort/enable/delete sheet
- permission matrix default: home overview open to all internal subjects
  so switching to supply chain no longer lands on an empty page
2026-09-12 15:04:08 +08:00
WuYongJie
43f8540c9a 是否有图纸/是否有视频接入图纸系统API+非DF铺货图兜底+筛选修复 2026-09-12 14:27:10 +08:00
WuYongJie
002aa9dc34 fix(installation-demo): route customer service through node permissions
删除安装演示权限里硬编码的客服强制只读分享口径:原 isCustomerServiceUser
(CUSTOMER_SERVICE 角色或部门路径含客服中心)直接返回 canEdit=false 且排在
节点权限 overrides 之前,导致正式站给客服配置的编辑权限永远无效,客服只能
查看和分享、无法新建编辑截取。

删除后客服与普通员工一致走节点权限配置:客服主体 customerService
(部门路径含客服中心),正式站已配置的编辑权限即刻生效,可新建图纸、
维护编号、截取、分享与下载。
2026-09-12 14:25:40 +08:00
WuYongJie
d9d1b39d1c fix(installation-demo): route customer service through node permissions
删除安装演示权限里硬编码的客服强制只读分享口径:原 isCustomerServiceUser
(CUSTOMER_SERVICE 角色或部门路径含客服中心)直接返回 canEdit=false 且排在
节点权限 overrides 之前,导致正式站给客服配置的编辑权限永远无效,客服只能
查看和分享、无法新建编辑截取。

删除后客服与普通员工一致走节点权限配置:客服主体 customerService
(部门路径含客服中心),正式站已配置的编辑权限即刻生效,可新建图纸、
维护编号、截取、分享与下载。
2026-09-12 14:24:58 +08:00
WuYongJie
4f2ea0bdf0 fix(installation-demo): route customer service through node permissions
删除安装演示权限里硬编码的客服强制只读分享口径:原 isCustomerServiceUser
(CUSTOMER_SERVICE 角色或部门路径含客服中心)直接返回 canEdit=false 且排在
节点权限 overrides 之前,导致正式站给客服配置的编辑权限永远无效,客服只能
查看和分享、无法新建编辑截取。

删除后客服与普通员工一致走节点权限配置:客服主体 customerService
(部门路径含客服中心),正式站已配置的编辑权限即刻生效,可新建图纸、
维护编号、截取、分享与下载。
2026-09-12 14:24:11 +08:00
WuYongJie
904b80d4c9 运营部门负责人/运营负责人接入聚水潭销售宽表cater/ider
- dws repository 新增 listLatestOpsOwners:rpt.dws_sn_sales_full_info_di 按 skuoid 取 stat_date 最新一条的 cater/ider,全条码范围
- enrichment 链路新增 ops owners patch:来源码 DWS_SALES_OPS 可追溯;字段级有值才覆盖(dws 无值保留档案原值)
- opsDirector(运营总负责人)不接
- 本地全量验证:抽样 10 条与上游 100% 一致;3001 API 验收通过
2026-09-12 11:14:04 +08:00
WuYongJie
aaa9c74eec Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/local-3001-integration 2026-09-12 11:13:15 +08:00
WuYongJie
dd693dfbd5 运营部门负责人/运营负责人接入聚水潭销售宽表cater/ider
- dws repository 新增 listLatestOpsOwners:rpt.dws_sn_sales_full_info_di 按 skuoid 取 stat_date 最新一条的 cater/ider,全条码范围
- enrichment 链路新增 ops owners patch:来源码 DWS_SALES_OPS 可追溯;字段级有值才覆盖(dws 无值保留档案原值)
- opsDirector(运营总负责人)不接
- 本地全量验证:抽样 10 条与上游 100% 一致;3001 API 验收通过
2026-09-12 11:12:50 +08:00
WuYongJie
1b3f210e0f feat(barcode-print): add size template and keep package codes on one line 2026-09-11 17:13:52 +08:00
WuYongJie
04b24f7972 feat(barcode-print): add size template and keep package codes on one line 2026-09-11 17:12:52 +08:00
WuYongJie
222f3e6a97 feat(barcode-print): add size template and keep package codes on one line 2026-09-11 17:08:02 +08:00
WuYongJie
02ad38908e fix(installation-demo): recapture by current viewpoint, align material counts, add finished-frame ending
- 编号重截取按当前视角取景(engine 截图支持 viewpoint:current,默认行为不变),成功/失败给可见消息
- 动作分解物料数量改为该动作实际命中节点数(fixed/action 帧同口径),徽标与编号标注共用管理编号覆盖值
- 动作分解末尾固定追加成品展示收尾帧:复用成品展示截图,不参与截取与计数,支持调整画面与文字标注
- 编号标注对账按编号兜底认领并清理超量残留,修复部件重绑后改数量导致标注叠加翻倍
- 成品线稿统一 detail 风格,与动作图线宽一致
2026-09-11 14:37:05 +08:00
WuYongJie
6f49206964 fix(installation-demo): recapture by current viewpoint, align material counts, add finished-frame ending
- 编号重截取按当前视角取景(engine 截图支持 viewpoint:current,默认行为不变),成功/失败给可见消息
- 动作分解物料数量改为该动作实际命中节点数(fixed/action 帧同口径),徽标与编号标注共用管理编号覆盖值
- 动作分解末尾固定追加成品展示收尾帧:复用成品展示截图,不参与截取与计数,支持调整画面与文字标注
- 编号标注对账按编号兜底认领并清理超量残留,修复部件重绑后改数量导致标注叠加翻倍
- 成品线稿统一 detail 风格,与动作图线宽一致
2026-09-11 14:34:04 +08:00
WuYongJie
0c045ff906 chore(product-archive): default daily sales schedule to Beijing 08:00
- local scheduled task installer: compute next 08:00 Asia/Shanghai start boundary
- production systemd timer installer: default OnCalendar 08:00 Asia/Shanghai
- aligns installers with already-active 08:00 runtimes on both ends
2026-09-11 09:46:58 +08:00
WuYongJie
0c7d147129 chore(product-archive): default daily sales schedule to Beijing 08:00
- local scheduled task installer: compute next 08:00 Asia/Shanghai start boundary
- production systemd timer installer: default OnCalendar 08:00 Asia/Shanghai
- aligns installers with already-active 08:00 runtimes on both ends
2026-09-11 09:44:56 +08:00
WuYongJie
e075a81eb7 DF开头SKU档案图片接入聚水潭skumap三表平台图
- 新增 jst-skumap-pic.repository:rpt.jst_skumap/_pdd/_qmcust_add 三表 UNION,按 sku_id 取 modified 最新非空 pic,仅 DF 开头
- enrichment 同步链路:DF 条码 image 字段改用 skumap 平台图(无图清空),来源记录 JST_SKUMAP_PIC 可追溯;其他 SKU 图片逻辑不变
- 变更日志字段清单加入 image(图片)
- source-image 图片代理白名单放行 JST_SKUMAP_PIC,修复 DF 档案平台图 404 灰框
- 本地全量回填验证:331 批扫描 66033 failed=0;DF 有图 2281→24700;非 DF 14974 不变
2026-09-11 09:14:48 +08:00
WuYongJie
9e5a0d20b5 DF开头SKU档案图片接入聚水潭skumap三表平台图
- 新增 jst-skumap-pic.repository:rpt.jst_skumap/_pdd/_qmcust_add 三表 UNION,按 sku_id 取 modified 最新非空 pic,仅 DF 开头
- enrichment 同步链路:DF 条码 image 字段改用 skumap 平台图(无图清空),来源记录 JST_SKUMAP_PIC 可追溯;其他 SKU 图片逻辑不变
- 变更日志字段清单加入 image(图片)
- source-image 图片代理白名单放行 JST_SKUMAP_PIC,修复 DF 档案平台图 404 灰框
- 本地全量回填验证:331 批扫描 66033 failed=0;DF 有图 2281→24700;非 DF 14974 不变
2026-09-11 09:12:05 +08:00
WuYongJie
56560cd6d2 fix(deploy): verify complete releases before switching production 2026-09-10 18:29:17 +08:00
WuYongJie
e2f211c7ac fix(deploy): verify complete releases before switching production 2026-09-10 18:29:13 +08:00
WuYongJie
7f9adebf88 feat(installation-demo): add manual catalog, permanent shares and hinge binding 2026-09-10 18:12:46 +08:00
WuYongJie
46a732d892 feat(installation-demo): add manual catalog, permanent shares and hinge binding 2026-09-10 17:59:58 +08:00
WuYongJie
2cfd57d678 feat(product-archive): add daily monthly yearly sales and daily sync 2026-09-10 13:16:26 +08:00
WuYongJie
aa1ca0afcf feat(product-archive): add daily monthly yearly sales and daily sync 2026-09-10 13:15:23 +08:00
WuYongJie
b66e6b1f11 Merge remote-tracking branch 'origin/main' into HEAD 2026-09-10 12:59:21 +08:00
WuYongJie
0875c87058 Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into HEAD 2026-09-10 12:59:20 +08:00
WuYongJie
a20b6cb79f fix(product-archive): add missing POST handlers for large filter payloads 2026-09-10 12:59:03 +08:00
WuYongJie
b9c85357e6 fix(intake): abandon permission checks creator and real designer keys
废弃权限白名单原读取 payload 中不存在的 appearanceDesigner 等键,导致外观/结构设计师无法废弃产品。改为按 创建人 + 外观设计师(designer 列/payload.designer) + 结构设计师(openingReference.structureDesigner,兼容顶层旧键) 判定,不再放行 SPU 负责人与采购负责人;管理员口径不变。
2026-09-10 12:32:19 +08:00
WuYongJie
5c7ebbe914 fix(intake): abandon permission checks creator and real designer keys
废弃权限白名单原读取 payload 中不存在的 appearanceDesigner 等键,导致外观/结构设计师无法废弃产品。改为按 创建人 + 外观设计师(designer 列/payload.designer) + 结构设计师(openingReference.structureDesigner,兼容顶层旧键) 判定,不再放行 SPU 负责人与采购负责人;管理员口径不变。
2026-09-10 12:32:06 +08:00
WuYongJie
2947ddebcf Merge remote-tracking branch 'origin/main' into HEAD 2026-09-10 11:53:26 +08:00
WuYongJie
69d19076bd Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into HEAD 2026-09-10 11:53:07 +08:00
WuYongJie
17d2e1d7bb Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/local-3001-integration 2026-09-10 11:52:31 +08:00
WuYongJie
351b7d7d2a feat(product-archive): take dropship df sku cost from sales goods_cost 2026-09-10 11:52:14 +08:00
WuYongJie
b8e80204a0 fix(barcode-print): raise Gujia certificate fields 2026-09-10 08:44:31 +08:00
WuYongJie
1042f35090 fix(barcode-print): raise Gujia certificate fields 2026-09-10 08:44:28 +08:00
WuYongJie
eedea9f791 fix(barcode-print): raise Gujia certificate fields 2026-09-10 08:43:13 +08:00
WuYongJie
0271da1b69 fix(barcode-print): recalibrate Gujia card and disable 2026 template 2026-09-09 17:56:14 +08:00
WuYongJie
b647cc2943 fix(barcode-print): recalibrate Gujia card and disable 2026 template 2026-09-09 17:55:27 +08:00
WuYongJie
3360ccb801 fix(barcode-print): recalibrate Gujia card and disable 2026 template 2026-09-09 17:54:04 +08:00
WuYongJie
1c0beffcb6 feat(barcode-print): add calibrated Gujia certificate template 2026-09-09 15:47:45 +08:00
WuYongJie
254cd92c5b fix(product-archive): stream material library images through a same-origin endpoint
the material library api hands out signed preview urls whose objects 404 on its cdn, so archive rows kept rendering empty placeholders; the batch endpoint now returns same-origin streaming urls (material-image?file=1) that resolve server-side, try the reachable s3 direct link first with the original signed url as fallback, and stream the bytes to the browser; the resolved image object is copied before url mapping so the service in-memory cache is no longer poisoned across requests.
2026-09-09 15:45:58 +08:00
WuYongJie
3976f96fcb fix(product-archive): stream material library images through a same-origin endpoint
the material library api hands out signed preview urls whose objects 404 on its cdn, so archive rows kept rendering empty placeholders; the batch endpoint now returns same-origin streaming urls (material-image?file=1) that resolve server-side, try the reachable s3 direct link first with the original signed url as fallback, and stream the bytes to the browser; the resolved image object is copied before url mapping so the service in-memory cache is no longer poisoned across requests.
2026-09-09 15:45:18 +08:00
WuYongJie
6161bb3e59 feat(barcode-print): add calibrated Gujia certificate template 2026-09-09 15:43:08 +08:00
WuYongJie
3cd064a467 Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into HEAD 2026-09-09 14:57:09 +08:00
WuYongJie
db05c1ca0a Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/local-3001-integration 2026-09-09 14:42:30 +08:00
WuYongJie
1bb87a454d feat(product-archive): persist jianshang stock on main table and harden filter select-all 2026-09-09 14:42:17 +08:00
WuYongJie
cc1e15582e fix(product-archive): fall back signed qnycdn material image urls to reachable s3 links
the material library api now returns signed preview urls on qnycdn.jiansun.vip/prod whose objects 404 on the cdn, so every resolved archive material image failed to render; rewrite those urls to the same path on s3.jiansun.vip/gongxiangpan (verified reachable) and drop the signature at every exit of the material image service (barcode lookup, sku zip index, white background fallback, resolved cache reads) so matched sku/barcode images display again.
2026-09-09 13:35:55 +08:00
WuYongJie
560579b7f9 fix(product-archive): fall back signed qnycdn material image urls to reachable s3 links
the material library api now returns signed preview urls on qnycdn.jiansun.vip/prod whose objects 404 on the cdn, so every resolved archive material image failed to render; rewrite those urls to the same path on s3.jiansun.vip/gongxiangpan (verified reachable) and drop the signature at every exit of the material image service (barcode lookup, sku zip index, white background fallback, resolved cache reads) so matched sku/barcode images display again.
2026-09-09 13:35:35 +08:00
WuYongJie
f0cc716dd0 fix(intake): map reorganized planning department to PLANNING role
钉钉组织调整后商品企划部更名为企划部并挂到公司根节点。部门到业务角色推断、非集成产品权限、节点权限主体与商品企划候选人筛选统一识别 企划部 及其下级在职人员;保留旧 品牌中心>商品企划部 口径兼容存量数据;在职/外部过滤逻辑不变。
2026-09-09 12:12:01 +08:00
WuYongJie
66746bdc7e fix(intake): map reorganized planning department to PLANNING role
钉钉组织调整后商品企划部更名为企划部并挂到公司根节点。部门到业务角色推断、非集成产品权限、节点权限主体与商品企划候选人筛选统一识别 企划部 及其下级在职人员;保留旧 品牌中心>商品企划部 口径兼容存量数据;在职/外部过滤逻辑不变。
2026-09-09 12:11:32 +08:00
WuYongJie
d563513875 feat(product-development-todos): drop early-stage reminders and read structured factory rows 2026-09-09 11:45:40 +08:00
WuYongJie
1a0d3a7429 feat(product-development-todos): drop early-stage reminders and read structured factory rows 2026-09-09 11:45:01 +08:00
WuYongJie
c20bf5314a feat(product-development-todos): drop early-stage reminders and read structured factory rows 2026-09-09 11:43:21 +08:00
f615161d8c Merge pull request 'fix(store-accounts): 按明文存储规则修正密码状态' (#21) from codex/xuxiake-mainline into main 2026-09-09 11:05:13 +08:00
WuYongJie
ed339ceb3f merge: sync latest xuxiake before password status release
# Conflicts:
#	furniture-3d-system/docs/integrations/finance-store-account-password-reveal.md
2026-09-09 11:02:18 +08:00
WuYongJie
149cf46b63 fix(store-accounts): align password status with plaintext storage 2026-09-09 11:01:42 +08:00
17daaee224 保持当前页分块读取限流额度 (#20) 2026-09-08 18:13:20 +08:00
2224112c29 保持当前页分块读取限流额度 (#19) 2026-09-08 18:11:34 +08:00
4fd134f636 发布财务当前页批量读取店铺密码 (#16)
Co-authored-by: xuxiake <2644600472@qq.com>
Co-committed-by: xuxiake <2644600472@qq.com>
2026-09-08 17:21:32 +08:00
caa960d70a 支持财务当前页批量读取店铺密码 (#18) 2026-09-08 17:19:01 +08:00
WuYongJie
9965c93c8d feat(store-accounts): store password and bank account as plaintext 2026-09-08 15:07:35 +08:00
WuYongJie
811926e67b feat(store-accounts): store password and bank account as plaintext 2026-09-08 15:06:43 +08:00
WuYongJie
c9d2f37ead feat(product-archive): add select-all to header filter option lists 2026-09-08 14:44:26 +08:00
WuYongJie
4deea6518a feat(product-archive): add select-all to header filter option lists 2026-09-08 14:43:37 +08:00
WuYongJie
0b4311a882 feat(store-accounts): show password and bank account in plaintext in list 2026-09-08 14:07:06 +08:00
WuYongJie
ecfd596ad0 feat(store-accounts): show password and bank account in plaintext in list 2026-09-08 14:05:44 +08:00
WuYongJie
df9c0ad67f feat(intake): add product abandon action to the product list
the product list rows gain an abandon action next to edit/copy/delete for admins and product owners (operations brand / spu / appearance / structure / purchase / merchandiser / planner / space owners matched by name); abandoning sets projectStatus to rejected which makes every workspace read-only and the existing daily todo push already skips rejected products, so abandoned products drop out of daily pushes automatically; the server enforces the same permission via a new POST abandon endpoint. Abandoned rows keep the restore action to come back.
2026-09-08 12:27:26 +08:00
WuYongJie
d2f71b3723 feat(intake): add product abandon action to the product list
the product list rows gain an abandon action next to edit/copy/delete for admins and product owners (operations brand / spu / appearance / structure / purchase / merchandiser / planner / space owners matched by name); abandoning sets projectStatus to rejected which makes every workspace read-only and the existing daily todo push already skips rejected products, so abandoned products drop out of daily pushes automatically; the server enforces the same permission via a new POST abandon endpoint. Abandoned rows keep the restore action to come back.
2026-09-08 12:26:44 +08:00
WuYongJie
5754172787 fix(product-development-todo): route missing purchaser reminders and use plain text 2026-09-08 10:31:22 +08:00
WuYongJie
ee8172c724 fix(product-development-todo): route missing purchaser reminders and use plain text 2026-09-08 10:21:19 +08:00
WuYongJie
7139f40c8d feat(oa-board): make workflow link cells clickable to open product detail
workflow link cells on the oa board detail table now render as anchors when the value is an http(s) url, opening the linked product detail in a new tab with platform link styling and hover underline; empty or non-url text still renders the dash placeholder.
2026-09-08 09:02:51 +08:00
WuYongJie
54bf9107fb Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/local-3001-integration 2026-09-08 09:01:57 +08:00
WuYongJie
41e962da37 feat(oa-board): make workflow link cells clickable to open product detail
workflow link cells on the oa board detail table now render as anchors when the value is an http(s) url, opening the linked product detail in a new tab with platform link styling and hover underline; empty or non-url text still renders the dash placeholder.
2026-09-08 09:01:37 +08:00
WuYongJie
22cede9ed0 merge(main): publish finance store password reveal 2026-09-08 06:22:23 +08:00
c1dd4e666e Merge pull request 'feat(store-accounts): 财务专用按需密码读取、审计与安全状态' (#17) from codex/wuyongjie-store-accounts-finance-reveal into codex/xuxiake-mainline 2026-09-08 06:21:56 +08:00
WuYongJie
494a1cf60f feat(store-accounts): add audited finance password reveal 2026-09-08 05:59:19 +08:00
WuYongJie
f5a8ec1c05 feat(intake): add product factory workbook export and import by sku code
the product factory workspace gains export/import template buttons (purchase/merchandiser already hold node download grants): the export brings out the sku code, set count and every factory fillable field, splitting set count 2 rows into their A/B package codes; the import matches rows by sku code, enforces a header layout identical to the exported template, validates supplier names against the supplier directory, warehouses against the warehouse directory, enum fields against system options and number/date formats, collapses or splits A/B packages when the imported set count changes, and rejects confirmed rows - any row error aborts the whole import with per-row line numbers and reasons. Size, color and material columns are reference only.
2026-09-07 17:14:07 +08:00
WuYongJie
e9f83b16ee feat(intake): add product factory workbook export and import by sku code
the product factory workspace gains export/import template buttons (purchase/merchandiser already hold node download grants): the export brings out the sku code, set count and every factory fillable field, splitting set count 2 rows into their A/B package codes; the import matches rows by sku code, enforces a header layout identical to the exported template, validates supplier names against the supplier directory, warehouses against the warehouse directory, enum fields against system options and number/date formats, collapses or splits A/B packages when the imported set count changes, and rejects confirmed rows - any row error aborts the whole import with per-row line numbers and reasons. Size, color and material columns are reference only.
2026-09-07 17:12:59 +08:00
WuYongJie
416e22754a 供应商新品推荐:新增采购数据看板与PK榜
看板双视图(明细数据/采购看板,仅采购与管理可见看板);按月聚合采购推荐SKU/涉及工厂/被选款/被选率;路径行明细支持工厂勾选后跳转明细数据;被选款数量卡片跳转已选明细;PK榜爬山形态铺满全屏,管理员可配置主题/总金额/层级门槛与金额/规则说明(存 SystemConfig,无迁移)。
2026-09-07 16:40:49 +08:00
WuYongJie
d4d26c43bf 供应商新品推荐:新增采购数据看板与PK榜
看板双视图(明细数据/采购看板,仅采购与管理可见看板);按月聚合采购推荐SKU/涉及工厂/被选款/被选率;路径行明细支持工厂勾选后跳转明细数据;被选款数量卡片跳转已选明细;PK榜爬山形态铺满全屏,管理员可配置主题/总金额/层级门槛与金额/规则说明(存 SystemConfig,无迁移)。
2026-09-07 16:39:38 +08:00
WuYongJie
8fb872a3f9 feat(intake): open sample measurement and pricing template access with spec name column
measurement and pricing nodes now default downloads for pricing/purchase/merchandiser subjects so non-admin owners see the export/import template buttons; both workbook exports add a spec name reference column after the barcode (imports stay barcode-keyed and ignore it); the pricing node also grants merchandiser download parity with purchase owners.
2026-09-07 11:48:18 +08:00
WuYongJie
f8ba4f1e25 feat(intake): open sample measurement and pricing template access with spec name column
measurement and pricing nodes now default downloads for pricing/purchase/merchandiser subjects so non-admin owners see the export/import template buttons; both workbook exports add a spec name reference column after the barcode (imports stay barcode-keyed and ignore it); the pricing node also grants merchandiser download parity with purchase owners.
2026-09-07 11:47:40 +08:00
WuYongJie
39d7fc02f6 feat(finished-product-progress): auto-import factory-confirmed SKUs on sample completion 2026-09-07 11:17:21 +08:00
WuYongJie
8ea6a54eaa feat(finished-product-progress): auto-import factory-confirmed SKUs on sample completion 2026-09-07 11:15:01 +08:00
WuYongJie
667438447a fix(intake): move import rowNumber off the shared sku export row type
the optional numeric rowNumber on NonIntegratedSkuExportRow broke the dynamic template column writes (string vs never) in the production build type check; import rows now use a dedicated NonIntegratedSkuImportRow type that extends the export row with a required rowNumber.
2026-09-05 12:26:25 +08:00
WuYongJie
092036786c fix(barcode-print): restore Sofia certificate row pitch 2026-09-05 11:55:52 +08:00
WuYongJie
4a574041a9 fix(barcode-print): restore Sofia certificate row pitch 2026-09-05 11:53:55 +08:00
WuYongJie
2dbb214006 fix(intake): move import rowNumber off the shared sku export row type
the optional numeric rowNumber on NonIntegratedSkuExportRow broke the dynamic template column writes (string vs never) in the production build type check; import rows now use a dedicated NonIntegratedSkuImportRow type that extends the export row with a required rowNumber.
2026-09-04 17:59:40 +08:00
WuYongJie
e926a5a990 fix(intake): replace unsafe cast in sku export empty row builder
adding the optional rowNumber field to NonIntegratedSkuExportRow made the Object.fromEntries cast insufficient for the production build type check; build the empty import row explicitly instead.
2026-09-04 17:49:57 +08:00
WuYongJie
4f6510271f fix(intake): replace unsafe cast in sku export empty row builder
adding the optional rowNumber field to NonIntegratedSkuExportRow made the Object.fromEntries cast insufficient for the production build type check; build the empty import row explicitly instead.
2026-09-04 17:49:31 +08:00
WuYongJie
fccf9485b7 feat(intake): validate sku import skuCode spu prefix
standard sku workbook import now rejects rows whose skuCode does not start with the product spu in uppercase (lowercase codes are reported as needing uppercase); rows with an empty skuCode still pass and get auto-generated on save; the whole import is rejected with per-row line numbers and reasons. barcodes are skipped since they derive from the sku code.
2026-09-04 17:40:11 +08:00
WuYongJie
f4712cd1a2 feat(intake): validate sku import skuCode spu prefix
standard sku workbook import now rejects rows whose skuCode does not start with the product spu in uppercase (lowercase codes are reported as needing uppercase); rows with an empty skuCode still pass and get auto-generated on save; the whole import is rejected with per-row line numbers and reasons. barcodes are skipped since they derive from the sku code.
2026-09-04 17:39:35 +08:00
WuYongJie
e27e4fe67e feat(sample-progress): add daily due-date reminder notifications 2026-09-04 16:15:44 +08:00
WuYongJie
4843e16124 feat(sample-progress): add daily due-date reminder notifications 2026-09-04 16:14:51 +08:00
WuYongJie
f41e87373b fix(barcode-print): normalize Sofia certificate row spacing 2026-09-04 15:27:09 +08:00
WuYongJie
d32988dde6 fix(barcode-print): normalize Sofia certificate row spacing 2026-09-04 15:26:23 +08:00
WuYongJie
b30e5e5850 fix(barcode-print): tune Sofia certificate field positions 2026-09-04 12:27:39 +08:00
WuYongJie
bbb35a44f3 fix(barcode-print): tune Sofia certificate field positions 2026-09-04 12:25:32 +08:00
WuYongJie
56f3a7fe3f fix(supply-chain): show feedback as timed toast 2026-09-04 11:27:55 +08:00
WuYongJie
d3dee58b2b fix(supply-chain): default batch selection operator 2026-09-04 11:27:55 +08:00
WuYongJie
3df9bf08b1 fix(supply-chain): show feedback as timed toast 2026-09-04 11:25:31 +08:00
WuYongJie
d08a244fcb fix(supply-chain): default batch selection operator 2026-09-04 11:23:54 +08:00
WuYongJie
b488e6084c fix(finished-product-progress): enforce first-due-date protection on import updates
Import rows must not bypass the confirmed-first-due-date protection. Drop the allowDueDateChanges flag from the import update path so first-due-date changes are rejected and weekly due-date changes still require a reason, matching manual edits. Legacy rows without an order-placed date keep their free-correction path.
2026-09-04 10:59:29 +08:00
WuYongJie
99f004a593 fix(finished-product-progress): enforce first-due-date protection on import updates
Import rows must not bypass the confirmed-first-due-date protection. Drop the allowDueDateChanges flag from the import update path so first-due-date changes are rejected and weekly due-date changes still require a reason, matching manual edits. Legacy rows without an order-placed date keep their free-correction path.
2026-09-04 10:55:55 +08:00
WuYongJie
a252c00ac2 fix: align sofia certificate print fields 2026-09-04 10:32:41 +08:00
WuYongJie
13dd73069e fix: align sofia certificate print fields 2026-09-04 10:28:58 +08:00
WuYongJie
592b53f0ac merge(main): 发布可切换抠图配置与异步智能抠图 2026-09-03 18:48:20 +08:00
WuYongJie
16d6069123 merge: 发布可切换抠图配置与异步智能抠图 2026-09-03 18:48:04 +08:00
WuYongJie
a5cca36d69 fix(bom-costing): 接入供应商异步智能抠图模型 2026-09-03 18:44:51 +08:00
WuYongJie
966a8340e4 Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/wuyongjie-bom-estimate-agent-phase0 2026-09-03 18:27:47 +08:00
WuYongJie
6b3760c6e7 feat(bom-costing): 支持可切换的抠图接口配置 2026-09-03 18:27:22 +08:00
WuYongJie
92bd812a17 feat(supply-chain): add supplier recommendation notice discard pool 2026-09-03 17:55:00 +08:00
WuYongJie
3456d1f465 Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/local-3001-integration 2026-09-03 17:53:56 +08:00
WuYongJie
ffcf242738 feat(supply-chain): add supplier recommendation notice discard pool 2026-09-03 17:51:56 +08:00
WuYongJie
24b5d5872f merge(main): 发布 BOM 商品详情图排列修正 2026-09-03 17:46:23 +08:00
WuYongJie
b335ef7719 merge: 修正 BOM 商品详情图排列 2026-09-03 17:46:11 +08:00
WuYongJie
394927d843 Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/wuyongjie-bom-estimate-agent-phase0 2026-09-03 17:45:59 +08:00
WuYongJie
f4adeb3808 fix(bom-costing): 纵向完整展示商品详情图 2026-09-03 17:45:38 +08:00
WuYongJie
452c41bc4d merge(main): 发布 BOM 商品图片布局修正 2026-09-03 17:24:23 +08:00
WuYongJie
cac0452672 merge: 修正 BOM 商品图片布局 2026-09-03 17:24:09 +08:00
WuYongJie
0c65f7af2b Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/wuyongjie-bom-estimate-agent-phase0 2026-09-03 17:23:54 +08:00
WuYongJie
4a9e5d99be fix(bom-costing): 按指定位置重排商品图片 2026-09-03 17:23:03 +08:00
WuYongJie
04d6219417 merge(main): 发布 BOM 商品抠图与图片分类 2026-09-03 17:06:12 +08:00
WuYongJie
30ab1b4b35 merge: 发布 BOM 商品抠图与图片分类 2026-09-03 17:05:49 +08:00
WuYongJie
b5d05f2779 Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/wuyongjie-bom-estimate-agent-phase0 2026-09-03 17:04:34 +08:00
WuYongJie
4ce03262c0 feat(bom-costing): 接入商品抠图并完善图片浏览 2026-09-03 17:04:05 +08:00
WuYongJie
4014f53b9e Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into HEAD 2026-09-03 15:52:13 +08:00
WuYongJie
3e92e7242a Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/local-3001-integration 2026-09-03 15:51:17 +08:00
WuYongJie
c10e787fcf fix(users): fall back permission subjects to business roles when department misses dedicated lines
Department dedicated lines stay authoritative; when no department path matches any line, resolve subjects from account business roles (OPERATIONS->operation, PURCHASING->purchase, STRATEGY->brandCenter, etc). Personal overrides still apply last. Fixes operations and four purchasing accounts being downgraded to plain viewers.
2026-09-03 15:51:09 +08:00
WuYongJie
17a0491a4a merge(main): 发布 BOM 九折预估与极简工作台
# Conflicts:
#	furniture-3d-system/prisma/schema.prisma
#	furniture-3d-system/src/app/intake/sample-progress/components/SampleProgressTableRow.tsx
#	furniture-3d-system/src/app/supply-chain/database/store-account-management/StoreAccountManagementView.tsx
#	furniture-3d-system/src/lib/store-account-management.ts
#	furniture-3d-system/src/server/integrations/notification-center/client.test.ts
#	furniture-3d-system/src/server/integrations/notification-center/client.ts
#	furniture-3d-system/src/server/integrations/notification-center/types.ts
#	furniture-3d-system/src/server/repositories/__tests__/store-account-management.repository.test.ts
#	furniture-3d-system/src/server/repositories/store-account-management.repository.ts
#	furniture-3d-system/src/server/services/__tests__/notification-delivery.service.test.ts
#	furniture-3d-system/src/server/services/__tests__/store-account-notification.service.test.ts
#	furniture-3d-system/src/server/services/notification-delivery.service.ts
#	furniture-3d-system/src/server/services/sample-progress.service.test.ts
#	furniture-3d-system/src/server/services/store-account-notification.service.ts
2026-09-03 15:44:35 +08:00
WuYongJie
1e1f667530 merge: 发布 BOM 九折预估与极简工作台 2026-09-03 15:41:55 +08:00
WuYongJie
33a13a8abb Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/wuyongjie-bom-estimate-agent-phase0 2026-09-03 15:38:14 +08:00
WuYongJie
4ceedf50ee feat(bom-costing): 固定九折预估并精简工作台 2026-09-03 15:37:34 +08:00
WuYongJie
41a6bbff48 chore: enforce operation log retention policy 2026-09-03 15:36:36 +08:00
WuYongJie
856522552f chore: enforce operation log retention policy 2026-09-03 15:33:22 +08:00
WuYongJie
c30aed3987 feat(supply-chain): enforce import required fields and move sample request column for supplier recommended products
- import rejects rows missing required fields and reports the row number with
  missing labels; drop-shipping fee is required only when shipping mode includes
  代发; date is system-filled on import and not required
- move 申请寄样 after 代发运费 across export, import template, table columns
  and the editor drawer
- gate the download-template button behind edit permission (the template belongs
  to the purchaser import workflow)
- add selection endpoint tests
2026-09-03 15:08:51 +08:00
WuYongJie
205db8234c feat(supply-chain): enforce import required fields and move sample request column for supplier recommended products
- import rejects rows missing required fields and reports the row number with
  missing labels; drop-shipping fee is required only when shipping mode includes
  代发; date is system-filled on import and not required
- move 申请寄样 after 代发运费 across export, import template, table columns
  and the editor drawer
- gate the download-template button behind edit permission (the template belongs
  to the purchaser import workflow)
- add selection endpoint tests
2026-09-03 15:07:09 +08:00
WuYongJie
1edbc110d4 feat(users): show last login status in account management list
账号管理表格在账号状态与操作之间新增最后登录列:从未登录灰色、今天 HH:mm、昨天、N 天前、日期,超 30 天橙色标记闲置账号;组织树接口对管理员已返回 lastLoginAt,本次仅前端改动。
2026-09-03 14:21:23 +08:00
WuYongJie
455025f546 feat(users): show last login status in account management list
账号管理表格在账号状态与操作之间新增最后登录列:从未登录灰色、今天 HH:mm、昨天、N 天前、日期,超 30 天橙色标记闲置账号;组织树接口对管理员已返回 lastLoginAt,本次仅前端改动。
2026-09-03 14:21:06 +08:00
WuYongJie
58d5e0ffdd feat: add operation log scrolling and pagination 2026-09-03 13:53:21 +08:00
WuYongJie
583e3240ce feat: add operation log scrolling and pagination 2026-09-03 13:44:06 +08:00
WuYongJie
88ddd67736 style(finished-product-progress): soften contract group separator to 1px light gray 2026-09-03 13:32:09 +08:00
WuYongJie
259f4b8e7e style(finished-product-progress): soften contract group separator to 1px light gray 2026-09-03 13:29:48 +08:00
WuYongJie
bb561d2482 feat(mobile): streamline mobile navigation and compact intake workspace UI 2026-09-03 13:20:42 +08:00
WuYongJie
8f5fde8d4e feat(mobile): streamline mobile navigation and compact intake workspace UI 2026-09-03 13:18:39 +08:00
WuYongJie
925868fb65 feat(intake): allow designers to delete their own product records
product intake list delete button now also resolves for designers (DESIGN business role or design department paths), restricted to records they created; admins keep full delete scope; server DELETE route and client row button share the same record-level predicate; delete stays soft with operation logs.
2026-09-03 13:06:01 +08:00
WuYongJie
3556b48c4a feat(intake): allow designers to delete their own product records
product intake list delete button now also resolves for designers (DESIGN business role or design department paths), restricted to records they created; admins keep full delete scope; server DELETE route and client row button share the same record-level predicate; delete stays soft with operation logs.
2026-09-03 13:03:49 +08:00
WuYongJie
74ac36f34d feat(finished-product-progress): add filtered export with embedded images
Add an export action that collects all rows matching the current filters (or everything when unfiltered), fetches each row's image from object storage, and builds an ExcelJS workbook with the images embedded in the image column. The workbook headers match the import template so the file round-trips through the existing import flow. Export progress, image fetch failures and completion counts are surfaced in a status panel.
2026-09-03 12:07:58 +08:00
WuYongJie
b129acdc48 feat(finished-product-progress): add filtered export with embedded images
Add an export action that collects all rows matching the current filters (or everything when unfiltered), fetches each row's image from object storage, and builds an ExcelJS workbook with the images embedded in the image column. The workbook headers match the import template so the file round-trips through the existing import flow. Export progress, image fetch failures and completion counts are surfaced in a status panel.
2026-09-03 12:06:29 +08:00
WuYongJie
ee9a7cb88e feat(supply-chain): add operations selection pools and selected-pool export to supplier recommended products
- selection pools: all/unselected/selected switch on the same page, batch select with
  operator search-confirm (status, operator and time recorded with change logs), batch return
- selected rows stamped with a red 已选款 seal on card/thumbnail/detail images and locked
  in the master pool to avoid accidental re-selection
- operations-role users (or admins) can select/return; service re-validates the operator
- selected-pool export: export endpoint follows all active filters, embeds product images
  into the image column (webp converted via sharp to 320px png, fallback to url text),
  includes selection status/operator/time columns, gated by download permission
2026-09-03 11:46:34 +08:00
WuYongJie
970cd3f234 feat(supply-chain): add operations selection pools and selected-pool export to supplier recommended products
- selection pools: all/unselected/selected switch on the same page, batch select with
  operator search-confirm (status, operator and time recorded with change logs), batch return
- selected rows stamped with a red 已选款 seal on card/thumbnail/detail images and locked
  in the master pool to avoid accidental re-selection
- operations-role users (or admins) can select/return; service re-validates the operator
- selected-pool export: export endpoint follows all active filters, embeds product images
  into the image column (webp converted via sharp to 320px png, fallback to url text),
  includes selection status/operator/time columns, gated by download permission
2026-09-03 11:44:41 +08:00
WuYongJie
7f15c5b756 fix finished product progress order dates 2026-09-03 10:01:26 +08:00
WuYongJie
25f5796dd0 fix finished product progress order dates 2026-09-03 09:54:33 +08:00
WuYongJie
ce34f56c39 feat(users): merchandiser inherits purchase permission subject
采购-跟单(merchandiser,含跟单组与雁兰/槐米特例)解析权限主体时追加采购负责人主体:给采购负责人开通的节点权限与供应链权限对跟单自动生效,无需重复开通;核价与质检主体保持独立。
2026-09-02 16:21:01 +08:00
WuYongJie
7ed6c061c4 feat(users): merchandiser inherits purchase permission subject
采购-跟单(merchandiser,含跟单组与雁兰/槐米特例)解析权限主体时追加采购负责人主体:给采购负责人开通的节点权限与供应链权限对跟单自动生效,无需重复开通;核价与质检主体保持独立。
2026-09-02 16:19:57 +08:00
WuYongJie
15f3331d58 feat(supply-chain): support clipboard paste image upload in supplier recommended product editor
- paste a screenshot anywhere in the drawer to upload and fill the storage url
- add file picker button, uploading state, error message and preview thumbnail
2026-09-02 15:51:02 +08:00
WuYongJie
cc0f5e65f4 feat(supply-chain): add category, card view and unified controls to supplier recommended products
- add category field with migration, import template column and multi-select filter (IN query)
- card view aligned with product selection dashboard style; table/card toggle persisted per browser
- purchaser/shipping-mode filters upgraded to searchable multi-select
- table actions as icon-only buttons with a view entry; date filter opens the picker on click
- column settings: reset to defaults and name search; page styles moved to a plain css file
2026-09-02 15:51:02 +08:00
WuYongJie
607b030034 feat(supply-chain): support clipboard paste image upload in supplier recommended product editor
- paste a screenshot anywhere in the drawer to upload and fill the storage url
- add file picker button, uploading state, error message and preview thumbnail
2026-09-02 15:44:14 +08:00
WuYongJie
021d7eab2b feat(supply-chain): add category, card view and unified controls to supplier recommended products
- add category field with migration, import template column and multi-select filter (IN query)
- card view aligned with product selection dashboard style; table/card toggle persisted per browser
- purchaser/shipping-mode filters upgraded to searchable multi-select
- table actions as icon-only buttons with a view entry; date filter opens the picker on click
- column settings: reset to defaults and name search; page styles moved to a plain css file
2026-09-02 15:44:13 +08:00
WuYongJie
adfecb4c67 feat(intake): add sample measurement and product pricing workbook import/export
sample measurement: export template with image/barcode/L-W-H-weight columns (all batches) and import backfill by barcode into draft rows with per-row skip reasons; product pricing: export template with image/barcode/cost-price and import backfill cost price only; both reuse the product limit workbook pattern, keep edits in draft until the existing confirm/push flow, no API or schema changes.
2026-09-02 15:19:48 +08:00
WuYongJie
5aedfea85c feat(intake): add sample measurement and product pricing workbook import/export
sample measurement: export template with image/barcode/L-W-H-weight columns (all batches) and import backfill by barcode into draft rows with per-row skip reasons; product pricing: export template with image/barcode/cost-price and import backfill cost price only; both reuse the product limit workbook pattern, keep edits in draft until the existing confirm/push flow, no API or schema changes.
2026-09-02 15:19:04 +08:00
WuYongJie
58966bba20 feat(finished-product-progress): align column settings drawer with unified standard
Replace the two-column popover with a right-docked drawer: single-column list with drag handles, checkboxes and lock markers; add column-name search, hide-all and reset-default buttons, and persist unlocked column order locally. Locked columns keep their default positions (trailing sticky actions column).
2026-09-02 14:43:01 +08:00
WuYongJie
7740433ae7 feat(finished-product-progress): align column settings drawer with unified standard
Replace the two-column popover with a right-docked drawer: single-column list with drag handles, checkboxes and lock markers; add column-name search, hide-all and reset-default buttons, and persist unlocked column order locally. Locked columns keep their default positions (trailing sticky actions column).
2026-09-02 14:41:19 +08:00
WuYongJie
5c43428a47 feat: route store account notices to DingTalk group 2026-09-02 13:57:51 +08:00
WuYongJie
7f078abb7b feat: route store account notices to DingTalk group 2026-09-02 13:55:00 +08:00
WuYongJie
940e94882e fix(finished-product-progress): allow negative in-transit quantity for over-delivery
Remove the Math.max(0,...) clamps in the in-transit calculation and delivery-record recompute, and switch the input schema inTransitQuantity validator to a dedicated negative-tolerant one. Ordered and delivered quantities stay non-negative.
2026-09-02 12:22:17 +08:00
WuYongJie
7126984caa feat(finished-product-progress): support clipboard paste image upload in editor drawer 2026-09-02 12:22:17 +08:00
WuYongJie
43b867ef55 feat(finished-product-progress): add contract group separators and alternating band backgrounds 2026-09-02 12:22:17 +08:00
WuYongJie
9c459e7195 fix(finished-product-progress): allow negative in-transit quantity for over-delivery
Remove the Math.max(0,...) clamps in the in-transit calculation and delivery-record recompute, and switch the input schema inTransitQuantity validator to a dedicated negative-tolerant one. Ordered and delivered quantities stay non-negative.
2026-09-02 12:19:07 +08:00
WuYongJie
e4df1b2576 feat(finished-product-progress): support clipboard paste image upload in editor drawer 2026-09-02 12:18:26 +08:00
WuYongJie
b2fc4dde6d feat(finished-product-progress): add contract group separators and alternating band backgrounds 2026-09-02 12:18:11 +08:00
WuYongJie
138dc0d644 feat(supply-chain): add finished product select all 2026-09-02 09:23:04 +08:00
WuYongJie
f91554c7dc feat(supply-chain): add finished product select all 2026-09-02 09:17:39 +08:00
WuYongJie
6ec0258001 Merge branch 'release-finished-product-progress' 2026-09-01 18:32:15 +08:00
WuYongJie
9c66706b67 release finished product progress import and management 2026-09-01 18:32:06 +08:00
WuYongJie
f89bc61af4 fix finished product progress import and management 2026-09-01 18:27:04 +08:00
WuYongJie
3c69088433 fix: map store account subject company header 2026-09-01 17:53:56 +08:00
WuYongJie
e60b3b1e60 fix: map store account subject company header 2026-09-01 17:47:45 +08:00
WuYongJie
d06e45b0c8 fix(supply-chain): support supplier recommendation import images 2026-09-01 17:44:32 +08:00
WuYongJie
03eca07f9f fix(supply-chain): support supplier recommendation import images 2026-09-01 17:38:57 +08:00
WuYongJie
242ed201ba feat(store-accounts): add profit center options 2026-09-01 16:04:51 +08:00
WuYongJie
6b8661dcfd feat(store-accounts): add profit center options 2026-09-01 16:02:49 +08:00
WuYongJie
b3f85d5f5a fix(work-record): persist monthly archive rows with stable ids 2026-09-01 14:33:15 +08:00
WuYongJie
dc6c45848a fix(work-record): persist monthly archive rows with stable ids 2026-09-01 14:31:58 +08:00
WuYongJie
eead468d4d fix(users): use standalone permission navigation 2026-09-01 13:37:19 +08:00
WuYongJie
fb51614a86 fix(users): use standalone permission navigation 2026-09-01 13:35:49 +08:00
WuYongJie
e7996abc6a feat(supply-chain): improve supplier recommended product management 2026-09-01 13:08:04 +08:00
WuYongJie
474f245ba7 feat(supply-chain): add supplier recommended products 2026-09-01 13:07:55 +08:00
WuYongJie
03f308261e feat(supply-chain): improve supplier recommended product management 2026-09-01 13:01:55 +08:00
WuYongJie
934b073eea fix(supply-chain): unify list pagination spacing 2026-09-01 12:37:41 +08:00
WuYongJie
9a3c5f9157 fix(supply-chain): unify list pagination spacing 2026-09-01 12:35:29 +08:00
WuYongJie
6e140e1445 fix(work-record): complete monthly archive release 2026-09-01 12:24:38 +08:00
WuYongJie
0b12ec28b9 feat(work-record): add monthly archive editing permissions 2026-09-01 12:08:25 +08:00
WuYongJie
2b5f49fc51 feat(work-record): add monthly archive editing permissions 2026-09-01 11:59:22 +08:00
WuYongJie
7b7e7d7e28 fix(oa-board): prevent production unified table redirect 2026-09-01 11:12:35 +08:00
WuYongJie
b01579ca99 fix(oa-board): prevent production unified table redirect 2026-09-01 11:10:59 +08:00
WuYongJie
d5f2b68d88 Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/local-3001-integration
# Conflicts:
#	furniture-3d-system/agents/script-monitor/README.md
#	furniture-3d-system/agents/script-monitor/agent.mjs
#	furniture-3d-system/agents/script-monitor/config.example.json
#	furniture-3d-system/src/app/api/supply-chain/store-account-management/[id]/bank-account/route.ts
#	furniture-3d-system/src/app/api/supply-chain/store-account-management/profiles/import/route.ts
#	furniture-3d-system/src/app/api/supply-chain/store-account-management/profiles/route.ts
#	furniture-3d-system/src/app/api/supply-chain/store-account-management/profiles/template/route.ts
#	furniture-3d-system/src/app/intake/sample-progress/components/SampleProgressTableRow.tsx
#	furniture-3d-system/src/app/supply-chain/database/store-account-management/StoreAccountManagementView.tsx
#	furniture-3d-system/src/app/supply-chain/script-monitor/ScriptMonitorView.test.tsx
#	furniture-3d-system/src/app/supply-chain/script-monitor/ScriptMonitorView.tsx
#	furniture-3d-system/src/app/supply-chain/script-monitor/page.tsx
#	furniture-3d-system/src/lib/store-account-management.ts
#	furniture-3d-system/src/lib/supply-chain-script-monitor.ts
#	furniture-3d-system/src/server/dto/script-monitor.dto.ts
#	furniture-3d-system/src/server/integrations/notification-center/client.test.ts
#	furniture-3d-system/src/server/integrations/notification-center/client.ts
#	furniture-3d-system/src/server/integrations/notification-center/types.ts
#	furniture-3d-system/src/server/repositories/__tests__/store-account-management.repository.test.ts
#	furniture-3d-system/src/server/repositories/script-monitor.repository.ts
#	furniture-3d-system/src/server/repositories/store-account-management.repository.ts
#	furniture-3d-system/src/server/services/__tests__/notification-delivery.service.test.ts
#	furniture-3d-system/src/server/services/__tests__/store-account-management-export.service.test.ts
#	furniture-3d-system/src/server/services/__tests__/store-account-notification.service.test.ts
#	furniture-3d-system/src/server/services/notification-delivery.service.ts
#	furniture-3d-system/src/server/services/sample-progress.service.test.ts
#	furniture-3d-system/src/server/services/script-monitor.service.test.ts
#	furniture-3d-system/src/server/services/script-monitor.service.ts
#	furniture-3d-system/src/server/services/store-account-management-export.service.ts
#	furniture-3d-system/src/server/services/store-account-notification.service.ts
2026-09-01 11:09:26 +08:00
WuYongJie
a2afbc40bb chore: reconcile worktree snapshot before origin merge (local branch only) 2026-09-01 11:07:53 +08:00
WuYongJie
facc1f0955 feat(supply-chain): restrict store account password visibility 2026-09-01 10:36:27 +08:00
WuYongJie
e38f8e813b feat(supply-chain): restrict store account password visibility 2026-09-01 10:33:49 +08:00
WuYongJie
70e268034e fix(intake): keep default workspace tab available
Cherry-picked as an isolated mainline release from 22227e7cadf00b45056c429f96de489af7c6767c.
2026-09-01 10:11:07 +08:00
WuYongJie
508e0a40d5 fix(intake): keep default workspace tab available 2026-09-01 10:08:05 +08:00
WuYongJie
c0562fdddc fix(intake): keep default workspace tab available 2026-09-01 10:02:57 +08:00
WuYongJie
22227e7cad fix(auth): preserve custom permission role in API context 2026-08-31 18:07:46 +08:00
WuYongJie
7089ada0a0 fix(auth): preserve custom permission role in API context 2026-08-31 18:05:56 +08:00
WuYongJie
9bb36756d1 fix(auth): preserve custom permission role in session 2026-08-31 17:43:30 +08:00
jingchuan
f2a554a330 fix(oa-board): mark XP2026081900000414 terminated 2026-08-31 17:39:04 +08:00
WuYongJie
0aaacb4bff fix(auth): preserve custom permission role in session 2026-08-31 17:38:38 +08:00
WuYongJie
bf7b71ab00 fix(auth): preserve custom permission role in session 2026-08-31 17:36:25 +08:00
WuYongJie
4ecad009b9 cherry-pick: publish approved supply chain updates 2026-08-31 17:17:23 +08:00
WuYongJie
e43bf78c73 feat: publish approved supply chain updates 2026-08-31 17:15:56 +08:00
WuYongJie
fecfd9053e fix: 完善店铺账号通知与筛选体验 2026-08-31 16:34:36 +08:00
WuYongJie
7c6be91a91 fix: 完善店铺账号通知与筛选体验 2026-08-31 16:30:58 +08:00
WuYongJie
7d246e1d5b fix(product-archive): include full export client hook
Add the client hook omitted from the isolated main integration.
2026-08-31 16:02:37 +08:00
WuYongJie
b974d83986 feat(product-archive): add full export and column filters
Applied only the product archive feature patch from 232eaa87 onto origin/main.
2026-08-31 15:52:20 +08:00
WuYongJie
232eaa8768 feat(product-archive): add full export and column filters
Cherry-picked feature patch from c2289ca0 onto latest codex/xuxiake-mainline without including unrelated working-tree changes.
2026-08-31 15:49:13 +08:00
WuYongJie
c2289ca074 feat(product-archive): add full export and column filters 2026-08-31 15:42:07 +08:00
WuYongJie
68916e2791 feat: store account management updates 2026-08-31 13:53:44 +08:00
WuYongJie
72262acfa0 feat: complete store account management updates 2026-08-31 13:52:18 +08:00
WuYongJie
4950135cfc fix: normalize notification line breaks 2026-08-31 12:19:26 +08:00
WuYongJie
d40b6ad197 fix: normalize notification line breaks 2026-08-31 12:15:44 +08:00
WuYongJie
315ede4f3f fix: normalize notification line breaks 2026-08-31 12:13:36 +08:00
WuYongJie
f3b0124d4c feat(intake): add supplier filter and fixed development toolbar 2026-08-31 11:19:57 +08:00
WuYongJie
fb53234811 feat(intake): add supplier filter and fixed development toolbar 2026-08-31 11:17:30 +08:00
WuYongJie
9e51faa192 feat: notify on store account creation 2026-08-31 10:39:08 +08:00
WuYongJie
916930a714 feat: notify on store account creation 2026-08-31 10:38:33 +08:00
WuYongJie
7c17626339 feat: notify on store account creation 2026-08-31 10:27:20 +08:00
WuYongJie
368107c17b release: publish approved sample and finished progress updates 2026-08-31 10:24:00 +08:00
WuYongJie
865be95b6e feat: submit approved sample and finished progress updates 2026-08-31 10:21:29 +08:00
WuYongJie
3efe408a27 merge: release product development base SPU grouping 2026-08-29 16:52:10 +08:00
WuYongJie
b1505b4d63 feat(intake): group product development list by base SPU 2026-08-29 16:14:03 +08:00
WuYongJie
e1b7683bfd fix(supply-chain): include finished product delivery service 2026-08-29 15:35:59 +08:00
WuYongJie
a69bf1997b feat(supply-chain): add multi-delivery tracking to finished product progress 2026-08-29 15:25:36 +08:00
WuYongJie
622fc9d527 feat(supply-chain): refine finished product progress import 2026-08-29 15:17:19 +08:00
WuYongJie
3409a9db50 feat(supply-chain): add multi-delivery tracking to finished product progress 2026-08-29 15:07:12 +08:00
WuYongJie
0f55175d9b fix: respect configured HR SQL Server schema
Use HR_SQLSERVER_SCHEMA for default employee directory queries so production can read the rpt source tables.
2026-08-29 14:07:33 +08:00
WuYongJie
0811bea57c fix: respect configured HR SQL Server schema
Use HR_SQLSERVER_SCHEMA for default employee directory queries so production can read the rpt source tables.
2026-08-29 14:06:54 +08:00
WuYongJie
0ff6f87757 feat(supply-chain): add supplier recommended products 2026-08-29 13:31:45 +08:00
WuYongJie
089845e7ee merge: integrate employee directory account protection 2026-08-29 13:25:24 +08:00
WuYongJie
ab5ea15e80 fix: protect external accounts during employee directory sync 2026-08-29 13:23:26 +08:00
WuYongJie
1347a34d14 feat(supply-chain): refine finished product progress import 2026-08-29 13:08:12 +08:00
WuYongJie
6a32957bf5 fix: protect external accounts during employee directory sync 2026-08-29 12:04:57 +08:00
WuYongJie
9bf54d95d3 fix: protect external accounts during employee directory sync 2026-08-29 12:01:18 +08:00
WuYongJie
dc16d2b75c merge: release script monitor machine deletion 2026-08-28 20:48:38 +08:00
WuYongJie
cd73be673d feat(script-monitor): allow deleting monitored machines 2026-08-28 20:47:47 +08:00
WuYongJie
85c31677c5 merge: release script monitor operations and store account completion
# Conflicts:
#	furniture-3d-system/prisma/schema.prisma
#	furniture-3d-system/src/app/api/supply-chain/store-account-management/[id]/bank-account/route.ts
#	furniture-3d-system/src/app/api/supply-chain/store-account-management/profiles/import/route.ts
#	furniture-3d-system/src/app/api/supply-chain/store-account-management/profiles/route.ts
#	furniture-3d-system/src/app/api/supply-chain/store-account-management/profiles/template/route.ts
2026-08-28 18:28:11 +08:00
WuYongJie
59634f8516 merge: integrate latest shared mainline 2026-08-28 18:23:01 +08:00
WuYongJie
d8495e98ef fix(store-account): complete unified profile workflow 2026-08-28 18:17:01 +08:00
WuYongJie
ea363984a7 merge: release approved store account updates 2026-08-28 17:49:53 +08:00
WuYongJie
47df269785 release: approve store account updates and users build fix 2026-08-28 17:49:53 +08:00
WuYongJie
19e39e0b8c fix: remove invalid users page export 2026-08-28 17:46:29 +08:00
WuYongJie
68c48086df feat: extend store account profiles and bank account 2026-08-28 17:46:14 +08:00
WuYongJie
e34c80cdd2 feat(supply-chain): add script monitor operations 2026-08-28 17:38:51 +08:00
WuYongJie
73b3049e01 fix(store-account): export bank account crypto helpers 2026-08-28 16:31:43 +08:00
WuYongJie
978b609325 merge: release accepted five features and latest long-lived changes 2026-08-28 16:12:43 +08:00
WuYongJie
6e9d0e474f merge: release five accepted features 2026-08-28 16:09:04 +08:00
WuYongJie
240ea4b01a feat(supply-chain): unify store profile entry 2026-08-28 16:07:35 +08:00
WuYongJie
422c23d3e5 release: merge latest development with script monitoring 2026-08-28 15:51:55 +08:00
WuYongJie
f974e7d4b4 feat(intake): bulk maintain barcode print imports 2026-08-28 15:43:15 +08:00
WuYongJie
b27399d908 feat(supply-chain): maintain supplier information by barcode 2026-08-28 15:43:15 +08:00
WuYongJie
cc7e45a225 feat(intake): add fuzzy SKU material import 2026-08-28 15:43:14 +08:00
WuYongJie
0a466dcb2b feat(installation): add hinge articulation motion 2026-08-28 15:43:14 +08:00
WuYongJie
4492c0eb8f feat(supply-chain): add finished product progress detail 2026-08-28 15:43:13 +08:00
WuYongJie
0f336e9d1e feat(supply-chain): add script monitoring center 2026-08-28 15:41:35 +08:00
WuYongJie
4d34a387a9 feat(supply-chain): add store account exports and permissions 2026-08-28 09:41:13 +08:00
WuYongJie
571b840911 feat(supply-chain): add store account exports and permissions 2026-08-28 09:39:08 +08:00
WuYongJie
78cfa85232 release: integrate approved product and notification updates
Includes 13268ee5, 01a9d1e2, 48e496b0, bf85258d.
Store account fix e48229ab is already integrated by parent d8141a63.
2026-08-27 16:39:54 +08:00
WuYongJie
d8141a63a9 fix(supply-chain): allow incomplete store account imports
Isolated main integration of e48229ab.
2026-08-27 16:34:50 +08:00
WuYongJie
bf85258d3c feat(notifications): add product development reminders 2026-08-27 15:13:22 +08:00
WuYongJie
e48229ab22 fix(supply-chain): allow incomplete store account imports 2026-08-27 15:13:21 +08:00
WuYongJie
48e496b01b fix(intake): preserve confirmation after factory edits 2026-08-27 15:13:20 +08:00
WuYongJie
01a9d1e2c8 fix(intake): default product type to all 2026-08-27 15:13:20 +08:00
WuYongJie
13268ee5b5 feat(intake): default sku material compliance 2026-08-27 15:13:19 +08:00
WuYongJie
55c99cbc4d feat(sample-progress): enhance detail collaboration 2026-08-27 13:53:01 +08:00
WuYongJie
023c4c07d2 feat(sample-progress): enhance detail collaboration 2026-08-27 13:52:56 +08:00
WuYongJie
37074cf25b feat(supply-chain): stream store account import progress 2026-08-27 13:14:23 +08:00
WuYongJie
1619213039 feat(supply-chain): stream store account import progress 2026-08-27 13:11:11 +08:00
WuYongJie
5da6c34758 feat(installation): auto-explode unnamed assemblies 2026-08-26 18:29:15 +08:00
WuYongJie
879a73e480 feat(supply-chain): add product, supplier, and store management 2026-08-26 18:28:55 +08:00
WuYongJie
629ad42104 feat(installation): auto-explode unnamed assemblies 2026-08-26 18:19:24 +08:00
WuYongJie
a34dee6825 feat(supply-chain): add product, supplier, and store management 2026-08-26 18:18:41 +08:00
WuYongJie
8b8a41f2d7 fix(intake): preserve sku identities on delete 2026-08-26 16:49:42 +08:00
WuYongJie
3f5cf79cc2 fix(intake): preserve sku identities on delete 2026-08-26 16:48:22 +08:00
WuYongJie
c093c32e1d fix(intake): preserve sku identities on delete 2026-08-26 16:46:57 +08:00
WuYongJie
07d6d0d03f fix(intake): isolate basic info confirmation by batch 2026-08-26 16:25:41 +08:00
WuYongJie
71e746b5f5 fix(intake): isolate basic info confirmation by batch 2026-08-26 16:23:46 +08:00
WuYongJie
9761465d45 fix(intake): isolate basic info confirmation by batch 2026-08-26 16:20:04 +08:00
WuYongJie
aa5d983b7d merge: release SKU batch workbook isolation 2026-08-26 13:19:46 +08:00
WuYongJie
d0ddbb6ccf fix(intake): isolate SKU batch workbook imports 2026-08-26 13:17:16 +08:00
WuYongJie
77b3b3c800 merge: release daily Jushuitan fixed virtual inventory 2026-08-25 14:27:28 +08:00
WuYongJie
2cd254fee5 fix(jushuitan): schedule fixed virtual inventory daily 2026-08-25 14:26:02 +08:00
WuYongJie
ffc9c1a727 fix(jushuitan): schedule fixed virtual inventory daily 2026-08-25 14:07:36 +08:00
jingchuan
b23e4122f2 feat(supply-chain): organize sample timing by plan tabs 2026-08-25 13:04:30 +08:00
WuYongJie
42e4503ce6 test(auth): cover sample editor permission scope 2026-08-25 11:12:55 +08:00
jingchuan
336b47a5fc fix: classify sample timing by review product type 2026-08-25 10:35:32 +08:00
jingchuan
ecf4d4f743 fix: pass supply chain scope through sample editor 2026-08-24 18:00:47 +08:00
jingchuan
5984f82c75 Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/jingchuan-sample-dimensions-preview-20260824 2026-08-24 17:57:37 +08:00
jingchuan
2e33b9b1c0 feat: refine sample timing and procurement analysis 2026-08-24 17:56:55 +08:00
WuYongJie
73ffcb17bb feat(users): create suppliers from warehouse directory 2026-08-24 17:43:52 +08:00
WuYongJie
8365286651 feat(auth): enforce permission contract 2026-08-24 17:43:52 +08:00
WuYongJie
b497c2d061 fix(intake): preserve basic info confirmation after factory sync 2026-08-24 16:42:03 +08:00
WuYongJie
10c32b2c97 fix(intake): preserve basic info confirmation after factory sync 2026-08-24 16:40:58 +08:00
WuYongJie
bfbe044005 feat(sample-progress): move editing into drawer 2026-08-24 10:20:59 +08:00
WuYongJie
0fa4481719 fix(intake): project factory model for barcode printing 2026-08-24 09:21:11 +08:00
WuYongJie
b0a9aa0c4b feat(intake): sync factory-confirmed SKUs to barcode printing 2026-08-21 18:32:51 +08:00
jingchuan
fcdf25ed38 fix(supply-chain): align flow detail dates with status 2026-08-21 17:34:26 +08:00
jingchuan
e8531fde1d feat(supply-chain): focus sample owner load on inflight tasks 2026-08-21 17:16:39 +08:00
jingchuan
64f1b2732b merge: synchronize latest production mainline before termination fix 2026-08-21 16:44:51 +08:00
jingchuan
9a68b886ac merge: synchronize latest shared mainline before termination fix 2026-08-21 16:44:43 +08:00
jingchuan
9bb14e391d fix(oa-board): preserve terminated opening status 2026-08-21 16:44:20 +08:00
jingchuan
d252924a21 feat(supply-chain): add sample performance analysis 2026-08-21 16:27:39 +08:00
WuYongJie
19f9affe5c 供应链:打样进度明细移入采购管理 2026-08-21 15:20:29 +08:00
jingchuan
b15c292f12 merge: synchronize shared mainline before OA binding release 2026-08-21 13:36:48 +08:00
jingchuan
03c97d234b merge: persist OA historical order binding 2026-08-21 13:33:18 +08:00
WuYongJie
b1cebf0707 Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/local-3001-integration
# Conflicts:
#	furniture-3d-system/src/app/supply-chain/database/product-archive/ProductArchiveView.tsx
#	furniture-3d-system/src/lib/permissions.ts
#	furniture-3d-system/src/lib/workspace-route-permissions.ts
#	furniture-3d-system/src/server/core/types.ts
#	furniture-3d-system/src/server/repositories/user.repository.ts
#	furniture-3d-system/src/server/services/auth.service.ts
2026-08-21 13:11:21 +08:00
WuYongJie
58fa0c7133 test(users): cover permission role activation 2026-08-21 11:47:16 +08:00
WuYongJie
2631849409 test(users): cover permission role activation 2026-08-21 11:46:33 +08:00
WuYongJie
e5985cf4cd feat(users): add administrator account filter 2026-08-21 11:33:32 +08:00
WuYongJie
bd651338dc feat(users): add administrator account filter 2026-08-21 11:30:21 +08:00
WuYongJie
022298db92 refactor(supply-chain): clarify sync and UI ownership
Integrate the reviewed supply-chain structure governance scope without unpublished image-search changes.
2026-08-21 11:12:34 +08:00
WuYongJie
d0c11dd979 refactor(supply-chain): clarify sync and UI ownership
Integrate the reviewed supply-chain structure governance scope without unpublished image-search changes.
2026-08-21 11:12:34 +08:00
WuYongJie
a26f53aaba refactor(supply-chain): clarify sync and UI ownership 2026-08-21 10:54:30 +08:00
jingchuan
1075eea0aa fix(oa-board): bind historical order approval 2026-08-21 10:45:26 +08:00
WuYongJie
b15329bec8 feat(users): sync permissions with real navigation 2026-08-21 10:34:45 +08:00
WuYongJie
9faa52d91d feat(users): sync permissions with real navigation 2026-08-21 10:34:45 +08:00
WuYongJie
c8c06c88e9 Merge origin/main into shared development mainline 2026-08-21 10:14:29 +08:00
WuYongJie
4fe426ce56 fix(intake): prevent modular barcode prefix clipping 2026-08-21 09:38:44 +08:00
WuYongJie
e8990d7e63 fix(intake): prevent modular barcode prefix clipping 2026-08-21 09:36:44 +08:00
WuYongJie
075f37f31f feat(users): redesign account and role permissions 2026-08-20 17:47:21 +08:00
WuYongJie
095bae1be8 feat(users): redesign account and role permissions 2026-08-20 17:40:21 +08:00
WuYongJie
1e035f10a4 feat(supply-chain): sync clearance stock 2026-08-20 16:34:46 +08:00
WuYongJie
f5d7cfb2df feat(supply-chain): sync clearance stock 2026-08-20 16:32:55 +08:00
jingchuan
26ce017921 feat(supply-chain): streamline terminated task details 2026-08-20 15:20:22 +08:00
WuYongJie
10ae34fc4a fix(intake): split modular barcode suffix 2026-08-20 15:12:22 +08:00
WuYongJie
482858405c feat(supply-chain): read JST public available stock 2026-08-20 15:12:22 +08:00
WuYongJie
aae05326ab fix(intake): split modular barcode suffix 2026-08-20 15:09:12 +08:00
WuYongJie
b20ffaa30f feat(supply-chain): read JST public available stock 2026-08-20 15:09:12 +08:00
WuYongJie
07516303d4 fix(intake): fit long certificate product names 2026-08-20 13:55:35 +08:00
WuYongJie
61c66362bb fix(intake): fit long certificate product names 2026-08-20 13:51:46 +08:00
jingchuan
633b7a37dd feat(supply-chain): filter termination feedback by creator 2026-08-20 11:00:41 +08:00
jingchuan
fa32c6bec4 fix(supply-chain): archive missing termination instances 2026-08-20 10:37:22 +08:00
jingchuan
22744b28c8 feat(supply-chain): archive termination reasons 2026-08-20 10:28:25 +08:00
jingchuan
2897f3b0c6 fix(supply-chain): refine terminated task drilldowns 2026-08-19 17:58:54 +08:00
jingchuan
718c6a8886 merge: add terminated task feedback 2026-08-19 16:59:54 +08:00
jingchuan
32fedbe898 feat(supply-chain): add terminated task feedback 2026-08-19 16:58:13 +08:00
WuYongJie
0bf579a6f0 merge: add admin barcode data export 2026-08-19 15:03:41 +08:00
WuYongJie
1dc2c04b20 feat(intake): add admin barcode data export 2026-08-19 15:02:48 +08:00
WuYongJie
e645a71283 merge: align permission nodes and add pricing role 2026-08-19 14:50:38 +08:00
WuYongJie
9a8c25fe24 fix(users): align permission nodes and add pricing role 2026-08-19 14:48:09 +08:00
WuYongJie
8ee0dd21fe feat(users): refine permission management 2026-08-19 13:50:13 +08:00
WuYongJie
c0aab147bf feat(users): refine permission management 2026-08-19 13:44:38 +08:00
jingchuan
52ac60cf29 merge: fix near-overtime display precision 2026-08-19 13:26:14 +08:00
jingchuan
6178d09e31 fix(supply-chain): round near-overtime remaining days 2026-08-19 13:25:45 +08:00
jingchuan
7e772dcfa8 merge: publish timing hover summaries 2026-08-19 13:06:33 +08:00
jingchuan
1f9293cc42 feat(supply-chain): refine timing hover summaries 2026-08-19 13:04:46 +08:00
jingchuan
e2b711c0df merge: include material in procurement timing 2026-08-19 12:01:08 +08:00
jingchuan
94f754c80b fix(supply-chain): include material in procurement timing 2026-08-19 11:11:02 +08:00
WuYongJie
2d1516e621 merge: add product archive scrollbar wheel 2026-08-19 11:06:41 +08:00
WuYongJie
68051dd0bf feat(supply-chain): add product archive scrollbar wheel 2026-08-19 11:05:41 +08:00
WuYongJie
a8184ae024 merge: fix installation demo snapshot resave 2026-08-19 10:40:36 +08:00
WuYongJie
efbf1c1a41 fix(installation-demo): preserve existing snapshots on resave 2026-08-19 10:39:33 +08:00
WuYongJie
66dced1c3f merge: add fee ratio and operating profit to product archive 2026-08-19 10:03:04 +08:00
WuYongJie
6024abe7aa feat(supply-chain): add fee ratio and operating profit 2026-08-19 10:01:21 +08:00
jingchuan
b16320c352 merge: publish supply-chain new product timing drilldown 2026-08-19 09:39:26 +08:00
jingchuan
8cfd3f8292 fix(supply-chain): preserve new product overview entry 2026-08-19 09:37:15 +08:00
jingchuan
053a603308 feat(supply-chain): add running timing department drilldown 2026-08-19 09:34:18 +08:00
WuYongJie
90eeba280d Merge sample progress detail rebuild into main
Includes only f8555a49: the approved sample progress detail table and its API, persistence, navigation, history, and migration changes.
2026-08-19 09:00:52 +08:00
WuYongJie
f8555a4946 feat: rebuild sample progress detail
Implement the complete sample progress detail table from the approved production baseline, including source-derived timing fields, editable sampling fields, review links, change history, pagination and supply-chain navigation updates.
2026-08-19 09:00:13 +08:00
jingchuan
b840de8d55 merge: supply-chain timing departments into main 2026-08-18 19:12:05 +08:00
jingchuan
641a162675 merge: completed timing department analysis 2026-08-18 19:06:19 +08:00
jingchuan
d507aa08f7 feat(supply-chain): drill into department overtime flows 2026-08-18 19:02:08 +08:00
jingchuan
90e58ecaf7 feat(supply-chain): add completed timing department metrics 2026-08-18 18:49:20 +08:00
WuYongJie
f0730ec03d fix: stabilize installation shares and restore supply-chain home navigation 2026-08-18 18:17:02 +08:00
WuYongJie
a51936ed98 Merge remote-tracking branch 'origin/main' into codex/wuyongjie-bom-source-retry-release 2026-08-18 17:01:56 +08:00
WuYongJie
91026fccd7 Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/wuyongjie-bom-source-retry-release 2026-08-18 16:55:27 +08:00
jingchuan
45f8aaf449 Merge shared timing image fix into main 2026-08-18 16:52:17 +08:00
jingchuan
6b6564e59a Merge timing detail image preview fix 2026-08-18 16:52:01 +08:00
jingchuan
58aa36720b fix(supply-chain): enlarge timing detail images 2026-08-18 16:50:26 +08:00
WuYongJie
5e20f8178f Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/wuyongjie-bom-source-retry-release 2026-08-18 16:48:10 +08:00
WuYongJie
239cae7b5a Merge branch 'codex/wuyongjie-bom-source-retry' into codex/wuyongjie-bom-source-retry-integration 2026-08-18 16:43:22 +08:00
WuYongJie
f2599a5a37 Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/wuyongjie-bom-source-retry 2026-08-18 16:41:22 +08:00
WuYongJie
2db8e39666 fix(oa): keep page module exports Next-compatible 2026-08-18 16:39:30 +08:00
WuYongJie
e267426f5e Merge BOM-only session redirect fix 2026-08-18 16:33:51 +08:00
WuYongJie
cfe9ce4e7b fix(auth): preserve BOM-only session source 2026-08-18 16:32:58 +08:00
WuYongJie
e9cd4e312a fix(auth): preserve BOM-only session source 2026-08-18 16:32:30 +08:00
WuYongJie
8d09d19206 Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/wuyongjie-bom-source-retry 2026-08-18 16:16:48 +08:00
WuYongJie
d3b4d56934 fix(bom): clarify source read failures and allow retry 2026-08-18 16:16:42 +08:00
WuYongJie
f5cc1764cc merge: release workspace switcher 2026-08-18 16:09:25 +08:00
WuYongJie
6f10780bc5 feat(auth): add BOM-costing-only account access 2026-08-18 15:59:08 +08:00
WuYongJie
d264efc2e7 feat(auth): add BOM-costing-only account access 2026-08-18 15:57:39 +08:00
WuYongJie
63b37b564a feat(auth): add BOM-costing-only account access 2026-08-18 15:57:05 +08:00
WuYongJie
3ba90b83e0 feat(navigation): add workspace switcher 2026-08-18 15:56:01 +08:00
WuYongJie
1ffee0d6cc merge: release installation demo part material workflow 2026-08-18 15:19:19 +08:00
WuYongJie
ec5875367a feat(installation-demo): add part material editing workflow 2026-08-18 15:16:20 +08:00
jingchuan
d6e78edaf2 Merge shared supply-chain timing overview into main 2026-08-18 15:02:00 +08:00
jingchuan
75f7b3ac7f Merge supply-chain new product timing overview 2026-08-18 15:01:20 +08:00
jingchuan
df3a8a7894 Merge latest shared mainline before supply-chain timing release 2026-08-18 14:59:34 +08:00
jingchuan
d8d254598b feat(supply-chain): refine timing metrics and detail imagery 2026-08-18 14:57:41 +08:00
jingchuan
b83a95df06 feat(supply-chain): split completed and running timing 2026-08-18 12:52:54 +08:00
jingchuan
20c6433273 fix(supply-chain): use single-select opening brand metric 2026-08-18 09:13:48 +08:00
WuYongJie
033c1058e9 merge: release BOM completion feedback 2026-08-18 00:33:11 +08:00
WuYongJie
86b60103c1 merge: align BOM completion feedback 2026-08-18 00:32:56 +08:00
WuYongJie
51f4c97489 fix(bom-costing): align completed history status 2026-08-18 00:27:14 +08:00
WuYongJie
4a8cbcdcb3 fix(bom-costing): refresh completed run feedback 2026-08-18 00:18:20 +08:00
WuYongJie
c30b094194 merge: synchronize BOM estimate result-first flow 2026-08-17 23:39:56 +08:00
WuYongJie
2be5f59220 merge: preserve BOM estimates after structure review 2026-08-17 23:39:17 +08:00
WuYongJie
c80c06d2f7 fix(bom-costing): preserve estimates after structure review 2026-08-17 23:22:50 +08:00
WuYongJie
4c03c35e8d Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/wuyongjie-bom-estimate-release-20260817 2026-08-17 21:21:18 +08:00
WuYongJie
c7da99562d Merge remote-tracking branch 'origin/codex/wuyongjie-bom-1688-realtime-cost' into codex/wuyongjie-bom-estimate-integration-20260817 2026-08-17 21:21:12 +08:00
WuYongJie
f08d4ff5c4 fix(bom-costing): read stored images through object storage 2026-08-17 21:20:56 +08:00
WuYongJie
fece43291b Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/wuyongjie-bom-estimate-release-20260817 2026-08-17 21:14:11 +08:00
WuYongJie
e674843455 Merge remote-tracking branch 'origin/codex/wuyongjie-bom-1688-realtime-cost' into codex/wuyongjie-bom-estimate-integration-20260817 2026-08-17 21:13:50 +08:00
WuYongJie
c038bcedbe fix(bom-costing): fetch stored images for realtime estimates 2026-08-17 21:13:23 +08:00
WuYongJie
f64fac7a68 Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/wuyongjie-bom-estimate-release-20260817 2026-08-17 20:59:43 +08:00
WuYongJie
6f0fdf3c71 Merge remote-tracking branch 'origin/codex/wuyongjie-bom-1688-realtime-cost' into codex/wuyongjie-bom-estimate-integration-20260817 2026-08-17 20:58:59 +08:00
WuYongJie
7f67bd858c Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/wuyongjie-bom-1688-realtime-cost 2026-08-17 20:54:52 +08:00
WuYongJie
d3a1049b55 feat(bom-costing): complete unified cost estimates 2026-08-17 20:54:45 +08:00
jingchuan
62e7ea28e4 fix(supply-chain): recognize configured node stage sla 2026-08-17 19:33:38 +08:00
jingchuan
e5e7c3bd07 fix(supply-chain): map review-stage sla 2026-08-17 19:26:58 +08:00
jingchuan
9a17715a21 fix(supply-chain): correct new product overview metrics 2026-08-17 19:19:14 +08:00
jingchuan
41b33c731c fix(oa-board): restore original workflow dashboard entry 2026-08-17 19:00:16 +08:00
jingchuan
156b431dda fix(intake): preserve existing new product dashboard 2026-08-17 18:53:46 +08:00
WuYongJie
fa48bcc48d Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/wuyongjie-bom-1688-realtime-cost 2026-08-17 18:48:08 +08:00
WuYongJie
4e892573df fix(bom-costing): sign 1688 image search requests 2026-08-17 18:48:02 +08:00
jingchuan
5ac98a6b82 fix(supply-chain): show product overview in sidebar 2026-08-17 18:46:47 +08:00
jingchuan
5441d1786d feat: add executive overview to supply chain new products 2026-08-17 18:36:57 +08:00
WuYongJie
5cfbcaa5fe feat(installation-demo): refine printable drawing workflow 2026-08-17 14:55:22 +08:00
jingchuan
330cf30ac9 feat: expose new product management dashboard 2026-08-17 13:44:30 +08:00
jingchuan
5af369ef3d feat: add new product management executive dashboard 2026-08-17 13:36:46 +08:00
WuYongJie
0c782238af Merge product archive sales performance into main 2026-08-17 12:09:00 +08:00
WuYongJie
5d60abd57b feat(supply-chain): sync product archive sales performance 2026-08-17 12:07:57 +08:00
jingchuan
ab66b20893 Merge OA latest child flow selection into main 2026-08-17 11:40:00 +08:00
jingchuan
dded0237d0 Merge OA latest child flow selection 2026-08-17 11:39:18 +08:00
jingchuan
fb4fa3efca fix(oa-board): prefer latest restarted child flow 2026-08-17 11:38:37 +08:00
WuYongJie
6a3bd0fa22 Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/wuyongjie-bom-1688-realtime-cost 2026-08-17 10:29:00 +08:00
jingchuan
9929f6a3c6 Merge remote-tracking branch 'origin/codex/xuxiake-mainline' 2026-08-17 09:14:56 +08:00
jingchuan
04379751f6 Merge remote-tracking branch 'origin/codex/jingchuan-unified-table-stage-fix' into codex/xuxiake-mainline 2026-08-17 09:14:41 +08:00
jingchuan
8c2776ec1d Rename sample progress detail view 2026-08-17 09:14:25 +08:00
jingchuan
54548071ad Merge remote-tracking branch 'origin/codex/xuxiake-mainline' 2026-08-16 20:33:41 +08:00
jingchuan
e24adfa492 Merge remote-tracking branch 'origin/codex/jingchuan-unified-table-stage-fix' into codex/xuxiake-mainline 2026-08-16 20:33:28 +08:00
jingchuan
54af0add08 Rename unified table to new product progress summary 2026-08-16 20:33:08 +08:00
jingchuan
d8dd85d380 Merge remote-tracking branch 'origin/codex/xuxiake-mainline' 2026-08-16 20:21:21 +08:00
jingchuan
867de78fe7 Merge remote-tracking branch 'origin/codex/jingchuan-unified-table-stage-fix' into codex/xuxiake-mainline 2026-08-16 20:21:06 +08:00
jingchuan
cd4b6f7023 fix(supply-chain): move timing view toggle to table header 2026-08-16 20:19:52 +08:00
jingchuan
ec687f4788 Merge remote-tracking branch 'origin/codex/xuxiake-mainline' 2026-08-16 19:57:21 +08:00
jingchuan
e0fb0f22a4 Merge remote-tracking branch 'origin/codex/jingchuan-unified-table-stage-fix' into codex/xuxiake-mainline 2026-08-16 19:57:10 +08:00
jingchuan
816d2ff544 fix(supply-chain): correct unified table stage groups 2026-08-16 19:56:54 +08:00
jingchuan
59a1449d73 Merge remote-tracking branch 'origin/codex/xuxiake-mainline' 2026-08-16 17:26:06 +08:00
jingchuan
7b5fea54c5 fix(supply-chain): refine unified table layout 2026-08-16 17:25:54 +08:00
jingchuan
bd33cfc936 Merge remote-tracking branch 'origin/codex/xuxiake-mainline' 2026-08-16 16:35:05 +08:00
jingchuan
b69f1c8eb6 Merge branch 'codex/jingchuan-unified-table-groups' into codex/xuxiake-mainline 2026-08-16 16:34:48 +08:00
jingchuan
016280c1a3 feat(supply-chain): group unified table columns 2026-08-15 18:28:54 +08:00
jingchuan
f78f8c1cf4 Merge supply chain new product views 2026-08-15 17:41:08 +08:00
jingchuan
7a1d05b7db Merge sample progress and supply chain new product views
# Conflicts:
#	furniture-3d-system/src/app/supply-chain/config/supply-chain-menu.test.ts
2026-08-15 17:40:03 +08:00
jingchuan
ceda595797 feat(supply-chain): add new product data views 2026-08-15 17:27:50 +08:00
WuYongJie
5082213a11 Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/wuyongjie-bom-1688-realtime-cost 2026-08-15 17:07:11 +08:00
WuYongJie
5b7435f218 Merge supply chain business permissions 2026-08-15 17:06:57 +08:00
WuYongJie
57b502e1c3 feat(bom-costing): prioritize realtime 1688 procurement estimates 2026-08-15 17:06:46 +08:00
WuYongJie
65c1cd6d93 Merge supply chain business permissions 2026-08-15 17:05:53 +08:00
WuYongJie
465a92612f feat(supply-chain): add business permission controls 2026-08-15 17:05:19 +08:00
WuYongJie
8ffbea63ce Merge installation demo drawing generator 2026-08-15 16:56:38 +08:00
WuYongJie
8b1cb357ea Merge installation demo drawing generator 2026-08-15 16:55:43 +08:00
WuYongJie
d711742300 feat(installation-demo): add printable installation drawings 2026-08-15 16:55:19 +08:00
WuYongJie
9142d24612 Merge development mainline for BOM GPT reasoning fix 2026-08-15 16:11:05 +08:00
WuYongJie
72f2519e3a Merge BOM GPT reasoning fix 2026-08-15 16:10:34 +08:00
WuYongJie
d0eb2d4bd4 fix(bom-costing): use supported GPT-5 reasoning effort 2026-08-15 16:09:56 +08:00
WuYongJie
2972a8fb7a Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/wuyongjie-bom-ai-release-20260815 2026-08-15 15:50:51 +08:00
WuYongJie
d2c6f0749a Merge remote-tracking branch 'origin/codex/wuyongjie-bom-ai-estimation' into codex/wuyongjie-bom-ai-integration-20260815 2026-08-15 15:46:40 +08:00
WuYongJie
a0029c7dec feat(bom-costing): estimate unresolved lines with multimodal model 2026-08-15 15:42:33 +08:00
WuYongJie
07008eeeb7 fix(intake): keep patent workspace on full product source 2026-08-14 15:05:27 +08:00
WuYongJie
16b43cba11 feat(intake): paginate product patent ledger 2026-08-14 14:09:23 +08:00
WuYongJie
c3566c4b83 Merge remote-tracking branch 'origin/main' into codex/local-3001-integration 2026-08-14 13:08:16 +08:00
WuYongJie
026ea66c62 Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/local-3001-integration 2026-08-14 13:06:32 +08:00
WuYongJie
4511c0ad51 fix: include selected patent applications in detail 2026-08-14 12:10:33 +08:00
WuYongJie
c0f9c147fa merge: release flip shoe cabinet costing fix 2026-08-14 11:49:25 +08:00
WuYongJie
ecce8a1a28 merge: fix flip shoe cabinet costing 2026-08-14 11:41:34 +08:00
WuYongJie
7c53b42199 fix(bom-costing): classify flip shoe cabinets correctly 2026-08-14 11:37:57 +08:00
WuYongJie
8016482127 merge(main): release product progress completion filters 2026-08-14 10:09:36 +08:00
WuYongJie
413debe66b merge(intake): add product progress completion filters 2026-08-14 10:08:29 +08:00
WuYongJie
f8d877c88c feat(intake): filter product progress by completion time 2026-08-14 10:07:34 +08:00
WuYongJie
c6c2790354 merge(main): retire sku operator archive source 2026-08-14 09:42:25 +08:00
WuYongJie
b2e672139e refactor(supply-chain): retire sku operator source 2026-08-14 09:40:23 +08:00
WuYongJie
0d6d080687 refactor(supply-chain): retire sku operator source 2026-08-14 09:39:20 +08:00
WuYongJie
ec38c6e64a merge(main): release on-demand product archive images 2026-08-14 08:55:33 +08:00
WuYongJie
d786515831 fix(supply-chain): cache material images on SPU lookup 2026-08-14 08:54:21 +08:00
WuYongJie
032b609f1b fix(supply-chain): cache material images on SPU lookup 2026-08-14 08:53:05 +08:00
WuYongJie
502d85decb merge(main): release archive group header alignment 2026-08-13 14:53:46 +08:00
jingchuan
88fbdbacd2 fix(sample-progress): match flow detail typography and filter defaults 2026-08-13 14:51:25 +08:00
WuYongJie
42ad3299a3 fix(supply-chain): center archive group headers 2026-08-13 14:45:24 +08:00
WuYongJie
bdb21cd824 fix(supply-chain): center archive group headers 2026-08-13 14:39:27 +08:00
jingchuan
a5c0a4f83d fix(sample-progress): preserve selected calendar dates 2026-08-13 14:37:36 +08:00
jingchuan
7371676f6f fix(sample-progress): restrict deletion and optional sample fields 2026-08-13 14:11:01 +08:00
WuYongJie
6acbc75a09 merge(main): release BOM responsive inspector and sample progress 2026-08-13 14:03:11 +08:00
WuYongJie
7b46654cb7 merge(sample-progress): preserve latest production flow 2026-08-13 14:01:36 +08:00
jingchuan
2ab42d68e1 fix(sample-progress): compact header and filter overdue items 2026-08-13 13:55:46 +08:00
WuYongJie
2199376f11 Merge branch 'codex/jingchuan-sample-progress-main' into main 2026-08-13 13:42:14 +08:00
WuYongJie
4192f27b17 Merge branch 'codex/xuxiake-mainline' into main 2026-08-13 13:40:10 +08:00
WuYongJie
ea834d8dea Merge branch 'codex/wuyongjie-bom-responsive-inspector' into codex/xuxiake-mainline 2026-08-13 13:39:34 +08:00
jingchuan
f34d938e2e fix(sample-progress): use fixed scrollable table workspace 2026-08-13 13:38:02 +08:00
WuYongJie
f6b28ecd63 fix(bom-costing): keep recalculation controls in view 2026-08-13 13:30:06 +08:00
jingchuan
ade0751701 fix(sample-progress): calculate dwell time and anomalies 2026-08-13 13:25:38 +08:00
WuYongJie
2ba8ab0f05 Merge commit '62017377' into codex/wuyongjie-promote-bom-optional-20260813 2026-08-13 12:38:47 +08:00
WuYongJie
62017377ad fix(bom): preserve resolved primary material 2026-08-13 12:36:47 +08:00
WuYongJie
2905c42455 Merge remote-tracking branch 'origin/codex/jingchuan-sample-progress-main' into codex/wuyongjie-promote-bom-optional-20260813 2026-08-13 12:15:27 +08:00
jingchuan
ce808f8c07 feat(sample-progress): allow no-sample status 2026-08-13 12:14:43 +08:00
WuYongJie
915450a748 Merge remote-tracking branch 'origin/codex/jingchuan-sample-progress-main' into codex/wuyongjie-promote-bom-optional-20260813 2026-08-13 12:11:31 +08:00
WuYongJie
adae1ac0f1 Merge remote-tracking branch 'origin/codex/wuyongjie-bom-category-skills-v7' into codex/wuyongjie-promote-bom-optional-20260813 2026-08-13 12:11:10 +08:00
jingchuan
db844e6126 fix(sample-progress): derive sample status from OA stage 2026-08-13 12:09:44 +08:00
WuYongJie
cdb807dcd5 Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/wuyongjie-bom-category-skills-v7 2026-08-13 12:08:42 +08:00
WuYongJie
97ade48cf8 fix(bom): preserve trusted slot geometry 2026-08-13 12:08:13 +08:00
WuYongJie
af97ea7074 merge: sync product archive operators 2026-08-13 12:02:14 +08:00
WuYongJie
138155b04a feat(supply-chain): sync product archive operators 2026-08-13 12:01:02 +08:00
WuYongJie
609e675659 merge: preserve latest sample progress stage alignment 2026-08-13 11:02:08 +08:00
WuYongJie
218a7d91a6 merge: preserve deployed sample progress updates 2026-08-13 11:00:32 +08:00
jingchuan
5318a30346 fix(sample-progress): align board with current OA stage 2026-08-13 10:57:13 +08:00
WuYongJie
c21b384050 merge: add storage rack and table v2 skills 2026-08-13 10:55:04 +08:00
WuYongJie
acd22bbffa feat(bom): add storage rack and table v2 skills 2026-08-13 10:54:26 +08:00
jingchuan
1433428cf7 fix(sample-progress): show current OA sample stage count 2026-08-13 10:38:22 +08:00
WuYongJie
5c05df5855 fix(bom): preserve sku material and module geometry 2026-08-13 09:56:21 +08:00
WuYongJie
35384031c6 Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/wuyongjie-bom-category-skills-v7 2026-08-13 09:28:12 +08:00
WuYongJie
fee8bdb0a8 fix(bom): harden category skill costing contracts 2026-08-13 09:27:59 +08:00
jingchuan
e2d739d1d4 fix(sample-progress): count only active managed samples 2026-08-13 09:08:33 +08:00
WuYongJie
f12b68ff56 Merge remote-tracking branch 'origin/codex/wuyongjie-bom-category-skills-v7' into codex/wuyongjie-promote-bom-optional-20260813 2026-08-13 06:32:43 +08:00
WuYongJie
869f0e6922 Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/wuyongjie-bom-category-skills-v7 2026-08-13 06:30:22 +08:00
WuYongJie
76421163c7 fix(bom-costing): enforce exclusive sku structure 2026-08-13 06:30:01 +08:00
WuYongJie
439b427259 Merge remote-tracking branch 'origin/codex/wuyongjie-bom-category-skills-v7' into codex/wuyongjie-promote-bom-optional-20260813 2026-08-13 04:49:02 +08:00
WuYongJie
6f8fc02b2f Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/wuyongjie-bom-category-skills-v7 2026-08-13 04:46:13 +08:00
WuYongJie
b12d1eeeb3 fix(bom-costing): preserve sku evidence and size scaling 2026-08-13 04:45:31 +08:00
WuYongJie
0c9491a370 merge: promote BOM optional SKU structure fix 2026-08-13 02:52:34 +08:00
WuYongJie
37430946d8 fix(bom-costing): isolate optional SKU structures 2026-08-13 02:51:12 +08:00
WuYongJie
eb0a6ad499 fix(bom-costing): restore layered category estimates 2026-08-13 01:18:19 +08:00
WuYongJie
de048075ef Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/wuyongjie-bom-category-skills-v7
# Conflicts:
#	furniture-3d-system/src/server/services/__tests__/intake-non-integrated-product.service.test.ts
#	furniture-3d-system/src/server/services/intake-non-integrated-product.service.ts
2026-08-12 21:06:18 +08:00
WuYongJie
c87266c2c7 fix(intake): bound virtual inventory processing 2026-08-12 20:51:03 +08:00
WuYongJie
a51ec8a162 fix(bom-costing): guard fact snapshot fingerprints 2026-08-12 20:49:07 +08:00
WuYongJie
14915ef1f2 fix(supply-chain): resolve new-product images by barcode 2026-08-12 18:10:08 +08:00
jingchuan
7a25001d6b fix(sample-progress): retain reached cancelled sample nodes 2026-08-12 17:48:52 +08:00
jingchuan
fd0354f1d0 fix(sample-progress): sync all OA flow statuses 2026-08-12 17:34:26 +08:00
WuYongJie
ae9ae0fcae Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/wu-product-development-barcode-print-20260812 2026-08-12 17:32:37 +08:00
WuYongJie
a36b5ede46 Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/wu-product-development-barcode-print-20260812 2026-08-12 17:29:44 +08:00
WuYongJie
e2a725ed58 feat(barcode-print): add barcode QR to Sofia carton label 2026-08-12 17:29:12 +08:00
WuYongJie
0b4f903325 fix(intake): require directory selections for suppliers and warehouses 2026-08-12 17:22:41 +08:00
jingchuan
8d7ab94cac fix(sample-progress): sync cancelled and terminated flows 2026-08-12 16:54:33 +08:00
jingchuan
27b16a9ce6 fix(sample-progress): refine board fields and unique codes 2026-08-12 16:20:53 +08:00
jingchuan
09dd4a1730 fix(sample-progress): normalize compact delivery dates 2026-08-12 16:04:59 +08:00
jingchuan
fc4763a8c9 fix(sample-progress): use configured prisma client for import 2026-08-12 16:03:17 +08:00
jingchuan
0b2bc3ce5a Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/jingchuan-sample-progress-main 2026-08-12 15:46:52 +08:00
jingchuan
73005fdd9b feat(sample-progress): import sampling data and completion time 2026-08-12 15:43:53 +08:00
jingchuan
c2e75dda3e fix(sample-progress): sort by OA arrival time 2026-08-12 14:54:15 +08:00
jingchuan
ac146f8879 Merge remote-tracking branch 'origin/main' into codex/jingchuan-sample-progress-main 2026-08-12 14:43:59 +08:00
jingchuan
4f1372668f fix(sample-progress): simplify summary and expand designer 2026-08-12 14:43:29 +08:00
WuYongJie
344e1b108a Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/xuxiake-installation-demo-integration-20260812 2026-08-12 14:25:56 +08:00
WuYongJie
a17844604c Merge commit '082d5cfc' into codex/xuxiake-installation-demo-integration-20260812 2026-08-12 14:24:23 +08:00
WuYongJie
082d5cfcee feat(installation-demo): complete sharing catalog and tool motion 2026-08-12 14:17:40 +08:00
WuYongJie
bdc8f393a8 fix(bom-costing): extend structure progress transaction 2026-08-12 14:16:31 +08:00
WuYongJie
76230f9f34 Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/wuyongjie-bom-category-skills-v7 2026-08-12 13:09:36 +08:00
WuYongJie
17dd830976 fix(bom-costing): harden category skill costing closure 2026-08-12 13:09:26 +08:00
WuYongJie
70d0e4e5d9 fix(supply-chain): move price type to data group 2026-08-12 11:24:06 +08:00
jingchuan
432cd9fc4f feat(sample-progress): enrich OA tracking board 2026-08-12 11:07:49 +08:00
WuYongJie
944b5b0a58 Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/wuyongjie-bom-category-skills-v7 2026-08-11 18:33:33 +08:00
WuYongJie
7040fd2d03 feat(bom-costing): add category skill engine v7 2026-08-11 18:33:04 +08:00
jingchuan
a040b015b5 fix(oa-board): align export default statuses 2026-08-11 18:08:32 +08:00
jingchuan
ad9afabc49 fix(sample-progress): exclude OA drafts from overdue metrics 2026-08-11 17:45:23 +08:00
jingchuan
2c201674a4 Merge remote-tracking branch 'origin/main' into codex/jingchuan-sample-progress-main 2026-08-11 17:36:57 +08:00
jingchuan
ba9adb0db4 fix(sample-progress): sync OA data and embed board tab 2026-08-11 17:36:57 +08:00
WuYongJie
7558939482 feat(supply-chain): sync supplier regions 2026-08-11 17:19:29 +08:00
WuYongJie
31ef235c80 fix(intake): keep opening name out of SKU specs 2026-08-11 17:02:31 +08:00
jingchuan
37851f0f04 Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/jingchuan-sample-progress-main 2026-08-11 16:32:03 +08:00
jingchuan
20a450c4bd feat(intake): reorder OA tabs and add sample flow links 2026-08-11 16:30:00 +08:00
WuYongJie
0ece1c38dc fix(supply-chain): populate warehouse config and pin archive actions 2026-08-11 16:14:36 +08:00
WuYongJie
179040b1ab Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/xuxiake-package-set-count-fix-20260811-1548 2026-08-11 16:12:21 +08:00
WuYongJie
6388b8f37a fix(intake): keep package set counts consistent 2026-08-11 16:05:00 +08:00
jingchuan
ac9deefe85 merge: release sample progress board 2026-08-11 16:02:00 +08:00
jingchuan
1fa15cef79 merge: add sample progress board 2026-08-11 16:00:59 +08:00
jingchuan
23c20714dd feat(sample-progress): integrate board into OA workspace 2026-08-11 16:00:17 +08:00
jingchuan
c031c36f8f feat(sample-progress): add sampling progress board module 2026-08-11 15:49:58 +08:00
WuYongJie
aa646a795a fix(supply-chain): isolate product archive column settings 2026-08-11 14:59:06 +08:00
jingchuan
6a19e30527 Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/jingchuan-brand-shipment-main-merge 2026-08-11 14:56:35 +08:00
jingchuan
43d8d43e16 Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/jingchuan-brand-shipment-main-merge 2026-08-11 14:56:23 +08:00
jingchuan
b2fb4165b5 Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/jingchuan-brand-shipment-todo 2026-08-11 14:54:22 +08:00
jingchuan
bdf20c2064 feat(oa-board): remind brand planning before shipment 2026-08-11 14:49:28 +08:00
WuYongJie
c86367c6b0 fix(intake): keep product type filters readable 2026-08-11 14:33:00 +08:00
jingchuan
4dcbed4c10 feat(oa-board): track unconfirmed completion followups 2026-08-11 13:46:32 +08:00
WuYongJie
199228241a fix(intake): save product tags and support all product types 2026-08-11 13:42:33 +08:00
WuYongJie
63ca46c030 fix(intake): save product tags and support all product types 2026-08-11 13:38:28 +08:00
WuYongJie
10ce1696dc fix(supply-chain): sync product development tags 2026-08-11 13:01:54 +08:00
WuYongJie
728495143e fix(supply-chain): sync product development tags 2026-08-11 12:52:30 +08:00
WuYongJie
20fee3b7ba merge: fix barcode certificate QR codes 2026-08-11 12:31:55 +08:00
WuYongJie
5c547dd25f fix(barcode-print): sync certificate QR codes 2026-08-11 12:31:52 +08:00
jingchuan
981df46e3f feat(oa-board): add monthly flow analysis 2026-08-11 12:06:51 +08:00
jingchuan
b24328f4f8 feat(oa-board): add daily stage report snapshots 2026-08-11 11:46:58 +08:00
WuYongJie
83a4d456b5 merge: release supply chain archive persistence 2026-08-11 10:46:55 +08:00
WuYongJie
be247f5a4b Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/tmp-supply-chain-mainline-20260811 2026-08-11 10:40:13 +08:00
WuYongJie
b47f745b83 feat(supply-chain): persist archive enrichment and image cache 2026-08-11 10:31:12 +08:00
jingchuan
35b9c5e080 feat(oa-board): read DingTalk source from JSReport replica 2026-08-11 10:17:18 +08:00
WuYongJie
759cba00d7 Merge remote-tracking branch 'origin/codex/wu-product-pricing-inventory-status' into codex/tmp-main-merge-product-pricing-20260811 2026-08-11 09:06:39 +08:00
WuYongJie
6858161f3f fix: show fixed virtual inventory task status 2026-08-11 08:58:37 +08:00
WuYongJie
7bfb69405d release: honor unitless internal dimensions 2026-08-11 05:59:32 +08:00
WuYongJie
814a7ed4be merge: honor unitless internal dimensions 2026-08-11 05:59:14 +08:00
WuYongJie
9383233130 fix(bom): honor unitless internal dimensions 2026-08-11 05:58:26 +08:00
WuYongJie
27ed15f55a release: isolate oversized board packing 2026-08-11 05:42:04 +08:00
WuYongJie
7b95e5af33 merge: isolate oversized board packing 2026-08-11 05:41:14 +08:00
WuYongJie
5bd554f762 fix(bom): isolate oversized board packing 2026-08-11 05:40:24 +08:00
WuYongJie
c1985a2396 release: area-scaled BOM fallback v6 2026-08-11 05:10:27 +08:00
WuYongJie
c538e175bc merge: area-scaled BOM fallback v6 2026-08-11 05:10:21 +08:00
WuYongJie
24a5043d15 fix(bom): scale fallback costs by board area 2026-08-11 05:10:02 +08:00
WuYongJie
0ba596a7c7 release: deterministic BOM blueprint v6 2026-08-11 04:39:10 +08:00
WuYongJie
bcf04b7438 merge: deterministic BOM blueprint v6 2026-08-11 04:38:52 +08:00
WuYongJie
d5e1f2af99 fix(bom): stabilize structure blueprint costing 2026-08-11 04:38:14 +08:00
WuYongJie
42095c6dab release: BOM tuple synthesis v6 2026-08-11 03:28:38 +08:00
WuYongJie
67ce313c9a merge: BOM tuple synthesis v6 2026-08-11 03:28:32 +08:00
WuYongJie
4f1571d508 fix(bom): compact sku synthesis into tuples 2026-08-11 03:27:15 +08:00
WuYongJie
871fd38bb5 release: BOM non-stream synthesis v6 2026-08-11 02:50:38 +08:00
WuYongJie
3c8d7f0801 merge: BOM non-stream synthesis v6 2026-08-11 02:50:17 +08:00
WuYongJie
7518041829 fix(bom-costing): avoid stalled synthesis streams 2026-08-11 02:49:47 +08:00
WuYongJie
164192bae7 release: BOM compact structure synthesis v6 2026-08-11 02:21:31 +08:00
WuYongJie
b9fd690e5d merge: BOM compact structure synthesis v6 2026-08-11 02:21:08 +08:00
WuYongJie
efbb08b7a5 fix(bom-costing): compact structure synthesis 2026-08-11 02:20:39 +08:00
WuYongJie
ebf0eca3b7 merge: publish complete BOM JSON stream handling 2026-08-11 01:25:16 +08:00
WuYongJie
5416bed96e merge: accept complete BOM JSON streams 2026-08-11 01:24:53 +08:00
WuYongJie
e945eff091 fix(bom-costing): accept completed json streams 2026-08-11 01:24:10 +08:00
WuYongJie
aefff98cad merge: publish BOM finish-reason stream fix 2026-08-11 00:38:32 +08:00
WuYongJie
be26f6d795 merge: finish completed BOM response streams 2026-08-11 00:38:25 +08:00
WuYongJie
f1a7b9c941 fix(bom-costing): finish completed response streams 2026-08-11 00:37:57 +08:00
WuYongJie
67a68067a0 merge: publish scoped BOM structure synthesis 2026-08-11 00:07:57 +08:00
WuYongJie
707feb213a merge: scope BOM structure synthesis requests 2026-08-11 00:06:57 +08:00
WuYongJie
2afdbb9ca7 fix(bom-costing): scope structure synthesis requests 2026-08-11 00:06:32 +08:00
WuYongJie
f2df3e53dc merge: publish BOM stream completion fix 2026-08-10 23:32:51 +08:00
WuYongJie
075fb38096 merge: fix completed BOM model streams 2026-08-10 23:32:14 +08:00
WuYongJie
dbf48330bd fix(bom-costing): stop completed model streams promptly 2026-08-10 23:27:39 +08:00
WuYongJie
7b220f824e merge: publish BOM structure blueprint v6 2026-08-10 22:16:31 +08:00
WuYongJie
e4554d86cf merge: BOM structure blueprint v6 2026-08-10 22:15:08 +08:00
WuYongJie
7a84b8fde0 feat(bom-costing): add v6 structure blueprint and batch cutting 2026-08-10 22:12:59 +08:00
jingchuan
fe48bbb181 feat(oa-board): add private daily report 2026-08-10 18:23:51 +08:00
WuYongJie
50317a3c0f merge: supply chain mainline into main 2026-08-10 17:28:10 +08:00
jingchuan
127730c5cf fix(oa-board): add manual termination overrides 2026-08-10 17:26:47 +08:00
WuYongJie
cbbf2fbb21 feat(supply-chain): map archive prices by barcode 2026-08-10 17:21:51 +08:00
WuYongJie
b9ec3511ac feat(supply-chain): add interactive archive dashboard 2026-08-10 16:25:16 +08:00
jingchuan
f55bddb017 fix(oa-board): honor manual termination overrides 2026-08-10 16:18:35 +08:00
jingchuan
9a1e588af3 fix(oa-board): terminate withdrawn XP2026070100000242 2026-08-10 16:17:11 +08:00
WuYongJie
c1ba901f7a feat(supply-chain): map warehouse attributes from JST 2026-08-10 16:05:13 +08:00
WuYongJie
cf74985e56 merge: publish BOM thickness evidence priority 2026-08-10 15:49:20 +08:00
WuYongJie
80c4dcc91b merge: release BOM thickness evidence priority 2026-08-10 15:48:48 +08:00
WuYongJie
c632402f2d Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/wuyongjie-bom-v5-runtime-fix 2026-08-10 15:44:46 +08:00
WuYongJie
2e3168bb99 fix(bom-costing): prefer product-wide explicit thickness facts 2026-08-10 15:42:16 +08:00
WuYongJie
d630a73433 fix(bom-costing): classify observed and estimated board thickness 2026-08-10 15:26:41 +08:00
WuYongJie
d90e6754b5 fix(bom-costing): bound history memory and price material substitutions 2026-08-10 14:54:09 +08:00
WuYongJie
cdf95640ae Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/wu-product-development-type-filter-main-release-20260810 2026-08-10 14:38:59 +08:00
WuYongJie
e7799331e0 Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/wu-product-development-type-filter 2026-08-10 14:37:50 +08:00
WuYongJie
dab6002b5c feat(intake): add product type filter 2026-08-10 14:37:34 +08:00
WuYongJie
b3d947fc16 Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/wuyongjie-release-main-bom-v5-20260810 2026-08-10 14:00:30 +08:00
WuYongJie
b0a9cd029f Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/wuyongjie-release-bom-v5-20260810 2026-08-10 13:57:52 +08:00
WuYongJie
a783e4c94b feat(supply-chain): map stock monitor daily sales 2026-08-10 13:55:58 +08:00
WuYongJie
2ca9d51339 Merge remote-tracking branch 'origin/codex/wuyongjie-bom-cost-v5-acceptance-fix' into codex/wuyongjie-release-bom-v5-20260810 2026-08-10 13:50:45 +08:00
WuYongJie
218941d97b fix(bom-costing): constrain history to product structure 2026-08-10 13:50:20 +08:00
WuYongJie
ff771b7bba feat(supply-chain): configure warehouse archive fields 2026-08-10 13:16:22 +08:00
WuYongJie
b010bd1355 merge: keep BOM estimates when synthesis is unavailable 2026-08-10 13:12:49 +08:00
WuYongJie
2dbe903c64 fix(bom-costing): price from saved facts on synthesis outage 2026-08-10 13:12:26 +08:00
WuYongJie
2f0c9a37e6 merge: improve BOM v5 production estimates 2026-08-10 12:59:54 +08:00
WuYongJie
22403f5a45 fix(bom-costing): select comparable whole BOM profiles 2026-08-10 12:59:27 +08:00
WuYongJie
d8a9be3d00 Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/wuyongjie-release-main-bom-v5-20260810
# Conflicts:
#	furniture-3d-system/src/server/services/__tests__/oa-board-sync.service.test.ts
#	furniture-3d-system/src/server/services/oa-board-sync.service.ts
2026-08-10 12:24:37 +08:00
WuYongJie
80179df9fc Merge remote-tracking branch 'origin/codex/wuyongjie-bom-cost-dynamic-20260807' into codex/wuyongjie-release-bom-v5-20260810 2026-08-10 12:18:10 +08:00
WuYongJie
c353757d6b Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/wuyongjie-bom-cost-dynamic-20260807 2026-08-10 12:14:30 +08:00
WuYongJie
9f368104b5 fix(oa-board): pass pending pricing stage to metrics 2026-08-10 12:14:11 +08:00
WuYongJie
d0b9b22879 feat(jushuitan): add fixed virtual inventory queue 2026-08-10 12:08:51 +08:00
WuYongJie
81b1453db5 Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/wuyongjie-bom-cost-dynamic-20260807 2026-08-10 12:08:06 +08:00
WuYongJie
d511fd9b9e feat(jushuitan): add fixed virtual inventory queue 2026-08-10 12:08:03 +08:00
WuYongJie
3ea61a2f07 feat(bom-costing): add layered v5 estimates 2026-08-10 12:08:01 +08:00
jingchuan
e869d32594 fix(oa-board): track pending pricing SLA 2026-08-10 11:45:10 +08:00
jingchuan
3c5c38e72b fix(oa-board): track pending pricing SLA 2026-08-10 11:34:35 +08:00
WuYongJie
8dda70ecdd Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/wuyongjie-bom-cost-dynamic-20260807 2026-08-10 11:04:17 +08:00
WuYongJie
b9a398830f perf(supply-chain): batch product archive image loading 2026-08-10 11:02:52 +08:00
WuYongJie
772c6813ed fix(bom-costing): restore detail slices and correct cost semantics 2026-08-10 10:43:00 +08:00
WuYongJie
ead86d523e feat(bom-costing): add auditable dynamic company cost engine 2026-08-09 14:23:45 +08:00
WuYongJie
a77c7a3296 merge: supply chain archive updates 2026-08-07 18:10:20 +08:00
WuYongJie
b87bb1ccb4 feat(supply-chain): add stock monitor fields 2026-08-07 18:09:56 +08:00
WuYongJie
c3f9ecbafd fix(supply-chain): backfill archive source images 2026-08-07 17:44:28 +08:00
WuYongJie
8b53afa468 fix(supply-chain): backfill archive source images 2026-08-07 17:43:34 +08:00
WuYongJie
28901d34b4 feat(supply-chain): enrich archive supplier profiles 2026-08-07 17:42:56 +08:00
WuYongJie
3c1983352a merge: release drawing and video archive media 2026-08-07 16:17:19 +08:00
WuYongJie
a69da95217 feat(supply-chain): import archive drawing and video media 2026-08-07 16:14:51 +08:00
WuYongJie
eaf7b9ecca feat(supply-chain): resolve archive images from material library 2026-08-07 15:56:36 +08:00
WuYongJie
e3047c8e6e feat(supply-chain): persist archive views and add multiselect filters 2026-08-07 14:38:18 +08:00
WuYongJie
e7f07a451c feat(supply-chain): add full JST archive sync 2026-08-07 13:02:22 +08:00
WuYongJie
b1566874e1 feat(supply-chain): add full JST archive sync 2026-08-07 13:01:47 +08:00
WuYongJie
2e87027672 fix(db): make JST archive source migration idempotent 2026-08-07 12:38:21 +08:00
WuYongJie
2a483ea415 fix(db): make JST archive source migration idempotent 2026-08-07 12:38:02 +08:00
WuYongJie
a1972a0103 merge: release supply chain product archive
# Conflicts:
#	furniture-3d-system/src/app/intake/oa-board/OaBoardClient.tsx
#	furniture-3d-system/src/server/services/__tests__/oa-board.service.test.ts
#	furniture-3d-system/src/server/services/oa-board.service.ts
2026-08-07 12:22:11 +08:00
WuYongJie
4b5affacd0 Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/tmp-supply-chain-archive-snapshot-20260804 2026-08-07 12:18:20 +08:00
WuYongJie
2bd31cc06b feat(supply-chain): complete product archive data pipeline 2026-08-07 12:17:47 +08:00
WuYongJie
1910c2581e fix(intake): expose CJ0267 installation demo 2026-08-07 11:55:13 +08:00
WuYongJie
501f561eae fix(intake): expose CJ0267 installation demo 2026-08-07 11:53:32 +08:00
jingchuan
16a70d2920 feat(oa-board): track production stage overtime 2026-08-06 18:44:01 +08:00
jingchuan
ddb153f0d7 fix(oa-board): track pending first-order SLA 2026-08-06 17:39:54 +08:00
jingchuan
41251e2223 fix(oa-board): separate overall and stage overtime 2026-08-06 17:02:04 +08:00
WuYongJie
f7885fa6ec fix(work-record): prepend newly added records
(cherry picked from commit 8a999c29dcc9d07202c6b685f58c8fb386ed04a7)
2026-08-06 16:31:18 +08:00
WuYongJie
8a999c29dc fix(work-record): prepend newly added records 2026-08-06 16:29:28 +08:00
jingchuan
f047d6b418 fix(oa-board): allow partial timing metrics 2026-08-06 15:55:04 +08:00
jingchuan
7914b07996 fix(oa-board): unify stage overtime followups 2026-08-06 15:45:23 +08:00
WuYongJie
5b0f98264c fix(deploy): align intake sku schema before release 2026-08-06 15:17:20 +08:00
WuYongJie
929b0eacd3 merge: 索菲亚合格证/箱贴模板修复 2026-08-06 14:33:43 +08:00
WuYongJie
4d24976ab9 fix(barcode-print): 索菲亚合格证/箱贴模板修复 + 顾家布局微调
索菲亚相关:
- barcodePrintSofiaCartonLayout 新增索菲亚箱贴布局
- barcodePrintCertificate 证书渲染逻辑调整
- BarcodePrintCertificateEditDialog 编辑弹窗重构

顾家相关:
- barcodePrintGujiaCertificateLayout 布局微调

纯前端/打印逻辑,不涉及数据库结构,可安全部署。
2026-08-06 14:33:24 +08:00
WuYongJie
a62ed2b6b7 merge: sync latest xuxiake mainline 2026-08-06 14:16:52 +08:00
WuYongJie
2ccb52e75b fix(intake): restore structured sku sales status normalization 2026-08-06 13:54:37 +08:00
WuYongJie
dc236236be docs: document product archive wide view contract 2026-08-06 13:21:41 +08:00
WuYongJie
3ba508e8bb docs: document product archive wide view contract 2026-08-06 13:20:47 +08:00
jingchuan
100d2d05fa fix(oa-board): keep total overtime separate from stage sla 2026-08-06 13:06:09 +08:00
WuYongJie
21572d3377 fix: calibrate Sofia carton certificate print layout 2026-08-06 12:58:37 +08:00
WuYongJie
09d0e169bc feat(supply-chain): unify archive source with product catalog wide table 2026-08-06 12:40:00 +08:00
WuYongJie
373aa0a753 feat(supply-chain): unify archive source with product catalog wide table 2026-08-06 12:29:36 +08:00
WuYongJie
ef1ad7999c feat(supply-chain): update product archive column headers 2026-08-06 12:11:17 +08:00
jingchuan
f60dbfa159 fix(oa-board): correct duration and sku flow matching 2026-08-06 10:53:56 +08:00
jingchuan
4efb7f4083 fix(oa-board): prefer active reassigned tasks 2026-08-06 09:59:50 +08:00
jingchuan
d3280f8fc5 fix(oa-board): refresh brand planning snapshot with timer 2026-08-06 09:39:36 +08:00
WuYongJie
c8b8fccc5b Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/release-bom-mincost-main-20260805 2026-08-05 21:07:34 +08:00
WuYongJie
1f001701a6 Merge branch 'codex/wuyongjie-bom-min-cost-20260805' into codex/release-bom-mincost-xuxiake-20260805 2026-08-05 21:06:58 +08:00
WuYongJie
db262e18db feat(bom): cross-check sibling sku facts 2026-08-05 21:04:13 +08:00
WuYongJie
2239d35564 fix(bom): estimate costs from saved sku facts 2026-08-05 20:56:48 +08:00
WuYongJie
5ea48578ec Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/wuyongjie-bom-main-release-20260805 2026-08-05 20:05:50 +08:00
WuYongJie
5a2438595e Merge branch 'codex/wuyongjie-bom-recalc-rootfix-20260805' into codex/xuxiake-mainline 2026-08-05 20:04:55 +08:00
WuYongJie
87a7e75d21 fix(bom): assemble exact sku structures from image facts 2026-08-05 19:56:34 +08:00
WuYongJie
d41ee6a61a fix(bom): bound gpt synthesis by real sku 2026-08-05 19:26:10 +08:00
jingchuan
25f4354823 fix(oa-board): use stage SLA for overtime 2026-08-05 17:53:15 +08:00
jingchuan
317eaa8655 fix(oa-board): allow historical overtime followup editing 2026-08-05 17:53:15 +08:00
jingchuan
d531a62361 fix(oa-board): retain historical overtime followups 2026-08-05 17:53:15 +08:00
WuYongJie
2a01b4929b Merge remote-tracking branch 'origin/main' into codex/wuyongjie-bom-recalc-rootfix-20260805 2026-08-05 16:31:02 +08:00
WuYongJie
cc01261d96 Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/wuyongjie-bom-recalc-rootfix-20260805 2026-08-05 16:28:53 +08:00
WuYongJie
6fbc44f0d1 fix(bom): tolerate partial image analysis failures 2026-08-05 16:27:07 +08:00
jingchuan
495a639c8d fix(oa-board): expose historical overtime followups 2026-08-05 16:23:44 +08:00
jingchuan
8e5c6f91ba fix(oa-board): expose historical overtime followups 2026-08-05 16:22:58 +08:00
WuYongJie
c08f5661b4 merge: sync management workspace access 2026-08-05 16:19:49 +08:00
WuYongJie
656a1d1d9e merge: add management workspace access 2026-08-05 16:19:01 +08:00
WuYongJie
8f14eebe15 feat: add management workspace access 2026-08-05 16:16:33 +08:00
WuYongJie
82b478a1fe fix(bom): clarify actual sku recalculation scope 2026-08-05 15:42:13 +08:00
WuYongJie
d6214a2c40 fix(bom): recover malformed image facts and clarify recalc 2026-08-05 15:39:12 +08:00
WuYongJie
2c13dd0312 Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/release-bom-live-stream-main-20260805 2026-08-05 14:25:35 +08:00
WuYongJie
06665326b2 Merge branch 'codex/wuyongjie-bom-live-stream-workbench-clean-20260805' into codex/release-bom-live-stream-xuxiake-20260805 2026-08-05 14:23:59 +08:00
WuYongJie
bc7be2e2ad feat(bom): surface live image analysis results 2026-08-05 14:19:38 +08:00
WuYongJie
52e8d8baef merge: release OpenSSH deploy key support to production main 2026-08-05 13:31:36 +08:00
WuYongJie
d12ecc74f2 merge: bound identity-key deployment connections 2026-08-05 13:31:14 +08:00
WuYongJie
5e0f34ecb7 fix(deploy): bound OpenSSH identity connections 2026-08-05 13:31:13 +08:00
WuYongJie
e2c22b260f merge: promote OpenSSH deploy key support to shared mainline 2026-08-05 13:27:13 +08:00
WuYongJie
fc74d265cd fix(deploy): support OpenSSH identity files 2026-08-05 13:26:49 +08:00
WuYongJie
8350e9f5fc merge: release product progress completion fix 2026-08-05 12:53:51 +08:00
WuYongJie
024be4307a merge: complete product progress after SKU entry 2026-08-05 12:52:55 +08:00
WuYongJie
a863754ae6 fix(product-progress): complete historical entered SPUs 2026-08-05 12:51:57 +08:00
WuYongJie
cd4ec9a23a feat(supply-chain): add product archive dashboard 2026-08-05 12:45:28 +08:00
WuYongJie
e5b97d7750 feat(supply-chain): add product archive dashboard 2026-08-05 12:43:51 +08:00
WuYongJie
f6460b1eda feat(supply-chain): add product archive dashboard 2026-08-05 12:43:13 +08:00
WuYongJie
c5ddcb428c merge: release BOM quality safeguards to production main 2026-08-05 11:45:39 +08:00
WuYongJie
68914fceec merge: promote BOM quality safeguards to shared mainline 2026-08-05 11:45:11 +08:00
WuYongJie
3d18a89e55 fix(bom): guard historical and invalid costing results 2026-08-05 11:42:28 +08:00
WuYongJie
f1dc44376a merge: retain current production BOM safeguards 2026-08-05 11:19:42 +08:00
WuYongJie
02c869cf70 Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/wu-product-development-main-merge-20260805 2026-08-05 11:04:43 +08:00
WuYongJie
8b9474262c Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/wu-product-development-release-20260805
# Conflicts:
#	furniture-3d-system/src/app/operation-logs/OperationLogClient.tsx
2026-08-05 11:02:16 +08:00
WuYongJie
f456c56d57 feat(intake): consolidate product development progress 2026-08-05 11:00:42 +08:00
WuYongJie
9a713e9a98 Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/xuxiake-supply-chain-archive-release-20260804 2026-08-05 10:50:57 +08:00
WuYongJie
1a90b2e7b3 Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/xuxiake-supply-chain-archive-release-20260804 2026-08-05 10:50:29 +08:00
WuYongJie
bc573566eb feat(operation-logs): separate workspace administrator views 2026-08-05 10:50:09 +08:00
WuYongJie
3cbbe8eb3d merge: release bounded BOM worker execution 2026-08-05 10:49:46 +08:00
WuYongJie
3136604a53 merge: bound BOM worker execution time 2026-08-05 10:49:17 +08:00
WuYongJie
d17d308be1 merge: sync shared mainline before BOM timeout fix 2026-08-05 10:47:55 +08:00
WuYongJie
115a8b8042 fix(bom): bound worker and model execution time 2026-08-05 10:47:49 +08:00
jingchuan
8cd5764428 fix(oa-board): allow historical overtime followup editing 2026-08-05 10:45:36 +08:00
WuYongJie
5b537b8f9c feat(intake): consolidate product development progress 2026-08-05 10:42:54 +08:00
WuYongJie
af596560b6 merge: skip unused onnx cuda runtime 2026-08-05 09:59:18 +08:00
WuYongJie
adaa1ed8ac fix(deploy): skip unused onnx cuda runtime 2026-08-05 09:58:43 +08:00
WuYongJie
9636cc9733 merge: release real BOM SKU verification 2026-08-05 09:51:45 +08:00
WuYongJie
86290edf6a merge: preserve real BOM SKU rows 2026-08-05 09:51:20 +08:00
WuYongJie
26063e1d16 fix(bom): verify real sku rows before costing 2026-08-05 09:50:42 +08:00
WuYongJie
461e10a3ab merge: release supply chain product archive 2026-08-05 09:06:03 +08:00
WuYongJie
2cb7a35c26 Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/xuxiake-supply-chain-archive-release-20260804
# Conflicts:
#	scripts/deploy-yydd-prod-from-git.ps1
2026-08-05 09:02:28 +08:00
WuYongJie
368fc1953a feat(supply-chain): integrate product archive pipeline 2026-08-05 09:00:49 +08:00
WuYongJie
3d382146f4 merge: release batched BOM result persistence 2026-08-05 03:07:44 +08:00
WuYongJie
03cc085514 merge: batch BOM result persistence 2026-08-05 03:07:33 +08:00
WuYongJie
1e41401ec3 fix(bom): batch large costing result persistence 2026-08-05 03:07:18 +08:00
WuYongJie
78fe9d6ded merge: release BOM tabletop area costing 2026-08-05 02:57:04 +08:00
WuYongJie
0d82963ffa merge: fix BOM tabletop area costing 2026-08-05 02:56:50 +08:00
WuYongJie
8c56a3a164 fix(bom): use sku dimensions for tabletop area costing 2026-08-05 02:56:32 +08:00
WuYongJie
5972bf4db4 merge: recover transient BOM upstream bad requests 2026-08-05 02:13:42 +08:00
WuYongJie
eaef521816 merge: recover transient BOM upstream bad requests 2026-08-05 02:13:31 +08:00
WuYongJie
faf313b940 fix(bom-costing): recover transient upstream bad requests 2026-08-05 02:13:18 +08:00
WuYongJie
dccc745d51 merge: auto-recover empty BOM model responses 2026-08-05 01:50:32 +08:00
WuYongJie
97b9359235 merge: auto-recover empty BOM model responses 2026-08-05 01:50:21 +08:00
WuYongJie
00b8070699 fix(bom-costing): auto-recover empty model responses 2026-08-05 01:49:55 +08:00
WuYongJie
76e60f2465 merge: reset BOM recovery budget after progress 2026-08-05 01:01:41 +08:00
WuYongJie
d5da0a0390 merge: reset BOM recovery budget after progress 2026-08-05 01:01:20 +08:00
WuYongJie
131e83ce77 fix(bom-costing): reset recovery budget after progress 2026-08-05 01:00:07 +08:00
WuYongJie
71a8d06c97 merge: default omitted BOM model confidence 2026-08-05 00:28:47 +08:00
WuYongJie
c6feb67947 merge: default omitted BOM model confidence 2026-08-05 00:28:41 +08:00
WuYongJie
96a3abda1d fix(bom-costing): default omitted model confidence conservatively 2026-08-05 00:28:24 +08:00
WuYongJie
300753f56c merge: auto-resume malformed BOM model responses 2026-08-05 00:21:51 +08:00
WuYongJie
09743ffc3f merge: auto-resume malformed BOM model responses 2026-08-05 00:21:46 +08:00
WuYongJie
0e20ab41c5 fix(bom-costing): auto-resume malformed model responses 2026-08-05 00:21:26 +08:00
WuYongJie
4ea787faf1 merge: auto-resume transient BOM model failures 2026-08-05 00:07:44 +08:00
WuYongJie
74ec257d96 merge: auto-resume transient BOM model failures 2026-08-05 00:07:31 +08:00
WuYongJie
e5e5f62504 fix(bom-costing): auto-resume transient model failures 2026-08-05 00:05:35 +08:00
WuYongJie
29b6992314 Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/release-bom-rearchive-main-20260804 2026-08-04 23:45:44 +08:00
WuYongJie
0a3bd1969c Merge branch 'codex/wuyongjie-bom-rearchive-batch-20260804' into codex/release-bom-rearchive-xuxiake-20260804 2026-08-04 23:45:26 +08:00
WuYongJie
ad43342687 Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/wuyongjie-bom-rearchive-batch-20260804 2026-08-04 23:45:12 +08:00
WuYongJie
22b02adda9 fix(bom-costing): split network timeouts immediately 2026-08-04 23:44:58 +08:00
WuYongJie
68d59842c5 Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/release-bom-rearchive-main-20260804 2026-08-04 22:47:35 +08:00
WuYongJie
c1e14b23a8 Merge branch 'codex/wuyongjie-bom-rearchive-batch-20260804' into codex/release-bom-rearchive-xuxiake-20260804 2026-08-04 22:47:22 +08:00
WuYongJie
86f0e56739 fix(bom-costing): checkpoint completed synthesis batches 2026-08-04 22:46:49 +08:00
WuYongJie
4dae247b96 Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/release-bom-rearchive-main-20260804 2026-08-04 22:04:25 +08:00
WuYongJie
f881fdda30 Merge branch 'codex/wuyongjie-bom-rearchive-batch-20260804' into codex/release-bom-rearchive-xuxiake-20260804 2026-08-04 22:04:07 +08:00
WuYongJie
86702e95ff Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/wuyongjie-bom-rearchive-batch-20260804 2026-08-04 22:03:54 +08:00
WuYongJie
6e296c6c61 fix(bom-costing): time out stalled response streams 2026-08-04 22:03:28 +08:00
WuYongJie
9c9c57ba5c merge: release immediate BOM timeout splitting 2026-08-04 21:37:52 +08:00
WuYongJie
2420cec963 merge: split timed out BOM synthesis immediately 2026-08-04 21:37:34 +08:00
WuYongJie
0f9caa09ca merge: synchronize compact BOM synthesis release 2026-08-04 21:37:13 +08:00
WuYongJie
56988d753d fix(bom-costing): split timed out synthesis immediately 2026-08-04 21:36:55 +08:00
WuYongJie
146b3992ab merge: release compact BOM synthesis facts 2026-08-04 21:23:12 +08:00
WuYongJie
6ac92f6274 merge: compact BOM synthesis facts 2026-08-04 21:22:55 +08:00
WuYongJie
e0ca7775a4 merge: synchronize stalled BOM recovery release 2026-08-04 21:22:39 +08:00
WuYongJie
634a2dad9d perf(bom-costing): compact product synthesis facts 2026-08-04 21:22:19 +08:00
WuYongJie
3b16f69efe merge: release stalled BOM synthesis batch recovery 2026-08-04 21:00:07 +08:00
WuYongJie
a61a10ffb9 merge: split stalled BOM synthesis batches 2026-08-04 20:59:49 +08:00
WuYongJie
b32fc52427 merge: synchronize BOM retry safeguards 2026-08-04 20:59:31 +08:00
WuYongJie
acf3dc8fc2 fix(bom-costing): split stalled synthesis batches 2026-08-04 20:59:00 +08:00
WuYongJie
89db71a377 merge: release BOM synthesis retry safeguards 2026-08-04 20:30:56 +08:00
WuYongJie
71558003a7 merge: retry incomplete BOM synthesis batches 2026-08-04 20:30:35 +08:00
WuYongJie
7211a2fc78 fix(bom-costing): retry incomplete synthesis batches 2026-08-04 20:30:07 +08:00
WuYongJie
eccbc1bc68 merge: release scoped BOM synthesis facts 2026-08-04 20:14:34 +08:00
WuYongJie
f6a54980df merge: scope BOM synthesis facts by SKU batch 2026-08-04 20:14:17 +08:00
WuYongJie
b23d0b44f6 Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/wuyongjie-bom-rearchive-batch-20260804 2026-08-04 20:14:02 +08:00
WuYongJie
99c6833b0c fix(bom-costing): scope synthesis facts to SKU batch 2026-08-04 20:13:48 +08:00
WuYongJie
c12194f333 merge: release bounded BOM synthesis reasoning 2026-08-04 20:05:02 +08:00
WuYongJie
179ec64a8c merge: bound BOM synthesis reasoning 2026-08-04 20:04:46 +08:00
WuYongJie
07cdfbb20c Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/wuyongjie-bom-rearchive-batch-20260804 2026-08-04 20:04:25 +08:00
WuYongJie
b6b4edeaf5 fix(bom-costing): bound GPT synthesis reasoning 2026-08-04 20:04:04 +08:00
WuYongJie
0b0e014d5e merge: release hardened BOM SKU batch processing 2026-08-04 19:49:29 +08:00
WuYongJie
10a23cb5b8 merge: harden BOM SKU batch processing 2026-08-04 19:49:12 +08:00
WuYongJie
d5b73444e5 fix(bom-costing): tolerate unknown measures in sku batches 2026-08-04 19:48:22 +08:00
WuYongJie
2e51bad245 Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/release-bom-rearchive-main-20260804 2026-08-04 19:16:17 +08:00
WuYongJie
041b60a563 Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/wuyongjie-bom-rearchive-batch-20260804 2026-08-04 19:15:15 +08:00
WuYongJie
143aa7fa3f fix(bom-costing): bound concurrent sku synthesis 2026-08-04 19:14:41 +08:00
WuYongJie
1b6e743dd0 merge: release 600 SKU BOM catalog support 2026-08-04 18:44:25 +08:00
WuYongJie
4841ca549b merge: support complete 600 SKU BOM catalogs 2026-08-04 18:43:59 +08:00
WuYongJie
f0687f2d5d fix(bom-costing): support complete 600 sku catalogs 2026-08-04 18:43:33 +08:00
WuYongJie
6ba383076a merge: release BOM synthesis progress serialization 2026-08-04 18:34:01 +08:00
WuYongJie
d2b123066b merge: serialize BOM synthesis progress events 2026-08-04 18:33:35 +08:00
WuYongJie
47f19da2e2 fix(bom-costing): serialize synthesis progress events 2026-08-04 18:32:48 +08:00
WuYongJie
b192e0359a merge: release BOM source recovery and batch synthesis 2026-08-04 18:23:45 +08:00
WuYongJie
a11aa15017 merge: recover BOM changed sources and batch synthesis 2026-08-04 18:22:35 +08:00
WuYongJie
45a501406b fix(bom-costing): recover changed sources and batch sku synthesis 2026-08-04 18:21:44 +08:00
WuYongJie
26906c0585 feat(supply-chain): snapshot product archive workspace 2026-08-04 18:20:12 +08:00
WuYongJie
899ec8b9f4 feat(supply-chain): snapshot product archive workspace 2026-08-04 18:19:44 +08:00
WuYongJie
ab2b64c0d4 merge: release BOM SKU intake repair 2026-08-04 17:49:56 +08:00
WuYongJie
6371d9f248 merge: harden BOM SKU intake and hints 2026-08-04 17:49:20 +08:00
WuYongJie
a78900ce64 fix(bom-costing): harden SKU intake and simplify hints 2026-08-04 17:48:48 +08:00
WuYongJie
71cd22a12a Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/release-bom-costing-promote-main-20260804 2026-08-04 17:14:40 +08:00
WuYongJie
c4628fb8c4 Merge remote-tracking branch 'origin/codex/release-bom-costing-main-20260804' into codex/release-bom-costing-xuxiake-20260804 2026-08-04 17:13:45 +08:00
WuYongJie
a7c371073b Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/release-bom-costing-xuxiake-20260804 2026-08-04 17:13:44 +08:00
WuYongJie
2899a1bcd4 fix(bom-costing): bound synthesis workload 2026-08-04 17:13:03 +08:00
WuYongJie
37cb0fd876 Merge remote-tracking branch 'origin/main' into codex/release-bom-costing-main-20260804 2026-08-04 16:53:10 +08:00
WuYongJie
a115a4a73c fix(bom-costing): retry transient synthesis failures 2026-08-04 16:52:40 +08:00
WuYongJie
8f8e03734c merge: guard intake basic info before factory confirmation 2026-08-04 16:45:07 +08:00
WuYongJie
747a201196 fix(intake): guard basic info confirmation before factory 2026-08-04 16:44:30 +08:00
WuYongJie
b3fd61dadb fix(bom-costing): bound GPT-5 reasoning time 2026-08-04 16:43:59 +08:00
WuYongJie
608da97538 Merge remote-tracking branch 'origin/main' into codex/release-bom-costing-main-20260804 2026-08-04 16:24:36 +08:00
WuYongJie
a36d7ab1a8 fix(bom-costing): recover interrupted worker jobs 2026-08-04 16:24:31 +08:00
jingchuan
6187aecab2 Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/main-oa-history-followup-release 2026-08-04 15:40:51 +08:00
jingchuan
02d3345763 fix(oa-board): retain historical overtime followups 2026-08-04 15:40:12 +08:00
WuYongJie
532e4724da merge: synchronize BOM synthesis resilience 2026-08-04 15:35:15 +08:00
WuYongJie
81f3c10ca3 merge: extend BOM synthesis resilience 2026-08-04 15:34:54 +08:00
WuYongJie
e5a5662303 fix(bom-costing): extend synthesis resilience 2026-08-04 15:34:30 +08:00
WuYongJie
16b22dcd40 merge: release BOM costing recovery 2026-08-04 15:13:48 +08:00
WuYongJie
e6580c9b26 merge: release BOM costing recovery 2026-08-04 15:13:08 +08:00
WuYongJie
425e5e444d fix(bom-costing): preserve viable cost estimates 2026-08-04 14:43:29 +08:00
jingchuan
3553211fc5 merge: block duplicate todo recovery 2026-08-03 17:52:08 +08:00
jingchuan
5848dcd71f merge: block duplicate todo recovery 2026-08-03 17:51:29 +08:00
jingchuan
5c370b3f57 fix(oa-board): block duplicate todo recovery 2026-08-03 17:51:05 +08:00
jingchuan
699fb65e10 merge: retain DingTalk todo ID on update 2026-08-03 17:40:48 +08:00
jingchuan
b80792d753 merge: retain DingTalk todo ID on update 2026-08-03 17:40:34 +08:00
jingchuan
6d92aa52ae fix(oa-board): retain todo id after dingtalk update 2026-08-03 17:40:17 +08:00
jingchuan
59e44086b1 merge: schedule OA overtime todo dispatch 2026-08-03 17:29:35 +08:00
jingchuan
a2040410e8 merge: enable scheduled OA overtime todo dispatch 2026-08-03 17:28:45 +08:00
jingchuan
5d01a3b2fd fix(oa-board): run overdue todo scan after snapshot sync 2026-08-03 17:26:56 +08:00
WuYongJie
9882dede2c merge: keep one intake workspace tab per page 2026-08-03 17:03:09 +08:00
WuYongJie
59dfb2a1b1 fix: keep one tab per intake workspace page 2026-08-03 17:01:47 +08:00
WuYongJie
e5f603ca8e merge: portal permissions and external barcode scope 2026-08-03 16:35:54 +08:00
WuYongJie
29c70537fb Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/xuxiake-supply-chain-permissions-20260803 2026-08-03 16:30:19 +08:00
WuYongJie
c58f642574 fix: scope external barcode print records 2026-08-03 16:28:40 +08:00
WuYongJie
076075a3a8 fix: stabilize intake and supply-chain tab navigation 2026-08-03 16:28:39 +08:00
WuYongJie
7acd668918 feat: move product permission controls to user management 2026-08-03 16:28:26 +08:00
WuYongJie
a50f81eaed feat: add portal and workspace access guards 2026-08-03 16:28:16 +08:00
WuYongJie
19c3409b09 Merge remote-tracking branch 'origin/main' into codex/wuyongjie-bom-unified-progress-20260803 2026-08-03 16:21:56 +08:00
WuYongJie
5a40b422f9 fix(bom-costing): 自动形成预估成本并精简工作台 2026-08-03 16:21:14 +08:00
WuYongJie
546cea38d9 fix: 对齐顾家合格证2026模板 2026-08-03 13:53:37 +08:00
WuYongJie
e3da1628ce fix: 删除首页门户return后的死代码(UTF8安全), 修复TS构建错误 2026-08-03 13:06:56 +08:00
WuYongJie
fee09e5116 fix: 删除首页门户 return 后的死代码,修复 TS 构建错误 2026-08-03 12:59:08 +08:00
WuYongJie
0bf9003485 fix: HomeClient editingProduct 可能为 null 的 TS 构建错误 2026-08-03 12:50:52 +08:00
WuYongJie
97c73a4e0c merge: 首页门户化 + 供应链中心框架 2026-08-03 12:43:57 +08:00
WuYongJie
51e44d1eaf feat: 首页门户化 + 供应链中心框架
首页改门户选择页:
- HomeClient.workspace 加门户短路,渲染 HomePortal(两个工作台入口)
- HomePortal:产品开发(→/intake) + 供应链中心(→/supply-chain) 两大入口卡
- 顶部导航:首页时只显示「首页」,进入产品开发后恢复全部功能导航
- 入口图标:产品开发用 Package,供应链用 Truck
- 时间问候语(早午晚好+用户名)

供应链中心(src/app/supply-chain/):
- 独立布局:窄边栏(76px) + hover二级菜单浮层 + 多标签页 + 顶栏搜索
- 复用主项目顶栏(保留首页导航),供应链路由时品牌切换为「由壹点「供应链中心」」
- 全局菜单搜索:模糊匹配 + 拼音全拼/首字母 + 键盘导航(↑↓Enter Esc)
- 4个页面:工作台(问候语)、产品全量档案、日常事项(空壳)、产品变价(空壳)
- 产品全量档案:多级表头(5分组81字段) + 列设置抽屉(勾选显隐+拖拽排序+重置) + localStorage持久化 + 浮动分页

视觉:白曜石规范(浅色精密、克制通透、石墨灰强调色)
基于 origin/main,不含其他线程改动
2026-08-03 12:39:02 +08:00
WuYongJie
5904e96494 merge: 发布 BOM 并发分析与进度可视化修复 2026-08-03 12:15:40 +08:00
WuYongJie
62d9578e1b merge: 合入 BOM 并发分析与进度可视化修复 2026-08-03 12:13:07 +08:00
WuYongJie
4f7acbdafa fix(bom-costing): 修复多模态结果解析并展示并发进度 2026-08-03 12:11:43 +08:00
WuYongJie
668fa3c219 merge: 发布 BOM 多模态事实传输契约修复 2026-08-03 09:25:04 +08:00
WuYongJie
a0f701f24d merge: 合入 BOM 多模态事实传输契约修复 2026-08-03 09:22:34 +08:00
WuYongJie
ecb6549341 fix(bom-costing): 拆分多模态事实传输契约 2026-08-02 01:03:10 +08:00
WuYongJie
dc46e09888 merge: 发布 BOM 多模态事实类型契约修复 2026-08-02 00:17:46 +08:00
WuYongJie
2261b3f663 merge: 合入 BOM 多模态事实类型契约修复 2026-08-02 00:16:55 +08:00
WuYongJie
45b5cb05ba fix(bom-costing): 统一多模态事实类型契约 2026-08-02 00:15:27 +08:00
WuYongJie
229a65b8aa merge: 发布 BOM 多模态输出契约根因修复 2026-08-01 23:24:34 +08:00
WuYongJie
9faa24fe9c merge: 合入 BOM 多模态输出契约根因修复 2026-08-01 23:23:38 +08:00
WuYongJie
62c89a81c8 fix(bom-costing): 重构多模态输出契约边界 2026-08-01 23:11:14 +08:00
WuYongJie
2e891c9a95 feat(bom-costing): 使用 GSAP 重构核算动效 2026-08-01 21:57:03 +08:00
WuYongJie
70a2d8eb55 fix(bom-costing): 修正多模态事实结果契约 2026-08-01 19:59:57 +08:00
WuYongJie
d9ebf7d9cb Merge remote-tracking branch 'origin/main' into codex/wuyongjie-bom-failure-root-20260801 2026-08-01 18:38:11 +08:00
WuYongJie
665151aa22 fix(bom-costing): 修复多模态核算失败链路 2026-08-01 18:32:49 +08:00
jingchuan
3289809a95 feat(oa-board): split current and historical timeout analysis 2026-08-01 09:22:22 +08:00
jingchuan
d9672a9231 Merge branch 'codex/xuxiake-mainline' of http://git.1080p.hk/WuYongJie/furniture-3d-system-Wu into codex/xuxiake-mainline-todo-unionid-20260729 2026-07-31 18:08:29 +08:00
jingchuan
c88e63978c fix(oa-board): retain historical timeout followups 2026-07-31 18:07:35 +08:00
WuYongJie
939810a702 fix(barcode-print): restore Gujia certificate field alignment 2026-07-31 17:36:18 +08:00
WuYongJie
d9052de583 fix(barcode-print): restore Gujia certificate field alignment 2026-07-31 17:34:57 +08:00
jingchuan
19ce18fa0b fix(oa-board): align stage overtime followups 2026-07-31 15:44:02 +08:00
jingchuan
adc97204ff fix(oa-board): fall back to current stage SLA alerts 2026-07-31 13:11:08 +08:00
WuYongJie
d3915c4cd3 merge: 同步徐霞客主线最新 OA 修复 2026-07-31 12:04:31 +08:00
jingchuan
781d92ea71 Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/xuxiake-mainline-todo-unionid-20260729 2026-07-31 12:00:13 +08:00
jingchuan
f4150e8e2c fix(oa-board): disable unconfirmed sample courier SLA 2026-07-31 11:59:47 +08:00
WuYongJie
9233a9013d merge: 同步最新生产主线后发布 BOM 改动 2026-07-31 11:40:21 +08:00
WuYongJie
4cc6a63267 merge: barcode print database pagination 2026-07-31 11:37:25 +08:00
WuYongJie
c598da5ef8 fix(barcode-print): page records at database 2026-07-31 11:36:37 +08:00
WuYongJie
e7de41ce8d merge: 同步最新徐霞客主线 2026-07-31 11:34:28 +08:00
WuYongJie
2d3d5f2ef0 feat(bom-costing): 按业务类型开放并留档人工核算 2026-07-31 11:33:31 +08:00
jingchuan
46a7f25619 Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/xuxiake-mainline-todo-unionid-20260729 2026-07-31 11:24:49 +08:00
jingchuan
64738ea4d6 fix(oa-board): unify current-node overtime alerts 2026-07-31 11:23:15 +08:00
WuYongJie
1d4f176337 merge: align Gujia certificate fields 2026-07-31 10:32:37 +08:00
WuYongJie
8d04698637 fix(intake): align Gujia certificate fields 2026-07-31 10:29:10 +08:00
WuYongJie
59b960d5ce merge: release barcode pagination and supplier brand SKU visibility 2026-07-30 23:37:06 +08:00
WuYongJie
783e7038b0 fix(barcode-print): page imports and expose supplier brand SKUs 2026-07-30 23:36:14 +08:00
WuYongJie
0264b4c63c merge: 发布 BOM 全量图片事实分析 2026-07-30 19:08:20 +08:00
WuYongJie
d9a7ac208a merge: BOM 全量图片事实分析 2026-07-30 19:05:35 +08:00
WuYongJie
ed424431a7 feat(bom-costing): 持久化全量图片事实分析 2026-07-30 19:01:56 +08:00
WuYongJie
e0cad57074 merge: barcode print append import and supplier scope 2026-07-30 17:28:09 +08:00
WuYongJie
d68f49620d fix(barcode-print): append imports and scope suppliers by name 2026-07-30 17:14:49 +08:00
WuYongJie
6af723ac40 feat(barcode-print): 条码打印后端真分页 + 供应商隐藏空字段
后端分页:
- BarcodePrintService 新增 listRowsPaged,take/skip 只查当前页 + count 取总数
- 筛选条件下沉到后端 where(货号/供应商查独立列,条码/型号等查 updates JSONB)
- GET /api/intake/barcode-print 接收 page/pageSize/筛选参数

前端真分页:
- 从全量拉取+内存切片 改为 带参数请求后端+只存当前页
- 翻页/搜索/重置/导入/删除 均触发后端重新请求
- 本地实测:打开 18.5s -> 0.4s

供应商隐藏空字段:
- tableConfig 新增供应商可见列集合 + getVisibleBarcodePrintColumns
- BarcodePrintTable 加 isSupplier prop,供应商隐藏 24 个始终为空的内部字段
- 验证:管理员仍看全部 46 列,供应商看 20 列

注意:本提交的 listRowsPaged 依赖 normalizedSupplierName 列
(schema.prisma + 迁移 20260730150000,由另一线程改动,未含在本提交)。
单独部署会因缺列报 500,需与另一线程的 schema/迁移一并合并。
2026-07-30 17:04:53 +08:00
WuYongJie
400bdaf1ef merge: release AI render catalog scope 2026-07-30 10:32:27 +08:00
WuYongJie
3a0f7079fc merge: scope AI render catalog to product development 2026-07-30 10:30:54 +08:00
WuYongJie
0a2f6b971b fix(ai-render): scope catalog to product development 2026-07-30 10:28:45 +08:00
WuYongJie
fc61e25d73 merge: release BOM costing cost truth 2026-07-29 20:02:49 +08:00
WuYongJie
10409ae69d Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/wuyongjie-bom-cost-truth-20260729 2026-07-29 19:56:50 +08:00
WuYongJie
91a5ce30ce fix(bom-costing): align material and packaging cost truth 2026-07-29 19:56:27 +08:00
jingchuan
a0bf3a21c7 Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/xuxiake-mainline-todo-unionid-20260729 2026-07-29 18:32:09 +08:00
jingchuan
2c178b5b49 fix(oa-board): use union ids for dingtalk todos 2026-07-29 18:31:29 +08:00
WuYongJie
03585c7872 merge: release intake AI render asset linking 2026-07-29 17:54:30 +08:00
WuYongJie
f8fe78e2cb merge: link intake products with AI render assets 2026-07-29 17:42:56 +08:00
WuYongJie
449eef2a19 feat(ai-render): link intake models and generated assets 2026-07-29 17:41:04 +08:00
jingchuan
e9d60543b0 fix(oa-board): dedupe dingtalk owner identities 2026-07-29 17:18:26 +08:00
jingchuan
e53d5555f6 fix(oa-board): dedupe dingtalk owner identities 2026-07-29 17:04:12 +08:00
WuYongJie
c3036152f2 merge: restrict barcode import permissions 2026-07-29 16:59:13 +08:00
WuYongJie
2697fb9f1d merge: restrict barcode import permissions 2026-07-29 16:57:56 +08:00
WuYongJie
d435d1f535 fix: restrict barcode import management permissions 2026-07-29 16:56:34 +08:00
WuYongJie
49fde0f5c0 Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/xuxiake-main-work-record-score-deploy-20260729 2026-07-29 16:01:48 +08:00
WuYongJie
c1151b6af5 Merge remote-tracking branch 'origin/codex/xuxiake-work-record-score-management-20260729' into codex/xuxiake-mainline-work-record-score-submit-20260729 2026-07-29 16:00:48 +08:00
WuYongJie
0259d685d2 Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/xuxiake-work-record-score-management-20260729 2026-07-29 15:59:53 +08:00
WuYongJie
679e726a14 feat(work-record): manage score rules 2026-07-29 15:59:41 +08:00
WuYongJie
03430313b5 merge: 同步 BOM 核算商品决策布局 2026-07-29 15:07:36 +08:00
WuYongJie
ec98f2b33c merge: 发布 BOM 核算商品决策布局 2026-07-29 15:07:08 +08:00
WuYongJie
0c9c2fccc8 feat(bom-costing): 重构竞品核算结果工作台 2026-07-29 15:06:16 +08:00
jingchuan
60f252571a Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/xuxiake-mainline-submit-20260704 2026-07-29 13:49:38 +08:00
WuYongJie
eafa63f962 Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/wuyongjie-restore-prod-ssh-main-20260729 2026-07-29 13:47:29 +08:00
jingchuan
c1f1c319c6 Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/xuxiake-mainline-submit-20260704 2026-07-29 13:47:14 +08:00
WuYongJie
f496d8aac9 Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/wuyongjie-restore-prod-ssh-20260729 2026-07-29 13:45:57 +08:00
WuYongJie
27789d37e8 revert(deploy): 恢复生产 SSH 直连入口 2026-07-29 13:45:50 +08:00
jingchuan
9ed6399c32 feat(oa-board): scaffold dingtalk todo dispatch 2026-07-29 13:45:41 +08:00
WuYongJie
a85c41383d merge: release square mobile nine-view previews 2026-07-29 13:39:15 +08:00
WuYongJie
654df39b1e fix(intake): square mobile nine-view previews 2026-07-29 13:38:30 +08:00
WuYongJie
8f1cfa68ce merge: release BOM streaming and workspace layout 2026-07-29 13:02:34 +08:00
WuYongJie
b7227f3ddc Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/wuyongjie-bom-524-layout-20260729 2026-07-29 13:00:37 +08:00
WuYongJie
2d866aa5e4 fix(bom-costing): 修复长请求并压缩图片工作区 2026-07-29 13:00:20 +08:00
WuYongJie
04aa9b3004 merge: release mobile intake detail adaptation 2026-07-29 12:26:55 +08:00
WuYongJie
97f7018c03 fix(intake): adapt product detail links for mobile 2026-07-29 12:26:18 +08:00
WuYongJie
b18418ff40 merge: sync production management jump path 2026-07-29 11:44:36 +08:00
WuYongJie
bdc8cdab8c merge: 生产管理跳板通道 2026-07-29 11:44:12 +08:00
WuYongJie
8d92e137cb ops(deploy): 切换生产管理跳板通道 2026-07-29 11:43:47 +08:00
WuYongJie
c437177609 merge: sync OA category normalization 2026-07-29 11:16:27 +08:00
WuYongJie
e6a4fdf21a fix(intake): normalize OA residential categories 2026-07-29 11:14:27 +08:00
WuYongJie
5dedc6ed72 merge: 同步 BOM SKU 尺寸契约修复 2026-07-29 11:10:53 +08:00
WuYongJie
e41966d1fa merge: BOM SKU 尺寸契约修复 2026-07-29 11:10:08 +08:00
WuYongJie
bfdefd20d3 fix(bom-costing): 统一 SKU 尺寸输出契约 2026-07-29 11:07:10 +08:00
WuYongJie
7aa0e67063 merge: sync top navigation layout 2026-07-29 10:56:48 +08:00
WuYongJie
a8dff278e0 fix: keep workbench navigation on one line 2026-07-29 10:48:15 +08:00
WuYongJie
11ad175bae merge: 同步 BOM 核算过程可视化 2026-07-29 10:36:12 +08:00
WuYongJie
30989452a9 merge: BOM 核算过程可视化 2026-07-29 10:35:20 +08:00
WuYongJie
b541acca8e feat(bom-costing): 增强核算过程可视化 2026-07-29 10:34:07 +08:00
WuYongJie
5f5a8f2036 merge: 同步 BOM 核算历史续跑 2026-07-28 21:13:17 +08:00
WuYongJie
7c632aba6f merge: BOM 核算历史续跑 2026-07-28 21:12:42 +08:00
WuYongJie
80b65bc7f3 fix(bom-costing): 收敛模型输入并支持历史续跑 2026-07-28 21:11:57 +08:00
WuYongJie
2093f809c7 merge: installation demo share preview parity 2026-07-28 19:03:25 +08:00
WuYongJie
155a6579aa fix: align installation demo share preview runtime 2026-07-28 18:59:14 +08:00
WuYongJie
3a7fbe81fb merge: 同步 BOM 核算异步工作台 2026-07-28 18:52:48 +08:00
WuYongJie
cab376d91c merge: BOM 核算异步工作台 2026-07-28 18:52:11 +08:00
WuYongJie
999969a78d Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/wuyongjie-bom-async-workbench 2026-07-28 18:50:59 +08:00
WuYongJie
156bff395d feat(bom-costing): 增加异步核算与资源缓存 2026-07-28 18:50:47 +08:00
WuYongJie
7f201fe08e merge: supplier barcode-only access 2026-07-28 17:39:50 +08:00
WuYongJie
7f0561df35 merge: supplier barcode-only access 2026-07-28 17:39:17 +08:00
WuYongJie
10be42a9c8 fix(auth): restrict supplier accounts to barcode printing 2026-07-28 17:38:23 +08:00
WuYongJie
63562013af Merge branch 'codex/xuxiake-mainline' into main 2026-07-28 17:01:33 +08:00
WuYongJie
6e6e0f5e4b Merge branch 'codex/wuyongjie-bom-real-provider' into codex/xuxiake-mainline 2026-07-28 17:01:01 +08:00
WuYongJie
5311fc2d51 Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/wuyongjie-bom-real-provider 2026-07-28 16:56:04 +08:00
WuYongJie
61a0c9e301 feat(bom-costing): 接入竞品详情与多模态核算 2026-07-28 16:55:59 +08:00
WuYongJie
175b5b7bb5 fix(intake): add barcode print page jump 2026-07-28 16:26:09 +08:00
jingchuan
c5000a5950 Merge branch 'codex/xuxiake-oa-flow-status-20260728' into HEAD 2026-07-28 16:21:33 +08:00
jingchuan
1b6ea9dcbe fix(oa-board): finalize component development flows 2026-07-28 16:20:36 +08:00
WuYongJie
78f5e8c570 fix(intake): wrap long Gujia barcode suffixes 2026-07-28 15:56:39 +08:00
WuYongJie
a423bddc63 merge: sync main before release notes deployment 2026-07-28 11:32:16 +08:00
WuYongJie
c927c831c5 feat: add release notes and sku warehouse batch initialization 2026-07-28 11:29:32 +08:00
WuYongJie
b2cf27ef68 merge: apply shared display refinements 2026-07-28 11:22:05 +08:00
WuYongJie
be9cdb8ed8 fix(installation-demo): refine shared display stages 2026-07-28 11:20:13 +08:00
WuYongJie
a36521f07c merge: apply carried-group screw playback fix 2026-07-28 10:52:47 +08:00
WuYongJie
7695bacfe2 fix(installation-demo): skip screw drive for carried groups 2026-07-28 10:49:08 +08:00
WuYongJie
84510b2f57 merge: 同步 AI 配置模型列表与共享主线 2026-07-28 10:17:56 +08:00
WuYongJie
e3d37c4391 merge: AI 配置读取可用模型列表 2026-07-28 10:17:34 +08:00
WuYongJie
edab790d0d Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/wuyongjie-ai-model-selector 2026-07-28 10:13:50 +08:00
WuYongJie
fd76a06942 fix(ai-config): 从服务端读取可用模型列表 2026-07-28 10:13:37 +08:00
WuYongJie
89acc2cd4b merge: publish installation demo playback refinements 2026-07-28 09:59:43 +08:00
WuYongJie
494bd0b04b merge: installation demo playback refinements 2026-07-28 09:56:36 +08:00
WuYongJie
b4e6bb865c feat(installation-demo): refine authored playback guidance 2026-07-28 09:55:52 +08:00
WuYongJie
22d54be408 merge: improve work record editing workflow 2026-07-27 14:37:57 +08:00
WuYongJie
117abf7df0 fix(work-record): improve table editing workflow 2026-07-27 14:37:20 +08:00
WuYongJie
0c73cdddc2 merge: save installation snapshots under production CSP 2026-07-27 14:20:13 +08:00
WuYongJie
2773368df1 merge: save installation snapshots under production CSP 2026-07-27 14:19:45 +08:00
WuYongJie
f30c1aa17f fix(installation-demo): save snapshots under production CSP 2026-07-27 14:19:14 +08:00
WuYongJie
fa9c4738e0 merge: release installation demo snapshot persistence 2026-07-27 12:00:25 +08:00
WuYongJie
4a9e8460c0 merge: persist installation demo snapshots 2026-07-27 11:59:48 +08:00
WuYongJie
cea36979d9 fix(installation-demo): persist numbering snapshots 2026-07-27 11:59:26 +08:00
WuYongJie
eb69c3473e merge: release installation demo tool loading fix 2026-07-27 10:53:20 +08:00
WuYongJie
d6e3be000b fix(installation-demo): stabilize tool model loading 2026-07-27 10:51:50 +08:00
WuYongJie
951f62832a merge: 同步徐霞客主线协作规则 2026-07-27 10:32:12 +08:00
WuYongJie
7e2f20bba9 merge: 回流主逻辑优先协作规则 2026-07-27 10:32:05 +08:00
WuYongJie
675237e16d docs(agents): 强化主逻辑优先规则 2026-07-27 10:31:43 +08:00
WuYongJie
f6f6ea4493 Merge remote-tracking branch 'origin/main' into codex/wuyongjie-bom-costing-ui-polish-20260723 2026-07-27 10:23:31 +08:00
WuYongJie
d48a905077 Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/wuyongjie-bom-costing-ui-polish-20260723 2026-07-27 10:23:26 +08:00
WuYongJie
e909cbdc94 refactor(bom-costing): 收紧空态工作台布局 2026-07-27 10:22:59 +08:00
WuYongJie
4e59043761 fix(installation-demo): recover screw-drive tool attachments 2026-07-27 09:00:45 +08:00
WuYongJie
8752d7c5ef merge: 同步主线后发布安装演示工作台
# Conflicts:
#	furniture-3d-system/prisma/schema.prisma
#	furniture-3d-system/src/app/installation-demo-workbench/InstallationDemoWorkbenchClient.tsx
#	furniture-3d-system/src/app/installation-demo-workbench/components/InstallationDetailsPanel.tsx
#	furniture-3d-system/src/app/installation-demo-workbench/components/InstallationInspector.tsx
#	furniture-3d-system/src/app/installation-demo-workbench/components/InstallationPartsInventoryBoard.tsx
#	furniture-3d-system/src/app/installation-demo-workbench/components/InstallationPreviewStage.tsx
#	furniture-3d-system/src/app/installation-demo-workbench/components/InstallationToolsInventoryBoard.tsx
#	furniture-3d-system/src/app/installation-demo-workbench/components/PartCatalogSection.tsx
#	furniture-3d-system/src/app/installation-demo-workbench/hooks/useInstallationLocalTransformPreview.ts
#	furniture-3d-system/src/app/installation-demo-workbench/hooks/useInstallationPreviewController.ts
#	furniture-3d-system/src/app/installation-demo-workbench/lib/installationNodeMaterialStorage.ts
#	furniture-3d-system/src/app/installation-demo-workbench/lib/installationNumberingSnapshotStorage.ts
2026-07-24 18:05:06 +08:00
WuYongJie
43ed761fae test: 补充安装步骤选择回归覆盖 2026-07-24 17:58:39 +08:00
WuYongJie
fc22c24fa0 feat: 完善安装演示工作台工具链 2026-07-24 17:58:02 +08:00
WuYongJie
ddd0556030 merge: release work record column resize 2026-07-24 17:36:17 +08:00
WuYongJie
de2c1e1011 fix(work-record): add resizable table columns 2026-07-24 17:35:49 +08:00
WuYongJie
2e43240f51 merge: release installation demo workbench 2026-07-24 15:45:51 +08:00
WuYongJie
bc6b5249c4 chore(installation-demo): fix source whitespace 2026-07-24 15:39:21 +08:00
WuYongJie
1973664e0a chore(installation-demo): normalize source line endings 2026-07-24 15:38:26 +08:00
WuYongJie
4025cf6538 merge: retain installation demo playback refinements 2026-07-24 15:32:33 +08:00
WuYongJie
a33b249e28 feat(installation-demo): refine playback and shared preview 2026-07-24 15:32:22 +08:00
WuYongJie
a8e48929ea feat(installation-demo): add toolbox authoring support 2026-07-24 15:30:39 +08:00
jingchuan
adc7fbd7b8 Merge branch 'codex/xuxiake-mainline-submit-20260704' into HEAD 2026-07-24 14:51:03 +08:00
jingchuan
2869c9a27c fix(oa-board): enforce brand planning field sources 2026-07-24 14:48:52 +08:00
WuYongJie
9ed61a8a14 fix(work-record): normalize underscore import labels 2026-07-24 14:03:59 +08:00
WuYongJie
67a0efc0a5 fix(work-record): normalize imported labels and dates 2026-07-24 13:17:16 +08:00
WuYongJie
63e3ab1504 fix(intake): 直连报表库获取供应商目录 2026-07-24 10:54:23 +08:00
WuYongJie
cb0f69d6c0 fix(work-record): preserve workspace tab labels 2026-07-24 09:30:01 +08:00
WuYongJie
32df0f8c84 feat(work-record): add designer work record workspace 2026-07-23 20:36:09 +08:00
WuYongJie
baea745394 merge: release BOM costing workbench 2026-07-23 14:38:42 +08:00
WuYongJie
026fab35c9 merge: add BOM costing workbench 2026-07-23 14:38:05 +08:00
WuYongJie
fabc7ebdb4 feat(bom-costing): add competitor costing workbench 2026-07-23 14:37:12 +08:00
WuYongJie
b24fcf26ca feat(operation-logs): record intake detail events 2026-07-23 10:20:35 +08:00
jingchuan
018feadda3 merge: scope OA brand planning board to new workflows 2026-07-22 18:48:54 +08:00
jingchuan
b1c8ef2e48 fix(oa-board): scope brand planning rows to new workflows 2026-07-22 18:48:21 +08:00
jingchuan
b5506df5d1 merge: add OA brand planning board 2026-07-22 18:32:10 +08:00
jingchuan
3150fea488 feat(oa-board): add brand planning board snapshot 2026-07-22 18:18:57 +08:00
WuYongJie
735a0edd1c merge: add operation-log admin navigation 2026-07-22 16:33:40 +08:00
WuYongJie
615b0013f4 fix(operation-logs): add admin navigation entry 2026-07-22 16:32:34 +08:00
WuYongJie
0a85348fbd fix(jushuitan): map package count to package type 2026-07-22 15:38:24 +08:00
WuYongJie
79618eb594 merge: release external intake visibility restriction 2026-07-22 13:43:07 +08:00
WuYongJie
36f3f6e910 merge: restrict external intake record visibility 2026-07-22 13:41:01 +08:00
WuYongJie
058272ecbf fix(intake): hide external entry records from internal users 2026-07-22 13:39:14 +08:00
WuYongJie
2c15734c11 merge: admin operation monitoring into main 2026-07-22 12:31:31 +08:00
WuYongJie
b870272eb5 merge: admin operation monitoring into mainline 2026-07-22 12:30:33 +08:00
WuYongJie
e18773d3aa feat(operation-logs): add admin operation monitoring 2026-07-22 12:29:58 +08:00
WuYongJie
ab954f3505 feat(external): 结构化产品定厂字段并扩展宽表 2026-07-22 11:58:54 +08:00
WuYongJie
53ded89075 feat(jushuitan): retry virtual inventory push 2026-07-22 10:44:47 +08:00
WuYongJie
28ea27f119 fix(intake): unify purchase owner authority 2026-07-21 20:42:54 +08:00
WuYongJie
5e722abde4 fix: OA预填改为系统已有值优先,OA只在系统没值时才填 2026-07-21 15:06:05 +08:00
WuYongJie
f4edd3e780 fix: 仓库模糊匹配——输入不完整仓库名自动纠正 2026-07-21 09:24:28 +08:00
WuYongJie
c18b54a92c fix: 非保留类目一级分类统一改为住宅家具 2026-07-20 14:56:51 +08:00
WuYongJie
7bd796b04a fix: 更新favicon.ico为新logo 2026-07-20 13:16:35 +08:00
WuYongJie
fe2c7f8249 Merge branch 'main' of http://git.1080p.hk/WuYongJie/furniture-3d-system-Wu 2026-07-20 13:07:28 +08:00
WuYongJie
edb1f30451 fix: 更新logo为最新版本 2026-07-20 13:06:44 +08:00
jingchuan
7a9ec16300 merge: measure first order from material completion 2026-07-18 14:36:50 +08:00
jingchuan
1c38d05b1d fix(oa-board): measure first order from material completion 2026-07-18 14:36:09 +08:00
jingchuan
df55db0bc7 merge: include OA board or-sign timing fix 2026-07-18 13:26:27 +08:00
jingchuan
e1781983aa fix(oa-board): exclude canceled or-sign tasks from timing 2026-07-18 12:06:04 +08:00
WuYongJie
a1228df9ea Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/wuyongjie-main-merge-oa-board-20260718 2026-07-18 11:45:19 +08:00
WuYongJie
3b18d3beab Merge remote-tracking branch 'origin/codex/xuxiake-mainline-submit-20260704' into codex/wuyongjie-release-oa-board-20260718 2026-07-18 11:28:45 +08:00
jingchuan
e8d2eb7509 fix(oa-board): parse source timestamps as china time 2026-07-18 10:35:33 +08:00
jingchuan
a35ad4776a fix(oa-board): sum repeated node durations 2026-07-18 10:10:22 +08:00
jingchuan
b0a7ec9591 fix(oa-board): wrap timeout followup exports 2026-07-17 18:28:02 +08:00
jingchuan
bb3445fdd1 fix(oa-board): honor canceled returned tasks 2026-07-17 17:14:54 +08:00
jingchuan
83b12fc11f fix(oa-board): backfill export creators 2026-07-17 15:48:13 +08:00
jingchuan
2cfa2eede0 fix(oa-board): resolve current task owners 2026-07-17 15:41:47 +08:00
jingchuan
1edfaec702 chore(oa-board): add scheduled snapshot sync 2026-07-17 14:32:55 +08:00
jingchuan
03f87e2dcb fix(oa-board): save all edited timeout stages 2026-07-17 13:57:41 +08:00
jingchuan
12b454505e fix(oa-board): preserve detail tab when filtering 2026-07-17 13:34:47 +08:00
WuYongJie
1c9bd11d20 Merge remote-tracking branch 'origin/codex/xuxiake-mainline-submit-20260704' 2026-07-17 13:04:45 +08:00
jingchuan
ebc261a730 feat(oa-board): restrict timeout followup editing 2026-07-17 12:30:19 +08:00
jingchuan
ea82b2c66f fix(oa-board): exclude exact threshold from overtime 2026-07-17 12:14:24 +08:00
jingchuan
a0e9913311 fix(oa-board): allow empty followup due date 2026-07-17 11:36:52 +08:00
WuYongJie
1cfe54ca17 Merge remote-tracking branch 'origin/codex/xuxiake-mainline-submit-20260704' into codex/non-integrated-detail-split 2026-07-17 10:58:59 +08:00
jingchuan
9115c519e1 fix(oa-board): make timeout action plan optional 2026-07-17 10:40:18 +08:00
WuYongJie
fdcfbae436 Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/non-integrated-detail-split 2026-07-17 10:37:14 +08:00
WuYongJie
c673fa18e3 refactor: 拆分产品资料详情组件 2026-07-17 10:36:29 +08:00
WuYongJie
f6b045d5ae Merge remote-tracking branch 'origin/codex/xuxiake-mainline-submit-20260704' 2026-07-17 10:10:39 +08:00
jingchuan
081e1f6cc4 feat(oa-board): filter detail by current approver 2026-07-17 09:55:05 +08:00
WuYongJie
cf0fc7c6ec feat: 权限判定增加成功日志(write allowed),方便排查 2026-07-17 09:08:35 +08:00
WuYongJie
8c9c662aeb fix: 更新浏览器页签logo 2026-07-16 15:18:37 +08:00
jingchuan
204ad40e76 perf(oa-board): make sidebar tab switches instant 2026-07-16 09:08:48 +08:00
WuYongJie
34077ca512 Merge branch 'main' of http://git.1080p.hk/WuYongJie/furniture-3d-system-Wu 2026-07-15 18:51:05 +08:00
WuYongJie
329d793705 fix: 厨房储物架/置物架一级类目改为住宅家具 2026-07-15 18:48:29 +08:00
WuYongJie
ad80419fae fix: send jst combine sku childList payload 2026-07-15 18:20:14 +08:00
WuYongJie
393893c295 Merge remote-tracking branch 'origin/codex/xuxiake-mainline-submit-20260704' 2026-07-15 18:11:45 +08:00
jingchuan
0eece49dc0 fix(oa-board): map pricing timeout owners correctly 2026-07-15 18:00:13 +08:00
jingchuan
cb2466c243 feat(oa-board): add timeout analysis and followup workflow 2026-07-15 17:56:48 +08:00
WuYongJie
e0fbc8c988 fix: use dedicated jst combine sku credentials 2026-07-15 17:48:47 +08:00
WuYongJie
f8e9d6855e fix: 权限拦截彻底修复——通用分支去硬拦截+权限判定日志+管理员豁免存量商品 2026-07-15 16:37:45 +08:00
WuYongJie
5e6fc49bb7 fix: 去掉保存时非当前节点的硬拦截,改为静默忽略 2026-07-15 15:46:16 +08:00
WuYongJie
f3e7d93aa3 fix: scope non-integrated product factory saves 2026-07-15 15:05:16 +08:00
WuYongJie
1b4ea170aa fix: scope product factory autosaves 2026-07-15 14:06:29 +08:00
WuYongJie
ff4950b69e fix: scope product factory download saves 2026-07-15 13:37:27 +08:00
WuYongJie
c1215a4617 chore(storage): use current qiniu resumable options 2026-07-14 17:00:02 +08:00
WuYongJie
9ae3dad2d0 fix(storage): use resumable upload for large documents 2026-07-14 16:51:06 +08:00
WuYongJie
299d737da8 fix(intake): harden large document uploads 2026-07-14 16:36:35 +08:00
WuYongJie
694230e30c fix(intake): stream large attachment uploads 2026-07-14 14:44:06 +08:00
WuYongJie
5825c52ad3 fix(intake): raise attachment upload limit to 1.5GB 2026-07-14 14:33:48 +08:00
WuYongJie
bb7f4260cf fix(intake): restore legacy barcode preview scaling 2026-07-14 10:38:22 +08:00
WuYongJie
5d13d2550b feat: add customer service business permissions 2026-07-13 16:02:59 +08:00
WuYongJie
ccfe925b5d feat: improve installation demo mobile share 2026-07-13 15:19:09 +08:00
WuYongJie
e1c4d5dc33 fix(users): classify strategy and general business roles 2026-07-13 15:07:36 +08:00
WuYongJie
5426a3782c fix(intake): add missing node permission targets 2026-07-13 13:53:49 +08:00
WuYongJie
386efdbeb2 feat: align sample review sheet template export 2026-07-13 13:36:12 +08:00
WuYongJie
5bf8f64d15 fix(users): append missing operations roles 2026-07-13 13:28:18 +08:00
WuYongJie
0c72819a12 feat(users): add operations business type 2026-07-13 13:08:18 +08:00
WuYongJie
23cf041ee7 merge: promote external intake confirm fix to main 2026-07-13 11:59:59 +08:00
WuYongJie
36763ca629 fix(intake): scope external confirm saves 2026-07-13 11:59:14 +08:00
WuYongJie
f2af9582af merge: promote intake updates to main 2026-07-13 11:11:20 +08:00
WuYongJie
5ab182a61e Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/tmp-intake-date-filter-20260713-105421 2026-07-13 10:57:37 +08:00
WuYongJie
f3eadddcb3 fix(intake): add product development created date filter 2026-07-13 10:57:01 +08:00
WuYongJie
89da950c07 fix(intake): save pricing workspace explicitly 2026-07-13 10:56:58 +08:00
WuYongJie
bae1563a98 fix(intake): enable suofeiya brand filing package download 2026-07-13 10:51:47 +08:00
WuYongJie
6172ead37c Merge remote-tracking branch 'origin/codex/xuxiake-mainline' 2026-07-13 10:10:57 +08:00
WuYongJie
c1bbdee0b9 refactor: 删除废弃的 buildApprovalNumber/padApprovalNumberPart 2026-07-13 10:09:59 +08:00
WuYongJie
df257bde69 Merge remote-tracking branch 'origin/codex/xuxiake-mainline' 2026-07-12 18:31:55 +08:00
WuYongJie
975ecee94a fix(intake): 确认节点先保存再打勾,SKU未入库提示修正 2026-07-12 09:35:48 +08:00
WuYongJie
35e08a6151 merge: promote brand filing updates to main 2026-07-10 18:24:25 +08:00
WuYongJie
bfcc1bdc39 feat(intake): 完善品牌报备模板流程 2026-07-10 18:10:43 +08:00
WuYongJie
5d8e136f12 merge: promote long-running branch to main 2026-07-10 18:07:56 +08:00
WuYongJie
11ea3e168b fix: restrict external intake owner selections 2026-07-10 18:01:48 +08:00
WuYongJie
a789124f10 feat: refine installation demo numbering preview 2026-07-10 16:49:30 +08:00
WuYongJie
c29e25b880 merge: 合并长期分支到 main 2026-07-10 14:42:28 +08:00
WuYongJie
6c407f208b Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into local/xuxiake-mainline-continue 2026-07-10 14:29:14 +08:00
WuYongJie
9c755282ce merge: 同步长期分支并保留品牌报备更新 2026-07-10 14:27:43 +08:00
WuYongJie
a866e0dffe fix: use public origin for installation demo shares 2026-07-10 14:24:07 +08:00
WuYongJie
d0af5d85dc fix: use public origin for installation demo shares 2026-07-10 14:23:20 +08:00
WuYongJie
be49070962 feat(intake): 完善品牌报备模板与已报备流程 2026-07-10 14:22:30 +08:00
WuYongJie
3346804952 fix(intake): scope factory saves to active sku batch 2026-07-10 13:57:36 +08:00
WuYongJie
58471baaf6 Merge remote-tracking branch 'origin/codex/wuyongjie-intake-factory-save-scope-20260710' into codex/tmp-promote-intake-factory-save-scope-20260710 2026-07-10 13:48:57 +08:00
WuYongJie
72b4581f77 fix(intake): scope product factory saves by target sku 2026-07-10 13:47:39 +08:00
WuYongJie
11be446db2 merge: retain latest main before installation demo build fix 2026-07-10 11:58:52 +08:00
WuYongJie
f035863350 fix(installation-demo): resolve playback focus type checks 2026-07-10 11:56:51 +08:00
WuYongJie
3e67badaf9 fix(installation-demo): resolve playback focus type checks 2026-07-10 11:55:14 +08:00
WuYongJie
c9405db975 merge: promote sku-scoped product factory permissions 2026-07-10 11:51:48 +08:00
WuYongJie
3c97d2b774 fix(intake): scope product factory writes by sku owner 2026-07-10 11:42:24 +08:00
WuYongJie
f178e9cceb fix(intake): scope product factory writes by sku owner 2026-07-10 11:41:24 +08:00
WuYongJie
5734fca012 merge: promote installation demo workbench to main 2026-07-10 11:25:10 +08:00
WuYongJie
d37bfcdde1 feat: add installation demo workbench 2026-07-10 11:16:26 +08:00
WuYongJie
8cb9252115 feat: add installation demo workbench 2026-07-10 11:02:00 +08:00
WuYongJie
fcc94ee37e fix(intake): tighten brand filing entry rules 2026-07-09 18:36:55 +08:00
WuYongJie
a98caca397 fix(intake): tighten brand filing entry rules 2026-07-09 18:35:59 +08:00
WuYongJie
3c78dbf994 feat(intake): add gujia brand filing templates 2026-07-09 17:43:46 +08:00
WuYongJie
05fe0cc783 feat(intake): add gujia brand filing templates 2026-07-09 17:40:54 +08:00
WuYongJie
149870943d feat(intake): add brand filing workspace and planning export 2026-07-09 13:56:23 +08:00
WuYongJie
cf44f1c371 feat(intake): add brand filing workspace and planning export 2026-07-09 13:55:52 +08:00
WuYongJie
29fe5aaeb2 fix: allow purchasing downloads for product data 2026-07-08 16:45:11 +08:00
WuYongJie
5f5dc2ead7 fix: allow purchasing downloads for product data 2026-07-08 16:38:36 +08:00
WuYongJie
071021fe54 fix: default intake sku sales status to 销售 2026-07-08 15:55:45 +08:00
WuYongJie
4982b37864 fix: default intake sku sales status to 销售 2026-07-08 15:45:49 +08:00
WuYongJie
30b1c5740d fix(intake): classify owner dropdown options 2026-07-07 14:08:01 +08:00
WuYongJie
37b2e89b49 fix(intake): classify owner dropdown options 2026-07-07 14:07:04 +08:00
WuYongJie
cec4b449ad fix(auth): 放行主管用户管理入口 2026-07-06 21:51:59 +08:00
WuYongJie
50b8f61a69 fix(auth): 补齐主管权限范围字段 2026-07-06 19:14:13 +08:00
jingchuan
cf54f94ba5 feat(oa-board): add summary tabs and stage graph updates 2026-07-04 16:46:38 +08:00
WuYongJie
5332202f9f fix(external): stream jsreport sync from stable cursor 2026-07-03 16:35:08 +08:00
WuYongJie
883d896406 chore(external): prefix jsreport sync tables 2026-07-03 16:08:00 +08:00
WuYongJie
122df58236 chore(external): sync jsreport every 45 minutes 2026-07-03 15:36:06 +08:00
WuYongJie
6c5ee5e512 chore: keep shell scripts lf 2026-07-03 15:00:15 +08:00
WuYongJie
0300858fdf feat(external): sync readonly views to jsreport 2026-07-03 14:46:59 +08:00
WuYongJie
611b7842c0 feat(external): expose development owners in catalog wide view 2026-07-03 13:26:59 +08:00
WuYongJie
1cbfeba4a6 merge: sync product category filter into main 2026-07-02 14:03:49 +08:00
WuYongJie
d598f30a0c feat(intake): add product category filter to development list 2026-07-02 14:03:07 +08:00
WuYongJie
8a28af209d merge: promote planning analysis upload copy to main 2026-07-02 10:40:20 +08:00
WuYongJie
f455dc47de fix(intake): update planning analysis upload copy 2026-07-02 10:39:56 +08:00
WuYongJie
f4dec4e1a9 merge: promote intake existing spu brand carryover to main
# Conflicts:
#	furniture-3d-system/src/server/services/__tests__/assisted-assembly-ai-render.service.test.ts
2026-07-02 10:15:46 +08:00
WuYongJie
b1f10cd37c feat(intake): align existing spu brand carryover 2026-07-02 10:09:49 +08:00
WuYongJie
553329ac80 test(ai-render): align delete storage mock 2026-07-01 15:50:56 +08:00
WuYongJie
921e0a1503 refactor(ai-render-workbench): split ai render backend flow 2026-07-01 15:44:06 +08:00
WuYongJie
a6fb2d8380 refactor(ai-render-workbench): split gallery and material panel 2026-07-01 15:44:06 +08:00
WuYongJie
0af59ed31b fix(ai-render): protect records when storage cleanup fails 2026-07-01 15:44:06 +08:00
WuYongJie
d02f27b28f refactor(ai-render-workbench): split ai render backend flow 2026-07-01 15:37:39 +08:00
WuYongJie
b59e6a241f fix(jst): push sales status and modular label 2026-07-01 15:05:40 +08:00
WuYongJie
135cab0990 fix(jst): push sales status and modular label 2026-07-01 15:03:36 +08:00
WuYongJie
f6f717eae8 refactor(ai-render-workbench): split gallery and material panel 2026-07-01 14:34:50 +08:00
WuYongJie
708e0679c2 fix(ai-render): protect records when storage cleanup fails 2026-07-01 13:13:40 +08:00
WuYongJie
397767604d fix(ai-render): make render deletion idempotent 2026-07-01 11:03:31 +08:00
WuYongJie
0df9f12c2c fix(intake): refine second-template barcode layout 2026-07-01 10:32:02 +08:00
WuYongJie
26b1d33eca merge: promote ai-white-background regenerate UI fix to main 2026-07-01 10:30:40 +08:00
WuYongJie
ad344c27eb Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/submit-barcode-second-template-followup-20260701 2026-07-01 10:30:06 +08:00
WuYongJie
8510049293 fix(intake): refine second-template barcode layout 2026-07-01 10:28:44 +08:00
WuYongJie
9b0bf2f31a fix(ai-white-background): hide duplicate regenerate entry and title in refinement popover
- 精修气泡展开时隐藏卡片底部「重新生成」按钮,避免功能入口重复占地方
- 去掉气泡内「重新生成 · 正视图」重复标题(上下文已在视图卡片内)
- 保留「生成」态与右侧精修面板,单张首生成能力不受影响
2026-07-01 10:25:27 +08:00
WuYongJie
354a154fee merge: promote second-template barcode print fix to main 2026-07-01 08:44:08 +08:00
WuYongJie
dd4dc279e1 fix(intake): stabilize second-template barcode printing 2026-07-01 08:43:45 +08:00
WuYongJie
91a9585b68 Merge remote-tracking branch 'origin/codex/xuxiake-mainline' 2026-06-30 17:50:37 +08:00
WuYongJie
9b799dd838 Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/wuyongjie-ai-white-background-color-fix 2026-06-30 17:41:13 +08:00
WuYongJie
c26113a6ec refactor(ai-white-background): 合并生成设置为单一材质方案区块
将「目标与源图」「单张修正」「材质方案」三个并列折叠区块合并为一个
「材质方案」区块,简化用户心智模型:

- 目标花色(本次生图主色) + 源条码 + 禁用原因 上移到材质方案区顶部
- 部位材质定义 保留为材质方案核心
- 各视图精修指令(原单张修正) 收纳为材质方案底部子区
- 「重新生成」按钮改为弹内联编辑气泡: 预填该视图精修提示词,
  可改可不改后确认重做, 不再跳转到独立单张修正区
- 简化 AiWhiteBackgroundSettingsSectionId 类型为单一 materials

纯前端 UI 重组, 不动后端 API/服务/数据模型。
type-check 通过, 19 个相关测试全部通过。
2026-06-30 17:39:51 +08:00
WuYongJie
960f867744 merge: promote opening-code oa prefill to main 2026-06-30 16:26:19 +08:00
WuYongJie
6d89e1c2c3 feat(intake): prefill create form from oa opening code 2026-06-30 16:25:09 +08:00
WuYongJie
70594a287d fix(intake): polish development toolbar controls 2026-06-30 14:31:39 +08:00
WuYongJie
4629eea670 fix(intake): keep development table header sticky 2026-06-30 14:13:51 +08:00
WuYongJie
2b0f3758bb feat(intake): add processed quick scope 2026-06-30 09:49:40 +08:00
WuYongJie
614e568763 fix(intake): align non-integrated quick scopes 2026-06-29 21:33:03 +08:00
WuYongJie
e0bc1db6bf feat(external): 宽表强制一条码一行保护(DISTINCT ON barcode_business_key)
根因:external.product_catalog_wide_v1 的 FROM sku_source 行粒度是 SKU,
barcode 只是字段。当前 43261 行 barcode 全唯一是数据巧合(formal 0 行 +
intake ready 每条码唯一),视图定义没强制一条码一行。一旦出现一条码多 SKU
(正式 ProductSKU 落库 + intake ready 同条码,或 intake 内部同条码多行),
宽表就会重复,污染外部系统 barcode 主同步键。

修复:复用项目已有 DISTINCT ON(barcode_business_key) 模式(latest_quality /
limit_by_barcode CTE),把原主查询整体包成 catalog_by_sku CTE,外层加
DISTINCT ON (barcode_business_key)。优先级:FORMAL > INTAKE_READY,
updated_at 最新 > created_at 最新。

末尾追加 catalog_source 字段输出(数据来源标记,对外部系统有用)。
用 CREATE OR REPLACE VIEW 保留 external_reader 已有 GRANT 权限。

生产验证:行数 43261 不变,0 重复 barcode 组,字段完整。
2026-06-29 19:40:23 +08:00
WuYongJie
9c04e39aaf Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/wuyongjie-ai-white-background-color-fix 2026-06-29 16:05:05 +08:00
WuYongJie
a9ec724127 fix(intake): rename modular tag to modular component 2026-06-29 15:59:57 +08:00
WuYongJie
2f59b46444 revert(storage): 恢复CDN直连307跳转,撤回流式转发
之前的流式转发改动(66928576)是误诊。真实根因是七牛AK/SK失效:
- 密钥有效时,307重定向到签名URL → 七牛返回200 + CORS头(Access-Control-Allow-Origin:*)
- 密钥失效时,签名URL无效 → 七牛返回401 → 浏览器误判为ORB拦截

七牛CDN本身已正确配置CORS,不存在跨域问题。流式转发反而违背'图片走CDN减轻
香港带宽压力'的架构目标(图片又挤回香港管道)。现撤回流式转发,恢复307直连CDN。
2026-06-29 15:44:05 +08:00
WuYongJie
906fd5b8e6 Merge remote-tracking branch 'origin/main' into codex/wuyongjie-ai-white-background-color-fix
# Conflicts:
#	furniture-3d-system/src/app/intake/oa-board/OaBoardClient.tsx
2026-06-29 15:01:22 +08:00
WuYongJie
98e89f1573 Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/wuyongjie-ai-white-background-color-fix 2026-06-29 13:59:38 +08:00
WuYongJie
66928576ac fix(storage): 图片代理改回流式转发修复跨域ORB拦截
根因:buildObjectReadResponse 默认对 https URL 返回 307 重定向到七牛 CDN,
浏览器对 cp.jiansun.vip → qnycdn.jiansun.vip 的跨域图片响应触发
ERR_BLOCKED_BY_ORB,导致所有 <img> 加载失败(图片全炸)。

修复:将 redirectHttps 默认值从 true 改为 false,强制走流式代理。
storage/object、intake assets 两处显式传 false 保持一致。
其余 6 处调用点自动继承新默认值,无需逐个改。

代价:Node 重新承担图片流式转发压力(后续签名URL下发优化解决)。
2026-06-29 13:58:51 +08:00
WuYongJie
5492ba316b fix(intake): prevent barcode print stretching 2026-06-29 13:30:03 +08:00
WuYongJie
772079750d fix(auth): allow redirect back to core workspaces 2026-06-29 13:30:03 +08:00
WuYongJie
cec5e2fcb2 fix(intake): prevent barcode print stretching 2026-06-29 13:11:13 +08:00
WuYongJie
b3d4a6f2ed fix(auth): allow redirect back to core workspaces 2026-06-29 13:11:12 +08:00
WuYongJie
391a085c2c fix(intake): derive product categories from secondary category 2026-06-29 11:54:38 +08:00
WuYongJie
b234134334 fix(intake): derive product categories from secondary category 2026-06-29 09:40:21 +08:00
WuYongJie
a0d597e14e refactor(ai-render-workbench): split workbench client modules 2026-06-26 21:25:20 +08:00
WuYongJie
3926fa4f26 refactor(ai-render-workbench): split workbench client modules 2026-06-26 21:18:28 +08:00
WuYongJie
443f56b099 fix(oa-board): compact filters and enable image preview 2026-06-26 17:56:56 +08:00
jingchuan
9c696e1947 fix(oa-board): stabilize filter and stage sync 2026-06-25 18:00:09 +08:00
WuYongJie
8cc0dc5218 fix(ai-white-background): 无花色借源颜色污染修复 + 工作区重排
根因:无花色 SKU 借源生图时,"未配置花色"占位符被当真实色传入,
后端 normalize 后 prompt 退化为"保持源图原色",AI 跟随源条码原图色调
生成错误颜色(SZ024203V01 案例:借胡桃色源图生成出胡桃色结果)。

A 方案(解开死结):
- Workspace 新增 inlineTargetColor 状态,用户可手动指定本次生图颜色
- isEffectiveAiWhiteBackgroundTargetColor 识别"未配置花色"等占位符为无效色
- targetColor 改为 effectiveTargetColor,占位符不再绕过空值校验
- Panel 目标花色区改为条件可输入:SKU 无色时显示输入框

B 方案(借源必填):
- 借源模式 + 无色时给出明确警示"必须指定目标颜色,否则跟随源条码色调"
- 输入框用警示色高亮

工作区重排(解决"操作小小一块"):
- 设置区加宽 310px→380px,字号放大 10/12px→11/13px
- 视图卡片放大 164/190px→180/210px,间距 gap-2→gap-3
- "目标与源图"小节默认展开(最常用不该藏)

验证:type-check 通过,白底图 61 个测试全过(含新增 3 个 A/B 方案验证)
2026-06-25 15:56:02 +08:00
WuYongJie
c172ce87bc docs(agents): 对齐当前项目真相并固化主线部署流程
- 全局规则引用改为 agent 中性(ZCode/Codex 各取所需)
- 技术栈口径改为以 package.json 为准,补 Tailwind v4 等漏列项
- 模型名 SKU 统一修正为 ProductSKU(业务概念与 Prisma 模型名对齐)
- 主链目录补充 approval-workflows/materials/mobile
- 对象存储路径修正为 src/lib/storage.ts(文件而非目录)
- 第 3 节新增主线部署标准流程(支线→xuxiake→main→部署)
- 合并验证与规范审查两节,修正不存在的 storage:gc 参数
2026-06-25 15:55:33 +08:00
WuYongJie
35ad68bf5e fix(storage): expose client runtime storage config 2026-06-24 18:28:32 +08:00
WuYongJie
caa5ca3c34 feat(draft-groups): add direct modular sku workflow 2026-06-24 17:58:54 +08:00
WuYongJie
e699dbb4b6 refactor(storage): remove legacy s3 aliases 2026-06-24 17:00:22 +08:00
WuYongJie
d42f4311e6 fix(storage): make gc provider aware 2026-06-24 16:54:23 +08:00
WuYongJie
d7d9317977 fix(storage): require explicit object provider 2026-06-24 16:44:01 +08:00
WuYongJie
13f8d94224 fix(intake): allow read-only adaptable brand viewing 2026-06-24 15:39:17 +08:00
WuYongJie
720644869c fix(ai-white-background): inherit related refinement context 2026-06-24 13:36:12 +08:00
WuYongJie
47a83a9f16 fix(ai-white-background): use generated views for refinement consistency 2026-06-24 11:25:08 +08:00
WuYongJie
350936d3c3 feat(asset-center): add asset family name code 2026-06-24 10:57:56 +08:00
WuYongJie
864b07c061 Align draft group deletion with modular detail references 2026-06-24 10:57:50 +08:00
WuYongJie
14ded3cab8 fix(ai-white-background): lock refinement camera prompts 2026-06-24 09:42:38 +08:00
WuYongJie
0eaa0ee023 Merge remote-tracking branch 'origin/main' 2026-06-24 09:38:56 +08:00
WuYongJie
708d28cd94 fix(intake): narrow brand center department permissions 2026-06-23 17:31:23 +08:00
WuYongJie
1188165fa1 fix(intake): narrow brand center department permissions 2026-06-23 17:25:03 +08:00
WuYongJie
2ba5062846 fix(intake): use basic info permission for sku download 2026-06-23 16:46:58 +08:00
WuYongJie
c9dd228c4b fix(ai-render): hide viewport helpers in references 2026-06-23 15:24:20 +08:00
WuYongJie
a62c1ee3b9 fix(ai-render): lock white render reference priority 2026-06-23 15:24:20 +08:00
WuYongJie
6702fd2995 fix(intake): lower document sidebar layer 2026-06-23 15:24:20 +08:00
WuYongJie
adba625280 fix(intake): lower document sidebar layer 2026-06-23 15:18:19 +08:00
WuYongJie
0d24fb3cef fix(draft-group): fit series spu field width 2026-06-23 14:43:11 +08:00
WuYongJie
1093772945 fix(ai-white-background): preserve generation progress 2026-06-23 14:13:13 +08:00
WuYongJie
3a17165562 test(draft-group): remove obsolete packaging fallback scaffolding 2026-06-23 13:01:27 +08:00
WuYongJie
534edddd11 feat(draft-group): refine assembly handoff linkage 2026-06-23 13:01:27 +08:00
WuYongJie
102a68ceb4 fix(assisted-assembly): align grouped barcode quantities
Sync draft-group grouped barcode quantity handling with assisted assembly and cover repeated grouped component sets in tests.
2026-06-23 13:01:27 +08:00
WuYongJie
7eef92219f Merge remote-tracking branch 'origin/main' into codex/wuyongjie-ai-wbg-upload-log 2026-06-23 11:40:56 +08:00
WuYongJie
9578c86201 fix(ai-white-background): trace batch generation views 2026-06-23 11:40:45 +08:00
WuYongJie
ff4f2bc962 fix(ai-render): add manual white-background detail references 2026-06-23 10:10:28 +08:00
WuYongJie
6d721d7b9a fix(ai-render): stabilize white render structure references 2026-06-23 10:10:28 +08:00
WuYongJie
ac03ec27de fix(intake): 串行补白底图生成 2026-06-23 09:07:48 +08:00
WuYongJie
c5733b8377 style(intake): 收纳AI白底图生成设置 2026-06-22 19:36:46 +08:00
WuYongJie
a77c87df35 fix(intake): 精确匹配AI白底图生成文件 2026-06-22 18:32:34 +08:00
WuYongJie
80e343b2d3 fix(intake): 优化产品资料与AI白底图界面密度 2026-06-22 17:40:22 +08:00
WuYongJie
c613a98b61 fix(intake): 增加产品资料缩略图悬浮预览 2026-06-22 17:02:33 +08:00
WuYongJie
2a185e6e79 fix(intake): 优化AI白底图反馈体验 2026-06-22 15:45:49 +08:00
WuYongJie
714036c751 fix(intake): 补齐AI白底图上传成功闭环 2026-06-22 14:20:42 +08:00
WuYongJie
5613f349a7 fix(intake): 为AI白底图素材库调用加超时 2026-06-22 13:32:56 +08:00
WuYongJie
e88bb2c8a5 fix(intake): 修复AI白底图素材库上传字段 2026-06-22 13:14:24 +08:00
WuYongJie
7eb044900c chore(intake): 记录AI白底图素材库上传失败 2026-06-22 12:02:11 +08:00
WuYongJie
531a84f5ab fix(intake): unify existing spu selection scope 2026-06-22 11:23:32 +08:00
WuYongJie
c144d836e9 fix(intake): 优化AI白底图单张修正 2026-06-22 11:12:11 +08:00
WuYongJie
17740ac822 fix(oa-board): 修复审批阶段状态类型收窄 2026-06-20 17:56:14 +08:00
jingchuan
2bd861527f fix(oa-board): align approval stage and owner resolution 2026-06-20 17:42:23 +08:00
WuYongJie
1fad12d718 fix(oa-board): align filter param parsing types 2026-06-20 16:31:39 +08:00
jingchuan
1cff836933 feat(oa-board): refine flow detail filters and layout 2026-06-20 16:10:46 +08:00
WuYongJie
9b5f2d9ee8 fix(ai-render): stabilize workbench white render pipeline 2026-06-20 16:06:11 +08:00
WuYongJie
710a208ba5 feat(intake): enhance product limit workbook flow 2026-06-20 14:12:12 +08:00
WuYongJie
3cb228fc16 feat(intake): scope sku batch workflow by batch 2026-06-20 11:38:52 +08:00
WuYongJie
3e860e0472 fix(asset-center): type mount point tuple 2026-06-20 08:47:21 +08:00
WuYongJie
251930b81d Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/xuxiake-mainline 2026-06-18 18:19:33 +08:00
WuYongJie
09c4e75f0e fix(asset-center): allow editing available model info 2026-06-18 18:19:23 +08:00
WuYongJie
30698a9480 feat(intake): 完善AI白底图重生修正交互 2026-06-18 15:15:34 +08:00
WuYongJie
875e8f3fc0 fix(intake): 防止AI白底图结果混入源图 2026-06-18 14:37:44 +08:00
WuYongJie
7bc7008799 fix(intake): 恢复AI白底图借源会话 2026-06-18 13:57:59 +08:00
WuYongJie
1af2c401d3 Merge remote-tracking branch 'origin/main' into codex/release-ai-white-background-structure 2026-06-18 13:25:16 +08:00
WuYongJie
dd4661ff6e Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/release-ai-white-background-structure 2026-06-18 13:20:18 +08:00
WuYongJie
394d2c2ce6 fix(intake): 修复AI白底图借源正视图丢失 2026-06-18 13:20:00 +08:00
WuYongJie
381999d69b fix(intake): stop modular detail fetch regressions 2026-06-18 11:26:59 +08:00
WuYongJie
4139f712cc fix(intake): stop modular detail fetch regressions 2026-06-18 11:25:35 +08:00
WuYongJie
013256dfbd Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/release-ai-white-background-structure 2026-06-18 11:12:02 +08:00
WuYongJie
e3217c6bc1 Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/wuyongjie-ai-render-texture-resign 2026-06-18 11:08:41 +08:00
WuYongJie
853d22d044 feat(intake): 优化AI白底图多视角生成 2026-06-18 11:08:29 +08:00
WuYongJie
7b62b0e291 fix(intake): require sample measurement before pricing push 2026-06-18 09:34:10 +08:00
WuYongJie
13b94e4263 fix(intake): require sample measurement before pricing push 2026-06-18 09:32:47 +08:00
WuYongJie
62eaf50800 优化AI白底图工作区布局 2026-06-17 19:36:20 +08:00
WuYongJie
d5a1427fff Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/wuyongjie-ai-render-texture-resign 2026-06-17 18:38:47 +08:00
WuYongJie
acd7756b2f fix(intake): allow modular parent sku push without cost price 2026-06-17 18:36:19 +08:00
WuYongJie
31391ee27e fix(draft-groups): harden modular detail navigation 2026-06-17 18:36:19 +08:00
WuYongJie
0fdd34ec24 fix(intake): 避免浏览触发自动保存 2026-06-17 18:30:51 +08:00
WuYongJie
d727459273 fix(intake): 按条码隔离实拍九视图 2026-06-17 17:58:54 +08:00
WuYongJie
0d2c648be4 Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/wuyongjie-ai-render-texture-resign
# Conflicts:
#	furniture-3d-system/src/app/intake/non-integrated/workspaces/ai-white-background/NonIntegratedAiWhiteBackgroundPanel.tsx
#	furniture-3d-system/src/app/intake/non-integrated/workspaces/ai-white-background/NonIntegratedAiWhiteBackgroundWorkspace.tsx
#	furniture-3d-system/src/app/intake/non-integrated/workspaces/documents/ProductPhotoWorkspace.tsx
2026-06-17 17:43:02 +08:00
WuYongJie
af601ddb01 feat(intake): 完善 AI 白底图材质方案工作区 2026-06-17 17:35:43 +08:00
WuYongJie
0a813f2557 Merge remote-tracking branch 'origin/main' into codex/wuyongjie-target-barcode-label-fix 2026-06-17 16:26:14 +08:00
WuYongJie
f3033b0886 fix(intake): show barcode first in photo sku tree 2026-06-17 16:22:01 +08:00
WuYongJie
05524721f3 feat(ai-render): make eight-view renders structure-aware 2026-06-17 16:11:47 +08:00
WuYongJie
286c4c743c fix(intake): require source selection before borrowing images 2026-06-17 15:43:42 +08:00
WuYongJie
26b7e9e8d1 fix(intake): show source barcode values in ai white panel 2026-06-17 14:51:49 +08:00
WuYongJie
97986fd530 fix(intake): show actual target barcode in ai white panel 2026-06-17 14:42:48 +08:00
WuYongJie
2c2baf4b64 fix(intake): show styleless sku barcodes in photo workspace 2026-06-17 14:10:18 +08:00
WuYongJie
b2aaf2acc0 feat(ai-render): add sku-scoped workbench generation 2026-06-17 14:07:03 +08:00
WuYongJie
9bdc6ff0bd fix(intake): support stock spu sku continuation 2026-06-17 12:13:52 +08:00
WuYongJie
ac24f0546c fix(intake): normalize barcode print data source 2026-06-17 11:15:11 +08:00
WuYongJie
882e112a2a fix(ai-render): extend external image generation timeout 2026-06-17 09:44:20 +08:00
WuYongJie
88fd9bc231 merge: promote ai white background barcode backfill 2026-06-17 09:20:48 +08:00
WuYongJie
509c5cf730 merge: promote ai white background barcode backfill 2026-06-17 09:20:00 +08:00
WuYongJie
1b90df2b20 feat(intake): support ai white background barcode backfill 2026-06-17 09:18:58 +08:00
WuYongJie
7de12951b7 fix(ai-render): persist shared workbench data 2026-06-16 20:27:25 +08:00
WuYongJie
2cb01feafd merge: promote ai white background generation hardening 2026-06-16 19:48:22 +08:00
WuYongJie
f4f956bc8c fix(intake): harden ai white background generation 2026-06-16 19:47:51 +08:00
WuYongJie
c6b53f4cb8 Merge remote-tracking branch 'origin/codex/wuyongjie-ai-render-texture-resign' into HEAD
# Conflicts:
#	furniture-3d-system/src/server/services/oa-board.service.ts
2026-06-16 18:44:03 +08:00
WuYongJie
cdd64ade24 fix(oa-board): normalize timeline metric typing 2026-06-16 18:35:42 +08:00
WuYongJie
815ea75745 Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/wuyongjie-ai-render-texture-resign 2026-06-16 18:31:13 +08:00
WuYongJie
fe139898e4 feat(intake): persist ai white background derived sku links 2026-06-16 18:31:05 +08:00
WuYongJie
c4caf7fc8e feat(intake): persist barcode print data 2026-06-16 18:14:38 +08:00
WuYongJie
15207d11fa fix(oa-board): type timeline metrics for build 2026-06-16 18:11:47 +08:00
jingchuan
03bdc6860f feat(oa-board): refine detail flow monitor 2026-06-16 18:05:26 +08:00
WuYongJie
5ec794e13e Merge remote-tracking branch 'origin/codex/wuyongjie-ai-render-texture-resign' into HEAD 2026-06-16 18:00:12 +08:00
jingchuan
874e67a185 feat(oa-board): refine detail flow monitor 2026-06-16 17:57:50 +08:00
WuYongJie
d6f9a861fc Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/wuyongjie-ai-render-texture-resign 2026-06-16 17:56:13 +08:00
WuYongJie
f4d93ddaa2 feat(intake): 支持AI白底图派生同批次SKU 2026-06-16 17:55:53 +08:00
WuYongJie
2e9f163c3a merge: promote ai render sku-bound tasks 2026-06-16 17:10:59 +08:00
WuYongJie
c2f0898784 feat(ai-render): bind render tasks to sku 2026-06-16 17:10:16 +08:00
WuYongJie
35ef013916 merge: promote AI render material signed urls to main 2026-06-16 16:21:30 +08:00
WuYongJie
fce3f46a04 merge: promote AI render material signed urls 2026-06-16 16:19:44 +08:00
WuYongJie
1954226df7 Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/wuyongjie-ai-render-texture-resign 2026-06-16 16:17:20 +08:00
WuYongJie
7b94f30108 fix(storage): 延长七牛私有读链接有效期 2026-06-16 16:12:55 +08:00
WuYongJie
18dc1aa8ed fix(ai-render): 直传材质贴图签名URL 2026-06-16 16:09:57 +08:00
WuYongJie
e2a97af404 feat(ai-render): finalize model-locked image generation 2026-06-16 16:08:50 +08:00
WuYongJie
1c055d97c5 feat(ai-render): finalize model-locked image generation 2026-06-16 16:08:09 +08:00
WuYongJie
ca2eb8c4a6 merge: 稳定非集成访问链路 2026-06-16 15:09:47 +08:00
WuYongJie
e3fa838b39 merge: 稳定非集成访问链路 2026-06-16 15:09:08 +08:00
WuYongJie
d374470151 fix: stabilize intake navigation requests 2026-06-16 15:08:06 +08:00
WuYongJie
f36c020902 fix(ai-render): 重新签发材质贴图引用 2026-06-16 14:47:10 +08:00
WuYongJie
6eb624f8e4 fix(intake): simplify AI white product photo controls 2026-06-16 13:53:32 +08:00
WuYongJie
351a14d3ad fix(auth): allow restricted users into AI render workbench 2026-06-16 13:26:59 +08:00
WuYongJie
a5fe03b721 fix(ai-render): normalize UV axis config values 2026-06-16 13:05:31 +08:00
WuYongJie
0bfa19ae9c merge: promote AI render workbench 2026-06-16 12:55:27 +08:00
WuYongJie
5ae0b39192 feat(ai-render): add model-locked render workbench 2026-06-16 12:52:22 +08:00
WuYongJie
f12832ee69 merge: fix copied brand SPU reservation 2026-06-16 11:37:32 +08:00
WuYongJie
f49b0c6036 Merge remote-tracking branch 'origin/main' into codex/fix-copy-spu-20260616-111430 2026-06-16 11:36:30 +08:00
WuYongJie
d6b72c7060 fix: prevent copied brand products from reserving new SPU 2026-06-16 11:36:09 +08:00
WuYongJie
88182d671b feat(intake): add barcode print supplier workflow 2026-06-16 11:32:57 +08:00
WuYongJie
1f767cc76f merge: promote AI 白底图花色选择布局修复到主线 2026-06-16 11:28:45 +08:00
WuYongJie
3740bec0d2 fix(intake): constrain AI white target finish picker 2026-06-16 11:25:33 +08:00
WuYongJie
72b49620d8 merge: promote AI 白底图工作区布局修复到主线 2026-06-16 11:11:16 +08:00
WuYongJie
4d137e8734 fix(intake): 修复 AI 白底图工作区布局 2026-06-16 11:09:37 +08:00
WuYongJie
74ccbb3a1e Merge remote-tracking branch 'origin/main' into codex/xuxiake-ai-render-mainline-20260616 2026-06-16 10:47:04 +08:00
WuYongJie
6796944dc8 feat(ai-render): sync render workbench baseline 2026-06-16 10:46:36 +08:00
WuYongJie
6ac6c99811 merge: fix AI 白底图拖动看图类型检查 2026-06-16 10:22:08 +08:00
WuYongJie
d936895438 fix(intake): 修复 AI 白底图拖动状态类型检查 2026-06-16 10:21:10 +08:00
WuYongJie
a5ea1c1889 merge: promote AI 白底图拖动看图到主线 2026-06-16 10:11:29 +08:00
WuYongJie
ce6c403637 fix(intake): 支持拖动查看放大的 AI 白底图原图 2026-06-16 10:09:00 +08:00
WuYongJie
83f02257d1 merge: promote AI 白底图滚轮缩放到主线 2026-06-16 09:55:50 +08:00
WuYongJie
b2fea563ca fix(intake): 支持滚轮缩放 AI 白底图原图 2026-06-16 09:46:38 +08:00
WuYongJie
fec144e6a9 merge: promote AI 白底图黑色遮罩看图到主线 2026-06-16 09:32:34 +08:00
WuYongJie
eda6600a6f fix(intake): 改为黑色遮罩查看 AI 白底图原图 2026-06-16 09:31:37 +08:00
WuYongJie
b8cf682edc merge: promote AI 白底图页内原图查看到主线 2026-06-15 21:20:14 +08:00
WuYongJie
285b1fb2d9 fix(intake): 改为页内查看 AI 白底图原图 2026-06-15 21:19:33 +08:00
WuYongJie
2704d33b6e merge: promote AI 白底图低噪音调整到主线 2026-06-15 20:51:44 +08:00
WuYongJie
57279aa91f fix(intake): 降低 AI 白底图界面噪音 2026-06-15 20:47:31 +08:00
WuYongJie
5e1efa7a64 merge: promote AI 白底图界面改造到主线 2026-06-15 20:20:03 +08:00
WuYongJie
562cf5bba3 feat(intake): 重写 AI 白底图工作区界面 2026-06-15 20:10:32 +08:00
WuYongJie
5fb0028b31 merge: promote clean consolidation to main 2026-06-15 20:00:09 +08:00
WuYongJie
33c2e2899f Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/xuxiake-clean-consolidation-20260615 2026-06-15 19:53:40 +08:00
WuYongJie
5a0593e952 feat(auth): add supplier barcode account access 2026-06-15 19:52:47 +08:00
WuYongJie
e5175cabb0 merge: sync ai white background workspace 2026-06-15 19:19:43 +08:00
WuYongJie
94432e04d3 feat(intake): 将 AI 白底图升级为产品实拍工作区 2026-06-15 19:18:10 +08:00
WuYongJie
2a84f66951 feat(intake): add barcode print workbench 2026-06-15 19:15:22 +08:00
WuYongJie
6b4dcbdd31 feat(asset-center): make modular assets immediately available 2026-06-15 18:51:19 +08:00
WuYongJie
2e42fb220f feat(draft-groups): push pre sku to series product 2026-06-15 18:37:54 +08:00
WuYongJie
ecc294e79f fix(storage): fallback object reads without configured prefix 2026-06-15 18:32:39 +08:00
WuYongJie
9dbb61cdd1 feat(ai-render): add render workbench 2026-06-15 18:28:59 +08:00
WuYongJie
2d789c596c merge: sync storage gc negative status retry 2026-06-15 17:53:14 +08:00
WuYongJie
c36d77fba8 fix(storage): 重试七牛负状态码错误 2026-06-15 17:52:52 +08:00
WuYongJie
d4c20518b3 merge: sync storage gc reserved prefix guard 2026-06-15 17:36:23 +08:00
WuYongJie
8f1751c917 fix(storage): 支持 GC 运行时保留前缀 2026-06-15 17:36:10 +08:00
WuYongJie
f0b0c699ef merge: sync storage gc retry hardening 2026-06-15 17:27:23 +08:00
WuYongJie
cb3d8434e0 fix(storage): 增加七牛 GC 请求重试 2026-06-15 17:27:07 +08:00
WuYongJie
14c632d063 merge: sync storage gc qiniu sdk fix 2026-06-15 17:20:46 +08:00
WuYongJie
169a8df718 fix(storage): 兼容七牛 SDK 默认导出 2026-06-15 17:20:30 +08:00
WuYongJie
3b891b5460 merge: sync oa board type fix from xuxiake mainline 2026-06-15 17:06:14 +08:00
WuYongJie
c180cf2b18 fix(oa-board): 补齐同步看板类型导出 2026-06-15 17:05:41 +08:00
WuYongJie
e7e2a9368d merge: promote storage gc governance to main 2026-06-15 16:58:25 +08:00
WuYongJie
b26a30a8ae merge: promote storage gc governance to xuxiake mainline 2026-06-15 16:57:10 +08:00
WuYongJie
24c65f4da8 feat(storage): 增加七牛垃圾文件治理 2026-06-15 16:51:57 +08:00
WuYongJie
0eda70edd8 merge xuxiake mainline oa board and ai asset fixes 2026-06-15 16:44:34 +08:00
jingchuan
3ee7122380 feat(oa-board): add new product flow monitor 2026-06-15 15:58:09 +08:00
WuYongJie
829f828bbe fix(intake): 延长 AI 白底图请求等待 2026-06-15 15:51:15 +08:00
WuYongJie
455686e1e3 fix(intake): 降低非集成素材读取超时 2026-06-15 15:30:39 +08:00
WuYongJie
68a17b1dff feat(intake): 对接 AI 白底图花色改色与素材库上传 2026-06-15 14:58:37 +08:00
WuYongJie
319d61e41a merge xuxiake mainline asset preview permission fix 2026-06-15 12:11:07 +08:00
WuYongJie
dcfa96206a fix(intake): allow asset previews with view permission 2026-06-15 12:10:50 +08:00
WuYongJie
5f371c7a70 merge xuxiake mainline image proxy fix 2026-06-15 11:54:47 +08:00
WuYongJie
35740a7bc5 fix(intake): proxy non-integrated asset reads 2026-06-15 11:54:29 +08:00
WuYongJie
5fc719ce88 merge: promote platform material flow to main 2026-06-15 11:15:55 +08:00
WuYongJie
3d9aca430d merge: 稳定移动端与素材访问链路 2026-06-15 11:15:29 +08:00
WuYongJie
f802d65490 merge: sync xuxiake mainline before platform material fix
# Conflicts:
#	furniture-3d-system/src/app/assisted-assembly/mobile/AssistedAssemblyMobileView.tsx
#	furniture-3d-system/src/app/mobile/HomeMobileView.tsx
#	furniture-3d-system/src/server/services/asset-center.service.ts
2026-06-15 11:03:11 +08:00
WuYongJie
77a78f4938 fix(platform): 稳定移动端与素材访问链路 2026-06-15 11:02:10 +08:00
WuYongJie
17c915f0a6 merge: promote workbench nav loading fix to main 2026-06-15 10:38:52 +08:00
WuYongJie
4f8d18f454 merge: 修复工作台导航首屏权限加载态 2026-06-15 10:38:12 +08:00
WuYongJie
5fbec8a7c9 fix(layout): 修复工作台导航首屏权限加载态 2026-06-15 10:37:29 +08:00
WuYongJie
12a90477f7 Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/xuxiake-promote-asset-center-sku-group-fix-20260615 2026-06-15 08:43:02 +08:00
WuYongJie
a611640927 merge: sync asset center sku group fix with mainline 2026-06-15 08:42:08 +08:00
WuYongJie
c58c7aaa22 fix(asset-center): read retired sku groups from metadata 2026-06-13 23:29:44 +08:00
WuYongJie
55d92d4e75 fix(ui): show immediate workbench nav feedback 2026-06-13 18:34:01 +08:00
WuYongJie
62da3f6993 fix(ui): show immediate workbench nav feedback 2026-06-13 18:33:40 +08:00
WuYongJie
79cc190a0f merge: promote nav smoothness optimization to main 2026-06-13 18:05:18 +08:00
WuYongJie
7ba1e86410 merge: promote nav smoothness optimization 2026-06-13 18:04:36 +08:00
WuYongJie
713b8986c9 fix(ui): smooth intake and quality navigation 2026-06-13 18:04:05 +08:00
WuYongJie
6a4b8775de merge: promote intake filter overflow fix to main 2026-06-13 16:53:23 +08:00
WuYongJie
f3d38be04b merge: promote intake filter overflow fix 2026-06-13 16:53:12 +08:00
WuYongJie
5ea46cdc68 fix(ui): prevent intake mobile filter overflow 2026-06-13 16:53:01 +08:00
WuYongJie
7f69e8271a merge: promote intake overflow fix to main 2026-06-13 16:45:22 +08:00
WuYongJie
6897c38f97 merge: promote intake overflow fix 2026-06-13 16:45:02 +08:00
WuYongJie
e973b1a0a1 fix(ui): constrain intake table overflow on mobile 2026-06-13 16:44:42 +08:00
WuYongJie
a7ad991c59 merge: promote smoothness fixes to main 2026-06-13 16:35:45 +08:00
WuYongJie
984ae53af3 merge: promote smoothness and migration alignment 2026-06-13 16:35:09 +08:00
WuYongJie
df05999c0f fix(ui): stabilize mobile hydration and layout overflow 2026-06-13 16:33:41 +08:00
WuYongJie
b9476733fc fix(db): add asset sku group restore migration 2026-06-13 16:33:36 +08:00
WuYongJie
0b5ae56dd5 merge: promote storage qiniu direct access normalization to main 2026-06-13 10:31:57 +08:00
WuYongJie
3972ca2edd merge: promote storage qiniu direct access normalization 2026-06-13 10:31:09 +08:00
WuYongJie
100e90a3be fix(storage): 统一公开资源直链与持久化地址归一 2026-06-13 10:24:41 +08:00
WuYongJie
e1b78d602e fix: intake 所有工作区表格表头 sticky 固定
- 所有 workspace 表格 thead 添加 sticky top-0 z-10,滚动时表头固定
- 外层容器 overflow-hidden 改为 overflow-auto,配合滚动生效
- 基础信息 SKU 表格合并双层 div 为单层,添加 max-h-[60vh] 高度约束
- BOM 表格同步添加 sticky thead
2026-06-12 18:40:21 +08:00
WuYongJie
e7d2c8626c merge: 解决冲突,保留使用 SharedAssetFamilyDTO 的类型修复 2026-06-12 17:30:49 +08:00
WuYongJie
034817f119 fix(asset-center): 修复移动端 AssetFamilyDTO 类型不兼容导致生产构建失败 2026-06-12 17:25:56 +08:00
WuYongJie
5e5687dcc1 fix: require manual confirmation for factory workflows 2026-06-12 17:16:47 +08:00
WuYongJie
05d90fe57e feat(ai-render): 支持多材质贴图传入外部生图API
- external-image-generate: 新增 additionalReferenceImages 字段支持多文件上传
- assisted-assembly-ai-render: 自动提取 assetSnapshot 中的材质贴图
- 按 materialConfig.id 去重聚合,最多 6 种材质
- 构建材质映射描述追加到 prompt,告诉 AI 每张贴图对应哪个部位
- 支持 FormData 混合模式(文件 + URL)和 curl 降级
2026-06-12 16:37:56 +08:00
WuYongJie
a2e9335fc5 feat: Stitch 移动端视觉重构 — 5 页面全部完成独立移动端视图
新增独立移动端视图文件(遵循用户要求:不堆砌在原臃肿文件中):
- asset-center/mobile/AssetCenterMobileView.tsx (274行) — 系列选择 + Tab + 搜索 + 卡片列表
- assisted-assembly/mobile/AssistedAssemblyMobileView.tsx (148行) — 系列列表 + 工作区提示
- draft-groups/mobile/DraftGroupsMobileView.tsx (173行) — 搜索 + 统计 + 草稿列表

原文件改动最小化(仅 +3 行导入 + 15 行条件分支):
- AssetCenterClient.tsx (7690行) → 添加 useBreakpoint + <AssetCenterMobileView />
- AssistedAssemblyClient.tsx (1580行) → 添加 useBreakpoint + <AssistedAssemblyMobileView />
- DraftGroupsFormalClient.tsx (4943行) → 添加 useBreakpoint + <DraftGroupsMobileView />

全局 Stitch 基础设施(已提交):
- globals.css: stitch-card 圆角修正 2px→12px,新增 20+ 原子类
- AppShell: 移动端背景 #F8F9FA
- MobileTabBar: 精确 Stitch BottomNavBar
- PlatformSplitLayout: 移动端 #F8F9FA 背景
- IntakeWorkspaceShell: 移动端紧凑间距
- StitchMobileLayout: 可复用布局组件

所有改动仅 max-lg 断点生效,桌面端零影响
2026-06-12 16:37:56 +08:00
WuYongJie
f27aa44ec5 refactor: 拆分移动端视图为独立文件,避免臃肿
- HomeClient.workspace.tsx: 925行 → 738行,移动端布局拆为 mobile/HomeMobileView.tsx (445行独立文件)
- IntakeWorkspaceTabBar.tsx: 109行 → 77行,移动端Tab拆为 IntakeWorkspaceTabBarMobile.tsx (56行独立文件)
- 原文件仅保留一行条件引用 <HomeMobileView /> / <IntakeWorkspaceTabBarMobile />
- 桌面端代码零改动
2026-06-12 16:37:56 +08:00
WuYongJie
03d4ee83e1 style: Stitch 移动端像素级复刻
- AppShell 移动端顶栏改为 Stitch TopAppBar(80% glass + 24px blur)
- globals.css 新增 Stitch 原子类库
- MobileTabBar 对齐 Stitch BottomNavBar
- 全部 max-lg 断点,桌面端零影响
2026-06-12 16:37:56 +08:00
WuYongJie
1b992cb89a style: MobileTabBar 对齐 Stitch BottomNavBar + 移动端卡片无边
- MobileTabBar: bg-80% opacity + backdrop-blur-24px + 微阴影
- 移除 TabBar 大圆角激活 pill,改为纯文字+图标
- section 单列模式移动端 rounded-none(全宽无边)
- mainContent 模式左侧栏移动端 rounded-none
2026-06-12 16:37:56 +08:00
WuYongJie
b1deae4c73 refactor: 对齐 Stitch 设计稿视觉规范
- section 改为透明布局容器,背景用 page background
- 侧边栏/主内容/右面板各自独立:rounded-xl + shadow
- 面板间用 gap-3 间距,不再依赖边框分隔
- metrics 芯片移除边框,改用白色卡片 + 微阴影
- 标题使用 Manrope 字体(font-family-display)
- 配色使用 CSS 变量,对齐 Stitch surface 层级
2026-06-12 16:37:56 +08:00
WuYongJie
e333f68cc3 refactor: PlatformSplitLayout 应用 Stitch 设计规范
- 移除 section 的 border/shadow/rounded-[2rem](No-Line Rule)
- 侧边栏和右面板移除 border,改用表面层级色差分离
- 背景改用 surface-container-low 色调
- 移除 metrics 芯片的边框
- IntakeMainLayout 移动端隐藏右面板
2026-06-12 16:37:56 +08:00
WuYongJie
1a8367a9da refactor: split intake detail controllers 2026-06-12 13:21:37 +08:00
WuYongJie
582d2ee8cb fix: allow product factory MOQ sync saves 2026-06-12 12:39:15 +08:00
WuYongJie
307d92b7f5 fix: PlatformSplitLayout 移动端响应式适配
- header/metrics/feedback 区域移动端缩减 padding
- metrics 区域移动端支持横向滚动
- 移除移动端大圆角卡片边框(平铺全宽)
- 侧边栏移动端撑满全宽(有 mainContent 时)
- 右侧面板移动端隐藏(后续可通过抽屉/切换展开)
- 使用 max-lg:! 优先级覆盖父组件的 !important 样式
2026-06-11 22:56:04 +08:00
WuYongJie
311cd74358 Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/wuyongjie-storage-unify-qiniu-20260611 2026-06-11 20:43:45 +08:00
WuYongJie
61a2834710 fix(storage): 公开素材直出七牛读取地址 2026-06-11 20:43:23 +08:00
WuYongJie
cb3b38a952 fix(intake): unify spu rule truth source 2026-06-11 17:41:11 +08:00
WuYongJie
3768e5d601 feat(intake): add sku order quantity field 2026-06-11 16:56:26 +08:00
WuYongJie
3724764079 merge: sync origin/codex/xuxiake-mainline 2026-06-11 14:08:21 +08:00
WuYongJie
655a882cf0 fix(storage): unify runtime asset reads to qiniu 2026-06-11 14:04:45 +08:00
WuYongJie
2ff31032c6 fix(intake): keep barcode print as reserved entry 2026-06-11 13:38:15 +08:00
WuYongJie
90ff111283 Merge remote-tracking branch 'origin/main' into codex/xuxiake-intake-primary-domain-sync-20260610
# Conflicts:
#	furniture-3d-system/src/app/intake/AccessoryLibraryPanel.tsx
#	furniture-3d-system/src/app/intake/IntakeClient.test.tsx
#	furniture-3d-system/src/app/intake/IntakeClient.tsx
#	furniture-3d-system/src/app/intake/SpuRuleLibraryWorkspace.test.tsx
#	furniture-3d-system/src/app/intake/SpuRuleLibraryWorkspace.tsx
#	furniture-3d-system/src/app/intake/bom-library/AccessoryLibraryLogic.tsx
#	furniture-3d-system/src/app/intake/bom-library/AccessoryLibraryPanel.tsx
#	furniture-3d-system/src/app/intake/bom-library/BomLibraryDomain.tsx
#	furniture-3d-system/src/app/intake/non-integrated/NonIntegratedDetailClient.tsx
#	furniture-3d-system/src/app/intake/non-integrated/nonIntegratedApi.ts
#	furniture-3d-system/src/app/intake/non-integrated/nonIntegratedAssetServer.ts
#	furniture-3d-system/src/app/intake/product-development/ProductDevelopmentDomain.tsx
#	furniture-3d-system/src/app/intake/product-development/SpuRuleLibraryWorkspace.test.tsx
#	furniture-3d-system/src/app/intake/product-development/SpuRuleLibraryWorkspace.tsx
#	furniture-3d-system/src/app/intake/product-development/spu-rule-library/SpuRuleLibraryWorkspace.test.tsx
#	furniture-3d-system/src/app/intake/product-development/spu-rule-library/SpuRuleLibraryWorkspace.tsx
#	furniture-3d-system/src/app/intake/product-development/spu-rule-library/spuRuleLibraryLogic.test.ts
#	furniture-3d-system/src/app/intake/product-development/spuRuleLibraryLogic.test.ts
#	furniture-3d-system/src/app/intake/product-patent/ProductPatentDomain.tsx
#	furniture-3d-system/src/app/intake/raw-material-library/RawMaterialLibraryDomain.tsx
#	furniture-3d-system/src/app/intake/raw-material-library/RawMaterialLibraryPanel.tsx
#	furniture-3d-system/src/app/intake/reference-estimator/ReferenceEstimatorDomain.tsx
#	furniture-3d-system/src/app/intake/spuRuleLibraryLogic.test.ts
2026-06-11 13:19:58 +08:00
WuYongJie
af27eff911 fix(intake): finalize non-integrated detail split 2026-06-11 12:52:56 +08:00
WuYongJie
5745abf0ae fix(intake): allow merchandisers to download factory files 2026-06-11 12:43:06 +08:00
WuYongJie
d5aba614fe fix(intake): allow deleting sku preview images 2026-06-11 11:56:12 +08:00
WuYongJie
16679896cf refactor(intake): complete primary domain split 2026-06-11 11:46:25 +08:00
WuYongJie
d1ddf2de7a refactor(intake): split non-integrated workspace containers 2026-06-11 11:46:25 +08:00
WuYongJie
1608b8f7ad refactor: split intake workspace structure 2026-06-11 11:46:24 +08:00
WuYongJie
5cc335acde fix(users): allow external intake business role 2026-06-11 09:40:25 +08:00
WuYongJie
bbdb63a9be merge: promote xuxiake mainline storage build closure 2026-06-11 01:43:01 +08:00
WuYongJie
1d4e8cffc2 fix(storage): 打通对象存储边界与生产构建 2026-06-11 01:41:47 +08:00
WuYongJie
71849d4aab merge: promote xuxiake mainline storage unify qiniu 2026-06-11 01:06:14 +08:00
WuYongJie
2f02d72565 feat(storage): 统一对象存储资源到稳定七牛访问口径 2026-06-11 01:03:56 +08:00
WuYongJie
28121b750d Merge remote-tracking branch 'origin/codex/xuxiake-mainline' 2026-06-10 23:11:02 +08:00
WuYongJie
b9698b1fbb fix: use anchor tag + router.push for mobile tab navigation 2026-06-10 23:10:27 +08:00
WuYongJie
f0fef3f906 Merge remote-tracking branch 'origin/codex/xuxiake-mainline' 2026-06-10 22:52:22 +08:00
WuYongJie
9f4e46d06d feat: mobile responsive adaptation for main pages
- Add useMediaQuery hook for breakpoint detection (1024px)
- Add MobileTabBar component for mobile bottom navigation
- Add MobileInspectorDrawer component with 3-position drag
- Update globals.css with responsive variables and touch targets
- Update AppShell/AppTopNav for mobile layout switching
- Update HomeClient, AssetCenter, AssistedAssembly, DraftGroups for responsive layout
- Update Button/Input/Modal/Select with 44px minimum touch targets
2026-06-10 22:33:24 +08:00
WuYongJie
aa2ce7910b refactor(intake): split primary intake domains 2026-06-10 19:35:39 +08:00
WuYongJie
64cd9d67a5 fix(intake): support existing spu sequence creation 2026-06-10 18:42:28 +08:00
WuYongJie
bc945b3328 refactor(intake): extract product patent domain 2026-06-10 18:32:44 +08:00
WuYongJie
b02bb244ec refactor(intake): extract review issue image manager 2026-06-10 18:32:43 +08:00
WuYongJie
df580ae8bd refactor(intake): extract review issue image cell 2026-06-10 18:32:43 +08:00
WuYongJie
bd777856e6 refactor(intake): extract detection workspace 2026-06-10 18:32:43 +08:00
WuYongJie
ac79549aca refactor(intake): extract installation workspace 2026-06-10 18:32:43 +08:00
WuYongJie
4b2e1c435c refactor(intake): extract first quality workspace 2026-06-10 18:32:43 +08:00
WuYongJie
0e923eadff refactor(intake): extract archive preview panel 2026-06-10 18:32:43 +08:00
WuYongJie
d604ee0474 refactor(intake): extract product limit workspace 2026-06-10 18:32:43 +08:00
WuYongJie
1dca970937 refactor(intake): extract product pricing workspace 2026-06-10 18:32:43 +08:00
WuYongJie
3799702d22 refactor(intake): extract quality inspection workspace 2026-06-10 18:32:43 +08:00
WuYongJie
8fc529d2eb refactor(intake): extract planning layout workspace 2026-06-10 18:32:43 +08:00
WuYongJie
9961f8a1b9 refactor(intake): extract finished sample shipment workspace 2026-06-10 18:32:43 +08:00
WuYongJie
921c55c97f refactor(intake): extract sample measurement workspace 2026-06-10 18:32:43 +08:00
WuYongJie
b60a0a45ab refactor(intake): extract patent application workspace 2026-06-10 18:32:42 +08:00
WuYongJie
4994957194 refactor(intake): extract quality standard workspace 2026-06-10 18:32:42 +08:00
WuYongJie
d7278d5fff refactor(intake): extract attachment workspace 2026-06-10 18:32:42 +08:00
WuYongJie
d6b0960272 refactor(intake): extract opening reference workspace 2026-06-10 18:32:42 +08:00
WuYongJie
ffd53ec713 refactor(intake): extract product data workspace 2026-06-10 18:32:42 +08:00
WuYongJie
778d03c903 refactor(intake): extract project review workspace 2026-06-10 18:32:42 +08:00
WuYongJie
a61d1a5dd7 refactor(intake): move basic info field config 2026-06-10 18:32:42 +08:00
WuYongJie
3fa86786e8 refactor(intake): extract progress helpers 2026-06-10 18:32:42 +08:00
WuYongJie
5eba6797ab refactor(intake): move url download helper 2026-06-10 18:32:42 +08:00
WuYongJie
2551018e22 refactor(intake): move blob fetch helper 2026-06-10 18:32:42 +08:00
WuYongJie
46f43eebdf refactor(intake): move download error helper 2026-06-10 18:32:42 +08:00
WuYongJie
ba226085a2 refactor(intake): move workspace zip filename helper 2026-06-10 18:32:41 +08:00
WuYongJie
c4ffbcba0b refactor(intake): move download helpers 2026-06-10 18:32:41 +08:00
WuYongJie
f5a3a6940a refactor(intake): move bom category helpers 2026-06-10 18:32:41 +08:00
WuYongJie
90856bbe3a refactor(intake): move product pricing system entry helper 2026-06-10 18:32:41 +08:00
WuYongJie
d5cafaa699 refactor(intake): move regex escape helper 2026-06-10 18:32:41 +08:00
WuYongJie
4bdb82b7ed refactor(intake): extract workflow confirmation helpers 2026-06-10 18:32:41 +08:00
WuYongJie
9eff0cb944 refactor(intake): extract product patent sync helper 2026-06-10 18:31:59 +08:00
WuYongJie
4589acfed2 refactor(intake): move image upload helpers 2026-06-10 18:31:08 +08:00
WuYongJie
d43feb85d7 refactor(intake): move opening reference form helpers 2026-06-10 18:31:08 +08:00
WuYongJie
9ce35b00a2 refactor(intake): extract product factory helpers 2026-06-10 18:31:08 +08:00
WuYongJie
46a143b4dc refactor(intake): extract bom value helpers 2026-06-10 18:31:07 +08:00
WuYongJie
441c14a97f refactor(intake): extract bom detail key helpers 2026-06-10 18:31:07 +08:00
WuYongJie
52fb2ff638 refactor(intake): extract upload metadata helpers 2026-06-10 18:31:07 +08:00
WuYongJie
4a2731d56e refactor(intake): extract product form value helpers 2026-06-10 18:31:07 +08:00
WuYongJie
2c46f40a38 refactor(intake): extract product workflow row helpers 2026-06-10 18:31:07 +08:00
WuYongJie
993675ef3f refactor(intake): move empty sku item helper 2026-06-10 18:31:07 +08:00
WuYongJie
863e4a470d refactor(intake): move empty style detail state 2026-06-10 18:31:07 +08:00
WuYongJie
cfe38da760 refactor(intake): reuse sku preview node helper 2026-06-10 18:31:07 +08:00
WuYongJie
40e992acf6 refactor(intake): extract sku preview node helpers 2026-06-10 18:31:07 +08:00
WuYongJie
645130a042 refactor(intake): move document workspace initialization 2026-06-10 18:31:07 +08:00
WuYongJie
a30151a24a refactor(intake): extract product factory key helpers 2026-06-10 18:31:07 +08:00
WuYongJie
8f8ea08155 refactor(intake): move sample shipment review helper 2026-06-10 18:31:06 +08:00
WuYongJie
8f9dffa3c6 refactor(intake): move review content helpers 2026-06-10 18:31:06 +08:00
WuYongJie
59689010ed refactor(intake): extract sku import batch helpers 2026-06-10 18:31:06 +08:00
WuYongJie
7b8142e1f6 refactor(intake): extract sku source helpers 2026-06-10 18:31:06 +08:00
WuYongJie
5acf2d3d9e refactor(intake): move sku row clone helper 2026-06-10 18:31:06 +08:00
WuYongJie
4f66a42cec refactor(intake): move detection workspace helpers 2026-06-10 18:31:06 +08:00
WuYongJie
96e7a4db8d refactor(intake): move project review owner helper 2026-06-10 18:31:06 +08:00
WuYongJie
03ac72700b refactor(intake): extract upload storage helper 2026-06-10 18:31:06 +08:00
WuYongJie
14d7ddcd4e refactor(intake): move document revision file helper 2026-06-10 18:31:06 +08:00
WuYongJie
d5834de00e refactor(intake): extract sku brand prefix helper 2026-06-10 18:31:05 +08:00
WuYongJie
ea14712dcc refactor(intake): extract local asset detection helper 2026-06-10 18:31:05 +08:00
WuYongJie
d5b5668154 refactor(intake): extract first quality helpers 2026-06-10 18:31:05 +08:00
WuYongJie
8faa646bb0 refactor(intake): extract sample workflow helpers 2026-06-10 18:31:05 +08:00
WuYongJie
8e8c713675 refactor(intake): extract upload category helper 2026-06-10 18:31:05 +08:00
WuYongJie
c282dd5ff1 refactor(intake): extract preview hole helpers 2026-06-10 18:31:05 +08:00
WuYongJie
46c0cfadca refactor(intake): extract component sku helpers 2026-06-10 18:31:05 +08:00
WuYongJie
b8ea7291a6 refactor(intake): extract series component detail helpers 2026-06-10 18:31:05 +08:00
WuYongJie
cbfad8de93 refactor(intake): extract asset sku content helpers 2026-06-10 18:31:05 +08:00
WuYongJie
9f834babda refactor(intake): move non-integrated sku normalization helpers 2026-06-10 18:31:05 +08:00
WuYongJie
eb24df17c9 refactor(intake): move non-integrated style detail initialization 2026-06-10 18:31:04 +08:00
WuYongJie
8ccf70f6a7 refactor(intake): extract non-integrated style detail state 2026-06-10 18:31:04 +08:00
WuYongJie
7dcee03161 refactor(intake): extract non-integrated basic info config 2026-06-10 18:31:04 +08:00
WuYongJie
a5be04560e refactor(intake): extract non-integrated quality inspection helpers 2026-06-10 18:31:04 +08:00
WuYongJie
76feacb340 refactor(intake): extract non-integrated review sheet helpers 2026-06-10 18:31:04 +08:00
WuYongJie
4124f5bac8 refactor(intake): move packaging workspace helpers 2026-06-10 18:31:04 +08:00
WuYongJie
da1b05fe6c merge: sync origin/codex/xuxiake-mainline before promote 2026-06-10 17:00:43 +08:00
WuYongJie
b5a400b5e9 fix(intake): 完善白底图预览与素材库上传链路 2026-06-10 16:42:46 +08:00
WuYongJie
8db587a4b3 fix(intake): 优先主表同步供应商代码 2026-06-10 16:42:28 +08:00
WuYongJie
6cfd123a70 fix(intake): 嵌入式白底图工作区对齐九视图布局 2026-06-10 13:59:07 +08:00
WuYongJie
3afc247c67 fix(intake): 嵌入式白底图工作区对齐九视图布局 2026-06-10 13:30:19 +08:00
WuYongJie
64b715f416 Improve mobile linked intake detail layout 2026-06-10 13:15:18 +08:00
WuYongJie
49c2fe97df fix(intake): preserve linked detail login redirects 2026-06-10 09:03:02 +08:00
WuYongJie
c413220fd0 Merge codex/xuxiake-mainline into main 2026-06-09 20:09:17 +08:00
WuYongJie
bb28d31c4b Limit production build workers 2026-06-09 20:09:01 +08:00
WuYongJie
be00ce9179 Merge codex/xuxiake-mainline into main 2026-06-09 19:40:25 +08:00
WuYongJie
337f3cfa09 Improve mobile linked intake detail layout 2026-06-09 19:39:37 +08:00
WuYongJie
d45f394e3b feat(intake): 独立归档AI白底图结果 2026-06-09 18:10:01 +08:00
WuYongJie
97dd9b5bfe feat(intake): 独立归档AI白底图结果 2026-06-09 18:09:55 +08:00
WuYongJie
faf11a3c15 feat(intake): 独立归档AI白底图结果 2026-06-09 18:08:17 +08:00
WuYongJie
86c27c772c merge: promote external intake role save fix 2026-06-09 17:50:14 +08:00
WuYongJie
2ad2994f22 fix(users): allow external intake business role 2026-06-09 17:49:39 +08:00
WuYongJie
dac76aabbe fix(intake): 稳定白底图源图预览加载 2026-06-09 17:33:54 +08:00
WuYongJie
a962c8d1c7 fix(intake): 稳定白底图源图预览加载 2026-06-09 17:33:07 +08:00
WuYongJie
27d43c3e11 fix(intake): 稳定白底图源图预览加载 2026-06-09 17:30:49 +08:00
WuYongJie
63926a667f merge: promote product tag and optional register job number 2026-06-09 17:10:53 +08:00
WuYongJie
b829efbaab fix: support product tag push and optional register job number 2026-06-09 17:09:25 +08:00
WuYongJie
6e71151104 merge: promote xuxiake mainline ai white background concurrency 2026-06-09 17:00:48 +08:00
WuYongJie
007dd0a986 fix(intake): 支持白底图按角度并发生成 2026-06-09 16:58:38 +08:00
WuYongJie
169d1a6d8b merge: promote xuxiake mainline external wide ready coverage 2026-06-09 16:09:24 +08:00
WuYongJie
99b56d308f Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/wuyongjie-storage-cdn-direct 2026-06-09 16:06:52 +08:00
WuYongJie
59a1106035 fix(external): 补齐宽表对 ready 数据的覆盖 2026-06-09 16:06:10 +08:00
WuYongJie
7276abf589 feat(intake): add external intake role isolation 2026-06-09 13:02:01 +08:00
WuYongJie
3ba9c451a5 feat(intake): support operations product limit workflow 2026-06-09 12:31:23 +08:00
WuYongJie
78a693621e fix: 修正白底图面板空值收窄构建错误 2026-06-09 11:47:23 +08:00
WuYongJie
b430a2cdcb Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/wuyongjie-storage-cdn-direct 2026-06-09 11:36:22 +08:00
WuYongJie
6c2c90989b feat: 接通非集成质检 AI 白底图上传链路 2026-06-09 11:35:51 +08:00
WuYongJie
cac624007c fix(intake): hide abandon product action 2026-06-09 10:29:23 +08:00
WuYongJie
53742ad094 fix(intake): avoid factory permission block on sku supplier save 2026-06-09 09:55:29 +08:00
WuYongJie
0419679c5d Merge remote-tracking branch 'origin/main' into codex/xuxiake-mainline 2026-06-08 18:17:18 +08:00
WuYongJie
9cd10c4b1e refactor(intake): move upload utility helpers 2026-06-08 18:12:41 +08:00
WuYongJie
d8b85d071a refactor(intake): move installation version helpers 2026-06-08 18:08:35 +08:00
WuYongJie
300130983d refactor(intake): move non-integrated sample shipment constants 2026-06-08 18:02:45 +08:00
WuYongJie
0c43017c45 refactor(intake): move non-integrated product factory options 2026-06-08 17:58:23 +08:00
WuYongJie
fa6c0d20aa merge: promote external readonly barcode catalog 2026-06-08 17:56:46 +08:00
WuYongJie
3d46e8d65a feat: align external readonly catalog on barcode key 2026-06-08 17:54:44 +08:00
WuYongJie
ad0081f692 refactor(intake): extract non-integrated workspace panel config 2026-06-08 17:49:31 +08:00
WuYongJie
3a8de95bf1 refactor(intake): extract non-integrated planning layout config 2026-06-08 17:42:15 +08:00
WuYongJie
7a84f14f8d refactor(intake): move non-integrated sku ui config 2026-06-08 17:32:40 +08:00
WuYongJie
2d29a50e2b refactor(intake): extract non-integrated sku field config 2026-06-08 17:27:04 +08:00
WuYongJie
0c1b6c1dc2 refactor(intake): extract non-integrated bom display config 2026-06-08 17:20:34 +08:00
WuYongJie
5732a1ab39 refactor(intake): extract non-integrated opening reference helpers 2026-06-08 17:14:20 +08:00
WuYongJie
af03f16690 refactor(intake): extract non-integrated document workspace helpers 2026-06-08 17:02:15 +08:00
WuYongJie
f33659a893 refactor(intake): extract non-integrated value helpers 2026-06-08 16:43:20 +08:00
WuYongJie
6cd4ab5a60 refactor(intake): extract non-integrated style file helpers 2026-06-08 16:38:59 +08:00
WuYongJie
c37e80fb02 refactor(intake): extract non-integrated detail helpers 2026-06-08 16:33:46 +08:00
WuYongJie
af2b5640c8 fix(intake): keep sku supplier ime input responsive 2026-06-08 15:53:24 +08:00
WuYongJie
71598ec0f3 refactor(intake): extract non-integrated sku material field 2026-06-08 15:45:11 +08:00
WuYongJie
0443b33705 refactor(intake): extract non-integrated sku barcode field 2026-06-08 15:39:33 +08:00
WuYongJie
c2f8cee9f0 refactor(intake): extract non-integrated bom image field 2026-06-08 15:33:22 +08:00
WuYongJie
ab9cd0fad5 refactor(intake): extract non-integrated sku directory fields 2026-06-08 15:26:39 +08:00
WuYongJie
922dbfa0bf refactor(intake): extract non-integrated document workspace controls 2026-06-08 15:04:11 +08:00
WuYongJie
a8598a1d80 refactor(intake): extract non-integrated assembly thumbnails 2026-06-08 14:54:09 +08:00
WuYongJie
651342e49d refactor(intake): extract non-integrated sku row fields 2026-06-08 14:44:02 +08:00
WuYongJie
1ca244a116 refactor(intake): extract non-integrated brand field 2026-06-08 14:23:48 +08:00
WuYongJie
fdf5d19480 refactor(intake): extract non-integrated upload field 2026-06-08 14:09:56 +08:00
WuYongJie
e52ad139f9 refactor(intake): extract non-integrated preview panels 2026-06-08 13:49:52 +08:00
WuYongJie
ff2d99ae70 refactor(intake): extract non-integrated shared controls 2026-06-08 13:33:56 +08:00
WuYongJie
d7b47c21f5 feat(intake): support sku group modular order workflow 2026-06-08 12:27:48 +08:00
WuYongJie
5adaec1376 fix(intake): keep sku ime input stable 2026-06-08 12:03:31 +08:00
WuYongJie
f3140692ff merge: promote xuxiake mainline storage object-first hotfix 2026-06-08 11:40:01 +08:00
WuYongJie
2239b7b186 merge: promote storage object-first legacy asset hotfix 2026-06-08 11:38:36 +08:00
WuYongJie
7a5dce6988 fix(storage): 非集成历史素材改为对象存储优先 2026-06-08 11:32:25 +08:00
WuYongJie
7835d69c9f fix(intake): allow payload-backed design owner sku image saves 2026-06-08 10:08:13 +08:00
WuYongJie
f5a6005b3e merge: promote xuxiake mainline qiniu cdn direct read rollout 2026-06-06 22:13:36 +08:00
WuYongJie
27ba97800f merge: promote xuxiake mainline qiniu cdn direct read rollout 2026-06-06 22:12:09 +08:00
WuYongJie
768b988843 feat: 切换对象读取到 CDN 直连签名跳转 2026-06-06 21:49:26 +08:00
WuYongJie
e8933ab8ac Merge codex/xuxiake-mainline jushuitan inventory skip fix 2026-06-06 18:08:23 +08:00
WuYongJie
7a3fd5c42f fix: treat non virtual inventory warehouses as skipped 2026-06-06 18:07:03 +08:00
WuYongJie
05f1b0af3a Merge remote-tracking branch 'origin/main' into codex/xuxiake-mainline 2026-06-06 14:21:44 +08:00
WuYongJie
5d31269736 feat: support modular draft SKU product flow 2026-06-06 14:21:35 +08:00
WuYongJie
aeed70f90f Merge codex/xuxiake-mainline formal jushuitan credentials 2026-06-06 10:20:58 +08:00
WuYongJie
a0eb15b980 fix: parse formal jushuitan credentials 2026-06-06 10:20:32 +08:00
WuYongJie
8ca0691e02 Merge codex/xuxiake-mainline jushuitan copy fix 2026-06-06 09:56:00 +08:00
WuYongJie
b75de86db1 fix: remove remaining jushuitan test copy 2026-06-06 09:55:37 +08:00
WuYongJie
d09a201a6a Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into merge-jst-copyfix 2026-06-06 09:47:18 +08:00
WuYongJie
77ac8f06c0 fix: remove jushuitan test environment copy 2026-06-06 09:45:03 +08:00
WuYongJie
840236610c merge: promote modular sku draft workflow 2026-06-06 09:23:10 +08:00
WuYongJie
575d5aa039 Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/xuxiake-mainline 2026-06-06 09:18:26 +08:00
WuYongJie
467e4182f2 feat: restore modular sku group draft workflow 2026-06-06 09:18:07 +08:00
WuYongJie
4b8e1d754a Revert "fix(intake): allow assigned purchaser pricing push"
This reverts commit d3093aead7fae7726c3102236fbd40b4df11d8f9.
2026-06-06 09:11:25 +08:00
WuYongJie
d3093aead7 fix(intake): allow assigned purchaser pricing push 2026-06-06 09:09:31 +08:00
WuYongJie
338bfb83e7 fix(intake): remove odmq factory production option 2026-06-05 18:38:04 +08:00
WuYongJie
a5f2fd01dd fix(intake): remove odmq factory production option 2026-06-05 18:35:51 +08:00
WuYongJie
59a52c8b95 merge: promote single sku master data guard 2026-06-05 18:31:56 +08:00
WuYongJie
f3c2b9bbee fix(intake): prevent single sku master data gaps 2026-06-05 18:29:01 +08:00
WuYongJie
369cf8ecf4 fix(intake): add factory shipping method 2026-06-05 18:14:00 +08:00
WuYongJie
1892a5d579 fix(intake): add factory shipping method 2026-06-05 18:09:20 +08:00
WuYongJie
805f8db179 merge: promote xuxiake non-integrated foundation 2026-06-05 18:03:55 +08:00
WuYongJie
b2837c6426 merge: sync non-integrated upload permission foundation 2026-06-05 17:50:57 +08:00
WuYongJie
0765c0596b Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/xuxiake-non-integrated-upload-permission 2026-06-05 17:47:26 +08:00
WuYongJie
f620762a4b fix(master-data): prevent dirty intake sync regressions 2026-06-05 16:25:13 +08:00
WuYongJie
86df630894 fix(master-data): scope shadow env to audit diff steps 2026-06-05 16:03:25 +08:00
WuYongJie
743d480b7c fix(intake): prefer sku images in product limits 2026-06-05 15:49:20 +08:00
WuYongJie
2194824e6c fix(intake): keep imported sku rows editable 2026-06-05 15:33:16 +08:00
WuYongJie
d650e2f562 fix(intake): match sku images by displayed spu 2026-06-05 15:16:29 +08:00
WuYongJie
cfc3e5cc0e fix(intake): 修复非集成图片上传节点权限 2026-06-05 14:59:29 +08:00
WuYongJie
0c2b05ce74 fix(intake): skip unmatched sku image autosave 2026-06-05 14:58:40 +08:00
WuYongJie
704a0b91ac fix(master-data): align merged boundary debt budget 2026-06-05 13:34:44 +08:00
WuYongJie
61da20e9b6 merge: sync data foundation into xuxiake mainline 2026-06-05 13:29:18 +08:00
WuYongJie
4f2414ef5c revert: defer asset sku group workflow for data foundation 2026-06-05 13:29:08 +08:00
WuYongJie
855ab98ce5 fix(master-data): avoid readonly quality false blockers 2026-06-05 12:40:43 +08:00
WuYongJie
54ef4c79f3 chore(master-data): register retired asset sku group migration 2026-06-05 12:35:33 +08:00
WuYongJie
839269ab8e fix(intake): defer autosave during ime input 2026-06-05 12:28:54 +08:00
WuYongJie
d38a373297 chore(master-data): retire legacy asset sku group drift 2026-06-05 12:27:56 +08:00
WuYongJie
fc6241ff46 fix(asset-center): backfill sku groups by goods code 2026-06-05 12:18:39 +08:00
WuYongJie
26e40b0e71 chore(master-data): 收紧资产规格款身份真相 2026-06-05 12:08:29 +08:00
WuYongJie
6cc339cdb5 Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/xuxiake-mainline 2026-06-05 12:07:29 +08:00
WuYongJie
cc7f3c4b8e feat(asset-center): support sku group asset workflow 2026-06-05 12:05:57 +08:00
WuYongJie
4737184f59 fix(intake): scope derived downstream saves 2026-06-05 11:59:09 +08:00
WuYongJie
8e0addb76c chore(master-data): 加强资产SKU同步副本巡检 2026-06-05 11:56:04 +08:00
WuYongJie
74cf5b0005 chore(master-data): 补齐聚水潭归档真相源矩阵 2026-06-05 11:44:52 +08:00
WuYongJie
3da51e926b chore(master-data): 增加聚水潭残留软归档脚本 2026-06-05 11:38:41 +08:00
WuYongJie
4974869f6a chore(master-data): 增加聚水潭分类匹配计划导出 2026-06-05 10:34:22 +08:00
WuYongJie
16d7f42506 fix(intake): allow sku details after spu assignment 2026-06-05 10:30:59 +08:00
WuYongJie
4ca58ffaa2 chore(master-data): 导出非集成产品分类审阅清单 2026-06-05 09:16:02 +08:00
WuYongJie
e26f958de0 fix(jst): 补齐存量SKU主数据兜底 2026-06-04 22:37:15 +08:00
WuYongJie
b4f088d70a fix(asset-center): 修复生产缩略图渲染域名来源 2026-06-04 21:32:19 +08:00
WuYongJie
3cd1102cd2 fix(data-foundation): 对齐治理应用迁移历史门禁 2026-06-04 20:20:31 +08:00
WuYongJie
d6ac21db0e fix(data-foundation): 收紧资产同步锁定字段门禁 2026-06-04 18:11:34 +08:00
WuYongJie
7459f2aef1 fix(data-foundation): 纳入资产同步事件积压门禁 2026-06-04 18:00:35 +08:00
WuYongJie
67bdc09e1e fix(data-foundation): 补齐生产只读证据验收门禁 2026-06-04 17:52:12 +08:00
WuYongJie
90bfa4b968 fix(data-foundation): 补齐生产只读地基巡检入口 2026-06-04 17:30:11 +08:00
WuYongJie
92af7ea29a fix(data-foundation): 识别已完成迁移链的 no-op 审计状态 2026-06-04 17:08:14 +08:00
WuYongJie
40e5f238a4 fix(data-foundation): 稳定历史对象源检查 2026-06-04 16:24:56 +08:00
WuYongJie
5bed29f6e9 fix(data-foundation): 纳入历史对象存储迁移门禁 2026-06-04 16:00:56 +08:00
WuYongJie
6f09b28196 fix(data-foundation): 加强历史素材迁移门禁 2026-06-04 15:47:55 +08:00
WuYongJie
7f41d0bca5 fix: preserve component detail spu on stock conflicts 2026-06-04 15:33:10 +08:00
WuYongJie
5de6de67e6 fix(data-foundation): 收口主数据边界 readiness 2026-06-04 15:25:17 +08:00
WuYongJie
e4809ef843 fix: sync configured component detail spu before opening 2026-06-04 15:15:30 +08:00
WuYongJie
d829ea6c05 fix: allow product owners to upload sku assets 2026-06-04 14:55:43 +08:00
WuYongJie
8f6f61858e fix: avoid duplicate default asset series seed 2026-06-04 14:28:53 +08:00
WuYongJie
cbe545f3e3 fix(data-foundation): 区分已恢复的回滚迁移记录 2026-06-04 14:18:50 +08:00
WuYongJie
d9db45ea3a fix: allow editing sku spec names 2026-06-04 14:04:08 +08:00
WuYongJie
6416408031 fix(data-foundation): 区分已恢复的回滚迁移记录 2026-06-04 13:58:46 +08:00
WuYongJie
f5d8f450b3 fix: sync configured series spu coding 2026-06-04 13:40:05 +08:00
WuYongJie
849e74118d fix: compress intake image uploads before submit 2026-06-04 12:17:05 +08:00
WuYongJie
01eb1647bc fix: allow packaging variants without real model 2026-06-04 10:09:33 +08:00
WuYongJie
5cb25981ef fix: prevent opening codes from becoming formal spu 2026-06-04 10:03:48 +08:00
WuYongJie
2ccaab0e72 fix: preserve material storage texture urls 2026-06-04 09:06:49 +08:00
WuYongJie
8d0930a5b2 fix(storage): 跳过未迁历史快照对象检查 2026-06-03 22:10:06 +08:00
WuYongJie
2764ac1e31 fix(storage): 阻止源缺失时执行历史素材迁移 2026-06-03 21:55:05 +08:00
WuYongJie
aeb6919da6 Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/xuxiake-mainline 2026-06-03 21:41:00 +08:00
WuYongJie
79106b69da feat(storage): 支持七牛历史素材迁移 2026-06-03 21:40:13 +08:00
WuYongJie
d16a2a201a fix: improve material upload and category deletion 2026-06-03 18:08:20 +08:00
WuYongJie
14db8a7816 feat: prevent duplicate assisted assembly specs 2026-06-03 18:08:09 +08:00
WuYongJie
252c744ddd fix(asset-center): scope hardware families by owning series 2026-06-03 17:13:40 +08:00
WuYongJie
b6c2a8ec62 fix(asset-center): uppercase deleted variant suffix 2026-06-03 16:53:37 +08:00
WuYongJie
b22ff2084c fix(asset-center): normalize variant soft delete marker 2026-06-03 16:36:34 +08:00
WuYongJie
91340f5d5f Merge remote-tracking branch 'origin/main' into codex/xuxiake-mainline 2026-06-03 15:52:33 +08:00
WuYongJie
6a362bcdde feat(data-foundation): sync draft packaging and series cleanup 2026-06-03 15:52:05 +08:00
WuYongJie
67ff6fba96 Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/xuxiake-mainline 2026-06-03 14:10:55 +08:00
WuYongJie
12a0f26bb7 merge: sync xuxiake data foundation for prod deploy 2026-06-03 13:16:16 +08:00
WuYongJie
2ca459e027 fix(data-foundation): 结构化核价行并归一化素材引用 2026-06-03 12:10:14 +08:00
WuYongJie
de045f4a56 Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/xuxiake-mainline 2026-06-03 11:12:47 +08:00
WuYongJie
e2a6dd1487 test(data-foundation): 收紧客户端缓存边界证据门禁 2026-06-03 10:46:06 +08:00
WuYongJie
b086e02dcd fix(data-foundation): 补齐对象存储外部契约与迁移历史 2026-06-03 10:31:21 +08:00
WuYongJie
4e5939071a merge: sync material foundation into xuxiake mainline 2026-06-03 10:26:08 +08:00
WuYongJie
5f2033ea90 wip: protect local material updates before mainline sync 2026-06-03 10:04:59 +08:00
WuYongJie
02ce03aeaa merge: sync storage model proxy fix for prod deploy 2026-06-03 09:28:53 +08:00
WuYongJie
8fd40e0846 fix(storage): 通过同源代理加载对象存储模型 2026-06-02 22:28:21 +08:00
WuYongJie
e41e93ede8 merge: sync xuxiake normalized migration fix for prod deploy 2026-06-02 22:04:18 +08:00
WuYongJie
73655af744 fix(data-foundation): 对齐非集成 SKU 规范化迁移预检 2026-06-02 22:03:56 +08:00
WuYongJie
b77fcb3351 merge: sync xuxiake data constraint fix for prod deploy 2026-06-02 21:55:22 +08:00
WuYongJie
6fee15d4aa fix(data-foundation): 放行非集成 SKU 冲突质量状态约束 2026-06-02 21:54:11 +08:00
3720 changed files with 633402 additions and 76634 deletions

217
AGENTS.md
View File

@ -1,102 +1,167 @@
# AGENTS.md
适用范围:`D:\AI Code\3danzhuang-wuyongjie-mainline` 全仓库。
未单独说明时,先遵循全局规则:`C:\Users\54972\.codex\AGENTS.md`。
这份文件只补充本仓库特有规则,不重复全局通用要求。
未单独说明时,先遵循当前 agent 自己的全局规则(ZCode:`C:\Users\54972\.zcode\v2\AGENTS.md`;Codex:`C:\Users\54972\.codex\AGENTS.md`)。
这份文件只记录本仓库当前有效的补充规则和核查入口,不保存密码、密钥、数据库连接串或对象存储密钥。
重要边界:`D:\AI Code\3danzhuang` 是历史旧现场;当前正式工作目录以 `D:\AI Code\3danzhuang-wuyongjie-mainline` 为准,除非用户明确要求,不要混用旧目录。
重要边界:`D:\AI Code\3danzhuang` 是历史旧现场;当前正式工作目录以 `D:\AI Code\3danzhuang-wuyongjie-mainline` 为准。除非用户明确要求,不要混用旧目录、旧服务器别名、旧部署脚本或旧截图结论。
## 1. 默认执行口径
## 1. 当前项目真相
- 用户未指定目录时,默认在 `furniture-3d-system/` 执行。
- 当任务发生在 `furniture-3d-system/` 时,默认按当前会话直接开发。
- 常规任务分支在完成必要验证、确认范围和目标后,可以按团队协作标准流程主动 `commit / push`。
- `main`、生产发布、破坏性操作、删除数据、改远端历史等高风险动作仍必须由用户明确授权。
- 禁止破坏性操作,除非用户明确授权。
- 没有“规范审查结论”,任务不算完成。
- 外层目录 `D:\AI Code\3danzhuang-wuyongjie-mainline` 是 Git 仓库根目录。
- 主应用目录是 `furniture-3d-system/`;用户未指定目录时,默认在这里执行代码、测试、构建和脚本。
- 技术栈以当前 `furniture-3d-system/package.json` 为准(核心:Next.js App Router + React + TypeScript + Prisma/PostgreSQL + Jest + Playwright + Tailwind CSS + Three.js/R3F;另有 ioredis、puppeteer、exceljs/xlsx、zod、zustand 等)。package.json 变动后文档不自动同步,以实时核查为准。
- `CLAUDE.md`、旧 README、`docs/历史归档/` 和早期部署文档里可能仍有旧苹果风格、MinIO、旧 Docker 部署、旧模型数量、旧 Product/SKU 口径等信息;只能当历史参考,不能覆盖当前代码、workflow 文档和实时命令结果。
- 工作树可能有用户或其他协作者留下的未跟踪文件、日志、贴图和本地产物。只处理本次任务明确相关的文件,不要顺手清理或回退无关现场。
## 2. 三条硬规则
## 2. 开工前固定入口
- 必须符合企业级架构标准。
- 必须符合统一视觉标准。
- 必须优先做根因修复,不能拿补丁冒充完成。
## 3. 固定协作入口
当任务发生在 `furniture-3d-system/` 时,人和 AI 都必须先读取下面 3 份协作文档,再开始改代码:
当任务发生在 `furniture-3d-system/` 时,改代码前必须先读这 3 份当前协作入口:
- `furniture-3d-system/docs/workflow/团队协作模式.md`
- `furniture-3d-system/docs/workflow/模块边界说明.md`
- `furniture-3d-system/docs/workflow/主线同步操作手册.md`
如果任务和包装协作有关,再额外读取:
按任务类型额外读取:
- `furniture-3d-system/docs/workflow/同事交接模板.md`
- `furniture-3d-system/docs/workflow/同事本地AGENTS模板.md`
- `furniture-3d-system/docs/开发指南/包装方案增强/新平台包装底座-同事AI对接说明-20260331.md`
- 生产、部署、线上排障:`furniture-3d-system/docs/workflow/生产部署入口说明.md`,并核对 `scripts/deploy-yydd-prod-from-git.ps1` 与其调用的 `scripts/create-yydd-prod-deploy.ps1` 当前默认参数。
- 前端、视觉、交互:`furniture-3d-system/docs/设计规范/白曜石工坊统一视觉标准.md` 和 `furniture-3d-system/docs/workflow/规范审查清单.md`。
- 包装、仓库、轻仓库协作:再读包装/仓库相关交接文档和 `src/app/warehouse`、`src/app/api/warehouse`、`src/app/api/pre-skus/[id]/packaging/route.ts` 的当前实现。
- 数据库、主数据、迁移:优先读 `docs/架构设计/内部主数据巡检基线-20260529.md`、`docs/架构设计/数据库混乱点治理台账.md` 和相关 `db:foundation:*` 脚本说明。
## 4. 协作与分支口径
如果文档和代码冲突,按这个顺序判断:当前代码和脚本、当前线上/命令返回、当前 workflow 文档、历史文档或旧记忆。
- `main` 是最终发布主线,不能随便直接推;生产发布必须另行确认。
- `origin/codex/xuxiake-mainline` 是当前共享开发主线和默认同步基线。
## 3. Git 与主线协作
- `main` 是最终发布主线,由 `WuYongJie` 收口;不要直接把半成品推到 `main`。
- `origin/codex/xuxiake-mainline` 是日常共享开发主线和默认同步基线。
- 新功能、优化和修复默认从最新 `origin/codex/xuxiake-mainline` 开任务分支,再按协作流程回流。
- 新分支统一使用 `codex/<负责人>-<工作域>` 或 `codex/<负责人>-<工作域>-<任务简称>`。
- 不再使用 `feature/packaging`、`packaging/...`、`codex/xuxiake-dev` 这类泛分支名。
- 长期共享分支同步当前共享开发主线时固定使用 `merge`。
- 临时个人任务分支只有在确认无人共用时才使用 `rebase`。
- 新分支统一使用 `codex/<负责人>-<工作域>` 或 `codex/<负责人>-<工作域>-<任务简称>`,不要再新建 `feature/...`、`packaging/...`、`dev`、`codex/xuxiake-dev` 这类泛分支名。
- 长期共享分支同步共享开发主线时固定使用 `merge origin/codex/xuxiake-mainline`;只有确认无人共用的临时个人任务分支才使用 `rebase origin/codex/xuxiake-mainline`。
- 每次开工或接续旧任务时,先看 `git status --short --branch`;涉及主线同步时再看 `git rev-list --left-right --count HEAD...origin/codex/xuxiake-mainline`,不要把“当前所在分支”误当成“已同步共享主线”。
- 用户问“xuxiake 主线有没有新提交”时,必须先 `git fetch origin`,再基于最新远端状态回答。
- 不要在脏工作树上直接 merge/rebase。先看 `git status --short`,分清本次改动、用户改动、本地产物和无关未跟踪文件。
- 常规任务完成必要验证后,可以按协作流程推进 `commit / push`;`main`、生产发布、破坏性操作、删除数据、改远端历史等高风险动作必须有用户明确授权。
## 5. 白曜石工坊视觉口径
### 主线部署标准流程(每次"主线部署"都走这一套)
- `furniture-3d-system/docs/设计规范/白曜石工坊统一视觉标准.md` 是当前新平台第一视觉标准。
- 新平台主链页面,以及 `/admin`、`/users` 等后台管理页面,默认都要向白曜石工坊口径统一。
- 白曜石工坊默认是浅色工作台;不再提供深浅色主题切换入口,也不要把后台页面继续做成默认黑底。
- 页面应保持工作台感、低噪音、克制、层级清楚,不能退回普通 SaaS 卡片墙或营销式大卡片布局。
- `Inspector(检视器)`、数据列表、表格、长文本和表单密集区必须优先保证可读性,不能用毛玻璃承载正文。
- 毛玻璃只允许用于顶栏、HUD、浮动工具条和轻量状态浮层。
- 板材、五金、组件、包装等资产应按 `Meta Component(元组件)` 表达,而不是普通图片卡片。
- 苹果风格视觉标准已降级为历史参考,不再作为新平台主链、后台管理页面、弹窗或验收的视觉依据。
本仓库有两根主线,都不直接提交:`main` 是生产部署唯一来源(由 `WuYongJie` 收口),`origin/codex/xuxiake-mainline` 是共享开发次主线(日常回流基线)。
## 6. 前端责任口径
每次用户要求"主线部署",严格按以下顺序,不跳步、不直接支线 → main:
- 未明确指定其他协作者时,前端、视觉、交互、`3D(立体)` 表现层和系统联调默认由当前 Codex 全权负责。
- 前端实现必须遵守白曜石工坊视觉标准、模块边界和最小验证要求。
- 处理前端页面时,优先用当前代码、实际页面表现、截图结论和设计规范判断,不再套用旧模型时期的保守分工。
- 复杂任务可以主动拆计划、并行排查和验证,但不能因为模型更强就扩大改动范围或跳过规范审查。
1. **在支线开发** → 提交到任务分支(如 `codex/wuyongjie-xxx`),不碰两根主线。
2. **fetch xuxiake** → 必须先 `git fetch origin`,看 `origin/codex/xuxiake-mainline` 有没有新提交。
3. **有新提交** → `merge origin/codex/xuxiake-mainline` 拉下来处理冲突(长期共享分支用 merge,不 rebase);只处理冲突文件,不扩范围。
4. **合并到 xuxiake** → 把支线合进 `origin/codex/xuxiake-mainline`,并 `push` 更新次主线。
5. **合并到 main** → 把 xuxiake 合进 `main`,并 `push origin main`(本步需用户授权)。
6. **用 main 部署** → 走 `scripts/deploy-yydd-prod-from-git.ps1`(默认源 `origin/main`),部署后核对 commit / 进程 / 端口 / 日志 / 关键页面(本步需用户授权)。
## 7. 截图和上下文续接
发布前必须确认 `git ls-remote origin refs/heads/main`、本地 `origin/main`、目标部署 commit 三者一致。
- 工程对话可以正常使用截图,但截图只用于当前判断。
- 压缩上下文、写会话恢复摘要或给新对话续接时,只保留文字结论、关键决策、当前状态和下一步。
- 不继承、不复述 `data:image base64(图片编码文本)`,也不要把图片原始内容写进摘要。
- 如果同一问题需要连续大量截图,优先开短图像对话处理,最后只把文字结论合回主工程对话。
## 4. 生产与部署
## 8. 敏感信息和交接材料
- 当前正式网址:`https://cp.jiansun.vip/`。
- 当前权威生产 SSH 入口:`root@ltai5tdb2ogyj59fquyedh.yydd.ai`,端口 `60022`。
- 当前生产部署目录:`/opt/furniture-3d-system-prod`。
- 当前 PM2 进程名:`furniture-3d-system-prod`。
- 当前应用本机端口:`3001`。
- 默认生产部署来源:`origin/main`。
- 当前主部署入口是外层 `scripts/deploy-yydd-prod-from-git.ps1`;它默认部署 `origin/main`,并调用 `scripts/create-yydd-prod-deploy.ps1` 生成发布包/远端发布脚本。不要把下层打包脚本单独当成唯一部署入口。
- 发布前必须确认 `git ls-remote origin refs/heads/main`、本地 `origin/main`、目标部署 commit 三者一致;本地 `main` 是否领先或落后远端不能替代 `origin/main` 的真实发布来源。
- 旧 `192.168.58.20`、本机泛别名 `prod`、旧 `gongxiangp2 / youyidian-3d-material` 生产信息都不能套用到本项目。
- 生产动作前至少实时确认:目标主机、远端目录、PM2 进程、`current` 软链接、监听端口和 `https://cp.jiansun.vip/api/health`。
- 生产数据库、迁移、主数据地基相关动作先只读核查;需要写入时必须有备份、目标库身份、确认口令和可回滚路径。
- 对象存储当前要按 `src/lib/storage.ts`、`storage:*` 脚本、七牛迁移/GC 文档和生产环境变量实时核对;不要把历史 MinIO/S3 备份脚本当成当前生产存储真相。
- 账号、密码、服务器地址、访问密钥、数据库连接、S3/MinIO 密钥等敏感信息,只能放在本地续接材料里。
- 含敏感信息的摘要、日志或交接文件不能提交 Git,不能发给同事,不能写进共享文档。
## 5. 当前主链与模块边界
当前新平台数据主链按这条线理解:
`资产中心 -> 辅助装配 -> 草稿组 -> 产品资料/正式入库 -> 仓库管理/轻仓库`
当前全局工作台导航还包含首页、OA 新品流程看板、仓库质检、AI 出图工作台等运营入口。导航入口不等于数据主链,不要把页面入口和主数据归属混成一件事。
主链默认入口和目录:
- 首页与全局壳:`src/app/page.tsx`、`src/components/layout`
- 资产中心:`src/app/asset-center`、`src/app/api/asset-center`
- 辅助装配:`src/app/assisted-assembly`、`src/app/api/assisted-assembly`
- 草稿组:`src/app/draft-groups`、`src/app/api/draft-groups`
- 产品资料/正式入库:`src/app/intake`、`src/app/api/intake`
- 仓库管理/轻仓库:`src/app/warehouse`、`src/app/api/warehouse`
- 仓库质检:`src/app/warehouse-quality`、`src/app/api/warehouse-quality`
- AI 出图工作台:`src/app/ai-render-workbench`
- 审批工作流:`src/app/approval-workflows`
- 素材库:`src/app/materials`
- 移动端:`src/app/mobile`
- 平台组件库:`src/components/platform`
- 主链规则与类型:`src/lib/open-product`、`src/lib/planning`
业务真相:
- 资产族:`AssetFamily`
- 规格款:`AssetVariant`
- 草稿组:`DraftGroup`
- 准 SKU:`PreSku`
- 入库结果:正式 `Product / ProductSKU`(`SKU` 是业务概念,Prisma 模型名是 `ProductSKU`)
- 产品资料侧非集成开品:`IntakeNonIntegratedProduct / IntakeNonIntegratedSkuItem`
旧 CAD 兼容链继续存在,但不是新平台主数据源:
- 页面:`src/app/products`、`src/app/products/[id]`、`src/app/products/[id]/assembly`、`src/app/products/[id]/skus/[skuId]`
- 旧链模型和服务包括 `Product / ProductSKU / Panel / Hole / ProductHardware`、`assembly.service`、`panel.service`、`hole.service` 等。
- 旧链可以继续服务历史产品、CAD 导入、装配步骤和兼容发布后能力,但不要反向主导资产中心、草稿组或产品资料主链。
共享接缝默认谨慎处理:
- `prisma/schema.prisma`
- `prisma/migrations`
- `src/app/layout.tsx`
- `src/app/intake`
- `src/app/draft-groups/[id]`
- `src/app/api/platform/schemas.ts`
- `src/server/services/draft-group.service.ts`
- `src/server/services/intake.service.ts`
- `src/server/repositories/draft-group.repository.ts`
改这些地方前先说明原因、影响范围和验证计划;不要为了局部 UI 或临时兼容绕过模块边界。
## 6. 架构硬规则
- API Route 负责收参、鉴权和返回,不直接堆业务规则或 Prisma 查询。
- Service 负责业务规则、权限校验、事务编排、缓存和跨模块协调。
- Repository 负责数据访问。
- 任何新接口必须有明确输入输出 DTO/校验口径。
- 不是必须的话,不要使用兜底逻辑;原生主逻辑能做好,就先把主逻辑、状态契约和数据来源修正确。兜底只能用于明确的兼容、降级或应急场景,不能用来掩盖本应修好的业务规则、接口契约或前端状态问题。
- 任何 Prisma 模型调整必须带迁移,并补齐主数据/迁移核查;生产/准生产禁止用 `prisma db push` 替代迁移。
- 影响命名、BOM、包装、安装步骤、SKU、barcode、批次、入库承接的数据问题,优先从数据源和业务规则修,不在页面上压报错或绕校验。
- SPU、SKU、barcode、批次不能混用:SPU 是父级产品款,SKU 是 SPU 下的具体子项(Prisma 模型名 `ProductSKU`),barcode 是绑定到最终 SKU 的业务定位值;基于某 SKU 派生的新 SKU 必须仍属于同一 SPU,并按业务要求继承或明确指定批次。
## 7. 前端与视觉硬规则
- 白曜石工坊统一视觉标准是本项目新平台第一视觉标准;默认是浅色工作台,不再提供深浅色主题切换入口。
- 本项目有自己的组件库与 `Meta Component(元组件)` 体系;前端实现必须优先复用 `src/components/platform`、资产中心已沉淀的工作台表达和现有元组件,不允许绕开组件库另起一套孤立 UI。
- 当用户指定 Stitch 设计稿或已有 Stitch 设计稿可参考时,Stitch 是当前功能的视觉规范与验收标准;实现方式是在本项目组件库/元组件体系内对齐 Stitch 的密度、层级、控件形态、留白、字号、圆角、阴影和低噪音表达,而不是无视组件库硬抄静态稿。
- 资产中心页面是当前项目内的优先参考样板;涉及资产、图片、SKU、AI 白底图、3D 预览、工作区和检视器时,默认参考资产中心的工作台感、元组件表达、低噪音操作面板和重点图片区。
- `Inspector(检视器)`、数据列表、表格、长文本和表单密集区必须优先保证可读性;毛玻璃只允许用于顶栏、HUD、浮动工具条和轻量状态浮层,不承载正文。
- 板材、五金、组件、包装等资产必须按元组件表达,不能退回普通图片卡片或营销式大卡片布局。
- 现有 `apple-*` token、深色 `ObsidianControlShell`、历史主题 CSS 只能作为兼容遗留;新功能不得以它们作为视觉方向。
- 前端改造不得把现有后台习惯、普通 SaaS 审美或共享组件默认皮肤当成最终依据;如果组件库默认视觉和 Stitch/资产中心标准冲突,应在组件库体系内做专用变体、重写皮肤或局部重构。
- 没有出现在设计稿、组件规范或业务必要路径里的说明文案、辅助卡片、重复统计块、解释区,不要为了“更易懂”自行添加;工作区必须抓重点,图片类功能要让图片和关键动作成为第一层级。
- 涉及前端动画、入退场、滚动联动、时间线、拖拽、SVG 或动效性能时,先判断是否有助于理解和操作;需要时优先按技术栈调用对应 GSAP Skill,并继续遵守 Impeccable。React 必须使用 `useGSAP` 或带 `gsap.context().revert()` 的完整清理,滚动相关实例必须销毁并在布局变化时正确刷新;优先动画 `transform/opacity`,尊重 `prefers-reduced-motion`,不得为装饰滥用动效。
- 前端修改后优先用真实页面、截图、1920×1080 视口和必要的移动端视口验证,不只看代码。
## 8. 验证、收口与规范审查
- 修改后必须做最小但有效的验证;不能验证就明确说明未验证原因和风险。
- 常用验证命令以当前 `package.json` 为准,优先选和改动范围匹配的最小集合:`npm run type-check`、`npm run build`、`npm test -- <相关测试>`、`npm run lint`;对象存储用 `npm run storage:gc:audit` / `npm run storage:gc:apply`,主数据用 `npm run db:foundation:*` 相关只读或授权脚本。
- 前端体验类改动要结合浏览器实际操作和截图验证;涉及 3D/canvas 时要确认画面非空、尺寸正确、交互可用。
- 生产验证必须包含真实健康检查和日志/PM2/端口等现场证据,不能只说"本地构建通过"。
- 规范审查固定只查三件事:架构有没有被破坏、视觉有没有跑偏、这次是不是根因修复。结论只能是 `passed` / `passed with warnings` / `rejected`;只要架构违规、视觉违规或明显补丁式修复,就必须 `rejected`。非独立复审时必须写明"这是自审,不是独立复审"。统一清单:`furniture-3d-system/docs/workflow/规范审查清单.md`。没有规范审查结论,任务不算完成。
## 9. 敏感信息和交接
- 账号密码、访问密钥、数据库连接串、对象存储密钥等敏感信息只能放在本地安全材料里,不写入 Git、共享文档、普通交接文本或长期记忆;生产域名、PM2 名称、部署目录这类非密钥运行入口可以写进仓库文档,但必须保持最新。
- 仓库内协作文档只写非敏感入口、流程和职责;凭据类信息必须用 `[REDACTED_SECRET]` 或本地路径说明替代。
- 发现摘要文件里混有明文凭据时,默认先提示风险,再决定是否拆成本地敏感版和仓库协作版。
## 9. 规范审查
规范审查固定只查三件事:
- 架构有没有被破坏
- 视觉有没有跑偏
- 这次是不是根因修复
结论只能是:
- `passed`
- `passed with warnings`
- `rejected`
只要架构违规、视觉违规,或明显是补丁式修复,就必须 `rejected`。
如果这次不是独立复审,必须明确写出“这是自审,不是独立复审”。
统一清单:`furniture-3d-system/docs/workflow/规范审查清单.md`
## 10. 标准来源
- `furniture-3d-system/docs/设计规范/白曜石工坊统一视觉标准.md`
- `furniture-3d-system/docs/workflow/规范审查清单.md`
- 发现摘要、日志或交接文件混有明文凭据时,先提示风险,再决定是否拆成本地敏感版和仓库协作版。
- 给下一个 agent 或同事交接时,写清:本次只改了什么、没改什么、验证跑了什么、风险是什么、下一步要看哪些文件。

View File

@ -0,0 +1,187 @@
# 本地运行
NODE_ENV=development
NEXT_PUBLIC_APP_URL=http://localhost:3000
# 数据库
DATABASE_URL=postgresql://USER:PASSWORD@HOST:5432/DB_NAME?schema=public
# 员工工时花名只读人员配置来源(可选;开发环境配置后会合并正式人员配置)
SUPPLY_CHAIN_PRODUCT_ARCHIVE_READ_DATABASE_URL=postgresql://READ_USER:READ_PASSWORD@READ_HOST:5432/READ_DB
SUPPLY_CHAIN_PRODUCT_ARCHIVE_READ_DATABASE_SSL_REJECT_UNAUTHORIZED=true
# 产品全量档案的聚水潭 SQL Server 只读来源
JST_REPORT_DB_HOST=
JST_REPORT_DB_PORT=1433
JST_REPORT_DB_NAME=
JST_REPORT_DB_SCHEMA=rpt
JST_REPORT_DB_USER=
JST_REPORT_DB_PASSWORD=
JST_REPORT_DB_ENCRYPT=true
JST_REPORT_DB_TRUST_SERVER_CERTIFICATE=true
# OA 新品流程看板数据源。默认 mysql;切换到 JSReportReplica 时设为 mssql。
OA_BOARD_SOURCE_DRIVER=mysql
# MySQL 来源兼容配置。
OA_BOARD_SOURCE_DB_HOST=
OA_BOARD_SOURCE_DB_PORT=3306
OA_BOARD_SOURCE_DB_NAME=
OA_BOARD_SOURCE_DB_USER=
OA_BOARD_SOURCE_DB_PASSWORD=
OA_BOARD_SOURCE_DB_CHARSET=utf8mb4
OA_BOARD_SOURCE_DB_SSL=false
# SQL Server 来源:JSReportReplica.rpt 的 ding_Workflow、ding_WorkflowScheme、ding_WorkflowTask 只读视图。
# 使用 mssql 来源时,快照仍写入下方配置的企划 MySQL 库。
OA_BOARD_MSSQL_HOST=
OA_BOARD_MSSQL_PORT=1433
OA_BOARD_MSSQL_DATABASE=JSReportReplica
OA_BOARD_MSSQL_SCHEMA=rpt
OA_BOARD_MSSQL_USERNAME=
OA_BOARD_MSSQL_PASSWORD=
OA_BOARD_MSSQL_ENCRYPT=false
OA_BOARD_MSSQL_TRUST_SERVER_CERTIFICATE=true
OA_BOARD_MSSQL_CONNECTION_TIMEOUT_MS=15000
OA_BOARD_MSSQL_REQUEST_TIMEOUT_MS=120000
# OA 看板快照目标(MySQL)。留空时兼容复用 PLANNING_DATABASE_URL / PLANNING_DB_*。
# 配置此组变量后,可保证切换 SQL Server 来源时所有快照、超时跟进和日报数据仍保存在企划库。
OA_BOARD_SNAPSHOT_DATABASE_URL=
OA_BOARD_SNAPSHOT_DB_HOST=
OA_BOARD_SNAPSHOT_DB_PORT=3306
OA_BOARD_SNAPSHOT_DB_NAME=
OA_BOARD_SNAPSHOT_DB_USER=
OA_BOARD_SNAPSHOT_DB_PASSWORD=
OA_BOARD_SNAPSHOT_DB_CHARSET=utf8mb4
OA_BOARD_SNAPSHOT_DB_SSL=false
# HR 员工目录(SQL Server)。OA 快照仅在本地员工目录无法匹配时使用,
# 用于将 ding_WorkflowTask.userId 解析成当前负责人的姓名。
HR_SQLSERVER_HOST=
HR_SQLSERVER_PORT=1433
HR_SQLSERVER_DATABASE=
HR_SQLSERVER_USERNAME=
HR_SQLSERVER_PASSWORD=
HR_SQLSERVER_ENCRYPT=false
HR_SQLSERVER_TRUST_SERVER_CERTIFICATE=true
# 产品资料供应商与仓库目录(SQL Server 报表库,只读)
JST_PLANNING_SQLSERVER_HOST=
JST_PLANNING_SQLSERVER_PORT=1433
JST_PLANNING_SQLSERVER_DATABASE=
JST_PLANNING_SQLSERVER_USERNAME=
JST_PLANNING_SQLSERVER_PASSWORD=
JST_PLANNING_SQLSERVER_SCHEMA=rpt
JST_PLANNING_SQLSERVER_ENCRYPT=false
JST_PLANNING_SQLSERVER_TRUST_SERVER_CERTIFICATE=true
# 登录和令牌
JWT_SECRET=replace-with-random-secret
JWT_EXPIRES_IN=7d
# 钉钉登录(OAuth 应用;只用于登录授权和用户信息获取)
DINGTALK_LOGIN_CLIENT_ID=
DINGTALK_LOGIN_CLIENT_SECRET=
DINGTALK_CALLBACK_ORIGIN=
# 钉钉开放平台 API(通讯录、待办、审批等服务端调用)
DINGTALK_API_CLIENT_ID=
DINGTALK_API_CLIENT_SECRET=
# 钉钉待办:OA 阶段超时预警,以及预计出货日前 15 天的品牌策划主管提醒。
DINGTALK_TODO_ENABLED=false
DINGTALK_TODO_DETAIL_BASE_URL=
DINGTALK_TODO_SOURCE_APP=oa-board
DINGTALK_TODO_ESCALATION_ENABLED=false
DINGTALK_TODO_ESCALATION_DAYS=3
# DINGTALK_AGENT_ID=
# 由壹点通知中心(仅服务端使用,项目密钥禁止提交或暴露给浏览器)
YYD_NOTIFICATION_CENTER_URL=https://sc.jiansun.vip/notification-center
YYD_NOTIFICATION_PROJECT_CODE=furniture_platform
YYD_NOTIFICATION_PROJECT_SECRET=
YYD_NOTIFICATION_TIMEOUT_MS=10000
# 店铺账号新增时,梦汐/扬灵群使用的钉钉“由壹点”群机器人。
# webhook 地址和签名密钥只配置在本地/正式站运行环境,不要提交真实值。
DINGTALK_STORE_ACCOUNT_GROUP_WEBHOOK_URL=
DINGTALK_STORE_ACCOUNT_GROUP_WEBHOOK_SECRET=
DINGTALK_STORE_ACCOUNT_GROUP_WEBHOOK_TIMEOUT_MS=10000
STORE_ACCOUNT_NOTIFICATION_RETRY_SECRET=
# 产品开发待办定时任务(仅服务端/部署 cron 使用,须与其他任务密钥分开)
PRODUCT_DEVELOPMENT_TODO_CRON_SECRET=
# 对象存储
# 必填,不能留空。当前生产桶使用 qiniu;只有明确切换适配器时才改为 s3/minio。
STORAGE_PROVIDER=qiniu
OBJECT_STORAGE_FILE_PATH=dev
STORAGE_FILE_PATH=dev
# 七牛云 Kodo
QINIU_ACCESS_KEY=replace-with-qiniu-access-key
QINIU_SECRET_KEY=replace-with-qiniu-secret-key
QINIU_BUCKET=sucaiku-js
QINIU_PUBLIC_DOMAIN=https://qny.jiansun.vip
QINIU_FILE_PATH=dev
QINIU_USE_HTTPS=true
QINIU_PRIVATE_BUCKET=true
QINIU_PRIVATE_URL_EXPIRES_SECONDS=7200
# S3/MinIO 兼容适配器
# STORAGE_PROVIDER=s3
S3_ENDPOINT=http://127.0.0.1:9000
S3_PUBLIC_ENDPOINT=http://127.0.0.1:9000
S3_ACCESS_KEY=replace-with-s3-access-key
S3_SECRET_KEY=replace-with-s3-secret-key
S3_BUCKET=replace-with-s3-bucket
S3_FILE_PATH=dev
S3_REGION=us-east-1
S3_ENABLE_PATH_STYLE=true
# 外部 AI 生图
EXTERNAL_IMAGE_GENERATE_TIMEOUT_MS=180000
# BOM 核算竞品分析
# 留空表示分析服务尚未接入;本地开发可显式填 fixture 验证完整工作台。
# 生产环境禁止使用 fixture。
BOM_COSTING_ANALYSIS_PROVIDER=
# 真实竞品分析使用 market-detail-multimodal;Key 只放本地/生产环境变量。
BOM_COSTING_MARKET_API_BASE_URL=https://qh.yydd.ai
BOM_COSTING_MARKET_API_KEY=replace-with-market-open-api-key
# 复用 AI 服务配置面板中已激活的多模态模型连接。
BOM_COSTING_AI_SERVICE_TYPE=prompt-optimizer
BOM_COSTING_MARKET_TIMEOUT_MS=60000
# 最终结构与 BOM 汇总默认最多等待 110 秒;GPT-5 兼容网关会强制收紧到该上限,避免上游两分钟截断造成任务假死。
BOM_COSTING_AI_TIMEOUT_MS=110000
BOM_COSTING_AI_MAX_IMAGES=20
# GPT-5 兼容网关的最终 BOM 汇总最多使用 12000 个输出额度,避免冗长回复拖垮整项任务。
BOM_COSTING_AI_MAX_TOKENS=12000
BOM_COSTING_AI_REASONING_EFFORT=low
BOM_COSTING_MODELING_PROVIDER=
# BOM 核算实时同款采购预估(1688 官方图搜)。
# 网关固定为 https://skills-gateway.1688.com/api/find_product/1.0.0。
# AK 只放服务器私密环境变量,服务端会按 x-csk-* 协议签名;实时同款价固定 × 90%
# 是整件采购预估,历史 BOM 仅保留为结构参考。不要把 AK 写入 Git 或 NEXT_PUBLIC_* 变量。
ALIBABA_1688_IMAGE_SEARCH_AK=replace-with-1688-image-search-ak
# 仅在官方网关地址调整时设置;系统会拒绝非官方 HTTPS 地址。
ALIBABA_1688_IMAGE_SEARCH_ENDPOINT_URL=https://skills-gateway.1688.com/api/find_product/1.0.0
ALIBABA_1688_IMAGE_SEARCH_TIMEOUT_MS=45000
ALIBABA_1688_IMAGE_SEARCH_MIN_SIMILARITY=0.78
ALIBABA_1688_IMAGE_SEARCH_PAGE_SIZE=10
ALIBABA_1688_IMAGE_SEARCH_PURCHASE_AMOUNT=1
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=
IMAGE_SEARCH_MODEL_SOURCE_BASE_URL=https://www.modelscope.cn/models/Xenova/dinov2-small/resolve/master
IMAGE_SEARCH_MIN_SHAPE_COSINE=0.78
IMAGE_SEARCH_MIN_SHAPE_MARGIN=0.08

1
furniture-3d-system/.gitattributes vendored Normal file
View File

@ -0,0 +1 @@
*.sh text eol=lf

View File

@ -2,6 +2,7 @@
# dependencies
/node_modules
/node_modules.pnpm-mixed
/.pnp
.pnp.*
.yarn/*
@ -15,6 +16,9 @@
# next.js
/.next/
/.next-*/
/.next-oa-preview*/
/.next-oa-webpack/
/out/
/.data/
@ -33,6 +37,7 @@ yarn-error.log*
# env files (can opt-in for committing if needed)
.env*
!.env.example
# vercel
.vercel
@ -44,6 +49,8 @@ next-env.d.ts
/src/generated/prisma
.database-backups/
.master-data-audits/
.storage-migration-audits/
.storage-gc-audits/
.master-data-cleanup/
# local workspace artifacts
@ -66,3 +73,9 @@ typecheck-latest.log
.local-test-*.log
tsconfig.typecheck.generated.json
local-packages/npm-cache/cache/_logs/
# 脚本监控 Agent 本机凭据
agents/script-monitor/config.json
# 下载中心本地运行时产物
.download-center/

View File

@ -0,0 +1,30 @@
# 供应链脚本监控 Agent
每台运行脚本的机器部署一份 Agent。Agent 只主动向产品平台发起 HTTP 请求,不需要为被监控机器开放入站端口。
## 接入
1. 在供应链中心的“脚本监控”页面点击“接入机器”,生成独立的 `agentId` 和 `agentToken`。
2. 复制本目录到目标机器,把 `config.example.json` 复制为 `config.json`,填入凭据、内外网地址和脚本任务说明。
3. 先运行 `node agent.mjs` 确认上报,再使用 `pm2 start ecosystem.config.cjs` 常驻。
需要在平台点击“立即运行”的脚本,必须在 `config.json` 对应任务中显式配置
`"allowManualRun": true`。平台只会下发脚本 `key`,Agent 始终执行本机配置里预存的
`command`,不会接收平台传来的任意命令。Agent 默认每 5 秒主动领取一次执行请求,
因此内网机器不需要开放入站端口。
`connectionMode` 支持:
- `intranet`:只走内网地址。
- `internet`:只走公网地址,生产必须使用 HTTPS。
- `auto`:优先内网,失败后自动尝试公网。
## 记录真实运行结果
对于需要记录开始时间、耗时、退出码和输出的任务,用包装器代替原命令:
```bash
node /opt/furniture-script-monitor/run-script.mjs supply-chain-archive-sync
```
包装器执行 `config.json` 中同一 `key` 的 `command`,并在开始与结束时上报。Agent Token 只保存在目标机器的 `config.json`,该文件已被仓库忽略。

View File

@ -0,0 +1,410 @@
import { createHash } from 'node:crypto'
import { execFileSync, spawn } from 'node:child_process'
import { existsSync, readFileSync, statSync } from 'node:fs'
import { hostname, platform, release } from 'node:os'
import { dirname, resolve } from 'node:path'
import { fileURLToPath, pathToFileURL } from 'node:url'
export const AGENT_VERSION = '1.0.0'
const AGENT_DIR = dirname(fileURLToPath(import.meta.url))
function requiredText(value, label) {
if (typeof value !== 'string' || !value.trim()) throw new Error(`缺少 ${label}`)
return value.trim()
}
export function loadConfig(configPath = process.env.SCRIPT_MONITOR_CONFIG || resolve(AGENT_DIR, 'config.json')) {
if (!existsSync(configPath)) {
throw new Error(`配置文件不存在:${configPath},请复制 config.example.json 为 config.json`)
}
const config = JSON.parse(readFileSync(configPath, 'utf8'))
config.agentId = requiredText(config.agentId, 'agentId')
config.agentToken = requiredText(config.agentToken, 'agentToken')
config.connectionMode = String(config.connectionMode || 'auto').toLowerCase()
if (!['intranet', 'internet', 'auto'].includes(config.connectionMode)) {
throw new Error('connectionMode 必须是 intranet、internet 或 auto')
}
config.reportIntervalSec = Math.max(15, Number(config.reportIntervalSec) || 60)
config.requestTimeoutMs = Math.max(1000, Number(config.requestTimeoutMs) || 10000)
config.commandPollIntervalSec = Math.min(60, Math.max(2, Number(config.commandPollIntervalSec) || 5))
config.scripts = Array.isArray(config.scripts) ? config.scripts : []
return config
}
function normalizeCenterUrl(value) {
if (typeof value !== 'string' || !value.trim()) return null
const url = new URL(value.trim())
if (!['http:', 'https:'].includes(url.protocol)) throw new Error(`不支持的中心地址协议:${url.protocol}`)
return url.toString().replace(/\/$/, '')
}
export function resolveCenterEndpoints(config) {
const intranet = normalizeCenterUrl(config.centerUrls?.intranet)
const internet = normalizeCenterUrl(config.centerUrls?.internet)
const endpoints = []
if (config.connectionMode !== 'internet' && intranet) endpoints.push({ kind: 'INTRANET', url: intranet })
if (config.connectionMode !== 'intranet' && internet) endpoints.push({ kind: 'INTERNET', url: internet })
if (endpoints.length === 0) throw new Error('当前连接模式没有可用的 centerUrls 地址')
return endpoints
}
export async function postToCenter(config, route, payload) {
let lastError = null
for (const endpoint of resolveCenterEndpoints(config)) {
const controller = new AbortController()
const timeout = setTimeout(() => controller.abort(), config.requestTimeoutMs)
try {
const response = await fetch(`${endpoint.url}${route}`, {
method: 'POST',
signal: controller.signal,
headers: {
'content-type': 'application/json',
authorization: `Bearer ${config.agentToken}`,
'x-script-monitor-agent-id': config.agentId,
},
body: JSON.stringify({
...payload,
...(route.endsWith('/report')
? { connectionKind: payload.connectionKind ?? endpoint.kind }
: {}),
}),
})
if (!response.ok) {
const message = await response.text().catch(() => '')
throw new Error(`HTTP ${response.status}${message ? `:${message.slice(0, 240)}` : ''}`)
}
return { endpoint, body: await response.json().catch(() => null) }
} catch (error) {
lastError = error
} finally {
clearTimeout(timeout)
}
}
throw lastError ?? new Error('没有可用的中心地址')
}
function hashKey(value) {
return createHash('sha256').update(value).digest('hex').slice(0, 20)
}
function redactCommand(command) {
return String(command || '')
.replace(/([A-Z0-9_]*(?:TOKEN|SECRET|PASSWORD|KEY)[A-Z0-9_]*=)([^\s]+)/gi, '$1[REDACTED]')
.replace(/(https?:\/\/[^:/\s]+:)[^@/\s]+@/gi, '$1[REDACTED]@')
.slice(0, 2000)
}
function readPm2() {
try {
const raw = execFileSync('pm2', ['jlist'], { encoding: 'utf8', timeout: 5000 })
return JSON.parse(raw)
} catch {
return []
}
}
function scanPm2() {
return readPm2().map((processInfo) => {
const name = String(processInfo.name || '未命名 PM2 进程')
const status = processInfo.pm2_env?.status === 'online'
? 'RUNNING'
: processInfo.pm2_env?.status === 'stopped'
? 'DISABLED'
: processInfo.pm2_env?.status === 'errored'
? 'FAILED'
: 'UNKNOWN'
return {
key: `pm2:${name}`,
name,
taskDescription: '',
jobType: 'PM2',
schedule: null,
commandPreview: redactCommand(processInfo.pm2_env?.pm_exec_path || ''),
expectedIntervalSec: null,
status,
isEnabled: status !== 'DISABLED',
canManualRun: false,
lastStartedAt: processInfo.pm2_env?.pm_uptime
? new Date(processInfo.pm2_env.pm_uptime).toISOString()
: null,
lastResultSummary: status === 'RUNNING'
? `PM2 在线,PID ${processInfo.pid || '-'}`
: `PM2 状态:${processInfo.pm2_env?.status || 'unknown'}`,
}
})
}
function parseCronLine(line) {
const value = line.trim()
if (!value || value.startsWith('#')) return null
const special = value.match(/^@(reboot|yearly|annually|monthly|weekly|daily|hourly)\s+(.+)$/)
const regular = value.match(/^(\S+\s+\S+\s+\S+\s+\S+\s+\S+)\s+(.+)$/)
const schedule = special ? `@${special[1]}` : regular?.[1]
const command = special?.[2] ?? regular?.[2]
if (!schedule || !command) return null
const tag = command.match(/#\s*([\w.-]{3,})\s*$/)?.[1]
const name = tag || command.split(/\s+/).filter(Boolean).slice(-1)[0] || 'cron task'
return {
key: `cron:${hashKey(`${schedule}\n${command}`)}`,
name,
taskDescription: '',
jobType: 'CRON',
schedule,
commandPreview: redactCommand(command.replace(/\s+#.*$/, '')),
expectedIntervalSec: null,
status: 'UNKNOWN',
isEnabled: true,
canManualRun: false,
}
}
function scanCron() {
if (platform() === 'win32') return []
try {
const raw = execFileSync('crontab', ['-l'], { encoding: 'utf8', timeout: 5000 })
return raw.split(/\r?\n/).map(parseCronLine).filter(Boolean)
} catch {
return []
}
}
function checkProcess(name) {
try {
if (platform() === 'win32') {
const output = execFileSync('tasklist', ['/FI', `IMAGENAME eq ${name}`], { encoding: 'utf8', timeout: 4000 })
return output.toLowerCase().includes(String(name).toLowerCase())
}
execFileSync('pgrep', ['-f', '--', name], { stdio: 'ignore', timeout: 4000 })
return true
} catch {
return false
}
}
function checkConfiguredScript(script) {
if (script.enabled === false) return { status: 'DISABLED', summary: '监控已停用' }
const check = script.resultCheck
if (!check) return { status: 'UNKNOWN', summary: null }
if (check.type === 'file') {
try {
const stat = statSync(requiredText(check.path, 'resultCheck.path'))
const ageSec = Math.max(0, Math.round((Date.now() - stat.mtimeMs) / 1000))
const fresh = !check.maxAgeSec || ageSec <= Number(check.maxAgeSec)
const largeEnough = check.minBytes == null || stat.size >= Number(check.minBytes)
return {
status: fresh && largeEnough ? 'SUCCESS' : 'FAILED',
summary: `文件 ${stat.size} B,${ageSec} 秒前更新`,
}
} catch (error) {
return { status: 'FAILED', summary: `文件检查失败:${error.message}` }
}
}
if (check.type === 'systemd') {
try {
const service = requiredText(check.service, 'resultCheck.service')
const state = execFileSync('systemctl', ['is-active', service], { encoding: 'utf8', timeout: 4000 }).trim()
return { status: state === 'active' ? 'RUNNING' : 'FAILED', summary: `systemd:${state}` }
} catch {
return { status: 'FAILED', summary: 'systemd 服务未运行' }
}
}
if (check.type === 'process') {
const running = checkProcess(requiredText(check.name, 'resultCheck.name'))
return { status: running ? 'RUNNING' : 'FAILED', summary: running ? '进程正在运行' : '未找到目标进程' }
}
return { status: 'UNKNOWN', summary: '未识别的结果检查类型' }
}
function normalizeConfiguredScript(script) {
const key = requiredText(script.key, 'scripts[].key')
const result = checkConfiguredScript(script)
return {
key,
name: requiredText(script.name, `scripts[${key}].name`),
taskDescription: String(script.taskDescription || '').slice(0, 2000),
jobType: String(script.jobType || 'OTHER').toUpperCase(),
schedule: script.schedule ? String(script.schedule).slice(0, 240) : null,
commandPreview: redactCommand(script.commandPreview || script.command || ''),
expectedIntervalSec: script.expectedIntervalSec ? Number(script.expectedIntervalSec) : null,
status: result.status,
isEnabled: script.enabled !== false,
canManualRun: script.allowManualRun === true && Boolean(script.command),
lastResultSummary: result.summary,
}
}
export function collectScripts(config) {
const byKey = new Map()
if (config.autoDiscover?.cron !== false) {
for (const script of scanCron()) byKey.set(script.key, script)
}
if (config.autoDiscover?.pm2 !== false) {
for (const script of scanPm2()) byKey.set(script.key, script)
}
for (const script of config.scripts) {
const normalized = normalizeConfiguredScript(script)
byKey.set(normalized.key, { ...(byKey.get(normalized.key) || {}), ...normalized })
}
return [...byKey.values()]
}
export async function reportSnapshot(config) {
const scripts = collectScripts(config)
const result = await postToCenter(config, '/api/internal/script-monitor/report', {
hostname: hostname(),
os: `${platform()} ${release()}`,
agentVersion: AGENT_VERSION,
reportIntervalSec: config.reportIntervalSec,
scripts,
})
console.log(`[script-monitor] 已通过${result.endpoint.kind === 'INTRANET' ? '内网' : '公网'}上报 ${scripts.length} 个脚本`)
}
function keepTail(current, chunk, limit = 12000) {
const next = current + chunk.toString()
return next.length > limit ? next.slice(-limit) : next
}
async function reportManualRun(config, command, input) {
return postToCenter(config, '/api/internal/script-monitor/run', {
commandId: command.id,
scriptKey: command.scriptKey,
scriptName: command.scriptName,
taskDescription: command.taskDescription || '',
jobType: String(command.jobType || 'OTHER').toUpperCase(),
...input,
})
}
export async function executeManualCommand(config, command) {
const script = config.scripts.find((item) => item.key === command.scriptKey)
const startedAt = new Date()
const externalRunId = `manual:${command.id}`
if (!script?.command || script.allowManualRun !== true) {
const message = 'Agent 配置未开放该脚本的手动运行权限'
await reportManualRun(config, command, {
externalRunId,
status: 'FAILED',
trigger: 'manual',
startedAt: startedAt.toISOString(),
finishedAt: new Date().toISOString(),
durationMs: 0,
exitCode: 1,
resultSummary: message,
errorTail: message,
})
return
}
await reportManualRun(config, command, {
externalRunId,
status: 'RUNNING',
trigger: 'manual',
startedAt: startedAt.toISOString(),
})
let outputTail = ''
let errorTail = ''
try {
const child = spawn(script.command, {
cwd: script.cwd ? resolve(script.cwd) : process.cwd(),
env: process.env,
shell: true,
stdio: ['ignore', 'pipe', 'pipe'],
})
child.stdout.on('data', (chunk) => {
outputTail = keepTail(outputTail, chunk)
process.stdout.write(chunk)
})
child.stderr.on('data', (chunk) => {
errorTail = keepTail(errorTail, chunk)
process.stderr.write(chunk)
})
const exitCode = await new Promise((resolveExit, reject) => {
child.once('error', reject)
child.once('close', (code) => resolveExit(code ?? 1))
})
const finishedAt = new Date()
await reportManualRun(config, command, {
externalRunId,
status: exitCode === 0 ? 'SUCCESS' : 'FAILED',
trigger: 'manual',
startedAt: startedAt.toISOString(),
finishedAt: finishedAt.toISOString(),
durationMs: finishedAt.getTime() - startedAt.getTime(),
exitCode,
resultSummary: exitCode === 0 ? '手动运行成功' : `手动运行失败,退出码 ${exitCode}`,
outputTail,
errorTail,
})
} catch (error) {
const finishedAt = new Date()
await reportManualRun(config, command, {
externalRunId,
status: 'FAILED',
trigger: 'manual',
startedAt: startedAt.toISOString(),
finishedAt: finishedAt.toISOString(),
durationMs: finishedAt.getTime() - startedAt.getTime(),
exitCode: 1,
resultSummary: `手动运行启动失败:${error.message}`,
errorTail: error.message,
})
}
}
export async function pollManualCommand(config) {
const result = await postToCenter(
config,
'/api/internal/script-monitor/commands/claim',
{},
)
const command = result.body?.data?.command
if (!command) return false
await executeManualCommand(config, command)
return true
}
async function main() {
const config = loadConfig()
const run = async () => {
try {
await reportSnapshot(config)
} catch (error) {
console.error('[script-monitor] 上报失败:', error.message)
}
}
let commandBusy = false
const poll = async () => {
if (commandBusy) return
commandBusy = true
try {
await pollManualCommand(config)
} catch (error) {
console.error('[script-monitor] 手动任务轮询失败:', error.message)
} finally {
commandBusy = false
}
}
await run()
await poll()
const timer = setInterval(run, config.reportIntervalSec * 1000)
const commandTimer = setInterval(poll, config.commandPollIntervalSec * 1000)
const stop = () => {
clearInterval(timer)
clearInterval(commandTimer)
process.exit(0)
}
process.on('SIGINT', stop)
process.on('SIGTERM', stop)
}
if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) {
main().catch((error) => {
console.error('[script-monitor] 启动失败:', error.message)
process.exit(1)
})
}

View File

@ -0,0 +1,79 @@
import assert from 'node:assert/strict'
import test from 'node:test'
import { executeManualCommand } from './agent.mjs'
function config(overrides = {}) {
return {
agentId: 'agent-1',
agentToken: 'secret-token',
connectionMode: 'internet',
centerUrls: { internet: 'https://monitor.example.test' },
requestTimeoutMs: 5000,
scripts: [{
key: 'local-check',
name: '本机检查',
taskDescription: '验证手动触发',
jobType: 'MANUAL',
allowManualRun: true,
command: `${JSON.stringify(process.execPath)} -e "console.log('manual-ok')"`,
}],
...overrides,
}
}
function command() {
return {
id: 'command-1',
scriptKey: 'local-check',
scriptName: '本机检查',
taskDescription: '验证手动触发',
jobType: 'MANUAL',
}
}
test('manual command executes only the local configured command and reports its result', async () => {
const previousFetch = globalThis.fetch
const reports = []
globalThis.fetch = async (_url, init) => {
reports.push(JSON.parse(init.body))
return new Response(JSON.stringify({ success: true, data: { ok: true } }), {
status: 200,
headers: { 'content-type': 'application/json' },
})
}
try {
await executeManualCommand(config(), command())
} finally {
globalThis.fetch = previousFetch
}
assert.equal(reports.length, 2)
assert.equal(reports[0].status, 'RUNNING')
assert.equal(reports[1].status, 'SUCCESS')
assert.equal(reports[1].exitCode, 0)
assert.match(reports[1].outputTail, /manual-ok/)
assert.equal(reports[1].commandId, 'command-1')
})
test('manual command is rejected when allowManualRun is not explicitly enabled', async () => {
const previousFetch = globalThis.fetch
const reports = []
globalThis.fetch = async (_url, init) => {
reports.push(JSON.parse(init.body))
return new Response(JSON.stringify({ success: true, data: { ok: true } }), {
status: 200,
headers: { 'content-type': 'application/json' },
})
}
const blockedConfig = config()
blockedConfig.scripts[0].allowManualRun = false
try {
await executeManualCommand(blockedConfig, command())
} finally {
globalThis.fetch = previousFetch
}
assert.equal(reports.length, 1)
assert.equal(reports[0].status, 'FAILED')
assert.match(reports[0].resultSummary, /未开放/)
})

View File

@ -0,0 +1,36 @@
{
"agentId": "PASTE_AGENT_ID_HERE",
"agentToken": "PASTE_AGENT_TOKEN_HERE",
"agentName": "仓库服务器 01",
"connectionMode": "auto",
"centerUrls": {
"intranet": "http://10.0.0.20:3001",
"internet": "https://cp.jiansun.vip"
},
"reportIntervalSec": 60,
"commandPollIntervalSec": 5,
"requestTimeoutMs": 10000,
"autoDiscover": {
"cron": true,
"pm2": true
},
"scripts": [
{
"key": "supply-chain-archive-sync",
"name": "供应链产品档案同步",
"taskDescription": "同步聚水潭商品档案、仓库和供应商扩展字段",
"jobType": "CRON",
"schedule": "*/15 * * * *",
"expectedIntervalSec": 900,
"allowManualRun": true,
"command": "npm run sync:supply-chain-product-archive",
"cwd": "/opt/furniture-3d-system-prod/current",
"resultCheck": {
"type": "file",
"path": "/opt/furniture-3d-system-prod/shared/logs/product-archive-sync.log",
"maxAgeSec": 1800,
"minBytes": 1
}
}
]
}

View File

@ -0,0 +1,15 @@
module.exports = {
apps: [{
name: 'furniture-script-monitor-agent',
script: 'agent.mjs',
cwd: __dirname,
instances: 1,
exec_mode: 'fork',
autorestart: true,
watch: false,
env: {
NODE_ENV: 'production',
SCRIPT_MONITOR_CONFIG: `${__dirname}/config.json`,
},
}],
}

View File

@ -0,0 +1,82 @@
import { spawn } from 'node:child_process'
import { randomUUID } from 'node:crypto'
import { resolve } from 'node:path'
import { loadConfig, postToCenter } from './agent.mjs'
function keepTail(current, chunk, limit = 12000) {
const next = current + chunk.toString()
return next.length > limit ? next.slice(-limit) : next
}
async function sendRun(config, script, input) {
return postToCenter(config, '/api/internal/script-monitor/run', {
scriptKey: script.key,
scriptName: script.name,
taskDescription: script.taskDescription || '',
jobType: String(script.jobType || 'OTHER').toUpperCase(),
...input,
})
}
async function main() {
const scriptKey = process.argv[2]
if (!scriptKey) throw new Error('用法:node run-script.mjs <script-key>')
const config = loadConfig()
const script = config.scripts.find((item) => item.key === scriptKey)
if (!script) throw new Error(`配置中不存在脚本:${scriptKey}`)
if (!script.command) throw new Error(`脚本 ${scriptKey} 未配置 command`)
const externalRunId = `${scriptKey}:${randomUUID()}`
const startedAt = new Date()
await sendRun(config, script, {
externalRunId,
status: 'RUNNING',
trigger: 'wrapper',
startedAt: startedAt.toISOString(),
}).catch((error) => {
console.warn('[script-monitor-run] 开始状态上报失败,不阻断原脚本:', error.message)
})
let outputTail = ''
let errorTail = ''
const child = spawn(script.command, {
cwd: script.cwd ? resolve(script.cwd) : process.cwd(),
env: process.env,
shell: true,
stdio: ['ignore', 'pipe', 'pipe'],
})
child.stdout.on('data', (chunk) => {
outputTail = keepTail(outputTail, chunk)
process.stdout.write(chunk)
})
child.stderr.on('data', (chunk) => {
errorTail = keepTail(errorTail, chunk)
process.stderr.write(chunk)
})
const exitCode = await new Promise((resolveExit, reject) => {
child.once('error', reject)
child.once('close', (code) => resolveExit(code ?? 1))
})
const finishedAt = new Date()
await sendRun(config, script, {
externalRunId,
status: exitCode === 0 ? 'SUCCESS' : 'FAILED',
trigger: 'wrapper',
startedAt: startedAt.toISOString(),
finishedAt: finishedAt.toISOString(),
durationMs: finishedAt.getTime() - startedAt.getTime(),
exitCode,
resultSummary: exitCode === 0 ? '脚本执行成功' : `脚本退出码 ${exitCode}`,
outputTail,
errorTail,
}).catch((error) => {
console.warn('[script-monitor-run] 运行结果上报失败,保留原退出码:', error.message)
})
process.exitCode = exitCode
}
main().catch((error) => {
console.error('[script-monitor-run] 执行失败:', error.message)
process.exitCode = 1
})

View File

@ -0,0 +1,153 @@
# 财务系统店铺密码直接显示契约
状态:生产接口已上线;2026-09-09 按当前明文存储规则修正 `passwordConfigured`。
## 存储事实与授权边界
- 密码真相源是 `public."StoreAccount"."passwordCiphertext"`。字段名是历史遗留;当前规则将新值以明文保存,读取链路对历史 `v1:` AES-256-GCM 值保留兼容。
- 存量 `v1:` 值已由 `scripts/migrate-store-account-plaintext-20260908.js` 迁移。`STORE_ACCOUNT_ENCRYPTION_KEY` 仅用于可能残留或恢复出的历史 `v1:` 值,不参与当前明文值读取。
- `passwordConfigured` 只表示存储值是否非空,不校验字符集或密码强度;因此包含 `+`、`/`、`=` 等字符的密码也会正确返回 true。它不返回密码本身。
- 普通 `store_account_reader` 可读视图、不能读取基表或密码列。此次迁移不增加其基表权限。
- 本接口只信任专用服务身份 `finance-store-accounts`。财务系统必须在自身后端校验用户登录态、查看/复制权限及店铺范围,然后从可信会话中取 `requesterId`。不得把客户端传入的任意人员 ID 当成操作者。服务凭据仅保存在财务后端,禁止交给浏览器。
- 不提供密码列表或批量导出。财务页面可在已通过自身页面权限校验后,自动读取当前可见页最多 50 个账号;CSV 和共享镜像仍只读取 `passwordConfigured`。复制操作继续走单账号接口。上游审计记录的用户 ID 是专用财务服务的身份断言,未授权请求中的 ID 不可信,`serviceId=null`。
## HTTP 契约
生产配置完成后的地址:
`POST https://cp.jiansun.vip/internal/store-accounts/{id}/password/reveal`
`POST https://cp.jiansun.vip/internal/store-accounts/visible-page/password/reveal`
`id` 为 `accounts_v1.id`,仅接受单个 1–128 位字母、数字、下划线或连字符。无查询参数;禁止在 URL、请求体、请求头中传待读取密码。
请求头:
```http
Authorization: Bearer <专用服务令牌>
Content-Type: application/json
```
请求体(严格拒绝额外字段,最大 1024 字节,1 秒内读取完毕):
```json
{ "requesterId": "finance-user-123", "purpose": "view" }
```
`purpose` 仅允许 `view` 或 `copy`。`requesterId` 是财务会话中的稳定人员标识,1–128 位 `[A-Za-z0-9_.:@-]`。不接受批量 ID、查询参数、普通登录 Cookie/JWT、普通数据库凭据或其他内部任务密钥。
当前页批量请求严格只接受以下字段,正文最大 8192 字节;`accountIds` 为 1 至 50 个互不重复且符合单账号 ID 规则的值:
```json
{ "requesterId": "finance-user-123", "purpose": "view", "accountIds": ["account-1", "account-2"] }
```
成功 `200`:
```json
{ "requestId": "<服务端生成的 UUID>", "password": "<仅本次响应体中的密码>" }
```
当前页批量成功的 HTTP 状态为 `200`,每个账号分别返回 `200/404/429/503`。每项都有独立 UUID;只有状态 `200` 的项包含密码,并按请求顺序完整返回:
```json
{
"requestId": "<批次 UUID>",
"items": [
{ "accountId": "account-1", "requestId": "<项目 UUID>", "status": 200, "password": "<密码>" },
{ "accountId": "account-2", "requestId": "<项目 UUID>", "status": 404 }
]
}
```
所有状态均返回 `Cache-Control: no-store`、`Pragma: no-cache`、`X-Content-Type-Options: nosniff`。`X-Request-Id` 与响应体、审计的追踪 ID 相同。不会在响应头中放密码。
错误体:
```json
{ "requestId": "<UUID>", "error": { "code": "FORBIDDEN", "message": "请求被拒绝" } }
```
| HTTP | code | 含义 |
|---|---|---|
| 403 | FORBIDDEN | 服务认证失败、非 HTTPS、非法 ID/JSON/用途、额外参数或非 POST |
| 404 | PASSWORD_NOT_FOUND | 账号不存在或没有可用密码,两者不区分 |
| 429 | RATE_LIMITED | 任一限流窗口超限 |
| 503 | UNAVAILABLE | 密钥/解密、数据库、限流或审计不可用;不返回内部异常 |
审计或数据库故障优先返回 503,绝不在无法持久化成功审计时返回密码。数据库完全不可用时无法保存该次失败审计,由网关仅记录追踪 ID、状态与时间供运维关联。成功审计表示服务器已授权并生成响应,不等同于浏览器已经复制成功。
## 限流和超时
- 单账号数据库滚动窗口:服务全部账号 30 次/分钟、300 次/24 小时;同一人员 10 次/分钟;同一账号 5 次/分钟。公网 TLS 网关保留 1 KiB 敏感请求正文上限,财务端把 50 行当前页拆成两个最多 25 个账号的批次;批次标记因此按服务 20 次/分钟、1000 次/24 小时、同一人员 10 次/分钟限流,等价于原每页额度。批次中的每个账号仍受同一账号 5 次/分钟限制。查看和复制共享账号额度,轮换令牌不重置服务身份或额度。
- 已认证的成功、404、参数拒绝和解密失败均占额度;429 不延长窗口。额度检查与审计在同一数据库事务中用事务级锁串行化,跨进程/重启共享状态。
- 429 不携带剩余数量或精确窗口,避免推测其他用户行为。客户端不得自动重试密码读取;提示用户稍后再试,持续超限联系管理员。
- 财务客户端设置总超时 10 秒、禁用自动重定向和重试。后端池上限 4、连接获取 1 秒、SQL 执行 1.5 秒/客户端查询 2 秒、锁等待 1 秒,路由运行预算 10 秒。网关必须配置请求体 1 秒和总请求截止,防止慢请求占用。
- 入口网关还需按来源 IP 对全部请求(包括未授权请求)限制为 60 次/分钟,返回 429 JSON 和 `no-store`;这项入口防滥用配置属于上线门禁。应用层数据库限流负责已认证的敏感读取。
## 环境变量(仅名称及要求)
| 名称 | 所在系统与要求 |
|---|---|
| STORE_ACCOUNT_ENCRYPTION_KEY | 仅用于兼容历史 `v1:` 密文,至少 32 字符;生产不回退到 JWT_SECRET |
| STORE_ACCOUNT_FINANCE_TOKEN | 上游专用服务令牌,密码学随机至少 32 字节,base64url 编码,43–128 字符;缺失时拒绝全部调用 |
| STORE_ACCOUNT_FINANCE_TOKEN_PREVIOUS | 上游可选旧令牌,仅用于短时轮换;主令牌缺失时旧令牌也不生效 |
| STORE_ACCOUNT_REVEAL_EDGE_TOKEN | 上游与可信 TLS 网关之间的独立随机凭据,base64url 43–128 字符;与财务令牌完全分离,不提供给财务系统;缺失时拒绝全部调用 |
| DATABASE_URL | 上游现有数据库连接;仅专用不输出查询/异常的连接池读取密文、写审计 |
| STORE_ACCOUNT_REVEAL_URL | 财务后端配置完整 HTTPS 地址模板 |
| STORE_ACCOUNT_REVEAL_VISIBLE_PAGE_URL | 财务后端配置当前页批量 HTTPS 地址;必须使用精确 `visible-page` 路径 |
| STORE_ACCOUNT_REVEAL_TOKEN | 财务后端保存与上游专用令牌对应的值 |
| STORE_REVEAL_TEST_ADMIN_URL | 仅本地集成测试;必须指向回环地址、有建测试库/角色权限的 PostgreSQL |
| STORE_REVEAL_TEST_POSTGRES_CONTAINER | 仅本地日志验收,指定本地 PostgreSQL 容器以只读扫描测试期间日志 |
## HTTPS、日志和缓存上线门禁
1. 财务后端至公开入口及跨主机的所有代理链路必须使用验证证书的 HTTPS,禁止 HTTP 跳转、跳过证书验证、明文 FRP/跨主机上游。当前 PR 不修改生产 TLS 拓扑;上线前逐跳核实,不满足即不启用令牌。
2. 应用不以 URL 或未经认证的转发头证明 TLS。必须由受控网关在实际 HTTPS 入口覆盖 `X-Forwarded-Proto: https` 及 `X-Store-Reveal-Edge-Token`(值来自独立 `STORE_ACCOUNT_REVEAL_EDGE_TOKEN`),剥除客户端原有同名头;HTTP 入口直接拒绝,不能注入上述凭据。应用对网关凭据做定时安全比较,缺失/错误或协议不是 https 时拒绝。凭据仅上游和网关持有,不提供给财务系统;配置文件 0600,不通过 `nginx -T` 等诊断输出展示。仍需封闭应用直连并逐跳验证 TLS,独立凭据不代替网络加密。
3. 对精确路径 `/internal/store-accounts/*/password/reveal` 禁止网关缓存、响应落盘缓冲、CDN缓存、请求/响应体及 Authorization 采集;包括 APM、WAF、异常上报、链路追踪和调试抓包。网关日志只允许时间、方法、路径(不带参数)、状态、耗时、追踪 ID。不要采集响应对象。
4. 网关自己的 403/404/429/503 也使用脱敏 JSON、`Cache-Control: no-store` 和追踪 ID;不得返回上游诊断页。禁止将该路由接入通用 `withApi`/request logger;目前路由直接返回白名单响应,不记录异常对象。
5. 数据库查询结果包含当前明文密码;严禁记录 SQL 结果、绑定参数或异常对象,不要启用该连接池的参数/结果追踪。密码写入及历史格式迁移期间同样关闭 PostgreSQL/pgAudit 的绑定参数和错误参数记录。
6. 财务后端同样禁用正文采集、缓存及异常对象打印。浏览器在进入已授权页面后只读取当前可见页,避免 localStorage/sessionStorage/Service Worker 持久化;页面隐藏或离开时清除页面密码,不把密码放入通知、埋点或消息队列。
## 视图与审计迁移
初始迁移:`prisma/migrations/20260908120000_store_account_password_reveal/migration.sql`。明文状态修复:`prisma/migrations/20260909100000_fix_store_account_password_configured_plaintext/migration.sql`。
- 新增 `StoreAccountPasswordAccessAudit`,固定字段为请求追踪 ID、人员标识、服务身份、账号 ID、用途、HTTP状态、数据库时间;不关联用户/店铺外键,以保留未授权、不存在和删除账号的失败审计。状态 200 表示成功,其他状态表示失败。
- 只在原视图存在时,以原视图查询作为子查询,在末尾追加 `passwordConfigured boolean`,保留原字段、筛选、每行内容、ID、所有者与 ACL;`EXISTS` 不会产生重复行。未配置外部视图的环境不擅自创建外部读入口。
- `passwordConfigured` 的当前定义为 `passwordCiphertext <> ''`。修复迁移仅替换这个布尔表达式,并在事务内核对原字段逐行内容、视图所有者和 ACL 未变;不读取或输出密码。
- 明文迁移已由独立脚本完成。本状态修复不改任何密码或银行账号数据,也不增加普通只读账号权限。审计数据至少保留 365 天。
迁移前由 DBA 只读确认目标库、视图定义/所有者/ACL、字段顺序、行数与唯一性、密码聚合、普通读取账号和其成员角色权限。若生产仍以本次基线验收,COUNT 与 DISTINCT id 必须均为 958;若业务新增/删除则暂停并重新确认基线,不改数据凑数。
备份与执行顺序(生产必须在明确批准后执行):
1. 备份当前 `main`/部署 SHA、现有视图 DDL/ACL/所有者及 `_prisma_migrations` 状态。生成目标库 `pg_dump -Fc` 备份并验证可读取;备份进入生产主机权限 0700 目录,文件 0600,不进入 Git 或共享日志。备份可能含密文,按敏感材料管理。
2. 本次不修改任何密码、银行卡号或服务令牌;备份不得进入 Git 或普通日志。
3. 执行明文状态修复迁移,视图短时获得 DDL 锁(最多等 5 秒,执行预算 30 秒);不回放未审查的其他迁移。
4. 在同一只读快照比较迁移前后原字段行集、顺序/类型、行数/唯一性和权限;运行 `scripts/sql/check-store-account-password-reveal.sql`,确认最后字段 boolean,报告 true/false/null 聚合。预计当前数据为 957/1/0,但必须执行后实测。
5. 按项目主线发布流程部署,完成逐跳 TLS、网关日志/限流/缓存检查后再配令牌。生产虚构账号的创建、调用与删除属于额外生产写入,需单独批准;不要调用真实账号作为测试。
回滚:先移除财务令牌阻止新调用并回退应用 SHA。新增状态列与审计表可安全保留以避免丢失审计;旧应用不依赖它们。需要完整结构回退时,经 DBA 检查依赖后,在事务中以备份 DDL 重建旧视图并恢复原所有者/ACL(PostgreSQL 不允许 `CREATE OR REPLACE VIEW` 删除尾列,禁止随意 `DROP ... CASCADE`)。审计记录保留,不直接删表;按项目规则处理已应用迁移历史,禁止直接删除 `_prisma_migrations` 记录或用 `db push`。任何完整结构回退先在隔离库演练。
## 密钥轮换
服务令牌:在受限凭据系统生成新随机令牌;旧值暂放 `STORE_ACCOUNT_FINANCE_TOKEN_PREVIOUS`,新值放主变量,重启上游;更新财务后端,使用虚构账号验收;移除旧值并重启。两值对应同一服务身份,审计/额度不变。怀疑泄露时立即撤销旧值,宁可短暂拒绝请求。
网关凭据:暂停此接口,在网关及上游同时更新 `STORE_ACCOUNT_REVEAL_EDGE_TOKEN` 后验收并恢复;不支持旧值并行。不得将网关凭据与服务令牌设成相同值。
历史兼容密钥:当前明文规则不需要为新值轮换数据密钥。若确认数据库中不存在 `v1:` 值并且备份保留策略允许,可另行评估退役历史解密密钥;退役前必须检查备份恢复场景,不能直接删除。
## 本次验证记录
- 当前页批量扩展的服务、HTTP 路由和代理边界共 3 个测试套件、41 项测试通过;`npm run type-check` 通过。测试覆盖 1–50 个账号、重复账号拒绝、部分 404/429/503、批次与每账号审计,以及失败时不返回明文。
- 单元/HTTP/代理路由测试:34 项通过,含伪造 HTTPS URL/转发头和错误网关凭据拒绝。
- 追加现有代理路由的 7 项回归,与 10 项集成测试合计 6 套件、51 项全部通过。
- 隔离本地 PostgreSQL 集成测试:10 项通过,覆盖完整加解密与 HTTP handler、已提交审计、403/404/429/no-store、并发/跨实例限流、分钟/24小时配额、审计失败拒绝泄露、原字段及筛选保留、958/958 与 boolean 957/1/0、普通只读角色拒绝基表/密码/审计读取。
- 开启本地测试连接的 PostgreSQL statement 日志,只在内存扫描 `docker logs`;应用日志、审计行、SQL请求与真实数据库日志均未检出随机测试密码。测试无需打印测试密码,断言只输出布尔结果。
- Type-check 通过;范围内 ESLint 通过。全仓库主数据边界巡检有 6 项已有违规,均位于本次未修改模块;不得据此声称全仓库通过。
- 2026-09-09 生产只读复核确认:958 行、958 个唯一 ID、957 条非空密码、1 条空密码;旧正则导致状态误报为全 false。本迁移用于恢复预期聚合 true/false/null = 957/1/0。
运行:`npx jest --runInBand --runTestsByPath src/server/services/store-account-password-reveal.service.test.ts src/proxy-store-account-reveal.test.ts "src/app/internal/store-accounts/[id]/password/reveal/route.test.ts" tests/integration/store-account-password-reveal.test.ts`。未配置本地测试库变量时,集成套件明确 skip,不能算真实数据库验收通过。
收口:**passed with warnings**。这是自审,不是独立复审。Route → Service → Repository 边界保持;未修改 UI;状态字段与当前明文存储规则对齐,专用认证、审计和限流边界不变。

View File

@ -0,0 +1,101 @@
# 1688 Dropship Integration
## Scope
The module at `/supply-chain/new-products/1688-dropship-data` includes the DF
product table, date and multi-select column filters, real-performance ordering,
and daily/weekly/monthly trend details with prior-month comparisons.
It reads the shared product archive and daily sales fact table. The shared
`barcode + statDate` key, unmatched source barcodes, synchronization lock,
staging, and atomic publication are preserved. No second daily table or
archive foreign key is introduced.
## Field Contract
| Display | Source / calculation |
| --- | --- |
| Sales amount | `sales_amount` -> daily `salesAmount` |
| Real performance | `real_performance` |
| After-sale amount | `after_sale_amount` |
| After-sale cost rate | After-sale amount / real performance; blank for zero denominator |
| Gross profit | Real performance - clear sales cost - send freight |
| SKU operator | DF barcode/day distinct `ider`, latest nonempty value in selected period |
| Total cost | Latest available daily `unitCost + estimatedFreight` in selected period; missing components are displayed as zero |
| Unit cost | Latest available daily DF cost from `goods_count = 1` observations; the existing product cost excludes freight; missing values are displayed as zero |
| Estimated freight | Average `send_freight` for the same DF barcode/day where `goods_count = 1`; the stored value remains null when no quantity-one observations exist and the page displays zero |
The archive's existing `shipAmount` continues to use `clear_goods_cost`.
It must not be substituted for the dropship module's `salesAmount`.
Cost ties use quantity-one frequency, total frequency, then descending unit cost. The page displays total cost before unit cost and estimated freight.
## Deployment Prerequisites
Code publication is not production deployment or database/data synchronization.
No credentials, database dumps, local accounts, or local environment files are
included in this change.
1. Confirm the target PostgreSQL identity, backup, and migration readiness using
the repository deployment/database workflow. Apply migration
`20260912120000_add_daily_sales_dropship_fields` after the existing mainline
daily-sales migration. Do not replay the old local prototype's daily-table
creation migrations against the mainline database.
2. Generate the Prisma client and build the application with the target's
existing environment configuration.
3. Run the existing `npm run sync:supply-chain-product-archive-daily --` command
with `--backfill`, `--apply`,
`--confirm=sync-supply-chain-product-archive-sales-performance-daily`,
`--expected-database-identity=...`, and
`--expected-source-database-identity=...` using verified identities.
The upstream requires read access to the configured
`JST_REPORT_DB_SCHEMA.dws_sn_sales_full_info_di`; credentials remain in the
target environment's `JST_REPORT_DB_*` variables.
4. Check the completed sales sync run, historic DF `salesAmount` completeness,
module view permission `newProduct1688DropshipData`, images, filters, totals,
and trend comparisons. The existing daily schedule then maintains the data.
The migrations add `skuOperator` (empty by default), `unitCost` (nullable),
`estimatedFreight` (nullable), and `salesAmount` (nullable). Historical facts need a backfill; the ordinary
seven-day refresh does not populate all older dates. Until backfilled, unknown
sales totals display as `--`, not zero or shipped amount.
Filtering uses the existing cache service with user-scoped keys and a 24-hour
TTL. Multi-process deployments should provide the existing shared Redis cache.
The module uses its own authenticated DF-only image route under
`/api/supply-chain/1688-dropship-data/source-image`. Its view permission does not
require granting access to the full product archive; the shared image service
and its source/URL restrictions are retained.
Rollback can restore the prior application version while retaining the additive
columns; do not drop the daily table or discard its history.
## Validation And Review
Independent review conclusion: `passed with warnings`. Architecture and
root-cause handling passed, including the corrected image-permission boundary.
The existing visual implementation was retained; target-environment screenshots
and real-data acceptance were not performed in this integration.
Final integration includes shared-mainline update `5156ead4`, retaining its
separate dropship-issues entry and permissions. After merging, 20 focused Jest
suites (171 tests), TypeScript checking, relevant ESLint, and the production
build passed. The build retains two unrelated broad-file-pattern warnings in
the existing OCR route and DWG wrapper.
The integration was checked with focused Jest suites for the page, trends,
filters, permissions, image route, service/repository mapping, and shared daily
sync; TypeScript checking; changed-file ESLint; and a production build using
isolated placeholder configuration (no live database). An in-memory PostgreSQL
compatibility replay confirmed that the additive migration retains existing
DF/non-DF facts and the original primary key. Image-route tests exercise the
real request/auth wrapper, including module-only access and denied requests.
The repository-wide `db:verify-master-data:code` and
`db:verify-master-data:truth` audits still fail on existing baseline issues.
Independent comparison with clean shared-mainline commit `38259cc4` found no
new blocking findings: code-audit finding content is unchanged apart from
schema line offsets, and the truth-audit JSON is identical. These global audit
failures are not presented as passing checks.
No target deployment, live target database migration/backfill, or live-data
browser validation of the integrated version is included in this publication.
The existing local test application and its prototype database are unchanged.

View File

@ -0,0 +1,62 @@
# CDN 直连上线交接(2026-06-06)
适用范围:`furniture-3d-system` 正式环境对象读取链路。
## 1. 当前结论
- 七牛 CDN 直连方案已在正式环境落地。
- 业务系统继续保留系统内稳定素材入口,不把七牛域名、bucket 或签名 URL 写死到业务页面和数据库字段里。
- 正式环境的对象读取链路已经从“应用服务器代理素材响应体”切到“应用鉴权后 `307` 跳转到七牛短时签名 CDN 地址”。
## 2. 当前正式环境口径
- 正式站点:`https://cp.jiansun.vip`
- 七牛空间:`sucaiku-js`
- 正式 CDN 域名:`https://qnycdn.jiansun.vip`
- 生产环境变量口径:`QINIU_PUBLIC_DOMAIN=https://qnycdn.jiansun.vip`
- 素材真相口径:
- 数据库继续保存系统稳定地址或对象 key
- 登录态素材稳定入口:`/api/storage/object?key=...`
- 分享态素材稳定入口:`/api/share/[token]/object?key=...`
- 服务端按当前 provider 生成短时签名外链,浏览器最终直连 CDN
## 3. 这次已确认的内容
- 代码侧已具备正式直连能力:
- `src/lib/storage.ts` 已明确区分稳定客户端入口和服务端直读地址
- `src/lib/object-storage-read-response.ts` 已支持 `HTTPS -> 307`
- `src/app/api/share/[token]/object/route.ts` 已存在
- `next.config.js` 已放行正式 CDN 域名
- 生产配置已切到正式 CDN 域名:`QINIU_PUBLIC_DOMAIN=https://qnycdn.jiansun.vip`
- 生产侧配置只读校验已通过:`npm run storage:verify-config`
- 真实业务侧抽验已完成:
- 页面:`https://cp.jiansun.vip/asset-center/material-library`
- 页面内真实纹理素材使用系统稳定入口 `/api/storage/object?...`
- 对该真实业务素材地址做校验时,服务端返回 `HTTP 307`
- `Location` 最终落到 `https://qnycdn.jiansun.vip/...`
## 4. 这次没走的路
- 审批访问页不是匿名链路,页面和接口都要求登录,不能拿它当公开验收入口。
- 当时生产里没有现成可直接拿来做公开分享验收的正式素材,所以分享页没有完成页面级抽验。
- 本次“真实业务侧”验收最终选用的是已登录正式环境中的资料页,而不是临时造生产数据。
## 5. 后续建议补的检查
1. 再抽验 `产品详情 / SKU 详情 / 3D 模型页`,确认模型和贴图都走 `307 -> qnycdn.jiansun.vip`。
2. 条件允许时补一轮 `分享页` 页面级验收,确认公开链路也落到正式 CDN。
3. 观察一到两天生产监控,重点看应用服务器出口流量、页面加载耗时、七牛命中率和回源量。
4. 后续任何人如果更换 bucket、CDN 域名或对象存储 provider,只改环境变量和 `src/lib/storage.ts` 的 storage adapter,不改业务页链接逻辑。
## 6. 回归检查清单
- `GET /api/storage/object?key=...` 对浏览器可直连资源仍返回 `307`
- `Location` 仍指向正式 CDN 域名,而不是旧测试域名或应用服务器自身
- `next.config.js` 与 CSP 仍放行 `https://qnycdn.jiansun.vip`
- 数据库里没有把短时签名 URL 当长期字段保存
- 分享页和审批页没有在前端直接拼七牛域名
## 7. 回滚提醒
- 不能只改 `QINIU_PUBLIC_DOMAIN` 就当回滚完成。
- 如果后续要回退,优先按“运行代码版本 + 共享环境变量”成组回退,避免出现“旧代码配新域名”或“新代码配旧口径”的半回滚状态。

View File

@ -1,64 +1,77 @@
# 主线同步操作手册
适用对象:会用 Codex 协作、但不一定熟悉 Git 的同事。
目标:当共享开发主线持续更新时,你仍然能把自己的分支保持在最新状态,不把代码做旧,不把冲突拖到最后一天。
目标:让每个分支都能跟上共享开发主线,不把旧代码、旧冲突和旧部署口径拖到最后。
---
## 1. 先记住一句话
你不要直接改 `main`。
你只在自己的分支里开发,然后经常把最新的 `origin/codex/xuxiake-mainline` 同步进来。
不要直接改 `main`。
日常开发从最新 `origin/codex/xuxiake-mainline` 开自己的任务分支,并且经常把共享开发主线同步进来。
当前推荐分工:
当前分工:
- `WuYongJie` 负责最终发布主线 `main`
- 日常开发默认以 `origin/codex/xuxiake-mainline` 为共享开发主线
- 其他协作者在自己的长期共享分支或临时任务分支开发
- `main` 是最终发布主线,由 `WuYongJie` 收口。
- `origin/codex/xuxiake-mainline` 是共享开发主线和默认同步基线。
- 其他协作者在自己的长期共享分支或临时任务分支开发。
分支命名固定规则:
分支命名:
- 新分支统一用 `codex/<负责人>-<工作域>` 或 `codex/<负责人>-<工作域>-<任务简称>`
- `main` 只由 `WuYongJie` 收口,其他协作者只在自己的分支里同步和开发
- 长期共享分支和临时任务分支都要把当前工作域写进分支名
- 不再使用 `codex/xuxiake-dev` 这种看不出职责的泛分支名
- 新分支用 `codex/<负责人>-<工作域>` 或 `codex/<负责人>-<工作域>-<任务简称>`。
- 不再使用 `codex/xuxiake-dev`、`feature/packaging`、`packaging/...` 这类泛分支名。
- 分支名要能看出负责人和工作域。
---
## 2. 你每天就做这 3 件事
## 2. 每次开工先查真实状态
### 开工前
在 `furniture-3d-system/` 所在仓库根目录执行:
先同步一次最新主线。
```bash
git fetch origin
git status --short --branch
git rev-list --left-right --count HEAD...origin/codex/xuxiake-mainline
```
### 主负责人刚推了重要改动后
怎么看:
再同步一次,不要继续拿旧代码往下写。
- `git status --short --branch` 会显示当前分支和未提交改动。
- `rev-list` 左边数字是当前分支独有提交数。
- `rev-list` 右边数字是 `origin/codex/xuxiake-mainline` 独有提交数。
- 右边大于 0,说明你落后共享开发主线。
- 左边大于 0,说明你当前分支有自己的提交。
### 你准备提交前
如果只是想确认远端最新提交,不要靠记忆,直接查:
最后再同步一次,避免你推上去的分支已经落后很多。
```bash
git fetch origin
git ls-remote origin refs/heads/codex/xuxiake-mainline refs/heads/main
```
---
## 3. 默认操作步骤
## 3. 有未提交改动时不要同步
先加一个固定前置动作:
先看:
```bash
git status --short
```
如果这里不是空的,先把当前改动处理好再同步主线:
如果不是空的,先处理当前改动:
- 要保留并提交:先提交到你当前分支
- 只是临时改动:先暂存(stash)再继续
- 要保留:先提交到当前分支。
- 只是临时现场:先 `stash`,并写清楚是什么现场。
- 是别人或其他 agent 的文件:不要擅自回退,不要擅自删除。
不要在“有一堆未提交改动”时直接 `merge origin/codex/xuxiake-mainline` 或 `rebase origin/codex/xuxiake-mainline`。
不要在一堆未提交改动上直接 `merge` 或 `rebase`。
---
如果你现在在长期共享分支,就把下面这 5 行命令原样交给 Codex 执行:
## 4. 长期共享分支用 merge
长期共享分支是别人可能会继续接手或反复同步的分支。固定用 `merge`:
```bash
git fetch origin
@ -67,118 +80,153 @@ git merge origin/codex/xuxiake-mainline
git push origin <你的长期共享分支>
```
这组命令的白话意思:
白话意思:
1. 先刷新远端分支信息
2. 切回你的长期共享分支
3. 把最新共享开发主线合进这个共享分支
4. 再把共享分支推上去
1. 刷新远端状态。
2. 回到你的长期共享分支。
3. 把最新共享开发主线合进来。
4. 推回远端。
长期共享分支不要 `rebase`,不要 `push --force`。
---
## 4. 如果你临时开了个人任务分支
## 5. 个人任务分支可以 rebase
只有在你从长期共享分支再开出个人任务分支时,才用下面这套:
只有确认这个分支没有别人共用时,才可以用:
```bash
git fetch origin
git checkout <你的分支名>
git checkout <你的个人任务分支>
git rebase origin/codex/xuxiake-mainline
git push --force-with-lease origin <你的分支名>
git push --force-with-lease origin <你的个人任务分支>
```
它的特点:
怎么选:
- 好处:任务分支提交更直
- 代价:会改写历史,不适合长期共享分支
- 长期共享分支:用 `merge`。
- 只有你自己使用的短期任务分支:可以用 `rebase`。
- 以后要交给别人继续接的分支:按长期共享分支处理。
怎么选最简单:
- 长期共享分支固定用 `merge`
- 只有你一个人在用的短期任务分支,才用 `rebase`
- 如果这个分支会被别人继续接手或反复同步,就按长期共享分支处理,不能为了图直线历史去改写公共提交
`--force-with-lease` 只允许用在个人任务分支,不能用在共享分支、`main` 或 `origin/codex/xuxiake-mainline`。
---
## 5. 什么时候说明你已经落后主线了
## 6. 什么时候必须同步
出现下面任意一种情况,就不要继续写了,先同步:
出现下面任意情况,先同步再继续:
- 主负责人刚改了你也会碰到的模块
- Codex 提示分支落后
- 你打开页面后,发现和主线最新效果不一样
- 你准备提交,但已经隔了半天或一天没同步
- 用户问“xuxiake 主线有没有新提交”。
- 你准备开始一个新任务。
- 主负责人刚推了会影响你模块的改动。
- 你准备提交或部署。
- 你发现页面表现和主线最新效果不一致。
- 你已经半天或一天没同步过。
同步前仍然要先处理未提交改动。
---
## 6. 如果同步时出现冲突
## 7. 冲突处理
不要慌,按这个顺序处理:
同步时遇到冲突,不要全选自己的版本,也不要全选主线版本。
1. 先停下来,不要乱点保存
2. 看冲突文件是不是你这次本来就在改的模块
3. 如果不是你负责的模块,先问主负责人
4. 如果是你负责的模块,就保留你的目标改动,再把主线的新内容接进去
5. 处理完后再继续让 Codex 帮你完成 `rebase` 或 `merge`
处理顺序:
可以直接把下面这段话发给 Codex:
1. 先停下来,看冲突文件属于哪个模块。
2. 如果不是你负责的模块,先问主负责人。
3. 如果是你负责的模块,优先保留主线最新结构,再把当前分支的目标改动接进去。
4. 只处理冲突文件,不趁机改无关模块。
5. 处理后跑最小验证,再继续 merge 或 rebase。
可以直接让 Codex 按下面口令处理:
```text
我在同步 `origin/codex/xuxiake-mainline` 到我的分支时遇到冲突了。
请只帮我处理这几个冲突文件,不要扩范围,不要改无关模块。
优先保留主线最新结构,再把我这个分支的目标改动接进去。
处理后请告诉我还需要执行什么命令。
我在同步 `origin/codex/xuxiake-mainline` 到当前分支时遇到冲突。
请只处理冲突文件,不扩范围,不回退无关改动。
优先保留主线最新结构,再把当前分支的目标改动接进去。
处理后告诉我需要继续执行什么命令和验证。
```
---
## 7. 绝对不要这样做
## 8. 主线负责人怎么收分支
- 不要直接在 `main` 上开发
- 不要长时间不同步主线
- 不要等“全部做完了”才第一次同步
- 不要把 `dev` 当万能长期分支名
- 不要对长期共享分支执行 `git rebase origin/codex/xuxiake-mainline`
- 不要看到冲突就随手全选自己的版本
- 不要在没看懂的情况下执行 `git reset --hard`
当同事分支已经推上来后,`WuYongJie` 按这个顺序看:
1. 看交接说明,确认本次做了什么、没做什么、验证跑了什么。
2. 看分支是否同步过最新 `origin/codex/xuxiake-mainline`。
3. 看改动是否超出任务范围。
4. 看是否碰到共享接缝。
5. 决定是 `merge`、`cherry-pick`,还是手工整合。
最简单判断:
- 分支干净、范围对、验证齐:`merge`。
- 只有部分提交该进主线:`cherry-pick`。
- 目标能收,但历史太乱或冲突太多:手工整合。
- 没同步、没说明、没验证:先不收。
共享接缝要多看一层:
- `prisma/schema.prisma`
- `prisma/migrations/`
- `src/app/layout.tsx`
- `src/app/intake/`
- `src/app/draft-groups/[id]/`
- `src/app/api/platform/schemas.ts`
- `src/server/services/draft-group.service.ts`
- `src/server/services/intake.service.ts`
- `src/server/repositories/draft-group.repository.ts`
---
## 8. 主线负责人怎么收同事分支
## 9. 发布前的主线核验
当同事分支已经推上来后,`WuYongJie` 固定按下面顺序做:
生产发布必须由用户明确授权。
1. 先看交接说明,确认本次只做什么、没做什么、验证跑了什么
2. 看分支有没有同步最新 `origin/codex/xuxiake-mainline`
3. 看改动有没有超出任务范围,或者碰到共享接缝
4. 再决定这次是 `merge`、`cherry-pick`,还是手工整合
发布代码默认来自 `origin/main`,部署入口以仓库根目录下的 `scripts/deploy-yydd-prod-from-git.ps1` 为准,底层打包脚本是 `scripts/create-yydd-prod-deploy.ps1`。
最简单判断法:
部署前至少核对:
- 分支干净、范围对、验证齐:`merge`
- 只有部分提交该进主线:`cherry-pick`
- 目标能收,但提交历史太乱或冲突太多:手工整合
- 没同步、没说明、没验证:先不收
```bash
git fetch origin
git ls-remote origin refs/heads/main
git rev-parse origin/main
```
如果同事改到了 `draft-group / intake / schema.prisma / src/app/draft-groups/[id]` 这类共享接缝,再多看一步:
如果本地 `origin/main` 和远端 `refs/heads/main` 不一致,先处理同步问题,不要部署。
- 为什么非改不可
- 这次改动是不是只为当前任务服务
- 有没有把包装逻辑带进正式入库或轻仓库主链
部署后不能只看公网 `/api/health`。还要按生产部署手册核对:
- 生产当前 commit。
- PM2 进程状态。
- 应用端口。
- 最近错误日志。
- 关键页面和关键接口。
---
## 9. 给同事最短口令版
## 10. 绝对不要这样做
如果你懒得记,只记这一段:
- 不要直接在 `main` 上开发。
- 不要长时间不同步共享开发主线。
- 不要等全部做完才第一次同步。
- 不要用 `dev` 这种看不出职责的长期分支名。
- 不要对长期共享分支执行 `rebase`。
- 不要对共享分支 `push --force`。
- 不要看到冲突就全选自己的版本。
- 不要在没看懂的情况下执行 `git reset --hard`。
- 不要把生产账号密码、访问密钥、数据库连接串、对象存储密钥写进 Git 文档;非密钥运行入口必须先核对当前现场再写。
---
## 11. 最短口令版
```text
每天开工前,先 fetch 远端,再把 `origin/codex/xuxiake-mainline` merge 进你的长期共享分支。
只有你临时开的个人任务分支才用 rebase。
如果冲突了,不要硬处理,先让 Codex 只处理冲突文件。
每天开工前先 fetch,再看 status 和当前分支相对 `origin/codex/xuxiake-mainline` 的 ahead/behind。
长期共享分支同步主线用 merge。
只有个人临时任务分支才可以 rebase。
冲突时只处理冲突文件,优先保留主线最新结构,再接回自己的目标改动。
生产部署前必须核对 origin/main 和远端 main,部署后要核对 commit、进程、端口、日志和关键页面。
```
补充说明:
- 历史文档里如果还出现固定包装分支名,那是旧协作阶段留下的例子,不再代表当前唯一允许的长期分支。
- 当前哪条分支算“长期共享分支”,以最新交接、当前任务边界和实际协作方式为准。

View File

@ -0,0 +1,54 @@
# 产品全量档案图片索引操作说明
全量档案本地图片索引只读取 `SupplyChainProductArchive.image`。上游「我司 SKU 命中」继续使用现有独立图搜服务。
## 更新规则
- 有图片且没有索引:建立索引。
- 图片地址变化、旧索引失效、模型版本或向量规格变化:重新索引。
- 图片为空或地址已经变化:旧向量退出本地搜索。任务会把旧记录标为 `STALE`。
- 下载或计算失败:保存 `FAILED` 和原因,六小时后再次尝试;图片地址再次变化则立即处理。
- 图片内容相同、地址不同:复用向量并更新地址与状态,避免反复处理同一张图片。
- 下载、计算期间图片改变或产品删除:提交前锁定并核对档案行,丢弃过期结果,下一轮处理最新图片。
- 所有索引任务使用同一 PostgreSQL 会话锁,避免手动补索引、同步触发与定时任务并发计算。
- 正常增量任务不会重新下载全部已就绪图片;同一 URL 下替换了图片内容时,使用完整内容核验命令。
## 本地命令
在主应用目录执行,模型沿用现有 `IMAGE_SEARCH_MODEL_PATH` 或 `.local-services/models/dinov2-small`。
```bash
# 每次最多处理 200 条缺失、换图或到期重试记录
npm run image-search:index
# 补齐当前全部待处理记录,并立即重试历史失败
npm run image-search:index -- --limit=0 --retry-now
# 下载所有带图档案,核验同一 URL 下的图片内容变化
npm run image-search:index -- --all --limit=0
```
命令持续输出已处理、成功、复用、失败、跳过、过期结果丢弃和失效条数;失败行输出条码与原因。最后输出本轮结果及待处理数量。失败仍会进入后续重试,本轮失败数必须单独关注,`remaining` 只计算已到重试时间的记录。
## 生产启用
代码必须先进入被授权的 `origin/main`,再经统一部署入口发布。安装定时器与首次补索引按用户授权执行,不能把本地开发完成等同于生产已启用。
安装入口:`scripts/install-product-archive-image-index-timer.sh`。它要求现有生产模型文件齐全,并让任务显式使用共享模型目录。当前应用启动命令已注入模型路径,无须为本功能重写应用环境。
- 服务:`furniture-product-archive-image-index.service`。
- 定时器:`furniture-product-archive-image-index.timer`,每五分钟检查,单次最多处理 200 条,失败六小时后重试。
- 两个现有全量同步服务分别增加本功能自己的 `image-index.conf`,同步成功后异步启动索引服务。
- 前台保存、导入及其他来源的图片变化由五分钟定时检查覆盖;索引下载与推理不阻塞保存请求。
- 通过 `PRODUCT_ARCHIVE_IMAGE_INDEX_LIMIT` 可在安装时调整单次处理上限。
- 脚本和日志通过发布目录与 systemd 管理,不手工修改发布目录。
```bash
bash scripts/install-product-archive-image-index-timer.sh
systemctl status furniture-product-archive-image-index.timer --no-pager
journalctl -u furniture-product-archive-image-index.service -n 50 --no-pager
```
首次补齐历史缺口可等待定时任务分批完成;需要一次补完时,手动索引命令必须指向共享模型路径。运行中的另一索引任务会返回 `LOCKED`,稍后重试。
验收应核对有图档案与索引的关联,而不是只看 `READY` 行数:当前地址一致、状态 `READY`、模型版本与 768 维向量均符合要求才是可用索引。历史 404 或不可读取地址不能靠索引任务生成图片,需要按条码定位源图片问题。

View File

@ -11,6 +11,7 @@
- 所属模块:
- 负责人:
- 所在分支:
- 同步基线:`origin/codex/xuxiake-mainline`
- 计划开始时间:
---
@ -59,6 +60,11 @@
-
### SPU / SKU / barcode / 批次
- 是否涉及:
- 如果涉及,四者分别是什么:
### 文档
-
@ -142,8 +148,15 @@
- 至少跑 `npm run type-check`
- 按需跑相关测试
- 能跑就跑 `npm run build`
- 前端改动按 1920x1080 做浏览器自查
- 涉及部署时核对 `origin/main`、PM2、端口、日志和关键页面
需要同步更新的文档:
- <文档路径 1>
- <文档路径 2>
规范审查:
- 架构有没有被破坏
- 视觉有没有跑偏
- 这次是不是根因修复
```

View File

@ -5,14 +5,16 @@
---
## 1. 先把这 5 句话说清楚
## 1. 先把这几句话说清楚
- 这次要做的模块是什么
- 现在做到哪一步了
- 你接下来只需要做什么
- 哪些目录不能碰
- 你应该在哪个分支继续做
- 是否已经同步 `origin/codex/xuxiake-mainline`
- 做完后怎么验证
- 规范审查结论是什么
---
@ -21,14 +23,20 @@
```text
任务名称:<名称>
目标分支:<固定写你要推到哪个分支,例如 codex/<负责人>-<工作域> 或 codex/<负责人>-<工作域>-<任务简称>>
同步基线:origin/codex/xuxiake-mainline
当前状态:<已完成到哪里>
你接手的部分:<只写这次要做的内容>
分支名称:<默认写 codex/<负责人>-<工作域>;如果是临时小任务,再写 codex/<负责人>-<工作域>-<任务简称>>
本次只改:<一句话说清本次改动范围>
本次明确没改:<一句话写清这次没碰什么>
是否碰共享接缝:<没有 / 有,改了哪些,为什么非改不可>
是否涉及 SPU / SKU / barcode / 批次:<没有 / 有,分别是什么,不能混写>
先看这几个文件:
- AGENTS.md
- docs/workflow/团队协作模式.md
- docs/workflow/模块边界说明.md
- docs/workflow/主线同步操作手册.md
- <文件 1>
- <文件 2>
- <文件 3>
@ -46,11 +54,16 @@
- `npm run type-check`
- `npm test -- --runTestsByPath <补具体文件>`
- `npm run build`
- 前端改动按 1920x1080 浏览器自查
- 人工打开:<页面路径>
风险与待主线决定:
- <风险 1>
- <是否建议 merge / cherry-pick / 手工整合>
规范审查:
- 这是自审,不是独立复审
- 结论:<passed / passed with warnings / rejected>
```
---
@ -66,7 +79,16 @@
当前背景:
<用 3 到 5 行说清楚现在已经做到哪一步>
开工前请先执行:
- `git fetch origin`
- `git status --short --branch`
- `git rev-list --left-right --count HEAD...origin/codex/xuxiake-mainline`
先看这些文件:
- AGENTS.md
- docs/workflow/团队协作模式.md
- docs/workflow/模块边界说明.md
- docs/workflow/主线同步操作手册.md
- <文件 1>
- <文件 2>
- <文件 3>
@ -88,6 +110,7 @@
- 至少跑 `npm run type-check`
- 按需跑相关测试
- 能跑就跑 `npm run build`
- 如果是前端改动,请按 1920x1080 做浏览器自查
如果你当前在长期共享分支上,请先同步共享开发主线再继续开发:
- `git fetch origin`
@ -104,6 +127,7 @@
2. 是否完成
3. 还有没有风险
4. 本次建议主线怎么收:`merge` / `cherry-pick` / `手工整合`
5. 规范审查结论:`passed` / `passed with warnings` / `rejected`
```
---
@ -119,4 +143,6 @@
- 没写有没有碰共享接缝
- 没写建议主线怎么收这次分支
- 没提醒对方先同步最新 `origin/codex/xuxiake-mainline`
- 没写清 SPU / SKU / barcode / 批次是否涉及
- 没写规范审查结论
- 没同步文档,导致下一个人继续靠猜

View File

@ -24,7 +24,7 @@
- 先读 `docs/workflow/团队协作模式.md`
- 先读 `docs/workflow/模块边界说明.md`
- 先读 `docs/workflow/主线同步操作手册.md`
- 包装协作额外读包装 AI 对接说明
- 前端、部署、数据库、包装等任务按任务类型额外读对应文档
这样就能做到:
@ -127,7 +127,7 @@ furniture-3d-system-codex-workspace/
- 默认用中文,面向非程序员表达
- 先看哪些文档
- 这位同事当前只负责哪些模块
- 这位同事当前负责哪些模块
- 哪些目录绝对不要碰
- 开工前先同步 `origin/codex/xuxiake-mainline`
- 结果汇报格式要简单
@ -151,10 +151,10 @@ furniture-3d-system-codex-workspace/
同事本地规则,放自己工作区外层 `AGENTS.md`:
- 他当前只负责包装相关模块
- 他当前负责的具体模块
- 先同步主线再开发
- 不扩范围
- 不擅自改主导航、辅助装配、草稿组、仓库主链
- 不擅自改主导航、辅助装配、草稿组、产品资料、仓库主链和共享接缝
- 回报格式尽量简单
---

View File

@ -1,7 +1,7 @@
# 同事本地 AGENTS 模板
使用方式:把下面内容复制到你同事自己电脑上的“项目外层工作区” `AGENTS.md` 中。
不要直接覆盖仓库里已经提交的那份 `AGENTS.md`。
使用方式:把下面内容复制到同事自己电脑上的“项目外层工作区” `AGENTS.md` 中。
不要直接覆盖仓库里已经提交的 `AGENTS.md`。
---
@ -13,107 +13,101 @@
## 1. 默认沟通方式
- 默认中文回复。
- 默认面向非程序员表达,先说结论,再说必要解释。
- 默认只说“现在你要做什么、是否完成、还有没有风险”。
- 默认先说结论,再说必要解释。
- 默认面向非程序员表达。
- 当前真实代码、脚本和线上返回优先于旧文档、旧记忆和历史摘要。
## 2. 先做什么
## 2. 开工先读
开始任务前,先看下面这些文件:
开始任务前,先看:
- `docs/README.md`
- `AGENTS.md`
- `docs/workflow/团队协作模式.md`
- `docs/workflow/模块边界说明.md`
- `docs/workflow/主线同步操作手册.md`
如果任务和包装有关,再看:
按任务补读:
- `docs/workflow/同事交接模板.md`
- `docs/开发指南/包装方案增强/新平台包装底座-同事AI对接说明-20260331.md`
- 前端/视觉:`docs/设计规范/白曜石工坊统一视觉标准.md`、`docs/开发指南/组件使用指南.md`
- 部署/线上:`docs/workflow/生产部署入口说明.md`
- 数据库/迁移:`docs/workflow/数据库迁移操作手册.md`、`docs/开发指南/数据库基础命令速查.md`
- 包装协作:`docs/开发指南/包装方案增强/` 下当前任务相关文档
## 3. 你的默认职责
## 3. 当前项目主链
你当前默认只负责包装相关模块,以及与包装对接的预留能力。
主业务链路是:
优先关注:
```text
资产中心 -> 辅助装配 -> 草稿组 -> 产品资料/正式入库 -> 仓库管理/轻仓库
```
- `src/app/api/pre-skus/[id]/packaging`
- `src/app/api/products/[id]/packaging`
- `src/server/services/packaging.service.ts`
- `src/server/repositories/packaging.repository.ts`
- `docs/开发指南/包装方案增强`
仓库质检、AI 出图工作台、OA 看板、用户和后台管理是工作台入口,不等于主数据链路本身。
## 4. 默认不要改的地方
## 4. 我的负责范围
除非任务明确要求,否则不要主动改下面这些地方:
把下面三项按实际任务改掉:
- 我当前负责:<例如 AI 白底图 / 包装 / 仓库质检 / 资产中心 / 产品资料>
- 我默认可以改:<目录或文件>
- 我默认不能改:<目录或文件>
如果不确定是否能改共享接缝,先问主负责人。
共享接缝包括:
- `src/app/asset-center`
- `src/app/assisted-assembly`
- `src/app/intake`
- `src/app/warehouse`
- `src/app/draft-groups/[id]`
- `src/server/services/draft-group.service.ts`
- `src/server/repositories/draft-group.repository.ts`
- `src/server/services/intake.service.ts`
- `src/server/services/warehouse.service.ts`
- `prisma/schema.prisma`
- 旧 CAD 兼容链的大范围逻辑
- `prisma/migrations/`
- `src/app/layout.tsx`
- `src/app/intake/`
- `src/app/draft-groups/[id]/`
- `src/app/api/platform/schemas.ts`
- `src/server/services/draft-group.service.ts`
- `src/server/services/intake.service.ts`
- `src/server/repositories/draft-group.repository.ts`
## 5. 开发前的固定动作
## 5. 分支和同步
- 开工前先同步最新 `origin/codex/xuxiake-mainline`
- 主负责人刚推了重要改动后,再同步一次
- 自己准备提交前,再同步一次
高频协作固定铁律见:
- `docs/workflow/高频协作五条铁律.md`
固定分支命名:
- 统一用 `codex/<负责人>-<工作域>` 或 `codex/<负责人>-<工作域>-<任务简称>`
- 长期分支使用 `codex/<负责人>-<工作域>`
- 临时小任务使用 `codex/<负责人>-<工作域>-<任务简称>`
- 不要使用 `dev` 这种看不出职责的泛分支名
默认同步步骤:
开工前、准备提交前、主负责人刚推重要改动后,都先同步状态:
```bash
git fetch origin
git checkout <你的长期共享分支>
git merge origin/codex/xuxiake-mainline
git push origin <你的长期共享分支>
git status --short --branch
git rev-list --left-right --count HEAD...origin/codex/xuxiake-mainline
```
如果你临时开的只是个人任务分支,再改用:
长期共享分支用 `merge`。
只有个人临时任务分支才可以 `rebase`。
```bash
git fetch origin
git checkout <你的分支名>
git rebase origin/codex/xuxiake-mainline
git push --force-with-lease origin <你的分支名>
```
分支命名:
## 6. 开发规则
- `codex/<负责人>-<工作域>`
- `codex/<负责人>-<工作域>-<任务简称>`
- 先修根因,不做表面补丁。
- 不顺手扩范围,不把自己的任务做成整个项目大改。
- Route / Page 不直接写 Prisma 业务规则。
- 先复用现有实现,不重复造新结构。
- 遇到冲突或边界不清,先停下来说明,不要硬改。
- 主导航、主流程骨架、公共数据结构,不要擅自大改。
不要使用 `dev`、`feature/...`、`packaging/...`、`codex/xuxiake-dev` 这类泛分支名。
## 7. 前端与文档
## 6. 前端规则
- 涉及前端时,优先保持现有统一视觉,不要自己另起一套样式。
- 涉及文档时,优先写短、直白、可执行的内容。
- 不要在文档里写你自己电脑上的绝对路径。
- 优先使用项目组件库、元组件和 `src/components/platform/`。
- 白曜石工坊是第一视觉标准。
- 如果有 Stitch 设计稿,按 Stitch 对齐密度、层级、控件、留白、字号和低噪音表达。
- 资产中心是项目内参考样板。
- 不堆说明,不堆统计,不做普通 SaaS 卡片墙。
- 前端改动按 1920x1080 做浏览器自查;受影响时补移动端自查。
## 8. 完成后默认回报格式
## 7. 数据和安全
完成后默认按下面格式回报:
- SPU、SKU、barcode、批次不能混用。
- 生产账号密码、访问密钥、数据库连接串、对象存储密钥不能写进 Git、共享文档或长期记忆。
- 生产、数据库、对象存储写操作必须先只读核查,再取得明确授权。
1. 现在你做了什么
2. 是否完成
## 8. 完成后回报
默认按下面格式回报:
1. 做了什么
2. 改了哪些文件或模块
3. 跑了什么验证
4. 还有没有风险或待确认项
4. 还有什么风险
5. 规范审查结论:`passed` / `passed with warnings` / `rejected`
6. 说明:这是自审,不是独立复审
```

View File

@ -1,323 +1,220 @@
# 团队协作模式
适用范围:`furniture-3d-system` 仓库。
目标:把当前项目从“单人连续开发”切换成“多人并行协作,但主链不打架”的模式。
适用范围:`furniture-3d-system/`。
目标:让多人和多 AI 在同一套主线、模块边界和视觉标准下协作,避免旧分支、旧流程和旧页面口径继续污染当前项目。
进入这个仓库协作时,固定先读这 3 份文档,再开始改代码:
1. `docs/workflow/团队协作模式.md`
2. `docs/workflow/模块边界说明.md`
3. `docs/workflow/主线同步操作手册.md`
这 3 份就是当前唯一的第一层协作入口,不再额外维护第二套平级入口文档。
本文件只写仓库内可以共享的规则。账号密码、访问密钥、数据库连接串、对象存储密钥等敏感信息不能写进 Git 文档;生产域名、PM2 名称、部署目录这类非密钥运行入口可以写,但必须保持最新。
---
## 1. 先记住一句话
## 1. 当前一句话
这个仓库现在默认按下面这条主链协作:
当前主业务链路是:
`资产中心 -> 辅助装配 -> 草稿组 -> 正式入库 -> 轻仓库`
```text
资产中心 -> 辅助装配 -> 草稿组 -> 产品资料/正式入库 -> 仓库管理/轻仓库
```
- 包装相关能力当前挂在 `DraftGroup / PreSku / Warehouse` 承接口里,不作为独立顶层主线入口。
- 旧的 `CAD -> 装配步骤` 链继续保留,但它是**历史兼容链**,不是新开品主链。
- 历史文档里如果还出现 `auto-combinations`、`自动组合`、`自动拼装`,统一按旧称呼理解,不再代表当前页面入口。
需要同时记住:
- 全局导航还包括首页、OA 新品流程看板、仓库质检、AI 出图工作台、用户和管理入口;这些是工作台入口,不等于主数据链路。
- 包装、组件、仓库、质检、AI 出图都必须挂回明确的产品、SKU、批次或草稿组上下文,不能另起一套数据口径。
- 旧 CAD 兼容链路仍存在,但它服务历史产品编辑,不是新平台主链。
- 历史文档里的固定包装分支、旧服务器别名、旧 `dev` 分支说法,只能当历史参考,不能当当前事实。
---
## 2. 当前协作口径
## 2. 开工固定入口
### 先固定两件事
当任务发生在 `furniture-3d-system/`,开始改代码前先读:
- `main` 是最终发布主线,由 `WuYongJie` 收口;日常开发默认同步 `origin/codex/xuxiake-mainline`。
- 这份文档现在只固定 Git 协作方式、主线收口规则和共享接缝处理原则,不再把当前模块职责长期写死在这里。
- 生产部署入口以 `docs/workflow/生产部署入口说明.md` 和 `scripts/deploy-yydd-prod-from-git.ps1` 为准;`192.168.58.20` 不是当前项目生产部署目标。
- `AGENTS.md`
- `docs/workflow/团队协作模式.md`
- `docs/workflow/模块边界说明.md`
- `docs/workflow/主线同步操作手册.md`
### 当前主线入口
如果任务涉及前端、生产或数据库,再按需补读:
- `src/app/asset-center`
- `src/app/assisted-assembly`
- `src/app/draft-groups`
- `src/app/intake`
- `src/app/warehouse`
- `src/app/api/asset-center`
- `src/app/api/assisted-assembly`
- `src/app/api/draft-groups`
- `src/app/api/intake`
- `src/app/api/pre-skus`
- `src/app/api/warehouse`
- `src/components/platform`
- `src/lib/open-product`
- `docs/设计规范/白曜石工坊统一视觉标准.md`
- `docs/workflow/规范审查清单.md`
- `docs/workflow/生产部署入口说明.md`
- `docs/workflow/数据库迁移操作手册.md`
- `docs/开发指南/数据库基础命令速查.md`
### 具体职责怎么定
开工时先确认真实现场:
- 具体谁负责哪个模块,不再以这份文档里的旧分工为准。
- 当前任务边界以任务单、交接摘要、已确认协作信息和实际代码现状为准。
- 如果任务碰到共享接缝,再和主线收口人对齐后再动。
```bash
git fetch origin
git status --short --branch
git rev-list --left-right --count HEAD...origin/codex/xuxiake-mainline
```
### 哪些地方仍然算共享接缝
`rev-list` 输出里,左边是当前分支独有提交数,右边是 `origin/codex/xuxiake-mainline` 独有提交数。右边大于 0 时,说明当前分支落后共享开发主线。
---
## 3. 当前权威分支
- `main` 是最终发布主线,由 `WuYongJie` 收口。
- `origin/codex/xuxiake-mainline` 是共享开发主线和默认同步基线。
- 新功能、优化和修复默认从最新 `origin/codex/xuxiake-mainline` 开任务分支。
- 常规分支命名使用 `codex/<负责人>-<工作域>` 或 `codex/<负责人>-<工作域>-<任务简称>`。
- 不再使用 `feature/packaging`、`packaging/...`、`codex/xuxiake-dev` 这类泛分支名。
长期共享分支同步共享开发主线时固定用 `merge`。
临时个人任务分支只有确认无人共用时才可以用 `rebase`。
任何人回答“xuxiake 主线有没有新提交”之前,都必须先 `git fetch origin`,再看真实远端状态。
---
## 4. 当前主要目录
主入口和全局布局:
- `src/app/page.tsx`
- `src/components/layout/`
- `src/app/layout.tsx`
新平台主链:
- `src/app/asset-center/`
- `src/app/api/asset-center/`
- `src/app/assisted-assembly/`
- `src/app/api/assisted-assembly/`
- `src/app/draft-groups/`
- `src/app/api/draft-groups/`
- `src/app/intake/`
- `src/app/api/intake/`
- `src/app/warehouse/`
- `src/app/api/warehouse/`
- `src/app/warehouse-quality/`
- `src/app/api/warehouse-quality/`
运营、AI 和平台能力:
- `src/app/ai-render-workbench/`
- `src/app/api/planning/`
- `src/app/api/platform/`
- `src/components/platform/`
- `src/lib/open-product/`
- `src/lib/planning/`
旧 CAD 兼容链路:
- `src/app/products/`
- `src/app/api/products/`
- `src/app/api/skus/`
- `src/components/assembly/`
- `src/components/3d/`
---
## 5. 共享接缝
下面这些位置属于高冲突区,改动前必须多看一层调用方,提交时也要讲清楚影响范围:
- `src/server/services/draft-group.service.ts`
- `src/server/repositories/draft-group.repository.ts`
- `src/server/services/intake.service.ts`
- `src/app/intake`
- `prisma/schema.prisma`
- `src/app/draft-groups/[id]`
- `prisma/migrations/`
- `src/app/layout.tsx`
- `src/app/intake/`
- `src/app/draft-groups/[id]/`
- `src/app/api/platform/schemas.ts`
- `src/server/services/draft-group.service.ts`
- `src/server/services/intake.service.ts`
- `src/server/repositories/draft-group.repository.ts`
共享接缝不是不能改,而是不能顺手改、不能带着局部假设改、不能不验证就合回主线。
---
## 3. 分支规则
## 6. 业务对象口径
### 主分支
当前新平台不能混用这些概念:
- `main` 继续保持受保护状态,只作为最终发布主线收口。
- 其他协作者默认不能直接 push `main`,只能走自己的功能分支。
- 日常开发基线是 `origin/codex/xuxiake-mainline`,不要把半成品直接推上最终发布主线。
- `AssetFamily`:资产中心里的产品族或产品款基础资产。
- `AssetVariant`:资产族下的规格、材质、花色等变体。
- `DraftGroup`:草稿组,是辅助装配到产品资料之间的承接对象。
- `PreSku`:正式 SKU 前的预 SKU 或待确认 SKU。
- `Product / SKU`:正式入库后的产品和 SKU。
- `IntakeNonIntegratedProduct / IntakeNonIntegratedSkuItem`:非集成产品实拍、AI 白底图、素材库回传等链路使用的对象。
### 开发分支
SPU、SKU、barcode、批次必须分清:
统一使用:
- SPU 是产品款。
- SKU 是 SPU 下的具体子项。
- barcode 是逐步收敛的唯一业务定位值,不能拿来代替 SPU 或 SKU。
- 批次只能属于当前产品资料链路的批次上下文。
- 基于某个 SKU 换色得到的新 SKU,仍属于同一个 SPU,并且只能归属当前批次。
- `codex/<负责人>-<工作域>`
- `codex/<负责人>-<工作域>-<任务简称>`
---
固定口径:
## 7. 前端和视觉协作
- 负责人必须进分支名,不能只用 `feature/...`、`packaging/...`、`dev` 这类泛名字
- `WuYongJie` 主线相关分支统一放 `codex/wuyongjie-mainline...`
- 长期共享分支和临时任务分支都要把当前工作域写进分支名,不再把人长期绑定在单一旧分支名上
- 其他负责人也按同一模式命名,不再额外发明新前缀
- 历史分支名不强制重命名,但从现在开始新分支全部按这套规则建
当前第一视觉标准是白曜石工坊,不是旧苹果风,也不是普通后台卡片墙。
示例:
前端任务必须同时遵守:
- `codex/wuyongjie-mainline`
- `codex/wuyongjie-mainline-asset-center`
- `codex/xuxiake-costing`
- `codex/xuxiake-intake-clean`
- `codex/zhangsan-cad-compat`
- 优先使用 `src/components/platform/` 和项目里的元组件、组件库。
- Stitch 设计稿是视觉规格和验收标准,但落地时要通过项目组件体系实现。
- 资产中心页面是当前项目内最重要的参考样板。
- 新平台页面保持浅色工作台、低噪音、图片和数据优先、层级清楚。
- 不堆说明块、不重复统计、不做营销式大卡片布局。
- `apple-*`、暗色 `ObsidianControlShell`、旧主题 CSS 只作为兼容历史页面存在,不能作为新页面方向。
### 合并方式
涉及图片、3D、AI 白底图、素材卡片时,重点是图本身和操作路径,不要让说明文字和状态块抢走主要视觉注意力。
- 主负责人负责把稳定内容从共享开发主线收口到 `main`。
- 其他协作者默认先推自己的功能分支。
- 同事的改动进入主线前,默认先由主负责人检查和收口。
- 协作者不要自己合并到 `main`,也不要自己改远端主线历史。
- 没有过验收清单,不合并,不并线。
---
当前固定流程:
## 8. 生产和部署协作
1. 协作者先从最新 `origin/codex/xuxiake-mainline` 同步到自己的长期分支或任务分支。
2. 协作者只在自己负责的工作域内开发。
3. 协作者推送自己的分支,并附上交接说明。
4. `WuYongJie` 负责检查范围、决定 merge / cherry-pick / 手工收口。
5. 只有进过主负责人收口的内容,才允许进入 `main`。
生产发布必须由用户明确授权。
发布代码默认以 `origin/main` 为来源,常用入口是仓库根目录下的 `scripts/deploy-yydd-prod-from-git.ps1`,底层打包脚本是 `scripts/create-yydd-prod-deploy.ps1`。
### 主线收口检查单
`WuYongJie` 收同事分支前,固定先看这 6 项:
1. 这个分支是不是已经同步过最新 `origin/codex/xuxiake-mainline`
2. 这次改动是不是只落在任务约定范围内
3. 有没有碰共享接缝;如果碰了,交接里有没有写清原因
4. 有没有最小验证结果,而不是只说“应该可以”
5. 有没有交接说明,且写清楚“这次只改了什么、没改什么、风险是什么”
6. 这次改动会不会影响正式入库、轻仓库或主链真相
只要这 6 项里有明显缺项,就先不要急着收口。
### merge / cherry-pick / 手工整合 怎么选
固定口径:
- 直接 `merge`
- 适用:改动范围干净,只在同事负责域内,没有混入共享接缝或无关改动
- 前提:已经同步最新 `origin/codex/xuxiake-mainline`,并且附了最小验证和交接说明
- 优先 `cherry-pick`
- 适用:同一个分支里既有可收的目标提交,也混入了暂时不该进主线的提交
- 常见场景:同一个协作分支里夹带了共享接缝改动、调试提交、试错提交
- 手工整合
- 适用:目标功能要收,但提交历史太脏、冲突太多,或者改动虽然合理但不能整段直接拿
- 常见场景:同一个文件同时碰到主线重构区和同事任务区
- 暂不收口
- 适用:没同步最新 `origin/codex/xuxiake-mainline`、没交接说明、没最小验证、范围失控、直接改了敏感共享接缝但没写原因
- 处理方式:先退回补说明、补同步、补验证,再继续看
### 同事和主线怎么保持同步
当前项目默认采用这个口径:
- `WuYongJie` 负责维护共享开发主线,并把稳定内容收口到 `main`
- 协作者长期在自己的长期共享分支或当前任务分支开发
- 同事不能等做完了再一次性同步,而是要高频同步 `origin/codex/xuxiake-mainline`
默认同步时机:
- 每天开始开发前同步一次
- 主负责人刚推了重要改动后同步一次
- 自己准备提交前再同步一次
默认推荐方式:
- 长期协作分支固定优先使用 `merge origin/codex/xuxiake-mainline`
- 只有临时个人任务分支才优先使用 `rebase origin/codex/xuxiake-mainline`
- 但不要一边长期不同步,一边继续堆改动
白话规则:
- 长期共享分支保留真实协作历史,所以固定用 `merge origin/codex/xuxiake-mainline`
- 临时个人任务分支只服务当前任务,为了保持提交线清楚,才用 `rebase origin/codex/xuxiake-mainline`
- 如果你拿不准自己现在是不是长期共享分支,就先按长期共享分支处理,不要擅自改写共享历史
长期共享分支日常同步固定步骤:
部署前至少核对:
```bash
git fetch origin
git checkout <你的长期共享分支>
git merge origin/codex/xuxiake-mainline
git push origin <你的长期共享分支>
git ls-remote origin refs/heads/main
git rev-parse origin/main
```
如果你临时开了个人任务分支,再用:
部署后不能只看公网 `/api/health` 就说完成,还要按生产部署手册核对发布 commit、进程、端口、日志和关键页面。
```bash
git fetch origin
git checkout <你的临时任务分支>
git rebase origin/codex/xuxiake-mainline
git push --force-with-lease origin <你的临时任务分支>
```
怎么选:
- 长期共享分支,用 `merge`
- 临时个人任务分支,用 `rebase`
详细操作说明见:`docs/workflow/主线同步操作手册.md`
高频协作总规则见:`docs/workflow/高频协作五条铁律.md`
仓库文档里不要写生产账号密码、访问密钥、数据库连接串或对象存储密钥。非密钥的生产入口信息可以写,但必须先核对当前脚本和线上现场。
---
## 4. 每个任务都按这个流程走
## 9. 本地文件和清理
1. 先建一张任务单。
2. 写清楚本次只做什么、不做什么、验收标准是什么。
3. 明确归属模块,不跨模块顺手乱改。
4. 开发前先读对应模块边界说明。
5. 改完后同步必要文档。
6. 跑最小验证。
7. 做一次规范自审,再推分支。
本仓库经常有本地产物、排错证据和临时交接文件,例如 `.next*`、审计目录、下载素材、截图、日志、Excel、JSON 摘要等。
额外固定动作:
清理前先判断它属于哪一类:
- 任务涉及共享接缝时,先在任务单里写清“为什么必须碰共享层”。
- 协作者推分支后,要附一段交接说明,明确本次只改了什么、没改什么、风险是什么。
- `WuYongJie` 收口同事改动时,优先按小任务合并,不一次性吞并长期积压改动。
- 如果任务只是要把局部工作区做完整,默认不要直接在 `src/app/draft-groups/[id]`、`src/app/intake` 这类共享接缝里堆实现;先确认是否该在当前工作域内独立收口。
- 如果这次改动会影响 `intake / warehouse / Product / SKU` 承接链,收口时额外确认:有没有把局部协作逻辑误带进正式入库主链,或者把旧兼容链重新当成主链真相。
- 正式源码和协作文档:不能随手删。
- 本地产物:确认不影响验证后可以清理。
- 排错证据:任务没收口前不要清。
- 敏感交接材料:不能提交 Git,必要时拆成本地版和仓库版。
- 用户或其他 agent 的未跟踪文件:默认不要动。
---
## 5. 人和 AI 都要遵守的几条硬规则
## 10. 每次任务的交接口径
### 先修根因,不打补丁
交接时讲清楚:
- 影响 3D 拼装、命名、包装、BOM、安装步骤的数据,优先在数据源修。
- 不允许只在页面上压报错、绕校验。
- 本次解决什么问题。
- 改了哪些模块和共享接缝。
- 是否同步过 `origin/codex/xuxiake-mainline`。
- 跑了哪些验证,失败了什么。
- 是否部署,部署来源 commit 是什么。
- 规范审查结论是什么。
### 不跨层
规范审查固定只查三件事:
- Page / Route 不直接写 Prisma 业务逻辑。
- Route 负责收参、鉴权、返回。
- Service 负责业务规则。
- Repository 负责数据访问。
- 架构有没有被破坏。
- 视觉有没有跑偏。
- 这次是不是根因修复。
### 不混主链和兼容链
- 新平台主链的数据真相在 `AssetFamily / AssetVariant / DraftGroup / PreSku`。
- 旧 `Product / SKU / Panel / Hole / ProductHardware` 继续服务兼容链。
- 包装本期要能挂接 `PreSku`,但不直接跳过草稿组去造正式产品。
### 不破坏统一视觉
- 新平台页面优先复用 `src/components/platform`。
- 旧装配编辑器继续走现有视觉体系,不另起一套杂样式。
---
## 6. 本地目录管理规则
### 需要长期保留的
- 源码
- 迁移
- 测试
- 正式文档
- 确认要进仓库的设计稿或说明
### 默认视为本地产物的
- `.dev-*.log`
- `.qa-*.log`
- `.start-*.log`
- `.next-start-*.log`
- `dev-server.log`
- `.codex-logs/`
- `_logs/`
- `output/`
- `temp/`
- `public/uploads/`
### 推荐习惯
- 提交前先运行:`npm run clean:local`
- 然后再看:`git status --short`
---
## 7. 提交前最少要交代什么
- 这次改的是哪个模块
- 本次只做了什么
- 哪些地方明确没做
- 跑了哪些验证
- 有没有风险或依赖别人继续接
详细清单见:`docs/workflow/提测与验收清单.md`
---
## 8. 给 Codex 的一句话协作口径
如果要把任务转给 AI,建议在任务里直接写明:
- 当前目标
- 所属模块
- 禁止改动的目录
- 验收标准
- 需要同步更新的文档
模板见:`docs/workflow/任务单模板.md` 和 `docs/workflow/同事交接模板.md`
---
## 9. 团队共享 AGENTS 和个人 AGENTS 怎么分
当前默认规则:
- 仓库里的 `AGENTS.md` 是团队共享规则
- 个人额外规则不要直接改仓库根目录那份
- 谁要给自己的 Codex 加私有约束,就在自己电脑上的项目外层工作区再放一份本地 `AGENTS.md`
推荐说明:
- 共享规则:继续放仓库里,大家共用
- 个人规则:放各自机器上的外层工作区,不进仓库
对应说明和模板见:
- `docs/workflow/同事本地AGENTS使用说明.md`
- `docs/workflow/同事本地AGENTS模板.md`
- `docs/workflow/高频协作五条铁律.md`
结论只能是 `passed`、`passed with warnings` 或 `rejected`。
如果这次只是自审,必须明确写“这是自审,不是独立复审”。

View File

@ -0,0 +1,213 @@
# 外部只读数据库开放操作说明
适用范围:给指定服务器读取本项目 PostgreSQL 的外部核心数据包,覆盖产品资料、产品限价、仓库管理、仓库质检。
## 1. 固定结论
- 不开放 `public` 业务原表。
- 不复用应用账号或管理员账号。
- 默认只开放 `external` schema 下经过白名单整理的核心视图。
- 外部系统优先读取 `external.product_catalog_wide_v1` 这张商品目录聚合宽表;需要仓库或质检明细时,再读取对应明细视图。
- 外部主同步、去重和幂等更新使用 `barcode_business_key`;`product_business_key` 只用于 SPU/型号归组,`sku_business_key` 只用于 SKU 编码兼容关联。
- 只允许指定服务器 IP 访问 PostgreSQL 端口。
- 密码、连接串、白名单 IP 只放生产本地交接材料,不写入 Git。
## 2. 对外数据入口
推荐给外部系统读取:
```sql
SELECT *
FROM external.product_catalog_wide_v1;
```
这张视图是一条码一行。正式 `Product / ProductSKU` 已存在时输出正式条码商品行;正式商品尚未落库但 non-integrated 已进入 ready 状态时,补输出可消费的开发阶段条码行。外部系统必须按 `barcode_business_key` 做主同步、去重和幂等更新;同一 SKU 下多个条码会输出多行。
补充口径:
- non-integrated ready 行会带上主图、开发阶段限价和基础商品字段。
- non-integrated ready 行会带上产品开发明细页的负责人字段:外观设计师、结构设计师、SPU 负责人、商品企划、空间设计师、采购负责人;正式商品行没有开发阶段上下文时这些列保持空值。
- non-integrated ready 行尚未正式化时,正式 BOM、包装库存、仓库库位和 SKU 质检摘要字段会保持 `0 / NULL / []`。
- 需要核对开发阶段头信息或 SKU 明细真值时,继续读取 `external.intake_products_ready_v1 / external.intake_sku_items_ready_v1`。
- 图片、模型、贴图等 URL 字段在外部视图中输出为完整系统入口,例如 `https://cp.jiansun.vip/api/storage/object?key=...`。这些入口仍由应用侧鉴权和对象存储跳转控制,不把私有桶短期签名 URL 固化进数据库。
- 非生产环境如需使用其他站点域名,可由数据库管理员设置 `ALTER DATABASE <DB_NAME> SET app.external_asset_base_url = '<APP_ORIGIN>';`;未设置时默认使用正式站点 `https://cp.jiansun.vip`。
限价口径固定如下:
- `存量商品明细` 的产品一般没有限价,宽表输出 `limit_price_status = NOT_APPLICABLE`。
- `产品开发明细`、`模块化产品明细` 的产品到 `产品限价` 节点确认后,限价就是正式口径,宽表输出 `limit_price_status = CONFIRMED`。
- 需要核对限价来源时读取 `external.intake_product_limits_v1`。
本次外部核心只读包包含:
| 方向 | 推荐视图 | 用途 |
| --- | --- | --- |
| 产品资料 | `external.product_catalog_wide_v1` | 一条码一行的商品目录宽表,覆盖正式条码与可消费的 non-integrated ready 条码 |
| 产品限价 | `external.intake_product_limits_v1` | 非集成产品限价来源明细 |
| 仓库管理 | `external.warehouse_locations_v1` | 库位资料 |
| 仓库管理 | `external.sku_warehouse_stock_v1` | 正式 SKU 库存和库位 |
| 仓库管理 | `external.packaged_items_v1` | 已关联正式 SKU 的包装库存项 |
| 仓库质检 | `external.sku_quality_inspections_v1` | SKU 质检记录 |
补充说明:`external.product_assets_v1` / `external.product_bom_v1` 是外部契约巡检覆盖的明细视图,但不在当前 `external_core_reader` 核心授权包内;外部系统确实需要逐条素材或 BOM 时,必须单独评审并追加白名单授权。
暂不开放:
- `public` schema 原表。
- 准 SKU 草稿包装过程数据。
- 质检 `metrics / attachments` 内部 JSON。
- 审批、权限、部门、团队、可见性等内部流程字段。
## 3. 生产执行顺序
1. 先确认目标服务器固定公网 IP 或内网 IP。
2. 在生产库完成迁移,确保本说明列出的 `external.*_v1` 视图已存在。
3. 创建专用只读账号,只授权外部核心视图。
4. 修改 PostgreSQL 网络白名单,只允许目标 IP 访问。
5. 从目标服务器实际连接测试。
6. 在生产侧跑外部只读契约巡检。
## 4. 数据库授权模板
在生产应用目录执行,密码从本地安全材料输入:
```bash
psql "$DATABASE_URL" \
-v database_name='<DB_NAME>' \
-v reader_role='external_core_reader' \
-v reader_password='<REDACTED_SECRET>' \
-f scripts/sql/grant-external-product-catalog-readonly.sql
```
该脚本只授予:
- `CONNECT` 当前数据库
- `USAGE` `external` schema
- `SELECT` 外部核心视图:
- `external.product_catalog_wide_v1`
- `external.intake_product_limits_v1`
- `external.warehouse_locations_v1`
- `external.sku_warehouse_stock_v1`
- `external.packaged_items_v1`
- `external.sku_quality_inspections_v1`
并撤销该角色对 `public` schema、`public` 表、`external` 未授权表/视图的权限。
## 5. PostgreSQL 网络白名单
`postgresql.conf` 至少确认:
```conf
listen_addresses = '*'
```
`pg_hba.conf` 增加一条只允许目标服务器的规则:
```conf
hostssl <DB_NAME> external_core_reader <TARGET_SERVER_IP>/32 scram-sha-256
```
如果当前没有启用 SSL,只能在受控内网/VPN 内临时使用:
```conf
host <DB_NAME> external_core_reader <TARGET_SERVER_IP>/32 scram-sha-256
```
不要写成 `0.0.0.0/0`。
## 6. 验证命令
从目标服务器验证:
```bash
psql "postgresql://external_core_reader:<REDACTED_SECRET>@<DB_HOST>:5432/<DB_NAME>" \
-c "SELECT COUNT(*) FROM external.product_catalog_wide_v1;"
```
验证仓库和质检明细:
```bash
psql "postgresql://external_core_reader:<REDACTED_SECRET>@<DB_HOST>:5432/<DB_NAME>" \
-c "SELECT COUNT(*) FROM external.sku_warehouse_stock_v1;"
psql "postgresql://external_core_reader:<REDACTED_SECRET>@<DB_HOST>:5432/<DB_NAME>" \
-c "SELECT COUNT(*) FROM external.sku_quality_inspections_v1;"
```
同时验证不能读业务原表:
```bash
psql "postgresql://external_core_reader:<REDACTED_SECRET>@<DB_HOST>:5432/<DB_NAME>" \
-c 'SELECT COUNT(*) FROM public."Product";'
```
第二条必须失败,出现 `permission denied` 才是正确结果。
生产侧再跑:
```bash
psql "$DATABASE_URL" -f scripts/sql/check-external-readonly-contract.sql
```
## 7. 同步到 JSReportReplica.app 中间库
如果外部系统不直接连本项目 PostgreSQL,而是统一从 `JSReportReplica` 中间库读取,本项目维护一份 `external` 核心视图到 `app` schema 的项目专属前缀表同步。
`JSReportReplica.app` 是多个项目共用的应用 schema,本项目同步目标表固定使用 `furniture3d_` 前缀,避免和其他项目的对外数据混用。源库视图仍是 `external.*`,只有中间库目标表加前缀。
默认同步表:
- `app.furniture3d_product_catalog_wide_v1`
- `app.furniture3d_intake_product_limits_v1`
- `app.furniture3d_warehouse_locations_v1`
- `app.furniture3d_sku_warehouse_stock_v1`
- `app.furniture3d_packaged_items_v1`
- `app.furniture3d_sku_quality_inspections_v1`
- `app.furniture3d_external_readonly_sync_runs`:同步运行日志。
主宽表仍然是:
```sql
SELECT *
FROM app.furniture3d_product_catalog_wide_v1;
```
同步入口:
```bash
npm run sync:external-jsreport
npm run sync:external-jsreport:apply
```
第一个命令只演练并输出源视图行数;第二个命令才会写入 `JSReportReplica.app`。脚本会先写临时表,再替换正式表,避免同步中途留下半截数据。
生产定时器安装入口:
```bash
bash scripts/install-external-jsreport-sync-timer.sh
```
安装脚本只会在生产服务器本地生成 `/opt/furniture-3d-system-prod/shared/external-jsreport-sync.env` 模板。真实连接信息只允许写在这个本地 env 文件里,并保持 `600` 权限,不要写入 Git、文档或群公告。
必要环境变量:
- `DATABASE_URL` 或 `EXTERNAL_JSREPORT_SOURCE_DATABASE_URL`:源 PostgreSQL 连接。
- `JSREPORT_REPLICA_HOST`
- `JSREPORT_REPLICA_PORT`
- `JSREPORT_REPLICA_DATABASE`
- `JSREPORT_REPLICA_USERNAME`
- `JSREPORT_REPLICA_PASSWORD`
- `JSREPORT_REPLICA_SCHEMA`
- `EXTERNAL_JSREPORT_TARGET_TABLE_PREFIX`:中间库目标表前缀,默认 `furniture3d_`。
默认定时频率是每 45 分钟同步一次。安装后 5 分钟触发首次同步,后续按上次同步完成时间间隔 45 分钟再次执行;可通过 `EXTERNAL_JSREPORT_SYNC_ON_BOOT_SEC` 调整首次触发延迟,通过 `EXTERNAL_JSREPORT_SYNC_INTERVAL` 调整同步间隔。
## 8. 规范审查
结论:`passed`
这是自审,不是独立复审。
- 架构:外部读取收口到 `external` 契约视图和专用只读角色,不开放业务原表;产品资料、仓库管理、仓库质检各有稳定视图。
- 视觉:不涉及前端视觉。
- 根因:通过数据库角色、schema 权限和网络白名单解决外部只读隔离,不靠口头约定。

View File

@ -8,8 +8,8 @@
- [ ] 任务范围没有失控,没有顺手改无关模块
- [ ] 关键文档已同步
- [ ] 先跑过 `npm run clean:local`
- [ ] `git status --short` 里没有不该提交的日志、缓存、临时文件
- [ ] `git status --short --branch` 已看过,知道哪些是本次改动、哪些是已有现场
- [ ] `git status --short` 里没有准备误提交的日志、缓存、临时文件
- [ ] 改动文件和任务单一致
- [ ] 自己能用一句话说清根因和修复方式
@ -45,6 +45,8 @@ npm run build
- [ ] 关键按钮/表单/保存动作能走通
- [ ] 关键接口返回正常
- [ ] 没有把别的入口改坏
- [ ] 前端视觉改动按 1920x1080 做过浏览器自查;移动端受影响时也补移动端自查
- [ ] 图片、AI、3D 或素材类页面确认主视觉不被说明块、统计块或弹层遮挡
---
@ -62,6 +64,7 @@ npm run build
- `docs/workflow/团队协作模式.md`
- `docs/workflow/模块边界说明.md`
- `docs/workflow/主线同步操作手册.md`
- 对应模块的专项说明文档
---

View File

@ -1,272 +1,297 @@
# 模块边界说明
目标:让人和 AI 在开工前先知道“哪里是新平台主链,哪里是旧兼容链,哪里不能乱碰”。
配套入口:
- 先读 `docs/workflow/团队协作模式.md`,确认你属于哪个工作域、该用什么分支名
- 再读 `docs/workflow/主线同步操作手册.md`,确认你当前分支该用 `merge origin/codex/xuxiake-mainline` 还是 `rebase origin/codex/xuxiake-mainline`
- 这份文档只负责讲边界,不额外创造新的角色体系或新的协作入口
- 这份文档只固定系统边界,不固定当前负责人;具体任务归属以当前任务单、交接摘要和已确认协作信息为准
适用范围:`furniture-3d-system/`。
目标:让前端、后端、数据库、AI 和包装协作时都知道自己该接在哪条链路上,避免把历史模块、新平台主链和运营入口混在一起。
---
## 1. 页面与入口边界
## 1. 页面入口边界
### 新平台主链入口
### 主业务链路
- `/`:新平台首页导航
- `/asset-center`:资产中心
- `/assisted-assembly`:辅助装配(手动拼装工作台入口)
- `/draft-groups`:草稿组列表
- `/draft-groups/[id]`:草稿组详情
- `/intake`:正式入库
- `/warehouse`:轻仓库
主业务链路是:
### 旧兼容链入口
```text
资产中心 -> 辅助装配 -> 草稿组 -> 产品资料/正式入库 -> 仓库管理/轻仓库
```
- `/products`
- `/products/[id]`
- `/products/[id]/assembly`
- `/products/[id]/skus/[skuId]`
- `/products/[id]/skus/[skuId]/assembly`
对应入口:
### 不要混的原则
- `src/app/asset-center/`
- `src/app/assisted-assembly/`
- `src/app/draft-groups/`
- `src/app/intake/`
- `src/app/warehouse/`
- 新平台入口负责“资产准备、辅助装配、草稿组承接、入库前整理”。
- 旧兼容链负责“历史产品、CAD 导入、板件/孔位/五金/装配步骤”。
- 不要把旧产品详情页重新改成新平台首页。
### 运营和工作台入口
---
## 2. 新平台主链边界
### 前端目录
这些入口服务工作台、看板、质检或 AI 生产,不等于主数据链路本身:
- `src/app/page.tsx`
- `src/app/asset-center`
- `src/app/assisted-assembly`
- `src/app/draft-groups`
- `src/app/intake`
- `src/app/warehouse`
- `src/components/platform`
- `src/app/home/`
- `src/app/intake/oa-board/`
- `src/app/warehouse-quality/`
- `src/app/ai-render-workbench/`
- `src/app/profile/`
- `src/app/users/`
- `src/app/admin/`
- `src/app/trash/`
### API 目录
### 旧 CAD 兼容入口
- `src/app/api/asset-center`
- `src/app/api/assisted-assembly`
- `src/app/api/draft-groups`
- `src/app/api/intake`
- `src/app/api/planning`
- `src/app/api/pre-skus`
- `src/app/api/series-templates`
- `src/app/api/warehouse`
旧 CAD 链路继续保留,但它不是新平台主链:
### 服务与仓库
- `src/app/products/`
- `src/app/products/[id]/`
- `src/app/products/[id]/assembly/`
- `src/app/products/[id]/skus/[skuId]/assembly/`
- `src/components/assembly/`
- `src/components/3d/`
- `src/server/services/asset-center.service.ts`
- `src/server/services/draft-group.service.ts`
- `src/server/services/intake.service.ts`
- `src/server/services/planning.service.ts`
- `src/server/services/series-template.service.ts`
- `src/server/services/warehouse.service.ts`
- `src/server/repositories/asset-center.repository.ts`
- `src/server/repositories/draft-group.repository.ts`
- `src/server/repositories/planning.repository.ts`
- `src/server/repositories/series-template.repository.ts`
- `src/server/repositories/warehouse.repository.ts`
### 纯规则与类型
- `src/lib/open-product`
- `src/lib/planning`
### 这里的业务真相是什么
- 资产族:`AssetFamily`
- 规格款:`AssetVariant`
- 草稿组:`DraftGroup`
- 准 SKU:`PreSku`
- 入库结果:正式 `Product / SKU`
### 不要做的事
- 不直接从辅助装配跳过草稿组生成正式产品。
- 不把历史 `auto-combinations` 旧链重新带回当前主线。
- 不把旧 `Product / SKU` 当作新平台组合阶段的数据真相。
- 不把包装做成绕过草稿组的独立系统。
旧链路可以修 bug,但不能把旧对象模型反向扩散到新平台主链。
---
## 3. 包装与仓库协作边界
## 2. 新平台主链模块
### 当前包装与仓库主要入口分四条
### 资产中心
- 轻仓库页面:`src/app/warehouse`
- 轻仓库接口:`src/app/api/warehouse`
- 新平台承接口:`src/app/api/pre-skus/[id]/packaging/route.ts`
- 旧正式产品口:`src/app/api/products/[id]/packaging`
职责:
### 主要服务与仓库
- 管理产品族、规格、材质、花色、元组件和基础资产。
- 为辅助装配、草稿组、产品资料和 AI 链路提供上游事实。
- `src/server/services/warehouse.service.ts`
- `src/server/repositories/warehouse.repository.ts`
- `src/server/services/packaging.service.ts`
- `src/server/repositories/packaging.repository.ts`
主要位置:
### 当前约定
- `src/app/asset-center/`
- `src/app/api/asset-center/`
- `src/components/platform/`
- 包装结果最终按单个 `PreSku` 保存。
- 正式产品包装链保留,作为兼容与发布后使用。
- 轻仓库工作区可以独立协作推进,但轻仓库承接的正式 `Product / SKU` 仍然属于主线真相,不在仓库侧重复造数。
- 工艺图组合本期只预留接口,不在主线里单独展开。
- 草稿组详情页里的包装区只负责主线最小联通版占位,不是最终包装工作区。
- 轻仓库可以增强包装成品、库存、库位和展示交互,但不能绕过正式入库单独制造主链结果。
不要在资产中心里直接完成正式入库、仓库流转或批次质检。
### 包装/仓库线可读但不默认直改的共享接缝
### 辅助装配
职责:
- 基于资产和元组件生成或维护装配方案。
- 输出可进入草稿组的中间结果。
主要位置:
- `src/app/assisted-assembly/`
- `src/app/api/assisted-assembly/`
- `src/lib/planning/`
- `src/app/api/planning/`
辅助装配不能绕过草稿组直接写正式产品。
### 草稿组
职责:
- 承接辅助装配结果。
- 管理进入产品资料前的草稿、预 SKU、批次和确认状态。
主要位置:
- `src/app/draft-groups/`
- `src/app/api/draft-groups/`
- `src/server/services/draft-group.service.ts`
- `src/server/repositories/draft-group.repository.ts`
草稿组是高共享接缝,改动时必须检查 `intake`、`pre-skus`、包装和仓库调用方。
### 产品资料和正式入库
职责:
- 管理正式入库前后的产品资料。
- 承接非集成实拍图、AI 白底图、素材库回传、SKU 和批次上下文。
主要位置:
- `src/app/intake/`
- `src/app/api/intake/`
- `src/server/services/intake.service.ts`
- `src/app/intake`
- `prisma/schema.prisma`
- `src/app/draft-groups/[id]`
- `src/lib/open-product/`
这些文件不是包装/仓库线的默认自由修改区。
如果任务必须改这里,先在任务单里写清原因、影响范围,再由主线负责人确认后再动。
非集成产品链路使用 `IntakeNonIntegratedProduct / IntakeNonIntegratedSkuItem`。这些对象不能和正式 `Product / SKU`、`AssetVariant` 或 `PreSku` 随便互换。
白话理解:
### 仓库管理和仓库质检
- 可以看,可以评估,可以提出接缝需求
- 不能在没有对齐的情况下直接开改
- 特别是 `draft-group`、`intake`、`schema.prisma` 这几处,本阶段只允许做边界说明,不允许顺手把实现也改掉
- 如果只是为了补包装 UI、材料选择器、最低成本方案,或补轻仓库展示与交互,不应默认落在这些共享接缝里完成
- 主线收口人检查同事分支时,如果这些共享接缝被改了,默认不要直接整支 `merge`;优先确认是否该 `cherry-pick` 或手工整合
职责:
### 包装与仓库负责人不要乱碰的地方
- `warehouse` 处理仓库管理和轻仓库流转。
- `warehouse-quality` 处理仓库质检工作台和质检任务。
- 辅助装配主链规则(含手动拼装工作台保存口径)
- 草稿组转正式 SKU 的规则
- 正式入库的核心落库规则
- 旧 CAD 的板件/孔位/五金推导链
主要位置:
- `src/app/warehouse/`
- `src/app/api/warehouse/`
- `src/app/warehouse-quality/`
- `src/app/api/warehouse-quality/`
仓库质检可以读取产品和 SKU 上下文,但不能在没有业务确认的情况下替代产品资料链路创建正式 SKU。
---
## 4. 旧 CAD 兼容链边界
## 3. SPU、SKU、barcode 和批次
### 页面目录
这四个概念是当前产品资料和素材库对接的硬边界:
- `src/app/products/[id]`
- `src/app/products/[id]/assembly`
- `src/app/products/[id]/skus/[skuId]`
- `src/app/products/[id]/skus/[skuId]/assembly`
- `src/components/assembly`
- `src/components/3d`
- SPU 是产品款。
- SKU 是 SPU 下的具体子项。
- barcode 是逐步收敛的唯一业务定位值。
- 批次是当前产品资料链路的批次上下文。
### API 目录
规则:
- `src/app/api/products/[id]/assembly`
- `src/app/api/products/[id]/panels`
- `src/app/api/products/[id]/holes`
- `src/app/api/products/[id]/import-hardware`
- `src/app/api/holes`
- `src/app/api/hardware`
- `src/app/api/product-hardware`
### 服务与库
- `src/server/services/assembly.service.ts`
- `src/server/services/panel.service.ts`
- `src/server/services/hole.service.ts`
- `src/server/services/hardware.service.ts`
- `src/server/services/sku-cad-import.helper.ts`
- `src/server/repositories/panel.repository.ts`
- `src/server/repositories/hole.repository.ts`
- `src/server/repositories/hardware.repository.ts`
- `src/server/repositories/product-hardware.repository.ts`
### 纯规则与共享链路
- `src/lib/assembly-generator.ts`
- `src/lib/panel-assembly-semantic.ts`
- `src/lib/sku-cad-data.ts`
- `src/lib/3d/engine.ts`
### 当前约定
- 装配语义是旧链增强能力,不是新平台主数据源。
- 旧链可以继续生成安装步骤和预览,但不要反向主导新平台资产模型。
- 不能把 SPU、SKU、barcode 当成同一个字段混用。
- 基于某个 SKU 换色得到的新 SKU,只能属于同一个 SPU。
- 换色得到的新 SKU 只能归属当前批次,不能漂到其他批次。
- 如果外部素材库要求 barcode,必须传真实产品条码,不能拿 SKU 编号或 SPU 编号代替。
- 如果一个功能还只是草稿或预览,命名里要写清楚 `draft`、`preview` 或 `pending`,不要伪装成正式 SKU。
---
## 5. 共享层边界
## 4. AI、图片和素材库边界
### 所有人都可能会碰到,但不要随手改
AI 白底图、AI 出图工作台、素材库回传和对象存储相关功能必须遵守:
- 图片上传后,数据库写入失败时要回滚刚上传的对象。
- 软删除、回收站记录仍然算引用,不能同步删除文件。
- 七牛或其他对象存储清理只能通过审计确认无引用后执行。
- 图片类页面以图片为第一视觉重点,不堆重复说明和统计块。
- 查看原图、缩放、拖拽、生成、上传素材库这些能力应就近服务当前产品、SKU、批次上下文。
主要位置:
- `src/app/ai-render-workbench/`
- `src/app/api/intake/`
- `src/app/api/storage/`
- `src/app/api/external/`
- `src/components/platform/`
素材库对接时必须严格区分 SPU、SKU、barcode 和产品分类。
---
## 5. 旧 CAD 兼容链路
旧链路主要围绕:
- `Product`
- `SKU`
- `Panel`
- `Hole`
- `ProductHardware`
- 旧 3D 组装和板件孔位编辑
主要位置:
- `src/app/products/`
- `src/app/api/products/`
- `src/app/api/skus/`
- `src/app/api/panels/`
- `src/app/api/holes/`
- `src/app/api/product-hardware/`
- `src/components/assembly/`
- `src/components/3d/`
旧链路可以和新平台共享基础数据,但不能把新平台的草稿组、资产中心、非集成产品资料逻辑硬塞进旧 CAD 编辑器。
---
## 6. 共享基础层
下面位置属于全项目共享基础层:
- `prisma/schema.prisma`
- `prisma/migrations`
- `src/server/services/index.ts`
- `src/server/repositories/index.ts`
- `prisma/migrations/`
- `src/app/layout.tsx`
- `src/components/layout/`
- `src/components/platform/`
- `src/app/api/platform/schemas.ts`
- `src/app/api/storage/`
- `src/lib/prisma.ts`
- `src/server/repositories/`
- `src/server/services/`
共享层边界的意思是:
要求:
- 这些地方一改,通常会同时影响主链、包装线或兼容链
- 所以默认先对齐影响面,再决定谁来改,不是谁先看到谁就顺手动
### 共享层规则
- 任何 Prisma 模型调整,都必须带迁移。
- 任何新接口,都要有明确输入输出 DTO。
- 任何跨模块字段变更,都要先同步文档,再改代码。
- 没有明确任务授权时,不要顺手扩写到 `draft-group`、`intake`、`schema.prisma` 这些敏感接缝实现。
- 前端页面优先使用平台组件和元组件,不绕开组件库单独造一套视觉。
- API 保持 `Route -> Service -> Repository` 分层,不在 route 里堆业务逻辑。
- Prisma schema 变化必须配套 migration,不能在生产或预生产直接 `prisma db push`。
- 共享服务改动后必须看调用方,不能只跑当前页面。
---
## 6. 看到这些情况要先停一下
## 7. 视觉边界
- 一个任务同时改新平台主链和旧兼容链,但任务单里没写原因。
- 包装任务直接改共享接缝文件,但没先和主线负责人对齐。
- Route 里开始直接写 Prisma 查询。
- 包装开始自己造一套 `Product / SKU`。
- 页面样式开始绕开 `src/components/platform` 和既有视觉标准。
- 改动影响命名、BOM、包装、安装步骤,但没说明数据来源。
- 分支改动会影响 `intake / warehouse / Product / SKU` 承接链,但交接里没说明对正式入库或轻仓库的影响。
当前视觉标准:
- 第一标准:白曜石工坊统一视觉。
- 实现载体:项目组件库、元组件和 `src/components/platform/`。
- 外部设计稿:Stitch 是规格和验收标准,不替代项目组件体系。
- 项目内参考:资产中心页面优先级最高。
不要做:
- 普通 SaaS 卡片墙。
- 营销式大 hero。
- 旧苹果风玻璃大面积正文。
- 默认黑底后台。
- 一堆重复说明块、重复统计块和无效状态块。
表格、表单、Inspector、列表和长文本必须优先保证可读性。
---
## 7. 开工前最少要看什么
## 8. 数据库和迁移边界
### 做新平台主链
数据库相关任务先看:
先看:
- `docs/workflow/数据库迁移操作手册.md`
- `docs/开发指南/数据库基础命令速查.md`
- `prisma/schema.prisma`
- `prisma/migrations/`
- `docs/workflow/团队协作模式.md`
- `src/components/platform/platform-navigation.ts`
- `src/lib/open-product/types.ts`
- `src/lib/open-product/assembly-workspace.ts`
- `src/lib/open-product/manual-assembly-launch.ts`
- `src/server/services/draft-group.service.ts`
规则:
### 做包装或仓库协作
- 结构变化走 migration。
- 数据修复先只读审计,再写修复脚本。
- 生产数据操作必须有用户明确授权。
- 不把一次性修复脚本伪装成长期业务逻辑。
先看:
---
- `docs/workflow/团队协作模式.md`
- `docs/开发指南/包装方案增强/包装方案增强-同事交接说明.md`
- `src/app/warehouse`
- `src/app/api/warehouse`
- `src/app/api/pre-skus/[id]/packaging/route.ts`
- `src/server/services/packaging.service.ts`
- `src/server/services/draft-group.service.ts`
## 9. 改动前最小检查
### 做旧 CAD 兼容链
不知道该从哪里开始时,先看这些文件:
先看:
- `AGENTS.md`
- `src/app/page.tsx`
- `src/components/layout/`
- `src/components/platform/`
- `src/app/asset-center/`
- `src/app/intake/`
- `src/app/draft-groups/`
- `src/app/api/platform/schemas.ts`
- `prisma/schema.prisma`
- `docs/solutions/workflow-issues/CAD插件装配语义导出改造说明-20260325.md`
- `src/app/products/[id]/assembly/AssemblyEditorWorkspace.tsx`
- `src/components/assembly/AssemblyEditorClient.tsx`
- `src/server/services/assembly.service.ts`
- `src/lib/assembly-generator.ts`
如果任务涉及生产、部署、数据库或对象存储,先只读核查现场,再决定是否写入。
---
## 10. 判断是否越界
出现下面情况,先停下来重新确认:
- 为了修一个页面,改了 schema、全局 layout 或共享 service。
- 为了一个 SKU,把 SPU、barcode、批次一起改了。
- 为了临时过 UI,把旧 CAD 模型塞进新平台主链。
- 为了清理文件,准备删除软删除或回收站仍引用的对象。
- 为了追视觉,把页面绕开组件库重写成另一套体系。
- 为了赶部署,跳过主线同步、构建、健康检查或规范审查。
越界不一定代表不能做,但必须说明原因、影响范围、验证方式和回滚路径。

View File

@ -15,9 +15,14 @@
## 本仓库部署脚本
- 外层仓库脚本:`../scripts/create-yydd-prod-deploy.ps1`
- 首选入口:仓库根目录 `scripts/deploy-yydd-prod-from-git.ps1`
- 底层打包脚本:仓库根目录 `scripts/create-yydd-prod-deploy.ps1`
执行部署或生产核查前,优先读取上面脚本的默认参数,并在远端实时确认当前 PM2 进程、`current` 软链接和健康检查结果。该脚本生成的远端发布脚本会执行 `npx prisma migrate deploy`,因此生产发布前必须先确认迁移目录、迁移历史和主数据 strict 巡检口径没有分叉。
执行部署或生产核查前,优先读取上面两个脚本的当前默认参数,并在远端实时确认当前 PM2 进程、`current` 软链接和健康检查结果。部署入口默认从 `origin/main` 取代码,发布前必须核对远端 `refs/heads/main` 和本地 `origin/main` 是否一致。
底层发布流程会执行依赖安装、`prisma generate`、迁移、构建、PM2 重启和健康检查。因此生产发布前必须先确认迁移目录、迁移历史和主数据 strict 巡检口径没有分叉。
只做生产/目标库地基核查时,优先在远端应用目录执行 `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` 才能把这份证据计入生产级地基收口。
## 花名册同步
@ -29,6 +34,57 @@
- 定时频率:每天 `03:00` 和 `15:00`。
- 源库连接信息只允许放在生产本地 `/opt/furniture-3d-system-prod/shared/employee-directory-sync.env`,不能写入 Git 或共享文档。
- 生产应用内部接口密钥继续从 `/opt/furniture-3d-system-prod/shared/.env` 的 `HR_SYNC_SECRET` 读取。
- 人员信息表的钉钉组织同步使用 `furniture-3d-system/scripts/install-dingtalk-directory-sync-timer.sh`,生产 systemd 名称为 `furniture-dingtalk-directory-sync.service` / `furniture-dingtalk-directory-sync.timer`。
- 钉钉同步每天 `03:00` 执行一次,读取生产共享 `.env` 中的 `DINGTALK_API_CLIENT_ID` 和 `DINGTALK_API_CLIENT_SECRET`,并自动更新人员信息;正式页面仍保持只读。钉钉网页登录使用独立的 `DINGTALK_LOGIN_CLIENT_ID`、`DINGTALK_LOGIN_CLIENT_SECRET`,两组凭据不能混用。
- 人员信息的每日更新口径:钉钉新人自动新增一行;在职人员按钉钉更新部门/负责人;人工覆盖(manualOverride)的行保持不变;**离职人员不做删除,只在页面把「在职状态」变为离职**(方案 B:先比对上一轮在册且本轮抓取范围内消失的人,再逐个调 `user/get` 确认 60121 用户不存在才标记;抓取人数明显缩水时整轮跳过并写入告警)。
- 首次部署后需在正式服务器执行该安装脚本,并手动执行一次 `npm run sync:dingtalk-directory:apply` 完成首次填充;后续由 systemd 每日执行。
## BOM 成本参考镜像
- 用户核算请求只读取本地 PostgreSQL 镜像,不在请求中连接上游 `rpt.pm_bomBaseInfo`。
- 低压只读预检:`npm run sync:bom-cost-reference -- --dry-run --max-pages=2`;默认单连接、每页 100 行、完整页之间等待 250ms。
- 真实同步必须显式传 `--apply`、固定确认词、目标 PostgreSQL 身份和来源 SQL Server 身份;上游登录必须是 `report_reader`,`app_writer` 会被程序拒绝。
- 生产定时器安装脚本:`furniture-3d-system/scripts/install-bom-cost-reference-sync-timer.sh`。
- 生产 systemd 名称:`furniture-bom-cost-reference-sync.service` / `furniture-bom-cost-reference-sync.timer`。
- 默认调度:每天中国标准时间 `02:00`,附带最多 10 分钟随机延迟;安装时可通过非敏感环境变量调整。
- 上游连接继续只使用生产共享 `.env` 中现有的 `JST_PLANNING_SQLSERVER_* / report_reader`;不在第二个文件中重复保存密码。
- 首次运行安装脚本只会创建权限为 `600` 的 `/opt/furniture-3d-system-prod/shared/bom-cost-reference-sync.env` 门禁与限速模板;目标/来源身份未填齐或现有只读配置不完整时拒绝安装定时器。
- 本地参考行只保留最新成功批次,避免每日约 76 万行无限增长;历史场景在自己的明细快照中保留来源行、指纹、单价、规则和原始计算依据,不依赖旧镜像行回查。
- 镜像新鲜度按最新成功批次的完成时间判断。上游当前可见的创建时间只作审计展示,不能用来判断当天同步是否过期。
- 准确度回测:`npm run backtest:bom-cost-reference -- --sample-size=30`,只读最新成功镜像,不访问上游;样本限制 20 至 50 个,任一可比较 SKU 超过固定 10% 误差门槛即失败。
- 迁移、真实同步、安装/启用定时器和生产回测都必须另行获得生产操作授权;部署代码本身不代表这些动作已经执行。
## 商品 SABC 月度核算
- 核算命令:`npm run run:product-sabc -- --year=2026 --month=8 --mode=APPLY --confirm=run-product-sabc`;试算(只算不写库)用 `--mode=PREVIEW`,末尾加 `--skip-if-exists` 可跳过已生成月份。
- 8 月人工表对账命令:`npm run compare:product-sabc-xlsx`,默认读取本机下载的人工工作簿;生产执行前应先完成产品全量档案日销售数据同步。
- 生产定时器安装脚本:`furniture-3d-system/scripts/install-product-sabc-timer.sh`。
- 生产 systemd 名称:`furniture-product-sabc-monthly.service` / `furniture-product-sabc-monthly.timer`。
- 默认调度:每月 3 日中国标准时间 `03:00`,附带最多 10 分钟随机延迟;服务生成上一个自然月的数据,已生成过则跳过(不覆盖管理员的重算结果)。
- 生成即冻结:每次生成会把整行明细写入 `SupplyChainProductArchiveSabcResult.snapshot`,页面、筛选下拉、导出和经营总览都只读取该快照。生成之后档案的销售状态、人员字段等变化都不会再改变已生成月份的数据。
- 重新生成(页面「执行核算」)仅限管理员(`ADMIN` / `SUPER_ADMIN`);其他账号只能查看冻结结果。本地一键刷新 `node scripts/local-sync-archive.mjs` 在每月 3 日前会跳过 SABC 步骤,需要立刻重算时用 `--force-sabc`。
- SABC 的销售收款总金额、真实业绩、发货件数、商品毛利额和售后金额统一读取 `SupplyChainProductArchiveSalesPerformanceDaily`,与产品全量档案页面保持同一月度口径。
- 首次安装生成权限为 `600` 的 `/opt/furniture-3d-system-prod/shared/product-sabc-timer.env`,默认 `PRODUCT_SABC_TIMER_MODE=APPLY`;如需先观察可临时改为 `PREVIEW`(只试算不写库、不冻结)。
- SABC 自动任务只读取现有 `DATABASE_URL`;上游日销售同步任务继续使用 `JST_REPORT_DB_*`,凭据不写入定时器配置。安装、迁移、首次 APPLY 和生产核算都需要单独授权。
## 操作日志保留策略
- 清理脚本:`npm run db:cleanup-operation-logs` 默认只做 dry-run;只有显式传入 `--apply --confirm=cleanup-operation-logs` 才会删除数据。
- 自动清理范围严格限定为 `OperationLog` 中 `SYSTEM + SUPPLY_CHAIN_PRODUCT_ARCHIVE + details.kind=UPDATED + SUCCESS` 且创建时间早于 30 天的字段变更。
- 用户操作、商品档案导入、失败/重试日志及其他业务范围日志至少保留 365 天;本次策略不自动删除超过 365 天的审计日志。
- 生产安装入口:`furniture-3d-system/scripts/install-operation-log-retention-timer.sh`。
- 生产 systemd 名称:`furniture-operation-log-retention.service` / `furniture-operation-log-retention.timer`,默认每天中国标准时间 `04:30` 执行,附带最多 10 分钟随机延迟。
- 安装脚本会在 `/opt/furniture-3d-system-prod/shared/operation-log-retention.env` 保存权限为 `600` 的数据库身份门禁和非敏感参数;身份未填写或保留天数不是 30 天时拒绝启用。
- 首次启用前必须先在生产执行 dry-run,核对候选数量、数据库身份和 systemd 落点;生产安装、首次 `--apply` 和后续代码部署均需另行获得授权。
## 停产需求确认超时收口
- 收口脚本:`furniture-3d-system/scripts/finalize-production-stop-confirmations.ts`,把超过确认截止时间仍未反馈的停产需求按「默认同意停产」落库,并给未反馈的确认人记 `TIMEOUT` 票。
- 生产安装入口:`furniture-3d-system/scripts/install-production-stop-confirmation-finalize-timer.sh`。
- 生产 systemd 名称:`furniture-production-stop-confirmation-finalize.service` / `furniture-production-stop-confirmation-finalize.timer`,默认每 10 分钟检查一次(北京时间),附带最多 1 分钟随机延迟;可用 `PRODUCTION_STOP_FINALIZE_CALENDAR` 覆盖调度。
- 收口逻辑幂等:只处理「已过截止时间且仍待确认」的需求,重复执行不会重复收口;失败由 systemd 在 5 分钟后自动重试。
- 运行日志:`journalctl -u furniture-production-stop-confirmation-finalize -n 100 --no-pager`,每次运行输出本次收口的需求数量。
- 首次安装与启用:在项目根执行 `bash scripts/install-production-stop-confirmation-finalize-timer.sh`(需 root,脚本写入 `/etc/systemd/system/` 并 `enable --now`);安装/启用定时器属于生产操作,需要另行获得授权。
## 明确排除
@ -44,4 +100,18 @@
2. 远端存在 `/opt/furniture-3d-system-prod`。
3. PM2 中存在 `furniture-3d-system-prod`。
4. 应用监听端口是 `3001`。
5. 不把密码、密钥或连接串写入 Git、文档或长期记忆。
5. 部署来源是 `origin/main`,且远端 `main` 和本地 `origin/main` 一致。
6. 生产只读地基核查优先使用 `npm run db:foundation:audit:production-readonly`。
7. 生产只读 manifest 回传后,使用 `npm run db:foundation:verify-production-readonly -- --manifest <manifest.json>` 验收,不能人工挑几个字段就宣称生产通过。
8. 不把密码、密钥或连接串写入 Git、文档或长期记忆。
## 部署后核查
部署后不能只看公网 `/api/health`。至少补核:
- 远端 `REVISION` 或当前发布目录 commit。
- `pm2 describe furniture-3d-system-prod`。
- 应用端口 `3001` 是否监听。
- 最近错误日志是否有启动失败、数据库失败、对象存储失败。
- `https://cp.jiansun.vip/api/health`。
- 关键页面:首页、产品资料、仓库质检、非集成详情或本次改动涉及页面。

View File

@ -14,6 +14,8 @@
- 有没有明显偏离当前统一视觉标准
- 新平台主链页面、后台管理页面是否优先符合 `白曜石工坊统一视觉标准.md`
- 是否仍把旧苹果风格规范当作当前视觉依据;新平台验收只按白曜石工坊口径判断
- 是否优先使用 `src/components/platform`、元组件和组件库,而不是另起一套页面皮肤
- 如果有 Stitch 设计稿,是否在组件库体系内对齐它的密度、层级、控件、留白、字号和低噪音表达
- 有没有临时拼凑感很强的新界面
- 新页面和现有页面的层级、间距、交互有没有失控
@ -38,5 +40,6 @@
- 明显破坏模块边界
- 明显破坏统一视觉
- 绕开组件库另造一套新平台视觉
- 明显只是补丁式修复
- 没做最小验证就宣称完成

View File

@ -24,10 +24,19 @@
- 每天开始开发前,先同步一次最新 `origin/codex/xuxiake-mainline`
- 主负责人刚推了重要改动后,再同步一次
- 自己准备提交前,再同步一次
- 每次回答“主线有没有新提交”前,先 `git fetch origin`
白话理解:
不要拿旧主线继续往下改。
推荐固定检查:
```bash
git fetch origin
git status --short --branch
git rev-list --left-right --count HEAD...origin/codex/xuxiake-mainline
```
### 铁律 2:每次任务先写清边界
- 先说这次只改什么

View File

@ -1,6 +1,8 @@
# 3D 装配动画使用指南
> 如何使用装配动画功能展示家具组装过程
>
> 当前状态:历史兼容指南。旧文中的 Apple 风格表述只代表当时实现口径;新平台 3D、装配、资产工作台视觉以白曜石工坊标准、平台组件和当前资产中心样板为准。
---
@ -10,7 +12,7 @@
- ✅ 流畅播放 3D 装配动画(60 FPS)
- ✅ 分步骤展示组装过程
- ✅ 支持播放控制(播放/暂停/跳转)
- ✅ Apple 风格的精美 UI
- ✅ 历史实现使用 Apple 风格 UI;新平台应按白曜石工坊和平台组件重整
- ✅ 响应式设计
---
@ -105,7 +107,9 @@ interface Panel {
## 🎨 UI 特性
### Apple 风格设计
### 历史 Apple 风格设计
当前新平台不再把 Apple 风格作为第一标准;这里仅用于理解旧实现。
- **流畅动画**:使用 GSAP 实现 60 FPS 动画
- **毛玻璃效果**:`backdrop-blur-md`
@ -240,7 +244,7 @@ GLTF 节点名称应该清晰易懂:
- [Three.js 文档](https://threejs.org/docs/)
- [GSAP 文档](https://greensock.com/docs/)
- [Apple HIG 2025](https://developer.apple.com/design/human-interface-guidelines/)
- [Apple HIG 2025](https://developer.apple.com/design/human-interface-guidelines/):历史参考,不作为当前新平台验收标准
---

View File

@ -1,5 +1,7 @@
# 产品上传 API 使用指南
> 当前状态:历史兼容指南。本文记录旧产品上传链路,里面出现的 Apple HIG、MinIO 等表述不能作为当前新平台视觉或生产存储真相。新前端视觉以 `docs/设计规范/白曜石工坊统一视觉标准.md` 和 `docs/开发指南/组件使用指南.md` 为准;对象存储以当前代码、`src/lib/storage`、对象存储接入文档和生产环境变量核对为准。
## 概述
产品上传功能已完成,支持批量上传 3D 模型文件并自动创建产品和 SKU。
@ -7,13 +9,13 @@
## 功能特性
✅ **已完成**:
- Apple HIG 2025 风格的上传弹窗
- 历史实现曾使用 Apple HIG 2025 风格上传弹窗;新界面应改按白曜石工坊和平台组件口径
- 拖拽上传和点击选择文件
- 批量上传多个文件
- 实时上传进度显示
- 文件验证(格式、大小)
- 产品信息表单
- 文件上传到 MinIO
- 历史实现上传到 MinIO;当前对象存储真相需按对象存储接入文档和代码核对
- 自动创建产品和 SKU 记录
- 上传成功后自动刷新列表
@ -84,7 +86,7 @@ GET /api/products
4. 拖拽或选择 3D 模型文件
5. 显示上传进度
6. 点击"确认上传"
7. 文件上传到 MinIO
7. 文件上传到历史 MinIO 存储;当前实现需按对象存储接入文档核对
8. 创建产品和 SKU 记录
9. 自动刷新产品列表
@ -100,7 +102,7 @@ GET /api/products
确保 `.env` 文件中配置了以下变量:
```env
# MinIO 配置
# 历史 MinIO 配置
S3_ENDPOINT=http://localhost:9000
S3_REGION=us-east-1
S3_BUCKET=furniture-models
@ -118,7 +120,7 @@ DATABASE_URL=postgresql://postgres:postgres@localhost:5432/furniture3d
npm run dev
```
2. 确保 MinIO 和 PostgreSQL 正在运行
2. 历史本地联调需确保 MinIO 和 PostgreSQL 正在运行;当前存储联调按对象存储接入文档核对
3. 访问 http://localhost:3000
@ -131,7 +133,7 @@ DATABASE_URL=postgresql://postgres:postgres@localhost:5432/furniture3d
## 故障排查
### 上传失败
- 检查 MinIO 是否运行:访问 http://localhost:9000
- 历史 MinIO 联调检查:访问 http://localhost:9000;当前对象存储问题按现行存储配置排查
- 检查数据库连接:运行 `npm run prisma:studio`
- 查看浏览器控制台错误信息
- 查看服务器日志

View File

@ -1,5 +1,7 @@
# 包装方案增强同事交接说明(新平台一期版)
当前状态:包装专项历史交接。本文适合理解包装增强任务背景,但当前分支、主线、视觉标准、共享接缝和路径判断必须以仓库根目录 `AGENTS.md`、`docs/workflow/团队协作模式.md`、`docs/workflow/模块边界说明.md` 和当前代码为准。
## 1. 先说结论
这份文档是给接手「包装方案增强」任务的同事看的。

View File

@ -1,5 +1,7 @@
# 包装方案增强操作手册(非程序员版,新平台一期)
当前状态:包装专项历史说明。本文可以帮助理解包装功能,但涉及分支、主线、视觉和代码路径时,必须以仓库根目录 `AGENTS.md`、workflow 文档和当前代码为准。
## 1. 先看结论
你这次要做的事情只有一条:

View File

@ -3,6 +3,8 @@
适用对象:接手「新平台包装子工作区」的 Codex。
目标:让你直接在现有后端底座上继续做包装 UI、算料和最低成本方案,不要再重定 DTO、数据表和入库规则。
当前状态:包装专项历史交接。本文仍可用于理解包装底座,但分支、主线、视觉、共享接缝和路径判断必须先看仓库根目录 `AGENTS.md`、`docs/workflow/团队协作模式.md`、`docs/workflow/模块边界说明.md`、`docs/workflow/主线同步操作手册.md`,再核对当前代码。不要把本文里的固定包装分支或旧协作口径当成当前唯一规则。
---
## 1. 先说结论

View File

@ -1,653 +1,170 @@
# Apple 风格 UI 组件使用指南
# 平台组件与元组件使用指南
> **⚠️ 重要提示**: 本文档提供代码示例,但设计标准以 `docs/设计规范/苹果风格视觉标准.md` 为准。在使用代码示例时,请确保符合最新的设计 Token(如颜色、字体等)。
适用范围:新平台页面、后台管理页、产品资料、AI 白底图、资产中心、仓库质检等前端改造。
本指南提供了 Apple 风格 UI 设计系统中所有组件的详细使用说明和最佳实践。
当前项目不再以旧 Apple 风格组件作为新页面标准。旧 `src/components/ui` 里的组件和旧苹果风文档只作为历史兼容参考;新平台默认优先使用平台组件、元组件和白曜石工坊视觉口径。
## 目录
---
1. [设计令牌](#设计令牌)
2. [基础组件](#基础组件)
3. [导航组件](#导航组件)
4. [数据展示组件](#数据展示组件)
5. [反馈组件](#反馈组件)
6. [媒体组件](#媒体组件)
7. [布局组件](#布局组件)
8. [动画组件](#动画组件)
9. [可访问性工具](#可访问性工具)
10. [性能优化](#性能优化)
## 1. 先记住一句话
## 设计令牌
新平台前端优先顺序是:
### 字体系统
```typescript
import { LargeTitle, Title1, Body, Caption1 } from '@/components/ui/Typography'
function MyComponent() {
return (
<div>
<LargeTitle>大标题 (34pt)</LargeTitle>
<Title1>标题 1 (28pt)</Title1>
<Body>正文内容 (17pt)</Body>
<Caption1>说明文字 (12pt)</Caption1>
</div>
)
}
```text
业务组件和元组件 -> src/components/platform -> 已有页面样板 -> 基础 UI 组件 -> 临时自写样式
```
**最佳实践:**
- 使用语义化的字体组件而不是直接的 CSS 类
- 遵循苹果的字体层级,不要跳级使用
- 中英文混排时使用 `fontType="chinese"` 属性
不要为了一个页面单独造一套皮肤。
如果 Stitch 设计稿和当前组件默认皮肤不一致,应该在组件库体系内做变体、重写皮肤或局部重构,而不是绕开组件库硬写静态稿。
### 颜色系统
---
```typescript
// 使用 Tailwind 类名
<div className="bg-apple-blue-light text-white">
蓝色背景
</div>
## 2. 当前第一视觉标准
// 使用设计令牌
import { appleColorTokens } from '@/lib/design-tokens'
const blueColor = appleColorTokens.system.blue.light
- 第一视觉标准:`docs/设计规范/白曜石工坊统一视觉标准.md`
- 规范审查:`docs/workflow/规范审查清单.md`
- 页面样板:资产中心页面
- 核心组件目录:`src/components/platform/`
页面应保持:
- 浅色工作台。
- 低噪音。
- 主体内容优先。
- 图片、3D、数据和操作路径优先。
- 表格、Inspector、表单和长文本可读性优先。
不要回到:
- 旧苹果风大玻璃组件。
- 普通 SaaS 卡片墙。
- 默认黑底后台。
- 营销式大 hero。
- 一堆解释性卡片和重复统计块。
---
## 3. 平台组件怎么选
优先看 `src/components/platform/`:
- `PlatformPageFrame`:页面骨架和工作台外框。
- `PlatformSplitLayout`:左右分栏,适合主画布 + Inspector。
- `PlatformSection`:工作台内的低噪音区块。
- `PlatformFlowNav`:平台流程导航。
- `PlatformHeaderTools`:顶部工具区。
- `PlatformMetricStrip`:少量关键指标条,不能堆成统计墙。
- `ViewportStageShell`:视口、画布、图片或 3D 工作台容器。
- `AssemblyWorkbench*`:装配和资产工作台相关承载。
- `AssetVariantThumbnailRenderer`、`AssemblyMaterialImage`:资产图和材质图展示。
使用前先搜现有调用:
```bash
rg -n "PlatformSplitLayout|PlatformPageFrame|ViewportStageShell|PlatformSection" src
```
**最佳实践:**
- 优先使用语义化颜色(如 `apple-bg-primary`)
- 当前产品固定浅色主题,不再提供深浅色切换
- 使用系统颜色保持一致性
---
## 基础组件
## 4. 元组件表达
### Button 按钮
板材、五金、组件、包装、花色、贴图、资产规格款都不是普通图片卡片。它们应按 `Meta Component(元组件)` 表达:
```typescript
import Button from '@/components/ui/Button'
- 有稳定业务身份。
- 有规格、材质、状态、来源。
- 能打开 Inspector 检视。
- 能进入装配、草稿、入库、仓库或质检链路。
- 能被 AI、素材库或对象存储链路追踪。
function ButtonExample() {
return (
<div className="space-x-4">
{/* 四种变体 */}
<Button variant="filled">Filled</Button>
<Button variant="tinted">Tinted</Button>
<Button variant="gray">Gray</Button>
<Button variant="plain">Plain</Button>
{/* 三种尺寸 */}
<Button size="small">Small (32px)</Button>
<Button size="regular">Regular (44px)</Button>
<Button size="large">Large (50px)</Button>
{/* 状态 */}
<Button loading>Loading</Button>
<Button disabled>Disabled</Button>
</div>
)
}
如果只是展示图片,也要保留业务身份和状态,不要退化成“图片 + 标题 + 说明”的营销卡片。
---
## 5. 图片和 AI 页面
AI 白底图、素材库、质检九视图、原图预览这类页面要把图片放在第一优先级:
- 图片网格要稳定,不因状态文字改变尺寸。
- 缺图要用明确空状态,不显示坏图图标。
- 原图查看要避免被页面内其他卡片盖住。
- 缩放、拖拽、重新生成、上传素材库等操作应贴近图片工作流。
- 不要堆重复说明、重复计数和无效提示。
目标花色、SPU、SKU、barcode、批次等字段必须从业务上下文传递,不要靠页面文案兜底。
---
## 6. Inspector 和表单
Inspector 是长期阅读和编辑区,不使用大面积毛玻璃。
推荐:
- 实体白底。
- 清楚边界。
- 紧凑但不拥挤。
- 字段分组少而明确。
- 关键操作按钮固定在可预期位置。
避免:
- 把说明文字堆进 Inspector。
- 用半透明背景承载正文。
- 用多个统计块挤压主图或主表单。
- 表单下拉展开后改变整体页面宽高或遮挡关键按钮。
---
## 7. Stitch 设计稿落地
当用户指定 Stitch 设计稿时:
- Stitch 是当前功能的视觉规格和验收标准。
- 项目组件库是实现载体。
- 资产中心是项目内参考样板。
- 低噪音、抓重点、图片优先是默认方向。
落地时重点对齐:
- 密度。
- 控件大小。
- 留白。
- 字号。
- 圆角。
- 阴影。
- 列表和表单层级。
- hover、focus、disabled、loading、empty、error 状态。
不要自行补设计稿里没有的说明区、统计区或装饰区,除非用户明确要求。
---
## 8. 基础 UI 组件使用边界
`src/components/ui/` 可以继续服务历史页面和基础控件,但新平台不能只看这个目录就定视觉方向。
使用原则:
- 能用平台组件就先用平台组件。
- 需要按钮、输入框、下拉、弹层等基础控件时,可以复用现有 UI 组件。
- 如果基础控件默认样式和白曜石/Stitch 冲突,应通过组件变体或局部样式修正。
- 不新增 Apple 命名的新组件,不新增深浅色主题切换入口。
---
## 9. 验证要求
前端改动至少自查:
- 1920x1080 下页面不拥挤、不重叠、不遮挡主内容。
- 关键移动端宽度没有明显炸版。
- 图片、3D、表格、Inspector、下拉、弹层状态都能正常显示。
- 文案不溢出按钮、标签、卡片或表格单元格。
- 没有引入一套和平台组件明显割裂的新皮肤。
收口时必须给规范审查结论:
```text
规范审查:passed / passed with warnings / rejected
这是自审,不是独立复审。
```
**最佳实践:**
- 主要操作使用 `filled` 变体
- 次要操作使用 `tinted` 或 `gray` 变体
- 确保按钮最小尺寸为 44px(苹果触摸目标标准)
- 使用 `loading` 状态而不是禁用按钮
### Input 输入框
```typescript
import Input from '@/components/ui/Input'
function InputExample() {
return (
<div className="space-y-4">
{/* 基础输入框 */}
<Input
label="用户名"
placeholder="请输入用户名"
helperText="用户名必须唯一"
required
/>
{/* 状态变体 */}
<Input
label="邮箱"
type="email"
state="success"
helperText="邮箱格式正确"
/>
<Input
label="密码"
type="password"
errorText="密码长度至少 8 位"
/>
{/* 带图标 */}
<Input
label="搜索"
leftIcon="🔍"
rightIcon="✕"
onRightIconClick={() => console.log('清除')}
/>
</div>
)
}
```
**最佳实践:**
- 始终提供 `label` 属性以支持可访问性
- 使用 `helperText` 提供额外说明
- 错误状态使用 `errorText` 而不是 `helperText`
- 合理使用图标增强用户体验
### Card 卡片
```typescript
import Card, { CardHeader, CardTitle, CardContent, CardFooter } from '@/components/ui/Card'
function CardExample() {
return (
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* 简单卡片 */}
<Card className="p-6">
<h3 className="text-lg font-semibold mb-2">卡片标题</h3>
<p className="text-gray-600">卡片内容</p>
</Card>
{/* 组合卡片 */}
<Card variant="elevated">
<CardHeader>
<CardTitle>高级卡片</CardTitle>
</CardHeader>
<CardContent>
<p>使用组合组件构建的卡片</p>
</CardContent>
<CardFooter>
<Button size="small">操作</Button>
</CardFooter>
</Card>
{/* 交互式卡片 */}
<Card
interactive
onClick={() => console.log('卡片点击')}
className="cursor-pointer"
>
<div className="p-6">
<h3>可点击卡片</h3>
<p>支持 hover 和 active 动画</p>
</div>
</Card>
</div>
)
}
```
**最佳实践:**
- 使用 `variant="elevated"` 创建层次感
- 交互式卡片添加 `interactive` 属性
- 保持卡片内容简洁,避免过度嵌套
## 导航组件
### NavigationBar 导航栏
```typescript
import NavigationBar, { BackButton, MenuButton } from '@/components/ui/NavigationBar'
function NavigationExample() {
return (
<div className="space-y-4">
{/* 大标题模式 */}
<NavigationBar
mode="large"
title="大标题导航"
subtitle="副标题说明"
leftContent={<BackButton onBack={() => history.back()} />}
rightContent={<MenuButton onMenu={() => console.log('菜单')} />}
/>
{/* 标准模式 */}
<NavigationBar
mode="standard"
title="标准导航"
leftContent={<BackButton />}
rightContent={
<div className="flex gap-2">
<Button size="small" variant="plain">编辑</Button>
<Button size="small" variant="tinted">保存</Button>
</div>
}
/>
</div>
)
}
```
### TabBar 标签栏
```typescript
import TabBar, { useTabBar } from '@/components/ui/TabBar'
function TabBarExample() {
const tabBar = useTabBar('home')
const tabs = [
{ id: 'home', label: '首页', icon: '🏠' },
{ id: 'search', label: '搜索', icon: '🔍' },
{ id: 'favorites', label: '收藏', icon: '❤️', badge: 3 },
{ id: 'profile', label: '我的', icon: '👤' },
]
return (
<TabBar
items={tabs}
activeId={tabBar.activeTab}
onTabChange={tabBar.switchTab}
/>
)
}
```
**最佳实践:**
- 标签栏最多显示 5 个标签
- 使用 `useTabBar` Hook 管理状态
- 为重要标签添加徽章提示
## 数据展示组件
### Badge 徽章
```typescript
import Badge, { Tag, NumberBadge, StatusDot } from '@/components/ui/Badge'
function BadgeExample() {
return (
<div className="space-y-4">
{/* 基础徽章 */}
<div className="flex gap-2">
<Badge>Default</Badge>
<Badge type="success">Success</Badge>
<Badge type="error">Error</Badge>
<Badge type="warning">Warning</Badge>
<Badge type="info">Info</Badge>
</div>
{/* 数字徽章 */}
<div className="flex gap-2">
<NumberBadge count={5} />
<NumberBadge count={99} max={99} />
<NumberBadge count={999} max={99} />
</div>
{/* 标签 */}
<div className="flex gap-2">
<Tag closable onClose={() => console.log('关闭')}>
可关闭标签
</Tag>
<Tag icon="🔥" type="success">
带图标标签
</Tag>
</div>
{/* 状态点 */}
<div className="flex gap-2">
<StatusDot status="online" />
<StatusDot status="busy" />
<StatusDot status="away" />
<StatusDot status="offline" />
</div>
</div>
)
}
```
### ProgressBar 进度条
```typescript
import ProgressBar, { StepProgress } from '@/components/ui/ProgressBar'
function ProgressExample() {
const [progress, setProgress] = useState(45)
return (
<div className="space-y-6">
{/* 线性进度条 */}
<ProgressBar
value={progress}
showLabel
showPercentage
label="下载进度"
animated
/>
{/* 环形进度条 */}
<ProgressBar
value={progress}
variant="circular"
size="large"
type="success"
showPercentage
/>
{/* 步骤进度 */}
<StepProgress
current={2}
total={4}
showLabels
labels={['开始', '配置', '确认', '完成']}
/>
</div>
)
}
```
## 反馈组件
### Modal 模态框
```typescript
import Modal, { ConfirmModal } from '@/components/ui/Modal'
function ModalExample() {
const [showModal, setShowModal] = useState(false)
const [showConfirm, setShowConfirm] = useState(false)
return (
<div>
<Button onClick={() => setShowModal(true)}>
打开模态框
</Button>
<Modal
open={showModal}
onClose={() => setShowModal(false)}
title="模态框标题"
description="模态框描述"
size="medium"
>
<div className="space-y-4">
<p>模态框内容</p>
<div className="flex gap-3">
<Button variant="filled">确认</Button>
<Button variant="gray" onClick={() => setShowModal(false)}>
取消
</Button>
</div>
</div>
</Modal>
<ConfirmModal
open={showConfirm}
onClose={() => setShowConfirm(false)}
title="确认删除"
description="此操作无法撤销"
onConfirm={() => console.log('确认删除')}
/>
</div>
)
}
```
### Toast 提示
```typescript
import { useToast, ToastContainer } from '@/components/ui/Toast'
function ToastExample() {
const toast = useToast()
return (
<div>
<div className="space-x-2">
<Button onClick={() => toast.success('操作成功')}>
成功提示
</Button>
<Button onClick={() => toast.error('操作失败')}>
错误提示
</Button>
<Button onClick={() => toast.info('新消息', {
title: '系统通知',
action: {
label: '查看',
onClick: () => console.log('查看消息')
}
})}>
信息提示
</Button>
</div>
<ToastContainer
toasts={toast.toasts}
onClose={toast.removeToast}
/>
</div>
)
}
```
## 动画组件
### AnimatedEntry 进入动画
```typescript
import AnimatedEntry, { SlideInUp, StaggeredAnimation } from '@/components/ui/AnimatedEntry'
function AnimationExample() {
return (
<div className="space-y-8">
{/* 基础进入动画 */}
<AnimatedEntry direction="up" duration="normal">
<Card className="p-6">
<h3>从下方滑入的卡片</h3>
</Card>
</AnimatedEntry>
{/* 预配置动画 */}
<SlideInUp delay={200}>
<div>延迟 200ms 的滑入动画</div>
</SlideInUp>
{/* 交错动画 */}
<StaggeredAnimation staggerDelay={100}>
{[1, 2, 3, 4].map(i => (
<Card key={i} className="p-4">
<p>卡片 {i}</p>
</Card>
))}
</StaggeredAnimation>
</div>
)
}
```
**最佳实践:**
- 使用 `trigger="visible"` 实现滚动触发动画
- 交错动画的延迟时间建议 50-150ms
- 响应用户的减少动画偏好设置
## 可访问性工具
### 用户偏好设置
```typescript
import { PreferencesProvider, AccessibilityPanel, useUserPreferences } from '@/lib/preferences'
function App() {
return (
<PreferencesProvider>
<div className="app">
<AccessibilityPanel />
<MyContent />
</div>
</PreferencesProvider>
)
}
function MyContent() {
const { preferences } = useUserPreferences()
return (
<div>
<p>当前字体大小: {preferences.fontSize}%</p>
<p>减少动画: {preferences.prefersReducedMotion ? '是' : '否'}</p>
</div>
)
}
```
### 包容性设计
```typescript
import { createInclusiveStatusIndicator } from '@/lib/accessibility'
function StatusExample() {
const successIndicator = createInclusiveStatusIndicator('success', {
showIcon: true,
showShape: true,
size: 'medium'
})
return (
<div
className="status-indicator success shape-circle"
style={successIndicator.style}
aria-label={successIndicator.ariaLabel}
>
{successIndicator.icon}
</div>
)
}
```
## 性能优化
### 虚拟滚动
```typescript
import VirtualList from '@/components/ui/VirtualList'
function VirtualListExample() {
const items = Array.from({ length: 10000 }, (_, i) => ({
id: i,
name: `项目 ${i + 1}`,
description: `描述 ${i + 1}`
}))
return (
<VirtualList
items={items}
itemHeight={60}
height={400}
renderItem={(item, index) => (
<div className="flex items-center p-4 border-b">
<div className="flex-1">
<h4>{item.name}</h4>
<p className="text-sm text-gray-500">{item.description}</p>
</div>
</div>
)}
/>
)
}
```
### 性能监控
```typescript
import { usePerformanceMonitor, useMemoryMonitor } from '@/lib/performance'
function PerformanceExample() {
usePerformanceMonitor('MyComponent')
const memoryInfo = useMemoryMonitor()
return (
<div>
{memoryInfo && (
<p>内存使用: {Math.round(memoryInfo.usedJSHeapSize / 1024 / 1024)}MB</p>
)}
</div>
)
}
```
## 迁移指南
### 从传统 CSS 迁移
```typescript
// 旧方式
<div className="bg-blue-500 text-white p-4 rounded-lg">
内容
</div>
// 新方式
<Card variant="filled" className="bg-apple-blue-light text-white">
<div className="p-apple-4">
内容
</div>
</Card>
```
### 从其他 UI 库迁移
```typescript
// Material-UI Button
<Button variant="contained" color="primary">
按钮
</Button>
// Apple UI Button
<Button variant="filled">
按钮
</Button>
```
## 常见问题
### Q: 如何自定义主题颜色?
A: 修改 `tailwind.config.ts` 中的颜色令牌,或在 CSS 中覆盖 CSS 变量。
### Q: 动画不生效怎么办?
A: 检查用户是否启用了减少动画偏好,或者组件是否设置了 `disabled` 属性。
### Q: 如何确保可访问性?
A: 使用语义化 HTML,提供 ARIA 属性,确保键盘导航,测试对比度。
### Q: 性能优化建议?
A: 使用 React.memo,避免不必要的重渲染,大列表使用虚拟滚动。
## Slider 滑动条(统一元组件)
`Slider` 元组件位于 `src/components/ui/Slider.tsx`,用于统一替代原生 `input[type="range"]`。
```tsx
import Slider from '@/components/ui/Slider'
function DemoSlider() {
return (
<Slider
min={0}
max={1}
step={0.01}
value={0.45}
onValueChange={(value) => console.log(value)}
aria-label="爆炸程度"
className="w-40"
/>
)
}
```
最佳实践:
- 必须使用 `onValueChange` 接收数值,避免在业务层反复做字符串转数字。
- 必须补齐 `aria-label`,保证键盘与读屏可访问性。
- 禁止新增独立的 range 样式分叉,统一使用 `apple-slider` 视觉规范。
## 总结
Apple 风格 UI 设计系统提供了完整的组件库和工具集,遵循苹果 Human Interface Guidelines 2025 标准。通过合理使用这些组件和工具,可以构建出美观、易用、高性能的用户界面。
记住始终考虑可访问性、性能和用户体验,让每个用户都能享受到优质的交互体验。

View File

@ -0,0 +1,163 @@
# 类目配置取数与筛选逻辑
> 用途:给需要接入类目配置数据的开发人员使用。本文描述当前代码和数据库的真实口径(2026-09-24 更新:合并为单表展示结构)。
>
> 重要:本文不包含数据库连接串、账号、密码。连接信息统一见《README.md》。
## 1. 数据源与责任边界
- **权威数据源**:本系统 `DATABASE_URL` 指向的 PostgreSQL,schema `public`,共 2 张表(见第 2 节)。数据通过类目配置页面人工维护 + Excel 导入,**没有外部同步来源**。
- **业务作用**:
1. 维护「二级类目 → 一级类目」的唯一归属(产品全量档案类目列的规范值来源);
2. 维护每个类目的 4 档规模阈值(爆款/热卖款/动销款/低动销款 × 业绩区间、日销区间)与「分类」归属。
- **人工维护原则**:直接写表会绕过唯一键校验和页面权限,接入方一律只读。
### 1.1 代码依据
- Prisma 模型:`prisma/schema.prisma` 中 `SupplyChainProductArchiveCategoryConfig`、`SupplyChainProductArchiveCategoryScaleRow`。
- 页面:`src/app/supply-chain/database/category-config/`(单表展示,数据源即规模行表)。
- 服务:`supply-chain-product-archive-category-scale-row.service.ts`(页面主数据源,含类目清单同步)、`supply-chain-product-archive-category-config.service.ts`(类目主表维护)。
## 2. 物理表清单与分工
| 表名 | 角色 | 当前行数量级 |
| --- | --- | --- |
| `"SupplyChainProductArchiveCategoryScaleRow"` | **类目规模行(页面当前数据源)**:一行 = 二级类目 × 规模标签,自带一级类目、分类、阈值 | 类目数 × 4 |
| `"SupplyChainProductArchiveCategoryConfig"` | **类目主表**:一行一个二级类目的身份记录(唯一性、启停状态),供产品档案同步、类目下拉等消费方使用 | 80+ |
两表关系:
```text
ScaleRow(页面表:一级/二级/分类/阈值/标签,行=类目×档位,唯一键 categoryL2+scaleLabel)
│ 服务写入时自动同步(新增/改行会把一级类目、分类落到主表)
▼
Config(类目身份表:categoryL2 全表唯一 + status 启停,供外部消费方取类目清单)
```
> 历史说明:旧三表结构中的「分类档位模板表」(`SupplyChainProductArchiveCategoryGroupTier`)已于 2026-09-24 迁移(20260924160000)删除,其数据已按类目展开并入规模行表;旧接口 `/api/supply-chain/category-config/group-tiers/*` 已下线。
## 3. 表字段字典
### 3.1 `"SupplyChainProductArchiveCategoryScaleRow"`(类目规模行,页面数据源)
| 字段 | 类型/约束 | 业务含义 | 取值规则 |
| --- | --- | --- | --- |
| `id` | `TEXT` 主键 | 行 ID | 系统生成 |
| `categoryL1` | `TEXT NOT NULL DEFAULT ''` | 一级类目(≤160 字符) | 页面/导入必填 |
| `categoryL2` | `TEXT NOT NULL` | 二级类目名(≤160 字符) | 与 `scaleLabel` 联合唯一 |
| `categoryGroup` | `TEXT NOT NULL DEFAULT ''` | 分类(如 电脑桌/书桌、其他沙发类) | 空 = 未分类 |
| `scaleLabel` | `TEXT NOT NULL` | 规模标签 | 只能是 爆款/热卖款/动销款/低动销款 |
| `performanceStart` | `DECIMAL(14,2)` NOT NULL | 业绩起点(元) | |
| `performanceEnd` | `DECIMAL(14,2)` 可空 | 业绩终点(元) | **NULL = 不设上限** |
| `dailySalesStart` | `DECIMAL(12,2)` NOT NULL | 日销起点(件/天) | 支持小数 |
| `dailySalesEnd` | `DECIMAL(12,2)` 可空 | 日销终点(件/天) | NULL = 不设上限 |
| `sortOrder` | `INTEGER` | 档位顺序:爆款1 → 热卖款2 → 动销款3 → 低动销款4 | 判定从高档往低档取第一个命中 |
| `createdAt` / `updatedAt` | `TIMESTAMP(3)` | 系统字段 | |
约束:`UNIQUE(categoryL2, scaleLabel)`——一个类目每个标签只有一行;索引:`categoryL2`、`categoryGroup`。
### 3.2 `"SupplyChainProductArchiveCategoryConfig"`(类目主表)
| 字段 | 类型/约束 | 业务含义 | 取值规则 |
| --- | --- | --- | --- |
| `id` | `TEXT` 主键 | 配置记录 ID | 系统生成 |
| `categoryL2` | `TEXT` 唯一 | 二级类目名(≤160 字符) | 全表唯一 |
| `categoryL1` | `TEXT NOT NULL` | 一级类目(≤160 字符) | |
| `categoryGroup` | `TEXT NOT NULL DEFAULT ''` | 分类 | 空 = 未分类 |
| `scaleLabel` | `TEXT NOT NULL DEFAULT ''` | 【预留未使用】 | 阈值数据在 ScaleRow 表 |
| `performanceStart` / `performanceEnd` | `DECIMAL(14,2)` 可空 | 【预留未使用】 | 同上 |
| `dailySalesStart` / `dailySalesEnd` | `DECIMAL(12,2)` 可空 | 【预留未使用】 | 同上 |
| `status` | `TEXT`,`ACTIVE` / `INACTIVE` | 启用/停用;停用的不进类目选项 | 默认 `ACTIVE` |
| `createdAt` / `updatedAt` | `TIMESTAMP(3)` | 系统字段 | |
索引:`categoryL1`、`categoryGroup`、`status`;`categoryL2` 唯一。
## 4. 其他线程取数方式
### 4.1 只读 SQL(直连数据库)
```sql
-- 4.1.1 页面同款数据:全部规模行(一级类目、二级类目、分类、4 档阈值)
SELECT "categoryL1", "categoryL2", "categoryGroup", "scaleLabel",
"performanceStart", "performanceEnd", "dailySalesStart", "dailySalesEnd"
FROM "SupplyChainProductArchiveCategoryScaleRow"
ORDER BY "categoryL2" ASC, "sortOrder" ASC;
-- 4.1.2 某个类目的 4 档阈值
SELECT "scaleLabel", "performanceStart", "performanceEnd", "dailySalesStart", "dailySalesEnd"
FROM "SupplyChainProductArchiveCategoryScaleRow"
WHERE "categoryL2" = '住宅家具 - 书桌'
ORDER BY "sortOrder" ASC;
-- 4.1.3 类目身份清单(供下拉/同步用,只取启用的)
SELECT "categoryL1", "categoryL2", "categoryGroup"
FROM "SupplyChainProductArchiveCategoryConfig"
WHERE "status" = 'ACTIVE'
ORDER BY "categoryL1" ASC, "categoryL2" ASC;
-- 4.1.4 某分类下的全部类目及其阈值
SELECT "categoryL2", "scaleLabel", "performanceStart", "performanceEnd"
FROM "SupplyChainProductArchiveCategoryScaleRow"
WHERE "categoryGroup" = '电脑桌/书桌'
ORDER BY "categoryL2" ASC, "sortOrder" ASC;
```
注意事项:
- 表名/字段名**必须带双引号**保持大小写。
- 终点列为 `NULL` 表示不设上限,不要写成 0。
- 停用(`INACTIVE`)类目历史仍在 Config 表里,统计按 `status` 过滤。
- 商品/档案行上的类目与分类是写入时的快照,改配置不回刷历史数据。
### 4.2 代码内取数(同仓库服务)
```ts
// 规模行(页面同款数据:一行=类目×档位,含一级类目与分类)
import { supplyChainProductArchiveCategoryScaleRowService } from
'@/server/services/supply-chain-product-archive-category-scale-row.service'
await supplyChainProductArchiveCategoryScaleRowService.list({ page: 1, pageSize: 100 })
await supplyChainProductArchiveCategoryScaleRowService.listAll({ categoryGroup: '电脑桌/书桌' })
// 类目身份清单(产品全量档案同步、类目下拉在用)
import { supplyChainProductArchiveCategoryConfigService } from
'@/server/services/supply-chain-product-archive-category-config.service'
await supplyChainProductArchiveCategoryConfigService.list({ page: 1, pageSize: 100, status: 'ACTIVE' })
import { supplyChainProductArchiveCategoryConfigRepository } from
'@/server/repositories/supply-chain-product-archive-category-config.repository'
await supplyChainProductArchiveCategoryConfigRepository.findActiveByCategoryL2('住宅家具 - 书桌')
```
### 4.3 走 API(需要登录 + 供应链权限)
| 接口 | 方法 | 说明 |
| --- | --- | --- |
| `/api/supply-chain/category-config/scale-rows` | GET / POST | 规模行列表 / 新增(写入时自动同步类目主表) |
| `/api/supply-chain/category-config/scale-rows/{id}` | PATCH / DELETE | 规模行编辑 / 删除 |
| `/api/supply-chain/category-config/scale-rows/export` · `/import` | GET / POST | 导出(筛选跟随当前搜索/分类/标签) / 流式导入(表头:一级类目、二级类目、分类、规模标签、业绩起终点、日销起终点) |
| `/api/supply-chain/category-config/scale-rows/groups` | GET | 去重分类列表(筛选项) |
| `/api/supply-chain/category-config` · `/{id}` | GET/POST/PATCH | 类目身份维护(供应商新品推荐下拉也走这里) |
| `/api/supply-chain/category-config/export` · `/import` | GET / POST | 类目身份导出 / 导入 |
权限:查询走类目节点「查看」,导出走「下载」,写入走「编辑」;角色 `SUPER_ADMIN/ADMIN/SUPERVISOR` 或业务角色 `MANAGEMENT`。
## 5. 现有消费方(改动前必读)
| 消费方 | 取数点 | 依赖 |
| --- | --- | --- |
| 产品全量档案(DF/聚水潭同步) | `jst-sku-product-archive-sync.service.ts`、`jst-sku-product-archive-full-sync.service.ts` | `findActiveByCategoryL2`:仅启用类目作为规范值 |
| 供应商新品推荐类目下拉 | `/api/supply-chain/supplier-recommended-products/category-options` | Config `service.list`,仅 `ACTIVE` |
| 类目配置页面 | 第 4.3 节接口 | — |
改 `status`、`categoryL2` 名称、删除类目前先确认不影响以上消费方。
## 6. 接入前检查清单
- [ ] 表名/字段名带双引号保持大小写。
- [ ] 方向:`categoryL2`(二级)→ `categoryL1`(一级);`categoryGroup`(分类)是独立维度,不是一级类目,空 = 未分类。
- [ ] 终点列 `NULL` = 不设上限,不要当 0 处理。
- [ ] 档位判定顺序:爆款 → 热卖款 → 动销款 → 低动销款,从高到低取第一个命中区间。
- [ ] 阈值数据取 `ScaleRow` 表;Config 表上的 `scaleLabel`/阈值列为预留未使用。
- [ ] 停用(`INACTIVE`)配置历史仍在 Config 表里,统计按 `status` 过滤。
- [ ] 商品/档案行上的类目、分类是写入时快照,改配置不回刷历史数据。
- [ ] 页面写操作会双写两表(规模行 + 类目主表同步),外部只读不写就不会产生不一致。

View File

@ -0,0 +1,99 @@
# 内容视频流程看板取数与筛选逻辑
> 用途:给需要接入「新品-视频进度(内容部)」(钉钉内容视频流程)数据的开发人员使用。本文描述当前代码和数据库的真实口径(2026-10-08 首版)。
>
> 重要:本文不包含数据库连接串、账号、密码。该页面数据**不存在本系统 PostgreSQL**,全部来自企划 MySQL 的同步快照。
## 1. 数据源与责任边界
- **页面直接数据源**:企划 MySQL(连接变量 `JST_PLANNING_DB_*`,生产为生产服务器本机实例)的 `planning_database` 库,快照表 `oa_video_flow_board_snapshot`。
- **原始数据源**:钉钉 OA 审批镜像库的 `ding_Workflow` / `ding_WorkflowScheme` / `ding_WorkflowTask` 三张表,按流程编码过滤:
- 内容视频流程 `PROC-AE9FF3E1-A442-4E6A-85CF-0F2B86409673`(登记在 `oa-board.constants.ts` 的 `OA_BOARD_PROCESS_CODES.video`)。
- **同步方式**:`scripts/sync-oa-video-flow-board.ts --apply`(即 `npm run sync:oa-video-flow:apply`,服务 `oa-video-flow-board.service.ts`)全量拉取该流程全部实例与表单字段,按"一条流程实例 = 一行"写入快照表,并用 `sync_version` 清理已消失的实例。生产由 systemd 定时器(`install-oa-board-sync-timer.sh` 安装的 `furniture-oa-board-sync.timer`)每 15 分钟在主看板、品牌策划看板之后顺序执行。
- **责任边界**:镜像库与钉钉侧的数据质量不由本系统负责;本系统保证"镜像库 → 快照 → 页面"这一段。下游一律**只读快照表**,不要直连 ding_ 三张源表(源表结构随钉钉变化,无稳定性承诺)。
- **与新品主链的关系**:内容视频流程**不参与** OA 新品看板主链的项目聚合、总状态与时效矩阵(其表单无开款编号/关联单号,仅有 SPU)。本表是独立流程维度的进度数据。
### 1.1 代码依据
- 同步与快照读写的唯一实现:`src/server/services/oa-video-flow-board.service.ts`(建表 DDL 在 `ensureSnapshotTable`,自动建表)。
- 源库连接复用:`oa-board-sync.service.ts` 导出的 `openOaBoardSourceMysqlConnection` / `openOaBoardSourceSqlServer`(跟随 `OA_BOARD_SOURCE_DRIVER`)。
- 页面接口:`GET /api/intake/oa-board/video-flow`(参数 `keyword` / `demandType` / `brand` / `status` / `page` / `pageSize`,需登录态)。
- 页面组件:`src/app/intake/oa-board/OaBoardVideoFlowPanel.tsx`(Tab 参数 `tab=video-flow`)。
## 2. 物理表(MySQL · planning_database)
| 物理表 | 用途 | 键与索引 |
| --- | --- | --- |
| `oa_video_flow_board_snapshot` | 最新快照,一行 = 一条内容视频流程实例 | `row_key` 唯一;`spu` / `status` / `stage_label` / `demand_type` / `brand` / `created_date` 有索引 |
同步任务自动建表(`CREATE TABLE IF NOT EXISTS`),引擎 InnoDB,字符集 utf8mb4。
## 3. 字段字典
| 字段 | 类型 | 含义 | 取值规则 |
| --- | --- | --- | --- |
| `row_key` | `VARCHAR(191)` 唯一 | 钉钉流程实例 ID(processInstanceId),幂等拉取键 | 源表主键,稳定不变 |
| `business_id` | `VARCHAR(128)` | 流程发起编号 | 形如 `202610071758000088167` |
| `title` | `VARCHAR(255)` | 流程标题 | `XX提交的内容视频和图片申请流程` |
| `submitter_name` | `VARCHAR(255)` | 提交人姓名 | 从标题提取;`originator_user_id` 存钉钉用户 ID 兜底 |
| `status` | `VARCHAR(32)` | 流程状态 | `RUNNING` / `COMPLETED` / `TERMINATED` |
| `demand_department` | `VARCHAR(255)` | 需求部门 | 表单字段「需求部门」 |
| `demand_type` | `VARCHAR(255)` | 需求类型 | 如 淘系-逛逛视频 / 抖音-千川素材视频 / 安装视频 / AI视频 等 13 种 |
| `task_type` | `VARCHAR(255)` | 任务类型 | 如 布景拍摄 / 二次剪辑 |
| `install_video_category` | `VARCHAR(255)` | 安装视频分类 | 仅安装视频类需求有值(如 B档) |
| `brand` | `VARCHAR(255)` | 品牌 | 表单字段「品牌」 |
| `scene_category` | `VARCHAR(255)` | 场景类别 | 如 卧室 / 客厅 / 玄关 |
| `spu` | `VARCHAR(64)` | SPU | 大写规范化;约 3% 行无值(账号维护等非产品需求) |
| `product_id` | `VARCHAR(128)` | 商品ID | 表单字段「商品ID」,可能为"暂无" |
| `sample_barcodes` | `TEXT` | 样品条码 | 表单「样品明细」表格中商品编码列,多值分号分隔 |
| `content_owner` | `VARCHAR(255)` | 内容部负责人 | 表单字段 |
| `planning_supervisor` | `VARCHAR(255)` | 品牌策划主管 | 表单字段 |
| `script_director` | `VARCHAR(255)` | 编导 | 流程推进到该环节时才填,未填为 NULL |
| `cameraman` | `VARCHAR(255)` | 摄像 | 同上 |
| `editor` | `VARCHAR(255)` | 剪辑 | 同上 |
| `retoucher` | `VARCHAR(255)` | 修图 | 可选环节 |
| `cover` | `VARCHAR(255)` | 封面 | 同上 |
| `stage_label` | `VARCHAR(64)` | 当前阶段 | 推断规则见第 4 节 |
| `acceptance_result` | `VARCHAR(255)` | 验收结果 | 表单「运营/策划/客服验收结果」 |
| `score` | `VARCHAR(64)` | 评分 | 表单「评分」 |
| `created_date` | `DATETIME` | 流程发起时间(北京时间) | 源表 createAt |
| `finished_date` | `DATETIME` | 流程完成时间 | RUNNING 为 NULL |
| `duration_days` | `DECIMAL(10,2)` | 总时效(天) | 发起 → 完成;未完成计至同步时刻 |
| `last_source_updated_at` | `DATETIME` | 源表最近更新时间 | 增量拉取判据 |
| `sync_version` | `VARCHAR(64)` | 本次同步批次标识 | 全表同批;用于清理已消失实例 |
| `synced_at` | `TIMESTAMP` | 本行最近同步时间 | 数据新鲜度判据(`MAX(synced_at)`) |
## 4. 阶段(stage_label)推断规则
源表只有 activityId 没有节点名,首版按表单字段的推进顺序推断(钉钉流程设计中角色字段按环节顺序逐步填写):
```text
status = COMPLETED → 已完结
status = TERMINATED → 已终止
验收结果或评分已填 → 验收/评分
否则取已填角色的最后一个:
编导 → 编导制作 → 摄像 → 摄影拍摄 → 剪辑 → 剪辑制作
→ 修图/封面 → 封面/修图制作
一个角色都没填 → 需求发起
```
若后续拿到钉钉流程节点名清单,可升级为 activityId 级精确映射(预留位:`oa-board.constants.ts` 的 `OA_BOARD_ACTIVITY_MAPPING_SEED.video` 当前为空数组)。
## 5. 接入示例(一律只读)
```sql
-- 按 SPU 拉该产品的全部视频流程
SELECT * FROM oa_video_flow_board_snapshot WHERE spu = 'SJ0159';
-- 只看进行中的
SELECT * FROM oa_video_flow_board_snapshot WHERE status = 'RUNNING';
-- 增量拉取(幂等键 row_key)
SELECT * FROM oa_video_flow_board_snapshot
WHERE last_source_updated_at > :上次拉取时间;
-- 取数前先看新鲜度(生产最多滞后 15 分钟)
SELECT MAX(synced_at) FROM oa_video_flow_board_snapshot;
```
关联口径:与产品数据对齐用 `spu` 或 `sample_barcodes` 中的条码;本表**没有开款编号**,不要与主看板的 `OPENING:` 维度直接关联。

View File

@ -0,0 +1,188 @@
# BOM 成本预估 v2(agent 方案)设计文档
> 日期:2026-09-16
> 状态:设计稿,待评审
> 前置阅读:`docs/项目总览.md`、`src/server/services/bom-costing/agent/`(已建成的引擎)
> 关联会话结论:1688 以图搜图锚定为过渡方案(双重失败已实测证伪),终局为 agent 估算 + 公司数据
## 1. 一句话定位
**粘贴竞品链接 → 几分钟后得到一张诚实的成本预估卡。**
原始需求从未变过。v2 的一切设计决策都回到这句话:数字 + 构成故事 + 诚实的区间,工序全部退到幕后。
## 2. v1 的教训(为什么 v2 长这样)
| v1 的问题 | 根因 | v2 的对策 |
|---|---|---|
| 工序暴露给用户(逐图泳道、五阶段时间线、重算模式、复核流) | 工程美德当成了产品 | 一根进度条 + 一张卡;审计链存在但默认隐藏 |
| 27 个 SKU 全量核算(用户等十几分钟才见结果) | 首结果时间没有被设计 | 首批 3 个精算先出结果,其余按需点算,结果缓存 |
| 十几分钟等待 | 按 SKU 串行/并发全量跑 | case 级一次运行,2-3 分钟 |
| 复杂度随精度承诺膨胀 | 用流程堆精度 | 用数据+校准逼近精度(±10% 是"尽量"的校准目标) |
| 1688 锚定顶替估算 | 上线压力下的权宜 | 1688 永远是后台交叉验证信号 |
**必须保留的教训**:用户会锚定数字。区间 + 置信度 + 不确定点是答案的一部分,不是脚注——这是"给一个数"和"给一个诚实的数"的差别。
## 3. 设计总纲(五条,裁决一切分歧)
1. **预估,不是核算**:输出是带区间与置信度的假设生成器,供决策筛选,永不承诺报价级精度。
2. **agent 主导分析,代码承载确定性**:看什么图、怎么拆结构、查什么价格由 agent 决定;一切数值计算(汇总、区间合成、损耗)由确定性代码完成。模型永不算数、永不编价。
3. **价格只认公司数据**:历史 BOM 镜像是唯一定价依据(检索→分类中位→cohort→损耗四级回退);1688 行情仅作后台交叉验证,永不成为展示价。
4. **前端来源中性(不可触碰)**:界面永不出现 1688 字样/报价/链接;一切序列化出口经过 `src/lib/bom-costing/public-estimate-text.ts`。
5. **实测闸门**:agent 流程未经回测与真实案例对比验证(偏差分布达标)前,不替换任何线上行为。
## 4. 用户形态
### 4.1 默认入口:预估卡 + 全量 SKU 按需核算
**覆盖率是需求,等待时间是体验问题**——所有 SKU 都要能出结果,但采用"先出一批、其余按需点算"的渐进交付:
```
粘贴链接 → [进度条:抓取中 → 分析中 → 出预估](2-3 分钟)→
┌────────────────────────────────────────┐
│ 源氏木语 白蜡木实木衣柜 │
│ 预计出厂成本 ¥260 – 380(首批 3 个 SKU)│
│ 置信度:中等 │
│ 板材 ¥180± · 五金 ¥25± · 加工+人工 ¥60± · 包材 ¥15± │
│ 依据:8 张商品图 + 公司历史 BOM 12 条 │
│ 不确定:板厚未确认;铰链数量按两门推定 │
└────────────────────────────────────────┘
SKU 列表(27 个):
[¥260.29 已核算] (3) 原木色 0.9米两门 ← 首批自动精算
[¥375.48 已核算] (7) 原木色 1.0米 ← 首批自动精算
[¥406.77 已核算] (18) 原木色 1.96米 ← 首批自动精算
[点击核算] (4) 原木色 0.9米/高2.2米 ← 用户点击后单独运行
[点击核算] ... ← 已算结果缓存,不重跑
────────────────────────────────────────
商品图片(保留现有形式:主图/SKU图/详情图 分类浏览 + 大图预览)
```
- **首批自动精算**:默认 3 个代表性 SKU(跨度原则:最小/最大/中间,或按材质/结构差异选),2-3 分钟出卡——用户立刻有结果。
- **其余 SKU 按需点算**:未核算的 SKU 显示占位按钮,用户点击触发单 SKU agent 运行(复用现有 per-SKU runner,1-2 分钟/个);**结果持久缓存**,重复点击不重跑。
- 进度状态只有三个:抓取中 / 分析中 / 出预估(含失败态与重试按钮)。
- **渐进展开**:点构成 → BOM 行明细;点行 → 证据(哪张图、哪条镜像参考行)。审计链存在,默认折叠。
- **商品图片保留现有展示形式**(主图/SKU 图/详情图分类 + 逐图状态 + 大图预览):用户需要直接查看竞品链接内容,图片是判断依据,不是工程噪声。
### 4.2 深度模式(保留现有工作台)
需要全量 27 SKU 拆解、人工核对、证据溯源的用户,从预估卡一键进入现有工作台。v2 验证前工作台不动;验证后它降级为二级入口,不再是默认。
## 5. 架构:case 级 agent 运行
### 5.1 运行编排(新,核心增量)
交付模型:**首批自动精算 + 其余按需点算**。现有 per-SKU runner 是按需点算的执行体,case 级编排负责首批选择与结果汇总。
```
阶段一:runBomCostingAgentEstimateForCase(caseId)(首批)
1. 取案例简报(标题/渠道/已解析品类 skill)+ 归档素材 + SKU 清单(含尺寸)
2. 代码侧选首批 SKU(默认 3 个:最小/最大/中间,或按材质/结构差异;
单 SKU 商品即为它自己)
3. 逐个运行现有 per-SKU agent 循环(结构行 + 查价)
4. 代码侧汇总出 EstimateCard:卡面五行/区间/置信度 + SKU 列表状态
(已核算 3 / 待核算 24)
阶段二:按需点算(用户驱动)
用户点击未核算 SKU → 单 SKU agent 运行(现有 runner)→
结果持久缓存 → 卡面/SKU 列表实时更新
```
- 首批预算:≤24 轮 / ≤32 次模型调用 / ≤8 分钟(per-SKU 预算 × 3 + 编排开销)。
- **结果缓存**:per-SKU 结果按 (caseId, skuId, 引擎版本) 持久化;点击已核算 SKU 直接展示缓存,不重跑。SKU 重算 = 显式刷新动作(换引擎版本或用户主动重跑)。
- 失败语义:首批中单个 SKU 失败 → 该 SKU 标记失败可单独重试,不拖垮整卡;结构成功但价格饥饿 → 出结果但区间放宽 + 不确定点说明(PARTIAL 是合法产物)。
- 并发上限与幂等:同一 SKU 的点算请求可去重合并(进行中重复点击返回同一运行)。
### 5.2 输出契约:EstimateCard(case 级)
```ts
interface BomEstimateCard {
caseId: string
title: string
skus: Array<{ // 全量 SKU 清单 + 状态
externalSkuId: string
name: string
status: 'ESTIMATED' | 'PENDING' | 'FAILED' | 'RUNNING'
estimate?: BomCostingAgentEstimate // 已核算时:行/区间/置信度/evidence
cachedAt?: string
}>
firstBatchSkuIds: string[] // 首批自动精算的 SKU
breakdown: Array<{ // 卡面五行(由已核算 SKU 的行汇总;随点算增长)
category: 'BOARD'|'STEEL'|'ACCESSORY'|'PACKAGING'|'PROCESSING'|'LABOR'
total: number | null // null = 该组无价,卡面显示"待补"
}>
totalRange: { min: number; max: number } // 当前已核算 SKU 的区间包络
confidence: number // 已核算 SKU 的最小置信度
openQuestions: string[] // 卡面"不确定"(已核算 SKU 的并集)
usage: { rounds; modelCalls; elapsedMs }
}
```
原则:明细(行级)保留在各 SKU 的 estimate 里供展开;卡面只消费 breakdown/totalRange/confidence/openQuestions,并随按需点算增量更新。
### 5.3 价格供给(核心依赖,最大缺口)
首测"定价饥饿"的根治项——把 v1 场景服务的回退链包装成 agent 工具:
| 优先级 | 工具 | 包装自 |
|---|---|---|
| P0 | 尺寸感知镜像检索(分类/材质词/尺寸/参考 SKU 多级过滤) | `findReferenceCandidates` |
| P0 | 分类画像中位价 | `fillCategoryProfileFallbacks` |
| P1 | 同类 cohort 报价 | `resolveCohortQuote` |
| P1 | 历史损耗率 | `applyHistoricalLossFactorFallbacks` |
| P2 | AI 补估(置信度上限 0.75 + 标记复核) | 现有 estimator |
工具输出纪律:摘要级(无供应商/店铺/链接),带 sourceRowId 供 evidence 引用,单位明确。**1688 工具维持现状**(只透出状态/参考价/区间/可比数四个中性字段)。
### 5.4 数据与知识资产接入(全部保留,形态不变)
| 资产 | v2 中的角色 | 接入形态 |
|---|---|---|
| 历史 BOM 镜像(每日 MSSQL 同步) | 唯一价格依据 | 工具(5.3) |
| 品类 Skill(5 个)+ 结构先验 | agent 脑中的专家清单:必备组件、证据纪律、校验规则 | 提示词注入(`renderBomCostingAgentSkillKnowledge`,已建成) |
| 材质目录 | 材质身份唯一权威 | 工具(已接线) |
| 商品抓取/归档/抠图 | 看图取证的素材 | 工具 + mission 组装(已接线;抠图模型待切 remove-background) |
| 1688 行情 | 后台交叉验证信号 | 工具(已接线,中性摘要) |
### 5.5 校准闭环(±10% "尽量"的实现方式)
- **回测**:历史 BOM 镜像抽 20-50 个真实 SKU,同一规则/agent 重算 vs 真实成本,产出误差分布(复用 `backtest:bom-cost-reference` 基建)。
- **shadow 对比**:agent 结果与现行引擎结果对同一案例并排落库(工程域),持续记录偏差(`report:bom-cost-shadow-divergence` 同款模式)。
- **阈值语义**:±10% 是回测循环追的校准目标(按品类分段看分布),不是单案例门禁。偏差大 → 调工具/知识/提示词 → 再回测。
- 未过门槛 → agent 流程保持旁路,线上仍是现行方案。
## 6. 前端形态
- **新入口**:`/bom-costing` 默认视图替换为预估卡流(粘贴框 → 卡 → SKU 列表 → 图片浏览)。
- **商品图片**:保留现有展示形式(主图/SKU 图/详情图分类 tab + 大图预览 + 逐图状态),用户需要直接查看竞品内容;它从"处理进度件"回归"内容主体"。
- **SKU 列表**:已核算显示价格,未核算显示"点击核算"按钮(运行中显示进度态);点击结果写缓存并即时更新卡面。
- **组件**:新 `BomEstimateCard`(平台组件体系 + 白曜石规范),状态只有 加载/成功/失败(可重试)。
- **现有工作台**:不动,作为深度模式从卡进入。
- **移动端**:卡、SKU 列表与图片浏览响应式。
- 红线复检:卡面文案经过 `publicEstimateWarningText` 清洗;evidence 展开层同样不得出现来源敏感词。
## 7. 落地顺序(每步可独立验证)
| 步骤 | 内容 | 验收 |
|---|---|---|
| ① | 价格回退工具(P0 两个) | 对已知案例重跑,未定价行显著下降 |
| ② | 首批编排 + EstimateCard 契约 + 按需点算与结果缓存 | 单案例 2-8 分钟出首批卡;点击未核算 SKU 增量出结果且缓存生效 |
| ③ | 预估卡前端(含 SKU 列表与现有图片浏览)+ 三态进度 | 真人走通 粘贴→出卡→点算其他 SKU→看图核对 全流程 |
| ④ | 回测 + shadow 接入,按品类出基线报告 | 误差分布报告(对照 ±10% 目标) |
| ⑤ | 实测样本扩到 5-10 案例,按品类定门槛 | 达标 → 翻转默认入口;未达标 → 继续迭代 |
## 8. 红线(重申)
1. 前端永不出现 1688 相关内容;一切文本出口过脱敏契约。
2. agent 引擎在过实测闸门前保持旁路(不替换线上行为)。
3. 区间 + 置信度 + 不确定点是答案的一部分,任何展示形态不得省略。
4. 单价只来自工具结果;总额只由代码汇总。
5. 现有工作台在 v2 过门槛前不删不改语义。
## 9. 当前资产清单(截至本文日期)
已建成:agent 契约/工具层/循环/网关适配/skill 知识渲染/per-SKU runner(27 测试)、镜像与材质只读仓储、1688 中性摘要工具、板材开料工具、shadow 报表、trial 试跑脚本、生产真实案例(源氏木语 27 SKU)对照数据。
待建:价格回退工具(5.3 表)、case 级编排、EstimateCard 契约与前端、回测接入。
外部依赖:1688 新 key(当前过期,影响搜图与 A/B 实测,不影响 ①② 开发)。

View File

@ -0,0 +1,43 @@
# BOM 成本预估产品口径
本文是 BOM 成本功能的产品口径裁决入口。代码、界面、测试或历史文档与本文冲突时,应先按本文区分“成本预估”与旧链路内部一致性检查,再决定是否修改实现。
## 产品承诺
- 本功能提供带区间、置信度和待确认项的成本预估,不提供报价级精确核算。
- 预估结果允许与实际采购价、旧规则结果或其他参考信号存在明显差异;差异本身不构成失败。
- 缺少依据的单价可以为空,未解决行应进入待确认项,不得为了提高覆盖率编造价格。
- Agent 决定查看什么资料、调用什么工具以及引用什么证据;总额、损耗、开料、分位数和行小计由确定性代码计算。
- 正式 Agent 引擎以公司历史 BOM 镜像、材质目录和损耗经验为主要价格依据;临时交付阶段按下节的整件九折方案展示单一预估结果。
## 临时整件九折方案
- 正式 Agent 引擎完善前,相似度合格的实时同款报价取中位数并固定乘以 `0.90`,作为前端唯一整件预估价。
- 没有可用同款、图片不可用或服务失败时,沿用结构预估,页面仍只显示一套结果。
- 原始报价、候选商品、九折过程和原结构价只保留在后台审计字段;普通接口和前端不得出现来源名称、商品链接、供应商或采购系数。
- 临时整件价生效时,前端不得同时显示结构分类金额、行小计、贡献占比或旧规则价格;BOM 部件、材质、尺寸和数量仍可在详情中查看。
- 正式 Agent 引擎通过产品验收后,在保持同一前端单结果契约的前提下替换临时价格来源。
## 验收口径
Agent 旁路试验主要验证:
1. 能否稳定完成工具调用并返回符合契约的结果。
2. 区间、置信度、证据和待确认项是否完整且容易理解。
3. 单价是否来自工具结果,总额是否由代码汇总。
4. 未定价行是否明确保留,而不是被虚构金额掩盖。
5. 前端是否保持来源中性,不泄露供应商、店铺、商品链接或实时市场来源。
Agent 与旧引擎、实际采购价或市场信号的差异仅作为观察数据,不设全局固定百分比通过门槛。按品类统计差异可以帮助理解结果分布,但不能自动判定预估失败。
## 旧 10% 门槛的边界
`backtest:bom-cost-reference` 中的 10% 只用于验证同一历史参考镜像内“已存总额”和“逐行确定性重建”是否一致,属于数据与重建逻辑检查。它不评价页面成本预估或 Agent 预估准确度,不得显示为产品目标,也不得复用于 Agent 灰度验收。
数据库中的 `targetErrorPercent` 暂时保留用于旧数据和接口兼容;新界面不得展示为成本预估承诺。后续清理旧模型时再通过正式 migration 移除。
## 术语
- 用户可见名称统一使用“BOM 成本预估”“预估结果”“重新预估”。
- `bom-costing` 等技术目录、路由和历史模型名暂时保留,避免为改名引入无关迁移。
- “核算”仅可用于描述已经确认的历史采购/生产成本,不能描述本功能输出。

View File

@ -0,0 +1,310 @@
# OA 看板钉钉待办接入方案
## 结论
正式系统**不能只靠新增环境变量**就直接发送钉钉待办。
原因:
1. 仓库当前只实现了钉钉登录与用户识别,没有“创建 / 更新 / 删除钉钉待办”的服务代码。
2. 当前没有“业务流程记录 <-> 钉钉待办 ID”的持久化表。
3. 当前没有发送幂等、失败重试、关闭待办、升级提醒等基础能力。
但从现有代码基础看,**补一层服务实现后,再由正式环境配置环境变量即可上线**。
## 现有基础
### 已具备
1. 钉钉 OAuth 登录能力
- 文件:`src/server/services/dingtalk-auth.service.ts`
- 已能完成:
- 构建授权地址
- 获取用户 access token
- 获取 unionId
- 获取 app access token
- 用 unionId 换取 userId
- 获取用户详情
2. 用户钉钉身份字段
- 系统已保存并使用:
- `dingtalkUserId`
- `dingtalkUnionId`
3. OA 看板责任人映射
- OA 看板已经能把不同阶段映射到默认责任角色 / 人员字段。
- 例如首单下单待发起时可兜底为 `采购/跟单`。
4. 责任与跟进界面基础
- 页面已有“责任与跟进”区域,适合作为钉钉待办的业务入口。
### 尚缺失
1. 钉钉待办发送 service
2. 待办投递日志表
3. 待办幂等规则
4. 待办关闭 / 更新规则
5. 定时升级提醒任务
6. 正式环境待办配置项
## 官方能力判断
根据钉钉开放平台官方文档,待办能力当前支持:
1. 创建工作待办
2. 更新待办
3. 删除待办
4. 查询用户待办
与本项目相关的关键点:
1. 创建待办时需要应用身份和用户身份,不能只知道姓名。
2. 待办通常需要可回跳的 `detailUrl`,跳回本系统详情页或看板页。
3. 如果要做闭环,流程状态变更后还要同步更新或关闭钉钉侧待办。
官方文档参考:
- [待办概述](https://open.dingtalk.com/document/development/dingtalk-todo-task-overview)
- [创建钉钉待办任务](https://open.dingtalk.com/document/development/add-dingtalk-to-do-task)
- [更新钉钉待办任务](https://open.dingtalk.com/document/development/updates-dingtalk-to-do-tasks)
- [删除钉钉待办任务](https://open.dingtalk.com/document/development/delete-dingtalk-to-do-tasks)
- [更新钉钉待办执行者状态](https://open.dingtalk.com/document/development/update-dingtalk-to-do-status)
## 正式环境建议新增的环境变量
下面这些变量建议作为**待办功能正式接入专用配置**。当前代码尚未消费它们。
```env
# 是否启用 OA 看板钉钉待办
DINGTALK_TODO_ENABLED=false
# 钉钉开放平台 API 应用配置(待办、审批等服务端调用)
DINGTALK_API_CLIENT_ID=
DINGTALK_API_CLIENT_SECRET=
# 钉钉网页登录使用独立 OAuth 应用配置,不与 API 应用混用
DINGTALK_LOGIN_CLIENT_ID=
DINGTALK_LOGIN_CLIENT_SECRET=
DINGTALK_CALLBACK_ORIGIN=
# 待办跳转回系统的基础地址
DINGTALK_TODO_DETAIL_BASE_URL=
# 用于构造钉钉待办来源标识 / 业务空间
DINGTALK_TODO_SOURCE_APP=oa-board
# 是否开启超时升级提醒
DINGTALK_TODO_ESCALATION_ENABLED=false
# 超时升级阈值(天)
DINGTALK_TODO_ESCALATION_DAYS=3
```
如果最终接入方案要求额外的应用标识,还可能需要:
```env
DINGTALK_AGENT_ID=
```
是否必须取决于最终选用的钉钉待办接口版本与应用类型。
## 建议新增的数据表
建议新增一张专门的待办投递表,避免把钉钉待办状态散落在 JSON 里。
建议表名:
- `oa_board_dingtalk_todo_dispatch`
用途:
1. 记录某个业务流程 / 阶段是否已创建钉钉待办
2. 保存钉钉待办 ID / 执行人 / 状态
3. 支持去重、重试、关闭、审计
SQL 草案见:
- `scripts/sql/create-oa-board-dingtalk-todo-dispatch-table.sql`
## 建议的发送时机
### 第一阶段:最小闭环
先只做一类待办:
1. 当流程进入新的“当前审批阶段”
2. 且解析出了唯一责任人
3. 且该阶段还没有有效待办
4. 则创建一条钉钉待办
建议首批接入阶段:
1. `产品设计-外观`
2. `产品设计-结构`
3. `产品生产-首单下单`
4. `限价-发起`
原因:
1. 这些阶段责任人相对清晰
2. 都已经有字段或规则可映射负责人
3. 业务价值高,适合先验证
### 第二阶段:关闭与更新
当以下任一情况发生时,同步钉钉待办:
1. 当前阶段变化
- 关闭旧待办
- 为新阶段责任人创建新待办
2. 流程完成 / 驳回 / 终止
- 关闭对应待办
3. 负责人变更
- 更新执行人,或关闭旧待办后重建
### 第三阶段:升级提醒
当流程满足以下条件时再发升级提醒:
1. 阶段超时
2. 原待办已创建
3. 距离上次提醒超过冷却时间
4. 升级接收人可解析
升级对象建议:
1. 当前负责人
2. 当前负责人上级
3. 商品企划 / 荆川 / 管理员可选兜底
## 负责人解析策略
待办发送目标不能只靠中文名,必须最终落到钉钉身份。
建议解析顺序:
1. 当前运行任务的 `userId`
- 直接映射 `dingtalkUserId`
2. 表单中责任人字段
- 如 `appearanceDesigner`、`structureDesigner`、`merchandiserOwner`、`pricingOwner`
- 用姓名匹配本地用户 / 员工目录,取 `dingtalkUserId`
3. 阶段默认角色兜底
- 例如 `采购/跟单`
- 通过角色到成员名单的映射表进一步解析
如果一个阶段只能解析到“角色名”而不能解析到唯一员工,建议:
1. 不直接发待办
2. 记录 `NEEDS_OWNER_MAPPING`
3. 在看板中提示“负责人映射未完成”
## 需要新增的服务模块
建议至少拆成这几个文件:
1. `src/server/services/dingtalk-todo.service.ts`
- 创建 / 更新 / 删除待办
- 管理 app token
2. `src/server/services/oa-board-dingtalk-todo-dispatch.service.ts`
- 负责业务规则
- 判断是否该发待办
- 生成 payload
- 落库记录
3. `src/server/services/oa-board-owner-resolution.service.ts`
- 负责将阶段负责人解析成钉钉用户
4. `scripts/sync-oa-board-todos.ts`
- 用于定时补发、关闭、升级
## 需要新增的 API / 任务
### 后台任务
建议新增定时任务:
1. `sync:oa-board:todos`
- 扫描当前有效流程
- 创建缺失待办
- 关闭失效待办
- 触发升级提醒
### 管理接口
如有需要,可新增:
1. `POST /api/intake/oa-board/todo-dispatch/retry`
- 手工重试失败待办
2. `GET /api/intake/oa-board/todo-dispatch`
- 查看待办发送日志
## 幂等与重试建议
待办系统必须做幂等。
建议唯一业务键:
```text
board_key + stage_key + receiver_dingtalk_user_id
```
规则:
1. 同一个业务键,只允许存在 1 条 `OPEN / PENDING / RETRYING` 记录
2. 接口超时后不立即重发,先查日志状态
3. 网络失败按指数退避重试
## 正式环境能否“只配置就开”
最终判断:
1. **现在不能**
- 因为缺少待办发送代码和日志表
2. **补完代码后可以**
- 正式系统只需配置环境变量
- 再开启定时任务即可
## 推荐落地顺序
### P1
1. 新增待办投递表
2. 新增 `dingtalk-todo.service.ts`
3. 先打通“创建待办”
4. 先只接 `产品生产-首单下单`
### P2
1. 接入更多阶段
2. 做关闭 / 更新逻辑
3. 做负责人映射异常日志
### P3
1. 做超时升级提醒
2. 做看板内待办发送状态可视化
3. 做手工重试与运维面板
## 建议的产品口径
为了降低误发风险,建议待办文案统一包含:
1. 开款编号
2. 当前阶段
3. 目标动作
4. 超时状态
5. 跳转链接
示例:
```text
【OA 新品流程看板】XP2026070900000291 已进入“产品生产-首单下单”,请尽快处理。
```

View File

@ -0,0 +1,121 @@
# 供应链产品全量档案字段来源对照表
更新时间:2026-10-16
## 1. 合并与追踪规则
- 汇总表:`SupplyChainProductArchive`
- 唯一合并键:`barcode`
- 来源记录:`SupplyChainProductArchiveSource`
- 字段级来源:`SupplyChainProductArchive.fieldSourceMap`
- 产品全量档案页面使用的来源数据必须先写入 `SupplyChainProductArchive` 或图片缓存;页面列表不再临时联查来源库。
- 新品图片先用 `external.product_catalog_wide_v1.archive_image` 判断当前 SKU/条码行是否真的有图;确认有图后,保存同一行标准化的绝对 `image_url`,绝对地址为空时才保留 `archive_image`。当 `archive_image` 为空时禁止读取 `image_url`,因为该列此时可能回退到产品级图片。宽表当前行无图时,再按当前条码/SKU精确查询素材库;命中的链接和来源信息保存在 `SupplyChainProductArchiveImageCache`。图片二进制不写入 PostgreSQL,图片变化不进入商品修改日志。
- 首次建立商品档案只记录一条“商品档案导入”;后续来源值实际变化时,按条码、字段、旧值、新值和来源写入 `OperationLog`。
- 扩展字段通过 `sync:supply-chain-product-archive-enrichment` 分批同步;重复执行时只有值发生变化的字段才更新主表并写日志。
- 生产定时入口为 `scripts/install-supply-chain-product-archive-sync-timer.sh`,默认每 15 分钟先同步 `rpt.jst_sku` 基础档案,再同步供应商、仓库、生产周期、日销和价格等扩展字段。
- 聚水潭来源只读取 `JSReportReplica.rpt.jst_sku` 中 `item_type = '成品'` 的行。
- `jst_sku` 只更新本表明确列出的字段;空值不覆盖其他来源的已有值。
- 已由 `JST_SKU` 提供且有值的字段,不再被产品开发同步覆盖。
- 一级类目由 `SupplyChainProductArchiveCategoryConfig` 按 JST 二级类目匹配,未配置时保持空值。
- 创建时间优先级固定为:`云杉商品资料.xlsx` 的“创建时间” > `jst_sku.created` > 产品开发创建时间。
- 云杉文件只在部署或补数时通过导入脚本读取一次,不进入 Git,页面运行时不读取 Excel。
- 体积统一由同一来源记录的长、宽、高相乘计算;任一尺寸缺失、不是有效数字或不大于 0 时保持空值。
- 未列出的字段不猜测、不兜底,保持原来源或空值。
## 2. 当前字段对照
| 全量档案字段 | 页面表头 | JST_SKU 来源 | 产品开发来源 | 其他来源 | 转换规则 |
| --- | --- | --- | --- | --- | --- |
| `barcode` | 条码 | `rpt.jst_sku.sku_id` | `external.product_catalog_wide_v1.barcode_business_key` | | 去空格并转大写;档案唯一键 |
| `spu` | SPU | `rpt.jst_sku.i_id` | `archive_spu` | | 去空格并转大写 |
| `sku` | SKU | `rpt.jst_sku.other_7` | `archive_sku` | | 去空格并转大写;JST 空值不兜底 |
| `image` | 图片 | | `external.product_catalog_wide_v1.archive_image` + `image_url` | 素材库接口 + `SupplyChainProductArchiveImageCache.previewUrl` | 新品以当前 SKU/条码行 `archive_image` 非空为前提,保存同一行标准化 `image_url`;`archive_image` 为空时不得使用可能回退到产品级图片的 `image_url`;当前行无图时再按当前条码/SKU精确查询素材库,命中链接长期复用,未命中则留空 |
| `businessCreatedAt` | 创建时间 | `rpt.jst_sku.created` | `product_development_created_at`,空时取 `source_created_at` | 云杉文件“创建时间”按条码匹配 | 按固定优先级覆盖 |
| `categoryL1` | 一级类目 | | `sales_category_name` | `SupplyChainProductArchiveCategoryConfig.categoryL1` | JST 行按完整二级类目匹配配置表 |
| `categoryL2` | 二级类目 | `rpt.jst_sku.category` | `archive_product_category_name` | | JST 原样保留完整类目文本 |
| `productName` | 商品名称 | `rpt.jst_sku.name` | `archive_product_name` | | 原样保留 |
| `designer` | 设计师 | 暂不接入 | `archive_designer` | | |
| `productStatus` | 商品状态 | `rpt.jst_sku.other_5` | `sku_sales_status` | | 只接受“销售、停产、退市” |
| `material` | 材质 | `rpt.jst_sku.other_10` | `archive_material` | | 原样保留 |
| `color` | 颜色 | `rpt.jst_sku.properties_value` | `archive_color` | | 原样保留 |
| `weight` | 重量 | `rpt.jst_sku.weight` | `sample_weight_kg` | | 转十进制数;空值不覆盖 |
| `length` | 长 | `rpt.jst_sku.l` | `package_length_cm` | | 转十进制数;空值不覆盖 |
| `width` | 宽 | `rpt.jst_sku.w` | `package_width_cm` | | 转十进制数;空值不覆盖 |
| `height` | 高 | `rpt.jst_sku.h` | `package_height_cm` | | 转十进制数;空值不覆盖 |
| `volume` | 体积 | `rpt.jst_sku.l * w * h` | `package_length_cm * package_width_cm * package_height_cm` | | 不读取来源体积列,统一按长宽高计算 |
| `packType` | 单双包 | `rpt.jst_sku.other_8` | `package_set_count` | | 1-9 转为非双包至九包;已是标准文字时原样保留 |
| `productTag` | 商品标签 | `rpt.jst_sku.labels` | `product_tag` | | 原样保留 |
| `installDocId` | 安装图纸/视频 ID | `rpt.jst_sku.other_9` | `drawing_id` | | 原样保留 |
| `spec` | 产品尺寸 | 从 `rpt.jst_sku.name` 提取 | `archive_spec` | | 提取三轴尺寸,统一 `/` 分隔,保留小数和范围 |
| `brand` | 品牌 | `rpt.jst_sku.brand` | `brand_name` | | 原样保留 |
| `orderType` | 叫货类型 | `rpt.jst_sku.other_price_2` | `purchase_order_method` | | 按现有枚举归一 |
| `storageType` | 入库/代发 | | | `SupplyChainProductArchiveWarehouseConfig.storageType` | 按出入库仓库名称匹配配置表 |
| `isSeasonal` | 是否季节性产品 | 暂无来源 | 暂无来源 | | 保持空值 |
| `replaceCount` | 替换款数量 | 暂无来源 | 暂无来源 | | 保持空值 |
| `region` | 区域 | | | `rpt.js_supplier.region` | 按供应商名称匹配后写主表 |
| `supplierName` | 供应商 | `rpt.jst_sku.supplier_name` | `archive_supplier_name` | | 原样保留 |
| `supplierType` | 供应商类型 | | | `rpt.js_supplier.scmtype` | 按供应商名称匹配后写主表 |
| `supplierStatus` | 供应商状态 | | | `rpt.js_supplier.coopersgn` | `1=合作`、`2=不合作` |
| `purchaseOwner` | 采购 | | `archive_purchase_owner` | `rpt.js_supplier.buyer` | 供应商采购非空时覆盖产品开发采购 |
| `settlementMethod` | 结算方式 | | | `rpt.js_supplier.paycyclesgn` | 按供应商名称匹配 |
| `boardProvider` | 板材提供方 | | | `rpt.js_supplier.undefined2` | 按供应商名称匹配 |
| `penaltyStatus` | 违约金赔付状态 | | | `rpt.js_supplier.undefined1` | 按供应商名称匹配 |
| `warehouseType` | 仓库属性 | | | `rpt.jst_warehouseInfo.remark2` | 按出入库仓库名称匹配 |
| `warehouseHead` | 仓库负责人 | | | `SupplyChainProductArchiveWarehouseConfig.warehouseHead` | 按出入库仓库名称匹配配置表 |
| `minimumOrderQuantity` | 产品起订量 | | `archive_minimum_order_quantity` | `rpt.pm_stockMonitor.minimum` | stock monitor 非空时覆盖产品开发起订量 |
| `firstLeadTime` | 首单生产周期 | | | `rpt.pm_stockMonitor.produceCycle` | 条码优先、SKU 唯一匹配兜底 |
| `palletSpec` | 托盘参数 | | | `rpt.pm_stockMonitor.trayParameter` | 条码优先、SKU 唯一匹配兜底 |
| `dailySales` | 采购日销 | | | `rpt.pm_stockMonitor.purchaseSale` | 按条码匹配最新记录 |
| `sales3d` | 3日日销 | | | `rpt.pm_stockMonitor.threeAvgSale` | 按条码匹配最新记录 |
| `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`;匹配不到时留空。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 第一批字段覆盖率
统计范围:`item_type = '成品'`,共 65,164 行。
| 来源字段 | 对应档案字段 | 非空行数 |
| --- | --- | ---: |
| `sku_id` | 条码 | 65,164 |
| `i_id` | SPU | 65,164 |
| `name` | 商品名称、规格解析 | 65,164 |
| `other_7` | SKU | 63,537 |
| `other_5` | 商品状态 | 63,771 |
| `other_9` | 安装图纸/视频 ID | 24,792 |
| `other_10` | 材质 | 31,394 |
| `properties_value` | 颜色 | 63,855 |
| `category` | 二级类目 | 63,539 |
| `weight` | 重量 | 35,754 |
| `l` | 长 | 38,224 |
| `w` | 宽 | 38,083 |
| `h` | 高 | 38,124 |
| `other_8` | 单双包 | 61,908 |
| `labels` | 商品标签 | 以本次来源库实时数据为准 |
| `brand` | 品牌 | 以本次来源库实时数据为准 |
| `supplier_name` | 供应商 | 以本次来源库实时数据为准 |
## 4. 来源代码
| 来源代码 | 来源系统 | Schema | 对象 |
| --- | --- | --- | --- |
| `JST_SKU` | 聚水潭 JSReportReplica | `rpt` | `jst_sku` |
| `PRODUCT_DEVELOPMENT` | 家具产品开发系统 | `external` | `product_catalog_wide_v1` |
| `CATEGORY_CONFIG` | 供应链中心类目配置 | `public` | `SupplyChainProductArchiveCategoryConfig` |
| `JST_SUPPLIER` | 聚水潭 JSReportReplica | `rpt` | `js_supplier` |
| `JST_STOCK_MONITOR` | 聚水潭 JSReportReplica | `rpt` | `pm_stockMonitor` |
| `JST_WAREHOUSE_INFO` | 聚水潭 JSReportReplica | `rpt` | `jst_warehouseInfo` |
| `JST_SKU_PRICE` | 聚水潭 JSReportReplica | `rpt` | `jst_sku` |
| `PRODUCT_CATALOG_COST_PRICE` | 家具产品开发系统 | `external` | `product_catalog_wide_v1` |
| `WAREHOUSE_CONFIG` | 供应链中心 | `public` | `SupplyChainProductArchiveWarehouseConfig` |
| `YUNSHAN_PRODUCT_EXPORT` | 云杉商品资料 | Excel 工作表名 | Excel 文件名 |
## 5. 生产同步入口
- 产品开发页面保存成功后,会按当前产品即时同步 `external.product_catalog_wide_v1` 的档案字段。
- 聚水潭及其关联表由 systemd timer 周期检查。同步过程可以重复执行,主表只写实际变化,商品修改日志只记录后续真实变化。
- 定时任务安装前必须在生产服务器的 `shared/supply-chain-product-archive-sync.env` 填写目标 PostgreSQL 和来源 SQL Server 的身份校验值;文件权限固定为 `600`,其中不保存数据库密码。
- 安装命令只在发布完成并获得生产操作授权后执行:`bash scripts/install-supply-chain-product-archive-sync-timer.sh`。
- 新品即时同步以宽表当前 SKU/条码行的 `archive_image` 是否存在为准,并把同一行标准化的绝对 `image_url` 写入档案和 `SupplyChainProductArchiveImageCache`。宽表当前行无图时,页面才按当前条码/SKU调用素材库接口并回写链接;存量图片规则不在本次新品图片调整范围内。

View File

@ -29,6 +29,7 @@
- `scripts/print-master-data-foundation-decision-package.js`
- `npm run db:verify-master-data:all`
- `npm run db:verify-master-data:audit`
- `npm run db:verify-master-data:production-readonly`
- `npm run db:verify-master-data:readiness`
- `npm run db:verify-master-data:readiness:audit`
- `node scripts/verify-master-data-code-boundaries.js --mode reference-estimator-cost-boundary`
@ -46,6 +47,8 @@
- `npm run db:foundation:apply-structure-repair`
- `npm run db:foundation:apply-structure-repair:audit`
- `npm run db:foundation:audit`
- `npm run db:foundation:audit:production-readonly`
- `npm run db:foundation:verify-production-readonly`
- `npm run db:foundation:audit:cleanup-template`
- `npm run db:foundation:completion-checklist`
- `npm run db:foundation:decision-package`
@ -72,9 +75,13 @@
完整地基审计批次使用 `npm run db:foundation:audit`。它会在 `.master-data-audits/foundation-audit-<timestamp>/` 下依次写出 `readiness.json`、`foundation-hard-ready-gate.json`、`foundation-preflight.json`、`foundation-migrate-plan.json` 和 `manifest.json`;即使其中任一门禁失败,也会继续收集后续证据并在 manifest 中汇总 blocker。这个入口只做只读/干跑证据收集,不生成备份、不执行迁移、不清洗数据;它的用途是给每次治理形成同一时刻的可复核证据包。`manifest.json` 会写入脱敏 `readinessSummary` 和 `foundationHardReadyGateSummary`,包含 `hardReady`、blocker 数、blocker key、数量值和迁移应用状态摘要,但不会复制历史 SKU、产品头或准 SKU 的样本行。`manifest.hardReady` 必须同时满足 readiness 总就绪度和 foundationHardReadyGate 总门禁都为 true,任何一个缺失或失败都不能宣称地基已十分稳固。
最终完成判断必须看 `manifest.foundationCompletionChecklist`,不能把某个局部命令的绿色结果当成全目标完成。该清单把本目标拆成 8 个机器可判定项:数据库结构与 Prisma 迁移历史、核心对象唯一真相源矩阵、后端写入边界、JSON/快照边界、文件系统边界、历史主数据字段清洗、PreSku 正式化生命周期和授权结构修复施工链。`npm run db:foundation:completion-checklist` 默认读取 `.master-data-audits/foundation-audit-current-goal/manifest.json`,输出脱敏清单;只要任一项 `blocked / missing`,命令就以非 0 状态退出。它不重新审计数据库,只是把同批证据的完成判定单独拎出来,给协作、验收和 goal completion audit 使用。
生产或目标库只读巡检优先使用 `npm run db:foundation:audit:production-readonly`。该入口会让 readiness 和 hard-ready gate 都走 `--read-only-quality`,迁移状态只通过只读查询确认;如果治理迁移还不是 `fullyReady`,脚本会直接把迁移缺口写入报告,不会在生产库事务内回放迁移 SQL。单项快速确认可用 `npm run db:verify-master-data:production-readonly`,它只输出脱敏 summary。生产只读证据包仍可能包含真实表结构、数量和阻断键,应留在生产本地或受控交接中,不提交 Git。拿到生产/目标库 manifest 后,再运行 `npm run db:foundation:verify-production-readonly -- --manifest <manifest.json>`,它只接受 `auditProfile = production-read-only`、`migrationApplicationStatus.source = read-only-migration-status`、`hardReady = true`、blocker 归零、步骤全通过且证据未过期的报告;本地 full audit、shadow 演练或混入写库计划的 manifest 不能冒充生产只读收口证据。
如果完成清单没有 8/8 通过,下一步施工判断看 `npm run db:foundation:decision-package`。它同样默认读取 `.master-data-audits/foundation-audit-current-goal/manifest.json`,只从 manifest 的脱敏摘要和完成清单生成“决策包”,把剩余阻断拆成结构修复授权、治理迁移落库、历史字段业务确认、PreSku 生命周期专用决策和最终验证五段。这个入口不写数据库、不复制敏感候选行、不输出绝对路径;用途是防止后续把 `apply-structure`、`resolve-baseline`、`apply-required`、历史清洗和 PreSku 专用修复混成一条危险捷径。决策包的 `nextGate` 只表示下一道施工门禁,不表示该门禁已经被授权或已完成。
最终完成判断必须看 `manifest.foundationCompletionChecklist`,不能把某个局部命令的绿色结果当成全目标完成。该清单把本目标拆成 9 个机器可判定项:数据库结构与 Prisma 迁移历史、核心对象唯一真相源矩阵、后端写入边界、JSON/快照边界、文件系统边界、历史主数据字段清洗、历史对象存储引用迁移、PreSku 正式化生命周期和授权结构修复施工链。`npm run db:foundation:completion-checklist` 默认读取 `.master-data-audits/foundation-audit-current-goal/manifest.json`,输出脱敏清单;只要任一项 `blocked / missing`,命令就以非 0 状态退出。它不重新审计数据库,只是把同批证据的完成判定单独拎出来,给协作、验收和 goal completion audit 使用。
历史对象存储引用迁移要用 `npm run storage:migrate-historical-assets:gate` 或在批次审计里显式追加 `--include-storage-migration-gate`。该 gate 只做 dry-run,但会要求旧本地素材、旧 S3/七牛链接的源文件可读取,且目标对象不存在时不能继续放行;失败报告只作为本地敏感证据包保存,不应提交 Git。
如果完成清单没有 9/9 通过,下一步施工判断看 `npm run db:foundation:decision-package`。它同样默认读取 `.master-data-audits/foundation-audit-current-goal/manifest.json`,只从 manifest 的脱敏摘要和完成清单生成“决策包”,把剩余阻断拆成结构修复授权、治理迁移落库、历史字段业务确认、历史对象存储引用迁移、PreSku 生命周期专用决策和最终验证六段。这个入口不写数据库、不复制敏感候选行、不输出绝对路径;用途是防止后续把 `apply-structure`、`resolve-baseline`、`apply-required`、历史清洗、对象存储迁移和 PreSku 专用修复混成一条危险捷径。决策包的 `nextGate` 只表示下一道施工门禁,不表示该门禁已经被授权或已完成。
决策包还必须输出 `executionChecklist`,把真实施工拆成“授权前刷新证据 -> 真实执行结构修复 SQL -> 结构修复后重新预检 -> 逐项补齐 Prisma 迁移历史 -> 应用 bootstrap-repair 结构/回填修复 -> 应用最终治理迁移/护栏 -> 业务确认后清洗历史质量债 -> 最终 hardReady 审计”。其中所有 `writeDatabase=true` 的阶段都必须有明确用户授权;`apply-structure` 成功前禁止进入 `resolve-baseline / apply-required / 历史清洗`,`post-repair preflight` 未证明结构 diff 归零前禁止 baseline resolve,治理迁移未 `fullyReady` 前禁止历史清洗。这个顺序是地基施工门禁,不是建议步骤。
@ -295,6 +302,8 @@ baseline resolve 完成后,治理迁移、外部只读视图、约束、触发
- 后端写入入口也必须和数据库护栏同口径,不能把数据库约束当成第一道业务校验。当前已收紧 `PlanningTemplate.code`、`WarehouseLocation.code`、`Substrate.code`、`Material.code`、`Hardware.code`、`CommonComponent.code`、正式 `Product.modelNumber`、正式 `ProductSKU.skuCode`、`IntakeNonIntegratedSkuItem.skuCode / barcode`、SKU CAD 导入、传统产品上传、准 SKU 包装方案保存、品牌物料准备项、正式入库承接链路、协作者角色和产品上传可见范围:进入仓储层前先规范化身份字段,空编码/空身份、游离状态、负数数量、重复物理库位、非法权限角色和可提前发现的冲突由服务层直接阻断。
- 仓储查询入口不能再把“调用方已经规范化”当隐含前提。资产中心、正式产品、正式 SKU、基础物料、库位和规划模板等核心 `findByCode / findBySkuCode / findByModelNumber / codeExists` 查询会先做 `trim + uppercase`;资产 CAD 导入也会在查找已有资产族、创建规格款和生成模型存储路径前归一手填编码,避免同一编码因大小写或首尾空格在服务层表现不一致。
- 正式 SKU 资产同步副本的身份字段必须在快照构建层二次规范化:`ProductSkuAssetProfile.model / specCode / barcode` 以及 `sourceVariants` 中对应字段进入结构化副本前统一 `trim + uppercase`,人工锁定字段也不能绕过数据库 `CHECK` 护栏。
- `ProductSkuAssetProfile.lockedFields / lockedReasons` 只能保存登记过的锁定字段名,快照构建会过滤空白、拼错和重复字段;readiness 会把历史 profile 中无效锁定字段或无效锁定原因列为 hard blocker,避免人工锁定优先级变成不可审计的自由 JSON。
- `AssetSkuSyncEvent` 不只检查字段形态,还要作为资产中心到正式 SKU 同步是否真实闭环的运行证据。readiness 会把 `FAILED`、超过 15 分钟仍停留在 `PENDING / PROCESSING`、以及完成态仍保留失败计数的事件列为 hard blocker,避免源资产变更、缩略图重生成或删除事件没有真正刷新正式 SKU 资产资料副本。
- `PackagedItem.code` 由正式 `ProductSKU.skuCode` 和包装箱编号生成;正式入库前必须校验包装箱编号是唯一正整数,且包装箱尺寸必须大于 0、预估重量不能为负数,避免用重复箱号、小数箱号或脏箱规生成冲突、不稳定的包装成品和轻仓库数据。
- `SpuNumberCounter.productCategory / prefix` 是正式 SPU 号段身份,不只要求非空和大写,还必须数据库级唯一;产品品类还要按去空格和大小写归一后唯一。不同产品品类不能共用同一个前缀,同一产品品类也不能因为空格差异形成两个号池,否则并发占号会变成不可追溯的双计数器。
- 正式入库前会校验 `PreSku.variantSelection` 引用的 `AssetVariant` 仍存在、未删除、状态可用、资产族和资产类型未漂移;正式生成板件、孔位和五金时读取当前 `AssetVariant` 结构化数据,不再信任旧快照里的尺寸和孔位。

View File

@ -18,7 +18,7 @@
地基批次审计:`npm run db:foundation:audit` 用于把 `readiness`、`foundationHardReadyGate` 总门禁、结构变更前置预检、baseline/结构修复施工图和迁移 dry-run 计划放进同一个本地证据目录,并生成 `manifest.json` 汇总 blocker。它不写数据库,不生成备份,也不替代迁移授权;用途是让每次治理前后的证据可对比、可复核。`manifest.hardReady` 必须同时满足 `readiness.hardReady = true`、`foundationHardReadyGate.hardReady = true` 且没有 blockers;总门禁原始报告会标记为敏感文件,manifest 只暴露脱敏的 `foundationHardReadyGateSummary`。
最终完成清单:完整批次审计会在 `manifest.foundationCompletionChecklist` 中把“数据库结构与迁移历史、唯一真相源矩阵、后端写入边界、JSON/快照边界、文件系统边界、历史字段清洗、PreSku 生命周期、授权施工链”拆成逐项 `passed / blocked / missing`。需要把这份清单单独输出给协作或验收时,运行 `npm run db:foundation:completion-checklist`;该命令默认读取 `.master-data-audits/foundation-audit-current-goal/manifest.json`,只打印脱敏清单,并且只在所有项都 passed 时以 0 状态退出。它不是新的审计来源,只是防止把某个局部绿色结果误判为“数据地基已完成”。
最终完成清单:完整批次审计会在 `manifest.foundationCompletionChecklist` 中把“数据库结构与迁移历史、唯一真相源矩阵、后端写入边界、JSON/快照边界、文件系统边界、历史字段清洗、历史对象存储引用迁移、PreSku 生命周期、授权施工链”拆成逐项 `passed / blocked / missing`。需要把这份清单单独输出给协作或验收时,运行 `npm run db:foundation:completion-checklist`;该命令默认读取 `.master-data-audits/foundation-audit-current-goal/manifest.json`,只打印脱敏清单,并且只在所有项都 passed 时以 0 状态退出。它不是新的审计来源,只是防止把某个局部绿色结果误判为“数据地基已完成”。对象存储引用迁移证据通过 `--include-storage-migration-gate` 纳入批次审计;未纳入时该项为 `missing`,源文件缺失或上传验证失败时该项为 `blocked`。
批次审计需要 shadow diff 留证时,显式使用 `--use-docker-shadow`。该模式会临时启动 Docker PostgreSQL shadow 库,把 `SHADOW_DATABASE_URL` 传给 preflight 和迁移计划步骤,结束后清理容器,并在 manifest 中写入 `shadowDiffEvidence`。它只证明迁移链和当前库/当前 schema 的差异,不代表目标数据库已经迁移完成。批次审计也可以显式追加 `--include-migration-rehearsal`,把从备份恢复到 Docker 克隆库并执行 `migrate deploy` 的演练报告写入同一目录的 `migration-rehearsal.json`。

View File

@ -12,14 +12,18 @@
- `external.*_v1` 视图是否齐全。
- `external.intake_*_ready_v1` 是否存在,外部稳定消费是否能避开不完整草稿资料。
- 仓库库位、SKU 库存、包装库存项和 SKU 质检记录是否已经进入外部契约层。
- 非集成产品限价是否已经进入结构化限价视图和 SKU 聚合宽表。
- 关键字段是否齐全。
- 正式 SKU、非集成产品和非集成 SKU 是否都暴露稳定业务键,避免外部系统误用内部技术 ID 做同步身份。
- 仓库视图是否暴露稳定库位编码,质检视图是否暴露稳定记录键。
- `external_reader` 是否被误授权到 `public` schema。
- `external_reader` 在 `external` schema 下是否只有 `SELECT` 权限。
- 对外字段里是否混入 `payload`、权限、内部路径、密钥类字段名。
- `external` 视图定义内部是否仍依赖 `payload / attributes / businessData / displaySummary / familySelection` 等 JSON 快照源。
- 正式 SKU、资产规格款、非集成 SKU 的关键字段是否存在明显空值。
- 正式 SKU 和非集成 ready 视图的业务键是否缺失或重复。
- 产品限价视图是否存在适用但未确认的限价记录。
- 正式产品素材和 BOM 明细视图是否携带可关联的稳定业务键。
- 素材 URL 是否仍有不适合长期对外同步的 `data:` 形态。
- 非集成 SKU 明细是否已经达到 `READY`,还是仍有 `INCOMPLETE` 草稿数据。
@ -43,22 +47,39 @@ npx prisma db execute --file scripts/sql/check-external-readonly-contract.sql
- `missing_external_column`
- `external_contract_private_column_risk`
- `external_view_json_source_dependency_risk`
- `external_reader_public_schema_grant_risk`
- `external_reader_non_select_grant_risk`
- `external_readonly_role_public_schema_grant_risk`
- `external_readonly_role_non_select_grant_risk`
- `external_asset_url_unstable_or_internal_risk`
- `product_assets_data_url_risk`
- `product_sku_business_key_duplicate_risk`
- `intake_product_business_key_duplicate_risk`
- `intake_sku_business_key_duplicate_risk`
- `product_assets_business_key_missing_risk`
- `product_bom_business_key_missing_risk`
- `sku_warehouse_stock_business_key_missing_risk`
- `packaged_items_business_key_missing_risk`
- `sku_quality_inspections_business_key_missing_risk`
- `intake_product_limits_business_key_missing_risk`
- `intake_product_limits_sku_business_key_duplicate_risk`
- `warehouse_location_code_duplicate_risk`
- `packaged_item_code_duplicate_risk`
- `sku_quality_record_key_duplicate_risk`
下面这些检查结果是计数项,不一定要求马上为 0,但要进入数据质量治理台账:
- `product_skus_blank_key_fields`
- `warehouse_locations_blank_key_fields`
- `sku_warehouse_stock_blank_key_fields`
- `packaged_items_blank_key_fields`
- `sku_quality_inspections_blank_key_fields`
- `asset_variants_blank_key_fields`
- `intake_sku_items_blank_key_fields`
- `intake_sku_items_quality_status_count`
- `intake_ready_business_key_missing_risk`
- `intake_product_limits_state_count`
- `intake_product_limits_applicable_unconfirmed_risk`
- `product_catalog_wide_limit_state_count`
- `product_catalog_wide_applicable_unconfirmed_limit_risk`
如果关键编码、名称、型号大量为空,说明外部系统虽然能读数据,但读到的是不完整资料,不能直接进入稳定对接阶段。
@ -72,7 +93,7 @@ npx prisma db execute --file scripts/sql/check-external-readonly-contract.sql
- 授权越界:撤回 `public` schema 或非 `SELECT` 权限,再重新执行授权脚本。
- 私有字段风险:从视图里移除字段,必要时新增白名单字段。
- 视图 JSON 源依赖风险:先判断该字段是否应该结构化;不能只改外部列名来绕过巡检。
- 业务键缺失或重复:回到对应真相源修复 `Product.modelNumber / ProductSKU.skuCode / IntakeNonIntegratedProduct.brand+spu / IntakeNonIntegratedSkuItem.skuCode`,不能让外部系统临时用技术 ID 兜底;如果素材或 BOM 明细缺业务键,优先修视图关联链,不要让外部系统自行跨表补。
- 业务键缺失或重复:回到对应真相源修复 `Product.modelNumber / ProductSKU.skuCode / IntakeNonIntegratedProduct.brand+spu / IntakeNonIntegratedSkuItem.skuCode / WarehouseLocation.code / ProductSkuQualityInspection.recordKey`,不能让外部系统临时用技术 ID 兜底;如果素材、BOM、仓库或质检明细缺业务键,优先修视图关联链,不要让外部系统自行跨表补。
- 关键字段为空:回到资产中心、产品资料或非集成录入链路修源数据。
- `data:` 素材:逐步迁移到文件 URL 或受控下载 API。
- `INCOMPLETE` 非集成 SKU:外部系统不能自动入库,先由产品资料链路补齐或确认废弃;如果业务确认分阶段补齐,原因只写入 `MasterDataQualityDecision` 做审计,不改变 ready 视图过滤规则。
@ -86,7 +107,7 @@ npx prisma db execute --file scripts/sql/check-external-readonly-contract.sql
`MasterDataQualityDecision` 不对外作为商品资料视图开放;它只是内部审计谁确认了暂不完整、缺哪些字段、为什么允许分阶段清洗。外部系统仍只能把 `READY` 当自动消费前提。
`npm run db:verify-master-data:all` 已把核心外部契约风险纳入硬失败:外部核心视图缺失、外部视图依赖内部 JSON、非集成产品负责人兼容列未优先读取结构化 profile、正式 SKU 视图业务键缺失或重复、正式产品素材/BOM 明细视图业务键缺失、ready SKU 视图混入非 READY 明细、ready 视图业务键缺失或重复时,命令必须失败。独立 SQL 巡检仍保留给人工查库和外部审计使用。
`npm run db:verify-master-data:all` 已把核心外部契约风险纳入硬失败:外部核心视图缺失、外部视图依赖内部 JSON、非集成产品负责人兼容列未优先读取结构化 profile、正式 SKU 视图业务键缺失或重复、正式产品素材/BOM 明细视图业务键缺失、仓库/质检视图业务键缺失、ready SKU 视图混入非 READY 明细、ready 视图业务键缺失或重复时,命令必须失败。独立 SQL 巡检仍保留给人工查库和外部审计使用。
## 5. 规范审查

View File

@ -8,6 +8,9 @@
- `prisma/migrations/20260530143000_tighten_external_readonly_view_sources/migration.sql`
- `prisma/migrations/20260530150000_add_intake_non_integrated_product_profile/migration.sql`
- `prisma/migrations/20260531103000_tighten_external_intake_views_to_structured_sources/migration.sql`
- `prisma/migrations/20260608143000_add_external_product_catalog_wide_view/migration.sql`
- `prisma/migrations/20260608150000_add_external_warehouse_quality_views/migration.sql`
- `prisma/migrations/20260608162000_add_intake_product_limit_rows/migration.sql`
关联巡检:`scripts/sql/check-external-readonly-contract.sql`
## 1. 使用原则
@ -23,6 +26,12 @@
| 视图 | 用途 | 主要来源 |
| --- | --- | --- |
| `external.product_skus_v1` | 正式产品 SKU 主数据 | `Product`、`ProductSKU`、`ProductSkuAssetProfile`、`ProductCategory`、`WarehouseLocation` |
| `external.product_catalog_wide_v1` | 商品目录聚合宽表 | `external.product_skus_v1`、`external.intake_products_ready_v1`、`external.intake_sku_items_ready_v1`、`external.intake_product_limits_v1`、正式素材/BOM/仓库/质检外部视图 |
| `external.intake_product_limits_v1` | 非集成产品限价白名单 | `IntakeNonIntegratedProductLimitRow`、`IntakeNonIntegratedSkuItem`、`IntakeNonIntegratedProductProfile` |
| `external.warehouse_locations_v1` | 仓库库位资料 | `WarehouseLocation` |
| `external.sku_warehouse_stock_v1` | 正式 SKU 库存和库位 | `external.product_skus_v1`、`external.warehouse_locations_v1` |
| `external.packaged_items_v1` | 已关联正式 SKU 的包装库存项 | `PackagedItem`、`ProductSKU`、`Product`、`WarehouseLocation` |
| `external.sku_quality_inspections_v1` | SKU 仓库/首批质检记录 | `ProductSkuQualityInspection`、`external.product_skus_v1` |
| `external.product_assets_v1` | 正式 SKU 图片/模型/缩略图 URL | `ProductSKU.images`、`ProductSKU.modelUrl`、`ProductSkuAssetProfile.imageUrl`、`Product.thumbnailUrl` |
| `external.product_bom_v1` | 正式产品板件和五金摘要 | `Panel`、`PanelMaterial`、`ProductHardware`、`Hardware` |
| `external.intake_products_v1` | 非集成开品主信息白名单 | `IntakeNonIntegratedProduct`、`IntakeNonIntegratedProductProfile` |
@ -62,6 +71,7 @@
| `asset_family_code` | 来源资产族编码 |
| `spec_code` | 来源规格款编码 |
| `barcode` | 条码 |
| `barcode_business_key` | 外部最小商品身份键,规则为产品条码的规范化大写值 |
| `size_json` | 尺寸 JSON |
| `color` | 颜色/花色 |
| `material` | 材质 |
@ -72,7 +82,8 @@
- 该视图过滤已软删除的 `Product` 和 `ProductSKU`。
- 外部系统读取正式商品资料时优先使用该视图。
- `product_id / sku_id` 只用于内部追踪和问题定位;外部系统同步、去重和幂等更新必须优先使用 `product_business_key / sku_business_key`。
- `product_id / sku_id` 只用于内部追踪和问题定位;外部系统同步、去重和幂等更新优先使用 `barcode_business_key`。
- `product_business_key` 用于 SPU/型号归组;`sku_business_key` 用于 SKU 编码兼容关联。
- 资产资料字段来自 `ProductSkuAssetProfile`;`assetSkuSnapshot` 只保留为内部兼容快照,不作为该视图主读来源。
## 4. 正式素材:`external.product_assets_v1`
@ -88,6 +99,7 @@
| `sku_id` | SKU ID,内部追踪用技术键,不作为外部业务身份 |
| `product_business_key` | 外部稳定产品业务键,规则为正式产品型号/SPU 的规范化大写值 |
| `sku_business_key` | 外部稳定 SKU 业务键,规则为 SKU 编码的规范化大写值 |
| `barcode_business_key` | 外部最小商品身份键,规则为产品条码的规范化大写值 |
| `asset_url` | 可访问素材 URL |
| `file_name` | 文件名,当前为空,后续可补 |
| `mime_type` | MIME 类型,当前为空,后续可补 |
@ -96,7 +108,7 @@
注意:
- 该视图只输出 URL,不输出磁盘路径。
- 外部系统关联素材时必须优先使用 `product_business_key / sku_business_key`,`product_id / sku_id` 仅用于内部追踪。
- 外部系统关联 SKU 素材时优先使用 `barcode_business_key`,`product_business_key / sku_business_key` 用于归组和兼容关联,`product_id / sku_id` 仅用于内部追踪。
- 后续如需要签名 URL,应在 API 层做,不直接改原表。
## 5. 正式 BOM 摘要:`external.product_bom_v1`
@ -112,6 +124,7 @@
| `sku_id` | SKU ID,可为空,内部追踪用技术键,不作为外部业务身份 |
| `product_business_key` | 外部稳定产品业务键,规则为正式产品型号/SPU 的规范化大写值 |
| `sku_business_key` | 外部稳定 SKU 业务键;当 BOM 行不绑定 SKU 时可为空 |
| `barcode_business_key` | 外部最小商品身份键;当 BOM 行不绑定 SKU 时可为空 |
| `item_id` | 物料行 ID |
| `item_code` | 物料编码 |
| `item_name` | 物料名称 |
@ -126,7 +139,7 @@
注意:
- 第一版重点是结构稳定,成本口径后续单独治理。
- 外部系统关联 BOM 时必须优先使用 `product_business_key / sku_business_key`,`product_id / sku_id` 仅用于内部追踪。
- 外部系统关联 SKU 级 BOM 时优先使用 `barcode_business_key`;产品级 BOM 行可使用 `product_business_key` 归组,`product_id / sku_id` 仅用于内部追踪。
- 孔位数据暂不进入普通 BOM 视图,避免把制造/3D 数据混进商品资料。
## 6. 非集成开品主信息:`external.intake_products_v1`
@ -155,6 +168,10 @@
| `visual_planning` | 空间/视觉负责人 |
| `structure_designer` | 结构设计负责人 |
| `purchase_owner` | 采购负责人结构化字段 |
| `appearance_designer` | 外观设计师明确字段;来自非集成产品基础列 |
| `spu_owner` | SPU 负责人明确字段;优先来自结构化 profile,旧运营负责人只作兜底 |
| `planning_owner` | 商品企划明确字段;来自结构化 profile |
| `space_designer` | 空间设计师明确字段;优先来自结构化 profile 的空间/视觉负责人 |
| `product_development_kind` | 开发类型 |
| `goods_type` | 商品类型 |
| `material_type` | 材料类型 |
@ -164,11 +181,269 @@
- 该视图过滤已软删除记录。
- 产品名称、图片、品类、年份、状态、开发类型、货品类型、材质类型、生产类型和结构化负责人字段来自 `IntakeNonIntegratedProductProfile`,不再直接读取 `payload`。
- `designer / operator / purchaser` 是兼容列,优先输出 profile 中的结构设计、商品企划、采购负责人;旧产品行列只作为兼容兜底。
- `designer / operator / purchaser` 是兼容列,优先输出 profile 中的结构设计、商品企划、采购负责人;旧产品行列只作为兼容兜底。新接入优先读取 `appearance_designer / structure_designer / spu_owner / planning_owner / space_designer / purchase_owner` 这些明确字段。
- `intake_product_id` 只用于内部追踪和问题定位;外部系统同步、去重和幂等更新必须优先使用 `product_business_key`。
- 不开放 `visibility`、`departmentId`、`teamId`、审批状态、内部确认状态。
- `external.intake_products_ready_v1` 使用同一字段结构,但只保留产品名称、品类、开发类型和 `product_business_key` 都已补齐的记录。外部系统做稳定同步时优先读取 ready 视图;`intake_products_v1` 用于治理对账和人工补齐。
## 6A. 商品目录聚合宽表:`external.product_catalog_wide_v1`
核心用途:给外部系统一张表同步商品目录。正式 `Product / ProductSKU` 已存在时优先输出正式 SKU 行;正式商品尚未落库但 non-integrated 已进入 ready 状态时,补输出可消费的开发阶段 SKU 行。
关键字段:
| 字段 | 含义 |
| --- | --- |
| `product_business_key` | 外部稳定产品业务键 |
| `sku_business_key` | 外部稳定 SKU 业务键 |
| `product_model_number` | 产品型号/SPU;正式行来自 `Product.modelNumber`,non-integrated ready 行来自 `spu` |
| `product_name` | 产品名称 |
| `product_category_name` | 产品分类 |
| `sku_code` | SKU 编码 |
| `sku_name` | SKU 名称 |
| `price` | 售价;non-integrated ready 行暂为空 |
| `cost_price` | 成本价;non-integrated ready 行暂为空 |
| `stock` | 库存;non-integrated ready 行默认 `0` |
| `record_status` | 外部统一状态:`ACTIVE` / `INACTIVE`;non-integrated ready 行默认 `ACTIVE` |
| `model_url` | SKU 或产品模型 URL |
| `image_url` | SKU 主图 URL |
| `image_urls` | SKU 图片 URL 数组 |
| `asset_family_code` | 来源资产族编码 |
| `spec_code` | 来源规格款编码 |
| `barcode` | 条码 |
| `barcode_business_key` | 外部最小商品身份键,规则为产品条码的规范化大写值 |
| `size_json` | 尺寸 JSON |
| `color` | 颜色/花色 |
| `material` | 材质 |
| `appearance_designer` | 产品开发明细的外观设计师;开发阶段 ready 行来自产品资料负责人字段,正式商品行暂为空 |
| `structure_designer` | 产品开发明细的结构设计师;开发阶段 ready 行来自产品资料负责人字段,正式商品行暂为空 |
| `spu_owner` | 产品开发明细的 SPU 负责人;开发阶段 ready 行来自产品资料负责人字段,正式商品行暂为空 |
| `planning_owner` | 产品开发明细的商品企划;开发阶段 ready 行来自产品资料负责人字段,正式商品行暂为空 |
| `space_designer` | 产品开发明细的空间设计师;开发阶段 ready 行来自产品资料负责人字段,正式商品行暂为空 |
| `purchase_owner` | 产品开发明细的采购负责人;开发阶段 ready 行来自产品资料负责人字段,正式商品行暂为空 |
| `supplier_name` | 产品定厂供应商名称;开发阶段 ready 行来自结构化 SKU 定厂资料,正式商品行暂为空 |
| `supplier_code` | 产品定厂供应商编码;开发阶段 ready 行来自结构化 SKU 定厂资料,正式商品行暂为空 |
| `factory_warehouse` | 产品定厂发货仓;与库存库位 `warehouse_location_name` 是两个不同业务字段 |
| `shipping_method` | 产品定厂发货方式 |
| `minimum_order_quantity` | 产品定厂最低起订量 |
| `tax_included_unit_price` | 产品定厂含税单价 |
| `lead_time` | 产品定厂交期 |
| `install_method` | 产品定厂安装方式 |
| `jst_product_pushed_at` | 商品资料成功推送聚水潭的时间;未成功推送时为空 |
| `is_external_intake` | 是否由外部录入人员创建 |
| `supply_chain_archive_eligible` | 是否满足供应链产品全量档案当前入档规则 |
| `source_pricing_row_id` | 产品开发核价记录 ID,用于追踪原始业务记录 |
| `source_product_id` | 产品开发产品 ID,用于定向增量同步和排查 |
| `source_sku_item_id` | 产品开发结构化 SKU 行 ID |
| `product_development_created_at` | 产品首次录入产品开发系统的时间,作为档案创建时间口径 |
| `product_development_updated_at` | 产品开发系统最近更新时间 |
| `archive_spu / archive_sku` | 产品全量档案使用的 SPU、SKU 口径 |
| `archive_product_name` | 产品全量档案商品名称,来自条码对应规格名称 |
| `archive_image` | 产品开发条码/SKU 图片原值 |
| `sales_category_name` | 一级类目,例如桌类、柜类 |
| `archive_product_category_name` | 二级类目原值,档案同步时补住宅家具/商业办公/其他前缀 |
| `sku_sales_status` | 销售状态原值;档案只接受销售、停产、退市 |
| `brand_name` | 产品开发品牌 |
| `sample_weight_kg` | 样包测量重量(千克) |
| `package_length_cm / package_width_cm / package_height_cm` | 样包长宽高(厘米) |
| `package_volume_cm3` | 样包体积(立方厘米) |
| `package_set_count` | 套件量;档案转换为非双包、双包至九包 |
| `purchase_order_method` | 叫货/采购方式 |
| `archive_shipping_method` | 产品全量档案的入仓/代发口径 |
| `warehouse_location_code` | 仓库库位编码 |
| `warehouse_zone / warehouse_aisle / warehouse_shelf / warehouse_bin` | 库位物理位置 |
| `warehouse_location_status` | 库位状态:`ACTIVE` / `INACTIVE` |
| `limit_price_applicable` | 是否适用限价;存量商品明细默认 `false`,产品开发明细和模块化产品明细默认 `true` |
| `limit_price_confirmed` | 产品限价节点是否已确认;只有适用限价时才有业务意义 |
| `limit_price_status` | 限价状态:`NOT_APPLICABLE` / `PENDING` / `CONFIRMED` |
| `expected_daily_limit_price` | 期望日常限价;来自结构化产品限价行 |
| `expected_limit_gross_margin_rate` | 期望限价毛利率 |
| `non_spu_owner_limit_price` | 非 SPU 负责人限价 |
| `limit_confirmed_at` | 产品限价确认时间 |
| `limit_source_product_business_key` | 限价来源非集成产品业务键 |
| `limit_source_sku_business_key` | 限价来源非集成 SKU 业务键 |
| `image_asset_count` | SKU 图片素材数量 |
| `model_asset_count` | SKU 模型素材数量 |
| `thumbnail_asset_count` | SKU 缩略图素材数量 |
| `first_model_asset_url` | 第一条模型素材 URL |
| `first_thumbnail_asset_url` | 第一条缩略图素材 URL |
| `bom_item_count` | BOM 行数 |
| `panel_item_count` | 板件行数 |
| `hardware_item_count` | 五金行数 |
| `material_codes` | BOM 涉及材质编码数组 |
| `material_names` | BOM 涉及材质名称数组 |
| `packaged_item_count` | 已关联正式 SKU 的包装库存项数量 |
| `packaged_stock_quantity` | 包装库存项库存合计 |
| `quality_record_count` | SKU 质检记录数 |
| `warehouse_quality_record_count` | 仓库质检记录数 |
| `first_batch_quality_record_count` | 首批质检记录数 |
| `latest_quality_record_key` | 最新质检记录业务键 |
| `latest_quality_source_type` | 最新质检来源:`WAREHOUSE` / `FIRST_BATCH` |
| `latest_quality_batch_no` | 最新质检批次号 |
| `latest_quality_inspected_at` | 最新质检时间 |
| `latest_quality_conclusion` | 最新质检结论 |
| `latest_quality_status` | 最新质检状态 |
| `created_at` | SKU 创建时间 |
| `updated_at` | SKU 更新时间 |
注意:
- 产品全量档案只读取本宽表中 `supply_chain_archive_eligible = true` 的记录,不再由同步程序直接关联产品开发业务表或解析 `payload`。
- 当前入档规则为:产品开发系统创建、商品资料已成功推送聚水潭、创建人不是外部录入人员。
- 同一条码即使最终目录行优先采用正式商品,产品开发的入档字段仍按条码附加在该行末尾,避免来源标记被正式行覆盖。
- 该视图保持一条码一行,适合外部系统做商品目录同步;同一 SKU 下多个条码会输出多行。
- 如果外部系统只想读一张表,优先使用该宽表;它已经覆盖正式 SKU 与可消费的 non-integrated ready SKU,以及产品资料、库存库位摘要、包装库存摘要和最新质检摘要。
- 需要逐条素材或逐条 BOM 时,仍读取 `external.product_assets_v1 / external.product_bom_v1`;这两张明细视图是否对外授权,以当前 `external_core_reader` 白名单为准。
- 所有 URL 字段都应是完整系统入口;相对的 `/api/...` 或 `/materials/...` 路径会在外部视图层按 `app.external_asset_base_url` 补全,未设置时默认补到 `https://cp.jiansun.vip`。
- `appearance_designer / structure_designer / spu_owner / planning_owner / space_designer / purchase_owner` 是产品开发明细页红框负责人字段的外部同步口径;它们只对 non-integrated ready 行有明确来源,正式商品行没有开发阶段上下文时保持空值。
- `supplier_name / supplier_code / factory_warehouse / shipping_method / minimum_order_quantity / tax_included_unit_price / lead_time / install_method` 来自结构化非集成 SKU 定厂资料;外部视图不读取 `payload.factoryRows`。其中 `factory_warehouse` 是定厂发货仓,不得与库存库位字段混用。
- 需要逐条包装库存或质检明细时,读取 `external.packaged_items_v1 / external.sku_quality_inspections_v1`。
- 限价口径:`存量商品明细` 的产品一般没有限价,宽表会输出 `limit_price_status = NOT_APPLICABLE`;`产品开发明细`、`模块化产品明细` 到 `产品限价` 节点确认后,限价视为正式口径并输出为 `CONFIRMED`。
- `product_id / sku_id` 只用于内部追踪;正式行会输出正式产品/SKU ID,non-integrated ready 行会输出 `intake_product_id / intake_sku_item_id`;外部主同步、去重和幂等更新必须使用 `barcode_business_key`。
- `product_business_key` 用于 SPU/型号归组;宽表里统一按 `SPU / sku_code` 的规范化值输出,不沿用 ready 视图里的 `brand:spu(:sku)` 组合键口径。
- 宽表限价优先按 `barcode_business_key` 匹配,只有条码缺失时才回退到 `sku_business_key / sku_code`。
- non-integrated ready 行尚未正式化时,正式 BOM、包装库存、仓库库位和 SKU 质检摘要字段默认仍为 `0 / NULL / []`;这些字段只有正式链路落库后才会有正式真值。
## 6A-1. 非集成产品限价:`external.intake_product_limits_v1`
核心用途:给外部系统读取非集成产品限价的结构化白名单。该视图一 SKU 一行,不开放整段 `payload`。
关键字段:
| 字段 | 含义 |
| --- | --- |
| `product_business_key` | 非集成产品业务键,规则为 `品牌:SPU` |
| `sku_business_key` | 非集成 SKU 业务键,规则为 `品牌:SPU:SKU编码` |
| `product_development_kind` | 开发类型:`product` 为产品开发明细,`series` 为模块化产品明细 |
| `process_template` | 流程模板;`存量商品` 表示存量商品明细 |
| `sku_code` | SKU 编码 |
| `barcode` | 条码 |
| `limit_price_applicable` | 是否适用限价 |
| `limit_price_confirmed` | 产品限价节点是否确认 |
| `limit_price_status` | `NOT_APPLICABLE` / `PENDING` / `CONFIRMED` |
| `expected_daily_limit_price` | 期望日常限价 |
| `expected_limit_gross_margin_rate` | 期望限价毛利率 |
| `non_spu_owner_limit_price` | 非 SPU 负责人限价 |
| `limit_confirmed_at` | 限价确认时间 |
注意:
- `存量商品明细` 默认限价不适用,即使历史工作区存在临时限价行,也不作为外部正式限价口径。
- `产品开发明细`、`模块化产品明细` 在 `产品限价` 节点确认后,限价字段就是正式口径。
- 外部主同步优先读 `external.product_catalog_wide_v1`;需要核对非集成限价来源时再读本视图。
## 6B. 仓库库位:`external.warehouse_locations_v1`
核心用途:给外部系统读取库位资料。
关键字段:
| 字段 | 含义 |
| --- | --- |
| `warehouse_location_id` | 库位 ID,内部追踪用技术键 |
| `warehouse_location_code` | 库位编码 |
| `warehouse_location_name` | 库位名称 |
| `zone` | 分区 |
| `aisle` | 巷道 |
| `shelf` | 货架 |
| `bin` | 箱位 |
| `is_active` | 是否启用 |
| `record_status` | `ACTIVE` / `INACTIVE` |
| `created_at` | 创建时间 |
| `updated_at` | 更新时间 |
注意:
- 外部系统同步库位时优先使用 `warehouse_location_code`。
- 该视图不输出库位备注,避免把内部管理说明带到外部系统。
## 6C. SKU 仓库库存:`external.sku_warehouse_stock_v1`
核心用途:给外部系统读取正式 SKU 的库存数量和库位。
关键字段:
| 字段 | 含义 |
| --- | --- |
| `product_business_key` | 外部稳定产品业务键 |
| `sku_business_key` | 外部稳定 SKU 业务键 |
| `sku_code` | SKU 编码 |
| `sku_name` | SKU 名称 |
| `barcode` | 产品条码 |
| `barcode_business_key` | 外部最小商品身份键,规则为产品条码的规范化大写值 |
| `stock_quantity` | SKU 库存数量 |
| `sku_record_status` | SKU 状态 |
| `warehouse_location_code` | 库位编码 |
| `warehouse_location_name` | 库位名称 |
| `warehouse_zone / warehouse_aisle / warehouse_shelf / warehouse_bin` | 库位物理位置 |
| `warehouse_location_status` | 库位状态 |
注意:
- 该视图一 SKU 一行,来源是正式 `ProductSKU.stock` 和 `ProductSKU.warehouseLocationId`。
- 外部系统关联库存时优先使用 `barcode_business_key`,不要使用 `sku_id` 做业务主键;`sku_business_key` 只做兼容关联。
## 6D. 包装库存项:`external.packaged_items_v1`
核心用途:给外部系统读取已关联正式 SKU 的包装库存项。
关键字段:
| 字段 | 含义 |
| --- | --- |
| `packaged_item_id` | 包装库存项 ID,内部追踪用技术键 |
| `packaged_item_code` | 包装库存项编码 |
| `packaged_item_name` | 包装库存项名称 |
| `product_business_key` | 外部稳定产品业务键 |
| `sku_business_key` | 外部稳定 SKU 业务键 |
| `sku_code` | SKU 编码 |
| `barcode` | 产品条码 |
| `barcode_business_key` | 外部最小商品身份键,规则为产品条码的规范化大写值 |
| `quantity_per_pack` | 单包数量 |
| `stock_quantity` | 包装库存数量 |
| `length / width / height` | 包装尺寸 |
| `weight` | 包装重量 |
| `status` | 包装项状态 |
| `warehouse_location_code` | 库位编码 |
注意:
- 该视图只输出已关联正式 SKU 的 `PackagedItem`;准 SKU 草稿包装过程数据不在本视图中开放。
- 不输出 `PackagedItem.payload`,避免把包装过程 JSON 和成本草稿整段暴露给外部系统。
## 6E. SKU 质检记录:`external.sku_quality_inspections_v1`
核心用途:给外部系统读取 SKU 级仓库质检和首批质检记录。
关键字段:
| 字段 | 含义 |
| --- | --- |
| `quality_inspection_id` | 质检记录 ID,内部追踪用技术键 |
| `record_key` | 质检记录业务键 |
| `source_type` | `WAREHOUSE` / `FIRST_BATCH` |
| `source_label` | 质检来源中文标签 |
| `batch_no` | 批次号 |
| `inspected_at` | 质检时间 |
| `inspector_name` | 质检人 |
| `conclusion` | 质检结论 |
| `detail` | 质检说明 |
| `inspected_quantity` | 检验数量 |
| `qualified_quantity` | 合格数量 |
| `qualified_rate` | 合格率 |
| `status` | 质检记录状态 |
| `product_business_key` | 外部稳定产品业务键 |
| `sku_business_key` | 外部稳定 SKU 业务键 |
| `sku_code` | SKU 编码 |
| `barcode` | 产品条码 |
| `barcode_business_key` | 外部最小商品身份键,规则为产品条码的规范化大写值 |
注意:
- 外部系统关联质检记录时优先使用 `barcode_business_key`,记录自身去重使用 `record_key`。
- 不输出 `metrics / attachments` 内部 JSON;如果外部系统后续确实需要附件,应单独设计附件白名单视图或只读 API。
## 7. 非集成 SKU 明细:`external.intake_sku_items_v1`
核心用途:给外部系统读取非集成开品中的 SKU 明细。

View File

@ -14,6 +14,13 @@
统一入口是 `src/lib/storage.ts`。
当前读取真相已明确拆成两层:
- 稳定客户端入口:数据库和 DTO 只保存系统稳定地址,例如登录态 `/api/storage/object?key=...`。
- 服务端直读地址:由 storage adapter 按当前 provider 生成临时可访问地址,例如签名 CDN URL。
业务页面、服务层和 DTO 只依赖“稳定入口”,不写死 bucket、CDN 域名或七牛 SDK 细节。
当前支持:
- `STORAGE_PROVIDER=s3`:MinIO / S3 兼容对象存储。
@ -48,6 +55,8 @@ QINIU_PRIVATE_URL_EXPIRES_SECONDS=3600
当前 `sucaiku-js` 空间为私有空间。七牛测试域名不支持 HTTPS,测试阶段可先使用 `QINIU_PUBLIC_DOMAIN=http://tevepd6od.hd-bkt.clouddn.com`,并设置 `QINIU_USE_HTTPS=false`。后续换正式 HTTPS 域名时只替换 `QINIU_PUBLIC_DOMAIN` 并恢复 `QINIU_USE_HTTPS=true`,数据库内的稳定系统访问地址不需要批量迁移。
七牛官方对测试域名的口径是:测试域名只供测试使用,不在 SLA 保障范围内,有效期有限;绑定自定义域名并设为默认域名后,只影响后续新生成外链,历史测试域名链接不会自动替换。因此测试域名、正式 CDN 域名、历史 S3 域名都不能作为数据库长期真相。
HTTP 测试域名只适合临时验证。素材需要保密时,正式环境应使用已绑定证书的 HTTPS CDN 域名,避免浏览器混合内容拦截和传输层明文风险。
私有空间不能直接把短期签名 URL 保存成长期资料字段。统一存储入口会在私有模式下保存 `/api/storage/object?key=...` 这类系统内稳定地址;读取时由后端受登录保护的接口临时生成七牛下载签名并跳转。
@ -58,6 +67,14 @@ HTTP 测试域名只适合临时验证。素材需要保密时,正式环境应
- 七牛 HTTP 测试域名:系统稳定读取接口不把 HTTP 签名 URL 直接暴露给浏览器,而是由后端代理读取并返回,避免 HTTPS 页面出现混合内容拦截。
- 后端代理只解决浏览器侧混合内容和签名 URL 暴露问题;HTTP 测试域名在七牛到应用服务器这一段仍不是加密传输,不能作为正式保密链路长期使用。
当前稳定读取入口分三类:
- 登录态素材:`GET /api/storage/object?key=...`,先走应用鉴权,再 `307` 到短时签名 CDN URL。
- 分享态素材:`GET /api/share/[token]/object?key=...&sku=...`,先校验分享 token 和对象范围,再 `307` 到短时签名 CDN URL。
- 审批态素材:审批 DTO 继续输出稳定系统入口,最终仍通过登录态读取链路完成鉴权和 `307` 跳转。
换 bucket、换 CDN 域名、甚至未来换 provider 时,只允许改环境变量和 `src/lib/storage.ts` 里的 storage adapter。业务页、分享页、审批页和服务层不应该重写链接拼接逻辑。
七牛侧如果保持“无自定义跨域规则”,空间默认允许跨域访问;如果后续新增自定义 CORS 规则,必须允许本系统正式域名和本地调试域名读取模型、贴图和缩略图,并暴露 `Content-Range`、`Accept-Ranges` 等范围请求相关响应头,否则浏览器中的 3D/WebGL 资源加载可能失败。
## 后续迁移规则
@ -80,3 +97,9 @@ HTTP 测试域名只适合临时验证。素材需要保密时,正式环境应
3. 历史 demo / seed 中的本地路径样本。
每次迁移后必须跑主数据巡检,确认没有新增“文件系统读取 + 主数据写入”的反向造数风险。
外部只读契约额外要求:
- `external.*_v1` 视图不能输出七牛测试域名、`data:`、`file:`、`localhost`、内网 IP、相对路径、内部 API 路径、`public/` 或 `uploads/` 这类不可长期外部读取的素材地址。
- 外部系统如果需要读取素材,必须拿到正式 HTTPS 可访问 URL,或走专门的后端下载/签名 API;不能依赖本系统登录态 API 路径。
- `scripts/sql/check-external-readonly-contract.sql` 和 `npm run db:verify-master-data` 会把上述风险作为外部契约问题检查出来。

View File

@ -0,0 +1,363 @@
# 店铺账号管理取数与筛选逻辑
> 用途:给需要接入店铺账号管理数据的开发人员使用。本文描述当前代码和数据库的真实口径。
>
> 重要:本文不包含数据库连接串、账号、密码、加密密钥或任何业务明文数据。当前数据由本系统人工维护,不直接从外部数据库同步。
## 1. 数据源与责任边界
- **权威数据源**:本系统使用 `DATABASE_URL` 指向的 PostgreSQL 数据库,数据通过本系统店铺账号管理页面/API 人工新增、编辑、导入和维护。
- **不属于数据源的内容**:聚水潭、京东、京喜等外部系统目前不是本模块的自动同步来源;不能假设外部系统中存在同一份实时数据。
- **推荐接入方式**:优先调用本系统鉴权 API。若确实需要数据库直读,应由数据库管理员单独创建只读账号或只读视图,并按本文的字段白名单开放;不要复用应用账号,也不要把应用数据库密码或加密密钥交给外部系统。
- **人工维护原则**:不要直接写入业务表绕过 Service。直接写入会跳过页面权限、字段权限、密文加密、唯一键校验和操作日志。
### 1.1 代码与迁移依据
当前数据契约以以下文件为准:
- Prisma 模型:`prisma/schema.prisma` 中 `StoreAccount`、`StoreAccountStoreProfile`、`StoreAccountChangeLog`、`StoreAccountFieldPermission`、`SupplyChainPermissionOverride`。
- 建表迁移:`prisma/migrations/20260826120000_add_store_account_management/migration.sql`。
- 店铺主体和银行卡字段、平台资料表迁移:`prisma/migrations/20260828100000_extend_store_account_profiles/migration.sql`。
- 列表筛选规则:`src/server/repositories/store-account-management.repository.ts`。
- 业务校验、脱敏和日志:`src/server/services/store-account-management.service.ts`。
- 导出字段:`src/server/services/store-account-management-export.service.ts`。
## 2. 物理表清单与关联关系
PostgreSQL 物理表名使用双引号保留大小写,取数时应写成下表中的名称。
| 物理表 | 用途 | 主要关联键 | 是否主业务数据 |
| --- | --- | --- | --- |
| `"StoreAccount"` | 店铺账号主记录,一行代表一个店铺下的一种账号 | `id`;业务唯一键为 `storeName + accountType + account` | 是 |
| `"StoreAccountStoreProfile"` | 京东/京喜店铺资料,一行代表一个店铺的一个平台资料 | 逻辑关联 `storeName`,平台由 `platform` 区分 | 是(扩展资料) |
| `"StoreAccountChangeLog"` | 新增、编辑、导入、查看敏感字段、资料维护的审计日志 | `storeAccountId` 可为空;资料日志用 `storeName` | 否(审计) |
| `"StoreAccountFieldPermission"` | 店铺账号字段对业务类型/个人的可见、只读、可编辑配置 | `fieldKey + subjectKey` 唯一 | 否(权限配置) |
| `"SupplyChainPermissionOverride"` | 页面级的查看、下载、编辑权限 | `targetKey + subjectKey + action` 唯一;本模块 `targetKey = 'storeAccountManagement'` | 否(权限配置) |
`StoreAccountStoreProfile` 与 `StoreAccount` 没有数据库外键,应用层按 `storeName` 关联。因此外部查询时要注意:资料表可能没有对应账号行,也可能一个店铺有多个账号行;不要用 `id` 去连接资料表。
## 3. `"StoreAccount"` 字段字典
| 字段 | PostgreSQL 类型/约束 | 业务含义 | 空值与敏感性 |
| --- | --- | --- | --- |
| `id` | `TEXT`,主键 | 账号记录 ID | 系统生成,不为空 |
| `storeName` | `TEXT NOT NULL` | 店铺名称,也是资料表的逻辑关联键 | 不应为空;支持多选筛选 |
| `operationOwner` | `TEXT NOT NULL DEFAULT ''` | 店铺运营负责人 | 空字符串表示未维护 |
| `operationDepartmentHead` | `TEXT NOT NULL DEFAULT ''` | 店铺运营部门负责人 | 空字符串表示未维护 |
| `operationDirector` | `TEXT NOT NULL DEFAULT ''` | 店铺运营总监 | 空字符串表示未维护 |
| `profitCenter` | `TEXT NOT NULL DEFAULT ''` | 店铺所属利润中心 | 空字符串或:`运营中心A`、`运营中心B`、`运营中心C`、`运营中心G` |
| `subject` | `TEXT NOT NULL DEFAULT ''` | 店铺主体 | 空字符串表示未维护 |
| `accountType` | `TEXT NOT NULL` | 账号类型 | 正常值 `MAIN`(主账号)、`SUB`(子账号);不完整导入允许空字符串或 `/` |
| `account` | `TEXT NOT NULL` | 登录账号 | 新增时必填 |
| `passwordCiphertext` | `TEXT NOT NULL DEFAULT ''` | 密码密文 | 绝不作为普通取数字段返回;空字符串表示未设置 |
| `boundBankAccountNumberCiphertext` | `TEXT NOT NULL DEFAULT ''` | 店铺绑定银行卡号密文 | 与密码同等级敏感;空字符串表示未设置 |
| `boundPhone` | `TEXT NOT NULL DEFAULT ''` | 绑定手机号 | 空字符串表示未维护 |
| `verificationContact` | `TEXT NOT NULL DEFAULT ''` | 手机号验证码对接人 | 空字符串表示未维护 |
| `jstOrderService` | `TEXT NOT NULL DEFAULT ''` | 聚水潭订单服务号 | 空字符串表示未维护 |
| `accountGroupName` | `TEXT NOT NULL DEFAULT ''` | 账号分组名称 | 空字符串表示未维护 |
| `createdById` | `TEXT` 可空 | 首次创建人 ID | 系统审计字段 |
| `updatedById` | `TEXT` 可空 | 最近修改人 ID | 系统审计字段 |
| `createdAt` | `TIMESTAMP(3) NOT NULL` | 账号记录创建时间 | 系统生成;列表按此字段倒序 |
| `updatedAt` | `TIMESTAMP(3) NOT NULL` | 账号记录最近修改时间 | 系统自动更新时间 |
约束和索引:
- 唯一约束:`("storeName", "accountType", "account")`,导入按这三个字段执行 upsert。
- 索引:`storeName`、`account`、`accountType`、`updatedAt`。
- 本表没有软删除字段;空白字段是空字符串,不使用“系统默认值”兜底。
## 4. `"StoreAccountStoreProfile"` 字段字典
该表用于“京东/京喜店铺资料”次页面的后续维护,不要求在账号主表导入时同时填写。
| 字段 | PostgreSQL 类型/约束 | 业务含义 | 取值规则 |
| --- | --- | --- | --- |
| `id` | `TEXT`,主键 | 店铺平台资料 ID | 系统生成 |
| `storeName` | `TEXT NOT NULL` | 店铺名称 | 与 `StoreAccount.storeName` 逻辑关联 |
| `platform` | `TEXT NOT NULL` | 平台代码 | `JD`=京东,`JX`=京喜 |
| `merchantNumber` | `TEXT NOT NULL DEFAULT ''` | 商户号(钱包商户号) | 空字符串表示未维护 |
| `secondaryMerchantNumber` | `TEXT NOT NULL DEFAULT ''` | 二级商户号 | 空字符串表示未维护 |
| `merchantStoreId` | `TEXT NOT NULL DEFAULT ''` | 商家 ID(钱包店铺号) | 空字符串表示未维护 |
| `companyId` | `TEXT NOT NULL DEFAULT ''` | 公司 ID(京麦店铺 ID) | 空字符串表示未维护 |
| `createdById` | `TEXT` 可空 | 首次创建人 ID | 系统审计字段 |
| `updatedById` | `TEXT` 可空 | 最近修改人 ID | 系统审计字段 |
| `createdAt` | `TIMESTAMP(3) NOT NULL` | 资料创建时间 | 系统生成 |
| `updatedAt` | `TIMESTAMP(3) NOT NULL` | 资料最近修改时间 | 系统自动更新时间 |
约束和平台规则:
- 唯一约束:`("storeName", "platform")`,同一店铺同一平台只有一条资料。
- 店铺名以 `京东-` 开头时只能使用 `JD`;以 `京喜-` 开头时只能使用 `JX`。
- 非 `京东-` 或 `京喜-` 开头的店铺不支持店铺资料页面/API,服务端会拒绝读取或保存。
- 资料字段通过 `storeName` 关联账号主表,导出时会按平台展开为京东四列和京喜四列。
## 5. `"StoreAccountChangeLog"` 字段与日志格式
| 字段 | PostgreSQL 类型/约束 | 含义 |
| --- | --- | --- |
| `id` | `TEXT`,主键 | 日志 ID |
| `storeAccountId` | `TEXT` 可空 | 账号记录 ID;店铺资料变更时为空 |
| `storeName` | `TEXT NOT NULL` | 店铺名称 |
| `operation` | `TEXT NOT NULL` | 操作类型 |
| `actorId` | `TEXT` 可空 | 操作人 ID |
| `actorName` | `TEXT NOT NULL` | 操作人名称 |
| `changes` | `JSONB NOT NULL` | 字段变更明细数组 |
| `createdAt` | `TIMESTAMP(3) NOT NULL` | 日志时间 |
当前 `operation` 代码:
| operation | 含义 |
| --- | --- |
| `CREATE` | 新增账号 |
| `UPDATE` | 编辑账号 |
| `IMPORT_CREATE` | 导入新增 |
| `IMPORT_UPDATE` | 导入更新 |
| `BATCH_UPDATE` | 批量修改 |
| `VIEW_PASSWORD` | 查看密码 |
| `VIEW_BANK_ACCOUNT` | 查看银行卡号 |
| `PROFILE_UPDATE` | 修改京东/京喜店铺资料 |
`changes` 的标准元素为:
```json
{
"field": "operationOwner",
"previousValue": "原值",
"nextValue": "新值"
}
```
特殊字段和资料字段:
- `password`、`boundBankAccountNumber` 只记录“已设置/未设置”,不写入明文或密文。
- 查看敏感字段时记录 `nextValue: "已查看"`。
- 平台资料字段使用 `JD.merchantNumber`、`JX.companyId` 这样的字段名。
- API 返回日志前还会根据当前调用人的字段权限过滤隐藏字段。
## 6. `"StoreAccountFieldPermission"` 字段字典
| 字段 | PostgreSQL 类型/约束 | 含义 |
| --- | --- | --- |
| `id` | `TEXT`,主键 | 权限记录 ID |
| `fieldKey` | `TEXT NOT NULL` | 店铺账号字段 key,如 `account`、`password`、`boundBankAccountNumber` |
| `subjectKey` | `TEXT NOT NULL` | 业务类型或个人主体 |
| `visibility` | `TEXT NOT NULL DEFAULT 'READONLY'` | `HIDDEN`、`READONLY` 或 `EDITABLE` |
| `updatedById` | `TEXT` 可空 | 最后配置人 ID |
| `createdAt` / `updatedAt` | `TIMESTAMP(3)` | 权限记录时间 |
当前可配置的业务类型 `subjectKey`:
`design`(设计)、`planning`(企划)、`brandCenter`(策划)、`purchase`(采购)、`merchandiser`(采购-跟单)、`pricing`(采购-核价)、`quality`(采购-质检)、`operation`(运营)、`warehouse`(仓库)、`customerService`(客服)、`management`(经管)、`viewer`(普通用户)。
个人例外使用 `user@<用户ID>`。唯一约束为 `("fieldKey", "subjectKey")`。`createdAt`、`updatedAt` 是系统字段,不能通过字段权限配置为可编辑。
## 7. 页面级权限表
`"SupplyChainPermissionOverride"` 的 `targetKey` 固定为 `storeAccountManagement`,`action` 为 `view`、`download`、`edit`。
| 场景 | 当前规则 |
| --- | --- |
| 未登录 | 所有店铺账号接口返回 401 |
| 页面查看 | 需要工作台可见且 `storeAccountManagement:view` 生效 |
| 新增、编辑、导入、批量修改 | 需要页面 `edit` |
| 导出全部或已选 | 需要页面可见,且调用人是管理员或经管 |
| 配置字段权限 | 需要页面可见,且调用人是管理员或经管 |
| 密码/银行卡查看 | 还要通过对应字段权限;服务端单独记录查看日志 |
字段可见性解析顺序:
1. 页面没有 `view` 时,所有字段均为 `HIDDEN`。
2. 先取页面编辑能力形成基础值:系统时间字段只读,密码和银行卡默认隐藏,普通字段按页面编辑能力为可编辑或只读。
3. 同一字段的业务类型配置按 `EDITABLE > READONLY > HIDDEN` 合并。
4. `user@<用户ID>` 的个人配置覆盖业务类型配置。
5. 没有页面编辑权时,即使字段配置为 `EDITABLE` 也会降为只读;系统时间字段永远不会变成可编辑。
列表接口对隐藏字段返回空字符串;密码和银行卡只返回 `passwordConfigured`、`boundBankAccountNumberConfigured` 布尔值,不返回密文。
## 8. 列表取数与筛选逻辑
### 8.1 API 参数
`GET /api/supply-chain/store-account-management`
| 参数 | 默认/限制 | 逻辑 |
| --- | --- | --- |
| `page` | 默认 `1`,最小 `1` | 页码 |
| `pageSize` | 默认 `50`,范围 `1..200` | 每页行数;页面常用 50/100/200 |
| `search` | 最多 120 字符 | 关键词,可用空格、逗号、中文逗号、分号分隔多个 token |
| `storeName` | 最多 120 字符 | 店铺名称多选值,同样支持逗号、中文逗号、分号分隔 |
| `accountType` | 空、`MAIN`/`SUB` 或显示名 | 包含匹配;页面显示主账号/子账号 |
| `operationOwner` | 空或文本 | 店铺运营负责人包含匹配 |
| `accountGroupName` | 空或文本 | 账号分组名称包含匹配 |
| `createdAtGranularity` | `year`、`month`、`day` | 创建时间筛选粒度 |
| `createdAtValue` | 年 `YYYY`;月 `YYYY-MM`;日 `YYYY-MM-DD` | 与粒度配套的值 |
### 8.2 条件组合
- 不同筛选项之间是 **AND**。
- `search` 内部每个 token 会对以下字段做 **OR**:`storeName`、`account`、`operationOwner`、`operationDepartmentHead`、`operationDirector`、`profitCenter`、`subject`、`boundPhone`、`verificationContact`、`jstOrderService`、`accountGroupName`。
- `search` 不搜索密码、银行卡、创建时间、修改时间和京东/京喜资料字段。
- `storeName` 会拆分并去重,使用精确 `IN` 匹配;最多取 500 个店铺名。
- `accountType`、`operationOwner`、`accountGroupName` 使用不区分大小写的 `contains` 匹配。
- 创建时间使用中国时区 `+08:00` 的半开区间:`[开始时间, 结束时间)`。例如 `2026-08` 是 `2026-08-01 00:00:00+08:00`(含)到 `2026-09-01 00:00:00+08:00`(不含)。
### 8.3 排序与分页
固定排序:
```sql
ORDER BY "createdAt" DESC, "id" ASC
```
因此最新创建的记录永远在前面;`id ASC` 只用于创建时间相同时稳定排序。分页使用 `OFFSET (page - 1) * pageSize LIMIT pageSize`,同时查询总数。
成功响应核心结构:
```json
{
"success": true,
"data": {
"rows": [],
"storeNames": ["京东-示例店"],
"pageInfo": { "page": 1, "pageSize": 50, "total": 0, "totalPages": 1 },
"fieldAccess": {}
},
"meta": { "requestId": "...", "timestamp": "..." }
}
```
`storeNames` 是全表去重后按名称升序返回的店铺列表,用于前端多选;它不受当前页面筛选条件限制。
## 9. 只读 SQL 示例
以下 SQL 仅用于说明字段和筛选口径,执行账号必须是单独的只读账号。示例不包含密码密文列。
### 9.1 最新账号列表
```sql
SELECT
"id", "storeName", "operationOwner", "operationDepartmentHead",
"operationDirector", "profitCenter", "subject", "accountType",
"account", "boundPhone", "verificationContact", "jstOrderService",
"accountGroupName", "createdAt", "updatedAt"
FROM "StoreAccount"
ORDER BY "createdAt" DESC, "id" ASC
LIMIT 50 OFFSET 0;
```
### 9.2 店铺 + 账号类型 + 月份筛选
```sql
SELECT
"id", "storeName", "accountType", "account", "createdAt", "updatedAt"
FROM "StoreAccount"
WHERE "storeName" IN ('京东-示例店', '京喜-示例店')
AND "accountType" = 'MAIN'
AND "createdAt" >= TIMESTAMPTZ '2026-08-01 00:00:00+08:00'
AND "createdAt" < TIMESTAMPTZ '2026-09-01 00:00:00+08:00'
ORDER BY "createdAt" DESC, "id" ASC;
```
### 9.3 关键词筛选的 SQL 等价写法
```sql
WHERE (
"storeName" ILIKE '%' || $1 || '%'
OR "account" ILIKE '%' || $1 || '%'
OR "operationOwner" ILIKE '%' || $1 || '%'
OR "operationDepartmentHead" ILIKE '%' || $1 || '%'
OR "operationDirector" ILIKE '%' || $1 || '%'
OR "profitCenter" ILIKE '%' || $1 || '%'
OR "subject" ILIKE '%' || $1 || '%'
OR "boundPhone" ILIKE '%' || $1 || '%'
OR "verificationContact" ILIKE '%' || $1 || '%'
OR "jstOrderService" ILIKE '%' || $1 || '%'
OR "accountGroupName" ILIKE '%' || $1 || '%'
)
```
多个 token 时,把每个 token 的上述括号用 `AND` 连接。
### 9.4 查询平台资料
```sql
SELECT
"storeName", "platform", "merchantNumber", "secondaryMerchantNumber",
"merchantStoreId", "companyId", "createdAt", "updatedAt"
FROM "StoreAccountStoreProfile"
WHERE "storeName" = $1
AND "platform" IN ('JD', 'JX')
ORDER BY "platform" ASC;
```
资料缺失时不要补默认值;没有行或字段为空都表示尚未维护。
### 9.5 查询审计日志
```sql
SELECT
"id", "storeAccountId", "storeName", "operation",
"actorId", "actorName", "changes", "createdAt"
FROM "StoreAccountChangeLog"
WHERE "storeName" = $1
ORDER BY "createdAt" DESC
LIMIT 200;
```
## 10. 当前 API 接口清单
所有接口都要求登录,并返回统一的 `{ success, data/error, meta }` 包装。接口权限还会检查工作台权限和 `storeAccountManagement` 页面权限。
| 方法 | 路径 | 用途 | 主要限制 |
| --- | --- | --- | --- |
| `GET` | `/api/supply-chain/store-account-management` | 分页列表、筛选、店铺名称选项 | 页面 `view` |
| `POST` | `/api/supply-chain/store-account-management` | 新增账号 | 页面 `edit`;店铺名、账号类型、账号必填 |
| `GET` | `/api/supply-chain/store-account-management/{id}` | 单条账号 | 页面 `view` |
| `PATCH` | `/api/supply-chain/store-account-management/{id}` | 编辑账号 | 页面 `edit`;只更新有权限的字段 |
| `POST` | `/api/supply-chain/store-account-management/import` | Excel/CSV 导入账号 | 页面 `edit`;10 MB、5000 行;NDJSON 进度 |
| `POST` | `/api/supply-chain/store-account-management/batch` | 批量修改前预览 | 页面 `edit` + 目标字段 `EDITABLE`;最多 500 条 |
| `PATCH` | `/api/supply-chain/store-account-management/batch` | 批量修改 | 同上;密码、银行卡和系统时间不可批量修改 |
| `GET` | `/api/supply-chain/store-account-management/export` | 导出全部账号 | 仅管理员或经管;包含明文密码/银行卡,必须严格控制调用方 |
| `GET` | `/api/supply-chain/store-account-management/export?scope=selected&ids=id1,id2` | 导出已选账号 | 仅管理员或经管;最多 200 个 ID |
| `GET` | `/api/supply-chain/store-account-management/logs?id=...` | 查询单账号日志 | 页面 `view`;返回内容按字段权限过滤 |
| `GET` | `/api/supply-chain/store-account-management/logs?storeName=...` | 查询店铺日志 | 页面 `view`;最多返回 200 条 |
| `GET` | `/api/supply-chain/store-account-management/{id}/password` | 查看密码明文 | 字段可见;响应不缓存,并写查看日志 |
| `GET` | `/api/supply-chain/store-account-management/{id}/bank-account` | 查看银行卡号明文 | 字段可见,并写查看日志 |
| `GET` | `/api/supply-chain/store-account-management/permissions` | 读取字段权限配置 | 仅管理员或经管 |
| `PUT` | `/api/supply-chain/store-account-management/permissions` | 保存字段权限配置 | 仅管理员或经管;最多 500 条更新 |
| `GET` | `/api/supply-chain/store-account-management/profiles?storeName=...` | 读取京东/京喜店铺资料 | 页面 `view`;店铺名前缀必须匹配 |
| `PATCH` | `/api/supply-chain/store-account-management/profiles` | 保存平台资料 | 页面 `edit`;平台必须与店铺名前缀一致 |
| `GET` | `/api/supply-chain/store-account-management/profiles/template?platform=JD\|JX` | 下载平台资料模板 | 页面 `edit` |
| `POST` | `/api/supply-chain/store-account-management/profiles/import` | 导入平台资料 | 页面 `edit`;表单字段 `file` + `platform`,10 MB、5000 行 |
导出文件为 XLSX,包含账号主表字段、解密后的密码和绑定银行卡号,以及按 `JD`/`JX` 展开的八个店铺资料字段。外部系统若不需要敏感字段,不应调用导出接口,应该调用列表接口或使用只读白名单视图。
## 11. 导入、更新和空白值规则
- 主表导入支持中文表头和内部英文 key;未知列会被忽略。
- 行数上限 5000,文件上限 10 MB;每一行按 `storeName + accountType + account` upsert,并写入 `IMPORT_CREATE` 或 `IMPORT_UPDATE` 日志。
- 普通文本字段导入空白会保存为空字符串,表示“未维护”;不会套用系统默认值。
- 密码和绑定银行卡号为空时不会覆盖已有密文;要清除敏感值需由具备相应字段编辑权限的业务流程明确处理,不能通过普通空白导入误清除。
- 新增账号时 `storeName`、`accountType`、`account` 必填,`accountType` 正常应为 `MAIN` 或 `SUB`。
- 批量修改会先返回所选记录当前值预览,再统一更新;每条实际发生变化的记录写一条 `BATCH_UPDATE` 日志。
- 平台资料导入只识别“店铺名称”和四个资料字段,按店铺名和平台 upsert;空白资料仍然允许保存。
## 12. 敏感字段与外部接入注意事项
1. `passwordCiphertext` 和 `boundBankAccountNumberCiphertext` 使用 AES-256-GCM 存储,密钥来自服务端 `STORE_ACCOUNT_ENCRYPTION_KEY`(生产必须配置至少 32 个字符)。
2. 外部系统不能自行解密,也不能索要或复制该密钥;密文列不应进入普通数据同步任务、日志或缓存。
3. 普通列表接口只返回“是否已设置”布尔值;明文只通过受权限保护的查看接口或管理员/经管导出得到,并且会留下审计日志。
4. 字段权限和页面权限不是同一层:先满足页面 `view/edit`,再应用业务类型和个人字段权限。
5. 直连数据库时建议只开放不敏感白名单字段,至少排除两个 `*Ciphertext` 字段、权限表和日志中的敏感上下文;如需日志,应单独授予审计读取权限。
6. 当前没有外部只读视图或公开 Token 契约。外部接入前应由系统负责人确认鉴权方式、限流、字段白名单、数据刷新频率和脱敏要求。
## 13. 接入前检查清单
- [ ] 确认读取的是当前 `DATABASE_URL` 对应的 PostgreSQL,而不是历史数据库或外部平台库。
- [ ] 使用本文中的双引号物理表名和字段名,确认迁移已执行。
- [ ] 明确只读范围:主表、平台资料、日志、权限配置是否都需要。
- [ ] 明确是否需要京东/京喜资料,按 `storeName + platform` 读取,不能按账号 `id` 连接。
- [ ] 明确筛选时区为中国 `+08:00`,创建时间使用半开区间。
- [ ] 按 `createdAt DESC, id ASC` 保持与页面相同的排序。
- [ ] 不读取或传播密码、银行卡密文;需要明文时走授权接口并保留审计记录。
- [ ] 不直接写表;新增、编辑、导入和权限变更必须走系统 API 或经负责人批准的维护流程。

View File

@ -25,7 +25,7 @@
- PostgreSQL 是本系统主业务数据库。
- 数据库保存业务数据、文件 URL、对象 key、元数据;图片、模型、附件等素材本体不直接存入数据库。
- 外部系统只能读专用只读契约层,不能直接读 `ProductSKU`、`IntakeNonIntegratedProduct.payload` 等业务原表。
- 外部系统同步、去重和幂等更新必须优先使用契约层里的稳定业务键,不能把内部 `id / product_id / sku_id / intake_product_id / intake_sku_item_id` 当业务身份。
- 外部系统主同步、去重和幂等更新必须优先使用 `barcode_business_key`;内部 `id / product_id / sku_id / intake_product_id / intake_sku_item_id` 只能用于问题追踪。
- 内部业务表允许随系统演进调整;外部契约必须版本化、稳定、可追溯。
- 对外开放字段必须白名单化,默认不开放内部流程、权限、草稿、审批、临时 UI 状态。
@ -109,7 +109,9 @@
- `external.product_skus_v1.product_business_key` 来自 `Product.modelNumber` 的规范化大写值。
- `external.product_skus_v1.sku_business_key` 来自 `ProductSKU.skuCode` 的规范化大写值。
- `external.product_skus_v1.barcode_business_key` 来自产品条码的规范化大写值,是外部系统最小商品身份。
- `external.product_assets_v1` 和 `external.product_bom_v1` 也必须携带同一套业务键,避免外部系统为了关联素材或 BOM 被迫使用内部技术 ID。
- `external.product_catalog_wide_v1` 在同一业务键口径上聚合正式 SKU,以及已进入 ready 状态但尚未正式落库的 non-integrated SKU;素材数量、BOM 摘要、仓库、质检和限价摘要优先按 `barcode_business_key` 关联。
### 3.4 非集成开品资料
@ -131,9 +133,12 @@
- `IntakeNonIntegratedSkuItem` 是非集成 SKU 明细的结构化承接表。
- `payload.skuItems` 作为历史过程快照保留,用于同步差异巡检,不作为对外主数据来源。
- `IntakeNonIntegratedProductProfile` 承接产品名称、主图、品类、销售分类、年份、状态、开发类型、货品类型、材质类型、生产类型、主要负责人等产品头主数据。
- `IntakeNonIntegratedProductLimitRow` 承接产品限价行,来源是 `productLimitRows` 的结构化同步副本,不允许外部系统直接读取 `payload` 推断限价。
- 产品定厂的供应商、发货仓、发货方式、最低起订量、含税单价、交期和安装方式由 `IntakeNonIntegratedSkuItem` 结构化列承接;`payload.factoryRows` 只保留兼容快照,不作为外部主读来源。
- `external.intake_products_v1` 读取 `IntakeNonIntegratedProductProfile`,不再直接从 `payload` / `openingReference` 读取产品头主数据。
- `external.intake_products_v1 / external.intake_sku_items_v1` 显式输出稳定业务键:`product_business_key = 品牌:SPU`,`sku_business_key = 品牌:SPU:SKU编码`;ready 视图会要求业务键非空。
- 非空 `skuCode / barcode` 由数据库唯一索引兜底。
- `external.intake_product_limits_v1` 读取 `IntakeNonIntegratedProductLimitRow`;`存量商品明细` 默认限价不适用,`产品开发明细` 和 `模块化产品明细` 到 `产品限价` 节点确认后才视为正式限价。
- `external.intake_products_v1 / external.intake_sku_items_v1` 显式输出稳定业务键:`product_business_key = 品牌:SPU`,`sku_business_key = 品牌:SPU:SKU编码`;这些键用于产品归组和兼容关联。
- 非空 `skuCode / barcode` 由数据库唯一索引兜底;外部正式商品最小身份以条码规范化后的 `barcode_business_key` 为准。
- 软删除非集成产品时,后端和数据库触发器都会清理结构化 SKU 明细,避免已删除记录占用编码池。
非集成对外只能做白名单投影,例如:
@ -210,6 +215,12 @@ CREATE SCHEMA IF NOT EXISTS external;
| 视图 | 用途 |
| --- | --- |
| `external.product_skus_v1` | 正式产品 SKU 主数据 |
| `external.product_catalog_wide_v1` | 一条码一行的商品目录聚合宽表,覆盖正式条码与可消费的 non-integrated ready 条码,以及产品资料、开发阶段负责人、限价、库存库位摘要、包装库存摘要和最新质检摘要 |
| `external.intake_product_limits_v1` | 非集成产品限价白名单 |
| `external.warehouse_locations_v1` | 仓库库位资料 |
| `external.sku_warehouse_stock_v1` | 正式 SKU 库存和库位 |
| `external.packaged_items_v1` | 已关联正式 SKU 的包装库存项 |
| `external.sku_quality_inspections_v1` | SKU 仓库/首批质检记录 |
| `external.product_assets_v1` | SKU 图片、模型、资产来源、素材引用 |
| `external.product_bom_v1` | 正式 SKU 的板材、组件、五金摘要 |
| `external.intake_products_v1` | 非集成开品主信息白名单 |
@ -217,17 +228,22 @@ CREATE SCHEMA IF NOT EXISTS external;
| `external.materials_v1` | 花色/材质基础资料 |
| `external.asset_variants_v1` | 可被正式产品引用的资产规格款 |
契约层允许保留内部技术 ID 作为追踪字段,但外部业务关联必须优先使用 `product_business_key / sku_business_key` 等稳定业务键。独立 SQL 巡检和 `npm run db:verify-master-data:all` 都会检查核心外部视图的业务键缺失或重复风险。
契约层允许保留内部技术 ID 作为追踪字段,但外部主同步必须优先使用 `barcode_business_key`;`product_business_key` 用于 SPU/型号归组,`sku_business_key` 用于 SKU 编码兼容关联。仓库库位优先使用 `warehouse_location_code`,质检记录自身去重使用 `record_key`。独立 SQL 巡检和 `npm run db:verify-master-data:all` 都会检查核心外部视图的业务键缺失或重复风险。
只读账号授权原则:
```sql
GRANT USAGE ON SCHEMA external TO external_reader;
GRANT SELECT ON ALL TABLES IN SCHEMA external TO external_reader;
ALTER DEFAULT PRIVILEGES IN SCHEMA external
GRANT SELECT ON TABLES TO external_reader;
GRANT USAGE ON SCHEMA external TO external_core_reader;
GRANT SELECT ON external.product_catalog_wide_v1 TO external_core_reader;
GRANT SELECT ON external.intake_product_limits_v1 TO external_core_reader;
GRANT SELECT ON external.warehouse_locations_v1 TO external_core_reader;
GRANT SELECT ON external.sku_warehouse_stock_v1 TO external_core_reader;
GRANT SELECT ON external.packaged_items_v1 TO external_core_reader;
GRANT SELECT ON external.sku_quality_inspections_v1 TO external_core_reader;
```
不要给外部账号使用 `SELECT ON ALL TABLES IN SCHEMA external` 这类全量授权;新增外部视图必须单独评审后追加白名单。
禁止:
- 给外部账号授权 `public` schema 全表读取。

View File

@ -160,6 +160,7 @@
- 代码巡检已增加“未归类本地存储”硬护栏:非测试源码里的 `localStorage/sessionStorage` 直接读写和 `createJSONStorage` 封装持久化用途必须被归类为主数据风险、兼容缓存、旧缓存,或明确列入非主数据 UI/登录态缓存;未归类时命令失败,避免新的本地主数据真相源漏进系统。
- 代码巡检已增加“应用层本地 JSON 文件主数据源”硬护栏:`src` 下不得用同步或异步 `readFile/writeFile`、`require/import` 读写或导入本地 `.json` 文件作为产品、资料库、权限或外部同步真相;导入载荷、迁移计划、备份和测试夹具必须留在明确的工具/测试边界内。
- 代码巡检已增加非集成产品 `payload` 查询护栏:开发类型、负责人、待办和列表筛选不得直接把 `payload` JSON 当作并列真相源;历史兜底必须通过 `payloadJsonWhenProfileMissing(...)` 明确绑定到 profile 缺失场景。
- 非集成产品定厂字段已结构化到 `IntakeNonIntegratedSkuItem`:供应商、供应商编码、工厂仓库、发货方式、最低起订量、含税单价、交期和安装方式均以 SKU 明细列为真相;`payload.factoryRows` 只保留迁移兼容快照和流程型字段,运行时回显与 `external.product_catalog_wide_v1` 不得再从该 JSON 兜底取定厂字段。`factoryWarehouse` 表示供应商发货仓,不得和库存仓位 `warehouse_location_name` 混用。
- 代码巡检已增加关键地基表写入边界护栏:`src/app` 下的 Route/Page 不得直接写 `SystemConfig / ProviderConfig / ThemeConfig / IntakeNodePermissionOverride / AssistedAssemblyAiRender / Feedback / ActivityLog`,必须进入 service/repository 的规范化写入口;回收站产品/SKU 日志写入已改走统一 `logActivity`。
- 代码巡检已继续扩展到核心主数据写入边界:`src/app` 下的 Route/Page 不得直接写 `Product / ProductSKU / ProductSkuAssetProfile / DraftGroup / PreSku / PreSkuBasicInfo / AssetFamily / AssetVariant / IntakeNonIntegratedProduct / IntakeNonIntegratedProductProfile / IntakeNonIntegratedSkuItem / ProductPatentRecord / SpuNumberCounter / IntakeReferenceLibraryItem`,防止页面接口绕开服务层的编码规范化、快照同步、质量状态和冲突检测;`src/server` 写入口会同时识别 Prisma client 别名和 relation 嵌套写入,子模型结构化副本不能藏在父模型 `create/update` 里绕过分类;资料库条目身份也禁止恢复 `id` 兜底 `itemKey`、直接 `itemKey: item.id`、`code || item.id` 或 `` `item:${item.id}` `` 的写法,有稳定编码的资料库必须用 `code` 作为条目身份,缺显式编码的资料库必须使用业务规格派生的稳定 key,避免浏览器临时 id 或数据库主键变成业务身份;草稿组编号禁止恢复纯 `Date.now()` 生成,服务层必须使用可读前缀和足够熵,数据库唯一索引只做最后兜底。
- 文件系统输入也纳入代码巡检:`filesystem-master-data-source` 会识别读取 `public/uploads`、上传临时文件、ZIP 解包目录等文件来源后又写 `Product / ProductSKU / Panel / ProductHardware / Hardware` 的组合。只有登记过的旧上传/导入入口可保留,新增未登记入口会阻断,防止把素材目录或本地文件扫描当成第二主数据源。

View File

@ -0,0 +1,289 @@
# 物流到货签收时长看板 · 订单明细每日快照
## 为什么要做
看板的全部数据来自远端只读报表视图 `rpt.jst_order_express_details`(外加 `jst_qmcust_orderlist_item` 条码明细和 `SupplyChainProductArchive` 产品档案富化)。
该视图是远端对象,一次扫描通常以分钟计,页面每次刷新、翻页、下钻、导出都要重新扫一遍,看板因此很难"高效运转"。
做法:每天把看板口径的订单明细物化到本地 Postgres,看板优先读本地快照,只有快照覆盖不到时才回落实时数据源。
## 数据与表
### 条码/供应商/采购的匹配来源(两套互补的订单表)
报表库把订单明细拆成了两套**互不重叠**的表,只看一套会漏掉近一半订单的条码:
| 表 | 覆盖平台 | 规模(实测) |
| --- | --- | --- |
| `rpt.jst_qmcust_orderlist` / `rpt.jst_qmcust_orderlist_item` | 淘宝天猫、阿里巴巴(1688)、淘工厂、天猫供销 | 408 万 / 576 万行 |
| `rpt.jst_Orders` / `rpt.jst_OrderItem` | 京东商城、京东厂家直送、拼多多、头条放心购(抖音)、得物、唯品会、线下、微信视频号、小红书 | 124 万 / 175 万行 |
看板取条码时会**同时查两张明细表并用 `INNER_ORDER_ID`(即 `o_id`)合并**(`listItemRows`,两表按订单号各发一批查询后合并结果)。修复前只查 `jst_qmcust_orderlist_item`,导致京东/拼多多/抖音/得物/唯品会的订单条码全空;修复后新生成的快照条码匹配率为 **100%**(此前整体 52.3%)。
> 注:`rpt.jst_salesOutboundInfo`(销售出库单头)可以读取,但只有 `o_id / so_id / l_id / shop_name / io_id / qty` 等出库单头字段,没有 SKU 列,不能单独提供条码。
纯新增两张表(迁移 `prisma/migrations/20260928120000_add_logistics_arrival_signing_duration_snapshot`):
| 表 | 作用 |
| --- | --- |
| `LogisticsArrivalSigningDurationSnapshotRun` | 批次元数据:`snapshotDate`、`windowFrom`、`windowTo`、`status`(RUNNING/COMPLETED/FAILED)、`rowCount`、`message` |
| `LogisticsArrivalSigningDurationSnapshot` | 明细行:与看板 09 订单明细一一对应,含条码/供应商/采购/`productItems` 等快照时点已固化的富化结果 |
要点:
- 只有 `status = COMPLETED` 的批次对读路径可见,写入中途或失败的批次不会被读到。
- 行唯一键 `(snapshotDate, rowKey)`,`rowKey` 与下钻键一致(内部单号 + 线上订单号 + 物流单号 + 签收时间),重复执行用 `skipDuplicates` 幂等。
- `(snapshotDate, signTime)` 有索引,按签收日期区间取数走索引。
## 生成任务
```bash
# 默认:快照日期=当天(Asia/Shanghai),窗口=最近 62 天(覆盖本月与上月),保留最近 2 批
npm run sync:logistics-arrival-signing-duration-snapshot -- \
--apply --confirm=sync-logistics-arrival-signing-duration-snapshot \
--expected-database-identity="<本地库身份>" \
--expected-source-database-identity="<远端报表身份>"
```
可调参数:`--snapshot-date=YYYY-MM-DD`、`--window-days`、`--chunk-days`(默认 7,按天分批抓取以控制内存)、`--batch-size`(默认 2000)、`--retention`、`--force`(当天批次已完成时强制重建)。
编排逻辑在 `src/server/services/logistics-arrival-signing-duration-snapshot.service.ts`:
1. 当天批次已 `COMPLETED` 且未 `--force` → 直接跳过;
2. 标记 `RUNNING` 并清空当天旧行;
3. 按 `chunk-days` 分片读远端并写入本地,同时记下真正抓到的最新签收时间 `dataThrough`;
4. 全部成功才标记 `COMPLETED`(连同 `dataThrough`),随后按 `retention` 清理更早批次;任一分片失败则标记 `FAILED` 并保留错误信息,读路径会自动继续用上一批快照;
5. 收尾关闭远端连接池(一次性任务必须关闭,否则进程挂在事件循环上)。
生产定时(systemd timer,默认北京时间 03:00):
```bash
sudo bash scripts/install-logistics-arrival-signing-duration-snapshot-timer.sh
```
首次安装会生成 `$BASE/shared/logistics-arrival-signing-duration-snapshot.env` 模板,需要填两个身份校验值(缺省为空会拒绝写入)。可用环境变量覆盖:`LOGISTICS_SNAPSHOT_CALENDAR`、`LOGISTICS_SNAPSHOT_WINDOW_DAYS`、`LOGISTICS_SNAPSHOT_RETENTION`。
## 读路径规则
`src/server/repositories/logistics-arrival-signing-duration.repository.ts` 的每个读方法都先判断快照是否可用:
- 取最近一批 `COMPLETED` 快照;请求区间**完全落在快照的实际覆盖范围**内才读快照(否则回落实时视图,避免返回不完整结果)。覆盖上限 = `min(windowTo - 1, dataThrough 当天)`:
- `windowTo` 是跑批当天,当天还没签收完,所以只能信任到它前一天;
- 再用批次记录的 `dataThrough`(该批真实最大签收时间)兜底,避免数据源滞后时把不完整的一天当成完整。
- 例:一批 10-08 生成、数据只到 10-08 05:30 的快照,对 `10-01~10-08` 不再命中,看板/导出自动改用实时源,不会少算 10-08。
- 命中快照时返回值带 `source: 'snapshot'` 与 `snapshotDate`,页面头部显示"快照数据 · 日期";未命中为 `source: 'live'`,显示原来的"数据更新 · 时间";
- 快照表还没迁移或还没有任何批次时,`findLatestCompletedRun()` 返回 null(并吞掉异常),整条链路自动等价于改造前行为。
因此在以下情况仍会走实时数据源(较慢,但结果正确):请求区间超出窗口、任务当天尚未跑完或失败、本地还没生成过快照。
## 指标口径(整区间精确 vs 样本)
命中快照时,**整块看板都是整区间精确值**,与 2,000 条样本彻底解耦:
| 看板块 | 精确口径实现 |
| --- | --- |
| 经营总览(6 个 KPI) | `aggregateDashboard()` 一次扫描聚合 |
| 履约环节拆解(5 环节平均/中位/P90) | 同上,`percentile_cont` 直接给出分位数 |
| 发货→签收时效分布 | 同上,4 个区间计数 |
| 付款→签收 / 付款→发货 多维经营分析(01–08) | `aggregatePayToSignAnalysis()` / `aggregatePayToSendAnalysis()`,按签收日期 + 采购/供应商/条码(`jsonb_array_elements` 展开 `productItems`)+ 物流公司/发货仓/平台/店铺分组 |
| 发货仓 / 物流公司 超时排名 | `aggregateThresholdRankings()` |
| 09 订单明细下钻 | `listDrilldownRows()`:按维度条件下钻,**列出全量匹配订单**并给出真实总数与页数 |
口径一致性由三点保证:筛选条件统一由 `snapshotFilterConditions()` 派生(Prisma 与原始 SQL 共用);比率/取整统一走 lib 的 `buildLogisticsArrivalSigningDurationOverview`、`buildPayToSignMetrics`、`buildPayToSendMetrics`;排序统一走 `comparePayToSignGroups` / `comparePayToSendGroups` / `compareLogisticsArrivalSigningDurationThresholdRows`。每个聚合块都用"把整区间明细喂给样本路径同样函数"的方式做过逐字段比对(MATCH)。
### 看板顺序与联动链路
### 履约环节拆解:目标值与看板宽度
### 经营总览:平均到货时长卡片与异常下钻
- 签收订单后面新增 **付款→签收平均到货时长** 卡片:主值是该环节平均时长,附行给出 **本月目标**(上月平均 × 98%)与 **目标达成率**,与履约环节拆解同一套口径。
- **缺少关键时间 / 时间异常** 两张卡片可点击:点击后用 `{ kind: 'anomaly', value: 'missing' | 'invalid' }` 条件下钻,09 订单明细列出对应订单;再点一次取消。判定口径与总览一致——缺少关键时间 = 四个时间任一为空;时间异常 = 四个时间齐全但至少一个环节时长算不出来(时间顺序异常)。
- **自动滚动只保留给经营总览的异常卡片**(卡片上写着"点击看明细"):点它们会把 09 订单明细滚到视口顶部(`detailBoardRef` + `scrollIntoView`,按"下钻签名"去重,不会在翻页时反复滚动)。**经营对象维度(平台/店铺/发货仓/物流/采购/供应商/商品/签收日期)的点选不再自动滚动**——那些看板是连着看的,一滚到底就没法接着看其他维度、还得手动往上拉;下钻筛选本身照常生效,09 明细就在原位置更新。
- 为保证"卡片数字 = 点击后的明细条数",**命中快照时经营总览始终按整区间精确统计**(`aggregateDashboard(..., includeStages=false)`,只算总览不算分位数);超出精确上限的大区间里,只有履约环节拆解/时效分布/经营分析/超时排名退回样本,页面脚注会写明。
### 经营总览视觉层次与看板工具条
- 经营总览从"7 张卡一张网格"改为**两组网格**:5 张履约指标卡一排(`--sc-kpi-cols`,标准 5 / 紧凑 3 / 展开 2),2 张数据质量卡单独成排(`--sc-kpi-alert-cols`,标准 2 / 展开 1)。7 张卡在常见宽度下不再是 5+2、第二排右侧大片留白。
- 卡片排版统一:标签 12px 用 `--sc-muted`,数值 24px 且 `font-variant-numeric: tabular-nums`,用 `margin-top: auto` 贴底、副标题保留 2 行最小高度(`min-height: 2.9em`)。同一行内标签换行不影响数值对齐,卡片等高、数字列对齐。
- 绿色强调卡(付款→签收平均到货时长、付款→签收履约率)改为"浅绿底 + 左侧 2px 竖条"(`inset 2px 0 0`),不再整卡大面积着色;点击态的竖条与边框跟着状态色走。
- 缺关键时间 / 时间异常改为横向"告警条":左侧图标 + 名称/说明,数值贴右,用低饱和暖色表达"异常"语义;点击下钻、`is-active` 选中态、`aria-pressed` 与 `aria-label`(含条数)都保留。
- 修复看板工具条(导出 / 尺寸切换)样式不生效的根因:`BoardTools` 是本文件内的**独立组件**,styled-jsx 按组件注入作用域类,主组件里的 `.sc-logistics-icon-button` / `.sc-logistics-board-tools` / `.sc-logistics-board-size-label` 规则永远匹配不到它的 DOM,四个看板右上角一直是"裸图标"。现在样式放进 `BoardTools` 自己的 `<style jsx>`(深色排名表头用 `:global(.sc-logistics-ranking-board-header)` 适配),并删除主样式块里的死规则。
- 每个环节多出一列 **目标**:`本月目标 = 上月同环节平均时长 × 98%`(`LOGISTICS_ARRIVAL_SIGNING_DURATION_TARGET_IMPROVEMENT_RATIO`)。基准月取所选区间结束日期所在月的上一个月(跨年正确),基准值由 `aggregateStageAverages()` 用同一套筛选条件跑上个月区间得到;当前平均不高于目标时目标值标绿表示达标,鼠标悬停可看到"目标 = 上月平均 × 98%"的算式。
- 目标后面还有 **目标达成率**:该环节时长不超过目标的订单占比。精确口径由 `aggregateStageTargetHits()` 按目标值统计命中订单数,样本口径由 `buildLogisticsArrivalSigningDurationStageAchievement()` 在样本上计算;无目标或无可计算订单时显示 `-`,鼠标悬停显示"命中 / 可计算"明细。环节行的平均/中位/P90/目标/达成率都只显示数值,单位(小时)只在表头语义里体现。
- 环节行按 `128px / 弹性条 / 74 / 74 / 86 / 92` 收紧列宽,列表支持横向滚动,避免 P90、目标被截断。
- 履约环节拆解与发货→签收时效分布之间加了**可拖动的分隔条**(`role="separator"`,支持左右方向键微调),比例默认 62% 并记在本地,可随时左右拖动调整两个看板的宽度。
经营对象问题排名各看板按"上级 → 下级"顺序排列,并逐级联动:
```
01 签收日期维度 → 02 平台维度 → 03 店铺维度 → 04 发货仓维度 → 05 物流维度
→ 06 采购维度 → 07 供应商维度 → 08 商品维度 → 09 订单明细
```
点击任意一级对象,都会保留更上级条件、丢弃更下级条件,并把这条链路作为筛选条件重新取数:
- 整区间口径:链路以 `cascade`(JSON 数组,`LogisticsArrivalSigningDurationDrilldownFilter[]`)随 dashboard 请求下发,服务端在 `aggregatePayToSignAnalysis` / `aggregatePayToSendAnalysis` / `aggregateThresholdRankings` 的 WHERE 上追加 `cascadeSqlWhere`,因此下级看板与 09 明细都只展示对应数据;
- 样本口径(实时源或超出精确上限):前端用 `logisticsArrivalSigningDurationRowMatchesCascade` 收窄样本后重算;
- 店铺维度分组键是"平台 + 店铺",因此平台级联动能精确到该平台下的店铺;
- 联动刷新是原位刷新(只有页面筛选变化才显示整块加载态),排名看板标题会显示当前链路与"联动加载中…"提示;
- 经营总览、履约环节拆解、时效分布属于页级口径,不随联动链路变化。
### 经营对象的目标与目标达成率
02–08 经营对象表(平台 / 店铺 / 发货仓 / 物流 / 采购 / 供应商 / 商品)在原有指标后面追加 **目标** 与 **目标达成率** 两列;01 签收日期维度的「指标 × 日期」矩阵与总计不追加(按日算"上月同日目标"没有业务含义)。
- **目标 = 该对象上月同环节平均时长 × 98%**,达成率 = `100% − (平均 − 目标) / 目标 × 100%`,与履约环节拆解同一套口径,只是精确到每个经营对象——所以每个对象的目标值不同(例如 08 月各采购平均不同,9 月目标 52.7 / 56.5 / 60.5 …),达成的判断是"相对自己上个月是否改善 2%"。
- 数值口径与展示一致:平均、目标都按 1 位小数取值后再算达成率,便于和表里看到的数字对账;没有上月基线的对象(新采购、新店铺、当月新条码)显示 `-`。
- 取数:`aggregateObjectBaselines()`(快照仓库)在上月区间一次扫描算出 7 个维度 × 付款→签收 / 付款→发货 两个环节的平均时长,随 dashboard 载荷以 `objectBaselines` 下发;前端用 `withObjectAnalysisTargets()` 按分组键(`JSON.stringify(keyParts)`,与样本路径同一套键)合并,样本口径与整区间口径共用。
- 这条聚合约 2–4 秒,因此按「快照日期 + 基准月 + 关键词/排除词」在服务端缓存 10 分钟(这些平均值只在每日快照重建时变化),并与看板其余查询并行执行;查询失败只让这两列显示 `-`,不会把整块看板拖回实时源。
- 「经营对象问题排名」导出的 CSV 同步带上这两列。
### 看板分页
> 页面顶部的「经营管理 / 物流时效 + 看板标题 + 物流签收明细口径 + 明细条数/更新时间」页头已去掉(用户口径:无需展示,标签页标题已经说明是哪个看板);快照/缓存状态在数据源异常时的黄色提示条里仍会给出。
### 批量关键词查询
筛选区的「关键词」支持批量输入(与排除关键词同一套拆分规则):
- 按中文逗号 `,`、英文逗号 `,`、换行拆分,自动去空、去重,最多 100 个、单个最长 120 字符(原始输入上限 4000 字符);
- **多个关键词之间是"或"**:命中任一字段即算匹配。整区间(快照)口径覆盖:内部单号、线上订单号、物流单号、物流公司、店铺、平台、发货仓、收货地址,以及快照时点固化的**条码、商品名称、供应商、采购**(`KEYWORD_FIELDS`)——所以能直接按采购/供应商/条码检索;
- 走实时源时(区间超出快照窗口)只覆盖前 8 个报表字段,条码/采购/供应商这三类需要本地快照支持,这是两套口径的已知差异;
- 解析规则由 `splitSearchKeywords()` 统一提供,服务端缓存键按关键词集合(排序后)计算,输入顺序不同不会重复缓存。
- 输入框与其它筛选控件**等高**(34px 单行高度,多行时框内滚动),"已识别 N 个关键词"显示在标签行右侧并在空间不足时省略,不再把这一格撑成两行高。
筛选区的三个按钮各管一件事,点下去**立即按新条件查询**(互不重复):
| 按钮 | 签收日期 | 关键词 | 排除关键词 | 结果 |
| --- | --- | --- | --- | --- |
| **重置** | 清空(输入框留空) | 清空 | 清空 | 没有任何筛选条件;空日期由服务端兜底为本月 1 号~昨日(`dateRangeCleared` 让输入框保持空白,不被兜底值回填) |
| **恢复默认** | 回到本月 1 号~昨日(显式填回输入框) | 保留 | 恢复系统默认配置(全部勾选) | 默认口径 |
| **清空排除** | 回到本月 1 号~昨日(显式填回输入框) | 保留 | 置空 | 默认区间 + 不排除任何关键词 |
三者过去都只是"改输入、等用户点查询"(重置只清日期与关键词、另两个只改排除词),结果看起来一样容易混;现在各自的语义、界面状态与结果都区分开了,按钮 hover 有对应说明,筛选栏底部也有一行图例。空日期提交时服务端仍按默认区间查询(跨天且当日快照未生成时可能落实时源,较慢但结果正确)。
**默认签收日期区间是「本月 1 号 ~ 昨日」**(由 `normalizeLogisticsArrivalSigningDurationFilters()` 生成):当日签收数据仍在滚动,且每日快照只覆盖到昨日,用"到昨日"作默认值可以让首屏直接命中快照(整区间精确 + 秒级),不再因为区间含当天而回落到实时源。每月 1 号时会自动退化为"昨日当天"(本月 1 号还没到昨日)。
筛选控件(签收日期、关键词输入框,以及排除关键词选择器的摘要)都是**受控并与服务端下发的筛选条件同步**:从菜单重新进入(无参数 URL)、重置后查询、浏览器前进/后退时,控件会刷成当前真正生效的条件。这一点很重要——否则会出现"数据已经是默认区间,输入框还停在上次条件"的错觉。
### 排除关键词选择器
筛选区的「排除关键词」是**按字段分组**的勾选式选择器(`ExcludeKeywordPicker.tsx`)。关键词只能配置在它要匹配的那个字段上,**不跨字段混用**——这是这一版最重要的口径修正。
**字段与匹配规则**
| 字段(配置分组) | 数据列 | 匹配方式 |
| --- | --- | --- |
| 发货仓 | `warehouseName` | 该字段值包含关键词(不区分大小写),命中即排除 |
| 物流公司 | `logisticCompanyName` | 同上 |
| 订单标签 | `orderLabels`(JSON 数组文本) | 同上 |
| 买家留言 | `buyerMessage` | 同上 |
| 卖家备注 | `remark` | 同上 |
配置与筛选都以「字段 → 关键词」存储(`LogisticsArrivalSigningDurationExcludeKeywords`)。SQL 侧对每个关键词只生成一条 `NOT (该字段 ILIKE '%词%')`;旧版"命中 5 个字段里任意一个就排除"的口径已废弃——那正是「物流」会连带命中订单标签里的 `商家指定物流`、`物流探查` 和买家留言里"物流单号"的根因。
**选择器交互**
- **按字段分组**:发货仓 / 物流公司 / 订单标签 / 买家留言 / 卖家备注,每组标题显示"共 N 项 · 已勾选 M 项",右侧索引条按字段跳转(字段没有候选词时不显示该组);
- **默认全选**:候选词 = 该字段的默认排除词 ∪ 当前已排除词(旧链接里带的自定义词不会丢),进入看板时全部处于"排除"状态;取消勾选 = 本次不排除该项,确定后立即以当前表单条件重新查询(`requestSubmit()` 在受控值落到 DOM 后再触发);一个都不勾时以 `excludeKeywordsMode=clear` 提交,避免回落到默认排除词;
- 选择器内含:搜索、`全部 / 已选项(n)` 页签、`全选 / 反选 / 清空`(作用于当前可见列表,全选带半选态)、`紧凑模式`(四列更密);
- **新增候选词仅系统管理员可操作**:底部先选字段、再输入关键词(回车或点「添加」),新词并入**该字段**的默认排除词并立即落库(`PUT …/settings`,服务端 `assertCanManage` 同样校验管理员身份),提示"已保存到「物流公司」默认排除关键词:…";非管理员只看到"需要新增请联系系统管理员";
- 页面原先独立的「默认排除关键词」设置卡片已删除,维护入口统一收敛到选择器;`清空排除`(不排除任何词)与 `恢复默认`(回到全部默认排除词)两个按钮保留,只改输入、点查询生效。
**编码与兼容**
- 筛选栏摘要按字段给数量(如"已排除 14 项:发货仓 3 · 物流公司 5 · 订单标签 2 · 买家留言 2 · 卖家备注 2"),URL 用 `excludeKeywords=字段:词,词;字段:词` 的紧凑写法;
- **老链接的平铺写法按当前配置归位**:`excludeKeywords=词1,词2` 会被逐个对应到配置里它所属的字段(大小写不敏感,`bbff` 会归一到配置里的 `BBFF`),配置里已经没有的词说明它没有字段、直接忽略——**不会再把一个词摊到 5 个字段上**(那会让选择器每组都列出全部关键词)。只有当配置为空、任何词都对应不上时才退回旧口径,避免老链接突然变成"不排除任何词"。
- 历史配置(数组)在 `normalizeExcludeKeywords()` 里仍按旧口径挂到全部 5 个字段(那是当年真实的匹配语义);但配置一旦以新结构保存,后续解析都会走"按字段归位"。
**当前初设配置**(2026-09-30 落库):发货仓=`自动发货仓 / 常州简尚家居有限公司 / 京东云仓`;物流公司=`京喜 / 物流 / 安能 / 自提 / 中铁`;订单标签=`配件 / 1688一件代发`;买家留言=`BBFF / SSDD`;卖家备注=`BBFF / SSDD`。2026-09-01~09-02 实测:不排除 26,296 行 → 按字段排除后 17,506 行(旧口径同样 14 个词是 16,552 行,多出的 954 行正是"标签里带物流、物流公司里没有"的那些)。
经营对象问题排名里的每个看板都有独立翻页,不再用"显示更多"或无限滚动,也不会因为对象太多被截断:
- 01 签收日期维度:按日期列翻页(默认 10 列/页,可切 10/20/50/100);
- 02–08 采购/供应商/条码/物流公司/发货仓/平台/店铺:按对象行翻页(默认 20 行/页);
- 发货仓 / 物流公司 超时排名:按对象行翻页(默认 20 行/页)。
- 09 订单明细:翻页控件就在看板内部(不再挂在页面底部);未下钻时按 URL 的 `page/pageSize` 走服务端分页(每页 25/50/100/200,命中快照时显示真实总数"共 N 条筛选结果"),下钻时用本地分页翻全量匹配明细。
分页条统一由 `BoardPagination.tsx` 渲染(含"共 N 条 · 第 x / y 页"、上/下一页与每页条数),行集合变化(例如切换采购后重新筛选供应商)时自动回到第 1 页。
**每页条数切换的坑(已修)**:下拉框原先在 `onChange` 里同时调了 `onPageSizeChange(新值)` 和 `onPageChange(1)`。服务端分页(09 订单明细未下钻时)这两次都会触发 `router.push`,而后一次带着**旧的** `pageSize` 构造 URL 并覆盖前一次,表现为"订单明细每页条数选了没反应、又跳回 50"。现在 `BoardPagination` 只通知"每页条数变了",回到第 1 页由调用方负责:服务端分页用 `href(1, { ...filters, pageSize })`,本地分页(各经营对象表、发货仓/物流公司排名、下钻后的 09 明细)在 `setState` 里带 `page: 1`。
店铺维度表在"店铺"列前额外展示"平台"列:点击平台即把该表筛成该平台的店铺(标题变成"店铺维度 · 天猫",再次点击取消),点击店铺名仍走 09 明细下钻。店铺分组的业务键是"平台 + 店铺",避免同名店铺跨平台互相覆盖;平台列在样本路径(JS 归组)与整区间路径(SQL `GROUPING SETS ((platform, shop))`)都做了同样的处理,并逐字段比对过一致性。
### 规模上限与样本回落
SQL 聚合成本与区间行数近似线性(约 5 万行/秒)。当**区间原始行数超过 `LOGISTICS_ARRIVAL_SIGNING_DURATION_EXACT_SCOPE_MAX_ROWS`(11 万条,约 11 天)**时,`getDashboard` 会退回区间内均匀抽取的 2,000 条样本(`listDashboardSampleRows`,5 层并行、按签收时间分层,不再是"取最近一段"),并在底部注释写明原因与"缩小签收日期范围即可得到精确值"。
返回值上的 `dataScope`(`exact` / `sample`)与 `dataScopeReason`(`live-source` / `range-too-large`)驱动卡片副标题、底部注释与 CSV 导出的口径说明。
实测(663k 行快照,含 13 个默认排除关键词):
| 区间 | 原始行数 | 口径 | 首屏耗时 |
| --- | --- | --- | --- |
| 1 天 | 约 1.3 万 | 整区间精确 | 0.55s |
| 2 天 | 约 2.6 万 | 整区间精确 | 1.7s |
| 5 天 | 约 6.5 万 | 整区间精确 | 3.1s |
| 本月 | 约 30 万 | 样本(超上限) | 3.8–5.0s |
服务端 60s 内重复访问直接命中缓存。要把大区间也做成亚秒精确,方向是在快照写入时就把 4 个时间戳、5 个环节时长落成列,并把多维指标的计数改为位掩码 `GROUP BY`(一次扫描产出全部桶计数),现有文档结论不变、只替换实现。
### 查询性能优化(2026-10)
上线后"每次查询生成太慢"的根因有三个,都在快照读路径上:
1. **整表顺序扫描**:聚合查询原先 `SELECT` 全列(`rowKey`、`productItems`、十几个文本列),
Postgres 只能对 900MB 的堆表做 `Seq Scan`(28 天区间实测 `read=106409` 页、6.3 秒)。
现在按用途收窄列集合——`FILTERED_TIMELINE_SELECT`(只取 4 个时间戳)用于整区间总览/环节目标,
`FILTERED_DIMENSION_SELECT`(时间戳 + 4 个维度列)用于签收日期/平台/店铺/发货仓/物流/超时排名——
并新增覆盖索引 `LogisticsArrivalSigningDurationSnapshot_cover_idx`
(`(snapshotDate, signTime) INCLUDE (payTime, sendTime, fetchTime, warehouseName, platformName, shopName, logisticCompanyName)`),
扫描变成 **Index Only Scan**(同样的 28 天区间 26ms、`Heap Fetches: 0`)。
2. **有序集聚合把整行拖进排序**:`percentile_cont(0.5/0.9) WITHIN GROUP` 原先和十几个计数写在同一条语句里,
输入行 14 列宽,PG 18 会把它整行送进排序(两个分位数单独就占 4.3 秒,环节分位数叠在一起甚至到分钟级)。
现在分位数**按列拆成独立语句**、只投影一个浮点列,并与计数语句并行下发(`Promise.all`)。
3. **CTE 表达式被重复内联**:`measured`/`durations` 的 `CASE + EXTRACT` 会在每一处引用被重新计算,
同一条聚合里重复十几次。改为 `AS MATERIALIZED` 后只算一次(计数语句从 2.3s 降到 ~0.5s)。
另外把 **环节目标(上月平均 × 98%)** 也接入 10 分钟内存缓存(此前每次刷新都重跑上月聚合)。
优化前后(本机 Postgres 18、快照 648,689 行、冷启动、无缓存):
| 场景 | 优化前 | 优化后 |
| --- | --- | --- |
| 仓库层 `getDashboard`(28 天,样本口径) | 7.3s | **2.3s** |
| 仓库层 `getDashboard`(10 天) | 4.4s | **0.4s** |
| 仓库层 `getDashboard`(2 天,精确口径) | 1.7s | **0.3s** |
| 浏览器首屏出数(冷启动,含 5.6MB 载荷 + 渲染) | ~7–10s | **2.7s** |
正确性用 2 天区间做过逐项对账:SQL 聚合与"取全量明细在 JS 里现算"在订单数、平均/中位/P90、
异常计数、达成率、时效分布、5 个环节的 可计算单量/平均/中位/P90 上完全一致。
时间戳在 SQL 里用 `NULLIF(BTRIM(col),'')::timestamp` 直接转型:快照写入的就是远端 `CONVERT(varchar(19), …, 120)` 结果,格式固定;早期版本用正则逐行校验,整区间聚合约慢一个数量级。
## 运维排查
```sql
-- 批次状态
SELECT "snapshotDate", status, "windowFrom", "windowTo", "rowCount", message, "finishedAt"
FROM "LogisticsArrivalSigningDurationSnapshotRun" ORDER BY "snapshotDate" DESC;
-- 当天行数
SELECT count(*) FROM "LogisticsArrivalSigningDurationSnapshot" WHERE "snapshotDate" = CURRENT_DATE;
```
- 页面长时间显示"数据更新"而不是"快照数据" → 先看批次是不是 `FAILED`,或窗口是否覆盖了页面的签收日期区间。
- 批次 `RUNNING` 卡住(进程被杀)→ 该批次不会发布;直接重跑或加 `--force` 即可(重跑会先清空当天行)。
- 需要重建历史窗口 → `--window-days=` 调大 + `--force`。
## 部署步骤(生产)
1. 部署新版本代码;
2. 执行迁移 `prisma/migrations/20260928120000_add_logistics_arrival_signing_duration_snapshot`(需按生产迁移流程授权、先备份);
3. 跑一次手工同步生成第一批快照(`--apply`),确认批次为 `COMPLETED`、行数与窗口符合预期;
4. 安装 timer 并核对 `systemctl list-timers`;
5. 打开看板确认头部显示"快照数据 · 日期"。
注意:Prisma Client 重新生成过,运行中的 dev/prod 进程需要重启才能拿到新模型。

View File

@ -1,6 +1,6 @@
# White Obsidian Atelier(白曜石工坊)统一视觉标准
适用范围:资产中心、辅助装配、草稿组、正式入库、轻仓库等新平台主链页面。
适用范围:资产中心、辅助装配、草稿组、产品资料/正式入库、仓库管理/轻仓库、仓库质检、AI 出图工作台,以及 `/admin`、`/users` 等新平台后台管理页面。
目标:统一“白曜石工坊 / White Obsidian Atelier”视觉方向。默认基调是浅色工作台,不再做深浅色主题切换;同时避免把资产中心做成普通 SaaS 卡片列表,也避免把毛玻璃效果滥用成低可读性的装饰。
@ -12,7 +12,7 @@ White Obsidian Atelier 的关键词是:浅色精密、克制通透、资产工
- `White Obsidian(白曜石)`:整体基调偏浅、安静、低干扰,用冷白、浅灰、石墨灰、金属中性色建立专业感。
- `Atelier(工坊)`:页面不是营销展示页,而是给人长期处理资产、装配、审核、入库的工作空间。
- `Unified(统一)`:资产中心、辅助装配、草稿组、正式入库、轻仓库要像同一套产品,不允许每个页面各自发明一套视觉语言。
- `Unified(统一)`:资产中心、辅助装配、草稿组、产品资料/正式入库、仓库管理、仓库质检和 AI 工作台要像同一套产品,不允许每个页面各自发明一套视觉语言。
- `3D Workbench(3D 工作台)`:新平台的第一感知应是“可操作的资产工作台”,而不是“管理后台卡片墙”。
视觉上应保持:
@ -58,6 +58,14 @@ White Obsidian Atelier 的关键词是:浅色精密、克制通透、资产工
在新平台里,板材、五金、组件、包装等不应被当作普通卡片处理,而应被当作 `Meta Component(元组件)`。
落地时优先使用项目现有组件库和平台组件:
- `src/components/platform/`
- 资产中心现有工作台组件
- 已有元组件、Inspector、工作台、HUD 和平台导航结构
Stitch 设计稿是视觉规格和验收标准,但不是绕开组件库重写静态页面的理由。实现时应在项目组件体系内对齐 Stitch 的密度、层级、控件形态、留白、字号、圆角、阴影和低噪音表达。
元组件的含义是:它是可以被系统理解、检视、规格化、装配、审核、材质配置和后续入库承接的资产元件。
元组件至少应具备这些表达能力:
@ -156,7 +164,9 @@ className="bg-white divide-y divide-zinc-100"
- 毛玻璃是否只出现在顶栏、HUD、浮动工具条和轻量状态浮层。
- 板材、五金、组件、包装是否按元组件表达,而不是普通展示卡片。
- 3D 工作台、Inspector、数据列表之间的主次关系是否明确。
- 当前页面是否仍符合项目主链:资产中心 -> 辅助装配 -> 草稿组 -> 正式入库 -> 轻仓库。
- 当前页面是否仍符合项目主链:资产中心 -> 辅助装配 -> 草稿组 -> 产品资料/正式入库 -> 仓库管理/轻仓库。
- 是否优先复用平台组件和元组件,而不是另起一套页面皮肤。
- 如果有 Stitch 设计稿,是否按设计稿控制密度、控件状态和低噪音表达。
结论口径:

View File

@ -1,207 +0,0 @@
# 苹果风格视觉设计标准 (Apple-Style Visual Design Standards)
> **版本**: 2.0 (2026-01-29)
> **状态**: 补充参考标准;当前新平台第一视觉标准以 `白曜石工坊统一视觉标准.md` 为准
> **优先级**: 如本文件与白曜石工坊标准冲突,优先执行白曜石工坊标准
> **目标**: 打造优雅、统一、沉浸式的用户体验,完全去除"模板化"和"AI味",接近苹果官网的视觉水准。
---
## 目录
1. [核心设计哲学](#1-核心设计哲学)
2. [详细视觉规范](#2-详细视觉规范)
* [色彩系统](#21-色彩系统)
* [排版系统](#22-排版系统)
* [布局与间距](#23-布局与间距)
* [材质与光影](#24-材质与光影)
3. [交互与动画](#3-交互与动画)
4. [去AI味专项指南](#4-去ai味专项指南)
5. [技术架构与文件结构](#5-技术架构与文件结构)
6. [组件开发规范](#6-组件开发规范)
---
## 1. 核心设计哲学
### 1.1 内容至上 (Content is King)
* **原则**:界面不仅是容器,更是内容的舞台。UI 应该"退后",让 3D 模型、产品图片和关键数据"向前"。
* **执行**:
* 背景色主要使用白色或极淡的灰色;当前产品不再提供深浅色主题切换。
* **拒绝**无意义的装饰性边框、分割线和阴影。只有在区分层级必要时才使用。
* **拒绝**均等分割的网格布局("AI味"典型特征)。使用非对称布局(如:左侧 2/3 展示 3D 模型,右侧 1/3 展示控制面板)。
### 1.2 物理感与材质 (Materiality & Physics)
* **原则**:数字界面应模拟真实世界的物理属性。
* **执行**:
* **Liquid Glass (毛玻璃)**:仅在覆盖内容时使用(如导航栏、侧边栏、浮动面板)。标准:`backdrop-filter: blur(20px) saturate(180%)`。
* **Spring Animation (弹簧动画)**:交互元素(按钮按压、弹窗出现)必须使用弹簧物理模拟,而不是简单的线性过渡。
* **光影**:光源应统一来自正上方。阴影应柔和、弥散,模拟环境光遮蔽 (Ambient Occlusion)。
### 1.3 优雅的排版 (Elegant Typography)
* **原则**:排版是信息层级的核心。
* **执行**:
* **动态字间距 (Dynamic Tracking)**:大标题收紧 (-0.4px),正文标准,小字加宽 (+0.06px)。
* **层次分明**:使用字重 (Weight) 和颜色 (Color) 区分层级,而不是单纯靠字号。
---
## 2. 详细视觉规范
### 2.1 色彩系统
**设计令牌位置**: `src/lib/design-tokens/colors.ts`
不要使用纯灰 (`#808080`),总是混入微量的蓝色或暖色调。
| 用途 | Light Mode | Dark Mode | 说明 |
| :--- | :--- | :--- | :--- |
| **背景** | `#FFFFFF` | `#000000` | 纯净背景,最大化对比度 |
| **次级背景** | `#F5F5F7` | `#1C1C1E` | 用于分组区块,**比系统灰(#F2F2F7)更淡、更优雅** |
| **主文本** | `#1D1D1F` | `#F5F5F7` | 接近纯黑但不是纯黑 (Off-black) |
| **次文本** | `#86868b` | `#86868b` | 经典的苹果灰,用于说明文字 |
| **强调色** | `#008D74` | `#00B896` | 品牌绿,仅用于链接和主按钮 |
| **分割线** | `rgba(0,0,0,0.1)` | `rgba(255,255,255,0.1)` | 极细,极淡 |
### 2.2 排版系统
**设计令牌位置**: `src/lib/design-tokens/typography.ts`
我们使用**合法的开源/系统字体栈**,严格避免版权风险:
* **西文**: `-apple-system`, `BlinkMacSystemFont`, `Inter`, `Segoe UI`
* **中文**: `-apple-system`, `Noto Sans CJK SC`, `Source Han Sans SC` (思源黑体), `Microsoft YaHei`
* **代码**: `JetBrains Mono`, `Fira Code`
| 样式 | 字号 (px) | 行高 (px) | 字间距 (px) | 用途 |
| :--- | :--- | :--- | :--- | :--- |
| **Large Title** | 34 | 41 | -0.4 | 页面主标题 |
| **Title 1** | 28 | 34 | -0.36 | 章节标题 |
| **Title 2** | 22 | 28 | -0.26 | 子章节标题 |
| **Headline** | 17 | 22 | -0.41 | 强调文本 (Semibold) |
| **Body** | 17 | 25 | -0.41 | 正文内容 |
| **Caption 1** | 12 | 16 | 0 | 说明文字 |
### 2.3 布局与间距
**设计令牌位置**: `src/lib/design-tokens/spacing.ts`
* **基础网格**: 4px
* **大留白原则**:
* 模块间距至少 `48px` 或 `64px`。
* 卡片内部 Padding 至少 `24px`。
* **流体布局**:
* 容器宽度随视口变化,最大宽度限制 (Max-width: 1440px)。
* **全宽突破**: Hero Banner 等关键元素应占据全宽。
### 2.4 材质与光影
* **Liquid Glass**:
* Light: `rgba(255, 255, 255, 0.72)` + Blur 20px
* Dark: `rgba(22, 22, 23, 0.72)` + Blur 20px
* **阴影 (Shadows)**:
* Subtle: `0 2px 8px rgba(0, 0, 0, 0.04)` (静态元素)
* Standard: `0 4px 12px rgba(0, 0, 0, 0.08)` (悬浮/交互)
* Elevated: `0 8px 24px rgba(0, 0, 0, 0.12)` (模态框)
---
## 3. 交互与动画
**设计令牌位置**: `src/lib/design-tokens/animations.ts`
所有交互必须有物理反馈。
* **按钮按压**: Scale 0.98, Duration 100ms
* **微交互 (Hover)**: Scale 1.02, Duration 200ms
* **页面过渡**: Duration 500ms, Curve `cubic-bezier(0.16, 1, 0.3, 1)`
* **Spring 参数**: 使用 `spring(mass: 1, stiffness: 100, damping: 15)` 模拟真实回弹。
---
## 4. 去AI味专项指南
"AI味" = 死板、重复、缺乏人情味、过度依赖默认组件。
### 4.1 拒绝"Lorem Ipsum"
* ❌ "Product Name 1", "This is a description."
* ✅ "Eames Lounge Chair", "由模压胶合板和皮革制成,经典的设计符号。"
* **要求**:所有占位文本必须是**真实场景数据**。
### 4.2 拒绝"均等分布"
* ❌ 4个功能点,每个占 25% 宽度,图标+标题+描述,完全一样。
* ✅ 1个核心功能占 60% 宽度配大图,其余 3 个功能以列表形式在侧边展示。打破对称。
### 4.3 拒绝"默认图标"
* ❌ 使用实心块状图标。
* ✅ 优先使用文字。如果必须用图标,使用细线条风格 (Stroke width 1.5px-2px)。
### 4.4 拒绝"彩虹渐变"
* ❌ 高饱和度彩虹渐变背景。
* ✅ 纯色、微渐变或高质量实拍图/3D渲染图。
---
## 5. 技术架构与文件结构
本项目基于 **Next.js 16 + Tailwind CSS + React 19** 构建。
### 5.1 目录结构
```
src/
├── lib/
│ └── design-tokens/ # 核心视觉配置
│ ├── typography.ts # 字体定义
│ ├── colors.ts # 颜色定义
│ ├── spacing.ts # 间距定义
│ └── animations.ts # 动画定义
├── components/
│ └── ui/ # 基础原子组件
│ ├── Button.tsx # 遵循规范的按钮
│ ├── Card.tsx # 遵循规范的卡片
│ ├── Typography.tsx # 文本组件封装
│ └── ...
└── app/
└── globals.css # CSS 变量映射 (自动从 tokens 生成)
```
### 5.2 迁移指南
如果你在代码中看到旧的 CSS 类(如 `text-gray-500`),请替换为语义化的设计令牌类:
* ❌ `text-gray-500` -> ✅ `text-(--color-label-secondary)` (或封装好的 Tailwind 类)
* ❌ `bg-white` -> ✅ `bg-(--color-background-primary)`
* ❌ `shadow-lg` -> ✅ `shadow-apple-lg`
---
## 6. 组件开发规范
### 6.1 Button
* **形状**: 高度 36px/44px,Pill shape (999px) 或 Rounded (8px)。
* **质感**: Primary (纯色无边框), Secondary (浅灰背景)。禁止复杂渐变。
### 6.2 Card
* **圆角**: 统一 `20px` 或 `24px`。
* **边框**: Light Mode 下通常无边框或极淡 `1px` 边框。
* **内容**: 内部 Padding 至少 `24px`。
### 6.3 Input
* **样式**: 移除默认边框,使用浅灰背景 (`#F5F5F7`),聚焦时出现 Focus Ring。
* **高度**: 至少 `44px`。
### 6.4 Slider
* **组件来源**: 统一使用 `src/components/ui/Slider.tsx`,禁止直接使用原生 `input[type="range"]`。
* **轨道样式**: 高度 `6px`,圆角 `999px`,未填充色 `#D4DCE6`,填充色使用品牌色 `#008D74`(深色场景可切换 `#00B896`)。
* **滑块样式**: 尺寸 `16px`,白色描边 `2px`,投影 `0 1px 3px rgba(0, 0, 0, 0.25)`。
* **状态规范**: 禁用态降低不透明度并禁用交互光效;拖动态允许轻微放大(建议 `scale(1.08)`)。
* **可访问性**: 必须提供 `aria-label`,并保持键盘可调节。
### 6.5 响应式
* 使用 `useBreakpoint` 或 Tailwind 的响应式前缀 (`md:`, `lg:`)。
* 支持 iPhone 刘海屏安全区域 (`safe-area-inset`).
---
*遵循此标准,我们将构建出一个不仅好用,而且令人愉悦的家具展示系统。*

View File

@ -111,7 +111,7 @@ STORAGE_PROVIDER=qiniu
QINIU_ACCESS_KEY=<只放在服务器环境变量里>
QINIU_SECRET_KEY=<只放在服务器环境变量里>
QINIU_BUCKET=<七牛空间名称>
QINIU_PUBLIC_DOMAIN=https://<七牛绑定域名>
QINIU_PUBLIC_DOMAIN=https://<七牛 CDN 域名>
QINIU_FILE_PATH=prod
QINIU_USE_HTTPS=true
QINIU_PRIVATE_BUCKET=true
@ -126,6 +126,12 @@ QINIU_PRIVATE_URL_EXPIRES_SECONDS=3600
- HTTP 测试域名只适合临时验证。素材需要保密时,正式环境应使用已绑定证书的 HTTPS CDN 域名,避免浏览器混合内容拦截和传输层明文风险。
- 系统稳定读取接口会自动处理私有桶读取:HTTPS 正式域名走 307 签名跳转;HTTP 测试域名由后端代理读取,避免浏览器直接访问 HTTP 签名 URL。
- 私有空间模式下,数据库保存系统内稳定访问地址,由后端按请求生成七牛临时签名 URL,不能把短期签名 URL 当作长期资料字段保存。
- 当前稳定入口约定:
- 登录态素材走 `/api/storage/object?key=...`
- 分享态素材走 `/api/share/[token]/object?key=...`
- 审批态素材继续输出系统稳定入口,再复用登录态鉴权读取链路
- 新增写入链路默认也应回写系统稳定入口,而不是把七牛外链或短时签名 URL 直接落库。
- 后续如果更换 bucket、CDN 域名或对象存储 provider,只改环境变量和 storage adapter,不改业务页链接逻辑。
- 七牛侧如果保持“无自定义跨域规则”,空间默认允许跨域访问;如果后续新增自定义 CORS 规则,必须至少包含:
- `Allowed Origins`:正式域名 `https://cp.jiansun.vip`,本地调试 `http://localhost:3000`,必要时加生产内网调试地址。
- `Allowed Methods`:`GET`、`HEAD`、`OPTIONS`。
@ -135,6 +141,14 @@ QINIU_PRIVATE_URL_EXPIRES_SECONDS=3600
- 生产服务器填完对象存储环境变量后,先运行 `npm run storage:verify-config` 做只读配置校验。该命令不会上传文件,也不会打印 AK/SK 或签名 token。
- 原有函数名 `uploadToS3`、`uploadBufferToS3` 仍保留为兼容入口,但内部已按 `STORAGE_PROVIDER` 分发到统一对象存储。
当前正式环境口径(2026-06-06):
- 七牛空间:`sucaiku-js`
- 正式 CDN 域名:`https://qnycdn.jiansun.vip`
- 正式环境对象读取:业务页继续使用系统稳定入口,服务端鉴权后统一 `307` 跳转到七牛短时签名 CDN 地址
- 数据库存储口径不变:继续保存系统稳定地址或对象 key,不把短时签名 URL 入库
- 若后续回滚,不要只改 `QINIU_PUBLIC_DOMAIN`;应把运行代码版本和对象存储配置作为一组一起回退
### 4.3 启动基础服务(数据库 / Redis / MinIO)
在 `furniture-3d-system` 目录下执行:
@ -245,6 +259,12 @@ npm run dev
4. **系统健康页面**
- 访问 `/system-health`,检查是否能正常渲染,且不会抛出前端错误
5. **对象存储直连验收**
- 登录态页面、分享页、审批页中的图片、贴图、模型和缩略图都能正常显示
- 浏览器网络面板里,大部分最终素材请求应落到正式 CDN 域名,而不是持续由 `cp.jiansun.vip` 回流响应体
- 如正式环境已启用 `QINIU_PUBLIC_DOMAIN=https://<cdn-domain>`,应确认 `307 -> signed CDN URL` 工作正常
- 2026-06-06 已在正式环境 `https://cp.jiansun.vip/asset-center/material-library` 做过一次真实业务侧抽验:页面纹理稳定入口指向 `/api/storage/object?...`,服务端响应为 `307`,最终 `Location` 落到 `https://qnycdn.jiansun.vip/...`
---
## 7. 与备份/运维的衔接

View File

@ -0,0 +1,124 @@
# 限价与破价判定规则(业务确认版)
> 适用范围:限价管理 · 破价订单审核(`/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` 不需要灌数:接口读取时用代码内置默认值,首次在「规则配置」保存时落库。
## 五、待办
- [ ] 限价流程数据接入 + 筛选逻辑(生效时间版本、货号维度)
- [ ] 非负责人限价 10-07 规则、负责人触发规则(前两个月同价 / 两个月后按销量)落到判定引擎
- [ ] 按天增量任务(cron / worker)与失败告警
- [ ] 凭证查重(hash / 相似度)、关键字段一致性校验
- [ ] 举证期限、乐捐财务对接流程
- [ ] 首单礼金 / 新品礼金出资方确认后补录出资方字典

View File

@ -1,17 +1,86 @@
/** @type {import('next').NextConfig} */
const isProduction = process.env.NODE_ENV === 'production'
function trimToNull(value) {
const normalized = value?.trim()
return normalized ? normalized : null
}
function buildUrlOrigin(rawUrl) {
const normalized = trimToNull(rawUrl)
if (!normalized) {
return null
}
const withProtocol = /^https?:\/\//i.test(normalized)
? normalized
: `https://${normalized}`
try {
const parsed = new URL(withProtocol)
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
return null
}
return parsed.origin
} catch {
return null
}
}
function collectStorageConnectSources() {
const origins = new Set()
for (const candidate of [
process.env.S3_PUBLIC_DOMAIN,
process.env.S3_PUBLIC_ENDPOINT,
process.env.S3_PUBLIC_URL,
process.env.S3_ENDPOINT,
process.env.QINIU_PUBLIC_DOMAIN,
process.env.QINIU_PUBLIC_ENDPOINT,
process.env.QINIU_PUBLIC_URL,
]) {
const origin = buildUrlOrigin(candidate)
if (origin) {
origins.add(origin)
}
}
return Array.from(origins)
}
const connectSources = [
"'self'",
'https://login.dingtalk.com',
'https://api.dingtalk.com',
'https://oapi.dingtalk.com',
...collectStorageConnectSources(),
'ws://127.0.0.1:8000',
'ws://127.0.0.1:18000',
'ws://localhost:8000',
'ws://localhost:18000',
'wss://localhost.lodop.net:8443',
...(!isProduction ? ['ws:', 'http://localhost:*', 'http://127.0.0.1:*'] : []),
]
const lodopScriptSources = [
'http://127.0.0.1:8000',
'http://127.0.0.1:18000',
'http://localhost:8000',
'http://localhost:18000',
'https://localhost.lodop.net:8443',
]
const contentSecurityPolicy = [
"default-src 'self'",
"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:",
"style-src 'self' 'unsafe-inline'",
`script-src 'self' 'unsafe-inline'${isProduction ? '' : " 'unsafe-eval'"}`,
`connect-src 'self' https://login.dingtalk.com https://api.dingtalk.com https://oapi.dingtalk.com${isProduction ? '' : ' ws: http://localhost:* http://127.0.0.1:*'}`,
`script-src 'self' 'unsafe-inline' ${lodopScriptSources.join(' ')}${isProduction ? '' : " 'unsafe-eval'"}`,
`connect-src ${connectSources.join(' ')}`,
"worker-src 'self' blob:",
"media-src 'self' blob:",
...(isProduction ? ["upgrade-insecure-requests"] : []),
@ -56,11 +125,6 @@ const baseSecurityHeaders = [
: []),
]
function trimToNull(value) {
const normalized = value?.trim()
return normalized ? normalized : null
}
function buildRemotePatternFromUrl(rawUrl) {
const normalized = trimToNull(rawUrl)
if (!normalized) {
@ -68,7 +132,10 @@ function buildRemotePatternFromUrl(rawUrl) {
}
try {
const parsed = new URL(normalized)
const withProtocol = /^https?:\/\//i.test(normalized)
? normalized
: `https://${normalized}`
const parsed = new URL(withProtocol)
const pathname = parsed.pathname.replace(/\/+$/, '')
return {
protocol: parsed.protocol.replace(':', ''),
@ -106,6 +173,9 @@ for (const candidate of [
process.env.S3_PUBLIC_ENDPOINT,
process.env.S3_PUBLIC_URL,
process.env.S3_ENDPOINT,
process.env.QINIU_PUBLIC_DOMAIN,
process.env.QINIU_PUBLIC_ENDPOINT,
process.env.QINIU_PUBLIC_URL,
]) {
const pattern = buildRemotePatternFromUrl(candidate)
if (!pattern) {
@ -125,7 +195,42 @@ for (const candidate of [
}
}
function buildClientStorageEnv() {
return {
STORAGE_PROVIDER:
trimToNull(process.env.STORAGE_PROVIDER) ||
trimToNull(process.env.OBJECT_STORAGE_PROVIDER) ||
'',
OBJECT_STORAGE_PROVIDER:
trimToNull(process.env.OBJECT_STORAGE_PROVIDER) ||
trimToNull(process.env.STORAGE_PROVIDER) ||
'',
STORAGE_FILE_PATH:
trimToNull(process.env.STORAGE_FILE_PATH) ||
trimToNull(process.env.OBJECT_STORAGE_FILE_PATH) ||
'',
OBJECT_STORAGE_FILE_PATH:
trimToNull(process.env.OBJECT_STORAGE_FILE_PATH) ||
trimToNull(process.env.STORAGE_FILE_PATH) ||
'',
S3_FILE_PATH: trimToNull(process.env.S3_FILE_PATH) || '',
QINIU_FILE_PATH: trimToNull(process.env.QINIU_FILE_PATH) || '',
S3_PUBLIC_DOMAIN: trimToNull(process.env.S3_PUBLIC_DOMAIN) || '',
S3_PUBLIC_ENDPOINT: trimToNull(process.env.S3_PUBLIC_ENDPOINT) || '',
S3_PUBLIC_URL: trimToNull(process.env.S3_PUBLIC_URL) || '',
S3_ENDPOINT: trimToNull(process.env.S3_ENDPOINT) || '',
S3_BUCKET: trimToNull(process.env.S3_BUCKET) || '',
QINIU_PUBLIC_DOMAIN: trimToNull(process.env.QINIU_PUBLIC_DOMAIN) || '',
QINIU_PUBLIC_ENDPOINT: trimToNull(process.env.QINIU_PUBLIC_ENDPOINT) || '',
QINIU_PUBLIC_URL: trimToNull(process.env.QINIU_PUBLIC_URL) || '',
QINIU_USE_HTTPS: trimToNull(process.env.QINIU_USE_HTTPS) || '',
}
}
const clientStorageEnv = buildClientStorageEnv()
const nextConfig = {
distDir: process.env.NEXT_DIST_DIR || '.next',
// 启用standalone输出,用于Docker部署
output: 'standalone',
@ -135,13 +240,14 @@ const nextConfig = {
},
// 服务器外部包配置(Next.js 16新配置)
serverExternalPackages: ['sharp', 'three', 'gsap'],
serverExternalPackages: ['sharp', 'three', 'gsap', '@huggingface/transformers', 'onnxruntime-node', 'mssql'],
// Turbopack配置(Next.js 16默认启用)
turbopack: {},
// Keep single request bodies bounded. Large non-integrated files use the chunked upload route.
experimental: {
cpus: 2,
proxyClientMaxBodySize: 500 * 1024 * 1024,
},
@ -238,6 +344,7 @@ const nextConfig = {
// 环境变量配置
env: {
CUSTOM_KEY: process.env.CUSTOM_KEY,
...clientStorageEnv,
},
// 重定向配置

View File

@ -13,6 +13,7 @@
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@hookform/resolvers": "^5.2.2",
"@huggingface/transformers": "3.8.1",
"@prisma/adapter-pg": "^7.3.0",
"@prisma/client": "^7.3.0",
"@react-three/drei": "^10.7.7",
@ -20,15 +21,18 @@
"@tanstack/react-query": "^5.90.20",
"@types/bcryptjs": "^2.4.6",
"@types/jsonwebtoken": "^9.0.10",
"@types/qrcode": "^1.5.6",
"autoprefixer": "^10.4.22",
"bcryptjs": "^3.0.3",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"crypto-js": "^4.2.0",
"date-fns": "^4.1.0",
"dockerode": "^4.0.9",
"exceljs": "^4.4.0",
"framer-motion": "^12.29.2",
"gsap": "^3.12.5",
"html2canvas": "^1.4.1",
"ioredis": "^5.9.2",
"jsonwebtoken": "^9.0.3",
"jszip": "^3.10.1",
@ -39,18 +43,22 @@
"next": "^16.1.6",
"obj2gltf": "^3.2.0",
"pg": "^8.17.2",
"pinyin-pro": "^3.28.1",
"pinyin-pro": "^3.29.5",
"postcss": "^8.5.6",
"print-js": "^1.6.0",
"puppeteer": "^24.40.0",
"qiniu": "^7.15.2",
"qr-scanner": "^1.4.2",
"qrcode": "^1.5.4",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"react-hook-form": "^7.54.2",
"server-only": "^0.0.1",
"sharp": "^0.34.5",
"ssh2": "^1.17.0",
"tailwind-merge": "^3.4.0",
"three": "^0.182.0",
"uuid": "^11.1.1",
"xlsx": "^0.18.5",
"zod": "^4.3.6",
"zustand": "^5.0.2"
@ -61,6 +69,7 @@
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
"@testing-library/user-event": "^14.6.1",
"@types/crypto-js": "^4.2.2",
"@types/dockerode": "^4.0.1",
"@types/ioredis": "^4.28.10",
"@types/jest": "^30.0.0",
@ -2961,6 +2970,27 @@
"react-hook-form": "^7.55.0"
}
},
"node_modules/@huggingface/jinja": {
"version": "0.5.9",
"resolved": "https://registry.npmjs.org/@huggingface/jinja/-/jinja-0.5.9.tgz",
"integrity": "sha512-uWTG+l3VJRsl7EXxYizuL3P+cCPoc3cRqbWWRcQN0FhejRfbdq0RNhCmbY/YDtnTcz9icdLYuLDjsnz4d8JMuw==",
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/@huggingface/transformers": {
"version": "3.8.1",
"resolved": "https://registry.npmjs.org/@huggingface/transformers/-/transformers-3.8.1.tgz",
"integrity": "sha512-tsTk4zVjImqdqjS8/AOZg2yNLd1z9S5v+7oUPpXaasDRwEDhB+xnglK1k5cad26lL5/ZIaeREgWWy0bs9y9pPA==",
"license": "Apache-2.0",
"dependencies": {
"@huggingface/jinja": "^0.5.3",
"onnxruntime-node": "1.21.0",
"onnxruntime-web": "1.22.0-dev.20250409-89f8206ba4",
"sharp": "^0.34.1"
}
},
"node_modules/@humanfs/core": {
"version": "0.19.1",
"resolved": "https://registry.npmmirror.com/@humanfs/core/-/core-0.19.1.tgz",
@ -3502,6 +3532,18 @@
"node": ">=12"
}
},
"node_modules/@isaacs/fs-minipass": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz",
"integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==",
"license": "ISC",
"dependencies": {
"minipass": "^7.0.4"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@istanbuljs/load-nyc-config": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz",
@ -5790,6 +5832,66 @@
"node": ">=14.0.0"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": {
"version": "1.7.1",
"dev": true,
"inBundle": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/wasi-threads": "1.1.0",
"tslib": "^2.4.0"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": {
"version": "1.7.1",
"dev": true,
"inBundle": true,
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": {
"version": "1.1.0",
"dev": true,
"inBundle": true,
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": {
"version": "1.1.0",
"dev": true,
"inBundle": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/core": "^1.7.1",
"@emnapi/runtime": "^1.7.1",
"@tybys/wasm-util": "^0.10.1"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": {
"version": "0.10.1",
"dev": true,
"inBundle": true,
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": {
"version": "2.8.1",
"dev": true,
"inBundle": true,
"license": "0BSD",
"optional": true
},
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.18.tgz",
@ -6047,6 +6149,13 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/crypto-js": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/@types/crypto-js/-/crypto-js-4.2.2.tgz",
"integrity": "sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/docker-modem": {
"version": "3.0.6",
"resolved": "https://registry.npmmirror.com/@types/docker-modem/-/docker-modem-3.0.6.tgz",
@ -6242,6 +6351,15 @@
"pg-types": "^2.2.0"
}
},
"node_modules/@types/qrcode": {
"version": "1.5.6",
"resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.6.tgz",
"integrity": "sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==",
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/react": {
"version": "19.2.10",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.10.tgz",
@ -7785,6 +7903,15 @@
"bare-path": "^3.0.0"
}
},
"node_modules/base64-arraybuffer": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
"license": "MIT",
"engines": {
"node": ">= 0.6.0"
}
},
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmmirror.com/base64-js/-/base64-js-1.5.1.tgz",
@ -7937,6 +8064,13 @@
"integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==",
"license": "MIT"
},
"node_modules/boolean": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz",
"integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==",
"deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.",
"license": "MIT"
},
"node_modules/bowser": {
"version": "2.13.1",
"resolved": "https://registry.npmjs.org/bowser/-/bowser-2.13.1.tgz",
@ -8186,7 +8320,6 @@
"version": "5.3.1",
"resolved": "https://registry.npmmirror.com/camelcase/-/camelcase-5.3.1.tgz",
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
@ -8751,6 +8884,22 @@
"node": ">= 8"
}
},
"node_modules/crypto-js": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
"integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==",
"deprecated": "Active development of CryptoJS has been discontinued. This library is no longer maintained.",
"license": "MIT"
},
"node_modules/css-line-break": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
"integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
"license": "MIT",
"dependencies": {
"utrie": "^1.0.2"
}
},
"node_modules/css.escape": {
"version": "1.5.1",
"resolved": "https://registry.npmmirror.com/css.escape/-/css.escape-1.5.1.tgz",
@ -8896,6 +9045,15 @@
}
}
},
"node_modules/decamelize": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
"integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/decimal.js": {
"version": "10.6.0",
"resolved": "https://registry.npmmirror.com/decimal.js/-/decimal.js-10.6.0.tgz",
@ -8989,7 +9147,6 @@
"version": "1.1.4",
"resolved": "https://registry.npmmirror.com/define-data-property/-/define-data-property-1.1.4.tgz",
"integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-define-property": "^1.0.0",
@ -9019,7 +9176,6 @@
"version": "1.2.1",
"resolved": "https://registry.npmmirror.com/define-properties/-/define-properties-1.2.1.tgz",
"integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
"dev": true,
"license": "MIT",
"dependencies": {
"define-data-property": "^1.0.1",
@ -9128,6 +9284,12 @@
"node": ">=8"
}
},
"node_modules/detect-node": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz",
"integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==",
"license": "MIT"
},
"node_modules/devtools-protocol": {
"version": "0.0.1581282",
"resolved": "https://registry.npmmirror.com/devtools-protocol/-/devtools-protocol-0.0.1581282.tgz",
@ -9154,6 +9316,12 @@
"node": ">= 8.0.0"
}
},
"node_modules/dijkstrajs": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz",
"integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==",
"license": "MIT"
},
"node_modules/docker-modem": {
"version": "5.0.6",
"resolved": "https://registry.npmmirror.com/docker-modem/-/docker-modem-5.0.6.tgz",
@ -9187,6 +9355,20 @@
"node": ">= 8.0"
}
},
"node_modules/dockerode/node_modules/uuid": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz",
"integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==",
"deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/doctrine": {
"version": "2.1.0",
"resolved": "https://registry.npmmirror.com/doctrine/-/doctrine-2.1.0.tgz",
@ -9646,6 +9828,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/es6-error": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz",
"integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==",
"license": "MIT"
},
"node_modules/esbuild": {
"version": "0.27.1",
"resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.27.1.tgz",
@ -9707,7 +9895,6 @@
"version": "4.0.0",
"resolved": "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
@ -10591,6 +10778,12 @@
"node": ">=16"
}
},
"node_modules/flatbuffers": {
"version": "25.9.23",
"resolved": "https://registry.npmjs.org/flatbuffers/-/flatbuffers-25.9.23.tgz",
"integrity": "sha512-MI1qs7Lo4Syw0EOzUl0xjs2lsoeqFku44KpngfIduHBYvzm8h2+7K8YMQh1JtVVVrUvhLpNwqVi4DERegUJhPQ==",
"license": "Apache-2.0"
},
"node_modules/flatted": {
"version": "3.3.3",
"resolved": "https://registry.npmmirror.com/flatted/-/flatted-3.3.3.tgz",
@ -11052,6 +11245,35 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/global-agent": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz",
"integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==",
"license": "BSD-3-Clause",
"dependencies": {
"boolean": "^3.0.1",
"es6-error": "^4.1.1",
"matcher": "^3.0.0",
"roarr": "^2.15.3",
"semver": "^7.3.2",
"serialize-error": "^7.0.1"
},
"engines": {
"node": ">=10.0"
}
},
"node_modules/global-agent/node_modules/semver": {
"version": "7.8.5",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
"integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/globals": {
"version": "14.0.0",
"resolved": "https://registry.npmmirror.com/globals/-/globals-14.0.0.tgz",
@ -11069,7 +11291,6 @@
"version": "1.0.4",
"resolved": "https://registry.npmmirror.com/globalthis/-/globalthis-1.0.4.tgz",
"integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"define-properties": "^1.2.1",
@ -11131,6 +11352,12 @@
"integrity": "sha512-P8/mMxVLU7o4+55+1TCnQrPmgjPKnwkzkXOK1asnR9Jg2lna4tEY5qBJjMmAaOBDDZWtlRjBXjLa0w53G/uBLA==",
"license": "Standard 'no charge' license: https://gsap.com/standard-license."
},
"node_modules/guid-typescript": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/guid-typescript/-/guid-typescript-1.0.9.tgz",
"integrity": "sha512-Y8T4vYhEfwJOTbouREvG+3XDsjr8E3kIr7uf+JZ0BYloFsttiHU0WfvANVsR7TxNUJa/WpCnw/Ino/p+DeBhBQ==",
"license": "ISC"
},
"node_modules/has-bigints": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/has-bigints/-/has-bigints-1.1.0.tgz",
@ -11158,7 +11385,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
"integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-define-property": "^1.0.0"
@ -11276,6 +11502,19 @@
"dev": true,
"license": "MIT"
},
"node_modules/html2canvas": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
"integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
"license": "MIT",
"dependencies": {
"css-line-break": "^2.1.0",
"text-segmentation": "^1.0.3"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/http-proxy-agent": {
"version": "7.0.2",
"resolved": "https://registry.npmmirror.com/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
@ -13244,6 +13483,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/json-stringify-safe": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
"integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==",
"license": "ISC"
},
"node_modules/json5": {
"version": "2.2.3",
"resolved": "https://registry.npmmirror.com/json5/-/json5-2.2.3.tgz",
@ -14073,6 +14318,18 @@
"tmpl": "1.0.5"
}
},
"node_modules/matcher": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz",
"integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==",
"license": "MIT",
"dependencies": {
"escape-string-regexp": "^4.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@ -14224,12 +14481,23 @@
"version": "7.1.2",
"resolved": "https://registry.npmmirror.com/minipass/-/minipass-7.1.2.tgz",
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
"dev": true,
"license": "ISC",
"engines": {
"node": ">=16 || 14 >=14.17"
}
},
"node_modules/minizlib": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz",
"integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==",
"license": "MIT",
"dependencies": {
"minipass": "^7.1.2"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/mitt": {
"version": "3.0.1",
"resolved": "https://registry.npmmirror.com/mitt/-/mitt-3.0.1.tgz",
@ -14677,7 +14945,6 @@
"version": "1.1.1",
"resolved": "https://registry.npmmirror.com/object-keys/-/object-keys-1.1.1.tgz",
"integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@ -14805,6 +15072,49 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/onnxruntime-common": {
"version": "1.21.0",
"resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.21.0.tgz",
"integrity": "sha512-Q632iLLrtCAVOTO65dh2+mNbQir/QNTVBG3h/QdZBpns7mZ0RYbLRBgGABPbpU9351AgYy7SJf1WaeVwMrBFPQ==",
"license": "MIT"
},
"node_modules/onnxruntime-node": {
"version": "1.21.0",
"resolved": "https://registry.npmjs.org/onnxruntime-node/-/onnxruntime-node-1.21.0.tgz",
"integrity": "sha512-NeaCX6WW2L8cRCSqy3bInlo5ojjQqu2fD3D+9W5qb5irwxhEyWKXeH2vZ8W9r6VxaMPUan+4/7NDwZMtouZxEw==",
"hasInstallScript": true,
"license": "MIT",
"os": [
"win32",
"darwin",
"linux"
],
"dependencies": {
"global-agent": "^3.0.0",
"onnxruntime-common": "1.21.0",
"tar": "^7.0.1"
}
},
"node_modules/onnxruntime-web": {
"version": "1.22.0-dev.20250409-89f8206ba4",
"resolved": "https://registry.npmjs.org/onnxruntime-web/-/onnxruntime-web-1.22.0-dev.20250409-89f8206ba4.tgz",
"integrity": "sha512-0uS76OPgH0hWCPrFKlL8kYVV7ckM7t/36HfbgoFw6Nd0CZVVbQC4PkrR8mBX8LtNUFZO25IQBqV2Hx2ho3FlbQ==",
"license": "MIT",
"dependencies": {
"flatbuffers": "^25.1.24",
"guid-typescript": "^1.0.9",
"long": "^5.2.3",
"onnxruntime-common": "1.22.0-dev.20250409-89f8206ba4",
"platform": "^1.3.6",
"protobufjs": "^7.2.4"
}
},
"node_modules/onnxruntime-web/node_modules/onnxruntime-common": {
"version": "1.22.0-dev.20250409-89f8206ba4",
"resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.22.0-dev.20250409-89f8206ba4.tgz",
"integrity": "sha512-vDJMkfCfb0b1A836rgHj+ORuZf4B4+cc2bASQtpeoJLueuFc5DuYwjIZUBrSvx/fO5IrLjLz+oTrB3pcGlhovQ==",
"license": "MIT"
},
"node_modules/open": {
"version": "10.2.0",
"resolved": "https://registry.npmmirror.com/open/-/open-10.2.0.tgz",
@ -14926,7 +15236,6 @@
"version": "2.2.0",
"resolved": "https://registry.npmmirror.com/p-try/-/p-try-2.2.0.tgz",
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
@ -15024,7 +15333,6 @@
"version": "4.0.0",
"resolved": "https://registry.npmmirror.com/path-exists/-/path-exists-4.0.0.tgz",
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
@ -15229,9 +15537,9 @@
}
},
"node_modules/pinyin-pro": {
"version": "3.28.1",
"resolved": "https://registry.npmmirror.com/pinyin-pro/-/pinyin-pro-3.28.1.tgz",
"integrity": "sha512-oqz8ulwRgtUXRi0vbqEfGNly19zpyCxYrjhkk5TibGcgSW6eNwS5woajCXRwqURi8Ehc2yOFTiB4uNoZ+NJOnA==",
"version": "3.29.5",
"resolved": "https://registry.npmjs.org/pinyin-pro/-/pinyin-pro-3.29.5.tgz",
"integrity": "sha512-Fk5YoRmtdHqJWqAuzYVG/NzvzeCbaG+ZEwbY6Nbps3QAIrvQQEtcm49frcyCM8i2b5najnzPDtYqrHRYTTiuwQ==",
"license": "MIT"
},
"node_modules/pirates": {
@ -15325,6 +15633,12 @@
"pathe": "^2.0.3"
}
},
"node_modules/platform": {
"version": "1.3.6",
"resolved": "https://registry.npmjs.org/platform/-/platform-1.3.6.tgz",
"integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==",
"license": "MIT"
},
"node_modules/playwright": {
"version": "1.59.1",
"resolved": "https://registry.npmmirror.com/playwright/-/playwright-1.59.1.tgz",
@ -15540,6 +15854,12 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/print-js": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/print-js/-/print-js-1.6.0.tgz",
"integrity": "sha512-BfnOIzSKbqGRtO4o0rnj/K3681BSd2QUrsIZy/+WdCIugjIswjmx3lDEZpXB2ruGf9d4b3YNINri81+J0FsBWg==",
"license": "MIT"
},
"node_modules/prisma": {
"version": "7.3.0",
"resolved": "https://registry.npmjs.org/prisma/-/prisma-7.3.0.tgz",
@ -15826,6 +16146,182 @@
"@types/offscreencanvas": "^2019.6.4"
}
},
"node_modules/qrcode": {
"version": "1.5.4",
"resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz",
"integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==",
"license": "MIT",
"dependencies": {
"dijkstrajs": "^1.0.1",
"pngjs": "^5.0.0",
"yargs": "^15.3.1"
},
"bin": {
"qrcode": "bin/qrcode"
},
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/qrcode/node_modules/cliui": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
"integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
"license": "ISC",
"dependencies": {
"string-width": "^4.2.0",
"strip-ansi": "^6.0.0",
"wrap-ansi": "^6.2.0"
}
},
"node_modules/qrcode/node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"license": "MIT"
},
"node_modules/qrcode/node_modules/find-up": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
"license": "MIT",
"dependencies": {
"locate-path": "^5.0.0",
"path-exists": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/qrcode/node_modules/locate-path": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
"integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
"license": "MIT",
"dependencies": {
"p-locate": "^4.1.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/qrcode/node_modules/p-limit": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
"license": "MIT",
"dependencies": {
"p-try": "^2.0.0"
},
"engines": {
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/qrcode/node_modules/p-locate": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
"integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
"license": "MIT",
"dependencies": {
"p-limit": "^2.2.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/qrcode/node_modules/pngjs": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz",
"integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==",
"license": "MIT",
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/qrcode/node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"license": "MIT",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/qrcode/node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/qrcode/node_modules/wrap-ansi": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/qrcode/node_modules/y18n": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
"integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==",
"license": "ISC"
},
"node_modules/qrcode/node_modules/yargs": {
"version": "15.4.1",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
"integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
"license": "MIT",
"dependencies": {
"cliui": "^6.0.0",
"decamelize": "^1.2.0",
"find-up": "^4.1.0",
"get-caller-file": "^2.0.1",
"require-directory": "^2.1.1",
"require-main-filename": "^2.0.0",
"set-blocking": "^2.0.0",
"string-width": "^4.2.0",
"which-module": "^2.0.0",
"y18n": "^4.0.0",
"yargs-parser": "^18.1.2"
},
"engines": {
"node": ">=8"
}
},
"node_modules/qrcode/node_modules/yargs-parser": {
"version": "18.1.3",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
"integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
"license": "ISC",
"dependencies": {
"camelcase": "^5.0.0",
"decamelize": "^1.2.0"
},
"engines": {
"node": ">=6"
}
},
"node_modules/qs": {
"version": "6.14.1",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz",
@ -16119,6 +16615,12 @@
"node": ">=0.10.0"
}
},
"node_modules/require-main-filename": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
"license": "ISC"
},
"node_modules/resolve": {
"version": "1.22.11",
"resolved": "https://registry.npmmirror.com/resolve/-/resolve-1.22.11.tgz",
@ -16237,6 +16739,29 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/roarr": {
"version": "2.15.4",
"resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz",
"integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==",
"license": "BSD-3-Clause",
"dependencies": {
"boolean": "^3.0.1",
"detect-node": "^2.0.4",
"globalthis": "^1.0.1",
"json-stringify-safe": "^5.0.1",
"semver-compare": "^1.0.0",
"sprintf-js": "^1.1.2"
},
"engines": {
"node": ">=8.0"
}
},
"node_modules/roarr/node_modules/sprintf-js": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz",
"integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==",
"license": "BSD-3-Clause"
},
"node_modules/rrweb-cssom": {
"version": "0.8.0",
"resolved": "https://registry.npmmirror.com/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz",
@ -16390,11 +16915,56 @@
"semver": "bin/semver.js"
}
},
"node_modules/semver-compare": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz",
"integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==",
"license": "MIT"
},
"node_modules/seq-queue": {
"version": "0.0.5",
"resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz",
"integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q=="
},
"node_modules/serialize-error": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz",
"integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==",
"license": "MIT",
"dependencies": {
"type-fest": "^0.13.1"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/serialize-error/node_modules/type-fest": {
"version": "0.13.1",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz",
"integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==",
"license": "(MIT OR CC0-1.0)",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/server-only": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/server-only/-/server-only-0.0.1.tgz",
"integrity": "sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==",
"license": "MIT"
},
"node_modules/set-blocking": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
"license": "ISC"
},
"node_modules/set-function-length": {
"version": "1.2.2",
"resolved": "https://registry.npmmirror.com/set-function-length/-/set-function-length-1.2.2.tgz",
@ -17305,6 +17875,22 @@
"url": "https://opencollective.com/webpack"
}
},
"node_modules/tar": {
"version": "7.5.22",
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.22.tgz",
"integrity": "sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA==",
"license": "BlueOak-1.0.0",
"dependencies": {
"@isaacs/fs-minipass": "^4.0.0",
"chownr": "^3.0.0",
"minipass": "^7.1.2",
"minizlib": "^3.1.0",
"yallist": "^5.0.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/tar-fs": {
"version": "2.1.4",
"resolved": "https://registry.npmmirror.com/tar-fs/-/tar-fs-2.1.4.tgz",
@ -17333,6 +17919,24 @@
"node": ">=6"
}
},
"node_modules/tar/node_modules/chownr": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz",
"integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==",
"license": "BlueOak-1.0.0",
"engines": {
"node": ">=18"
}
},
"node_modules/tar/node_modules/yallist": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",
"integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==",
"license": "BlueOak-1.0.0",
"engines": {
"node": ">=18"
}
},
"node_modules/tarn": {
"version": "3.0.2",
"resolved": "https://registry.npmmirror.com/tarn/-/tarn-3.0.2.tgz",
@ -17468,6 +18072,15 @@
"b4a": "^1.6.4"
}
},
"node_modules/text-segmentation": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
"integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
"license": "MIT",
"dependencies": {
"utrie": "^1.0.2"
}
},
"node_modules/thenify": {
"version": "3.3.1",
"resolved": "https://registry.npmmirror.com/thenify/-/thenify-3.3.1.tgz",
@ -18247,17 +18860,26 @@
"node": ">= 4"
}
},
"node_modules/utrie": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
"integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
"license": "MIT",
"dependencies": {
"base64-arraybuffer": "^1.0.2"
}
},
"node_modules/uuid": {
"version": "10.0.0",
"resolved": "https://registry.npmmirror.com/uuid/-/uuid-10.0.0.tgz",
"integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==",
"version": "11.1.1",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.1.tgz",
"integrity": "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"bin": {
"uuid": "dist/bin/uuid"
"uuid": "dist/esm/bin/uuid"
}
},
"node_modules/v8-to-istanbul": {
@ -18459,6 +19081,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/which-module": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz",
"integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==",
"license": "ISC"
},
"node_modules/which-typed-array": {
"version": "1.1.19",
"resolved": "https://registry.npmmirror.com/which-typed-array/-/which-typed-array-1.1.19.tgz",

View File

@ -3,8 +3,9 @@
"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",
"dev": "next dev --webpack",
"build": "prisma generate && next build",
"start": "next start",
"lint": "eslint",
@ -16,16 +17,53 @@
"prisma:check-client-sync": "node scripts/check-prisma-client-sync.js",
"prisma:push": "node scripts/prisma-db-push-local.js",
"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",
"sync:supply-chain-product-archive-enrichment": "tsx scripts/sync-supply-chain-product-archive-enrichment.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",
"backfill:product-archive-opshead:apply": "tsx scripts/backfill-product-archive-opshead-from-jst-sku-info-manage.ts --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",
"run:product-sabc": "tsx scripts/run-product-sabc.ts",
"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",
"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",
"report:bom-agent-baseline": "tsx scripts/report-bom-agent-baseline.ts",
"trial:bom-costing-agent": "tsx scripts/run-bom-costing-agent-trial.ts",
"import:yunshan-created-at": "tsx scripts/import-yunshan-product-created-at.ts",
"import:product-archive-drawing-media": "tsx scripts/import-product-archive-drawing-media.ts",
"db:verify-master-data": "node scripts/verify-master-data-foundation.js",
"db:verify-master-data:audit": "node scripts/verify-master-data-foundation.js --output .master-data-audits/foundation-hard-ready-gate-current.json --summary-output .master-data-audits/foundation-hard-ready-gate-current.summary.json --summary-stdout",
"db:verify-master-data:strict": "node scripts/verify-master-data-foundation.js --require-applied",
"db:verify-master-data:migration-status": "node scripts/verify-master-data-foundation.js --read-only-migration-status",
"db:verify-master-data:production-readonly": "node scripts/verify-master-data-foundation.js --read-only-quality --summary-output .master-data-audits/foundation-production-readonly-current.summary.json --summary-stdout",
"db:cleanup-master-data": "node scripts/apply-master-data-cleanup-plan.js",
"db:cleanup-master-data:validate": "node scripts/apply-master-data-cleanup-plan.js --validate-plan",
"db:cleanup-operation-logs": "tsx scripts/cleanup-operation-logs.ts",
"db:cleanup-operation-logs:apply": "tsx scripts/cleanup-operation-logs.ts --apply --confirm=cleanup-operation-logs",
"db:export-product-category-review": "node scripts/export-non-integrated-product-category-review.js",
"db:build-product-category-cleanup-plan": "node scripts/build-non-integrated-product-category-cleanup-plan.js",
"db:match-jst-product-category-cleanup-plan": "node scripts/match-jst-product-category-cleanup-plan.js",
"db:archive-jst-unresolved-products": "node scripts/archive-jst-unresolved-non-integrated-products.js",
"prisma:seed": "tsx prisma/seed.ts",
"open-platform:seed": "tsx scripts/seed-open-platform-demo.ts",
"type-check": "node scripts/run-type-check.js",
"storage:verify-config": "tsx scripts/verify-object-storage-config.ts",
"storage:migrate-historical-assets": "tsx scripts/migrate-historical-assets-to-qiniu.ts",
"storage:migrate-historical-assets:gate": "tsx scripts/migrate-historical-assets-to-qiniu.ts --fail-on-missing-sources",
"storage:migrate-historical-assets:apply": "tsx scripts/migrate-historical-assets-to-qiniu.ts --apply",
"storage:gc:audit": "tsx scripts/storage-gc.ts",
"storage:gc:apply": "tsx scripts/storage-gc.ts --apply",
"storage:inventory:filesystem": "tsx scripts/migrate-historical-assets-to-qiniu.ts --skip-database-scan --include-filesystem-inventory",
"storage:inventory:filesystem:apply": "tsx scripts/migrate-historical-assets-to-qiniu.ts --skip-database-scan --include-filesystem-inventory --apply",
"demo:create": "node scripts/create-demo-data.js",
"demo:setup": "npm run prisma:push && npm run demo:create",
"demo:update": "node scripts/update-demo-models.js",
@ -33,11 +71,31 @@
"thumbnails:backfill": "tsx scripts/backfill-asset-variant-thumbnails.ts",
"thumbnails:backfill:apply": "tsx scripts/backfill-asset-variant-thumbnails.ts --apply",
"materials:sync-textures": "tsx scripts/sync-material-textures.ts",
"materials:sync-textures:apply": "tsx scripts/sync-material-textures.ts --apply",
"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",
"sync:oa-board": "tsx scripts/sync-oa-board.ts",
"sync:oa-board:apply": "tsx scripts/sync-oa-board.ts --apply",
"sync:oa-brand-planning": "tsx scripts/sync-oa-brand-planning-board.ts",
"sync:oa-brand-planning:apply": "tsx scripts/sync-oa-brand-planning-board.ts --apply",
"sync:oa-video-flow": "tsx scripts/sync-oa-video-flow-board.ts",
"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",
"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",
"report:oa-board-activities": "tsx scripts/report-oa-board-activities.ts",
"sync:employee-directory": "tsx scripts/sync-employee-directory.ts",
"sync:employee-directory:apply": "tsx scripts/sync-employee-directory.ts --apply",
"sync:dingtalk-directory:apply": "tsx scripts/sync-dingtalk-directory.ts --apply",
"sync:external-jsreport": "tsx scripts/sync-external-readonly-to-jsreport.ts",
"sync:external-jsreport:apply": "tsx scripts/sync-external-readonly-to-jsreport.ts --apply",
"db:verify-master-data:truth": "node scripts/verify-master-data-truth-sources.js",
"db:verify-master-data:code": "node scripts/verify-master-data-code-boundaries.js",
"db:verify-master-data:all": "npm run db:verify-master-data:truth && npm run db:verify-master-data:code && npm run db:verify-master-data:migration-status && npm run db:verify-master-data",
@ -63,7 +121,9 @@
"db:foundation:apply-pre-sku-formalization-repair": "node scripts/apply-pre-sku-formalization-repair.js",
"db:foundation:audit": "node scripts/run-master-data-foundation-audit.js",
"db:foundation:audit:cleanup-template": "node scripts/run-master-data-foundation-audit.js --include-cleanup-template",
"db:foundation:audit:full": "node scripts/run-master-data-foundation-audit.js --include-cleanup-template --use-docker-shadow --include-structure-repair-rehearsal --output-dir .master-data-audits/foundation-audit-current-goal",
"db:foundation:audit:production-readonly": "node scripts/run-master-data-foundation-audit.js --production-read-only --output-dir .master-data-audits/foundation-production-readonly-current",
"db:foundation:verify-production-readonly": "node scripts/verify-production-readonly-foundation-manifest.js",
"db:foundation:audit:full": "node scripts/run-master-data-foundation-audit.js --include-cleanup-template --include-storage-migration-gate --use-docker-shadow --include-structure-repair-rehearsal --output-dir .master-data-audits/foundation-audit-current-goal",
"db:foundation:completion-checklist": "node scripts/print-master-data-foundation-checklist.js",
"db:foundation:decision-package": "node scripts/print-master-data-foundation-decision-package.js"
},
@ -73,6 +133,7 @@
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@hookform/resolvers": "^5.2.2",
"@huggingface/transformers": "3.8.1",
"@prisma/adapter-pg": "^7.3.0",
"@prisma/client": "^7.3.0",
"@react-three/drei": "^10.7.7",
@ -80,15 +141,18 @@
"@tanstack/react-query": "^5.90.20",
"@types/bcryptjs": "^2.4.6",
"@types/jsonwebtoken": "^9.0.10",
"@types/qrcode": "^1.5.6",
"autoprefixer": "^10.4.22",
"bcryptjs": "^3.0.3",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"crypto-js": "^4.2.0",
"date-fns": "^4.1.0",
"dockerode": "^4.0.9",
"exceljs": "^4.4.0",
"framer-motion": "^12.29.2",
"gsap": "^3.12.5",
"html2canvas": "^1.4.1",
"ioredis": "^5.9.2",
"jsonwebtoken": "^9.0.3",
"jszip": "^3.10.1",
@ -99,18 +163,22 @@
"next": "^16.1.6",
"obj2gltf": "^3.2.0",
"pg": "^8.17.2",
"pinyin-pro": "^3.28.1",
"pinyin-pro": "^3.29.5",
"postcss": "^8.5.6",
"print-js": "^1.6.0",
"puppeteer": "^24.40.0",
"qiniu": "^7.15.2",
"qr-scanner": "^1.4.2",
"qrcode": "^1.5.4",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"react-hook-form": "^7.54.2",
"server-only": "^0.0.1",
"sharp": "^0.34.5",
"ssh2": "^1.17.0",
"tailwind-merge": "^3.4.0",
"three": "^0.182.0",
"uuid": "^11.1.1",
"xlsx": "^0.18.5",
"zod": "^4.3.6",
"zustand": "^5.0.2"
@ -144,6 +212,7 @@
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
"@testing-library/user-event": "^14.6.1",
"@types/crypto-js": "^4.2.2",
"@types/dockerode": "^4.0.1",
"@types/ioredis": "^4.28.10",
"@types/jest": "^30.0.0",
@ -166,5 +235,20 @@
"tailwindcss": "^4.1.18",
"tsx": "^4.21.0",
"typescript": "^5"
},
"pnpm": {
"onlyBuiltDependencies": [
"@parcel/watcher",
"@prisma/engines",
"cpu-features",
"esbuild",
"onnxruntime-node",
"prisma",
"protobufjs",
"puppeteer",
"sharp",
"ssh2",
"unrs-resolver"
]
}
}

View File

@ -83,6 +83,14 @@ BEGIN
OR cardinality("missingRequiredFields") > 0
)
)
OR
(
"dataQualityStatus" = 'CONFLICT'
AND (
NULLIF(btrim("skuCode"), '') IS NOT NULL
OR NULLIF(btrim("barcode"), '') IS NOT NULL
)
)
);
END IF;
END $$;

View File

@ -234,7 +234,8 @@ BEGIN
SELECT 'IntakeNonIntegratedSkuItem.skuCode' AS field_name, upper(btrim("skuCode")) AS normalized_value
FROM "IntakeNonIntegratedSkuItem"
WHERE NULLIF(btrim("skuCode"), '') IS NOT NULL
WHERE "dataQualityStatus" = 'READY'
AND NULLIF(btrim("skuCode"), '') IS NOT NULL
GROUP BY upper(btrim("skuCode"))
HAVING COUNT(*) > 1
@ -242,7 +243,8 @@ BEGIN
SELECT 'IntakeNonIntegratedSkuItem.barcode' AS field_name, upper(btrim("barcode")) AS normalized_value
FROM "IntakeNonIntegratedSkuItem"
WHERE NULLIF(btrim("barcode"), '') IS NOT NULL
WHERE "dataQualityStatus" = 'READY'
AND NULLIF(btrim("barcode"), '') IS NOT NULL
GROUP BY upper(btrim("barcode"))
HAVING COUNT(*) > 1
)

View File

@ -0,0 +1,24 @@
-- Repair environments where the non-integrated SKU quality enum was created
-- before CONFLICT was added to the Prisma schema and governance migrations.
-- Target relation: "IntakeNonIntegratedSkuItem"."dataQualityStatus".
DO $$
BEGIN
IF EXISTS (
SELECT 1
FROM pg_type t
JOIN pg_namespace n ON n.oid = t.typnamespace
WHERE n.nspname = 'public'
AND t.typname = 'IntakeNonIntegratedSkuDataQualityStatus'
)
AND NOT EXISTS (
SELECT 1
FROM pg_enum e
JOIN pg_type t ON t.oid = e.enumtypid
JOIN pg_namespace n ON n.oid = t.typnamespace
WHERE n.nspname = 'public'
AND t.typname = 'IntakeNonIntegratedSkuDataQualityStatus'
AND e.enumlabel = 'CONFLICT'
) THEN
ALTER TYPE "IntakeNonIntegratedSkuDataQualityStatus" ADD VALUE 'CONFLICT';
END IF;
END $$;

View File

@ -0,0 +1,232 @@
-- Structure non-integrated product pricing rows so external price queries do not read payload JSON.
-- Each active SKU gets one pricing-state row. The row may have no cost yet, but confirmation status is database-owned.
CREATE TABLE "IntakeNonIntegratedPricingRow" (
"id" TEXT NOT NULL,
"productId" TEXT NOT NULL,
"skuItemId" TEXT NOT NULL,
"skuCode" TEXT NOT NULL DEFAULT '',
"barcode" TEXT NOT NULL DEFAULT '',
"costPrice" DECIMAL(10,2),
"productTag" TEXT NOT NULL DEFAULT '',
"pricingConfirmed" BOOLEAN NOT NULL DEFAULT false,
"pricingConfirmedAt" TIMESTAMP(3),
"systemEnteredAt" TIMESTAMP(3),
"jstVirtualInventoryStatus" TEXT NOT NULL DEFAULT '',
"jstVirtualInventoryScheduledAt" TIMESTAMP(3),
"jstVirtualInventoryPushedAt" TIMESTAMP(3),
"jstVirtualInventoryError" TEXT NOT NULL DEFAULT '',
"jstVirtualInventorySoId" TEXT NOT NULL DEFAULT '',
"source" TEXT NOT NULL DEFAULT 'PAYLOAD_SYNC',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "IntakeNonIntegratedPricingRow_pkey" PRIMARY KEY ("id")
);
ALTER TABLE "IntakeNonIntegratedPricingRow"
ADD CONSTRAINT "IntakeNonIntegratedPricingRow_productId_fkey"
FOREIGN KEY ("productId") REFERENCES "IntakeNonIntegratedProduct"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "IntakeNonIntegratedPricingRow"
ADD CONSTRAINT "IntakeNonIntegratedPricingRow_product_sku_item_fkey"
FOREIGN KEY ("productId", "skuItemId") REFERENCES "IntakeNonIntegratedSkuItem"("productId", "skuItemId")
ON DELETE CASCADE ON UPDATE CASCADE;
CREATE UNIQUE INDEX "IntakeNonIntegratedPricingRow_productId_skuItemId_key"
ON "IntakeNonIntegratedPricingRow"("productId", "skuItemId");
CREATE INDEX "IntakeNonIntegratedPricingRow_productId_idx" ON "IntakeNonIntegratedPricingRow"("productId");
CREATE INDEX "IntakeNonIntegratedPricingRow_skuCode_idx" ON "IntakeNonIntegratedPricingRow"("skuCode");
CREATE INDEX "IntakeNonIntegratedPricingRow_barcode_idx" ON "IntakeNonIntegratedPricingRow"("barcode");
CREATE INDEX "IntakeNonIntegratedPricingRow_pricingConfirmed_idx" ON "IntakeNonIntegratedPricingRow"("pricingConfirmed");
ALTER TABLE "IntakeNonIntegratedPricingRow"
ADD CONSTRAINT "IntakeNonIntegratedPricingRow_identity_valid"
CHECK (
NULLIF(btrim("skuItemId"), '') IS NOT NULL
AND "skuCode" = upper(btrim("skuCode"))
AND "barcode" = upper(btrim("barcode"))
);
ALTER TABLE "IntakeNonIntegratedPricingRow"
ADD CONSTRAINT "IntakeNonIntegratedPricingRow_cost_state_valid"
CHECK (
("costPrice" IS NULL OR "costPrice" >= 0)
AND "jstVirtualInventoryStatus" IN ('', 'pending', 'pushed', 'failed', 'skipped')
);
ALTER TABLE "IntakeNonIntegratedPricingRow"
ADD CONSTRAINT "IntakeNonIntegratedPricingRow_optional_text_not_blank"
CHECK (
"productTag" = btrim("productTag")
AND "jstVirtualInventoryStatus" = btrim("jstVirtualInventoryStatus")
AND "jstVirtualInventoryError" = btrim("jstVirtualInventoryError")
AND "jstVirtualInventorySoId" = btrim("jstVirtualInventorySoId")
AND "source" = btrim("source")
AND NULLIF("source", '') IS NOT NULL
);
CREATE OR REPLACE FUNCTION "__parse_intake_non_integrated_pricing_timestamp"(value TEXT)
RETURNS TIMESTAMP(3) AS $$
BEGIN
IF NULLIF(btrim(COALESCE(value, '')), '') IS NULL THEN
RETURN NULL;
END IF;
RETURN btrim(value)::TIMESTAMP(3);
EXCEPTION WHEN OTHERS THEN
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
WITH source_sku_items AS (
SELECT
p."id" AS product_id,
item.sku_item,
item.ordinality::integer AS sort_order,
COALESCE(NULLIF(btrim(item.sku_item ->> 'id'), ''), 'sku-' || item.ordinality::text) AS raw_sku_item_id
FROM "IntakeNonIntegratedProduct" p
CROSS JOIN LATERAL jsonb_array_elements(
CASE
WHEN jsonb_typeof(p."payload"::jsonb -> 'skuItems') = 'array'
THEN p."payload"::jsonb -> 'skuItems'
ELSE '[]'::jsonb
END
) WITH ORDINALITY AS item(sku_item, ordinality)
WHERE p."deletedAt" IS NULL
),
deduped_sku_items AS (
SELECT
source_sku_items.*,
CASE
WHEN COUNT(*) OVER (PARTITION BY product_id, raw_sku_item_id) = 1
THEN raw_sku_item_id
ELSE raw_sku_item_id || '-' || ROW_NUMBER() OVER (
PARTITION BY product_id, raw_sku_item_id
ORDER BY sort_order
)::text
END AS sku_item_id
FROM source_sku_items
),
structured_sku_items AS (
SELECT
source.product_id,
source.raw_sku_item_id,
sku."skuItemId" AS sku_item_id,
upper(btrim(COALESCE(sku."skuCode", ''))) AS sku_code,
upper(btrim(COALESCE(sku."barcode", ''))) AS barcode
FROM deduped_sku_items source
JOIN "IntakeNonIntegratedSkuItem" sku
ON sku."productId" = source.product_id
AND sku."skuItemId" = source.sku_item_id
),
pricing_payload AS (
SELECT
p."id" AS product_id,
pricing_row.pricing_row,
btrim(COALESCE(pricing_row.pricing_row ->> 'skuId', '')) AS raw_sku_item_id
FROM "IntakeNonIntegratedProduct" p
CROSS JOIN LATERAL jsonb_array_elements(
CASE
WHEN jsonb_typeof(p."payload"::jsonb -> 'pricingRows') = 'array'
THEN p."payload"::jsonb -> 'pricingRows'
ELSE '[]'::jsonb
END
) AS pricing_row(pricing_row)
WHERE p."deletedAt" IS NULL
),
source_rows AS (
SELECT
product."id" AS product_id,
sku.sku_item_id,
sku.sku_code,
sku.barcode,
pricing.pricing_row,
COALESCE(
product."payload"::jsonb #>> ARRAY['documentWorkspaceConfirmations', '产品核价'],
'false'
) = 'true' AS pricing_confirmed,
product."payload"::jsonb #>> ARRAY['documentWorkspaceApprovedAt', '产品核价'] AS pricing_confirmed_at
FROM structured_sku_items sku
JOIN "IntakeNonIntegratedProduct" product ON product."id" = sku.product_id
LEFT JOIN LATERAL (
SELECT payload.pricing_row
FROM pricing_payload payload
WHERE payload.product_id = sku.product_id
AND (
payload.raw_sku_item_id = sku.raw_sku_item_id
OR payload.raw_sku_item_id = sku.sku_item_id
)
LIMIT 1
) pricing ON TRUE
)
INSERT INTO "IntakeNonIntegratedPricingRow" (
"id",
"productId",
"skuItemId",
"skuCode",
"barcode",
"costPrice",
"productTag",
"pricingConfirmed",
"pricingConfirmedAt",
"systemEnteredAt",
"jstVirtualInventoryStatus",
"jstVirtualInventoryScheduledAt",
"jstVirtualInventoryPushedAt",
"jstVirtualInventoryError",
"jstVirtualInventorySoId",
"source",
"createdAt",
"updatedAt"
)
SELECT
product_id || ':' || sku_item_id AS id,
product_id,
sku_item_id,
sku_code,
barcode,
CASE
WHEN btrim(COALESCE(pricing_row ->> 'costPrice', '')) ~ '^[0-9]+(\.[0-9]+)?$'
AND round((pricing_row ->> 'costPrice')::numeric, 2) <= 99999999.99
THEN round((pricing_row ->> 'costPrice')::numeric, 2)::DECIMAL(10,2)
ELSE NULL
END AS cost_price,
btrim(COALESCE(pricing_row ->> 'productTag', '')) AS product_tag,
pricing_confirmed,
"__parse_intake_non_integrated_pricing_timestamp"(pricing_confirmed_at),
"__parse_intake_non_integrated_pricing_timestamp"(pricing_row ->> 'systemEnteredAt'),
CASE
WHEN pricing_row ->> 'jstVirtualInventoryStatus' IN ('pending', 'pushed', 'failed', 'skipped')
THEN pricing_row ->> 'jstVirtualInventoryStatus'
ELSE ''
END AS jst_virtual_inventory_status,
"__parse_intake_non_integrated_pricing_timestamp"(pricing_row ->> 'jstVirtualInventoryScheduledAt'),
"__parse_intake_non_integrated_pricing_timestamp"(pricing_row ->> 'jstVirtualInventoryPushedAt'),
btrim(COALESCE(pricing_row ->> 'jstVirtualInventoryError', '')) AS jst_virtual_inventory_error,
btrim(COALESCE(pricing_row ->> 'jstVirtualInventorySoId', '')) AS jst_virtual_inventory_so_id,
'PAYLOAD_SYNC',
CURRENT_TIMESTAMP,
CURRENT_TIMESTAMP
FROM source_rows
ON CONFLICT ("productId", "skuItemId") DO NOTHING;
DROP FUNCTION "__parse_intake_non_integrated_pricing_timestamp"(TEXT);
CREATE OR REPLACE FUNCTION "cleanup_intake_non_integrated_pricing_rows_on_soft_delete"()
RETURNS TRIGGER AS $$
BEGIN
DELETE FROM "IntakeNonIntegratedPricingRow"
WHERE "productId" = NEW."id";
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
DROP TRIGGER IF EXISTS "IntakeNonIntegratedProduct_soft_delete_cleanup_pricing_rows"
ON "IntakeNonIntegratedProduct";
CREATE TRIGGER "IntakeNonIntegratedProduct_soft_delete_cleanup_pricing_rows"
AFTER UPDATE OF "deletedAt" ON "IntakeNonIntegratedProduct"
FOR EACH ROW
WHEN (OLD."deletedAt" IS NULL AND NEW."deletedAt" IS NOT NULL)
EXECUTE FUNCTION "cleanup_intake_non_integrated_pricing_rows_on_soft_delete"();

View File

@ -0,0 +1,102 @@
CREATE OR REPLACE FUNCTION "normalize_internal_asset_storage_url"(input_url TEXT)
RETURNS TEXT AS $$
DECLARE
raw_value TEXT;
object_path TEXT;
BEGIN
IF input_url IS NULL THEN
RETURN NULL;
END IF;
raw_value := btrim(input_url);
IF raw_value = '' THEN
RETURN input_url;
END IF;
IF raw_value ~* '^https?://(localhost|127\.0\.0\.1|\[::1\])(:[0-9]+)?/api/storage/object\?key=' THEN
RETURN regexp_replace(raw_value, '^https?://[^/]+', '', 'i');
END IF;
IF raw_value ~* '^https?://(localhost|127\.0\.0\.1|\[::1\])(:[0-9]+)?/furniture-3d/' THEN
object_path := regexp_replace(raw_value, '^https?://[^/]+/furniture-3d/', '', 'i');
object_path := split_part(object_path, '?', 1);
IF object_path <> '' THEN
RETURN '/api/storage/object?key=' || object_path;
END IF;
END IF;
IF raw_value ~* '^https?://[^/]*hd-bkt\.clouddn\.com/' THEN
object_path := regexp_replace(raw_value, '^https?://[^/]+/', '', 'i');
object_path := split_part(object_path, '?', 1);
IF object_path <> '' THEN
RETURN '/api/storage/object?key=' || object_path;
END IF;
END IF;
RETURN input_url;
END;
$$ LANGUAGE plpgsql IMMUTABLE;
UPDATE "Product"
SET
"modelUrl" = "normalize_internal_asset_storage_url"("modelUrl"),
"thumbnailUrl" = "normalize_internal_asset_storage_url"("thumbnailUrl")
WHERE "normalize_internal_asset_storage_url"("modelUrl") IS DISTINCT FROM "modelUrl"
OR "normalize_internal_asset_storage_url"("thumbnailUrl") IS DISTINCT FROM "thumbnailUrl";
UPDATE "ProductSKU"
SET
"modelUrl" = "normalize_internal_asset_storage_url"("modelUrl"),
"images" = ARRAY(
SELECT "normalize_internal_asset_storage_url"(image_url)
FROM unnest("images") AS image_url
)
WHERE "normalize_internal_asset_storage_url"("modelUrl") IS DISTINCT FROM "modelUrl"
OR EXISTS (
SELECT 1
FROM unnest("images") AS image_url
WHERE "normalize_internal_asset_storage_url"(image_url) IS DISTINCT FROM image_url
);
UPDATE "ProductSkuAssetProfile"
SET "imageUrl" = "normalize_internal_asset_storage_url"("imageUrl")
WHERE "normalize_internal_asset_storage_url"("imageUrl") IS DISTINCT FROM "imageUrl";
UPDATE "AssetVariant"
SET
"modelUrl" = "normalize_internal_asset_storage_url"("modelUrl"),
"thumbnailUrl" = "normalize_internal_asset_storage_url"("thumbnailUrl")
WHERE "normalize_internal_asset_storage_url"("modelUrl") IS DISTINCT FROM "modelUrl"
OR "normalize_internal_asset_storage_url"("thumbnailUrl") IS DISTINCT FROM "thumbnailUrl";
UPDATE "Material"
SET
"thumbnailUrl" = "normalize_internal_asset_storage_url"("thumbnailUrl"),
"textureUrl" = "normalize_internal_asset_storage_url"("textureUrl"),
"normalMapUrl" = "normalize_internal_asset_storage_url"("normalMapUrl")
WHERE "normalize_internal_asset_storage_url"("thumbnailUrl") IS DISTINCT FROM "thumbnailUrl"
OR "normalize_internal_asset_storage_url"("textureUrl") IS DISTINCT FROM "textureUrl"
OR "normalize_internal_asset_storage_url"("normalMapUrl") IS DISTINCT FROM "normalMapUrl";
UPDATE "Hardware"
SET
"modelUrl" = "normalize_internal_asset_storage_url"("modelUrl"),
"thumbnailUrl" = "normalize_internal_asset_storage_url"("thumbnailUrl")
WHERE "normalize_internal_asset_storage_url"("modelUrl") IS DISTINCT FROM "modelUrl"
OR "normalize_internal_asset_storage_url"("thumbnailUrl") IS DISTINCT FROM "thumbnailUrl";
UPDATE "CommonComponent"
SET
"modelUrl" = "normalize_internal_asset_storage_url"("modelUrl"),
"thumbnailUrl" = "normalize_internal_asset_storage_url"("thumbnailUrl")
WHERE "normalize_internal_asset_storage_url"("modelUrl") IS DISTINCT FROM "modelUrl"
OR "normalize_internal_asset_storage_url"("thumbnailUrl") IS DISTINCT FROM "thumbnailUrl";
UPDATE "IntakeNonIntegratedSkuItem"
SET
"image" = "normalize_internal_asset_storage_url"("image"),
"model3dDataUrl" = "normalize_internal_asset_storage_url"("model3dDataUrl")
WHERE "normalize_internal_asset_storage_url"("image") IS DISTINCT FROM "image"
OR "normalize_internal_asset_storage_url"("model3dDataUrl") IS DISTINCT FROM "model3dDataUrl";
DROP FUNCTION "normalize_internal_asset_storage_url"(TEXT);

View File

@ -0,0 +1,90 @@
-- Add a goods-code grouping layer between AssetFamily and AssetVariant.
-- Existing active variants are backfilled by goods code so multiple models can share one goods-code group.
CREATE TABLE "AssetSkuGroup" (
"id" TEXT NOT NULL,
"assetFamilyId" TEXT NOT NULL,
"code" TEXT NOT NULL,
"name" TEXT NOT NULL,
"description" TEXT,
"sortOrder" INTEGER NOT NULL DEFAULT 0,
"metadata" JSONB,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "AssetSkuGroup_pkey" PRIMARY KEY ("id")
);
ALTER TABLE "AssetVariant"
ADD COLUMN "skuGroupId" TEXT;
WITH sku_group_sources AS (
SELECT
"assetFamilyId",
"code",
(ARRAY_AGG(COALESCE(NULLIF(TRIM("name"), ''), "code") ORDER BY "createdAt" ASC, "id" ASC))[1] AS "name",
MIN("createdAt") AS "createdAt",
MAX("updatedAt") AS "updatedAt",
JSONB_AGG("id" ORDER BY "createdAt" ASC, "id" ASC) AS "sourceAssetVariantIds"
FROM "AssetVariant"
WHERE "deletedAt" IS NULL
GROUP BY "assetFamilyId", "code"
),
ranked_sku_groups AS (
SELECT
"assetFamilyId",
"code",
"name",
"createdAt",
"updatedAt",
"sourceAssetVariantIds",
ROW_NUMBER() OVER (
PARTITION BY "assetFamilyId"
ORDER BY "createdAt" ASC, "code" ASC
) AS row_no
FROM sku_group_sources
)
INSERT INTO "AssetSkuGroup" (
"id",
"assetFamilyId",
"code",
"name",
"description",
"sortOrder",
"metadata",
"createdAt",
"updatedAt"
)
SELECT
CONCAT('asg_', MD5("assetFamilyId" || ':' || "code")),
"assetFamilyId",
"code",
"name",
NULL,
row_no,
jsonb_build_object('source', 'variant-backfill', 'sourceAssetVariantIds', "sourceAssetVariantIds"),
"createdAt",
"updatedAt"
FROM ranked_sku_groups;
UPDATE "AssetVariant" AS variant
SET "skuGroupId" = CONCAT('asg_', MD5(variant."assetFamilyId" || ':' || variant."code"))
WHERE variant."skuGroupId" IS NULL
AND variant."deletedAt" IS NULL;
CREATE UNIQUE INDEX "AssetSkuGroup_assetFamilyId_code_key"
ON "AssetSkuGroup"("assetFamilyId", "code");
CREATE INDEX "AssetSkuGroup_assetFamilyId_idx" ON "AssetSkuGroup"("assetFamilyId");
CREATE INDEX "AssetSkuGroup_code_idx" ON "AssetSkuGroup"("code");
CREATE INDEX "AssetSkuGroup_sortOrder_idx" ON "AssetSkuGroup"("sortOrder");
CREATE INDEX "AssetVariant_skuGroupId_idx" ON "AssetVariant"("skuGroupId");
ALTER TABLE "AssetSkuGroup"
ADD CONSTRAINT "AssetSkuGroup_assetFamilyId_fkey"
FOREIGN KEY ("assetFamilyId") REFERENCES "AssetFamily"("id")
ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "AssetVariant"
ADD CONSTRAINT "AssetVariant_skuGroupId_fkey"
FOREIGN KEY ("skuGroupId") REFERENCES "AssetSkuGroup"("id")
ON DELETE SET NULL ON UPDATE CASCADE;

View File

@ -0,0 +1,80 @@
DO $$
BEGIN
IF to_regclass('public."AssetSkuGroup"') IS NOT NULL
AND EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = 'AssetVariant'
AND column_name = 'skuGroupId'
)
THEN
EXECUTE $SQL$
UPDATE "AssetVariant" AS variant
SET "businessData" = jsonb_set(
CASE
WHEN variant."businessData" IS NULL OR jsonb_typeof(variant."businessData") <> 'object'
THEN '{}'::jsonb
ELSE variant."businessData"
END,
'{legacySkuGroup}',
jsonb_build_object(
'id', sku_group.id,
'code', sku_group.code,
'name', sku_group.name,
'description', sku_group.description,
'sortOrder', sku_group."sortOrder",
'metadata', sku_group.metadata,
'assetFamilyId', sku_group."assetFamilyId",
'source', 'AssetSkuGroup.retired'
),
true
)
FROM "AssetSkuGroup" AS sku_group
WHERE variant."skuGroupId" = sku_group.id
$SQL$;
EXECUTE $SQL$
UPDATE "AssetFamily" AS family
SET metadata = jsonb_set(
CASE
WHEN family.metadata IS NULL OR jsonb_typeof(family.metadata) <> 'object'
THEN '{}'::jsonb
ELSE family.metadata
END,
'{legacySkuGroups}',
grouped.groups,
true
)
FROM (
SELECT
"assetFamilyId",
jsonb_agg(
jsonb_build_object(
'id', id,
'code', code,
'name', name,
'description', description,
'sortOrder', "sortOrder",
'metadata', metadata,
'source', 'AssetSkuGroup.retired'
)
ORDER BY "sortOrder", code
) AS groups
FROM "AssetSkuGroup"
GROUP BY "assetFamilyId"
) AS grouped
WHERE family.id = grouped."assetFamilyId"
$SQL$;
END IF;
END $$;
ALTER TABLE "AssetVariant"
DROP CONSTRAINT IF EXISTS "AssetVariant_skuGroupId_fkey";
DROP INDEX IF EXISTS "AssetVariant_skuGroupId_idx";
ALTER TABLE "AssetVariant"
DROP COLUMN IF EXISTS "skuGroupId";
DROP TABLE IF EXISTS "AssetSkuGroup";

View File

@ -0,0 +1,96 @@
-- Add a single wide external product-catalog view for read-only consumers.
-- This view only aggregates existing external contract views; it does not
-- expose public business tables or internal JSON snapshots directly.
CREATE SCHEMA IF NOT EXISTS external;
CREATE OR REPLACE VIEW external.product_catalog_wide_v1 AS
WITH asset_summary AS (
SELECT
product_business_key,
sku_business_key,
COUNT(*) FILTER (WHERE asset_type = 'SKU_IMAGE')::integer AS image_asset_count,
COUNT(*) FILTER (WHERE asset_type = 'SKU_MODEL')::integer AS model_asset_count,
COUNT(*) FILTER (WHERE asset_type = 'SKU_THUMBNAIL')::integer AS thumbnail_asset_count,
MIN(asset_url) FILTER (WHERE asset_type = 'SKU_MODEL') AS first_model_asset_url,
MIN(asset_url) FILTER (WHERE asset_type = 'SKU_THUMBNAIL') AS first_thumbnail_asset_url
FROM external.product_assets_v1
GROUP BY product_business_key, sku_business_key
)
SELECT
sku.product_id,
sku.product_model_number,
sku.product_business_key,
sku.product_name,
sku.product_description,
sku.product_category_id,
sku.product_category_name,
sku.product_thumbnail_url,
sku.product_model_url,
sku.sku_id,
sku.sku_code,
sku.sku_business_key,
sku.sku_name,
sku.price,
sku.cost_price,
sku.stock,
sku.is_active,
sku.record_status,
sku.model_url,
sku.image_url,
sku.image_urls,
sku.source_pre_sku_id,
sku.source_asset_variant_id,
sku.source_asset_variant_ids,
sku.asset_family_code,
sku.asset_family_name,
sku.spec_code,
sku.spec_name,
sku.barcode,
sku.size_json,
sku.color,
sku.material,
sku.source_status,
sku.synced_at,
sku.warehouse_location_id,
sku.warehouse_location_code,
sku.warehouse_location_name,
COALESCE(asset_summary.image_asset_count, 0) AS image_asset_count,
COALESCE(asset_summary.model_asset_count, 0) AS model_asset_count,
COALESCE(asset_summary.thumbnail_asset_count, 0) AS thumbnail_asset_count,
asset_summary.first_model_asset_url,
asset_summary.first_thumbnail_asset_url,
COALESCE(bom_summary.bom_item_count, 0) AS bom_item_count,
COALESCE(bom_summary.panel_item_count, 0) AS panel_item_count,
COALESCE(bom_summary.hardware_item_count, 0) AS hardware_item_count,
COALESCE(bom_summary.material_codes, ARRAY[]::text[]) AS material_codes,
COALESCE(bom_summary.material_names, ARRAY[]::text[]) AS material_names,
sku.created_at,
sku.updated_at
FROM external.product_skus_v1 sku
LEFT JOIN asset_summary
ON asset_summary.product_business_key = sku.product_business_key
AND asset_summary.sku_business_key = sku.sku_business_key
LEFT JOIN LATERAL (
SELECT
COUNT(*)::integer AS bom_item_count,
COUNT(*) FILTER (WHERE item_type = 'PANEL')::integer AS panel_item_count,
COUNT(*) FILTER (WHERE item_type = 'HARDWARE')::integer AS hardware_item_count,
array_remove(array_agg(DISTINCT material_code ORDER BY material_code), NULL) AS material_codes,
array_remove(array_agg(DISTINCT material_name ORDER BY material_name), NULL) AS material_names
FROM external.product_bom_v1 bom
WHERE bom.product_business_key = sku.product_business_key
AND (
bom.sku_business_key = sku.sku_business_key
OR bom.sku_business_key IS NULL
)
) bom_summary ON true;
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'external_reader') THEN
GRANT USAGE ON SCHEMA external TO external_reader;
GRANT SELECT ON external.product_catalog_wide_v1 TO external_reader;
END IF;
END
$$;

View File

@ -0,0 +1,303 @@
-- Add external warehouse and SKU quality views for read-only consumers.
-- These views expose a stable whitelist for product data, warehouse stock,
-- packaged items, and warehouse quality inspection records.
CREATE SCHEMA IF NOT EXISTS external;
CREATE OR REPLACE VIEW external.warehouse_locations_v1 AS
SELECT
wh."id" AS warehouse_location_id,
wh."code" AS warehouse_location_code,
wh."name" AS warehouse_location_name,
wh."zone" AS zone,
wh."aisle" AS aisle,
wh."shelf" AS shelf,
wh."bin" AS bin,
wh."isActive" AS is_active,
CASE WHEN wh."isActive" THEN 'ACTIVE' ELSE 'INACTIVE' END AS record_status,
wh."createdAt" AS created_at,
wh."updatedAt" AS updated_at
FROM "WarehouseLocation" wh;
CREATE OR REPLACE VIEW external.sku_warehouse_stock_v1 AS
SELECT
sku.product_id,
sku.product_model_number,
sku.product_business_key,
sku.product_name,
sku.product_category_id,
sku.product_category_name,
sku.sku_id,
sku.sku_code,
sku.sku_business_key,
sku.sku_name,
sku.barcode,
sku.stock AS stock_quantity,
sku.is_active AS sku_is_active,
sku.record_status AS sku_record_status,
sku.warehouse_location_id,
sku.warehouse_location_code,
sku.warehouse_location_name,
wh.zone AS warehouse_zone,
wh.aisle AS warehouse_aisle,
wh.shelf AS warehouse_shelf,
wh.bin AS warehouse_bin,
wh.is_active AS warehouse_location_is_active,
wh.record_status AS warehouse_location_status,
sku.created_at,
sku.updated_at
FROM external.product_skus_v1 sku
LEFT JOIN external.warehouse_locations_v1 wh
ON wh.warehouse_location_id = sku.warehouse_location_id;
CREATE OR REPLACE VIEW external.packaged_items_v1 AS
SELECT
item."id" AS packaged_item_id,
item."code" AS packaged_item_code,
item."name" AS packaged_item_name,
CASE
WHEN NULLIF(btrim(COALESCE(product."modelNumber", '')), '') IS NOT NULL
THEN upper(btrim(product."modelNumber"))
ELSE NULL
END AS product_business_key,
CASE
WHEN NULLIF(btrim(COALESCE(sku."skuCode", '')), '') IS NOT NULL
THEN upper(btrim(sku."skuCode"))
ELSE NULL
END AS sku_business_key,
product."id" AS product_id,
product."modelNumber" AS product_model_number,
product."name" AS product_name,
sku."id" AS sku_id,
sku."skuCode" AS sku_code,
COALESCE(
NULLIF(sku."name", ''),
NULLIF(profile."specName", ''),
sku."skuCode"
) AS sku_name,
profile."barcode" AS barcode,
item."quantityPerPack" AS quantity_per_pack,
item."stockQuantity" AS stock_quantity,
item."length" AS length,
item."width" AS width,
item."height" AS height,
item."weight" AS weight,
item."status" AS status,
item."warehouseLocationId" AS warehouse_location_id,
wh."code" AS warehouse_location_code,
wh."name" AS warehouse_location_name,
wh."zone" AS warehouse_zone,
wh."aisle" AS warehouse_aisle,
wh."shelf" AS warehouse_shelf,
wh."bin" AS warehouse_bin,
wh."isActive" AS warehouse_location_is_active,
item."createdAt" AS created_at,
item."updatedAt" AS updated_at
FROM "PackagedItem" item
LEFT JOIN "ProductSKU" sku
ON sku."id" = item."skuId"
AND sku."deletedAt" IS NULL
LEFT JOIN "Product" product
ON product."id" = sku."productId"
AND product."deletedAt" IS NULL
LEFT JOIN "ProductSkuAssetProfile" profile
ON profile."skuId" = sku."id"
LEFT JOIN "WarehouseLocation" wh
ON wh."id" = item."warehouseLocationId"
WHERE sku."id" IS NOT NULL
AND product."id" IS NOT NULL;
CREATE OR REPLACE VIEW external.sku_quality_inspections_v1 AS
SELECT
inspection."id" AS quality_inspection_id,
inspection."recordKey" AS record_key,
inspection."sourceType" AS source_type,
CASE
WHEN inspection."sourceType" = 'FIRST_BATCH' THEN '首批质检'
ELSE '仓库质检'
END AS source_label,
inspection."sourceProductId" AS source_product_id,
inspection."sourceProductSpu" AS source_product_spu,
inspection."sourceSkuItemId" AS source_sku_item_id,
inspection."batchNo" AS batch_no,
inspection."inspectedAt" AS inspected_at,
inspection."inspectorName" AS inspector_name,
inspection."conclusion" AS conclusion,
inspection."detail" AS detail,
inspection."inspectedQuantity" AS inspected_quantity,
inspection."qualifiedQuantity" AS qualified_quantity,
inspection."qualifiedRate" AS qualified_rate,
inspection."status" AS status,
sku.product_id,
sku.product_model_number,
sku.product_business_key,
sku.product_name,
sku.product_category_id,
sku.product_category_name,
sku.sku_id,
sku.sku_code,
sku.sku_business_key,
sku.sku_name,
sku.barcode,
sku.warehouse_location_id,
sku.warehouse_location_code,
sku.warehouse_location_name,
inspection."createdAt" AS created_at,
inspection."updatedAt" AS updated_at
FROM "ProductSkuQualityInspection" inspection
JOIN external.product_skus_v1 sku
ON sku.sku_id = inspection."skuId";
DROP VIEW IF EXISTS external.product_catalog_wide_v1;
CREATE VIEW external.product_catalog_wide_v1 AS
WITH asset_summary AS (
SELECT
product_business_key,
sku_business_key,
COUNT(*) FILTER (WHERE asset_type = 'SKU_IMAGE')::integer AS image_asset_count,
COUNT(*) FILTER (WHERE asset_type = 'SKU_MODEL')::integer AS model_asset_count,
COUNT(*) FILTER (WHERE asset_type = 'SKU_THUMBNAIL')::integer AS thumbnail_asset_count,
MIN(asset_url) FILTER (WHERE asset_type = 'SKU_MODEL') AS first_model_asset_url,
MIN(asset_url) FILTER (WHERE asset_type = 'SKU_THUMBNAIL') AS first_thumbnail_asset_url
FROM external.product_assets_v1
GROUP BY product_business_key, sku_business_key
),
packaged_summary AS (
SELECT
sku_business_key,
COUNT(*)::integer AS packaged_item_count,
COALESCE(SUM(stock_quantity), 0)::integer AS packaged_stock_quantity
FROM external.packaged_items_v1
WHERE NULLIF(sku_business_key, '') IS NOT NULL
GROUP BY sku_business_key
),
quality_summary AS (
SELECT
sku_business_key,
COUNT(*)::integer AS quality_record_count,
COUNT(*) FILTER (WHERE source_type = 'WAREHOUSE')::integer AS warehouse_quality_record_count,
COUNT(*) FILTER (WHERE source_type = 'FIRST_BATCH')::integer AS first_batch_quality_record_count
FROM external.sku_quality_inspections_v1
WHERE NULLIF(sku_business_key, '') IS NOT NULL
GROUP BY sku_business_key
),
latest_quality AS (
SELECT DISTINCT ON (sku_business_key)
sku_business_key,
record_key AS latest_quality_record_key,
source_type AS latest_quality_source_type,
batch_no AS latest_quality_batch_no,
inspected_at AS latest_quality_inspected_at,
conclusion AS latest_quality_conclusion,
status AS latest_quality_status
FROM external.sku_quality_inspections_v1
WHERE NULLIF(sku_business_key, '') IS NOT NULL
ORDER BY sku_business_key, COALESCE(inspected_at, updated_at, created_at) DESC, created_at DESC
)
SELECT
sku.product_id,
sku.product_model_number,
sku.product_business_key,
sku.product_name,
sku.product_description,
sku.product_category_id,
sku.product_category_name,
sku.product_thumbnail_url,
sku.product_model_url,
sku.sku_id,
sku.sku_code,
sku.sku_business_key,
sku.sku_name,
sku.price,
sku.cost_price,
sku.stock,
sku.is_active,
sku.record_status,
sku.model_url,
sku.image_url,
sku.image_urls,
sku.source_pre_sku_id,
sku.source_asset_variant_id,
sku.source_asset_variant_ids,
sku.asset_family_code,
sku.asset_family_name,
sku.spec_code,
sku.spec_name,
sku.barcode,
sku.size_json,
sku.color,
sku.material,
sku.source_status,
sku.synced_at,
sku.warehouse_location_id,
sku.warehouse_location_code,
sku.warehouse_location_name,
stock.warehouse_zone,
stock.warehouse_aisle,
stock.warehouse_shelf,
stock.warehouse_bin,
stock.warehouse_location_is_active,
stock.warehouse_location_status,
COALESCE(asset_summary.image_asset_count, 0) AS image_asset_count,
COALESCE(asset_summary.model_asset_count, 0) AS model_asset_count,
COALESCE(asset_summary.thumbnail_asset_count, 0) AS thumbnail_asset_count,
asset_summary.first_model_asset_url,
asset_summary.first_thumbnail_asset_url,
COALESCE(bom_summary.bom_item_count, 0) AS bom_item_count,
COALESCE(bom_summary.panel_item_count, 0) AS panel_item_count,
COALESCE(bom_summary.hardware_item_count, 0) AS hardware_item_count,
COALESCE(bom_summary.material_codes, ARRAY[]::text[]) AS material_codes,
COALESCE(bom_summary.material_names, ARRAY[]::text[]) AS material_names,
COALESCE(packaged_summary.packaged_item_count, 0) AS packaged_item_count,
COALESCE(packaged_summary.packaged_stock_quantity, 0) AS packaged_stock_quantity,
COALESCE(quality_summary.quality_record_count, 0) AS quality_record_count,
COALESCE(quality_summary.warehouse_quality_record_count, 0) AS warehouse_quality_record_count,
COALESCE(quality_summary.first_batch_quality_record_count, 0) AS first_batch_quality_record_count,
latest_quality.latest_quality_record_key,
latest_quality.latest_quality_source_type,
latest_quality.latest_quality_batch_no,
latest_quality.latest_quality_inspected_at,
latest_quality.latest_quality_conclusion,
latest_quality.latest_quality_status,
sku.created_at,
sku.updated_at
FROM external.product_skus_v1 sku
LEFT JOIN external.sku_warehouse_stock_v1 stock
ON stock.sku_business_key = sku.sku_business_key
LEFT JOIN asset_summary
ON asset_summary.product_business_key = sku.product_business_key
AND asset_summary.sku_business_key = sku.sku_business_key
LEFT JOIN LATERAL (
SELECT
COUNT(*)::integer AS bom_item_count,
COUNT(*) FILTER (WHERE item_type = 'PANEL')::integer AS panel_item_count,
COUNT(*) FILTER (WHERE item_type = 'HARDWARE')::integer AS hardware_item_count,
array_remove(array_agg(DISTINCT material_code ORDER BY material_code), NULL) AS material_codes,
array_remove(array_agg(DISTINCT material_name ORDER BY material_name), NULL) AS material_names
FROM external.product_bom_v1 bom
WHERE bom.product_business_key = sku.product_business_key
AND (
bom.sku_business_key = sku.sku_business_key
OR bom.sku_business_key IS NULL
)
) bom_summary ON true
LEFT JOIN packaged_summary
ON packaged_summary.sku_business_key = sku.sku_business_key
LEFT JOIN quality_summary
ON quality_summary.sku_business_key = sku.sku_business_key
LEFT JOIN latest_quality
ON latest_quality.sku_business_key = sku.sku_business_key;
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'external_reader') THEN
GRANT USAGE ON SCHEMA external TO external_reader;
GRANT SELECT ON external.product_catalog_wide_v1 TO external_reader;
GRANT SELECT ON external.warehouse_locations_v1 TO external_reader;
GRANT SELECT ON external.sku_warehouse_stock_v1 TO external_reader;
GRANT SELECT ON external.packaged_items_v1 TO external_reader;
GRANT SELECT ON external.sku_quality_inspections_v1 TO external_reader;
END IF;
END
$$;

View File

@ -0,0 +1,901 @@
-- Structure non-integrated product limit rows before exposing them through
-- the external read-only contract. Stock-product detail rows are marked as
-- limit-price not applicable in the external views.
CREATE SCHEMA IF NOT EXISTS external;
CREATE TABLE IF NOT EXISTS "IntakeNonIntegratedProductLimitRow" (
"id" TEXT NOT NULL,
"productId" TEXT NOT NULL,
"skuItemId" TEXT NOT NULL,
"skuCode" TEXT NOT NULL DEFAULT '',
"barcode" TEXT NOT NULL DEFAULT '',
"expectedDailyLimitPrice" TEXT NOT NULL DEFAULT '',
"expectedLimitGrossMarginRate" TEXT NOT NULL DEFAULT '',
"nonSpuOwnerLimitPrice" TEXT NOT NULL DEFAULT '',
"limitConfirmed" BOOLEAN NOT NULL DEFAULT false,
"limitConfirmedAt" TIMESTAMP(3),
"source" TEXT NOT NULL DEFAULT 'PAYLOAD_SYNC',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "IntakeNonIntegratedProductLimitRow_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX IF NOT EXISTS "IntakeNonIntegratedProductLimitRow_productId_skuItemId_key"
ON "IntakeNonIntegratedProductLimitRow"("productId", "skuItemId");
CREATE INDEX IF NOT EXISTS "IntakeNonIntegratedProductLimitRow_productId_idx"
ON "IntakeNonIntegratedProductLimitRow"("productId");
CREATE INDEX IF NOT EXISTS "IntakeNonIntegratedProductLimitRow_skuCode_idx"
ON "IntakeNonIntegratedProductLimitRow"("skuCode");
CREATE INDEX IF NOT EXISTS "IntakeNonIntegratedProductLimitRow_barcode_idx"
ON "IntakeNonIntegratedProductLimitRow"("barcode");
CREATE INDEX IF NOT EXISTS "IntakeNonIntegratedProductLimitRow_limitConfirmed_idx"
ON "IntakeNonIntegratedProductLimitRow"("limitConfirmed");
ALTER TABLE "IntakeNonIntegratedProductLimitRow"
DROP CONSTRAINT IF EXISTS "IntakeNonIntegratedProductLimitRow_productId_fkey",
ADD CONSTRAINT "IntakeNonIntegratedProductLimitRow_productId_fkey"
FOREIGN KEY ("productId") REFERENCES "IntakeNonIntegratedProduct"("id")
ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "IntakeNonIntegratedProductLimitRow"
DROP CONSTRAINT IF EXISTS "IntakeNonIntegratedProductLimitRow_product_sku_item_fkey",
ADD CONSTRAINT "IntakeNonIntegratedProductLimitRow_product_sku_item_fkey"
FOREIGN KEY ("productId", "skuItemId") REFERENCES "IntakeNonIntegratedSkuItem"("productId", "skuItemId")
ON DELETE CASCADE ON UPDATE CASCADE;
WITH limit_payload AS (
SELECT
product."id" AS product_id,
product."payload"::jsonb AS payload_json,
CASE
WHEN product."payload"::jsonb -> 'documentWorkspaceConfirmations' ->> '产品限价' = 'true'
THEN true
ELSE false
END AS limit_confirmed,
CASE
WHEN COALESCE(product."payload"::jsonb -> 'documentWorkspaceApprovedAt' ->> '产品限价', '') ~ '^\d{4}-\d{2}-\d{2}T'
THEN (product."payload"::jsonb -> 'documentWorkspaceApprovedAt' ->> '产品限价')::timestamp(3)
ELSE NULL
END AS limit_confirmed_at
FROM "IntakeNonIntegratedProduct" product
),
limit_rows AS (
SELECT
sku."productId" AS product_id,
sku."skuItemId" AS sku_item_id,
sku."skuCode" AS sku_code,
sku."barcode" AS barcode,
limit_payload.limit_confirmed,
limit_payload.limit_confirmed_at,
limit_item.item_json,
sku."createdAt" AS created_at,
sku."updatedAt" AS updated_at
FROM "IntakeNonIntegratedSkuItem" sku
JOIN limit_payload
ON limit_payload.product_id = sku."productId"
LEFT JOIN LATERAL (
SELECT item AS item_json
FROM jsonb_array_elements(
CASE
WHEN jsonb_typeof(limit_payload.payload_json -> 'productLimitRows') = 'array'
THEN limit_payload.payload_json -> 'productLimitRows'
ELSE '[]'::jsonb
END
) AS item
WHERE item ->> 'skuId' = sku."skuItemId"
LIMIT 1
) limit_item ON true
)
INSERT INTO "IntakeNonIntegratedProductLimitRow" (
"id",
"productId",
"skuItemId",
"skuCode",
"barcode",
"expectedDailyLimitPrice",
"expectedLimitGrossMarginRate",
"nonSpuOwnerLimitPrice",
"limitConfirmed",
"limitConfirmedAt",
"source",
"createdAt",
"updatedAt"
)
SELECT
product_id || ':' || sku_item_id,
product_id,
sku_item_id,
COALESCE(sku_code, ''),
COALESCE(barcode, ''),
COALESCE(NULLIF(btrim(item_json ->> 'expectedDailyLimitPrice'), ''), ''),
COALESCE(NULLIF(btrim(item_json ->> 'expectedLimitGrossMarginRate'), ''), ''),
COALESCE(NULLIF(btrim(item_json ->> 'nonSpuOwnerLimitPrice'), ''), ''),
limit_confirmed,
limit_confirmed_at,
'PAYLOAD_SYNC',
created_at,
updated_at
FROM limit_rows
ON CONFLICT ("productId", "skuItemId") DO NOTHING;
CREATE OR REPLACE FUNCTION "cleanup_intake_non_integrated_product_limit_rows_on_soft_delete"()
RETURNS TRIGGER AS $$
BEGIN
DELETE FROM "IntakeNonIntegratedProductLimitRow"
WHERE "productId" = NEW."id";
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
DROP TRIGGER IF EXISTS "IntakeNonIntegratedProduct_soft_delete_cleanup_product_limit_rows"
ON "IntakeNonIntegratedProduct";
CREATE TRIGGER "IntakeNonIntegratedProduct_soft_delete_cleanup_product_limit_rows"
AFTER UPDATE OF "deletedAt" ON "IntakeNonIntegratedProduct"
FOR EACH ROW
WHEN (OLD."deletedAt" IS NULL AND NEW."deletedAt" IS NOT NULL)
EXECUTE FUNCTION "cleanup_intake_non_integrated_product_limit_rows_on_soft_delete"();
CREATE OR REPLACE VIEW external.intake_product_limits_v1 AS
WITH limit_source AS (
SELECT
product."id" AS intake_product_id,
product."spu" AS spu,
product."brand" AS brand,
profile."productDevelopmentKind" AS product_development_kind,
profile."processTemplate" AS process_template,
profile."productStatus" AS product_status,
sku."skuItemId" AS intake_sku_item_id,
sku."skuSequence" AS sku_sequence,
sku."batchNo" AS batch_no,
sku."skuCode" AS sku_code,
sku."barcode" AS barcode,
limits."expectedDailyLimitPrice" AS expected_daily_limit_price_raw,
limits."expectedLimitGrossMarginRate" AS expected_limit_gross_margin_rate_raw,
limits."nonSpuOwnerLimitPrice" AS non_spu_owner_limit_price_raw,
limits."limitConfirmed" AS limit_confirmed_raw,
limits."limitConfirmedAt" AS limit_confirmed_at_raw,
limits."createdAt" AS created_at,
limits."updatedAt" AS updated_at,
CASE
WHEN btrim(COALESCE(profile."processTemplate", '')) IN ('存量商品', '存量商品明细')
OR btrim(COALESCE(profile."productStatus", '')) IN ('存量商品', '存量商品明细')
THEN false
WHEN btrim(COALESCE(profile."productDevelopmentKind", '')) IN ('product', 'series')
OR btrim(COALESCE(profile."processTemplate", '')) IN ('产品开发明细', '模块化产品明细')
OR btrim(COALESCE(profile."productStatus", '')) IN ('产品开发明细', '模块化产品明细')
THEN true
ELSE false
END AS limit_price_applicable
FROM "IntakeNonIntegratedProductLimitRow" limits
JOIN "IntakeNonIntegratedSkuItem" sku
ON sku."productId" = limits."productId"
AND sku."skuItemId" = limits."skuItemId"
JOIN "IntakeNonIntegratedProduct" product
ON product."id" = limits."productId"
LEFT JOIN "IntakeNonIntegratedProductProfile" profile
ON profile."productId" = product."id"
WHERE product."deletedAt" IS NULL
)
SELECT
intake_product_id,
spu,
brand,
product_development_kind,
process_template,
product_status,
intake_sku_item_id,
sku_sequence,
batch_no,
sku_code,
barcode,
CASE
WHEN NULLIF(btrim(COALESCE(brand, '')), '') IS NOT NULL
AND NULLIF(btrim(COALESCE(spu, '')), '') IS NOT NULL
THEN upper(btrim(brand)) || ':' || upper(btrim(spu))
ELSE NULL
END AS product_business_key,
CASE
WHEN NULLIF(btrim(COALESCE(brand, '')), '') IS NOT NULL
AND NULLIF(btrim(COALESCE(spu, '')), '') IS NOT NULL
AND NULLIF(btrim(COALESCE(sku_code, '')), '') IS NOT NULL
THEN upper(btrim(brand)) || ':' || upper(btrim(spu)) || ':' || upper(btrim(sku_code))
ELSE NULL
END AS sku_business_key,
limit_price_applicable,
CASE
WHEN limit_price_applicable
AND limit_confirmed_raw
AND NULLIF(btrim(COALESCE(expected_daily_limit_price_raw, '')), '') IS NOT NULL
AND NULLIF(btrim(COALESCE(expected_limit_gross_margin_rate_raw, '')), '') IS NOT NULL
AND NULLIF(btrim(COALESCE(non_spu_owner_limit_price_raw, '')), '') IS NOT NULL
THEN true
ELSE false
END AS limit_price_confirmed,
CASE
WHEN NOT limit_price_applicable THEN 'NOT_APPLICABLE'
WHEN limit_confirmed_raw
AND NULLIF(btrim(COALESCE(expected_daily_limit_price_raw, '')), '') IS NOT NULL
AND NULLIF(btrim(COALESCE(expected_limit_gross_margin_rate_raw, '')), '') IS NOT NULL
AND NULLIF(btrim(COALESCE(non_spu_owner_limit_price_raw, '')), '') IS NOT NULL
THEN 'CONFIRMED'
ELSE 'PENDING'
END AS limit_price_status,
CASE WHEN limit_price_applicable THEN expected_daily_limit_price_raw ELSE NULL END AS expected_daily_limit_price,
CASE WHEN limit_price_applicable THEN expected_limit_gross_margin_rate_raw ELSE NULL END AS expected_limit_gross_margin_rate,
CASE WHEN limit_price_applicable THEN non_spu_owner_limit_price_raw ELSE NULL END AS non_spu_owner_limit_price,
CASE
WHEN limit_price_applicable
AND limit_confirmed_raw
AND NULLIF(btrim(COALESCE(expected_daily_limit_price_raw, '')), '') IS NOT NULL
AND NULLIF(btrim(COALESCE(expected_limit_gross_margin_rate_raw, '')), '') IS NOT NULL
AND NULLIF(btrim(COALESCE(non_spu_owner_limit_price_raw, '')), '') IS NOT NULL
THEN limit_confirmed_at_raw
ELSE NULL
END AS limit_confirmed_at,
created_at,
updated_at
FROM limit_source;
CREATE OR REPLACE VIEW external.product_skus_v1 AS
SELECT
p."id" AS product_id,
p."modelNumber" AS product_model_number,
p."name" AS product_name,
p."description" AS product_description,
pc."id" AS product_category_id,
pc."name" AS product_category_name,
p."thumbnailUrl" AS product_thumbnail_url,
p."modelUrl" AS product_model_url,
sku."id" AS sku_id,
sku."skuCode" AS sku_code,
COALESCE(
NULLIF(sku."name", ''),
NULLIF(profile."specName", ''),
sku."skuCode"
) AS sku_name,
sku."price" AS price,
sku."costPrice" AS cost_price,
sku."stock" AS stock,
sku."isActive" AS is_active,
CASE WHEN sku."isActive" THEN 'ACTIVE' ELSE 'INACTIVE' END AS record_status,
COALESCE(NULLIF(sku."modelUrl", ''), NULLIF(p."modelUrl", '')) AS model_url,
COALESCE(
NULLIF(profile."imageUrl", ''),
sku."images"[1],
NULLIF(p."thumbnailUrl", '')
) AS image_url,
sku."images" AS image_urls,
sku."sourcePreSkuId" AS source_pre_sku_id,
sku."sourceAssetVariantId" AS source_asset_variant_id,
sku."sourceAssetVariantIds" AS source_asset_variant_ids,
profile."model" AS asset_family_code,
profile."modelName" AS asset_family_name,
profile."specCode" AS spec_code,
profile."specName" AS spec_name,
profile."barcode" AS barcode,
profile."size" AS size_json,
profile."color" AS color,
profile."material" AS material,
profile."sourceStatus"::text AS source_status,
profile."syncedAt"::text AS synced_at,
wh."id" AS warehouse_location_id,
wh."code" AS warehouse_location_code,
wh."name" AS warehouse_location_name,
sku."createdAt" AS created_at,
sku."updatedAt" AS updated_at,
CASE
WHEN NULLIF(btrim(COALESCE(p."modelNumber", '')), '') IS NOT NULL
THEN upper(btrim(p."modelNumber"))
ELSE NULL
END AS product_business_key,
CASE
WHEN NULLIF(btrim(COALESCE(sku."skuCode", '')), '') IS NOT NULL
THEN upper(btrim(sku."skuCode"))
ELSE NULL
END AS sku_business_key,
CASE
WHEN NULLIF(btrim(COALESCE(profile."barcode", '')), '') IS NOT NULL
THEN upper(btrim(profile."barcode"))
ELSE NULL
END AS barcode_business_key
FROM "ProductSKU" sku
JOIN "Product" p ON p."id" = sku."productId"
LEFT JOIN "ProductSkuAssetProfile" profile ON profile."skuId" = sku."id"
LEFT JOIN "ProductCategory" pc ON pc."id" = p."categoryId"
LEFT JOIN "WarehouseLocation" wh ON wh."id" = sku."warehouseLocationId"
WHERE p."deletedAt" IS NULL
AND sku."deletedAt" IS NULL;
CREATE OR REPLACE VIEW external.product_assets_v1 AS
WITH image_assets AS (
SELECT
sku."id" AS sku_id,
sku."productId" AS product_id,
p."modelNumber" AS product_model_number,
sku."skuCode" AS sku_code,
profile."barcode" AS barcode,
image_item.image_url,
image_item.sort_order
FROM "ProductSKU" sku
JOIN "Product" p ON p."id" = sku."productId"
LEFT JOIN "ProductSkuAssetProfile" profile ON profile."skuId" = sku."id"
CROSS JOIN LATERAL unnest(sku."images") WITH ORDINALITY AS image_item(image_url, sort_order)
WHERE p."deletedAt" IS NULL
AND sku."deletedAt" IS NULL
),
model_assets AS (
SELECT
sku."id" AS sku_id,
sku."productId" AS product_id,
p."modelNumber" AS product_model_number,
sku."skuCode" AS sku_code,
profile."barcode" AS barcode,
COALESCE(NULLIF(sku."modelUrl", ''), NULLIF(p."modelUrl", '')) AS model_url
FROM "ProductSKU" sku
JOIN "Product" p ON p."id" = sku."productId"
LEFT JOIN "ProductSkuAssetProfile" profile ON profile."skuId" = sku."id"
WHERE p."deletedAt" IS NULL
AND sku."deletedAt" IS NULL
),
thumbnail_assets AS (
SELECT
sku."id" AS sku_id,
sku."productId" AS product_id,
p."modelNumber" AS product_model_number,
sku."skuCode" AS sku_code,
profile."barcode" AS barcode,
COALESCE(
NULLIF(profile."imageUrl", ''),
NULLIF(p."thumbnailUrl", '')
) AS thumbnail_url
FROM "ProductSKU" sku
JOIN "Product" p ON p."id" = sku."productId"
LEFT JOIN "ProductSkuAssetProfile" profile ON profile."skuId" = sku."id"
WHERE p."deletedAt" IS NULL
AND sku."deletedAt" IS NULL
)
SELECT
'SKU_IMAGE' AS asset_type,
product_id,
sku_id,
image_url AS asset_url,
NULL::text AS file_name,
NULL::text AS mime_type,
sort_order::integer AS sort_order,
CASE
WHEN NULLIF(btrim(COALESCE(product_model_number, '')), '') IS NOT NULL
THEN upper(btrim(product_model_number))
ELSE NULL
END AS product_business_key,
CASE
WHEN NULLIF(btrim(COALESCE(sku_code, '')), '') IS NOT NULL
THEN upper(btrim(sku_code))
ELSE NULL
END AS sku_business_key,
CASE
WHEN NULLIF(btrim(COALESCE(barcode, '')), '') IS NOT NULL
THEN upper(btrim(barcode))
ELSE NULL
END AS barcode_business_key
FROM image_assets
WHERE NULLIF(image_url, '') IS NOT NULL
UNION ALL
SELECT
'SKU_MODEL' AS asset_type,
product_id,
sku_id,
model_url AS asset_url,
NULL::text AS file_name,
NULL::text AS mime_type,
0 AS sort_order,
CASE
WHEN NULLIF(btrim(COALESCE(product_model_number, '')), '') IS NOT NULL
THEN upper(btrim(product_model_number))
ELSE NULL
END AS product_business_key,
CASE
WHEN NULLIF(btrim(COALESCE(sku_code, '')), '') IS NOT NULL
THEN upper(btrim(sku_code))
ELSE NULL
END AS sku_business_key,
CASE
WHEN NULLIF(btrim(COALESCE(barcode, '')), '') IS NOT NULL
THEN upper(btrim(barcode))
ELSE NULL
END AS barcode_business_key
FROM model_assets
WHERE NULLIF(model_url, '') IS NOT NULL
UNION ALL
SELECT
'SKU_THUMBNAIL' AS asset_type,
product_id,
sku_id,
thumbnail_url AS asset_url,
NULL::text AS file_name,
NULL::text AS mime_type,
0 AS sort_order,
CASE
WHEN NULLIF(btrim(COALESCE(product_model_number, '')), '') IS NOT NULL
THEN upper(btrim(product_model_number))
ELSE NULL
END AS product_business_key,
CASE
WHEN NULLIF(btrim(COALESCE(sku_code, '')), '') IS NOT NULL
THEN upper(btrim(sku_code))
ELSE NULL
END AS sku_business_key,
CASE
WHEN NULLIF(btrim(COALESCE(barcode, '')), '') IS NOT NULL
THEN upper(btrim(barcode))
ELSE NULL
END AS barcode_business_key
FROM thumbnail_assets
WHERE NULLIF(thumbnail_url, '') IS NOT NULL;
CREATE OR REPLACE VIEW external.product_bom_v1 AS
SELECT
'PANEL' AS item_type,
p."productId" AS product_id,
p."skuId" AS sku_id,
p."id" AS item_id,
p."code" AS item_code,
p."name" AS item_name,
1::numeric AS quantity,
'piece' AS unit,
m."id" AS material_id,
m."code" AS material_code,
m."name" AS material_name,
jsonb_build_object(
'length', p."length",
'width', p."width",
'thickness', p."thickness",
'unit', 'mm'
) AS dimensions,
NULL::numeric AS unit_cost,
NULL::numeric AS total_cost,
p."createdAt" AS created_at,
p."updatedAt" AS updated_at,
CASE
WHEN NULLIF(btrim(COALESCE(product."modelNumber", '')), '') IS NOT NULL
THEN upper(btrim(product."modelNumber"))
ELSE NULL
END AS product_business_key,
CASE
WHEN NULLIF(btrim(COALESCE(sku."skuCode", '')), '') IS NOT NULL
THEN upper(btrim(sku."skuCode"))
ELSE NULL
END AS sku_business_key,
CASE
WHEN NULLIF(btrim(COALESCE(profile."barcode", '')), '') IS NOT NULL
THEN upper(btrim(profile."barcode"))
ELSE NULL
END AS barcode_business_key
FROM "Panel" p
JOIN "Product" product ON product."id" = p."productId"
LEFT JOIN "ProductSKU" sku ON sku."id" = p."skuId"
LEFT JOIN "ProductSkuAssetProfile" profile ON profile."skuId" = sku."id"
LEFT JOIN "PanelMaterial" pm ON pm."panelId" = p."id"
LEFT JOIN "Material" m ON m."id" = pm."materialId"
WHERE product."deletedAt" IS NULL
AND (sku."id" IS NULL OR sku."deletedAt" IS NULL)
UNION ALL
SELECT
'HARDWARE' AS item_type,
ph."productId" AS product_id,
ph."skuId" AS sku_id,
ph."id" AS item_id,
NULLIF(btrim(COALESCE(h."code", ph."sourceBlockName", '')), '') AS item_code,
COALESCE(h."name", ph."sourceBlockName", '未匹配五金') AS item_name,
1::numeric AS quantity,
'piece' AS unit,
NULL::text AS material_id,
NULL::text AS material_code,
NULL::text AS material_name,
h."dimensions" AS dimensions,
NULL::numeric AS unit_cost,
NULL::numeric AS total_cost,
ph."createdAt" AS created_at,
ph."updatedAt" AS updated_at,
CASE
WHEN NULLIF(btrim(COALESCE(product."modelNumber", '')), '') IS NOT NULL
THEN upper(btrim(product."modelNumber"))
ELSE NULL
END AS product_business_key,
CASE
WHEN NULLIF(btrim(COALESCE(sku."skuCode", '')), '') IS NOT NULL
THEN upper(btrim(sku."skuCode"))
ELSE NULL
END AS sku_business_key,
CASE
WHEN NULLIF(btrim(COALESCE(profile."barcode", '')), '') IS NOT NULL
THEN upper(btrim(profile."barcode"))
ELSE NULL
END AS barcode_business_key
FROM "ProductHardware" ph
JOIN "Product" product ON product."id" = ph."productId"
LEFT JOIN "ProductSKU" sku ON sku."id" = ph."skuId"
LEFT JOIN "ProductSkuAssetProfile" profile ON profile."skuId" = sku."id"
LEFT JOIN "Hardware" h ON h."id" = ph."hardwareId"
WHERE product."deletedAt" IS NULL
AND (sku."id" IS NULL OR sku."deletedAt" IS NULL);
CREATE OR REPLACE VIEW external.sku_warehouse_stock_v1 AS
SELECT
sku.product_id,
sku.product_model_number,
sku.product_business_key,
sku.product_name,
sku.product_category_id,
sku.product_category_name,
sku.sku_id,
sku.sku_code,
sku.sku_business_key,
sku.sku_name,
sku.barcode,
sku.stock AS stock_quantity,
sku.is_active AS sku_is_active,
sku.record_status AS sku_record_status,
sku.warehouse_location_id,
sku.warehouse_location_code,
sku.warehouse_location_name,
wh.zone AS warehouse_zone,
wh.aisle AS warehouse_aisle,
wh.shelf AS warehouse_shelf,
wh.bin AS warehouse_bin,
wh.is_active AS warehouse_location_is_active,
wh.record_status AS warehouse_location_status,
sku.created_at,
sku.updated_at,
sku.barcode_business_key
FROM external.product_skus_v1 sku
LEFT JOIN external.warehouse_locations_v1 wh
ON wh.warehouse_location_id = sku.warehouse_location_id;
CREATE OR REPLACE VIEW external.packaged_items_v1 AS
SELECT
item."id" AS packaged_item_id,
item."code" AS packaged_item_code,
item."name" AS packaged_item_name,
CASE
WHEN NULLIF(btrim(COALESCE(product."modelNumber", '')), '') IS NOT NULL
THEN upper(btrim(product."modelNumber"))
ELSE NULL
END AS product_business_key,
CASE
WHEN NULLIF(btrim(COALESCE(sku."skuCode", '')), '') IS NOT NULL
THEN upper(btrim(sku."skuCode"))
ELSE NULL
END AS sku_business_key,
product."id" AS product_id,
product."modelNumber" AS product_model_number,
product."name" AS product_name,
sku."id" AS sku_id,
sku."skuCode" AS sku_code,
COALESCE(
NULLIF(sku."name", ''),
NULLIF(profile."specName", ''),
sku."skuCode"
) AS sku_name,
profile."barcode" AS barcode,
item."quantityPerPack" AS quantity_per_pack,
item."stockQuantity" AS stock_quantity,
item."length" AS length,
item."width" AS width,
item."height" AS height,
item."weight" AS weight,
item."status" AS status,
item."warehouseLocationId" AS warehouse_location_id,
wh."code" AS warehouse_location_code,
wh."name" AS warehouse_location_name,
wh."zone" AS warehouse_zone,
wh."aisle" AS warehouse_aisle,
wh."shelf" AS warehouse_shelf,
wh."bin" AS warehouse_bin,
wh."isActive" AS warehouse_location_is_active,
item."createdAt" AS created_at,
item."updatedAt" AS updated_at,
CASE
WHEN NULLIF(btrim(COALESCE(profile."barcode", '')), '') IS NOT NULL THEN upper(btrim(profile."barcode"))
ELSE NULL
END AS barcode_business_key
FROM "PackagedItem" item
LEFT JOIN "ProductSKU" sku
ON sku."id" = item."skuId"
AND sku."deletedAt" IS NULL
LEFT JOIN "Product" product
ON product."id" = sku."productId"
AND product."deletedAt" IS NULL
LEFT JOIN "ProductSkuAssetProfile" profile
ON profile."skuId" = sku."id"
LEFT JOIN "WarehouseLocation" wh
ON wh."id" = item."warehouseLocationId"
WHERE sku."id" IS NOT NULL
AND product."id" IS NOT NULL;
CREATE OR REPLACE VIEW external.sku_quality_inspections_v1 AS
SELECT
inspection."id" AS quality_inspection_id,
inspection."recordKey" AS record_key,
inspection."sourceType" AS source_type,
CASE
WHEN inspection."sourceType" = 'FIRST_BATCH' THEN '首批质检'
ELSE '仓库质检'
END AS source_label,
inspection."sourceProductId" AS source_product_id,
inspection."sourceProductSpu" AS source_product_spu,
inspection."sourceSkuItemId" AS source_sku_item_id,
inspection."batchNo" AS batch_no,
inspection."inspectedAt" AS inspected_at,
inspection."inspectorName" AS inspector_name,
inspection."conclusion" AS conclusion,
inspection."detail" AS detail,
inspection."inspectedQuantity" AS inspected_quantity,
inspection."qualifiedQuantity" AS qualified_quantity,
inspection."qualifiedRate" AS qualified_rate,
inspection."status" AS status,
sku.product_id,
sku.product_model_number,
sku.product_business_key,
sku.product_name,
sku.product_category_id,
sku.product_category_name,
sku.sku_id,
sku.sku_code,
sku.sku_business_key,
sku.sku_name,
sku.barcode,
sku.warehouse_location_id,
sku.warehouse_location_code,
sku.warehouse_location_name,
inspection."createdAt" AS created_at,
inspection."updatedAt" AS updated_at,
sku.barcode_business_key
FROM "ProductSkuQualityInspection" inspection
JOIN external.product_skus_v1 sku
ON sku.sku_id = inspection."skuId";
DROP VIEW IF EXISTS external.product_catalog_wide_v1;
CREATE VIEW external.product_catalog_wide_v1 AS
WITH sku_source AS (
SELECT
sku.*
FROM external.product_skus_v1 sku
),
asset_summary AS (
SELECT
product_business_key,
sku_business_key,
COUNT(*) FILTER (WHERE asset_type = 'SKU_IMAGE')::integer AS image_asset_count,
COUNT(*) FILTER (WHERE asset_type = 'SKU_MODEL')::integer AS model_asset_count,
COUNT(*) FILTER (WHERE asset_type = 'SKU_THUMBNAIL')::integer AS thumbnail_asset_count,
MIN(asset_url) FILTER (WHERE asset_type = 'SKU_MODEL') AS first_model_asset_url,
MIN(asset_url) FILTER (WHERE asset_type = 'SKU_THUMBNAIL') AS first_thumbnail_asset_url
FROM external.product_assets_v1
GROUP BY product_business_key, sku_business_key
),
packaged_summary AS (
SELECT
barcode_business_key,
COUNT(*)::integer AS packaged_item_count,
COALESCE(SUM(stock_quantity), 0)::integer AS packaged_stock_quantity
FROM external.packaged_items_v1
WHERE NULLIF(barcode_business_key, '') IS NOT NULL
GROUP BY barcode_business_key
),
quality_summary AS (
SELECT
barcode_business_key,
COUNT(*)::integer AS quality_record_count,
COUNT(*) FILTER (WHERE source_type = 'WAREHOUSE')::integer AS warehouse_quality_record_count,
COUNT(*) FILTER (WHERE source_type = 'FIRST_BATCH')::integer AS first_batch_quality_record_count
FROM external.sku_quality_inspections_v1
WHERE NULLIF(barcode_business_key, '') IS NOT NULL
GROUP BY barcode_business_key
),
latest_quality AS (
SELECT DISTINCT ON (barcode_business_key)
barcode_business_key,
record_key AS latest_quality_record_key,
source_type AS latest_quality_source_type,
batch_no AS latest_quality_batch_no,
inspected_at AS latest_quality_inspected_at,
conclusion AS latest_quality_conclusion,
status AS latest_quality_status
FROM external.sku_quality_inspections_v1
WHERE NULLIF(barcode_business_key, '') IS NOT NULL
ORDER BY barcode_business_key, COALESCE(inspected_at, updated_at, created_at) DESC, created_at DESC
),
limit_by_barcode AS (
SELECT DISTINCT ON (identity_value)
identity_value,
product_business_key AS limit_source_product_business_key,
sku_business_key AS limit_source_sku_business_key,
limit_price_applicable,
limit_price_confirmed,
limit_price_status,
expected_daily_limit_price,
expected_limit_gross_margin_rate,
non_spu_owner_limit_price,
limit_confirmed_at
FROM (
SELECT
upper(btrim(barcode)) AS identity_value,
product_business_key,
sku_business_key,
limit_price_applicable,
limit_price_confirmed,
limit_price_status,
expected_daily_limit_price,
expected_limit_gross_margin_rate,
non_spu_owner_limit_price,
limit_confirmed_at,
updated_at
FROM external.intake_product_limits_v1
WHERE NULLIF(btrim(COALESCE(barcode, '')), '') IS NOT NULL
) limit_rows
WHERE NULLIF(identity_value, '') IS NOT NULL
ORDER BY
identity_value,
limit_price_applicable DESC,
limit_price_confirmed DESC,
limit_confirmed_at DESC NULLS LAST,
updated_at DESC
),
limit_by_sku AS (
SELECT DISTINCT ON (identity_value)
identity_value,
product_business_key AS limit_source_product_business_key,
sku_business_key AS limit_source_sku_business_key,
limit_price_applicable,
limit_price_confirmed,
limit_price_status,
expected_daily_limit_price,
expected_limit_gross_margin_rate,
non_spu_owner_limit_price,
limit_confirmed_at
FROM (
SELECT
upper(btrim(sku_code)) AS identity_value,
product_business_key,
sku_business_key,
limit_price_applicable,
limit_price_confirmed,
limit_price_status,
expected_daily_limit_price,
expected_limit_gross_margin_rate,
non_spu_owner_limit_price,
limit_confirmed_at,
updated_at
FROM external.intake_product_limits_v1
WHERE NULLIF(btrim(COALESCE(sku_code, '')), '') IS NOT NULL
) limit_rows
WHERE NULLIF(identity_value, '') IS NOT NULL
ORDER BY
identity_value,
limit_price_applicable DESC,
limit_price_confirmed DESC,
limit_confirmed_at DESC NULLS LAST,
updated_at DESC
)
SELECT
sku.product_id,
sku.product_model_number,
sku.product_business_key,
sku.product_name,
sku.product_description,
sku.product_category_id,
sku.product_category_name,
sku.product_thumbnail_url,
sku.product_model_url,
sku.sku_id,
sku.sku_code,
sku.sku_business_key,
sku.sku_name,
sku.price,
sku.cost_price,
sku.stock,
sku.is_active,
sku.record_status,
sku.model_url,
sku.image_url,
sku.image_urls,
sku.source_pre_sku_id,
sku.source_asset_variant_id,
sku.source_asset_variant_ids,
sku.asset_family_code,
sku.asset_family_name,
sku.spec_code,
sku.spec_name,
sku.barcode,
sku.barcode_business_key,
sku.size_json,
sku.color,
sku.material,
sku.source_status,
sku.synced_at,
sku.warehouse_location_id,
sku.warehouse_location_code,
sku.warehouse_location_name,
stock.warehouse_zone,
stock.warehouse_aisle,
stock.warehouse_shelf,
stock.warehouse_bin,
stock.warehouse_location_is_active,
stock.warehouse_location_status,
COALESCE(limit_by_barcode.limit_price_applicable, limit_by_sku.limit_price_applicable, false) AS limit_price_applicable,
COALESCE(limit_by_barcode.limit_price_confirmed, limit_by_sku.limit_price_confirmed, false) AS limit_price_confirmed,
COALESCE(limit_by_barcode.limit_price_status, limit_by_sku.limit_price_status, 'NOT_APPLICABLE') AS limit_price_status,
COALESCE(limit_by_barcode.expected_daily_limit_price, limit_by_sku.expected_daily_limit_price) AS expected_daily_limit_price,
COALESCE(limit_by_barcode.expected_limit_gross_margin_rate, limit_by_sku.expected_limit_gross_margin_rate) AS expected_limit_gross_margin_rate,
COALESCE(limit_by_barcode.non_spu_owner_limit_price, limit_by_sku.non_spu_owner_limit_price) AS non_spu_owner_limit_price,
COALESCE(limit_by_barcode.limit_confirmed_at, limit_by_sku.limit_confirmed_at) AS limit_confirmed_at,
COALESCE(limit_by_barcode.limit_source_product_business_key, limit_by_sku.limit_source_product_business_key) AS limit_source_product_business_key,
COALESCE(limit_by_barcode.limit_source_sku_business_key, limit_by_sku.limit_source_sku_business_key) AS limit_source_sku_business_key,
COALESCE(asset_summary.image_asset_count, 0) AS image_asset_count,
COALESCE(asset_summary.model_asset_count, 0) AS model_asset_count,
COALESCE(asset_summary.thumbnail_asset_count, 0) AS thumbnail_asset_count,
asset_summary.first_model_asset_url,
asset_summary.first_thumbnail_asset_url,
COALESCE(bom_summary.bom_item_count, 0) AS bom_item_count,
COALESCE(bom_summary.panel_item_count, 0) AS panel_item_count,
COALESCE(bom_summary.hardware_item_count, 0) AS hardware_item_count,
COALESCE(bom_summary.material_codes, ARRAY[]::text[]) AS material_codes,
COALESCE(bom_summary.material_names, ARRAY[]::text[]) AS material_names,
COALESCE(packaged_summary.packaged_item_count, 0) AS packaged_item_count,
COALESCE(packaged_summary.packaged_stock_quantity, 0) AS packaged_stock_quantity,
COALESCE(quality_summary.quality_record_count, 0) AS quality_record_count,
COALESCE(quality_summary.warehouse_quality_record_count, 0) AS warehouse_quality_record_count,
COALESCE(quality_summary.first_batch_quality_record_count, 0) AS first_batch_quality_record_count,
latest_quality.latest_quality_record_key,
latest_quality.latest_quality_source_type,
latest_quality.latest_quality_batch_no,
latest_quality.latest_quality_inspected_at,
latest_quality.latest_quality_conclusion,
latest_quality.latest_quality_status,
sku.created_at,
sku.updated_at
FROM sku_source sku
LEFT JOIN external.sku_warehouse_stock_v1 stock
ON stock.barcode_business_key = sku.barcode_business_key
LEFT JOIN limit_by_sku
ON limit_by_sku.identity_value = upper(btrim(COALESCE(sku.sku_code, '')))
LEFT JOIN limit_by_barcode
ON limit_by_barcode.identity_value = sku.barcode_business_key
LEFT JOIN asset_summary
ON asset_summary.product_business_key = sku.product_business_key
AND asset_summary.sku_business_key = sku.sku_business_key
LEFT JOIN LATERAL (
SELECT
COUNT(*)::integer AS bom_item_count,
COUNT(*) FILTER (WHERE item_type = 'PANEL')::integer AS panel_item_count,
COUNT(*) FILTER (WHERE item_type = 'HARDWARE')::integer AS hardware_item_count,
array_remove(array_agg(DISTINCT material_code ORDER BY material_code), NULL) AS material_codes,
array_remove(array_agg(DISTINCT material_name ORDER BY material_name), NULL) AS material_names
FROM external.product_bom_v1 bom
WHERE bom.product_business_key = sku.product_business_key
AND (
bom.sku_business_key = sku.sku_business_key
OR bom.sku_business_key IS NULL
)
) bom_summary ON true
LEFT JOIN packaged_summary
ON packaged_summary.barcode_business_key = sku.barcode_business_key
LEFT JOIN quality_summary
ON quality_summary.barcode_business_key = sku.barcode_business_key
LEFT JOIN latest_quality
ON latest_quality.barcode_business_key = sku.barcode_business_key;
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'external_reader') THEN
GRANT USAGE ON SCHEMA external TO external_reader;
GRANT SELECT ON external.product_catalog_wide_v1 TO external_reader;
GRANT SELECT ON external.intake_product_limits_v1 TO external_reader;
END IF;
END
$$;

View File

@ -0,0 +1 @@
ALTER TYPE "BusinessAccountType" ADD VALUE IF NOT EXISTS 'EXTERNAL_INTAKE';

View File

@ -0,0 +1,421 @@
-- Expand the external product catalog wide view so external systems can
-- consume ready non-integrated SKU rows before formal Product / ProductSKU
-- records are materialized.
CREATE OR REPLACE VIEW external.product_catalog_wide_v1 AS
WITH formal_sku_source AS (
SELECT
sku.product_id,
sku.product_model_number,
sku.product_name,
sku.product_description,
sku.product_category_id,
sku.product_category_name,
sku.product_thumbnail_url,
sku.product_model_url,
sku.sku_id,
sku.sku_code,
sku.sku_name,
sku.price,
sku.cost_price,
sku.stock,
sku.is_active,
sku.record_status,
sku.model_url,
sku.image_url,
sku.image_urls,
sku.source_pre_sku_id,
sku.source_asset_variant_id,
sku.source_asset_variant_ids,
sku.asset_family_code,
sku.asset_family_name,
sku.spec_code,
sku.spec_name,
sku.barcode,
sku.size_json,
sku.color,
sku.material,
sku.source_status,
sku.synced_at,
sku.warehouse_location_id,
sku.warehouse_location_code,
sku.warehouse_location_name,
sku.created_at,
sku.updated_at,
sku.product_business_key,
sku.sku_business_key,
sku.barcode_business_key,
'FORMAL'::text AS catalog_source
FROM external.product_skus_v1 sku
),
formal_identity_map AS (
SELECT
barcode_business_key,
CASE
WHEN NULLIF(btrim(COALESCE(sku_code, '')), '') IS NOT NULL
THEN upper(btrim(sku_code))
ELSE NULL
END AS normalized_sku_code
FROM formal_sku_source
),
intake_ready_sku_source AS (
SELECT
product.intake_product_id AS product_id,
NULLIF(btrim(COALESCE(product.spu, '')), '') AS product_model_number,
NULLIF(btrim(COALESCE(product.product_name, '')), '') AS product_name,
NULL::text AS product_description,
NULL::text AS product_category_id,
NULLIF(btrim(COALESCE(product.product_category, '')), '') AS product_category_name,
COALESCE(
NULLIF(btrim(COALESCE(product.product_image_url, '')), ''),
NULLIF(btrim(COALESCE(sku.image_url, '')), '')
) AS product_thumbnail_url,
NULL::text AS product_model_url,
sku.intake_sku_item_id AS sku_id,
NULLIF(btrim(COALESCE(sku.sku_code, '')), '') AS sku_code,
COALESCE(
NULLIF(btrim(COALESCE(sku.spec_name, '')), ''),
NULLIF(btrim(COALESCE(sku.sku_code, '')), '')
) AS sku_name,
NULL::numeric(10,2) AS price,
NULL::numeric(10,2) AS cost_price,
0::integer AS stock,
true AS is_active,
'ACTIVE'::text AS record_status,
NULLIF(btrim(COALESCE(sku.model_3d_url, '')), '') AS model_url,
COALESCE(
NULLIF(btrim(COALESCE(sku.image_url, '')), ''),
NULLIF(btrim(COALESCE(product.product_image_url, '')), '')
) AS image_url,
CASE
WHEN COALESCE(
NULLIF(btrim(COALESCE(sku.image_url, '')), ''),
NULLIF(btrim(COALESCE(product.product_image_url, '')), '')
) IS NOT NULL
THEN ARRAY[
COALESCE(
NULLIF(btrim(COALESCE(sku.image_url, '')), ''),
NULLIF(btrim(COALESCE(product.product_image_url, '')), '')
)
]
ELSE ARRAY[]::text[]
END AS image_urls,
NULL::text AS source_pre_sku_id,
NULL::text AS source_asset_variant_id,
ARRAY[]::text[] AS source_asset_variant_ids,
NULLIF(btrim(COALESCE(sku.model, '')), '') AS asset_family_code,
NULL::text AS asset_family_name,
NULLIF(btrim(COALESCE(sku.attribute_code, '')), '') AS spec_code,
NULLIF(btrim(COALESCE(sku.spec_name, '')), '') AS spec_name,
NULLIF(btrim(COALESCE(sku.barcode, '')), '') AS barcode,
CASE
WHEN NULLIF(btrim(COALESCE(sku.size, '')), '') IS NOT NULL
THEN jsonb_build_object('label', btrim(sku.size))
ELSE NULL::jsonb
END AS size_json,
NULLIF(btrim(COALESCE(sku.color, '')), '') AS color,
NULLIF(btrim(COALESCE(sku.material, '')), '') AS material,
NULL::text AS source_status,
NULL::text AS synced_at,
NULL::text AS warehouse_location_id,
NULL::text AS warehouse_location_code,
NULL::text AS warehouse_location_name,
product.created_at AS created_at,
product.updated_at AS updated_at,
CASE
WHEN NULLIF(btrim(COALESCE(product.spu, '')), '') IS NOT NULL
THEN upper(btrim(product.spu))
ELSE NULL
END AS product_business_key,
CASE
WHEN NULLIF(btrim(COALESCE(sku.sku_code, '')), '') IS NOT NULL
THEN upper(btrim(sku.sku_code))
ELSE NULL
END AS sku_business_key,
CASE
WHEN NULLIF(btrim(COALESCE(sku.barcode, '')), '') IS NOT NULL
THEN upper(btrim(sku.barcode))
ELSE NULL
END AS barcode_business_key,
'INTAKE_READY'::text AS catalog_source
FROM external.intake_sku_items_ready_v1 sku
JOIN external.intake_products_ready_v1 product
ON product.intake_product_id = sku.intake_product_id
WHERE NOT EXISTS (
SELECT 1
FROM formal_identity_map formal
WHERE (
NULLIF(btrim(COALESCE(sku.barcode, '')), '') IS NOT NULL
AND formal.barcode_business_key = upper(btrim(sku.barcode))
)
OR (
NULLIF(btrim(COALESCE(sku.barcode, '')), '') IS NULL
AND NULLIF(btrim(COALESCE(sku.sku_code, '')), '') IS NOT NULL
AND formal.normalized_sku_code = upper(btrim(sku.sku_code))
)
)
),
sku_source AS (
SELECT * FROM formal_sku_source
UNION ALL
SELECT * FROM intake_ready_sku_source
),
asset_summary AS (
SELECT
product_business_key,
sku_business_key,
COUNT(*) FILTER (WHERE asset_type = 'SKU_IMAGE')::integer AS image_asset_count,
COUNT(*) FILTER (WHERE asset_type = 'SKU_MODEL')::integer AS model_asset_count,
COUNT(*) FILTER (WHERE asset_type = 'SKU_THUMBNAIL')::integer AS thumbnail_asset_count,
MIN(asset_url) FILTER (WHERE asset_type = 'SKU_MODEL') AS first_model_asset_url,
MIN(asset_url) FILTER (WHERE asset_type = 'SKU_THUMBNAIL') AS first_thumbnail_asset_url
FROM external.product_assets_v1
GROUP BY product_business_key, sku_business_key
),
packaged_summary AS (
SELECT
barcode_business_key,
COUNT(*)::integer AS packaged_item_count,
COALESCE(SUM(stock_quantity), 0)::integer AS packaged_stock_quantity
FROM external.packaged_items_v1
WHERE NULLIF(barcode_business_key, '') IS NOT NULL
GROUP BY barcode_business_key
),
quality_summary AS (
SELECT
barcode_business_key,
COUNT(*)::integer AS quality_record_count,
COUNT(*) FILTER (WHERE source_type = 'WAREHOUSE')::integer AS warehouse_quality_record_count,
COUNT(*) FILTER (WHERE source_type = 'FIRST_BATCH')::integer AS first_batch_quality_record_count
FROM external.sku_quality_inspections_v1
WHERE NULLIF(barcode_business_key, '') IS NOT NULL
GROUP BY barcode_business_key
),
latest_quality AS (
SELECT DISTINCT ON (barcode_business_key)
barcode_business_key,
record_key AS latest_quality_record_key,
source_type AS latest_quality_source_type,
batch_no AS latest_quality_batch_no,
inspected_at AS latest_quality_inspected_at,
conclusion AS latest_quality_conclusion,
status AS latest_quality_status
FROM external.sku_quality_inspections_v1
WHERE NULLIF(barcode_business_key, '') IS NOT NULL
ORDER BY barcode_business_key, COALESCE(inspected_at, updated_at, created_at) DESC, created_at DESC
),
limit_by_barcode AS (
SELECT DISTINCT ON (identity_value)
identity_value,
product_business_key AS limit_source_product_business_key,
sku_business_key AS limit_source_sku_business_key,
limit_price_applicable,
limit_price_confirmed,
limit_price_status,
expected_daily_limit_price,
expected_limit_gross_margin_rate,
non_spu_owner_limit_price,
limit_confirmed_at
FROM (
SELECT
upper(btrim(barcode)) AS identity_value,
product_business_key,
sku_business_key,
limit_price_applicable,
limit_price_confirmed,
limit_price_status,
expected_daily_limit_price,
expected_limit_gross_margin_rate,
non_spu_owner_limit_price,
limit_confirmed_at,
updated_at
FROM external.intake_product_limits_v1
WHERE NULLIF(btrim(COALESCE(barcode, '')), '') IS NOT NULL
) limit_rows
WHERE NULLIF(identity_value, '') IS NOT NULL
ORDER BY
identity_value,
limit_price_applicable DESC,
limit_price_confirmed DESC,
limit_confirmed_at DESC NULLS LAST,
updated_at DESC
),
limit_by_sku AS (
SELECT DISTINCT ON (identity_value)
identity_value,
product_business_key AS limit_source_product_business_key,
sku_business_key AS limit_source_sku_business_key,
limit_price_applicable,
limit_price_confirmed,
limit_price_status,
expected_daily_limit_price,
expected_limit_gross_margin_rate,
non_spu_owner_limit_price,
limit_confirmed_at
FROM (
SELECT
upper(btrim(sku_code)) AS identity_value,
product_business_key,
sku_business_key,
limit_price_applicable,
limit_price_confirmed,
limit_price_status,
expected_daily_limit_price,
expected_limit_gross_margin_rate,
non_spu_owner_limit_price,
limit_confirmed_at,
updated_at
FROM external.intake_product_limits_v1
WHERE NULLIF(btrim(COALESCE(sku_code, '')), '') IS NOT NULL
) limit_rows
WHERE NULLIF(identity_value, '') IS NOT NULL
ORDER BY
identity_value,
limit_price_applicable DESC,
limit_price_confirmed DESC,
limit_confirmed_at DESC NULLS LAST,
updated_at DESC
)
SELECT
sku.product_id,
sku.product_model_number,
sku.product_business_key,
sku.product_name,
sku.product_description,
sku.product_category_id,
sku.product_category_name,
sku.product_thumbnail_url,
sku.product_model_url,
sku.sku_id,
sku.sku_code,
sku.sku_business_key,
sku.sku_name,
sku.price,
sku.cost_price,
sku.stock,
sku.is_active,
sku.record_status,
sku.model_url,
sku.image_url,
sku.image_urls,
sku.source_pre_sku_id,
sku.source_asset_variant_id,
sku.source_asset_variant_ids,
sku.asset_family_code,
sku.asset_family_name,
sku.spec_code,
sku.spec_name,
sku.barcode,
sku.barcode_business_key,
sku.size_json,
sku.color,
sku.material,
sku.source_status,
sku.synced_at,
sku.warehouse_location_id,
sku.warehouse_location_code,
sku.warehouse_location_name,
stock.warehouse_zone,
stock.warehouse_aisle,
stock.warehouse_shelf,
stock.warehouse_bin,
stock.warehouse_location_is_active,
stock.warehouse_location_status,
COALESCE(limit_by_barcode.limit_price_applicable, limit_by_sku.limit_price_applicable, false) AS limit_price_applicable,
COALESCE(limit_by_barcode.limit_price_confirmed, limit_by_sku.limit_price_confirmed, false) AS limit_price_confirmed,
COALESCE(limit_by_barcode.limit_price_status, limit_by_sku.limit_price_status, 'NOT_APPLICABLE') AS limit_price_status,
COALESCE(limit_by_barcode.expected_daily_limit_price, limit_by_sku.expected_daily_limit_price) AS expected_daily_limit_price,
COALESCE(limit_by_barcode.expected_limit_gross_margin_rate, limit_by_sku.expected_limit_gross_margin_rate) AS expected_limit_gross_margin_rate,
COALESCE(limit_by_barcode.non_spu_owner_limit_price, limit_by_sku.non_spu_owner_limit_price) AS non_spu_owner_limit_price,
COALESCE(limit_by_barcode.limit_confirmed_at, limit_by_sku.limit_confirmed_at) AS limit_confirmed_at,
COALESCE(limit_by_barcode.limit_source_product_business_key, limit_by_sku.limit_source_product_business_key) AS limit_source_product_business_key,
COALESCE(limit_by_barcode.limit_source_sku_business_key, limit_by_sku.limit_source_sku_business_key) AS limit_source_sku_business_key,
CASE
WHEN sku.catalog_source = 'INTAKE_READY'
THEN CASE WHEN NULLIF(btrim(COALESCE(sku.image_url, '')), '') IS NOT NULL THEN 1 ELSE 0 END
ELSE COALESCE(asset_summary.image_asset_count, 0)
END AS image_asset_count,
CASE
WHEN sku.catalog_source = 'INTAKE_READY'
THEN CASE WHEN NULLIF(btrim(COALESCE(sku.model_url, '')), '') IS NOT NULL THEN 1 ELSE 0 END
ELSE COALESCE(asset_summary.model_asset_count, 0)
END AS model_asset_count,
CASE
WHEN sku.catalog_source = 'INTAKE_READY'
THEN CASE
WHEN COALESCE(
NULLIF(btrim(COALESCE(sku.product_thumbnail_url, '')), ''),
NULLIF(btrim(COALESCE(sku.image_url, '')), '')
) IS NOT NULL
THEN 1
ELSE 0
END
ELSE COALESCE(asset_summary.thumbnail_asset_count, 0)
END AS thumbnail_asset_count,
COALESCE(
asset_summary.first_model_asset_url,
CASE
WHEN sku.catalog_source = 'INTAKE_READY'
THEN NULLIF(btrim(COALESCE(sku.model_url, '')), '')
ELSE NULL
END
) AS first_model_asset_url,
COALESCE(
asset_summary.first_thumbnail_asset_url,
CASE
WHEN sku.catalog_source = 'INTAKE_READY'
THEN COALESCE(
NULLIF(btrim(COALESCE(sku.product_thumbnail_url, '')), ''),
NULLIF(btrim(COALESCE(sku.image_url, '')), '')
)
ELSE NULL
END
) AS first_thumbnail_asset_url,
COALESCE(bom_summary.bom_item_count, 0) AS bom_item_count,
COALESCE(bom_summary.panel_item_count, 0) AS panel_item_count,
COALESCE(bom_summary.hardware_item_count, 0) AS hardware_item_count,
COALESCE(bom_summary.material_codes, ARRAY[]::text[]) AS material_codes,
COALESCE(bom_summary.material_names, ARRAY[]::text[]) AS material_names,
COALESCE(packaged_summary.packaged_item_count, 0) AS packaged_item_count,
COALESCE(packaged_summary.packaged_stock_quantity, 0) AS packaged_stock_quantity,
COALESCE(quality_summary.quality_record_count, 0) AS quality_record_count,
COALESCE(quality_summary.warehouse_quality_record_count, 0) AS warehouse_quality_record_count,
COALESCE(quality_summary.first_batch_quality_record_count, 0) AS first_batch_quality_record_count,
latest_quality.latest_quality_record_key,
latest_quality.latest_quality_source_type,
latest_quality.latest_quality_batch_no,
latest_quality.latest_quality_inspected_at,
latest_quality.latest_quality_conclusion,
latest_quality.latest_quality_status,
sku.created_at,
sku.updated_at
FROM sku_source sku
LEFT JOIN external.sku_warehouse_stock_v1 stock
ON stock.barcode_business_key = sku.barcode_business_key
LEFT JOIN limit_by_sku
ON limit_by_sku.identity_value = upper(btrim(COALESCE(sku.sku_code, '')))
LEFT JOIN limit_by_barcode
ON limit_by_barcode.identity_value = sku.barcode_business_key
LEFT JOIN asset_summary
ON asset_summary.product_business_key = sku.product_business_key
AND asset_summary.sku_business_key = sku.sku_business_key
LEFT JOIN LATERAL (
SELECT
COUNT(*)::integer AS bom_item_count,
COUNT(*) FILTER (WHERE item_type = 'PANEL')::integer AS panel_item_count,
COUNT(*) FILTER (WHERE item_type = 'HARDWARE')::integer AS hardware_item_count,
array_remove(array_agg(DISTINCT material_code ORDER BY material_code), NULL) AS material_codes,
array_remove(array_agg(DISTINCT material_name ORDER BY material_name), NULL) AS material_names
FROM external.product_bom_v1 bom
WHERE bom.product_business_key = sku.product_business_key
AND (
bom.sku_business_key = sku.sku_business_key
OR bom.sku_business_key IS NULL
)
) bom_summary ON true
LEFT JOIN packaged_summary
ON packaged_summary.barcode_business_key = sku.barcode_business_key
LEFT JOIN quality_summary
ON quality_summary.barcode_business_key = sku.barcode_business_key
LEFT JOIN latest_quality
ON latest_quality.barcode_business_key = sku.barcode_business_key;

View File

@ -0,0 +1,6 @@
DROP INDEX IF EXISTS "IntakeNonIntegratedProduct_spu_key";
DROP INDEX IF EXISTS "IntakeNonIntegratedProduct_spu_brand_key";
DROP INDEX IF EXISTS "IntakeNonIntegratedProduct_spu_brand_normalized_key";
CREATE INDEX IF NOT EXISTS "IntakeNonIntegratedProduct_spu_idx"
ON "IntakeNonIntegratedProduct"("spu");

View File

@ -0,0 +1,2 @@
-- Keep IntakeReferenceLibraryItem SPU_RULE rows as the only SPU truth source.
DROP TABLE IF EXISTS "SpuNumberCounter";

View File

@ -0,0 +1,175 @@
-- Restore the AssetSkuGroup schema expected by the current asset-center code.
-- The previous retirement migration preserved group data in JSONB legacy fields;
-- this migration restores the relational table/column and backfills from that data.
CREATE TABLE IF NOT EXISTS "AssetSkuGroup" (
"id" TEXT NOT NULL,
"assetFamilyId" TEXT NOT NULL,
"code" TEXT NOT NULL,
"name" TEXT NOT NULL,
"description" TEXT,
"sortOrder" INTEGER NOT NULL DEFAULT 0,
"metadata" JSONB,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "AssetSkuGroup_pkey" PRIMARY KEY ("id")
);
ALTER TABLE "AssetVariant"
ADD COLUMN IF NOT EXISTS "skuGroupId" TEXT;
WITH legacy_family_groups AS (
SELECT
family.id AS "assetFamilyId",
NULLIF(BTRIM(group_item ->> 'code'), '') AS code,
NULLIF(BTRIM(group_item ->> 'name'), '') AS name,
NULLIF(BTRIM(group_item ->> 'description'), '') AS description,
CASE
WHEN NULLIF(BTRIM(group_item ->> 'sortOrder'), '') ~ '^-?[0-9]+$'
THEN NULLIF(BTRIM(group_item ->> 'sortOrder'), '')::INTEGER
ELSE 0
END AS "sortOrder",
CASE
WHEN jsonb_typeof(group_item -> 'metadata') = 'object' THEN group_item -> 'metadata'
ELSE NULL
END AS metadata,
0 AS source_rank
FROM "AssetFamily" AS family
CROSS JOIN LATERAL jsonb_array_elements(
CASE
WHEN jsonb_typeof(family.metadata -> 'legacySkuGroups') = 'array'
THEN family.metadata -> 'legacySkuGroups'
ELSE '[]'::jsonb
END
) AS group_item
),
legacy_variant_groups AS (
SELECT
variant."assetFamilyId",
NULLIF(BTRIM(variant."businessData" -> 'legacySkuGroup' ->> 'code'), '') AS code,
NULLIF(BTRIM(variant."businessData" -> 'legacySkuGroup' ->> 'name'), '') AS name,
NULLIF(BTRIM(variant."businessData" -> 'legacySkuGroup' ->> 'description'), '') AS description,
CASE
WHEN NULLIF(BTRIM(variant."businessData" -> 'legacySkuGroup' ->> 'sortOrder'), '') ~ '^-?[0-9]+$'
THEN NULLIF(BTRIM(variant."businessData" -> 'legacySkuGroup' ->> 'sortOrder'), '')::INTEGER
ELSE 0
END AS "sortOrder",
CASE
WHEN jsonb_typeof(variant."businessData" -> 'legacySkuGroup' -> 'metadata') = 'object'
THEN variant."businessData" -> 'legacySkuGroup' -> 'metadata'
ELSE NULL
END AS metadata,
1 AS source_rank
FROM "AssetVariant" AS variant
WHERE jsonb_typeof(variant."businessData" -> 'legacySkuGroup') = 'object'
),
variant_code_groups AS (
SELECT
variant."assetFamilyId",
NULLIF(BTRIM(variant.code), '') AS code,
(ARRAY_AGG(COALESCE(NULLIF(BTRIM(variant.name), ''), variant.code) ORDER BY variant."createdAt" ASC, variant.id ASC))[1] AS name,
NULL::TEXT AS description,
ROW_NUMBER() OVER (
PARTITION BY variant."assetFamilyId"
ORDER BY MIN(variant."createdAt") ASC, NULLIF(BTRIM(variant.code), '') ASC
)::INTEGER AS "sortOrder",
jsonb_build_object(
'source',
'variant-backfill-after-sku-group-restore',
'sourceAssetVariantIds',
jsonb_agg(variant.id ORDER BY variant."createdAt" ASC, variant.id ASC)
) AS metadata,
2 AS source_rank
FROM "AssetVariant" AS variant
WHERE NULLIF(BTRIM(variant.code), '') IS NOT NULL
GROUP BY variant."assetFamilyId", NULLIF(BTRIM(variant.code), '')
),
candidate_groups AS (
SELECT * FROM legacy_family_groups WHERE code IS NOT NULL
UNION ALL
SELECT * FROM legacy_variant_groups WHERE code IS NOT NULL
UNION ALL
SELECT * FROM variant_code_groups WHERE code IS NOT NULL
),
deduped_groups AS (
SELECT DISTINCT ON ("assetFamilyId", code)
CONCAT('asg_', MD5("assetFamilyId" || ':' || code)) AS id,
"assetFamilyId",
code,
COALESCE(NULLIF(name, ''), code) AS name,
description,
"sortOrder",
metadata
FROM candidate_groups
ORDER BY "assetFamilyId", code, source_rank ASC, "sortOrder" ASC, name ASC
)
INSERT INTO "AssetSkuGroup" (
"id",
"assetFamilyId",
"code",
"name",
"description",
"sortOrder",
"metadata",
"createdAt",
"updatedAt"
)
SELECT
id,
"assetFamilyId",
code,
name,
description,
"sortOrder",
metadata,
CURRENT_TIMESTAMP,
CURRENT_TIMESTAMP
FROM deduped_groups
ON CONFLICT ("id") DO UPDATE
SET
"assetFamilyId" = EXCLUDED."assetFamilyId",
code = EXCLUDED.code,
name = EXCLUDED.name,
description = EXCLUDED.description,
"sortOrder" = EXCLUDED."sortOrder",
metadata = EXCLUDED.metadata,
"updatedAt" = CURRENT_TIMESTAMP;
UPDATE "AssetVariant" AS variant
SET "skuGroupId" = sku_group.id
FROM "AssetSkuGroup" AS sku_group
WHERE variant."skuGroupId" IS NULL
AND sku_group."assetFamilyId" = variant."assetFamilyId"
AND sku_group.code = COALESCE(
NULLIF(BTRIM(variant."businessData" -> 'legacySkuGroup' ->> 'code'), ''),
NULLIF(BTRIM(variant.code), '')
);
CREATE UNIQUE INDEX IF NOT EXISTS "AssetSkuGroup_assetFamilyId_code_key"
ON "AssetSkuGroup"("assetFamilyId", "code");
CREATE INDEX IF NOT EXISTS "AssetSkuGroup_assetFamilyId_idx" ON "AssetSkuGroup"("assetFamilyId");
CREATE INDEX IF NOT EXISTS "AssetSkuGroup_code_idx" ON "AssetSkuGroup"("code");
CREATE INDEX IF NOT EXISTS "AssetSkuGroup_sortOrder_idx" ON "AssetSkuGroup"("sortOrder");
CREATE INDEX IF NOT EXISTS "AssetVariant_skuGroupId_idx" ON "AssetVariant"("skuGroupId");
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_constraint WHERE conname = 'AssetSkuGroup_assetFamilyId_fkey'
) THEN
ALTER TABLE "AssetSkuGroup"
ADD CONSTRAINT "AssetSkuGroup_assetFamilyId_fkey"
FOREIGN KEY ("assetFamilyId") REFERENCES "AssetFamily"("id")
ON DELETE CASCADE ON UPDATE CASCADE;
END IF;
IF NOT EXISTS (
SELECT 1 FROM pg_constraint WHERE conname = 'AssetVariant_skuGroupId_fkey'
) THEN
ALTER TABLE "AssetVariant"
ADD CONSTRAINT "AssetVariant_skuGroupId_fkey"
FOREIGN KEY ("skuGroupId") REFERENCES "AssetSkuGroup"("id")
ON DELETE SET NULL ON UPDATE CASCADE;
END IF;
END $$;

View File

@ -0,0 +1,5 @@
ALTER TABLE "User"
ADD COLUMN "supplierCode" TEXT,
ADD COLUMN "supplierName" TEXT;
CREATE INDEX "User_supplierCode_idx" ON "User"("supplierCode");

View File

@ -0,0 +1,37 @@
CREATE TABLE IF NOT EXISTS "BarcodePrintImportRecord" (
"id" TEXT NOT NULL,
"kind" TEXT NOT NULL,
"skuCode" TEXT NOT NULL,
"normalizedSku" TEXT NOT NULL,
"supplierCode" TEXT NOT NULL DEFAULT '',
"supplierName" TEXT NOT NULL DEFAULT '',
"sourceFileName" TEXT NOT NULL DEFAULT '',
"rowNumber" INTEGER NOT NULL DEFAULT 0,
"importedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updates" JSONB NOT NULL,
"createdBy" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "BarcodePrintImportRecord_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX IF NOT EXISTS "BarcodePrintImportRecord_kind_normalizedSku_supplierCode_key"
ON "BarcodePrintImportRecord"("kind", "normalizedSku", "supplierCode");
CREATE INDEX IF NOT EXISTS "BarcodePrintImportRecord_kind_idx" ON "BarcodePrintImportRecord"("kind");
CREATE INDEX IF NOT EXISTS "BarcodePrintImportRecord_normalizedSku_idx" ON "BarcodePrintImportRecord"("normalizedSku");
CREATE INDEX IF NOT EXISTS "BarcodePrintImportRecord_supplierCode_idx" ON "BarcodePrintImportRecord"("supplierCode");
CREATE INDEX IF NOT EXISTS "BarcodePrintImportRecord_createdBy_idx" ON "BarcodePrintImportRecord"("createdBy");
CREATE INDEX IF NOT EXISTS "BarcodePrintImportRecord_createdAt_idx" ON "BarcodePrintImportRecord"("createdAt");
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_constraint WHERE conname = 'BarcodePrintImportRecord_createdBy_fkey'
) THEN
ALTER TABLE "BarcodePrintImportRecord"
ADD CONSTRAINT "BarcodePrintImportRecord_createdBy_fkey"
FOREIGN KEY ("createdBy") REFERENCES "User"("id")
ON DELETE CASCADE ON UPDATE CASCADE;
END IF;
END $$;

View File

@ -0,0 +1,67 @@
CREATE TABLE "AiRenderWorkbenchSpu" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"description" TEXT NOT NULL DEFAULT '',
"sortOrder" INTEGER NOT NULL DEFAULT 0,
"createdBy" TEXT NOT NULL,
"updatedBy" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "AiRenderWorkbenchSpu_pkey" PRIMARY KEY ("id")
);
CREATE TABLE "AiRenderWorkbenchSku" (
"id" TEXT NOT NULL,
"spuId" TEXT NOT NULL,
"name" TEXT NOT NULL,
"modelUrl" TEXT NOT NULL DEFAULT '',
"thumbnailUrl" TEXT,
"note" TEXT,
"materialColor" TEXT NOT NULL DEFAULT '#d8c6ad',
"materialTextureUrl" TEXT,
"nodeMaterials" JSONB,
"sortOrder" INTEGER NOT NULL DEFAULT 0,
"createdBy" TEXT NOT NULL,
"updatedBy" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "AiRenderWorkbenchSku_pkey" PRIMARY KEY ("id")
);
ALTER TABLE "AssistedAssemblyAiRender"
ADD COLUMN "workbenchSpuId" TEXT,
ADD COLUMN "workbenchSkuId" TEXT;
CREATE INDEX "AiRenderWorkbenchSpu_createdBy_idx" ON "AiRenderWorkbenchSpu"("createdBy");
CREATE INDEX "AiRenderWorkbenchSpu_sortOrder_createdAt_idx" ON "AiRenderWorkbenchSpu"("sortOrder", "createdAt");
CREATE INDEX "AiRenderWorkbenchSku_spuId_sortOrder_createdAt_idx" ON "AiRenderWorkbenchSku"("spuId", "sortOrder", "createdAt");
CREATE INDEX "AiRenderWorkbenchSku_createdBy_idx" ON "AiRenderWorkbenchSku"("createdBy");
CREATE INDEX "AssistedAssemblyAiRender_workbenchSpuId_createdAt_idx" ON "AssistedAssemblyAiRender"("workbenchSpuId", "createdAt");
CREATE INDEX "AssistedAssemblyAiRender_workbenchSkuId_createdAt_idx" ON "AssistedAssemblyAiRender"("workbenchSkuId", "createdAt");
ALTER TABLE "AiRenderWorkbenchSpu"
ADD CONSTRAINT "AiRenderWorkbenchSpu_createdBy_fkey"
FOREIGN KEY ("createdBy") REFERENCES "User"("id")
ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "AiRenderWorkbenchSku"
ADD CONSTRAINT "AiRenderWorkbenchSku_spuId_fkey"
FOREIGN KEY ("spuId") REFERENCES "AiRenderWorkbenchSpu"("id")
ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "AiRenderWorkbenchSku"
ADD CONSTRAINT "AiRenderWorkbenchSku_createdBy_fkey"
FOREIGN KEY ("createdBy") REFERENCES "User"("id")
ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "AssistedAssemblyAiRender"
ADD CONSTRAINT "AssistedAssemblyAiRender_workbenchSpuId_fkey"
FOREIGN KEY ("workbenchSpuId") REFERENCES "AiRenderWorkbenchSpu"("id")
ON DELETE SET NULL ON UPDATE CASCADE;
ALTER TABLE "AssistedAssemblyAiRender"
ADD CONSTRAINT "AssistedAssemblyAiRender_workbenchSkuId_fkey"
FOREIGN KEY ("workbenchSkuId") REFERENCES "AiRenderWorkbenchSku"("id")
ON DELETE SET NULL ON UPDATE CASCADE;

View File

@ -0,0 +1,84 @@
-- 20260613151000 temporarily restored AssetSkuGroup for legacy-data recovery.
-- The current schema deliberately keeps this grouping as JSON compatibility data,
-- so replaying the migration chain must retire the transient relational shape again.
DO $$
BEGIN
IF to_regclass('public."AssetSkuGroup"') IS NOT NULL
AND EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = 'AssetVariant'
AND column_name = 'skuGroupId'
)
THEN
EXECUTE $SQL$
UPDATE "AssetVariant" AS variant
SET "businessData" = jsonb_set(
CASE
WHEN variant."businessData" IS NULL OR jsonb_typeof(variant."businessData") <> 'object'
THEN '{}'::jsonb
ELSE variant."businessData"
END,
'{legacySkuGroup}',
jsonb_build_object(
'id', sku_group.id,
'code', sku_group.code,
'name', sku_group.name,
'description', sku_group.description,
'sortOrder', sku_group."sortOrder",
'metadata', sku_group.metadata,
'assetFamilyId', sku_group."assetFamilyId",
'source', 'AssetSkuGroup.replay-aligned'
),
true
)
FROM "AssetSkuGroup" AS sku_group
WHERE variant."skuGroupId" = sku_group.id
$SQL$;
EXECUTE $SQL$
UPDATE "AssetFamily" AS family
SET metadata = jsonb_set(
CASE
WHEN family.metadata IS NULL OR jsonb_typeof(family.metadata) <> 'object'
THEN '{}'::jsonb
ELSE family.metadata
END,
'{legacySkuGroups}',
grouped.groups,
true
)
FROM (
SELECT
"assetFamilyId",
jsonb_agg(
jsonb_build_object(
'id', id,
'code', code,
'name', name,
'description', description,
'sortOrder', "sortOrder",
'metadata', metadata,
'source', 'AssetSkuGroup.replay-aligned'
)
ORDER BY "sortOrder", code
) AS groups
FROM "AssetSkuGroup"
GROUP BY "assetFamilyId"
) AS grouped
WHERE family.id = grouped."assetFamilyId"
$SQL$;
END IF;
END $$;
ALTER TABLE "AssetVariant"
DROP CONSTRAINT IF EXISTS "AssetVariant_skuGroupId_fkey";
DROP INDEX IF EXISTS "AssetVariant_skuGroupId_idx";
ALTER TABLE "AssetVariant"
DROP COLUMN IF EXISTS "skuGroupId";
DROP TABLE IF EXISTS "AssetSkuGroup";

View File

@ -0,0 +1,445 @@
-- 把宽表 product_catalog_wide_v1 的口径从"一 SKU 一行"明确保护成"一条码一行"。
-- 原视图 FROM sku_source 的行粒度是 SKU,barcode 只是字段;当前 43261 行 barcode 全唯一是数据巧合,
-- 一旦出现"一条码多 SKU"(正式 ProductSKU 落库 + intake ready 同条码 / intake 内部同条码多行),
-- 宽表就会重复,污染外部系统的 barcode 主同步键。
-- 修复:复用项目已有的 DISTINCT ON(barcode_business_key) 模式(latest_quality / limit_by_barcode CTE),
-- 把原主查询整体包成 catalog_by_sku CTE,外层加 DISTINCT ON (barcode_business_key)。
-- 优先级:FORMAL(正式商品)> INTAKE_READY(开发阶段),同源内 updated_at 最新 > created_at 最新。
-- 用 CREATE OR REPLACE VIEW(不用 DROP),保留 external_reader 已有的 GRANT SELECT 权限。
-- Expand the external product catalog wide view so external systems can
-- consume ready non-integrated SKU rows before formal Product / ProductSKU
-- records are materialized.
CREATE OR REPLACE VIEW external.product_catalog_wide_v1 AS
WITH formal_sku_source AS (
SELECT
sku.product_id,
sku.product_model_number,
sku.product_name,
sku.product_description,
sku.product_category_id,
sku.product_category_name,
sku.product_thumbnail_url,
sku.product_model_url,
sku.sku_id,
sku.sku_code,
sku.sku_name,
sku.price,
sku.cost_price,
sku.stock,
sku.is_active,
sku.record_status,
sku.model_url,
sku.image_url,
sku.image_urls,
sku.source_pre_sku_id,
sku.source_asset_variant_id,
sku.source_asset_variant_ids,
sku.asset_family_code,
sku.asset_family_name,
sku.spec_code,
sku.spec_name,
sku.barcode,
sku.size_json,
sku.color,
sku.material,
sku.source_status,
sku.synced_at,
sku.warehouse_location_id,
sku.warehouse_location_code,
sku.warehouse_location_name,
sku.created_at,
sku.updated_at,
sku.product_business_key,
sku.sku_business_key,
sku.barcode_business_key,
'FORMAL'::text AS catalog_source
FROM external.product_skus_v1 sku
),
formal_identity_map AS (
SELECT
barcode_business_key,
CASE
WHEN NULLIF(btrim(COALESCE(sku_code, '')), '') IS NOT NULL
THEN upper(btrim(sku_code))
ELSE NULL
END AS normalized_sku_code
FROM formal_sku_source
),
intake_ready_sku_source AS (
SELECT
product.intake_product_id AS product_id,
NULLIF(btrim(COALESCE(product.spu, '')), '') AS product_model_number,
NULLIF(btrim(COALESCE(product.product_name, '')), '') AS product_name,
NULL::text AS product_description,
NULL::text AS product_category_id,
NULLIF(btrim(COALESCE(product.product_category, '')), '') AS product_category_name,
COALESCE(
NULLIF(btrim(COALESCE(product.product_image_url, '')), ''),
NULLIF(btrim(COALESCE(sku.image_url, '')), '')
) AS product_thumbnail_url,
NULL::text AS product_model_url,
sku.intake_sku_item_id AS sku_id,
NULLIF(btrim(COALESCE(sku.sku_code, '')), '') AS sku_code,
COALESCE(
NULLIF(btrim(COALESCE(sku.spec_name, '')), ''),
NULLIF(btrim(COALESCE(sku.sku_code, '')), '')
) AS sku_name,
NULL::numeric(10,2) AS price,
NULL::numeric(10,2) AS cost_price,
0::integer AS stock,
true AS is_active,
'ACTIVE'::text AS record_status,
NULLIF(btrim(COALESCE(sku.model_3d_url, '')), '') AS model_url,
COALESCE(
NULLIF(btrim(COALESCE(sku.image_url, '')), ''),
NULLIF(btrim(COALESCE(product.product_image_url, '')), '')
) AS image_url,
CASE
WHEN COALESCE(
NULLIF(btrim(COALESCE(sku.image_url, '')), ''),
NULLIF(btrim(COALESCE(product.product_image_url, '')), '')
) IS NOT NULL
THEN ARRAY[
COALESCE(
NULLIF(btrim(COALESCE(sku.image_url, '')), ''),
NULLIF(btrim(COALESCE(product.product_image_url, '')), '')
)
]
ELSE ARRAY[]::text[]
END AS image_urls,
NULL::text AS source_pre_sku_id,
NULL::text AS source_asset_variant_id,
ARRAY[]::text[] AS source_asset_variant_ids,
NULLIF(btrim(COALESCE(sku.model, '')), '') AS asset_family_code,
NULL::text AS asset_family_name,
NULLIF(btrim(COALESCE(sku.attribute_code, '')), '') AS spec_code,
NULLIF(btrim(COALESCE(sku.spec_name, '')), '') AS spec_name,
NULLIF(btrim(COALESCE(sku.barcode, '')), '') AS barcode,
CASE
WHEN NULLIF(btrim(COALESCE(sku.size, '')), '') IS NOT NULL
THEN jsonb_build_object('label', btrim(sku.size))
ELSE NULL::jsonb
END AS size_json,
NULLIF(btrim(COALESCE(sku.color, '')), '') AS color,
NULLIF(btrim(COALESCE(sku.material, '')), '') AS material,
NULL::text AS source_status,
NULL::text AS synced_at,
NULL::text AS warehouse_location_id,
NULL::text AS warehouse_location_code,
NULL::text AS warehouse_location_name,
product.created_at AS created_at,
product.updated_at AS updated_at,
CASE
WHEN NULLIF(btrim(COALESCE(product.spu, '')), '') IS NOT NULL
THEN upper(btrim(product.spu))
ELSE NULL
END AS product_business_key,
CASE
WHEN NULLIF(btrim(COALESCE(sku.sku_code, '')), '') IS NOT NULL
THEN upper(btrim(sku.sku_code))
ELSE NULL
END AS sku_business_key,
CASE
WHEN NULLIF(btrim(COALESCE(sku.barcode, '')), '') IS NOT NULL
THEN upper(btrim(sku.barcode))
ELSE NULL
END AS barcode_business_key,
'INTAKE_READY'::text AS catalog_source
FROM external.intake_sku_items_ready_v1 sku
JOIN external.intake_products_ready_v1 product
ON product.intake_product_id = sku.intake_product_id
WHERE NOT EXISTS (
SELECT 1
FROM formal_identity_map formal
WHERE (
NULLIF(btrim(COALESCE(sku.barcode, '')), '') IS NOT NULL
AND formal.barcode_business_key = upper(btrim(sku.barcode))
)
OR (
NULLIF(btrim(COALESCE(sku.barcode, '')), '') IS NULL
AND NULLIF(btrim(COALESCE(sku.sku_code, '')), '') IS NOT NULL
AND formal.normalized_sku_code = upper(btrim(sku.sku_code))
)
)
),
sku_source AS (
SELECT * FROM formal_sku_source
UNION ALL
SELECT * FROM intake_ready_sku_source
),
asset_summary AS (
SELECT
product_business_key,
sku_business_key,
COUNT(*) FILTER (WHERE asset_type = 'SKU_IMAGE')::integer AS image_asset_count,
COUNT(*) FILTER (WHERE asset_type = 'SKU_MODEL')::integer AS model_asset_count,
COUNT(*) FILTER (WHERE asset_type = 'SKU_THUMBNAIL')::integer AS thumbnail_asset_count,
MIN(asset_url) FILTER (WHERE asset_type = 'SKU_MODEL') AS first_model_asset_url,
MIN(asset_url) FILTER (WHERE asset_type = 'SKU_THUMBNAIL') AS first_thumbnail_asset_url
FROM external.product_assets_v1
GROUP BY product_business_key, sku_business_key
),
packaged_summary AS (
SELECT
barcode_business_key,
COUNT(*)::integer AS packaged_item_count,
COALESCE(SUM(stock_quantity), 0)::integer AS packaged_stock_quantity
FROM external.packaged_items_v1
WHERE NULLIF(barcode_business_key, '') IS NOT NULL
GROUP BY barcode_business_key
),
quality_summary AS (
SELECT
barcode_business_key,
COUNT(*)::integer AS quality_record_count,
COUNT(*) FILTER (WHERE source_type = 'WAREHOUSE')::integer AS warehouse_quality_record_count,
COUNT(*) FILTER (WHERE source_type = 'FIRST_BATCH')::integer AS first_batch_quality_record_count
FROM external.sku_quality_inspections_v1
WHERE NULLIF(barcode_business_key, '') IS NOT NULL
GROUP BY barcode_business_key
),
latest_quality AS (
SELECT DISTINCT ON (barcode_business_key)
barcode_business_key,
record_key AS latest_quality_record_key,
source_type AS latest_quality_source_type,
batch_no AS latest_quality_batch_no,
inspected_at AS latest_quality_inspected_at,
conclusion AS latest_quality_conclusion,
status AS latest_quality_status
FROM external.sku_quality_inspections_v1
WHERE NULLIF(barcode_business_key, '') IS NOT NULL
ORDER BY barcode_business_key, COALESCE(inspected_at, updated_at, created_at) DESC, created_at DESC
),
limit_by_barcode AS (
SELECT DISTINCT ON (identity_value)
identity_value,
product_business_key AS limit_source_product_business_key,
sku_business_key AS limit_source_sku_business_key,
limit_price_applicable,
limit_price_confirmed,
limit_price_status,
expected_daily_limit_price,
expected_limit_gross_margin_rate,
non_spu_owner_limit_price,
limit_confirmed_at
FROM (
SELECT
upper(btrim(barcode)) AS identity_value,
product_business_key,
sku_business_key,
limit_price_applicable,
limit_price_confirmed,
limit_price_status,
expected_daily_limit_price,
expected_limit_gross_margin_rate,
non_spu_owner_limit_price,
limit_confirmed_at,
updated_at
FROM external.intake_product_limits_v1
WHERE NULLIF(btrim(COALESCE(barcode, '')), '') IS NOT NULL
) limit_rows
WHERE NULLIF(identity_value, '') IS NOT NULL
ORDER BY
identity_value,
limit_price_applicable DESC,
limit_price_confirmed DESC,
limit_confirmed_at DESC NULLS LAST,
updated_at DESC
),
limit_by_sku AS (
SELECT DISTINCT ON (identity_value)
identity_value,
product_business_key AS limit_source_product_business_key,
sku_business_key AS limit_source_sku_business_key,
limit_price_applicable,
limit_price_confirmed,
limit_price_status,
expected_daily_limit_price,
expected_limit_gross_margin_rate,
non_spu_owner_limit_price,
limit_confirmed_at
FROM (
SELECT
upper(btrim(sku_code)) AS identity_value,
product_business_key,
sku_business_key,
limit_price_applicable,
limit_price_confirmed,
limit_price_status,
expected_daily_limit_price,
expected_limit_gross_margin_rate,
non_spu_owner_limit_price,
limit_confirmed_at,
updated_at
FROM external.intake_product_limits_v1
WHERE NULLIF(btrim(COALESCE(sku_code, '')), '') IS NOT NULL
) limit_rows
WHERE NULLIF(identity_value, '') IS NOT NULL
ORDER BY
identity_value,
limit_price_applicable DESC,
limit_price_confirmed DESC,
limit_confirmed_at DESC NULLS LAST,
updated_at DESC
),
catalog_by_sku AS (
SELECT
sku.product_id,
sku.product_model_number,
sku.product_business_key,
sku.product_name,
sku.product_description,
sku.product_category_id,
sku.product_category_name,
sku.product_thumbnail_url,
sku.product_model_url,
sku.sku_id,
sku.sku_code,
sku.sku_business_key,
sku.sku_name,
sku.price,
sku.cost_price,
sku.stock,
sku.is_active,
sku.record_status,
sku.model_url,
sku.image_url,
sku.image_urls,
sku.source_pre_sku_id,
sku.source_asset_variant_id,
sku.source_asset_variant_ids,
sku.asset_family_code,
sku.asset_family_name,
sku.spec_code,
sku.spec_name,
sku.barcode,
sku.barcode_business_key,
sku.size_json,
sku.color,
sku.material,
sku.source_status,
sku.synced_at,
sku.warehouse_location_id,
sku.warehouse_location_code,
sku.warehouse_location_name,
stock.warehouse_zone,
stock.warehouse_aisle,
stock.warehouse_shelf,
stock.warehouse_bin,
stock.warehouse_location_is_active,
stock.warehouse_location_status,
COALESCE(limit_by_barcode.limit_price_applicable, limit_by_sku.limit_price_applicable, false) AS limit_price_applicable,
COALESCE(limit_by_barcode.limit_price_confirmed, limit_by_sku.limit_price_confirmed, false) AS limit_price_confirmed,
COALESCE(limit_by_barcode.limit_price_status, limit_by_sku.limit_price_status, 'NOT_APPLICABLE') AS limit_price_status,
COALESCE(limit_by_barcode.expected_daily_limit_price, limit_by_sku.expected_daily_limit_price) AS expected_daily_limit_price,
COALESCE(limit_by_barcode.expected_limit_gross_margin_rate, limit_by_sku.expected_limit_gross_margin_rate) AS expected_limit_gross_margin_rate,
COALESCE(limit_by_barcode.non_spu_owner_limit_price, limit_by_sku.non_spu_owner_limit_price) AS non_spu_owner_limit_price,
COALESCE(limit_by_barcode.limit_confirmed_at, limit_by_sku.limit_confirmed_at) AS limit_confirmed_at,
COALESCE(limit_by_barcode.limit_source_product_business_key, limit_by_sku.limit_source_product_business_key) AS limit_source_product_business_key,
COALESCE(limit_by_barcode.limit_source_sku_business_key, limit_by_sku.limit_source_sku_business_key) AS limit_source_sku_business_key,
CASE
WHEN sku.catalog_source = 'INTAKE_READY'
THEN CASE WHEN NULLIF(btrim(COALESCE(sku.image_url, '')), '') IS NOT NULL THEN 1 ELSE 0 END
ELSE COALESCE(asset_summary.image_asset_count, 0)
END AS image_asset_count,
CASE
WHEN sku.catalog_source = 'INTAKE_READY'
THEN CASE WHEN NULLIF(btrim(COALESCE(sku.model_url, '')), '') IS NOT NULL THEN 1 ELSE 0 END
ELSE COALESCE(asset_summary.model_asset_count, 0)
END AS model_asset_count,
CASE
WHEN sku.catalog_source = 'INTAKE_READY'
THEN CASE
WHEN COALESCE(
NULLIF(btrim(COALESCE(sku.product_thumbnail_url, '')), ''),
NULLIF(btrim(COALESCE(sku.image_url, '')), '')
) IS NOT NULL
THEN 1
ELSE 0
END
ELSE COALESCE(asset_summary.thumbnail_asset_count, 0)
END AS thumbnail_asset_count,
COALESCE(
asset_summary.first_model_asset_url,
CASE
WHEN sku.catalog_source = 'INTAKE_READY'
THEN NULLIF(btrim(COALESCE(sku.model_url, '')), '')
ELSE NULL
END
) AS first_model_asset_url,
COALESCE(
asset_summary.first_thumbnail_asset_url,
CASE
WHEN sku.catalog_source = 'INTAKE_READY'
THEN COALESCE(
NULLIF(btrim(COALESCE(sku.product_thumbnail_url, '')), ''),
NULLIF(btrim(COALESCE(sku.image_url, '')), '')
)
ELSE NULL
END
) AS first_thumbnail_asset_url,
COALESCE(bom_summary.bom_item_count, 0) AS bom_item_count,
COALESCE(bom_summary.panel_item_count, 0) AS panel_item_count,
COALESCE(bom_summary.hardware_item_count, 0) AS hardware_item_count,
COALESCE(bom_summary.material_codes, ARRAY[]::text[]) AS material_codes,
COALESCE(bom_summary.material_names, ARRAY[]::text[]) AS material_names,
COALESCE(packaged_summary.packaged_item_count, 0) AS packaged_item_count,
COALESCE(packaged_summary.packaged_stock_quantity, 0) AS packaged_stock_quantity,
COALESCE(quality_summary.quality_record_count, 0) AS quality_record_count,
COALESCE(quality_summary.warehouse_quality_record_count, 0) AS warehouse_quality_record_count,
COALESCE(quality_summary.first_batch_quality_record_count, 0) AS first_batch_quality_record_count,
latest_quality.latest_quality_record_key,
latest_quality.latest_quality_source_type,
latest_quality.latest_quality_batch_no,
latest_quality.latest_quality_inspected_at,
latest_quality.latest_quality_conclusion,
latest_quality.latest_quality_status,
sku.created_at,
sku.updated_at,
sku.catalog_source
FROM sku_source sku
LEFT JOIN external.sku_warehouse_stock_v1 stock
ON stock.barcode_business_key = sku.barcode_business_key
LEFT JOIN limit_by_sku
ON limit_by_sku.identity_value = upper(btrim(COALESCE(sku.sku_code, '')))
LEFT JOIN limit_by_barcode
ON limit_by_barcode.identity_value = sku.barcode_business_key
LEFT JOIN asset_summary
ON asset_summary.product_business_key = sku.product_business_key
AND asset_summary.sku_business_key = sku.sku_business_key
LEFT JOIN LATERAL (
SELECT
COUNT(*)::integer AS bom_item_count,
COUNT(*) FILTER (WHERE item_type = 'PANEL')::integer AS panel_item_count,
COUNT(*) FILTER (WHERE item_type = 'HARDWARE')::integer AS hardware_item_count,
array_remove(array_agg(DISTINCT material_code ORDER BY material_code), NULL) AS material_codes,
array_remove(array_agg(DISTINCT material_name ORDER BY material_name), NULL) AS material_names
FROM external.product_bom_v1 bom
WHERE bom.product_business_key = sku.product_business_key
AND (
bom.sku_business_key = sku.sku_business_key
OR bom.sku_business_key IS NULL
)
) bom_summary ON true
LEFT JOIN packaged_summary
ON packaged_summary.barcode_business_key = sku.barcode_business_key
LEFT JOIN quality_summary
ON quality_summary.barcode_business_key = sku.barcode_business_key
LEFT JOIN latest_quality
ON latest_quality.barcode_business_key = sku.barcode_business_key
)
SELECT DISTINCT ON (catalog_by_sku.barcode_business_key)
catalog_by_sku.*
FROM catalog_by_sku
WHERE catalog_by_sku.barcode_business_key IS NOT NULL
AND btrim(catalog_by_sku.barcode_business_key) <> ''
ORDER BY
catalog_by_sku.barcode_business_key,
CASE WHEN catalog_by_sku.catalog_source IS NULL THEN 1
WHEN catalog_by_sku.catalog_source = 'FORMAL' THEN 0
ELSE 1 END,
catalog_by_sku.updated_at DESC NULLS LAST,
catalog_by_sku.created_at DESC;

View File

@ -0,0 +1,390 @@
-- Normalize asset URLs exposed by external read-only views.
--
-- Business tables still store stable in-app object routes such as
-- /api/storage/object?key=... . External database consumers need complete
-- URLs, so the external contract layer prefixes relative application routes
-- with the current production site origin. The routes remain application
-- controlled; this migration does not expose private bucket objects directly.
CREATE OR REPLACE FUNCTION external.to_external_asset_url(raw_value text)
RETURNS text
LANGUAGE sql
STABLE
AS $$
WITH normalized AS (
SELECT NULLIF(btrim(raw_value), '') AS value
),
base_url AS (
SELECT COALESCE(
NULLIF(btrim(current_setting('app.external_asset_base_url', true)), ''),
NULLIF(btrim(current_setting('app.public_base_url', true)), ''),
'https://cp.jiansun.vip'
) AS value
)
SELECT
CASE
WHEN normalized.value IS NULL THEN NULL
WHEN normalized.value ~* '^https?://(localhost|127\.0\.0\.1|\[::1\])(:[0-9]+)?/' THEN
regexp_replace(base_url.value, '/+$', '') ||
regexp_replace(
normalized.value,
'^https?://(localhost|127\.0\.0\.1|\[::1\])(:[0-9]+)?',
'',
'i'
)
WHEN normalized.value LIKE '/%' THEN regexp_replace(base_url.value, '/+$', '') || normalized.value
ELSE normalized.value
END
FROM normalized, base_url;
$$;
CREATE OR REPLACE VIEW external.materials_v1 AS
SELECT
m."id" AS material_id,
m."code" AS material_code,
m."name" AS material_name,
c."name" AS category_name,
external.to_external_asset_url(m."thumbnailUrl") AS thumbnail_url,
external.to_external_asset_url(m."textureUrl") AS texture_url,
external.to_external_asset_url(m."normalMapUrl") AS normal_map_url,
m."color" AS base_color,
m."metalness" AS metalness,
m."roughness" AS roughness,
m."opacity" AS opacity,
CASE WHEN m."isActive" THEN 'ACTIVE' ELSE 'INACTIVE' END AS record_status
FROM "Material" m
JOIN "MaterialCategory" c ON c."id" = m."categoryId";
CREATE OR REPLACE VIEW external.asset_variants_v1 AS
SELECT
v."id" AS asset_variant_id,
v."code" AS spec_code,
v."name" AS spec_name,
v."assetType"::text AS asset_type,
v."status"::text AS asset_status,
f."id" AS asset_family_id,
f."code" AS asset_family_code,
f."name" AS asset_family_name,
s."id" AS asset_series_id,
s."code" AS asset_series_code,
s."name" AS asset_series_name,
pc."id" AS product_category_id,
pc."name" AS product_category_name,
v."sizeLabel" AS size_label,
v."dimensions" AS dimensions,
external.to_external_asset_url(v."modelUrl") AS model_url,
external.to_external_asset_url(v."thumbnailUrl") AS thumbnail_url,
v."barcode" AS barcode,
v."supplierName" AS supplier_name,
v."supplierCode" AS supplier_code,
v."versionNo" AS version_no,
sub."id" AS substrate_id,
sub."name" AS substrate_name,
finish."id" AS finish_id,
finish."code" AS finish_code,
finish."name" AS finish_name,
CASE
WHEN sub."name" IS NOT NULL AND finish."name" IS NOT NULL THEN sub."name" || ' + ' || finish."name"
ELSE COALESCE(sub."name", finish."name")
END AS material_label,
v."weightKg" AS weight_kg,
v."costPrice" AS cost_price,
v."estimatedCostPrice" AS estimated_cost_price,
v."createdAt" AS created_at,
v."updatedAt" AS updated_at
FROM "AssetVariant" v
JOIN "AssetFamily" f ON f."id" = v."assetFamilyId"
LEFT JOIN "AssetSeries" s ON s."id" = f."assetSeriesId"
LEFT JOIN "ProductCategory" pc ON pc."id" = f."productCategoryId"
LEFT JOIN "Substrate" sub ON sub."id" = v."substrateId"
LEFT JOIN "Material" finish ON finish."id" = v."finishId"
WHERE v."deletedAt" IS NULL;
CREATE OR REPLACE VIEW external.product_skus_v1 AS
SELECT
p."id" AS product_id,
p."modelNumber" AS product_model_number,
p."name" AS product_name,
p."description" AS product_description,
pc."id" AS product_category_id,
pc."name" AS product_category_name,
external.to_external_asset_url(p."thumbnailUrl") AS product_thumbnail_url,
external.to_external_asset_url(p."modelUrl") AS product_model_url,
sku."id" AS sku_id,
sku."skuCode" AS sku_code,
COALESCE(
NULLIF(sku."name", ''),
NULLIF(profile."specName", ''),
sku."skuCode"
) AS sku_name,
sku."price" AS price,
sku."costPrice" AS cost_price,
sku."stock" AS stock,
sku."isActive" AS is_active,
CASE WHEN sku."isActive" THEN 'ACTIVE' ELSE 'INACTIVE' END AS record_status,
external.to_external_asset_url(
COALESCE(NULLIF(sku."modelUrl", ''), NULLIF(p."modelUrl", ''))
) AS model_url,
external.to_external_asset_url(
COALESCE(
NULLIF(profile."imageUrl", ''),
sku."images"[1],
NULLIF(p."thumbnailUrl", '')
)
) AS image_url,
ARRAY(
SELECT external.to_external_asset_url(image_item.image_url)
FROM unnest(sku."images") AS image_item(image_url)
WHERE NULLIF(btrim(image_item.image_url), '') IS NOT NULL
) AS image_urls,
sku."sourcePreSkuId" AS source_pre_sku_id,
sku."sourceAssetVariantId" AS source_asset_variant_id,
sku."sourceAssetVariantIds" AS source_asset_variant_ids,
profile."model" AS asset_family_code,
profile."modelName" AS asset_family_name,
profile."specCode" AS spec_code,
profile."specName" AS spec_name,
profile."barcode" AS barcode,
profile."size" AS size_json,
profile."color" AS color,
profile."material" AS material,
profile."sourceStatus"::text AS source_status,
profile."syncedAt"::text AS synced_at,
wh."id" AS warehouse_location_id,
wh."code" AS warehouse_location_code,
wh."name" AS warehouse_location_name,
sku."createdAt" AS created_at,
sku."updatedAt" AS updated_at,
CASE
WHEN NULLIF(btrim(COALESCE(p."modelNumber", '')), '') IS NOT NULL
THEN upper(btrim(p."modelNumber"))
ELSE NULL
END AS product_business_key,
CASE
WHEN NULLIF(btrim(COALESCE(sku."skuCode", '')), '') IS NOT NULL
THEN upper(btrim(sku."skuCode"))
ELSE NULL
END AS sku_business_key,
CASE
WHEN NULLIF(btrim(COALESCE(profile."barcode", '')), '') IS NOT NULL
THEN upper(btrim(profile."barcode"))
ELSE NULL
END AS barcode_business_key
FROM "ProductSKU" sku
JOIN "Product" p ON p."id" = sku."productId"
LEFT JOIN "ProductSkuAssetProfile" profile ON profile."skuId" = sku."id"
LEFT JOIN "ProductCategory" pc ON pc."id" = p."categoryId"
LEFT JOIN "WarehouseLocation" wh ON wh."id" = sku."warehouseLocationId"
WHERE p."deletedAt" IS NULL
AND sku."deletedAt" IS NULL;
CREATE OR REPLACE VIEW external.product_assets_v1 AS
WITH image_assets AS (
SELECT
sku."id" AS sku_id,
sku."productId" AS product_id,
p."modelNumber" AS product_model_number,
sku."skuCode" AS sku_code,
profile."barcode" AS barcode,
image_item.image_url,
image_item.sort_order
FROM "ProductSKU" sku
JOIN "Product" p ON p."id" = sku."productId"
LEFT JOIN "ProductSkuAssetProfile" profile ON profile."skuId" = sku."id"
CROSS JOIN LATERAL unnest(sku."images") WITH ORDINALITY AS image_item(image_url, sort_order)
WHERE p."deletedAt" IS NULL
AND sku."deletedAt" IS NULL
),
model_assets AS (
SELECT
sku."id" AS sku_id,
sku."productId" AS product_id,
p."modelNumber" AS product_model_number,
sku."skuCode" AS sku_code,
profile."barcode" AS barcode,
COALESCE(NULLIF(sku."modelUrl", ''), NULLIF(p."modelUrl", '')) AS model_url
FROM "ProductSKU" sku
JOIN "Product" p ON p."id" = sku."productId"
LEFT JOIN "ProductSkuAssetProfile" profile ON profile."skuId" = sku."id"
WHERE p."deletedAt" IS NULL
AND sku."deletedAt" IS NULL
),
thumbnail_assets AS (
SELECT
sku."id" AS sku_id,
sku."productId" AS product_id,
p."modelNumber" AS product_model_number,
sku."skuCode" AS sku_code,
profile."barcode" AS barcode,
COALESCE(
NULLIF(profile."imageUrl", ''),
NULLIF(p."thumbnailUrl", '')
) AS thumbnail_url
FROM "ProductSKU" sku
JOIN "Product" p ON p."id" = sku."productId"
LEFT JOIN "ProductSkuAssetProfile" profile ON profile."skuId" = sku."id"
WHERE p."deletedAt" IS NULL
AND sku."deletedAt" IS NULL
)
SELECT
'SKU_IMAGE' AS asset_type,
product_id,
sku_id,
external.to_external_asset_url(image_url) AS asset_url,
NULL::text AS file_name,
NULL::text AS mime_type,
sort_order::integer AS sort_order,
CASE
WHEN NULLIF(btrim(COALESCE(product_model_number, '')), '') IS NOT NULL
THEN upper(btrim(product_model_number))
ELSE NULL
END AS product_business_key,
CASE
WHEN NULLIF(btrim(COALESCE(sku_code, '')), '') IS NOT NULL
THEN upper(btrim(sku_code))
ELSE NULL
END AS sku_business_key,
CASE
WHEN NULLIF(btrim(COALESCE(barcode, '')), '') IS NOT NULL
THEN upper(btrim(barcode))
ELSE NULL
END AS barcode_business_key
FROM image_assets
WHERE NULLIF(image_url, '') IS NOT NULL
UNION ALL
SELECT
'SKU_MODEL' AS asset_type,
product_id,
sku_id,
external.to_external_asset_url(model_url) AS asset_url,
NULL::text AS file_name,
NULL::text AS mime_type,
0 AS sort_order,
CASE
WHEN NULLIF(btrim(COALESCE(product_model_number, '')), '') IS NOT NULL
THEN upper(btrim(product_model_number))
ELSE NULL
END AS product_business_key,
CASE
WHEN NULLIF(btrim(COALESCE(sku_code, '')), '') IS NOT NULL
THEN upper(btrim(sku_code))
ELSE NULL
END AS sku_business_key,
CASE
WHEN NULLIF(btrim(COALESCE(barcode, '')), '') IS NOT NULL
THEN upper(btrim(barcode))
ELSE NULL
END AS barcode_business_key
FROM model_assets
WHERE NULLIF(model_url, '') IS NOT NULL
UNION ALL
SELECT
'SKU_THUMBNAIL' AS asset_type,
product_id,
sku_id,
external.to_external_asset_url(thumbnail_url) AS asset_url,
NULL::text AS file_name,
NULL::text AS mime_type,
0 AS sort_order,
CASE
WHEN NULLIF(btrim(COALESCE(product_model_number, '')), '') IS NOT NULL
THEN upper(btrim(product_model_number))
ELSE NULL
END AS product_business_key,
CASE
WHEN NULLIF(btrim(COALESCE(sku_code, '')), '') IS NOT NULL
THEN upper(btrim(sku_code))
ELSE NULL
END AS sku_business_key,
CASE
WHEN NULLIF(btrim(COALESCE(barcode, '')), '') IS NOT NULL
THEN upper(btrim(barcode))
ELSE NULL
END AS barcode_business_key
FROM thumbnail_assets
WHERE NULLIF(thumbnail_url, '') IS NOT NULL;
CREATE OR REPLACE VIEW external.intake_products_v1 AS
SELECT
product."id" AS intake_product_id,
product."spu" AS spu,
product."spuBase" AS spu_base,
product."brand" AS brand,
profile."productName" AS product_name,
external.to_external_asset_url(profile."productImageUrl") AS product_image_url,
profile."productCategory" AS product_category,
profile."salesCategory" AS sales_category,
profile."productYear" AS product_year,
profile."productStatus" AS product_status,
COALESCE(NULLIF(profile."structureDesigner", ''), NULLIF(product."designer", '')) AS designer,
COALESCE(NULLIF(profile."productPlanning", ''), NULLIF(product."operator", '')) AS operator,
COALESCE(NULLIF(profile."purchaseOwner", ''), NULLIF(product."purchaser", '')) AS purchaser,
profile."productDevelopmentKind" AS product_development_kind,
profile."goodsType" AS goods_type,
profile."materialType" AS material_type,
profile."productionType" AS production_type,
product."createdAt" AS created_at,
product."updatedAt" AS updated_at,
profile."productPlanning" AS product_planning,
profile."visualPlanning" AS visual_planning,
profile."structureDesigner" AS structure_designer,
profile."purchaseOwner" AS purchase_owner,
CASE
WHEN NULLIF(btrim(COALESCE(product."brand", '')), '') IS NOT NULL
AND NULLIF(btrim(COALESCE(product."spu", '')), '') IS NOT NULL
THEN upper(btrim(product."brand")) || ':' || upper(btrim(product."spu"))
ELSE NULL
END AS product_business_key
FROM "IntakeNonIntegratedProduct" product
LEFT JOIN "IntakeNonIntegratedProductProfile" profile ON profile."productId" = product."id"
WHERE product."deletedAt" IS NULL;
CREATE OR REPLACE VIEW external.intake_sku_items_v1 AS
SELECT
product."id" AS intake_product_id,
product."spu" AS spu,
product."brand" AS brand,
sku."skuItemId" AS intake_sku_item_id,
sku."skuSequence" AS sku_sequence,
sku."batchNo" AS batch_no,
external.to_external_asset_url(sku."image") AS image_url,
sku."imageName" AS image_name,
sku."model" AS model,
sku."skuCode" AS sku_code,
sku."specName" AS spec_name,
sku."barcode" AS barcode,
sku."size" AS size,
sku."color" AS color,
sku."material" AS material,
sku."versionNo" AS version_no,
sku."attributeCode" AS attribute_code,
sku."drawingId" AS drawing_id,
sku."installMethod" AS install_method,
sku."executeStandard" AS execute_standard,
sku."manufacturer" AS manufacturer,
sku."manufacturerCode" AS manufacturer_code,
sku."salesStatus" AS sales_status,
external.to_external_asset_url(sku."model3dDataUrl") AS model_3d_url,
sku."source" AS source,
sku."sourceTable" AS source_table,
sku."sourceSkuId" AS source_sku_id,
sku."missingRequiredFields" AS missing_required_fields,
sku."dataQualityStatus"::text AS data_quality_status,
sku."contentLocked" AS content_locked,
sku."sortOrder" AS sort_order,
CASE
WHEN NULLIF(btrim(COALESCE(product."brand", '')), '') IS NOT NULL
AND NULLIF(btrim(COALESCE(product."spu", '')), '') IS NOT NULL
THEN upper(btrim(product."brand")) || ':' || upper(btrim(product."spu"))
ELSE NULL
END AS product_business_key,
CASE
WHEN NULLIF(btrim(COALESCE(product."brand", '')), '') IS NOT NULL
AND NULLIF(btrim(COALESCE(product."spu", '')), '') IS NOT NULL
AND NULLIF(btrim(COALESCE(sku."skuCode", '')), '') IS NOT NULL
THEN upper(btrim(product."brand")) || ':' || upper(btrim(product."spu")) || ':' || upper(btrim(sku."skuCode"))
ELSE NULL
END AS sku_business_key
FROM "IntakeNonIntegratedSkuItem" sku
JOIN "IntakeNonIntegratedProduct" product ON product."id" = sku."productId"
WHERE product."deletedAt" IS NULL;

View File

@ -0,0 +1,510 @@
-- Add explicit development-owner columns to the external catalog wide view.
--
-- The external main entry remains external.product_catalog_wide_v1. The new
-- columns mirror the product-development list people fields and are appended to
-- the existing wide view so current consumers keep their column order.
CREATE OR REPLACE VIEW external.intake_products_v1 AS
SELECT
product."id" AS intake_product_id,
product."spu" AS spu,
product."spuBase" AS spu_base,
product."brand" AS brand,
profile."productName" AS product_name,
external.to_external_asset_url(profile."productImageUrl") AS product_image_url,
profile."productCategory" AS product_category,
profile."salesCategory" AS sales_category,
profile."productYear" AS product_year,
profile."productStatus" AS product_status,
COALESCE(NULLIF(profile."structureDesigner", ''), NULLIF(product."designer", '')) AS designer,
COALESCE(NULLIF(profile."productPlanning", ''), NULLIF(product."operator", '')) AS operator,
COALESCE(NULLIF(profile."purchaseOwner", ''), NULLIF(product."purchaser", '')) AS purchaser,
profile."productDevelopmentKind" AS product_development_kind,
profile."goodsType" AS goods_type,
profile."materialType" AS material_type,
profile."productionType" AS production_type,
product."createdAt" AS created_at,
product."updatedAt" AS updated_at,
profile."productPlanning" AS product_planning,
profile."visualPlanning" AS visual_planning,
profile."structureDesigner" AS structure_designer,
profile."purchaseOwner" AS purchase_owner,
CASE
WHEN NULLIF(btrim(COALESCE(product."brand", '')), '') IS NOT NULL
AND NULLIF(btrim(COALESCE(product."spu", '')), '') IS NOT NULL
THEN upper(btrim(product."brand")) || ':' || upper(btrim(product."spu"))
ELSE NULL
END AS product_business_key,
NULLIF(btrim(COALESCE(product."designer", '')), '') AS appearance_designer,
COALESCE(
NULLIF(btrim(COALESCE(profile."spuOwner", '')), ''),
NULLIF(btrim(COALESCE(product."operator", '')), ''),
NULLIF(btrim(COALESCE(profile."operationsOwner", '')), '')
) AS spu_owner,
NULLIF(btrim(COALESCE(profile."productPlanning", '')), '') AS planning_owner,
COALESCE(
NULLIF(btrim(COALESCE(profile."visualPlanning", '')), ''),
NULLIF(btrim(COALESCE(profile."visualDirector", '')), '')
) AS space_designer
FROM "IntakeNonIntegratedProduct" product
LEFT JOIN "IntakeNonIntegratedProductProfile" profile ON profile."productId" = product."id"
WHERE product."deletedAt" IS NULL;
CREATE OR REPLACE VIEW external.intake_products_ready_v1 AS
SELECT *
FROM external.intake_products_v1
WHERE NULLIF(btrim(COALESCE(product_name, '')), '') IS NOT NULL
AND NULLIF(btrim(COALESCE(product_category, '')), '') IS NOT NULL
AND NULLIF(btrim(COALESCE(product_development_kind, '')), '') IS NOT NULL
AND NULLIF(btrim(COALESCE(product_business_key, '')), '') IS NOT NULL;
CREATE OR REPLACE VIEW external.product_catalog_wide_v1 AS
WITH formal_sku_source AS (
SELECT
sku.product_id,
sku.product_model_number,
sku.product_name,
sku.product_description,
sku.product_category_id,
sku.product_category_name,
sku.product_thumbnail_url,
sku.product_model_url,
sku.sku_id,
sku.sku_code,
sku.sku_name,
sku.price,
sku.cost_price,
sku.stock,
sku.is_active,
sku.record_status,
sku.model_url,
sku.image_url,
sku.image_urls,
sku.source_pre_sku_id,
sku.source_asset_variant_id,
sku.source_asset_variant_ids,
sku.asset_family_code,
sku.asset_family_name,
sku.spec_code,
sku.spec_name,
sku.barcode,
sku.size_json,
sku.color,
sku.material,
sku.source_status,
sku.synced_at,
sku.warehouse_location_id,
sku.warehouse_location_code,
sku.warehouse_location_name,
sku.created_at,
sku.updated_at,
sku.product_business_key,
sku.sku_business_key,
sku.barcode_business_key,
'FORMAL'::text AS catalog_source,
NULL::text AS appearance_designer,
NULL::text AS structure_designer,
NULL::text AS spu_owner,
NULL::text AS planning_owner,
NULL::text AS space_designer,
NULL::text AS purchase_owner
FROM external.product_skus_v1 sku
),
formal_identity_map AS (
SELECT
barcode_business_key,
CASE
WHEN NULLIF(btrim(COALESCE(sku_code, '')), '') IS NOT NULL
THEN upper(btrim(sku_code))
ELSE NULL
END AS normalized_sku_code
FROM formal_sku_source
),
intake_ready_sku_source AS (
SELECT
product.intake_product_id AS product_id,
NULLIF(btrim(COALESCE(product.spu, '')), '') AS product_model_number,
NULLIF(btrim(COALESCE(product.product_name, '')), '') AS product_name,
NULL::text AS product_description,
NULL::text AS product_category_id,
NULLIF(btrim(COALESCE(product.product_category, '')), '') AS product_category_name,
COALESCE(
NULLIF(btrim(COALESCE(product.product_image_url, '')), ''),
NULLIF(btrim(COALESCE(sku.image_url, '')), '')
) AS product_thumbnail_url,
NULL::text AS product_model_url,
sku.intake_sku_item_id AS sku_id,
NULLIF(btrim(COALESCE(sku.sku_code, '')), '') AS sku_code,
COALESCE(
NULLIF(btrim(COALESCE(sku.spec_name, '')), ''),
NULLIF(btrim(COALESCE(sku.sku_code, '')), '')
) AS sku_name,
NULL::numeric(10,2) AS price,
NULL::numeric(10,2) AS cost_price,
0::integer AS stock,
true AS is_active,
'ACTIVE'::text AS record_status,
NULLIF(btrim(COALESCE(sku.model_3d_url, '')), '') AS model_url,
COALESCE(
NULLIF(btrim(COALESCE(sku.image_url, '')), ''),
NULLIF(btrim(COALESCE(product.product_image_url, '')), '')
) AS image_url,
CASE
WHEN COALESCE(
NULLIF(btrim(COALESCE(sku.image_url, '')), ''),
NULLIF(btrim(COALESCE(product.product_image_url, '')), '')
) IS NOT NULL
THEN ARRAY[
COALESCE(
NULLIF(btrim(COALESCE(sku.image_url, '')), ''),
NULLIF(btrim(COALESCE(product.product_image_url, '')), '')
)
]
ELSE ARRAY[]::text[]
END AS image_urls,
NULL::text AS source_pre_sku_id,
NULL::text AS source_asset_variant_id,
ARRAY[]::text[] AS source_asset_variant_ids,
NULLIF(btrim(COALESCE(sku.model, '')), '') AS asset_family_code,
NULL::text AS asset_family_name,
NULLIF(btrim(COALESCE(sku.attribute_code, '')), '') AS spec_code,
NULLIF(btrim(COALESCE(sku.spec_name, '')), '') AS spec_name,
NULLIF(btrim(COALESCE(sku.barcode, '')), '') AS barcode,
CASE
WHEN NULLIF(btrim(COALESCE(sku.size, '')), '') IS NOT NULL
THEN jsonb_build_object('label', btrim(sku.size))
ELSE NULL::jsonb
END AS size_json,
NULLIF(btrim(COALESCE(sku.color, '')), '') AS color,
NULLIF(btrim(COALESCE(sku.material, '')), '') AS material,
NULL::text AS source_status,
NULL::text AS synced_at,
NULL::text AS warehouse_location_id,
NULL::text AS warehouse_location_code,
NULL::text AS warehouse_location_name,
product.created_at AS created_at,
product.updated_at AS updated_at,
CASE
WHEN NULLIF(btrim(COALESCE(product.spu, '')), '') IS NOT NULL
THEN upper(btrim(product.spu))
ELSE NULL
END AS product_business_key,
CASE
WHEN NULLIF(btrim(COALESCE(sku.sku_code, '')), '') IS NOT NULL
THEN upper(btrim(sku.sku_code))
ELSE NULL
END AS sku_business_key,
CASE
WHEN NULLIF(btrim(COALESCE(sku.barcode, '')), '') IS NOT NULL
THEN upper(btrim(sku.barcode))
ELSE NULL
END AS barcode_business_key,
'INTAKE_READY'::text AS catalog_source,
product.appearance_designer,
product.structure_designer,
product.spu_owner,
product.planning_owner,
product.space_designer,
product.purchase_owner
FROM external.intake_sku_items_ready_v1 sku
JOIN external.intake_products_ready_v1 product
ON product.intake_product_id = sku.intake_product_id
WHERE NOT EXISTS (
SELECT 1
FROM formal_identity_map formal
WHERE (
NULLIF(btrim(COALESCE(sku.barcode, '')), '') IS NOT NULL
AND formal.barcode_business_key = upper(btrim(sku.barcode))
)
OR (
NULLIF(btrim(COALESCE(sku.barcode, '')), '') IS NULL
AND NULLIF(btrim(COALESCE(sku.sku_code, '')), '') IS NOT NULL
AND formal.normalized_sku_code = upper(btrim(sku.sku_code))
)
)
),
sku_source AS (
SELECT * FROM formal_sku_source
UNION ALL
SELECT * FROM intake_ready_sku_source
),
asset_summary AS (
SELECT
product_business_key,
sku_business_key,
COUNT(*) FILTER (WHERE asset_type = 'SKU_IMAGE')::integer AS image_asset_count,
COUNT(*) FILTER (WHERE asset_type = 'SKU_MODEL')::integer AS model_asset_count,
COUNT(*) FILTER (WHERE asset_type = 'SKU_THUMBNAIL')::integer AS thumbnail_asset_count,
MIN(asset_url) FILTER (WHERE asset_type = 'SKU_MODEL') AS first_model_asset_url,
MIN(asset_url) FILTER (WHERE asset_type = 'SKU_THUMBNAIL') AS first_thumbnail_asset_url
FROM external.product_assets_v1
GROUP BY product_business_key, sku_business_key
),
packaged_summary AS (
SELECT
barcode_business_key,
COUNT(*)::integer AS packaged_item_count,
COALESCE(SUM(stock_quantity), 0)::integer AS packaged_stock_quantity
FROM external.packaged_items_v1
WHERE NULLIF(barcode_business_key, '') IS NOT NULL
GROUP BY barcode_business_key
),
quality_summary AS (
SELECT
barcode_business_key,
COUNT(*)::integer AS quality_record_count,
COUNT(*) FILTER (WHERE source_type = 'WAREHOUSE')::integer AS warehouse_quality_record_count,
COUNT(*) FILTER (WHERE source_type = 'FIRST_BATCH')::integer AS first_batch_quality_record_count
FROM external.sku_quality_inspections_v1
WHERE NULLIF(barcode_business_key, '') IS NOT NULL
GROUP BY barcode_business_key
),
latest_quality AS (
SELECT DISTINCT ON (barcode_business_key)
barcode_business_key,
record_key AS latest_quality_record_key,
source_type AS latest_quality_source_type,
batch_no AS latest_quality_batch_no,
inspected_at AS latest_quality_inspected_at,
conclusion AS latest_quality_conclusion,
status AS latest_quality_status
FROM external.sku_quality_inspections_v1
WHERE NULLIF(barcode_business_key, '') IS NOT NULL
ORDER BY barcode_business_key, COALESCE(inspected_at, updated_at, created_at) DESC, created_at DESC
),
limit_by_barcode AS (
SELECT DISTINCT ON (identity_value)
identity_value,
product_business_key AS limit_source_product_business_key,
sku_business_key AS limit_source_sku_business_key,
limit_price_applicable,
limit_price_confirmed,
limit_price_status,
expected_daily_limit_price,
expected_limit_gross_margin_rate,
non_spu_owner_limit_price,
limit_confirmed_at
FROM (
SELECT
upper(btrim(barcode)) AS identity_value,
product_business_key,
sku_business_key,
limit_price_applicable,
limit_price_confirmed,
limit_price_status,
expected_daily_limit_price,
expected_limit_gross_margin_rate,
non_spu_owner_limit_price,
limit_confirmed_at,
updated_at
FROM external.intake_product_limits_v1
WHERE NULLIF(btrim(COALESCE(barcode, '')), '') IS NOT NULL
) limit_rows
WHERE NULLIF(identity_value, '') IS NOT NULL
ORDER BY
identity_value,
limit_price_applicable DESC,
limit_price_confirmed DESC,
limit_confirmed_at DESC NULLS LAST,
updated_at DESC
),
limit_by_sku AS (
SELECT DISTINCT ON (identity_value)
identity_value,
product_business_key AS limit_source_product_business_key,
sku_business_key AS limit_source_sku_business_key,
limit_price_applicable,
limit_price_confirmed,
limit_price_status,
expected_daily_limit_price,
expected_limit_gross_margin_rate,
non_spu_owner_limit_price,
limit_confirmed_at
FROM (
SELECT
upper(btrim(sku_code)) AS identity_value,
product_business_key,
sku_business_key,
limit_price_applicable,
limit_price_confirmed,
limit_price_status,
expected_daily_limit_price,
expected_limit_gross_margin_rate,
non_spu_owner_limit_price,
limit_confirmed_at,
updated_at
FROM external.intake_product_limits_v1
WHERE NULLIF(btrim(COALESCE(sku_code, '')), '') IS NOT NULL
) limit_rows
WHERE NULLIF(identity_value, '') IS NOT NULL
ORDER BY
identity_value,
limit_price_applicable DESC,
limit_price_confirmed DESC,
limit_confirmed_at DESC NULLS LAST,
updated_at DESC
),
catalog_by_sku AS (
SELECT
sku.product_id,
sku.product_model_number,
sku.product_business_key,
sku.product_name,
sku.product_description,
sku.product_category_id,
sku.product_category_name,
sku.product_thumbnail_url,
sku.product_model_url,
sku.sku_id,
sku.sku_code,
sku.sku_business_key,
sku.sku_name,
sku.price,
sku.cost_price,
sku.stock,
sku.is_active,
sku.record_status,
sku.model_url,
sku.image_url,
sku.image_urls,
sku.source_pre_sku_id,
sku.source_asset_variant_id,
sku.source_asset_variant_ids,
sku.asset_family_code,
sku.asset_family_name,
sku.spec_code,
sku.spec_name,
sku.barcode,
sku.barcode_business_key,
sku.size_json,
sku.color,
sku.material,
sku.source_status,
sku.synced_at,
sku.warehouse_location_id,
sku.warehouse_location_code,
sku.warehouse_location_name,
stock.warehouse_zone,
stock.warehouse_aisle,
stock.warehouse_shelf,
stock.warehouse_bin,
stock.warehouse_location_is_active,
stock.warehouse_location_status,
COALESCE(limit_by_barcode.limit_price_applicable, limit_by_sku.limit_price_applicable, false) AS limit_price_applicable,
COALESCE(limit_by_barcode.limit_price_confirmed, limit_by_sku.limit_price_confirmed, false) AS limit_price_confirmed,
COALESCE(limit_by_barcode.limit_price_status, limit_by_sku.limit_price_status, 'NOT_APPLICABLE') AS limit_price_status,
COALESCE(limit_by_barcode.expected_daily_limit_price, limit_by_sku.expected_daily_limit_price) AS expected_daily_limit_price,
COALESCE(limit_by_barcode.expected_limit_gross_margin_rate, limit_by_sku.expected_limit_gross_margin_rate) AS expected_limit_gross_margin_rate,
COALESCE(limit_by_barcode.non_spu_owner_limit_price, limit_by_sku.non_spu_owner_limit_price) AS non_spu_owner_limit_price,
COALESCE(limit_by_barcode.limit_confirmed_at, limit_by_sku.limit_confirmed_at) AS limit_confirmed_at,
COALESCE(limit_by_barcode.limit_source_product_business_key, limit_by_sku.limit_source_product_business_key) AS limit_source_product_business_key,
COALESCE(limit_by_barcode.limit_source_sku_business_key, limit_by_sku.limit_source_sku_business_key) AS limit_source_sku_business_key,
CASE
WHEN sku.catalog_source = 'INTAKE_READY'
THEN CASE WHEN NULLIF(btrim(COALESCE(sku.image_url, '')), '') IS NOT NULL THEN 1 ELSE 0 END
ELSE COALESCE(asset_summary.image_asset_count, 0)
END AS image_asset_count,
CASE
WHEN sku.catalog_source = 'INTAKE_READY'
THEN CASE WHEN NULLIF(btrim(COALESCE(sku.model_url, '')), '') IS NOT NULL THEN 1 ELSE 0 END
ELSE COALESCE(asset_summary.model_asset_count, 0)
END AS model_asset_count,
CASE
WHEN sku.catalog_source = 'INTAKE_READY'
THEN CASE
WHEN COALESCE(
NULLIF(btrim(COALESCE(sku.product_thumbnail_url, '')), ''),
NULLIF(btrim(COALESCE(sku.image_url, '')), '')
) IS NOT NULL
THEN 1
ELSE 0
END
ELSE COALESCE(asset_summary.thumbnail_asset_count, 0)
END AS thumbnail_asset_count,
COALESCE(
asset_summary.first_model_asset_url,
CASE
WHEN sku.catalog_source = 'INTAKE_READY'
THEN NULLIF(btrim(COALESCE(sku.model_url, '')), '')
ELSE NULL
END
) AS first_model_asset_url,
COALESCE(
asset_summary.first_thumbnail_asset_url,
CASE
WHEN sku.catalog_source = 'INTAKE_READY'
THEN COALESCE(
NULLIF(btrim(COALESCE(sku.product_thumbnail_url, '')), ''),
NULLIF(btrim(COALESCE(sku.image_url, '')), '')
)
ELSE NULL
END
) AS first_thumbnail_asset_url,
COALESCE(bom_summary.bom_item_count, 0) AS bom_item_count,
COALESCE(bom_summary.panel_item_count, 0) AS panel_item_count,
COALESCE(bom_summary.hardware_item_count, 0) AS hardware_item_count,
COALESCE(bom_summary.material_codes, ARRAY[]::text[]) AS material_codes,
COALESCE(bom_summary.material_names, ARRAY[]::text[]) AS material_names,
COALESCE(packaged_summary.packaged_item_count, 0) AS packaged_item_count,
COALESCE(packaged_summary.packaged_stock_quantity, 0) AS packaged_stock_quantity,
COALESCE(quality_summary.quality_record_count, 0) AS quality_record_count,
COALESCE(quality_summary.warehouse_quality_record_count, 0) AS warehouse_quality_record_count,
COALESCE(quality_summary.first_batch_quality_record_count, 0) AS first_batch_quality_record_count,
latest_quality.latest_quality_record_key,
latest_quality.latest_quality_source_type,
latest_quality.latest_quality_batch_no,
latest_quality.latest_quality_inspected_at,
latest_quality.latest_quality_conclusion,
latest_quality.latest_quality_status,
sku.created_at,
sku.updated_at,
sku.catalog_source,
sku.appearance_designer,
sku.structure_designer,
sku.spu_owner,
sku.planning_owner,
sku.space_designer,
sku.purchase_owner
FROM sku_source sku
LEFT JOIN external.sku_warehouse_stock_v1 stock
ON stock.barcode_business_key = sku.barcode_business_key
LEFT JOIN limit_by_sku
ON limit_by_sku.identity_value = upper(btrim(COALESCE(sku.sku_code, '')))
LEFT JOIN limit_by_barcode
ON limit_by_barcode.identity_value = sku.barcode_business_key
LEFT JOIN asset_summary
ON asset_summary.product_business_key = sku.product_business_key
AND asset_summary.sku_business_key = sku.sku_business_key
LEFT JOIN LATERAL (
SELECT
COUNT(*)::integer AS bom_item_count,
COUNT(*) FILTER (WHERE item_type = 'PANEL')::integer AS panel_item_count,
COUNT(*) FILTER (WHERE item_type = 'HARDWARE')::integer AS hardware_item_count,
array_remove(array_agg(DISTINCT material_code ORDER BY material_code), NULL) AS material_codes,
array_remove(array_agg(DISTINCT material_name ORDER BY material_name), NULL) AS material_names
FROM external.product_bom_v1 bom
WHERE bom.product_business_key = sku.product_business_key
AND (
bom.sku_business_key = sku.sku_business_key
OR bom.sku_business_key IS NULL
)
) bom_summary ON true
LEFT JOIN packaged_summary
ON packaged_summary.barcode_business_key = sku.barcode_business_key
LEFT JOIN quality_summary
ON quality_summary.barcode_business_key = sku.barcode_business_key
LEFT JOIN latest_quality
ON latest_quality.barcode_business_key = sku.barcode_business_key
)
SELECT DISTINCT ON (catalog_by_sku.barcode_business_key)
catalog_by_sku.*
FROM catalog_by_sku
WHERE catalog_by_sku.barcode_business_key IS NOT NULL
AND btrim(catalog_by_sku.barcode_business_key) <> ''
ORDER BY
catalog_by_sku.barcode_business_key,
CASE WHEN catalog_by_sku.catalog_source IS NULL THEN 1
WHEN catalog_by_sku.catalog_source = 'FORMAL' THEN 0
ELSE 1 END,
catalog_by_sku.updated_at DESC NULLS LAST,
catalog_by_sku.created_at DESC;

View File

@ -0,0 +1,16 @@
CREATE TABLE "InstallationDemoConfig" (
"id" TEXT NOT NULL,
"drawingId" TEXT NOT NULL,
"parts" JSONB NOT NULL,
"steps" JSONB NOT NULL,
"numbering" JSONB,
"uploadedModel" JSONB,
"explodeTemplate" JSONB,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "InstallationDemoConfig_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "InstallationDemoConfig_drawingId_key"
ON "InstallationDemoConfig"("drawingId");

View File

@ -0,0 +1,26 @@
ALTER TABLE "IntakeNonIntegratedSkuItem"
ADD COLUMN "demandFlowNo" TEXT NOT NULL DEFAULT '',
ADD COLUMN "purchaseOwner" TEXT NOT NULL DEFAULT '';
UPDATE "IntakeNonIntegratedSkuItem" AS sku
SET
"demandFlowNo" = COALESCE(
NULLIF(BTRIM(product."payload" -> 'skuBatchFlowNos' ->> sku."batchNo"::text), ''),
NULLIF(BTRIM(profile."demandFlowNo"), ''),
''
),
"purchaseOwner" = COALESCE(
NULLIF(BTRIM(profile."purchaseOwner"), ''),
NULLIF(BTRIM(product."purchaser"), ''),
''
)
FROM "IntakeNonIntegratedProduct" AS product
LEFT JOIN "IntakeNonIntegratedProductProfile" AS profile
ON profile."productId" = product."id"
WHERE sku."productId" = product."id";
CREATE INDEX "IntakeNonIntegratedSkuItem_productId_purchaseOwner_idx"
ON "IntakeNonIntegratedSkuItem"("productId", "purchaseOwner");
CREATE INDEX "IntakeNonIntegratedSkuItem_demandFlowNo_idx"
ON "IntakeNonIntegratedSkuItem"("demandFlowNo");

View File

@ -0,0 +1,12 @@
-- Prisma @updatedAt columns are maintained by application writes, not by a
-- database default. Align fresh migration replay with the current schema and
-- the existing local database.
ALTER TABLE "AiRenderWorkbenchSku"
ALTER COLUMN "updatedAt" DROP DEFAULT;
ALTER TABLE "AiRenderWorkbenchSpu"
ALTER COLUMN "updatedAt" DROP DEFAULT;
ALTER TABLE "BarcodePrintImportRecord"
ALTER COLUMN "updatedAt" DROP DEFAULT;

View File

@ -0,0 +1 @@
ALTER TYPE "BusinessAccountType" ADD VALUE IF NOT EXISTS 'OPERATIONS';

View File

@ -0,0 +1,6 @@
UPDATE "User" AS u
SET "businessRoles" = ARRAY['OPERATIONS'::"BusinessAccountType"]
FROM "Department" AS d
WHERE u."departmentId" = d."id"
AND cardinality(u."businessRoles") = 0
AND regexp_replace(d."fullPath", '^\s*[^>]+>\s*', '') LIKE '运营中心%';

View File

@ -0,0 +1,9 @@
UPDATE "User" AS u
SET "businessRoles" = CASE
WHEN cardinality(u."businessRoles") = 0 THEN ARRAY['OPERATIONS'::"BusinessAccountType"]
ELSE array_append(u."businessRoles", 'OPERATIONS'::"BusinessAccountType")
END
FROM "Department" AS d
WHERE u."departmentId" = d."id"
AND split_part(d."fullPath", ' > ', 2) LIKE '运营中心%'
AND NOT ('OPERATIONS'::"BusinessAccountType" = ANY(u."businessRoles"));

View File

@ -0,0 +1,2 @@
ALTER TYPE "BusinessAccountType" ADD VALUE IF NOT EXISTS 'GENERAL';
ALTER TYPE "BusinessAccountType" ADD VALUE IF NOT EXISTS 'STRATEGY';

View File

@ -0,0 +1,30 @@
UPDATE "User" AS u
SET "businessRoles" = CASE
WHEN cardinality(array_remove(u."businessRoles", 'GENERAL'::"BusinessAccountType")) = 0 THEN ARRAY['PLANNING'::"BusinessAccountType"]
ELSE array_append(array_remove(u."businessRoles", 'GENERAL'::"BusinessAccountType"), 'PLANNING'::"BusinessAccountType")
END
FROM "Department" AS d
WHERE u."departmentId" = d."id"
AND split_part(d."fullPath", ' > ', 2) = '品牌中心'
AND (
d."fullPath" LIKE '% > 空间视觉部%'
OR d."fullPath" LIKE '% > 商品企划部%'
)
AND NOT ('PLANNING'::"BusinessAccountType" = ANY(u."businessRoles"));
UPDATE "User" AS u
SET "businessRoles" = CASE
WHEN cardinality(array_remove(u."businessRoles", 'GENERAL'::"BusinessAccountType")) = 0 THEN ARRAY['STRATEGY'::"BusinessAccountType"]
ELSE array_append(array_remove(u."businessRoles", 'GENERAL'::"BusinessAccountType"), 'STRATEGY'::"BusinessAccountType")
END
FROM "Department" AS d
WHERE u."departmentId" = d."id"
AND split_part(d."fullPath", ' > ', 2) = '品牌中心'
AND d."fullPath" NOT LIKE '% > 空间视觉部%'
AND d."fullPath" NOT LIKE '% > 商品企划部%'
AND NOT ('STRATEGY'::"BusinessAccountType" = ANY(u."businessRoles"));
UPDATE "User"
SET "businessRoles" = ARRAY['GENERAL'::"BusinessAccountType"]
WHERE cardinality("businessRoles") = 0
AND (NULLIF(BTRIM(COALESCE("supplierCode", '')), '') IS NULL);

View File

@ -0,0 +1 @@
ALTER TYPE "BusinessAccountType" ADD VALUE IF NOT EXISTS 'CUSTOMER_SERVICE';

View File

@ -0,0 +1,20 @@
WITH customer_service_department AS (
SELECT U&'\5BA2\670D\4E2D\5FC3'::text AS name
)
UPDATE "User" AS u
SET "businessRoles" = CASE
WHEN cardinality(array_remove(COALESCE(u."businessRoles", ARRAY[]::"BusinessAccountType"[]), 'GENERAL'::"BusinessAccountType")) = 0
THEN ARRAY['CUSTOMER_SERVICE'::"BusinessAccountType"]
ELSE array_append(
array_remove(COALESCE(u."businessRoles", ARRAY[]::"BusinessAccountType"[]), 'GENERAL'::"BusinessAccountType"),
'CUSTOMER_SERVICE'::"BusinessAccountType"
)
END
FROM "Department" AS d, customer_service_department AS cs
WHERE u."departmentId" = d."id"
AND (
d."fullPath" = cs.name
OR split_part(d."fullPath", ' > ', 2) = cs.name
OR d."fullPath" LIKE ('% > ' || cs.name || '%')
)
AND NOT ('CUSTOMER_SERVICE'::"BusinessAccountType" = ANY(COALESCE(u."businessRoles", ARRAY[]::"BusinessAccountType"[])));

View File

@ -0,0 +1,13 @@
ALTER TABLE "IntakeNonIntegratedPricingRow"
ADD COLUMN "jstVirtualInventoryRetryCount" INTEGER NOT NULL DEFAULT 0,
ADD COLUMN "jstVirtualInventoryLastAttemptAt" TIMESTAMP(3);
ALTER TABLE "IntakeNonIntegratedPricingRow"
ADD CONSTRAINT "IntakeNonIntegratedPricingRow_virtual_inventory_retry_count_valid"
CHECK ("jstVirtualInventoryRetryCount" >= 0);
CREATE INDEX "IntakeNonIntegratedPricingRow_virtual_inventory_due_idx"
ON "IntakeNonIntegratedPricingRow" (
"jstVirtualInventoryStatus",
"jstVirtualInventoryScheduledAt"
);

View File

@ -0,0 +1,38 @@
CREATE TYPE "OperationLogAction" AS ENUM (
'INTAKE_PRODUCT_WRITE',
'JUSHUITAN_PRODUCT_PUSH',
'JUSHUITAN_VIRTUAL_INVENTORY_PUSH'
);
CREATE TYPE "OperationLogStatus" AS ENUM ('SUCCESS', 'FAILED', 'RETRY');
CREATE TYPE "OperationLogSource" AS ENUM ('USER', 'SYSTEM');
CREATE TABLE "OperationLog" (
"id" TEXT NOT NULL,
"action" "OperationLogAction" NOT NULL,
"status" "OperationLogStatus" NOT NULL,
"source" "OperationLogSource" NOT NULL DEFAULT 'USER',
"actorId" TEXT,
"actorName" TEXT NOT NULL,
"productId" TEXT,
"productSpu" TEXT,
"skuId" TEXT,
"skuCode" TEXT,
"workspaceCategory" TEXT,
"errorCode" TEXT,
"message" TEXT NOT NULL,
"requestId" TEXT,
"details" JSONB,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "OperationLog_pkey" PRIMARY KEY ("id"),
CONSTRAINT "OperationLog_actorId_fkey"
FOREIGN KEY ("actorId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE
);
CREATE INDEX "OperationLog_status_createdAt_idx" ON "OperationLog"("status", "createdAt");
CREATE INDEX "OperationLog_action_createdAt_idx" ON "OperationLog"("action", "createdAt");
CREATE INDEX "OperationLog_actorId_createdAt_idx" ON "OperationLog"("actorId", "createdAt");
CREATE INDEX "OperationLog_productSpu_createdAt_idx" ON "OperationLog"("productSpu", "createdAt");
CREATE INDEX "OperationLog_productId_createdAt_idx" ON "OperationLog"("productId", "createdAt");

View File

@ -0,0 +1,659 @@
-- Move product-factory fields out of the compatibility payload and into the
-- structured non-integrated SKU truth. The payload is used only for this
-- one-time backfill; runtime reads and the external contract use columns.
ALTER TABLE "IntakeNonIntegratedSkuItem"
ADD COLUMN IF NOT EXISTS "factoryWarehouse" TEXT NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS "shippingMethod" TEXT NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS "minimumOrderQuantity" TEXT NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS "taxIncludedUnitPrice" TEXT NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS "leadTime" TEXT NOT NULL DEFAULT '';
WITH factory_rows AS (
SELECT DISTINCT ON (product."id", factory_row.item ->> 'skuId')
product."id" AS product_id,
factory_row.item ->> 'skuId' AS sku_item_id,
factory_row.item
FROM "IntakeNonIntegratedProduct" product
CROSS JOIN LATERAL jsonb_array_elements(
CASE
WHEN jsonb_typeof(product."payload" -> 'factoryRows') = 'array'
THEN product."payload" -> 'factoryRows'
ELSE '[]'::jsonb
END
) WITH ORDINALITY AS factory_row(item, ordinal_position)
WHERE NULLIF(btrim(COALESCE(factory_row.item ->> 'skuId', '')), '') IS NOT NULL
ORDER BY product."id", factory_row.item ->> 'skuId', factory_row.ordinal_position DESC
)
UPDATE "IntakeNonIntegratedSkuItem" sku
SET
"manufacturer" = COALESCE(
NULLIF(btrim(sku."manufacturer"), ''),
btrim(COALESCE(factory.item ->> 'supplierName', ''))
),
"manufacturerCode" = COALESCE(
NULLIF(btrim(sku."manufacturerCode"), ''),
upper(btrim(COALESCE(factory.item ->> 'supplierCode', '')))
),
"installMethod" = COALESCE(
NULLIF(btrim(sku."installMethod"), ''),
btrim(COALESCE(factory.item ->> 'installMethod', ''))
),
"factoryWarehouse" = COALESCE(
NULLIF(btrim(sku."factoryWarehouse"), ''),
btrim(COALESCE(factory.item ->> 'warehouse', ''))
),
"shippingMethod" = COALESCE(
NULLIF(btrim(sku."shippingMethod"), ''),
btrim(COALESCE(factory.item ->> 'shippingMethod', ''))
),
"minimumOrderQuantity" = COALESCE(
NULLIF(btrim(sku."minimumOrderQuantity"), ''),
btrim(COALESCE(factory.item ->> 'minimumOrderQuantity', ''))
),
"taxIncludedUnitPrice" = COALESCE(
NULLIF(btrim(sku."taxIncludedUnitPrice"), ''),
btrim(COALESCE(factory.item ->> 'taxIncludedUnitPrice', ''))
),
"leadTime" = COALESCE(
NULLIF(btrim(sku."leadTime"), ''),
btrim(COALESCE(factory.item ->> 'leadTime', ''))
)
FROM factory_rows factory
WHERE sku."productId" = factory.product_id
AND sku."skuItemId" = factory.sku_item_id;
COMMENT ON COLUMN "IntakeNonIntegratedSkuItem"."factoryWarehouse" IS
'Product-factory shipping warehouse; not the inventory warehouse location.';
CREATE OR REPLACE VIEW external.intake_sku_items_v1 AS
SELECT
product."id" AS intake_product_id,
product."spu" AS spu,
product."brand" AS brand,
sku."skuItemId" AS intake_sku_item_id,
sku."skuSequence" AS sku_sequence,
sku."batchNo" AS batch_no,
external.to_external_asset_url(sku."image") AS image_url,
sku."imageName" AS image_name,
sku."model" AS model,
sku."skuCode" AS sku_code,
sku."specName" AS spec_name,
sku."barcode" AS barcode,
sku."size" AS size,
sku."color" AS color,
sku."material" AS material,
sku."versionNo" AS version_no,
sku."attributeCode" AS attribute_code,
sku."drawingId" AS drawing_id,
sku."installMethod" AS install_method,
sku."executeStandard" AS execute_standard,
sku."manufacturer" AS manufacturer,
sku."manufacturerCode" AS manufacturer_code,
sku."salesStatus" AS sales_status,
external.to_external_asset_url(sku."model3dDataUrl") AS model_3d_url,
sku."source" AS source,
sku."sourceTable" AS source_table,
sku."sourceSkuId" AS source_sku_id,
sku."missingRequiredFields" AS missing_required_fields,
sku."dataQualityStatus"::text AS data_quality_status,
sku."contentLocked" AS content_locked,
sku."sortOrder" AS sort_order,
CASE
WHEN NULLIF(btrim(COALESCE(product."brand", '')), '') IS NOT NULL
AND NULLIF(btrim(COALESCE(product."spu", '')), '') IS NOT NULL
THEN upper(btrim(product."brand")) || ':' || upper(btrim(product."spu"))
ELSE NULL
END AS product_business_key,
CASE
WHEN NULLIF(btrim(COALESCE(product."brand", '')), '') IS NOT NULL
AND NULLIF(btrim(COALESCE(product."spu", '')), '') IS NOT NULL
AND NULLIF(btrim(COALESCE(sku."skuCode", '')), '') IS NOT NULL
THEN upper(btrim(product."brand")) || ':' || upper(btrim(product."spu")) || ':' || upper(btrim(sku."skuCode"))
ELSE NULL
END AS sku_business_key,
sku."factoryWarehouse" AS factory_warehouse,
sku."shippingMethod" AS shipping_method,
sku."minimumOrderQuantity" AS minimum_order_quantity,
sku."taxIncludedUnitPrice" AS tax_included_unit_price,
sku."leadTime" AS lead_time
FROM "IntakeNonIntegratedSkuItem" sku
JOIN "IntakeNonIntegratedProduct" product ON product."id" = sku."productId"
WHERE product."deletedAt" IS NULL;
CREATE OR REPLACE VIEW external.intake_sku_items_ready_v1 AS
SELECT sku.*
FROM external.intake_sku_items_v1 sku
JOIN external.intake_products_ready_v1 product
ON product.intake_product_id = sku.intake_product_id
WHERE sku.data_quality_status = 'READY'
AND NULLIF(btrim(COALESCE(sku.sku_business_key, '')), '') IS NOT NULL;
CREATE OR REPLACE VIEW external.intake_products_v1 AS
SELECT
product."id" AS intake_product_id,
product."spu" AS spu,
product."spuBase" AS spu_base,
product."brand" AS brand,
profile."productName" AS product_name,
external.to_external_asset_url(profile."productImageUrl") AS product_image_url,
profile."productCategory" AS product_category,
profile."salesCategory" AS sales_category,
profile."productYear" AS product_year,
profile."productStatus" AS product_status,
COALESCE(NULLIF(profile."structureDesigner", ''), NULLIF(product."designer", '')) AS designer,
COALESCE(NULLIF(profile."productPlanning", ''), NULLIF(product."operator", '')) AS operator,
COALESCE(NULLIF(profile."purchaseOwner", ''), NULLIF(product."purchaser", '')) AS purchaser,
profile."productDevelopmentKind" AS product_development_kind,
profile."goodsType" AS goods_type,
profile."materialType" AS material_type,
profile."productionType" AS production_type,
product."createdAt" AS created_at,
product."updatedAt" AS updated_at,
profile."productPlanning" AS product_planning,
profile."visualPlanning" AS visual_planning,
profile."structureDesigner" AS structure_designer,
profile."purchaseOwner" AS purchase_owner,
CASE
WHEN NULLIF(btrim(COALESCE(product."brand", '')), '') IS NOT NULL
AND NULLIF(btrim(COALESCE(product."spu", '')), '') IS NOT NULL
THEN upper(btrim(product."brand")) || ':' || upper(btrim(product."spu"))
ELSE NULL
END AS product_business_key,
NULLIF(btrim(COALESCE(product."designer", '')), '') AS appearance_designer,
COALESCE(
NULLIF(btrim(COALESCE(profile."spuOwner", '')), ''),
NULLIF(btrim(COALESCE(product."operator", '')), ''),
NULLIF(btrim(COALESCE(profile."operationsOwner", '')), '')
) AS spu_owner,
NULLIF(btrim(COALESCE(profile."productPlanning", '')), '') AS planning_owner,
COALESCE(
NULLIF(btrim(COALESCE(profile."visualPlanning", '')), ''),
NULLIF(btrim(COALESCE(profile."visualDirector", '')), '')
) AS space_designer
FROM "IntakeNonIntegratedProduct" product
LEFT JOIN "IntakeNonIntegratedProductProfile" profile ON profile."productId" = product."id"
WHERE product."deletedAt" IS NULL;
CREATE OR REPLACE VIEW external.intake_products_ready_v1 AS
SELECT *
FROM external.intake_products_v1
WHERE NULLIF(btrim(COALESCE(product_name, '')), '') IS NOT NULL
AND NULLIF(btrim(COALESCE(product_category, '')), '') IS NOT NULL
AND NULLIF(btrim(COALESCE(product_development_kind, '')), '') IS NOT NULL
AND NULLIF(btrim(COALESCE(product_business_key, '')), '') IS NOT NULL;
CREATE OR REPLACE VIEW external.product_catalog_wide_v1 AS
WITH formal_sku_source AS (
SELECT
sku.product_id,
sku.product_model_number,
sku.product_name,
sku.product_description,
sku.product_category_id,
sku.product_category_name,
sku.product_thumbnail_url,
sku.product_model_url,
sku.sku_id,
sku.sku_code,
sku.sku_name,
sku.price,
sku.cost_price,
sku.stock,
sku.is_active,
sku.record_status,
sku.model_url,
sku.image_url,
sku.image_urls,
sku.source_pre_sku_id,
sku.source_asset_variant_id,
sku.source_asset_variant_ids,
sku.asset_family_code,
sku.asset_family_name,
sku.spec_code,
sku.spec_name,
sku.barcode,
sku.size_json,
sku.color,
sku.material,
sku.source_status,
sku.synced_at,
sku.warehouse_location_id,
sku.warehouse_location_code,
sku.warehouse_location_name,
sku.created_at,
sku.updated_at,
sku.product_business_key,
sku.sku_business_key,
sku.barcode_business_key,
'FORMAL'::text AS catalog_source,
NULL::text AS appearance_designer,
NULL::text AS structure_designer,
NULL::text AS spu_owner,
NULL::text AS planning_owner,
NULL::text AS space_designer,
NULL::text AS purchase_owner,
NULL::text AS supplier_name,
NULL::text AS supplier_code,
NULL::text AS factory_warehouse,
NULL::text AS shipping_method,
NULL::text AS minimum_order_quantity,
NULL::text AS tax_included_unit_price,
NULL::text AS lead_time,
NULL::text AS install_method
FROM external.product_skus_v1 sku
),
formal_identity_map AS (
SELECT
barcode_business_key,
CASE
WHEN NULLIF(btrim(COALESCE(sku_code, '')), '') IS NOT NULL
THEN upper(btrim(sku_code))
ELSE NULL
END AS normalized_sku_code
FROM formal_sku_source
),
intake_ready_sku_source AS (
SELECT
product.intake_product_id AS product_id,
NULLIF(btrim(COALESCE(product.spu, '')), '') AS product_model_number,
NULLIF(btrim(COALESCE(product.product_name, '')), '') AS product_name,
NULL::text AS product_description,
NULL::text AS product_category_id,
NULLIF(btrim(COALESCE(product.product_category, '')), '') AS product_category_name,
COALESCE(
NULLIF(btrim(COALESCE(product.product_image_url, '')), ''),
NULLIF(btrim(COALESCE(sku.image_url, '')), '')
) AS product_thumbnail_url,
NULL::text AS product_model_url,
sku.intake_sku_item_id AS sku_id,
NULLIF(btrim(COALESCE(sku.sku_code, '')), '') AS sku_code,
COALESCE(
NULLIF(btrim(COALESCE(sku.spec_name, '')), ''),
NULLIF(btrim(COALESCE(sku.sku_code, '')), '')
) AS sku_name,
NULL::numeric(10,2) AS price,
NULL::numeric(10,2) AS cost_price,
0::integer AS stock,
true AS is_active,
'ACTIVE'::text AS record_status,
NULLIF(btrim(COALESCE(sku.model_3d_url, '')), '') AS model_url,
COALESCE(
NULLIF(btrim(COALESCE(sku.image_url, '')), ''),
NULLIF(btrim(COALESCE(product.product_image_url, '')), '')
) AS image_url,
CASE
WHEN COALESCE(
NULLIF(btrim(COALESCE(sku.image_url, '')), ''),
NULLIF(btrim(COALESCE(product.product_image_url, '')), '')
) IS NOT NULL
THEN ARRAY[
COALESCE(
NULLIF(btrim(COALESCE(sku.image_url, '')), ''),
NULLIF(btrim(COALESCE(product.product_image_url, '')), '')
)
]
ELSE ARRAY[]::text[]
END AS image_urls,
NULL::text AS source_pre_sku_id,
NULL::text AS source_asset_variant_id,
ARRAY[]::text[] AS source_asset_variant_ids,
NULLIF(btrim(COALESCE(sku.model, '')), '') AS asset_family_code,
NULL::text AS asset_family_name,
NULLIF(btrim(COALESCE(sku.attribute_code, '')), '') AS spec_code,
NULLIF(btrim(COALESCE(sku.spec_name, '')), '') AS spec_name,
NULLIF(btrim(COALESCE(sku.barcode, '')), '') AS barcode,
CASE
WHEN NULLIF(btrim(COALESCE(sku.size, '')), '') IS NOT NULL
THEN jsonb_build_object('label', btrim(sku.size))
ELSE NULL::jsonb
END AS size_json,
NULLIF(btrim(COALESCE(sku.color, '')), '') AS color,
NULLIF(btrim(COALESCE(sku.material, '')), '') AS material,
NULL::text AS source_status,
NULL::text AS synced_at,
NULL::text AS warehouse_location_id,
NULL::text AS warehouse_location_code,
NULL::text AS warehouse_location_name,
product.created_at AS created_at,
product.updated_at AS updated_at,
CASE
WHEN NULLIF(btrim(COALESCE(product.spu, '')), '') IS NOT NULL
THEN upper(btrim(product.spu))
ELSE NULL
END AS product_business_key,
CASE
WHEN NULLIF(btrim(COALESCE(sku.sku_code, '')), '') IS NOT NULL
THEN upper(btrim(sku.sku_code))
ELSE NULL
END AS sku_business_key,
CASE
WHEN NULLIF(btrim(COALESCE(sku.barcode, '')), '') IS NOT NULL
THEN upper(btrim(sku.barcode))
ELSE NULL
END AS barcode_business_key,
'INTAKE_READY'::text AS catalog_source,
product.appearance_designer,
product.structure_designer,
product.spu_owner,
product.planning_owner,
product.space_designer,
product.purchase_owner,
NULLIF(btrim(COALESCE(sku.manufacturer, '')), '') AS supplier_name,
NULLIF(btrim(COALESCE(sku.manufacturer_code, '')), '') AS supplier_code,
NULLIF(btrim(COALESCE(sku.factory_warehouse, '')), '') AS factory_warehouse,
NULLIF(btrim(COALESCE(sku.shipping_method, '')), '') AS shipping_method,
NULLIF(btrim(COALESCE(sku.minimum_order_quantity, '')), '') AS minimum_order_quantity,
NULLIF(btrim(COALESCE(sku.tax_included_unit_price, '')), '') AS tax_included_unit_price,
NULLIF(btrim(COALESCE(sku.lead_time, '')), '') AS lead_time,
NULLIF(btrim(COALESCE(sku.install_method, '')), '') AS install_method
FROM external.intake_sku_items_ready_v1 sku
JOIN external.intake_products_ready_v1 product
ON product.intake_product_id = sku.intake_product_id
WHERE NOT EXISTS (
SELECT 1
FROM formal_identity_map formal
WHERE (
NULLIF(btrim(COALESCE(sku.barcode, '')), '') IS NOT NULL
AND formal.barcode_business_key = upper(btrim(sku.barcode))
)
OR (
NULLIF(btrim(COALESCE(sku.barcode, '')), '') IS NULL
AND NULLIF(btrim(COALESCE(sku.sku_code, '')), '') IS NOT NULL
AND formal.normalized_sku_code = upper(btrim(sku.sku_code))
)
)
),
sku_source AS (
SELECT * FROM formal_sku_source
UNION ALL
SELECT * FROM intake_ready_sku_source
),
asset_summary AS (
SELECT
product_business_key,
sku_business_key,
COUNT(*) FILTER (WHERE asset_type = 'SKU_IMAGE')::integer AS image_asset_count,
COUNT(*) FILTER (WHERE asset_type = 'SKU_MODEL')::integer AS model_asset_count,
COUNT(*) FILTER (WHERE asset_type = 'SKU_THUMBNAIL')::integer AS thumbnail_asset_count,
MIN(asset_url) FILTER (WHERE asset_type = 'SKU_MODEL') AS first_model_asset_url,
MIN(asset_url) FILTER (WHERE asset_type = 'SKU_THUMBNAIL') AS first_thumbnail_asset_url
FROM external.product_assets_v1
GROUP BY product_business_key, sku_business_key
),
packaged_summary AS (
SELECT
barcode_business_key,
COUNT(*)::integer AS packaged_item_count,
COALESCE(SUM(stock_quantity), 0)::integer AS packaged_stock_quantity
FROM external.packaged_items_v1
WHERE NULLIF(barcode_business_key, '') IS NOT NULL
GROUP BY barcode_business_key
),
quality_summary AS (
SELECT
barcode_business_key,
COUNT(*)::integer AS quality_record_count,
COUNT(*) FILTER (WHERE source_type = 'WAREHOUSE')::integer AS warehouse_quality_record_count,
COUNT(*) FILTER (WHERE source_type = 'FIRST_BATCH')::integer AS first_batch_quality_record_count
FROM external.sku_quality_inspections_v1
WHERE NULLIF(barcode_business_key, '') IS NOT NULL
GROUP BY barcode_business_key
),
latest_quality AS (
SELECT DISTINCT ON (barcode_business_key)
barcode_business_key,
record_key AS latest_quality_record_key,
source_type AS latest_quality_source_type,
batch_no AS latest_quality_batch_no,
inspected_at AS latest_quality_inspected_at,
conclusion AS latest_quality_conclusion,
status AS latest_quality_status
FROM external.sku_quality_inspections_v1
WHERE NULLIF(barcode_business_key, '') IS NOT NULL
ORDER BY barcode_business_key, COALESCE(inspected_at, updated_at, created_at) DESC, created_at DESC
),
limit_by_barcode AS (
SELECT DISTINCT ON (identity_value)
identity_value,
product_business_key AS limit_source_product_business_key,
sku_business_key AS limit_source_sku_business_key,
limit_price_applicable,
limit_price_confirmed,
limit_price_status,
expected_daily_limit_price,
expected_limit_gross_margin_rate,
non_spu_owner_limit_price,
limit_confirmed_at
FROM (
SELECT
upper(btrim(barcode)) AS identity_value,
product_business_key,
sku_business_key,
limit_price_applicable,
limit_price_confirmed,
limit_price_status,
expected_daily_limit_price,
expected_limit_gross_margin_rate,
non_spu_owner_limit_price,
limit_confirmed_at,
updated_at
FROM external.intake_product_limits_v1
WHERE NULLIF(btrim(COALESCE(barcode, '')), '') IS NOT NULL
) limit_rows
WHERE NULLIF(identity_value, '') IS NOT NULL
ORDER BY
identity_value,
limit_price_applicable DESC,
limit_price_confirmed DESC,
limit_confirmed_at DESC NULLS LAST,
updated_at DESC
),
limit_by_sku AS (
SELECT DISTINCT ON (identity_value)
identity_value,
product_business_key AS limit_source_product_business_key,
sku_business_key AS limit_source_sku_business_key,
limit_price_applicable,
limit_price_confirmed,
limit_price_status,
expected_daily_limit_price,
expected_limit_gross_margin_rate,
non_spu_owner_limit_price,
limit_confirmed_at
FROM (
SELECT
upper(btrim(sku_code)) AS identity_value,
product_business_key,
sku_business_key,
limit_price_applicable,
limit_price_confirmed,
limit_price_status,
expected_daily_limit_price,
expected_limit_gross_margin_rate,
non_spu_owner_limit_price,
limit_confirmed_at,
updated_at
FROM external.intake_product_limits_v1
WHERE NULLIF(btrim(COALESCE(sku_code, '')), '') IS NOT NULL
) limit_rows
WHERE NULLIF(identity_value, '') IS NOT NULL
ORDER BY
identity_value,
limit_price_applicable DESC,
limit_price_confirmed DESC,
limit_confirmed_at DESC NULLS LAST,
updated_at DESC
),
catalog_by_sku AS (
SELECT
sku.product_id,
sku.product_model_number,
sku.product_business_key,
sku.product_name,
sku.product_description,
sku.product_category_id,
sku.product_category_name,
sku.product_thumbnail_url,
sku.product_model_url,
sku.sku_id,
sku.sku_code,
sku.sku_business_key,
sku.sku_name,
sku.price,
sku.cost_price,
sku.stock,
sku.is_active,
sku.record_status,
sku.model_url,
sku.image_url,
sku.image_urls,
sku.source_pre_sku_id,
sku.source_asset_variant_id,
sku.source_asset_variant_ids,
sku.asset_family_code,
sku.asset_family_name,
sku.spec_code,
sku.spec_name,
sku.barcode,
sku.barcode_business_key,
sku.size_json,
sku.color,
sku.material,
sku.source_status,
sku.synced_at,
sku.warehouse_location_id,
sku.warehouse_location_code,
sku.warehouse_location_name,
stock.warehouse_zone,
stock.warehouse_aisle,
stock.warehouse_shelf,
stock.warehouse_bin,
stock.warehouse_location_is_active,
stock.warehouse_location_status,
COALESCE(limit_by_barcode.limit_price_applicable, limit_by_sku.limit_price_applicable, false) AS limit_price_applicable,
COALESCE(limit_by_barcode.limit_price_confirmed, limit_by_sku.limit_price_confirmed, false) AS limit_price_confirmed,
COALESCE(limit_by_barcode.limit_price_status, limit_by_sku.limit_price_status, 'NOT_APPLICABLE') AS limit_price_status,
COALESCE(limit_by_barcode.expected_daily_limit_price, limit_by_sku.expected_daily_limit_price) AS expected_daily_limit_price,
COALESCE(limit_by_barcode.expected_limit_gross_margin_rate, limit_by_sku.expected_limit_gross_margin_rate) AS expected_limit_gross_margin_rate,
COALESCE(limit_by_barcode.non_spu_owner_limit_price, limit_by_sku.non_spu_owner_limit_price) AS non_spu_owner_limit_price,
COALESCE(limit_by_barcode.limit_confirmed_at, limit_by_sku.limit_confirmed_at) AS limit_confirmed_at,
COALESCE(limit_by_barcode.limit_source_product_business_key, limit_by_sku.limit_source_product_business_key) AS limit_source_product_business_key,
COALESCE(limit_by_barcode.limit_source_sku_business_key, limit_by_sku.limit_source_sku_business_key) AS limit_source_sku_business_key,
CASE
WHEN sku.catalog_source = 'INTAKE_READY'
THEN CASE WHEN NULLIF(btrim(COALESCE(sku.image_url, '')), '') IS NOT NULL THEN 1 ELSE 0 END
ELSE COALESCE(asset_summary.image_asset_count, 0)
END AS image_asset_count,
CASE
WHEN sku.catalog_source = 'INTAKE_READY'
THEN CASE WHEN NULLIF(btrim(COALESCE(sku.model_url, '')), '') IS NOT NULL THEN 1 ELSE 0 END
ELSE COALESCE(asset_summary.model_asset_count, 0)
END AS model_asset_count,
CASE
WHEN sku.catalog_source = 'INTAKE_READY'
THEN CASE
WHEN COALESCE(
NULLIF(btrim(COALESCE(sku.product_thumbnail_url, '')), ''),
NULLIF(btrim(COALESCE(sku.image_url, '')), '')
) IS NOT NULL
THEN 1
ELSE 0
END
ELSE COALESCE(asset_summary.thumbnail_asset_count, 0)
END AS thumbnail_asset_count,
COALESCE(
asset_summary.first_model_asset_url,
CASE
WHEN sku.catalog_source = 'INTAKE_READY'
THEN NULLIF(btrim(COALESCE(sku.model_url, '')), '')
ELSE NULL
END
) AS first_model_asset_url,
COALESCE(
asset_summary.first_thumbnail_asset_url,
CASE
WHEN sku.catalog_source = 'INTAKE_READY'
THEN COALESCE(
NULLIF(btrim(COALESCE(sku.product_thumbnail_url, '')), ''),
NULLIF(btrim(COALESCE(sku.image_url, '')), '')
)
ELSE NULL
END
) AS first_thumbnail_asset_url,
COALESCE(bom_summary.bom_item_count, 0) AS bom_item_count,
COALESCE(bom_summary.panel_item_count, 0) AS panel_item_count,
COALESCE(bom_summary.hardware_item_count, 0) AS hardware_item_count,
COALESCE(bom_summary.material_codes, ARRAY[]::text[]) AS material_codes,
COALESCE(bom_summary.material_names, ARRAY[]::text[]) AS material_names,
COALESCE(packaged_summary.packaged_item_count, 0) AS packaged_item_count,
COALESCE(packaged_summary.packaged_stock_quantity, 0) AS packaged_stock_quantity,
COALESCE(quality_summary.quality_record_count, 0) AS quality_record_count,
COALESCE(quality_summary.warehouse_quality_record_count, 0) AS warehouse_quality_record_count,
COALESCE(quality_summary.first_batch_quality_record_count, 0) AS first_batch_quality_record_count,
latest_quality.latest_quality_record_key,
latest_quality.latest_quality_source_type,
latest_quality.latest_quality_batch_no,
latest_quality.latest_quality_inspected_at,
latest_quality.latest_quality_conclusion,
latest_quality.latest_quality_status,
sku.created_at,
sku.updated_at,
sku.catalog_source,
sku.appearance_designer,
sku.structure_designer,
sku.spu_owner,
sku.planning_owner,
sku.space_designer,
sku.purchase_owner,
sku.supplier_name,
sku.supplier_code,
sku.factory_warehouse,
sku.shipping_method,
sku.minimum_order_quantity,
sku.tax_included_unit_price,
sku.lead_time,
sku.install_method
FROM sku_source sku
LEFT JOIN external.sku_warehouse_stock_v1 stock
ON stock.barcode_business_key = sku.barcode_business_key
LEFT JOIN limit_by_sku
ON limit_by_sku.identity_value = upper(btrim(COALESCE(sku.sku_code, '')))
LEFT JOIN limit_by_barcode
ON limit_by_barcode.identity_value = sku.barcode_business_key
LEFT JOIN asset_summary
ON asset_summary.product_business_key = sku.product_business_key
AND asset_summary.sku_business_key = sku.sku_business_key
LEFT JOIN LATERAL (
SELECT
COUNT(*)::integer AS bom_item_count,
COUNT(*) FILTER (WHERE item_type = 'PANEL')::integer AS panel_item_count,
COUNT(*) FILTER (WHERE item_type = 'HARDWARE')::integer AS hardware_item_count,
array_remove(array_agg(DISTINCT material_code ORDER BY material_code), NULL) AS material_codes,
array_remove(array_agg(DISTINCT material_name ORDER BY material_name), NULL) AS material_names
FROM external.product_bom_v1 bom
WHERE bom.product_business_key = sku.product_business_key
AND (
bom.sku_business_key = sku.sku_business_key
OR bom.sku_business_key IS NULL
)
) bom_summary ON true
LEFT JOIN packaged_summary
ON packaged_summary.barcode_business_key = sku.barcode_business_key
LEFT JOIN quality_summary
ON quality_summary.barcode_business_key = sku.barcode_business_key
LEFT JOIN latest_quality
ON latest_quality.barcode_business_key = sku.barcode_business_key
)
SELECT DISTINCT ON (catalog_by_sku.barcode_business_key)
catalog_by_sku.*
FROM catalog_by_sku
WHERE catalog_by_sku.barcode_business_key IS NOT NULL
AND btrim(catalog_by_sku.barcode_business_key) <> ''
ORDER BY
catalog_by_sku.barcode_business_key,
CASE WHEN catalog_by_sku.catalog_source IS NULL THEN 1
WHEN catalog_by_sku.catalog_source = 'FORMAL' THEN 0
ELSE 1 END,
catalog_by_sku.updated_at DESC NULLS LAST,
catalog_by_sku.created_at DESC;

View File

@ -0,0 +1,18 @@
ALTER TABLE "InstallationDemoConfig"
ADD COLUMN IF NOT EXISTS "toolIds" JSONB;
CREATE TABLE IF NOT EXISTS "InstallationDemoTool" (
"id" TEXT NOT NULL,
"toolCode" TEXT NOT NULL,
"name" TEXT NOT NULL,
"modelUrl" TEXT NOT NULL,
"fileName" TEXT NOT NULL,
"mountOffset" JSONB,
"mountRotation" JSONB,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "InstallationDemoTool_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX IF NOT EXISTS "InstallationDemoTool_toolCode_key"
ON "InstallationDemoTool"("toolCode");

View File

@ -0,0 +1,4 @@
ALTER TYPE "OperationLogAction" ADD VALUE IF NOT EXISTS 'INTAKE_PRODUCT_CREATE';
ALTER TYPE "OperationLogAction" ADD VALUE IF NOT EXISTS 'INTAKE_PRODUCT_DELETE';
ALTER TYPE "OperationLogAction" ADD VALUE IF NOT EXISTS 'INTAKE_FILE_UPLOAD';
ALTER TYPE "OperationLogAction" ADD VALUE IF NOT EXISTS 'INTAKE_WORKSPACE_CONFIRMATION';

View File

@ -0,0 +1,180 @@
CREATE TYPE "BomCostingCaseStatus" AS ENUM (
'DRAFT',
'QUEUED',
'ANALYZING',
'READY',
'FAILED'
);
CREATE TYPE "BomCostingLineCategory" AS ENUM (
'BOARD',
'STEEL',
'ACCESSORY',
'PACKAGING',
'PROCESSING',
'LABOR'
);
CREATE TYPE "BomCostingMatchStatus" AS ENUM (
'UNMATCHED',
'MATCHED',
'NEEDS_CONFIRMATION'
);
CREATE TABLE "BomCostingCase" (
"id" TEXT NOT NULL,
"sourceUrl" TEXT NOT NULL,
"sourceHost" TEXT NOT NULL,
"title" TEXT NOT NULL DEFAULT '',
"providerCode" TEXT NOT NULL DEFAULT '',
"providerTaskId" TEXT NOT NULL DEFAULT '',
"analysisSchemaVersion" TEXT NOT NULL DEFAULT 'competitor-analysis/v1',
"status" "BomCostingCaseStatus" NOT NULL DEFAULT 'DRAFT',
"progress" INTEGER NOT NULL DEFAULT 0,
"statusMessage" TEXT NOT NULL DEFAULT '',
"targetErrorPercent" DECIMAL(5,2) NOT NULL DEFAULT 10,
"currency" TEXT NOT NULL DEFAULT 'CNY',
"rawAnalysisSnapshot" JSONB,
"createdById" TEXT NOT NULL,
"deletedAt" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "BomCostingCase_pkey" PRIMARY KEY ("id")
);
CREATE TABLE "BomCostingSku" (
"id" TEXT NOT NULL,
"caseId" TEXT NOT NULL,
"externalSkuId" TEXT NOT NULL,
"name" TEXT NOT NULL DEFAULT '',
"structureGroupKey" TEXT NOT NULL DEFAULT '',
"imageUrl" TEXT,
"attributes" JSONB,
"lengthMm" DECIMAL(12,3),
"widthMm" DECIMAL(12,3),
"heightMm" DECIMAL(12,3),
"weightKg" DECIMAL(12,3),
"packageLengthMm" DECIMAL(12,3),
"packageWidthMm" DECIMAL(12,3),
"packageHeightMm" DECIMAL(12,3),
"packageWeightKg" DECIMAL(12,3),
"estimatedCost" DECIMAL(12,2),
"costCalculatedAt" TIMESTAMP(3),
"sortOrder" INTEGER NOT NULL DEFAULT 0,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "BomCostingSku_pkey" PRIMARY KEY ("id")
);
CREATE TABLE "BomCostingLine" (
"id" TEXT NOT NULL,
"skuId" TEXT NOT NULL,
"externalLineId" TEXT NOT NULL,
"category" "BomCostingLineCategory" NOT NULL,
"partName" TEXT NOT NULL,
"materialCode" TEXT NOT NULL DEFAULT '',
"materialName" TEXT NOT NULL DEFAULT '',
"materialSpec" TEXT NOT NULL DEFAULT '',
"processName" TEXT NOT NULL DEFAULT '',
"packagingKind" TEXT NOT NULL DEFAULT '',
"lengthMm" DECIMAL(12,3),
"widthMm" DECIMAL(12,3),
"heightMm" DECIMAL(12,3),
"quantity" DECIMAL(12,4) NOT NULL DEFAULT 1,
"unit" TEXT NOT NULL DEFAULT '',
"weightKg" DECIMAL(12,4),
"lossFactor" DECIMAL(8,4) NOT NULL DEFAULT 1,
"sourceConfidence" DECIMAL(5,4),
"evidence" JSONB,
"matchStatus" "BomCostingMatchStatus" NOT NULL DEFAULT 'UNMATCHED',
"matchedLibraryType" TEXT NOT NULL DEFAULT '',
"matchedLibraryRecordId" TEXT NOT NULL DEFAULT '',
"matchedLibraryName" TEXT NOT NULL DEFAULT '',
"matchedLibrarySpec" TEXT NOT NULL DEFAULT '',
"resolvedUnitPrice" DECIMAL(12,4),
"calculatedQuantity" DECIMAL(14,6),
"subtotal" DECIMAL(12,2),
"sortOrder" INTEGER NOT NULL DEFAULT 0,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "BomCostingLine_pkey" PRIMARY KEY ("id")
);
CREATE TABLE "BomCostingPriceSnapshot" (
"id" TEXT NOT NULL,
"lineId" TEXT NOT NULL,
"sourceType" TEXT NOT NULL,
"sourceRecordId" TEXT NOT NULL,
"sourceName" TEXT NOT NULL,
"sourceSpec" TEXT NOT NULL DEFAULT '',
"unitPrice" DECIMAL(12,4) NOT NULL,
"unit" TEXT NOT NULL,
"currency" TEXT NOT NULL DEFAULT 'CNY',
"lossFactor" DECIMAL(8,4) NOT NULL DEFAULT 1,
"isCurrent" BOOLEAN NOT NULL DEFAULT true,
"matchedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "BomCostingPriceSnapshot_pkey" PRIMARY KEY ("id")
);
CREATE TABLE "BomCostingTaskEvent" (
"id" TEXT NOT NULL,
"caseId" TEXT NOT NULL,
"eventType" TEXT NOT NULL,
"status" TEXT NOT NULL,
"progress" INTEGER NOT NULL DEFAULT 0,
"message" TEXT NOT NULL DEFAULT '',
"providerTaskId" TEXT NOT NULL DEFAULT '',
"payload" JSONB,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "BomCostingTaskEvent_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "BomCostingSku_caseId_externalSkuId_key"
ON "BomCostingSku"("caseId", "externalSkuId");
CREATE UNIQUE INDEX "BomCostingLine_skuId_externalLineId_key"
ON "BomCostingLine"("skuId", "externalLineId");
CREATE INDEX "BomCostingCase_createdById_deletedAt_createdAt_idx"
ON "BomCostingCase"("createdById", "deletedAt", "createdAt");
CREATE INDEX "BomCostingCase_status_deletedAt_idx"
ON "BomCostingCase"("status", "deletedAt");
CREATE INDEX "BomCostingCase_providerTaskId_idx"
ON "BomCostingCase"("providerTaskId");
CREATE INDEX "BomCostingSku_caseId_sortOrder_idx"
ON "BomCostingSku"("caseId", "sortOrder");
CREATE INDEX "BomCostingSku_structureGroupKey_idx"
ON "BomCostingSku"("structureGroupKey");
CREATE INDEX "BomCostingLine_skuId_sortOrder_idx"
ON "BomCostingLine"("skuId", "sortOrder");
CREATE INDEX "BomCostingLine_category_matchStatus_idx"
ON "BomCostingLine"("category", "matchStatus");
CREATE INDEX "BomCostingLine_matchedLibraryType_matchedLibraryRecordId_idx"
ON "BomCostingLine"("matchedLibraryType", "matchedLibraryRecordId");
CREATE INDEX "BomCostingPriceSnapshot_lineId_isCurrent_idx"
ON "BomCostingPriceSnapshot"("lineId", "isCurrent");
CREATE INDEX "BomCostingPriceSnapshot_sourceType_sourceRecordId_idx"
ON "BomCostingPriceSnapshot"("sourceType", "sourceRecordId");
CREATE INDEX "BomCostingTaskEvent_caseId_createdAt_idx"
ON "BomCostingTaskEvent"("caseId", "createdAt");
CREATE INDEX "BomCostingTaskEvent_providerTaskId_idx"
ON "BomCostingTaskEvent"("providerTaskId");
ALTER TABLE "BomCostingCase"
ADD CONSTRAINT "BomCostingCase_createdById_fkey"
FOREIGN KEY ("createdById") REFERENCES "User"("id")
ON DELETE RESTRICT ON UPDATE CASCADE;
ALTER TABLE "BomCostingSku"
ADD CONSTRAINT "BomCostingSku_caseId_fkey"
FOREIGN KEY ("caseId") REFERENCES "BomCostingCase"("id")
ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "BomCostingLine"
ADD CONSTRAINT "BomCostingLine_skuId_fkey"
FOREIGN KEY ("skuId") REFERENCES "BomCostingSku"("id")
ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "BomCostingPriceSnapshot"
ADD CONSTRAINT "BomCostingPriceSnapshot_lineId_fkey"
FOREIGN KEY ("lineId") REFERENCES "BomCostingLine"("id")
ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "BomCostingTaskEvent"
ADD CONSTRAINT "BomCostingTaskEvent_caseId_fkey"
FOREIGN KEY ("caseId") REFERENCES "BomCostingCase"("id")
ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -0,0 +1,84 @@
-- CreateTable
CREATE TABLE "WorkRecordScoreRule" (
"id" TEXT NOT NULL,
"primary" TEXT NOT NULL,
"secondary" TEXT NOT NULL DEFAULT '',
"unitScore" DECIMAL(12,4),
"sortOrder" INTEGER NOT NULL DEFAULT 0,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "WorkRecordScoreRule_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "WorkRecord" (
"id" TEXT NOT NULL,
"workDate" DATE NOT NULL,
"designerId" TEXT NOT NULL,
"designerName" TEXT NOT NULL,
"primary" TEXT NOT NULL,
"secondary" TEXT NOT NULL DEFAULT '',
"scoreRuleId" TEXT,
"approvalOrTask" TEXT NOT NULL DEFAULT '',
"spu" TEXT NOT NULL DEFAULT '',
"quantity" DECIMAL(12,2) NOT NULL DEFAULT 1,
"unitScore" DECIMAL(12,4),
"taskTotal" DECIMAL(14,4),
"fileStoragePath" TEXT NOT NULL DEFAULT '',
"createdById" TEXT NOT NULL,
"updatedById" TEXT NOT NULL,
"deletedAt" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "WorkRecord_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "WorkRecordMonthlyArchive" (
"id" TEXT NOT NULL,
"monthKey" TEXT NOT NULL,
"periodStart" DATE NOT NULL,
"periodEnd" DATE NOT NULL,
"fileName" TEXT NOT NULL,
"summary" JSONB NOT NULL,
"records" JSONB NOT NULL,
"recordCount" INTEGER NOT NULL,
"generatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "WorkRecordMonthlyArchive_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "WorkRecordScoreRule_primary_sortOrder_idx" ON "WorkRecordScoreRule"("primary", "sortOrder");
-- CreateIndex
CREATE INDEX "WorkRecordScoreRule_isActive_sortOrder_idx" ON "WorkRecordScoreRule"("isActive", "sortOrder");
-- CreateIndex
CREATE INDEX "WorkRecord_workDate_deletedAt_idx" ON "WorkRecord"("workDate", "deletedAt");
-- CreateIndex
CREATE INDEX "WorkRecord_designerId_workDate_idx" ON "WorkRecord"("designerId", "workDate");
-- CreateIndex
CREATE INDEX "WorkRecord_primary_workDate_idx" ON "WorkRecord"("primary", "workDate");
-- CreateIndex
CREATE INDEX "WorkRecord_scoreRuleId_idx" ON "WorkRecord"("scoreRuleId");
-- CreateIndex
CREATE UNIQUE INDEX "WorkRecordMonthlyArchive_monthKey_key" ON "WorkRecordMonthlyArchive"("monthKey");
-- CreateIndex
CREATE INDEX "WorkRecordMonthlyArchive_periodStart_idx" ON "WorkRecordMonthlyArchive"("periodStart");
-- AddForeignKey
ALTER TABLE "WorkRecord" ADD CONSTRAINT "WorkRecord_designerId_fkey" FOREIGN KEY ("designerId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "WorkRecord" ADD CONSTRAINT "WorkRecord_scoreRuleId_fkey" FOREIGN KEY ("scoreRuleId") REFERENCES "WorkRecordScoreRule"("id") ON DELETE SET NULL ON UPDATE CASCADE;

View File

@ -0,0 +1,7 @@
UPDATE "WorkRecordScoreRule"
SET
"secondary" = '将图纸上传图纸系统并生成对应二维码需存入对应SPU文件夹',
"updatedAt" = CURRENT_TIMESTAMP
WHERE
"primary" = '上传图纸+制作二维码'
AND "secondary" = '将图纸上传图纸系统并生成对应二维码 / 需存入对应SPU文件夹';

View File

@ -0,0 +1,36 @@
CREATE TYPE "ReleaseNoteStatus" AS ENUM ('DRAFT', 'PUBLISHED', 'ARCHIVED');
CREATE TABLE "ReleaseNote" (
"id" TEXT NOT NULL,
"title" TEXT NOT NULL,
"summary" TEXT NOT NULL,
"content" JSONB NOT NULL,
"targetBusinessRoles" "BusinessAccountType"[] NOT NULL DEFAULT ARRAY[]::"BusinessAccountType"[],
"status" "ReleaseNoteStatus" NOT NULL DEFAULT 'DRAFT',
"publishedAt" TIMESTAMP(3),
"createdById" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "ReleaseNote_pkey" PRIMARY KEY ("id"),
CONSTRAINT "ReleaseNote_createdById_fkey"
FOREIGN KEY ("createdById") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE
);
CREATE TABLE "ReleaseNoteAcknowledgement" (
"id" TEXT NOT NULL,
"releaseNoteId" TEXT NOT NULL,
"readerId" TEXT NOT NULL,
"readAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "ReleaseNoteAcknowledgement_pkey" PRIMARY KEY ("id"),
CONSTRAINT "ReleaseNoteAcknowledgement_releaseNoteId_fkey"
FOREIGN KEY ("releaseNoteId") REFERENCES "ReleaseNote"("id") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "ReleaseNoteAcknowledgement_readerId_fkey"
FOREIGN KEY ("readerId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE
);
CREATE INDEX "ReleaseNote_status_publishedAt_idx" ON "ReleaseNote"("status", "publishedAt");
CREATE INDEX "ReleaseNote_createdById_idx" ON "ReleaseNote"("createdById");
CREATE UNIQUE INDEX "ReleaseNoteAcknowledgement_releaseNoteId_readerId_key" ON "ReleaseNoteAcknowledgement"("releaseNoteId", "readerId");
CREATE INDEX "ReleaseNoteAcknowledgement_readerId_readAt_idx" ON "ReleaseNoteAcknowledgement"("readerId", "readAt");

View File

@ -0,0 +1,91 @@
CREATE TYPE "BomCostingSourceStatus" AS ENUM (
'PENDING',
'FETCHING',
'ARCHIVING',
'READY',
'FAILED'
);
CREATE TYPE "BomCostingSourceAssetKind" AS ENUM (
'MAIN',
'DETAIL',
'SKU'
);
CREATE TYPE "BomCostingSourceAssetStatus" AS ENUM (
'PENDING',
'STORED',
'FAILED'
);
CREATE TABLE "BomCostingSourceSnapshot" (
"id" TEXT NOT NULL,
"sourceKey" TEXT NOT NULL,
"sourceUrl" TEXT NOT NULL,
"sourceHost" TEXT NOT NULL,
"channel" TEXT NOT NULL,
"externalItemId" TEXT NOT NULL,
"title" TEXT NOT NULL DEFAULT '',
"shopName" TEXT NOT NULL DEFAULT '',
"detailUrl" TEXT,
"status" "BomCostingSourceStatus" NOT NULL DEFAULT 'PENDING',
"skuCount" INTEGER NOT NULL DEFAULT 0,
"imageCount" INTEGER NOT NULL DEFAULT 0,
"storedImageCount" INTEGER NOT NULL DEFAULT 0,
"failedImageCount" INTEGER NOT NULL DEFAULT 0,
"rawSnapshot" JSONB,
"errorMessage" TEXT NOT NULL DEFAULT '',
"fetchedAt" TIMESTAMP(3),
"completedAt" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "BomCostingSourceSnapshot_pkey" PRIMARY KEY ("id")
);
CREATE TABLE "BomCostingSourceAsset" (
"id" TEXT NOT NULL,
"sourceSnapshotId" TEXT NOT NULL,
"assetKey" TEXT NOT NULL,
"kind" "BomCostingSourceAssetKind" NOT NULL,
"status" "BomCostingSourceAssetStatus" NOT NULL DEFAULT 'PENDING',
"originalUrl" TEXT NOT NULL,
"storedUrl" TEXT NOT NULL DEFAULT '',
"storageKey" TEXT NOT NULL DEFAULT '',
"contentType" TEXT NOT NULL DEFAULT '',
"byteSize" INTEGER,
"externalSkuIds" JSONB NOT NULL DEFAULT '[]',
"sortOrder" INTEGER NOT NULL DEFAULT 0,
"errorMessage" TEXT NOT NULL DEFAULT '',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "BomCostingSourceAsset_pkey" PRIMARY KEY ("id")
);
ALTER TABLE "BomCostingCase"
ADD COLUMN "sourceSnapshotId" TEXT,
ADD COLUMN "sourceCacheHit" BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN "processingStartedAt" TIMESTAMP(3),
ADD COLUMN "processingFinishedAt" TIMESTAMP(3);
CREATE UNIQUE INDEX "BomCostingSourceSnapshot_sourceKey_key"
ON "BomCostingSourceSnapshot"("sourceKey");
CREATE INDEX "BomCostingSourceSnapshot_status_updatedAt_idx"
ON "BomCostingSourceSnapshot"("status", "updatedAt");
CREATE INDEX "BomCostingSourceSnapshot_channel_externalItemId_idx"
ON "BomCostingSourceSnapshot"("channel", "externalItemId");
CREATE UNIQUE INDEX "BomCostingSourceAsset_sourceSnapshotId_assetKey_key"
ON "BomCostingSourceAsset"("sourceSnapshotId", "assetKey");
CREATE INDEX "BomCostingSourceAsset_sourceSnapshotId_status_sortOrder_idx"
ON "BomCostingSourceAsset"("sourceSnapshotId", "status", "sortOrder");
CREATE INDEX "BomCostingCase_sourceSnapshotId_idx"
ON "BomCostingCase"("sourceSnapshotId");
ALTER TABLE "BomCostingCase"
ADD CONSTRAINT "BomCostingCase_sourceSnapshotId_fkey"
FOREIGN KEY ("sourceSnapshotId") REFERENCES "BomCostingSourceSnapshot"("id")
ON DELETE SET NULL ON UPDATE CASCADE;
ALTER TABLE "BomCostingSourceAsset"
ADD CONSTRAINT "BomCostingSourceAsset_sourceSnapshotId_fkey"
FOREIGN KEY ("sourceSnapshotId") REFERENCES "BomCostingSourceSnapshot"("id")
ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -0,0 +1,188 @@
CREATE TYPE "IntakeAiRenderOutputMode" AS ENUM ('WHITE_BACKGROUND', 'EIGHT_VIEW', 'SCENE');
CREATE TYPE "IntakeAiRenderStorageClass" AS ENUM ('TEMPORARY', 'PERMANENT');
CREATE TABLE "IntakeProductModelAsset" (
"id" TEXT NOT NULL,
"productId" TEXT NOT NULL,
"modelKey" TEXT NOT NULL,
"displayName" TEXT NOT NULL DEFAULT '',
"createdBy" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "IntakeProductModelAsset_pkey" PRIMARY KEY ("id")
);
CREATE TABLE "IntakeProductModelVersion" (
"id" TEXT NOT NULL,
"assetId" TEXT NOT NULL,
"versionNo" INTEGER NOT NULL,
"originalFileName" TEXT NOT NULL,
"mimeType" TEXT NOT NULL,
"fileSize" INTEGER NOT NULL,
"checksumSha256" TEXT NOT NULL DEFAULT '',
"sourceStorageKey" TEXT NOT NULL,
"sourceUrl" TEXT NOT NULL,
"previewStorageKey" TEXT,
"previewUrl" TEXT,
"createdBy" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "IntakeProductModelVersion_pkey" PRIMARY KEY ("id")
);
CREATE TABLE "IntakeSkuAiProfile" (
"id" TEXT NOT NULL,
"skuItemId" TEXT NOT NULL,
"currentModelVersionId" TEXT,
"currentMaterialVersionId" TEXT,
"createdBy" TEXT NOT NULL,
"updatedBy" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "IntakeSkuAiProfile_pkey" PRIMARY KEY ("id")
);
CREATE TABLE "IntakeSkuAiMaterialVersion" (
"id" TEXT NOT NULL,
"profileId" TEXT NOT NULL,
"versionNo" INTEGER NOT NULL,
"materialColor" TEXT NOT NULL DEFAULT '#d8c6ad',
"textureStorageKey" TEXT,
"textureUrl" TEXT,
"nodeMaterials" JSONB,
"uvConfig" JSONB,
"createdBy" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "IntakeSkuAiMaterialVersion_pkey" PRIMARY KEY ("id")
);
ALTER TABLE "AssistedAssemblyAiRender"
ADD COLUMN "intakeSkuItemId" TEXT,
ADD COLUMN "modelVersionId" TEXT,
ADD COLUMN "materialVersionId" TEXT,
ADD COLUMN "outputMode" "IntakeAiRenderOutputMode",
ADD COLUMN "batchId" TEXT,
ADD COLUMN "viewName" TEXT,
ADD COLUMN "viewIndex" INTEGER,
ADD COLUMN "storageClass" "IntakeAiRenderStorageClass" NOT NULL DEFAULT 'TEMPORARY';
CREATE UNIQUE INDEX "IntakeProductModelAsset_productId_modelKey_key"
ON "IntakeProductModelAsset"("productId", "modelKey");
CREATE INDEX "IntakeProductModelAsset_productId_createdAt_idx"
ON "IntakeProductModelAsset"("productId", "createdAt");
CREATE INDEX "IntakeProductModelAsset_createdBy_idx"
ON "IntakeProductModelAsset"("createdBy");
CREATE UNIQUE INDEX "IntakeProductModelVersion_assetId_versionNo_key"
ON "IntakeProductModelVersion"("assetId", "versionNo");
CREATE INDEX "IntakeProductModelVersion_assetId_createdAt_idx"
ON "IntakeProductModelVersion"("assetId", "createdAt");
CREATE INDEX "IntakeProductModelVersion_checksumSha256_idx"
ON "IntakeProductModelVersion"("checksumSha256");
CREATE INDEX "IntakeProductModelVersion_createdBy_idx"
ON "IntakeProductModelVersion"("createdBy");
CREATE UNIQUE INDEX "IntakeSkuAiProfile_skuItemId_key"
ON "IntakeSkuAiProfile"("skuItemId");
CREATE UNIQUE INDEX "IntakeSkuAiProfile_currentMaterialVersionId_key"
ON "IntakeSkuAiProfile"("currentMaterialVersionId");
CREATE INDEX "IntakeSkuAiProfile_currentModelVersionId_idx"
ON "IntakeSkuAiProfile"("currentModelVersionId");
CREATE INDEX "IntakeSkuAiProfile_createdBy_idx"
ON "IntakeSkuAiProfile"("createdBy");
CREATE UNIQUE INDEX "IntakeSkuAiMaterialVersion_profileId_versionNo_key"
ON "IntakeSkuAiMaterialVersion"("profileId", "versionNo");
CREATE INDEX "IntakeSkuAiMaterialVersion_profileId_createdAt_idx"
ON "IntakeSkuAiMaterialVersion"("profileId", "createdAt");
CREATE INDEX "IntakeSkuAiMaterialVersion_createdBy_idx"
ON "IntakeSkuAiMaterialVersion"("createdBy");
CREATE INDEX "AssistedAssemblyAiRender_intakeSkuItemId_createdAt_idx"
ON "AssistedAssemblyAiRender"("intakeSkuItemId", "createdAt");
CREATE INDEX "AssistedAssemblyAiRender_intakeSkuItemId_outputMode_createdAt_idx"
ON "AssistedAssemblyAiRender"("intakeSkuItemId", "outputMode", "createdAt");
CREATE INDEX "AssistedAssemblyAiRender_modelVersionId_idx"
ON "AssistedAssemblyAiRender"("modelVersionId");
CREATE INDEX "AssistedAssemblyAiRender_materialVersionId_idx"
ON "AssistedAssemblyAiRender"("materialVersionId");
CREATE INDEX "AssistedAssemblyAiRender_storageClass_createdAt_idx"
ON "AssistedAssemblyAiRender"("storageClass", "createdAt");
ALTER TABLE "IntakeProductModelAsset"
ADD CONSTRAINT "IntakeProductModelAsset_productId_fkey"
FOREIGN KEY ("productId") REFERENCES "IntakeNonIntegratedProduct"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "IntakeProductModelAsset"
ADD CONSTRAINT "IntakeProductModelAsset_createdBy_fkey"
FOREIGN KEY ("createdBy") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
ALTER TABLE "IntakeProductModelVersion"
ADD CONSTRAINT "IntakeProductModelVersion_assetId_fkey"
FOREIGN KEY ("assetId") REFERENCES "IntakeProductModelAsset"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "IntakeProductModelVersion"
ADD CONSTRAINT "IntakeProductModelVersion_createdBy_fkey"
FOREIGN KEY ("createdBy") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
ALTER TABLE "IntakeSkuAiProfile"
ADD CONSTRAINT "IntakeSkuAiProfile_skuItemId_fkey"
FOREIGN KEY ("skuItemId") REFERENCES "IntakeNonIntegratedSkuItem"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "IntakeSkuAiProfile"
ADD CONSTRAINT "IntakeSkuAiProfile_currentModelVersionId_fkey"
FOREIGN KEY ("currentModelVersionId") REFERENCES "IntakeProductModelVersion"("id") ON DELETE SET NULL ON UPDATE CASCADE;
ALTER TABLE "IntakeSkuAiProfile"
ADD CONSTRAINT "IntakeSkuAiProfile_createdBy_fkey"
FOREIGN KEY ("createdBy") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
ALTER TABLE "IntakeSkuAiMaterialVersion"
ADD CONSTRAINT "IntakeSkuAiMaterialVersion_profileId_fkey"
FOREIGN KEY ("profileId") REFERENCES "IntakeSkuAiProfile"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "IntakeSkuAiMaterialVersion"
ADD CONSTRAINT "IntakeSkuAiMaterialVersion_createdBy_fkey"
FOREIGN KEY ("createdBy") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
ALTER TABLE "IntakeSkuAiProfile"
ADD CONSTRAINT "IntakeSkuAiProfile_currentMaterialVersionId_fkey"
FOREIGN KEY ("currentMaterialVersionId") REFERENCES "IntakeSkuAiMaterialVersion"("id") ON DELETE SET NULL ON UPDATE CASCADE;
ALTER TABLE "AssistedAssemblyAiRender"
ADD CONSTRAINT "AssistedAssemblyAiRender_intakeSkuItemId_fkey"
FOREIGN KEY ("intakeSkuItemId") REFERENCES "IntakeNonIntegratedSkuItem"("id") ON DELETE SET NULL ON UPDATE CASCADE;
ALTER TABLE "AssistedAssemblyAiRender"
ADD CONSTRAINT "AssistedAssemblyAiRender_modelVersionId_fkey"
FOREIGN KEY ("modelVersionId") REFERENCES "IntakeProductModelVersion"("id") ON DELETE SET NULL ON UPDATE CASCADE;
ALTER TABLE "AssistedAssemblyAiRender"
ADD CONSTRAINT "AssistedAssemblyAiRender_materialVersionId_fkey"
FOREIGN KEY ("materialVersionId") REFERENCES "IntakeSkuAiMaterialVersion"("id") ON DELETE SET NULL ON UPDATE CASCADE;
UPDATE "AssistedAssemblyAiRender"
SET
"outputMode" = CASE
WHEN "params"->>'mode' = 'white-background' THEN 'WHITE_BACKGROUND'::"IntakeAiRenderOutputMode"
WHEN "params"->>'mode' = 'eight-view' THEN 'EIGHT_VIEW'::"IntakeAiRenderOutputMode"
WHEN "params"->>'mode' = 'scene' THEN 'SCENE'::"IntakeAiRenderOutputMode"
ELSE NULL
END,
"batchId" = NULLIF("params"->>'batchId', ''),
"viewName" = NULLIF("params"->>'viewName', ''),
"viewIndex" = CASE
WHEN ("params"->>'viewIndex') ~ '^[0-9]+$' THEN ("params"->>'viewIndex')::INTEGER
ELSE NULL
END;
WITH exact_matches AS (
SELECT render."id" AS "renderId", MIN(item."id") AS "skuItemId"
FROM "AssistedAssemblyAiRender" render
JOIN "AiRenderWorkbenchSku" workbench_sku ON workbench_sku."id" = render."workbenchSkuId"
JOIN "AiRenderWorkbenchSpu" workbench_spu ON workbench_spu."id" = workbench_sku."spuId"
JOIN "IntakeNonIntegratedSkuItem" item
ON UPPER(BTRIM(item."barcode")) = UPPER(BTRIM(workbench_sku."name"))
JOIN "IntakeNonIntegratedProduct" product
ON product."id" = item."productId"
AND UPPER(BTRIM(product."spu")) = UPPER(BTRIM(workbench_spu."name"))
WHERE BTRIM(item."barcode") <> ''
GROUP BY render."id"
HAVING COUNT(*) = 1
)
UPDATE "AssistedAssemblyAiRender" render
SET "intakeSkuItemId" = exact_matches."skuItemId"
FROM exact_matches
WHERE render."id" = exact_matches."renderId";

View File

@ -0,0 +1,49 @@
-- CreateEnum
CREATE TYPE "BomCostingFactAnalysisStatus" AS ENUM (
'PENDING',
'PROCESSING',
'READY',
'FAILED'
);
-- CreateTable
CREATE TABLE "BomCostingSourceFactAnalysis" (
"id" TEXT NOT NULL,
"sourceSnapshotId" TEXT NOT NULL,
"schemaVersion" TEXT NOT NULL,
"assetFingerprint" TEXT NOT NULL,
"status" "BomCostingFactAnalysisStatus" NOT NULL DEFAULT 'PENDING',
"model" TEXT NOT NULL DEFAULT '',
"factSnapshot" JSONB,
"analyzedImageCount" INTEGER NOT NULL DEFAULT 0,
"totalImageCount" INTEGER NOT NULL DEFAULT 0,
"batchCount" INTEGER NOT NULL DEFAULT 0,
"promptTokens" INTEGER NOT NULL DEFAULT 0,
"completionTokens" INTEGER NOT NULL DEFAULT 0,
"durationMs" INTEGER NOT NULL DEFAULT 0,
"errorMessage" TEXT NOT NULL DEFAULT '',
"startedAt" TIMESTAMP(3),
"completedAt" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "BomCostingSourceFactAnalysis_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "BomCostingSourceFactAnalysis_sourceSnapshotId_schemaVersion_key"
ON "BomCostingSourceFactAnalysis"("sourceSnapshotId", "schemaVersion");
-- CreateIndex
CREATE INDEX "BomCostingSourceFactAnalysis_sourceSnapshotId_status_idx"
ON "BomCostingSourceFactAnalysis"("sourceSnapshotId", "status");
-- CreateIndex
CREATE INDEX "BomCostingSourceFactAnalysis_status_updatedAt_idx"
ON "BomCostingSourceFactAnalysis"("status", "updatedAt");
-- AddForeignKey
ALTER TABLE "BomCostingSourceFactAnalysis"
ADD CONSTRAINT "BomCostingSourceFactAnalysis_sourceSnapshotId_fkey"
FOREIGN KEY ("sourceSnapshotId") REFERENCES "BomCostingSourceSnapshot"("id")
ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -0,0 +1,27 @@
-- Barcode imports are append-only business records. Rows with the same SKU,
-- barcode, or supplier must remain independently printable.
ALTER TABLE "BarcodePrintImportRecord"
ADD COLUMN "normalizedSupplierName" TEXT NOT NULL DEFAULT '';
WITH supplier_names AS (
SELECT
"id",
btrim(
CASE
WHEN btrim("supplierName") <> '' THEN "supplierName"
ELSE COALESCE("updates" ->> 'supplierName', '')
END
) AS "supplierName"
FROM "BarcodePrintImportRecord"
)
UPDATE "BarcodePrintImportRecord" AS records
SET
"supplierName" = supplier_names."supplierName",
"normalizedSupplierName" = lower(supplier_names."supplierName")
FROM supplier_names
WHERE records."id" = supplier_names."id";
DROP INDEX "BarcodePrintImportRecord_kind_normalizedSku_supplierCode_key";
CREATE INDEX "BarcodePrintImportRecord_normalizedSupplierName_idx"
ON "BarcodePrintImportRecord"("normalizedSupplierName");

View File

@ -0,0 +1,140 @@
CREATE TYPE "BomCostingFactAnalysisOrigin" AS ENUM (
'INITIAL',
'SELECTED_REANALYSIS',
'FULL_REANALYSIS'
);
CREATE TYPE "BomCostingAnalysisRunMode" AS ENUM (
'INITIAL',
'REUSE_FACTS',
'REANALYZE_SELECTED',
'REANALYZE_ALL'
);
CREATE TYPE "BomCostingAnalysisRunStatus" AS ENUM (
'PENDING',
'PROCESSING',
'READY',
'FAILED'
);
ALTER TABLE "BomCostingSourceFactAnalysis"
ADD COLUMN "revision" INTEGER NOT NULL DEFAULT 1,
ADD COLUMN "origin" "BomCostingFactAnalysisOrigin" NOT NULL DEFAULT 'INITIAL',
ADD COLUMN "baseAnalysisId" TEXT,
ADD COLUMN "selectedAssetIds" JSONB NOT NULL DEFAULT '[]';
DROP INDEX "BomCostingSourceFactAnalysis_sourceSnapshotId_schemaVersion_key";
CREATE UNIQUE INDEX "BomCostingSourceFactAnalysis_sourceSnapshotId_schemaVersion_revision_key"
ON "BomCostingSourceFactAnalysis"("sourceSnapshotId", "schemaVersion", "revision");
CREATE INDEX "BomCostingSourceFactAnalysis_baseAnalysisId_idx"
ON "BomCostingSourceFactAnalysis"("baseAnalysisId");
ALTER TABLE "BomCostingSourceFactAnalysis"
ADD CONSTRAINT "BomCostingSourceFactAnalysis_baseAnalysisId_fkey"
FOREIGN KEY ("baseAnalysisId") REFERENCES "BomCostingSourceFactAnalysis"("id")
ON DELETE SET NULL ON UPDATE CASCADE;
CREATE TABLE "BomCostingAnalysisRun" (
"id" TEXT NOT NULL,
"caseId" TEXT NOT NULL,
"revision" INTEGER NOT NULL,
"mode" "BomCostingAnalysisRunMode" NOT NULL,
"status" "BomCostingAnalysisRunStatus" NOT NULL DEFAULT 'PENDING',
"factAnalysisId" TEXT,
"selectedAssetIds" JSONB NOT NULL DEFAULT '[]',
"instruction" TEXT NOT NULL DEFAULT '',
"resultSnapshot" JSONB,
"model" TEXT NOT NULL DEFAULT '',
"callId" TEXT NOT NULL DEFAULT '',
"promptTokens" INTEGER NOT NULL DEFAULT 0,
"completionTokens" INTEGER NOT NULL DEFAULT 0,
"durationMs" INTEGER NOT NULL DEFAULT 0,
"errorMessage" TEXT NOT NULL DEFAULT '',
"createdById" TEXT,
"startedAt" TIMESTAMP(3),
"completedAt" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "BomCostingAnalysisRun_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "BomCostingAnalysisRun_caseId_revision_key"
ON "BomCostingAnalysisRun"("caseId", "revision");
CREATE INDEX "BomCostingAnalysisRun_caseId_status_idx"
ON "BomCostingAnalysisRun"("caseId", "status");
CREATE INDEX "BomCostingAnalysisRun_factAnalysisId_idx"
ON "BomCostingAnalysisRun"("factAnalysisId");
CREATE INDEX "BomCostingAnalysisRun_status_updatedAt_idx"
ON "BomCostingAnalysisRun"("status", "updatedAt");
CREATE INDEX "BomCostingAnalysisRun_createdById_idx"
ON "BomCostingAnalysisRun"("createdById");
ALTER TABLE "BomCostingAnalysisRun"
ADD CONSTRAINT "BomCostingAnalysisRun_caseId_fkey"
FOREIGN KEY ("caseId") REFERENCES "BomCostingCase"("id")
ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "BomCostingAnalysisRun"
ADD CONSTRAINT "BomCostingAnalysisRun_factAnalysisId_fkey"
FOREIGN KEY ("factAnalysisId") REFERENCES "BomCostingSourceFactAnalysis"("id")
ON DELETE SET NULL ON UPDATE CASCADE;
ALTER TABLE "BomCostingAnalysisRun"
ADD CONSTRAINT "BomCostingAnalysisRun_createdById_fkey"
FOREIGN KEY ("createdById") REFERENCES "User"("id")
ON DELETE SET NULL ON UPDATE CASCADE;
INSERT INTO "BomCostingAnalysisRun" (
"id",
"caseId",
"revision",
"mode",
"status",
"factAnalysisId",
"resultSnapshot",
"model",
"callId",
"errorMessage",
"createdById",
"startedAt",
"completedAt",
"createdAt",
"updatedAt"
)
SELECT
'legacy-' || c."id",
c."id",
1,
'INITIAL'::"BomCostingAnalysisRunMode",
CASE c."status"
WHEN 'ANALYZING' THEN 'PROCESSING'::"BomCostingAnalysisRunStatus"
WHEN 'READY' THEN 'READY'::"BomCostingAnalysisRunStatus"
WHEN 'FAILED' THEN 'FAILED'::"BomCostingAnalysisRunStatus"
ELSE 'PENDING'::"BomCostingAnalysisRunStatus"
END,
(
SELECT f."id"
FROM "BomCostingSourceFactAnalysis" f
WHERE f."sourceSnapshotId" = c."sourceSnapshotId"
AND f."schemaVersion" = 'competitor-facts/v1'
ORDER BY f."revision" DESC
LIMIT 1
),
c."rawAnalysisSnapshot",
'',
c."providerTaskId",
CASE WHEN c."status" = 'FAILED' THEN c."statusMessage" ELSE '' END,
c."createdById",
c."processingStartedAt",
c."processingFinishedAt",
c."createdAt",
c."updatedAt"
FROM "BomCostingCase" c;

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