Compare commits

..

5 Commits

1933 changed files with 16940 additions and 197836 deletions

View File

@ -5,10 +5,6 @@ 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
@ -78,15 +74,11 @@ 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_CLIENT_ID=
DINGTALK_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=
@ -101,13 +93,6 @@ 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=
@ -161,13 +146,14 @@ BOM_COSTING_MODELING_PROVIDER=
# BOM 核算实时同款采购预估(1688 官方图搜)。
# 网关固定为 https://skills-gateway.1688.com/api/find_product/1.0.0。
# AK 只放服务器私密环境变量,服务端会按 x-csk-* 协议签名;实时同款价固定 × 90%
# AK 只放服务器私密环境变量,服务端会按 x-csk-* 协议签名;实时同款价 × 采购系数
# 是整件采购预估,历史 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_PROCUREMENT_RATE=0.90
ALIBABA_1688_IMAGE_SEARCH_PAGE_SIZE=10
ALIBABA_1688_IMAGE_SEARCH_PURCHASE_AMOUNT=1
ALIBABA_1688_IMAGE_SEARCH_TAGS=4306497
@ -176,9 +162,6 @@ ALIBABA_1688_IMAGE_SEARCH_TAGS=4306497
MATERIAL_LIBRARY_WHITE_BACKGROUND_API_ENABLED=false
MATERIAL_LIBRARY_WHITE_BACKGROUND_API_BASE_URL=https://example.com
MATERIAL_LIBRARY_WHITE_BACKGROUND_API_KEY=replace-with-material-library-api-key
# 检测报告查询(不填则复用上面素材平台的 BaseUrl / API Key;都没有时「是否有检测报告」显示为空)
MATERIAL_LIBRARY_CERTIFICATE_REPORT_API_BASE_URL=
MATERIAL_LIBRARY_CERTIFICATE_REPORT_API_KEY=
# 产品全量档案以图搜图(模型文件放在仓库外,不提交到 Git)
IMAGE_SEARCH_MODEL_PATH=

View File

@ -2,7 +2,6 @@
# dependencies
/node_modules
/node_modules.pnpm-mixed
/.pnp
.pnp.*
.yarn/*
@ -16,7 +15,6 @@
# next.js
/.next/
/.next-*/
/.next-oa-preview*/
/.next-oa-webpack/
/out/
@ -76,6 +74,3 @@ local-packages/npm-cache/cache/_logs/
# 脚本监控 Agent 本机凭据
agents/script-monitor/config.json
# 下载中心本地运行时产物
.download-center/

View File

@ -2,6 +2,9 @@
每台运行脚本的机器部署一份 Agent。Agent 只主动向产品平台发起 HTTP 请求,不需要为被监控机器开放入站端口。
需要交给安装同事逐台执行时,请使用完整的
[《脚本监控 Agent 逐台接入操作手册》](../../docs/workflow/脚本监控Agent逐台接入操作手册.md)。
## 接入
1. 在供应链中心的“脚本监控”页面点击“接入机器”,生成独立的 `agentId` 和 `agentToken`。

View File

@ -1,153 +0,0 @@
# 财务系统店铺密码直接显示契约
状态:生产接口已上线;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

@ -1,101 +0,0 @@
# 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

@ -1,54 +0,0 @@
# 产品全量档案图片索引操作说明
全量档案本地图片索引只读取 `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

@ -34,10 +34,6 @@
- 定时频率:每天 `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 成本参考镜像
@ -54,38 +50,6 @@
- 准确度回测:`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`);安装/启用定时器属于生产操作,需要另行获得授权。
## 明确排除
- `192.168.58.20` 不是当前 `furniture-3d-system` 的生产部署目标。

View File

@ -0,0 +1,963 @@
# 脚本监控 Agent 逐台接入操作手册
适用对象:负责在各台脚本机器上安装、配置和验收监控 Agent 的同事。
适用版本:脚本监控 Agent `1.0.0`。
平台入口:供应链中心 → 脚本监控。
本文按“安装人员完全不了解产品平台代码和项目结构”编写。安装人员不需要取得产品平台源码、不需要理解 Git,也不需要登录代码仓库。
## 0. 开始前先确认
### 0.1 先选择一种接入方式
| 你的实际情况 | 选择 | 阅读范围 |
| --- | --- | --- |
| 只需要监控普通脚本、Cron、PM2 或结果文件 | 标准 Agent | 按第 1~17 节操作,不需要阅读附录 A |
| 已经有影刀等专用采集程序,并且要由该程序直接推送应用和日志 | 自定义采集器 | 先读第 1~4 节,再由开发人员按附录 A 改造和验收 |
同一台机器、同一组 Agent 凭据只能有一个上报程序。禁止同时运行标准 `agent.mjs` 和自定义采集器,否则两个程序会互相覆盖脚本快照,页面数据会时有时无。
### 0.2 你应当拿到的资料
开始前必须具备:
1. 本手册;
2. 这台机器独立的 `agentId` 和 `agentToken`,或平台生成的完整配置;
3. 已确认的网络模式:内网、公网或自动;
4. 本机需要监控的脚本清单,至少包含脚本名称、用途、运行方式和计划;
5. 如果选择标准 Agent,能够在目标机器下载第 5 节的 4 个程序文件;
6. 如果选择自定义采集器,能够修改采集程序的开发人员和附录 A。
缺少独立凭据、脚本清单或网络信息时,不要自行猜测,也不要复制其他机器的配置,应联系平台负责人补齐后再开始。
### 0.3 本文中的称呼
- “平台”:`https://cp.jiansun.vip` 中的“供应链中心 → 脚本监控”;
- “平台负责人”:在平台创建机器、生成凭据并核对页面结果的人;
- “安装人员”:在目标 Windows 或 Linux 机器部署 Agent 的人;
- “Agent”:常驻在目标机器、主动向平台上报的程序;
- “脚本 key”:一个脚本在本机长期不变的唯一标识,不是脚本名称。
### 0.4 什么情况下算接入完成
标准 Agent 必须同时满足:前台启动明确显示上报成功、平台机器在线、平台脚本数量与本机清单一致、状态和最近结果符合实际、重启后 Agent 自动恢复。
自定义采集器必须同时满足:`/report` 返回 HTTP 200、平台显示全部脚本、至少一条真实 `/run` 记录可见、连续两个上报周期没有 400/401/403/404/405。只有业务明确要求平台手动运行时,才把 `/commands/claim` 作为完成条件。
## 1. 任务目标
每台运行脚本的机器都需要一个常驻上报进程:选择标准 Agent 时运行 `agent.mjs`,选择自定义采集器时由已有采集程序承担同一职责。上报进程主动向产品平台汇报:
- 机器是否在线,以及本次通过内网还是公网连接;
- 机器上配置或自动发现了哪些脚本、Cron、PM2 进程;
- 脚本最近一次运行状态、时间、耗时、退出码和结果摘要;
- 经明确授权的脚本,是否可以从平台点击“立即运行”。
Agent 只主动发起 HTTP 请求,目标机器不需要开放任何入站端口,也不需要把脚本文件上传到平台。
普通机器优先使用平台提供的 `agent.mjs`。影刀等工具如果使用自定义采集器直接调用平台 API,必须由能够修改采集程序的开发人员按附录 A 接入;不能沿用旧脚本监控项目的 WebSocket 或 `/api/agent/...` 接口。
## 2. 必须遵守的安全规则
1. 一台机器使用一套独立的 `agentId` 和 `agentToken`,禁止多台机器共用。
2. Token 由平台负责人单独发给安装人员,不得发到群聊、工单正文或普通文档。
3. `config.json` 不得提交到 Git;Linux 权限建议设为 `600`,Windows 只允许运行 Agent 的账号读取。
4. 配置中的 `command` 不得直接写密码、Token、数据库连接串等敏感信息,应从目标机器原有安全环境变量或私有配置读取。
5. 默认不要开启平台手动运行。只有已确认安全、可重复执行、不会等待人工输入的脚本,才允许设置 `"allowManualRun": true`。
6. 订单写入、库存调整、财务、删除、批量覆盖等高风险脚本,未经业务负责人书面确认,不得开放手动运行。
## 3. 平台负责人准备,安装人员只需核对
以下步骤由平台负责人执行,安装人员不需要登录平台管理页面。每接入一台机器,平台负责人都要单独执行一次:
1. 登录产品平台。
2. 打开“供应链中心 → 脚本监控”。
3. 点击“接入机器”。
4. 机器名称填写可识别名称,例如“杭州仓库脚本机 01”,不要只写“服务器”。
5. 选择网络模式:内网、互联网或自动。
6. 填写对应的平台地址。
7. 点击“生成 Agent 凭据”。
8. 立即复制完整配置,通过安全方式交给安装人员。
凭据只显示一次,关闭弹窗后不能再次查看当前 Token;遗失时应联系平台负责人重新生成,不能从数据库或日志中找回明文 Token。
安装人员收到配置后只核对三项:机器名称是否是本机、网络模式是否符合本机、`agentId`/`agentToken` 是否为非空占位符以外的真实值。不要把完整 Token 截图或转发到群聊。
## 4. 选择内网还是公网
| 机器网络情况 | `connectionMode` | `centerUrls` | 说明 |
| --- | --- | --- | --- |
| 固定在公司/仓库内网,并能访问平台内网地址 | `intranet` | 只填 `intranet` | 推荐,所有上报只走内网 |
| 只能访问互联网 | `internet` | 只填 `internet` | 公网生产地址必须使用 HTTPS |
| 网络会在内外网之间切换 | `auto` | 内外网都填 | 优先内网,失败后再尝试公网 |
公网生产地址:
```text
https://cp.jiansun.vip
```
内网地址由平台运维负责人提供,文档中的 `<平台内网地址>` 只是占位符,不能照抄。
已知机器长期只处于一种网络时,优先选择固定的 `intranet` 或 `internet`,避免 `auto` 每次先等待不可达地址超时。
公网机器在安装前先验证基础连通性。Windows PowerShell:
```powershell
Invoke-RestMethod -Uri 'https://cp.jiansun.vip/api/health' -Method Get -TimeoutSec 15
```
Linux:
```bash
curl -fsS --max-time 15 https://cp.jiansun.vip/api/health
```
响应中应包含 `"success": true` 和 `"status": "healthy"`。这只证明机器能访问平台,不代表 Agent 凭据或上报数据已经正确;最终仍以第 10 节验收为准。内网机器的测试地址和端口由平台运维负责人提供。
不要在浏览器地址栏直接打开 `/api/internal/script-monitor/report` 判断接口是否正常:浏览器发送的是 GET,而上报接口只接受 POST,看到 405 属于测试方法错误。
## 5. 下载每台机器需要的文件
平台页面目前没有 Agent 安装包下载按钮。安装人员不需要取得整个产品平台项目,也不需要登录代码仓库;直接执行本节与你操作系统对应的下载命令即可。
当前受支持版本:`Agent 1.0.0`。
| 文件 | 固定下载地址 |
| --- | --- |
| `agent.mjs` | `http://git.1080p.hk/WuYongJie/furniture-3d-system-Wu/raw/commit/dc16d2b75c41a5009354ba56caf4c1517d77ccce/furniture-3d-system/agents/script-monitor/agent.mjs` |
| `run-script.mjs` | `http://git.1080p.hk/WuYongJie/furniture-3d-system-Wu/raw/commit/dc16d2b75c41a5009354ba56caf4c1517d77ccce/furniture-3d-system/agents/script-monitor/run-script.mjs` |
| `ecosystem.config.cjs` | `http://git.1080p.hk/WuYongJie/furniture-3d-system-Wu/raw/commit/dc16d2b75c41a5009354ba56caf4c1517d77ccce/furniture-3d-system/agents/script-monitor/ecosystem.config.cjs` |
| `config.example.json` | `http://git.1080p.hk/WuYongJie/furniture-3d-system-Wu/raw/commit/dc16d2b75c41a5009354ba56caf4c1517d77ccce/furniture-3d-system/agents/script-monitor/config.example.json` |
这些地址中的文件内容固定不变。下载服务当前只提供 HTTP,因此必须按下面的命令自动核对 SHA256;校验不通过时禁止运行文件。安装人员不需要理解地址中的项目和版本信息。
### 5.1 Windows PowerShell 一次下载并校验
以 PowerShell 打开目标机器,完整执行:
```powershell
$agentDir = 'C:\furniture-script-monitor'
$baseUrl = 'http://git.1080p.hk/WuYongJie/furniture-3d-system-Wu/raw/commit/dc16d2b75c41a5009354ba56caf4c1517d77ccce/furniture-3d-system/agents/script-monitor'
$expectedHashes = @{
'agent.mjs' = '2C67D765F0BCFFE64B4BD1838A046D12DCCAFECDD51FAE7C92379C9DC19E0889'
'run-script.mjs' = '7168F227ED63E5EC6B05EF4D849709F3CB6BC6B89B9CB2186439F92ACE333207'
'ecosystem.config.cjs' = '1D03D8D15C39B358E5BF819B7C124FB9804FEFFB9CB239AB73561607E339F6D7'
'config.example.json' = '689D6A36180DACF5E63059B6FFE7BBD5F7CAF53AC3E8954776FA67B0F5410B5C'
}
New-Item -ItemType Directory -Path $agentDir -Force | Out-Null
foreach ($fileName in $expectedHashes.Keys) {
$targetPath = Join-Path $agentDir $fileName
Invoke-WebRequest -UseBasicParsing -Uri "$baseUrl/$fileName" -OutFile $targetPath
$actualHash = (Get-FileHash -Algorithm SHA256 -LiteralPath $targetPath).Hash
if ($actualHash -ne $expectedHashes[$fileName]) {
Remove-Item -LiteralPath $targetPath -Force
throw "$fileName 下载校验失败,已删除,请停止安装并联系平台负责人"
}
}
Copy-Item -LiteralPath "$agentDir\config.example.json" -Destination "$agentDir\config.json" -Force
Get-ChildItem -LiteralPath $agentDir
```
看到 5 个文件(包括复制出来的 `config.json`)且没有红色报错,才可以继续第 7 节填写配置。
### 5.2 Linux 一次下载并校验
```bash
sudo mkdir -p /opt/furniture-script-monitor
sudo chown -R "$(id -un):$(id -gn)" /opt/furniture-script-monitor
cd /opt/furniture-script-monitor
base_url='http://git.1080p.hk/WuYongJie/furniture-3d-system-Wu/raw/commit/dc16d2b75c41a5009354ba56caf4c1517d77ccce/furniture-3d-system/agents/script-monitor'
curl -fL "$base_url/agent.mjs" -o agent.mjs
curl -fL "$base_url/run-script.mjs" -o run-script.mjs
curl -fL "$base_url/ecosystem.config.cjs" -o ecosystem.config.cjs
curl -fL "$base_url/config.example.json" -o config.example.json
sha256sum -c <<'EOF'
2c67d765f0bcffe64b4bd1838a046d12dccafecdd51fae7c92379c9dc19e0889 agent.mjs
7168f227ed63e5ec6b05ef4d849709f3cb6bc6b89b9cb2186439f92ace333207 run-script.mjs
1d03d8d15c39b358e5bf819b7c124fb9804feffb9cb239ab73561607e339f6d7 ecosystem.config.cjs
689d6a36180dacf5e63059b6ffe7bbd5f7caf53ac3e8954776fa67b0f5410b5c config.example.json
EOF
cp config.example.json config.json
chmod 600 config.json
```
四行都显示 `OK` 才能继续安装。若目标机器不能访问 `git.1080p.hk`,停止下载并联系平台负责人通过公司文件传输渠道发送同版本文件;安装人员不需要自行寻找其他下载网站。负责人提供的压缩包不得包含任何机器已经填写过的 `config.json`。
需要的文件清单:
```text
agent.mjs
run-script.mjs
ecosystem.config.cjs
config.example.json
```
不要复制其他机器的 `config.json`。
运行环境要求:
- Node.js 20 或 22 LTS,最低不得低于 Node.js 18;
- Linux 建议安装 PM2 作为常驻进程管理器;
- Windows 建议使用任务计划程序在开机时启动 Agent;
- 目标机器需要能够主动访问所选平台地址。
下载完成后先检查 Node.js。Windows PowerShell:
```powershell
node --version
(Get-Command node).Source
```
Linux:
```bash
node --version
command -v node
```
版本低于 18 或提示找不到 `node` 时,停止安装并联系本机管理员安装 Node.js 20/22 LTS;不要从不明网站下载运行环境。
## 6. 安装目录
推荐目录:
```text
Linux: /opt/furniture-script-monitor
Windows: C:\furniture-script-monitor
```
Linux 示例:
```bash
sudo mkdir -p /opt/furniture-script-monitor
sudo chown -R <运行账号>:<运行账号组> /opt/furniture-script-monitor
chmod 700 /opt/furniture-script-monitor
```
Windows 请确认运行 Agent 的账号对安装目录、被监控脚本目录和结果文件有读取权限;需要手动运行时,还必须有执行权限。
## 7. 编写 `config.json`
将 `config.example.json` 复制为同目录的 `config.json`,再填入本机配置。
如果平台负责人已经发来本机完整配置,就用该内容覆盖新建的 `config.json`;不要把配置内容粘贴进 Word、聊天软件富文本或 Markdown 链接后再复制回来。`centerUrls` 必须是普通 URL,Token 中的下划线 `_` 必须原样保留,不能写成 `\_`。
平台生成的初始配置可能是 `"scripts": []`。这只代表机器凭据已经创建,不代表系统能够自动知道本机的全部脚本:
- Windows 不会自动发现任务计划程序或影刀应用,标准 Agent 必须在 `scripts` 数组中逐项配置;
- Linux 只能自动发现当前运行账号的 Crontab,以及该账号可见的 PM2 进程;
- 影刀自定义采集器按附录 A 直接生成 `scripts` 快照,不要同时启动标准 Agent;
- 未列入配置或未被自动发现的脚本不会出现在平台页面。
### 7.1 公网 Windows 示例
```json
{
"agentId": "由平台生成,禁止照抄",
"agentToken": "由平台生成,禁止照抄",
"agentName": "财务脚本机 01",
"connectionMode": "internet",
"centerUrls": {
"internet": "https://cp.jiansun.vip"
},
"reportIntervalSec": 60,
"commandPollIntervalSec": 5,
"requestTimeoutMs": 10000,
"autoDiscover": {
"cron": false,
"pm2": true
},
"scripts": [
{
"key": "daily-finance-export",
"name": "每日财务导出",
"taskDescription": "生成每日财务导出文件",
"jobType": "OTHER",
"schedule": "Windows 任务计划:每天 02:00",
"expectedIntervalSec": 86400,
"allowManualRun": false,
"command": "C:\\Python311\\python.exe D:\\scripts\\daily_finance_export.py",
"cwd": "D:\\scripts",
"resultCheck": {
"type": "file",
"path": "D:\\scripts\\output\\latest.xlsx",
"maxAgeSec": 90000,
"minBytes": 1
}
}
]
}
```
JSON 中的 Windows 路径必须使用双反斜杠,例如 `D:\\scripts`。
### 7.2 内网 Linux 示例
```json
{
"agentId": "由平台生成,禁止照抄",
"agentToken": "由平台生成,禁止照抄",
"agentName": "仓库脚本机 01",
"connectionMode": "intranet",
"centerUrls": {
"intranet": "<平台内网地址>"
},
"reportIntervalSec": 60,
"commandPollIntervalSec": 5,
"requestTimeoutMs": 5000,
"autoDiscover": {
"cron": true,
"pm2": true
},
"scripts": [
{
"key": "warehouse-stock-sync",
"name": "仓库库存同步",
"taskDescription": "同步仓库库存到业务系统",
"jobType": "CRON",
"schedule": "*/15 * * * *",
"expectedIntervalSec": 900,
"allowManualRun": true,
"command": "/usr/bin/bash /opt/scripts/warehouse-stock-sync.sh",
"cwd": "/opt/scripts",
"resultCheck": {
"type": "file",
"path": "/var/log/warehouse-stock-sync/latest.log",
"maxAgeSec": 1800,
"minBytes": 1
}
}
]
}
```
配置完成后,Linux 执行:
```bash
chmod 600 /opt/furniture-script-monitor/config.json
```
## 8. 脚本字段怎么填
| 字段 | 必填 | 填写要求 |
| --- | --- | --- |
| `key` | 是 | 本机唯一且长期不变,只用英文、数字、短横线,后续不要随意改 |
| `name` | 是 | 页面显示名称,要让非开发人员看得懂 |
| `taskDescription` | 建议 | 说明脚本做什么、处理什么数据 |
| `jobType` | 是 | 只能是 `CRON`、`PM2`、`SYSTEMD`、`MANUAL`、`OTHER` |
| `schedule` | 否 | Cron 表达式或“Windows 任务计划:每天 02:00”等说明 |
| `expectedIntervalSec` | 否 | 预期运行间隔,单位秒;配合运行结果上报判断静默/超时 |
| `command` | 需要执行时 | 使用绝对路径,必须是本机已经验证过的真实命令 |
| `cwd` | 建议 | 命令工作目录,使用绝对路径 |
| `allowManualRun` | 是 | 默认 `false`;只有审核通过后才改为 `true` |
| `resultCheck` | 建议 | Agent 定期判断当前结果是否有效 |
同一台机器内不得配置重复 `key`。显式配置的脚本会覆盖相同 key 的自动发现结果。
## 9. 三种接入深度
### 9.1 只查看“机器上有哪些任务”
开启自动发现:
```json
{
"autoDiscover": {
"cron": true,
"pm2": true
}
}
```
- Linux 可以发现当前运行账号的 Crontab;
- 安装 PM2 的机器可以发现 PM2 进程;
- Windows 不会自动发现任务计划程序;Windows 脚本必须写入 `scripts`;
- 自动发现的 Cron 通常只能显示任务和计划,不能凭空知道真实执行结果;
- 自动发现任务不允许平台手动运行。
### 9.2 定期检查当前结果
在脚本中配置 `resultCheck`。
检查文件:
```json
{
"resultCheck": {
"type": "file",
"path": "/absolute/path/to/result.log",
"maxAgeSec": 1800,
"minBytes": 1
}
}
```
检查进程:
```json
{
"resultCheck": {
"type": "process",
"name": "python"
}
}
```
检查 Linux systemd 服务:
```json
{
"resultCheck": {
"type": "systemd",
"service": "warehouse-sync.service"
}
}
```
Agent 的运行账号必须有权读取目标文件或查询目标进程/服务,否则页面会显示检查失败。
### 9.3 记录每次运行的真实结果
如果要记录开始时间、结束时间、耗时、退出码、标准输出和错误输出,应让原定时任务调用 `run-script.mjs`,由包装器再执行 `config.json` 中的真实 `command`。
Linux 示例:
```bash
/usr/bin/node /opt/furniture-script-monitor/run-script.mjs warehouse-stock-sync
```
Windows 任务计划程序:
```text
程序:C:\Program Files\nodejs\node.exe
参数:C:\furniture-script-monitor\run-script.mjs daily-finance-export
起始于:C:\furniture-script-monitor
```
改造定时任务后,必须确认原命令没有继续单独运行,否则会造成同一个脚本重复执行。
包装器上报失败时不会擅自把原脚本改成成功;原脚本退出码仍会原样保留。
## 10. 首次前台启动和验收
先不要配置开机自启,必须前台运行一次。
Linux:
```bash
cd /opt/furniture-script-monitor
node agent.mjs
```
Windows PowerShell:
```powershell
Set-Location C:\furniture-script-monitor
node .\agent.mjs
```
正确输出示例:
```text
[script-monitor] 已通过内网上报 3 个脚本
```
或:
```text
[script-monitor] 已通过公网上报 3 个脚本
```
然后在平台检查:
1. 机器从“待接入”变为“在线 · 内网”或“在线 · 公网”。
2. 顶部“在线/已接入”分母增加;从未上报的机器不会计入已接入分母。
3. 页面脚本数量与本机配置一致。
4. 每个脚本名称、计划和任务说明正确。
5. `resultCheck` 状态符合本机实际情况。
6. 关闭前台 Agent 后,等待至少 3 分钟,页面应显示“Agent 离线”。
Agent 在线判断时间为“至少 3 分钟,或 3 倍上报间隔,取较大值”。例如 `reportIntervalSec` 为 60 秒时,超过约 3 分钟未上报会显示离线。
安装人员没有平台账号时,不需要索要账号或密码。请把前台成功输出、机器名称、预期脚本数量和验收时间发给平台负责人,由负责人完成以上页面核对并明确回复“通过”或具体差异。
## 11. 配置常驻运行
### 11.1 Linux 使用 PM2
```bash
cd /opt/furniture-script-monitor
npm install -g pm2
pm2 start ecosystem.config.cjs
pm2 save
pm2 startup
```
`pm2 startup` 会输出一条需要使用管理员权限执行的命令,按它的提示执行一次,然后再次运行:
```bash
pm2 save
pm2 status furniture-script-monitor-agent
pm2 logs furniture-script-monitor-agent --lines 50
```
### 11.2 Windows 使用任务计划程序
先在 PowerShell 执行:
```powershell
(Get-Command node).Source
```
把输出的真实 `node.exe` 路径填到下面的“程序”中,不要在实际路径不同时照抄示例。
打开 Windows“任务计划程序”,创建任务:
- 名称:`Furniture Script Monitor Agent`;
- 触发器:启动时;
- 程序:上一步查到的真实 `node.exe` 路径,例如 `C:\Program Files\nodejs\node.exe`;
- 参数:`C:\furniture-script-monitor\agent.mjs`;
- 起始于:`C:\furniture-script-monitor`;
- 选择“不管用户是否登录都要运行”;
- 勾选“使用最高权限运行”;
- 失败后每 1 分钟重启,至少重试 3 次;
- 不要设置短于脚本运行时间的自动停止时限。
任务使用哪个 Windows 账号运行,该账号就必须能读取 `config.json`、脚本目录和结果文件,并有权执行已开放的命令。
## 12. 平台手动运行
手动运行只有同时满足以下条件才可点击:
1. 当前用户有脚本监控编辑权限;
2. Agent 在线;
3. 本机脚本配置了 `command`;
4. 本机明确设置 `"allowManualRun": true`;
5. 当前没有等待或正在执行的同一任务。
平台不会下发任意命令,只下发脚本 `key`。Agent 收到 key 后,只执行本机 `config.json` 中预先保存的 `command`。
开放手动运行前逐项确认:
- 命令不需要人工输入;
- 重复执行不会产生重复订单、重复扣减或覆盖数据;
- 脚本自身有锁,避免与原定时任务同时执行;
- 运行账号权限最小化;
- 已在目标机器直接执行并确认退出码为 0;
- 失败时能够从输出和错误日志定位原因。
手动运行请求约 5 分钟未被 Agent 领取会过期。Agent 默认每 5 秒主动领取一次,无需开放入站端口。
## 13. 重启验收
每台机器安装结束前必须重启一次或模拟一次开机启动:
1. 重启机器或停止并重新启动常驻任务。
2. 确认 Agent 进程自动恢复。
3. 等待一个上报周期。
4. 在平台确认机器重新显示在线。
5. 查看 Agent 日志,确认没有持续 401、超时或配置错误。
未完成重启验收,不算安装完成。
## 14. 常见故障
出现错误时先保留当前窗口,不要反复改 Token、改接口路径或换请求体。按本节定位一次,并按第 14.11 节的模板反馈。
### 14.1 提示找不到 `node`
说明 Node.js 未安装、版本不符合要求或不在当前账号的 PATH 中。把 `node --version` 和 `(Get-Command node).Source`(Windows)或 `command -v node`(Linux)的输出发给本机管理员;不要继续配置开机自启。
### 14.2 提示“配置文件不存在”
确认 `config.json` 与 `agent.mjs` 位于同一目录,或通过环境变量指定:
```text
SCRIPT_MONITOR_CONFIG=/absolute/path/config.json
```
### 14.3 返回 HTTP 401
通常是 `agentId` 和 `agentToken` 不匹配、Token 被复制不完整,或使用了其他机器的凭据。不要反复猜 Token,联系平台负责人重新生成。
如果凭据曾经粘贴到群聊、公开文档或其他非授权位置,应在平台删除原机器接入记录并重新生成,旧 Token 不再继续使用。
### 14.4 内网能通但 Agent 走了公网
检查 `connectionMode`、`centerUrls.intranet` 和内网 DNS/端口。`auto` 会先尝试内网,内网失败后才走公网。
### 14.5 机器在线但没有脚本
检查:
- `scripts` 是否为 JSON 数组;
- 每一项是否都有唯一的 `key` 和非空 `name`;
- `jobType` 是否使用支持的枚举值;
- Windows 是否误以为能自动发现任务计划程序;
- Agent 日志是否出现 HTTP 400 或 JSON 解析错误。
Windows 机器的 `scripts: []` 不会自动变成任务计划程序或影刀应用列表。影刀场景必须选择附录 A 的自定义采集器,或由负责人把每个应用人工配置为标准 Agent 脚本。
### 14.6 “立即运行”按钮不可用
页面会给出具体原因。重点检查 Agent 是否在线、脚本是否有 `command`,以及是否明确设置了 `"allowManualRun": true`。
### 14.7 结果文件明明存在,但页面显示失败
使用运行 Agent 的同一个系统账号检查文件权限、路径、更新时间和大小。相对路径容易受启动目录影响,`resultCheck.path` 必须使用绝对路径。
### 14.8 页面显示“最近成功”,但 Agent 离线
“最近成功”表示脚本历史上的最后一次结果;“Agent 离线”表示机器当前没有继续上报,两者含义不同。先恢复 Agent,再判断脚本是否仍在正常执行。
### 14.9 返回 HTTP 400、403、404 或 405
- `400 / VALIDATION_003`:请求体格式错误;标准 Agent 检查 `config.json` 中的枚举、数字和 JSON 语法,自定义采集器按附录 A 对照;
- `403`:请求被安全策略拒绝,保留完整响应正文和请求 ID 后联系平台负责人;
- `404`:请求地址错误,自定义程序通常仍在使用旧接口;标准 Agent 不应自行修改接口路径;
- `405`:实际请求方法不是 `POST`,浏览器地址栏直接打开接口得到 405 不能证明上报程序的 POST 请求正确。
### 14.10 JSON 配置无法读取
常见原因是 Windows 路径只写了一个反斜杠、复制了 Markdown 链接、Token 中出现 `\_`、末尾多逗号或使用了中文引号。使用文本编辑器保存为 UTF-8 JSON;不要使用 Word 编辑。可以在 Agent 目录执行以下命令验证 JSON 语法:
```powershell
Get-Content -LiteralPath '.\config.json' -Raw | ConvertFrom-Json | Out-Null
Write-Host 'config.json 语法正确'
```
### 14.11 统一故障反馈模板
安装人员反馈问题时一次提供以下内容,避免只说“失败了”:
```text
机器名称:
操作系统及版本:
选择的接入方式:标准 Agent / 自定义采集器
Node.js 版本:
网络模式:intranet / internet / auto
执行时间(含时区):
执行的命令:
预期脚本数量:
Agent 实际发现/上报数量:
最终 HTTP 方法和 URL(自定义采集器必填):
HTTP 状态码:
完整响应正文或报错:
最近 30 行 Agent 日志:
```
反馈中禁止包含完整 `agentToken`、完整 `Authorization` 请求头或未脱敏的 `config.json`。如果必须说明凭据是否一致,只保留 Agent ID,Token 最多保留前后各 2 位并遮盖中间内容。
## 15. 升级 Agent
只有平台负责人明确通知新版本时才升级,不要自行使用不明来源文件。升级时只替换这些程序文件:
```text
agent.mjs
run-script.mjs
ecosystem.config.cjs
```
不要覆盖目标机器的 `config.json`。
Linux:
```bash
pm2 restart furniture-script-monitor-agent
pm2 logs furniture-script-monitor-agent --lines 50
```
Windows:结束并重新运行对应任务计划,然后在平台确认一个上报周期内恢复在线。
## 16. 每台机器交接记录
安装人员每完成一台机器,填写一行;严禁在表格中记录 Token。
| 机器名称 | 操作系统 | 平台机器名称 | 网络模式 | 平台地址 | 安装目录 | 常驻方式 | 脚本数量 | 允许手动运行的脚本 | 负责人 | 安装时间 | 重启验收 |
| --- | --- | --- | --- | --- | --- | --- | ---: | --- | --- | --- | --- |
| 示例:仓库脚本机 01 | Ubuntu 24.04 | 杭州仓库脚本机 01 | intranet | 由运维提供 | `/opt/furniture-script-monitor` | PM2 | 3 | warehouse-stock-sync | 张三 | 2026-08-28 | 通过 |
## 17. 单机完成清单
- [ ] 已明确选择标准 Agent 或自定义采集器,未使用同一凭据同时运行两种上报程序;
- [ ] 平台为本机生成了独立凭据;
- [ ] 已拿到本机脚本清单,实际脚本数量有明确依据;
- [ ] `config.json` 未使用示例 Token,文件权限已收紧;
- [ ] 网络模式和平台地址与机器实际网络一致;
- [ ] 所有脚本都有唯一 key、清晰名称和正确工作目录;
- [ ] 高风险脚本未开放手动运行;
- [ ] 前台启动成功并显示内网或公网上报;
- [ ] 平台机器由待接入变为在线;
- [ ] 平台脚本数量与本机一致;
- [ ] 至少一个脚本的结果检查或包装器运行已验证;
- [ ] 常驻进程或任务计划已配置;
- [ ] 重启后 Agent 能自动恢复;
- [ ] 已填写单机交接记录,记录中不含 Token。
## 附录 A:自定义采集器 HTTP API 契约
本附录适用于影刀等已有采集程序直接向平台推送数据的场景,执行人必须能够修改并调试采集程序。普通安装人员如果只能安装软件、不能修改代码,应停止在本附录继续尝试,改用标准 Agent 或把本附录交给采集程序开发人员。
使用官方 `agent.mjs` 时,不需要手工拼接这些请求;只需填写第 7 节的 `config.json`,Agent 会自动补全接口路径和 `connectionKind`。自定义采集器投入运行后,不得再使用同一组凭据启动官方 Agent。
### A.0 按顺序完成联调
不要一开始同时推送 16 个应用、17 条日志和手动运行。按以下顺序逐项验收,前一步未达到预期时不要进入下一步:
1. 使用 1 个真实影刀应用调用 `/report`,确认 HTTP 200,平台显示 1 个脚本;
2. 改为上报全部 16 个应用,确认平台数量为 16,13 个有可靠成功记录的任务为 `SUCCESS`,其余为 `UNKNOWN`;
3. 使用 1 条真实任务日志调用 `/run`,确认页面出现运行记录;
4. 再补传其余日志,每条使用稳定唯一的 `externalRunId`;
5. 最后才实现 `/commands/claim` 和平台手动运行;不需要手动运行时保持 `canManualRun: false`。
每一步都必须记录最终请求方法、最终 URL、HTTP 状态码和完整响应正文。只记录程序内部的 `success=False` 不算有效验收证据。
### A.1 接口和认证头
公网平台基础地址只能写纯文本:
```text
https://cp.jiansun.vip
```
禁止写成 Markdown 链接 `[https://cp.jiansun.vip](https://cp.jiansun.vip)`,也不要把 `/api/internal/script-monitor/report` 重复写进 `centerUrls.internet`。
当前接口如下:
| 用途 | 方法和完整路径 |
| --- | --- |
| 上报机器和脚本当前快照 | `POST https://cp.jiansun.vip/api/internal/script-monitor/report` |
| 上报脚本的一次运行记录 | `POST https://cp.jiansun.vip/api/internal/script-monitor/run` |
| 领取平台“立即运行”命令 | `POST https://cp.jiansun.vip/api/internal/script-monitor/commands/claim` |
三个接口都使用同一组请求头:
```text
Content-Type: application/json
Authorization: Bearer <本机 Agent Token>
x-script-monitor-agent-id: <本机 Agent ID>
```
`agentId` 和 `agentToken` 只放请求头,不得再放入 JSON 请求体。Token 不要经过 Markdown 转义;Token 中如果有下划线 `_`,原样保留,不能写成 `\_`。
### A.2 `/report` 完整合法 JSON
下面是已按当前平台校验规则整理的完整示例。影刀应用统一使用 `jobType: "OTHER"`;所有枚举值必须大写。
```json
{
"hostname": "cloud-pc-01",
"os": "Windows 11",
"agentVersion": "yingdao-collector-1.0.0",
"connectionKind": "INTERNET",
"reportIntervalSec": 60,
"scripts": [
{
"key": "yingdao-app-10001",
"name": "订单数据同步",
"taskDescription": "影刀应用:同步订单数据",
"jobType": "OTHER",
"schedule": "影刀调度:每天 08:00",
"commandPreview": null,
"expectedIntervalSec": 86400,
"status": "SUCCESS",
"isEnabled": true,
"canManualRun": false,
"lastStartedAt": "2026-08-28T08:00:00+08:00",
"lastFinishedAt": "2026-08-28T08:02:09+08:00",
"lastDurationMs": 129000,
"lastExitCode": 0,
"lastResultSummary": "本次运行成功;今日已触发 3 次",
"lastOutputTail": "处理 125 条记录",
"lastErrorTail": null
},
{
"key": "yingdao-app-10002",
"name": "库存检查",
"taskDescription": "影刀应用:检查库存异常",
"jobType": "OTHER",
"schedule": null,
"commandPreview": null,
"expectedIntervalSec": null,
"status": "UNKNOWN",
"isEnabled": true,
"canManualRun": false,
"lastStartedAt": null,
"lastFinishedAt": null,
"lastDurationMs": null,
"lastExitCode": null,
"lastResultSummary": "已发现应用,暂未取得运行结果",
"lastOutputTail": null,
"lastErrorTail": null
}
]
}
```
`scripts` 是当前机器的完整快照,不是只放本次发生变化的脚本。若发现 16 个影刀应用,应上报 16 项:有可靠成功记录的 13 项填 `SUCCESS`,其余 3 项填 `UNKNOWN`,不能把没有结果的应用也推断为成功。
### A.3 `/report` 字段限制
平台使用严格校验:请求体顶层和每个 `scripts[]` 对象都不能出现未定义字段。多一个字段、枚举大小写错误、数字写成字符串或时间格式错误,都会返回 `VALIDATION_003`。
顶层只允许以下字段:
| 字段 | 必填 | 类型和限制 |
| --- | --- | --- |
| `hostname` | 否 | 字符串或 `null`,最多 160 字符 |
| `os` | 否 | 字符串或 `null`,最多 240 字符 |
| `agentVersion` | 否 | 字符串或 `null`,最多 40 字符 |
| `connectionKind` | 是 | 只能是 `INTRANET` 或 `INTERNET` |
| `reportIntervalSec` | 否 | 15~3600 的整数,默认 60;不能写成字符串 `"60"` |
| `scripts` | 是 | 数组,最多 1000 项 |
每个 `scripts[]` 对象只允许以下字段:
| 字段 | 必填 | 类型和限制 |
| --- | --- | --- |
| `key` | 是 | 稳定且本机唯一的字符串,1~240 字符;优先使用影刀应用 ID 生成 |
| `name` | 是 | 字符串,1~180 字符 |
| `taskDescription` | 否 | 字符串,最多 2000 字符,默认空字符串 |
| `jobType` | 是 | `CRON`、`PM2`、`SYSTEMD`、`MANUAL`、`OTHER` 之一 |
| `schedule` | 否 | 字符串或 `null`,最多 240 字符 |
| `commandPreview` | 否 | 字符串或 `null`,最多 2000 字符;不得包含凭据 |
| `expectedIntervalSec` | 否 | 15~31536000 的整数或 `null` |
| `status` | 否 | `UNKNOWN`、`RUNNING`、`SUCCESS`、`FAILED`、`SILENT`、`DISABLED` 之一,默认 `UNKNOWN` |
| `isEnabled` | 否 | 布尔值,默认 `true` |
| `canManualRun` | 否 | 布尔值,默认 `false` |
| `lastStartedAt` | 否 | 带时区的 ISO 8601 字符串或 `null` |
| `lastFinishedAt` | 否 | 带时区的 ISO 8601 字符串或 `null` |
| `lastDurationMs` | 否 | 0~2147483647 的整数或 `null`,单位毫秒 |
| `lastExitCode` | 否 | -2147483648~2147483647 的整数或 `null` |
| `lastResultSummary` | 否 | 字符串或 `null`,最多 4000 字符 |
| `lastOutputTail` | 否 | 字符串或 `null`,最多 12000 字符,只保留末尾内容 |
| `lastErrorTail` | 否 | 字符串或 `null`,最多 12000 字符,只保留末尾内容 |
没有值时应省略字段或传 `null`,不要给日期、数字、布尔字段传空字符串。日期必须包含 `Z` 或时区偏移,例如 `2026-08-28T08:02:09+08:00`;`2026-08-28 08:02:09` 不合法。
以下字段不能放入 `/report` 请求体:`agentId`、`agentToken`、`agentName`、`success`、`todayTriggers`、`taskLogs`、`apps`。需要展示“今日 57 次调度”等统计时,把各脚本自己的统计写入对应的 `lastResultSummary`;真实的每次任务日志使用 `/run` 上报。
### A.4 影刀字段映射
| 影刀采集结果 | 平台字段 |
| --- | --- |
| 影刀应用唯一 ID | `scripts[].key`,例如 `yingdao-app-<ID>` |
| 影刀应用名称 | `scripts[].name` |
| 应用/任务说明 | `scripts[].taskDescription` |
| 调度计划文字 | `scripts[].schedule` |
| 影刀任务正在执行 | `scripts[].status = "RUNNING"` |
| 影刀任务成功 | `scripts[].status = "SUCCESS"` |
| 影刀任务失败 | `scripts[].status = "FAILED"` |
| 已发现但没有可靠结果 | `scripts[].status = "UNKNOWN"` |
| 被停用 | `scripts[].status = "DISABLED"`,同时 `isEnabled = false` |
| 开始/完成时间 | `lastStartedAt` / `lastFinishedAt`,转为带时区 ISO 8601 |
| 耗时 | `lastDurationMs`,单位毫秒整数 |
| 结果摘要 | `lastResultSummary` |
| 日志末尾 | `lastOutputTail` / `lastErrorTail` |
采集程序内部即使使用小写 `success`,发给平台前也必须映射成大写 `SUCCESS`。影刀不是平台枚举中的任务类型,因此 `jobType` 必须固定为 `OTHER`,不能传 `YINGDAO`。
### A.5 `/run` 上报每次任务日志
发现的 17 条任务日志不应作为 `taskLogs` 塞进 `/report`,而应逐条调用 `/run`。完整合法示例:
```json
{
"commandId": null,
"scriptKey": "yingdao-app-10001",
"scriptName": "订单数据同步",
"taskDescription": "影刀应用:同步订单数据",
"jobType": "OTHER",
"externalRunId": "yingdao-run-20260828-000123",
"status": "SUCCESS",
"trigger": "YINGDAO_SCHEDULE",
"startedAt": "2026-08-28T08:00:00+08:00",
"finishedAt": "2026-08-28T08:02:09+08:00",
"durationMs": 129000,
"exitCode": 0,
"resultSummary": "运行成功,处理 125 条记录",
"outputTail": "处理完成",
"errorTail": null
}
```
`/run` 的必填字段是 `scriptKey`、`scriptName`、`jobType`、`externalRunId`、`status`、`startedAt`。其中:
- `status` 只能是 `RUNNING`、`SUCCESS`、`FAILED`;
- `externalRunId` 必须是这台机器内一次运行的稳定唯一 ID,最长 240 字符;重复发送相同 ID 会更新同一条记录,不会重复新增;
- `startedAt` 必须是带时区 ISO 8601;
- `commandId` 只有响应平台手动运行命令时才填写,否则传 `null` 或省略;
- 每条历史日志都只上报自身的实际状态,不能把汇总状态代替单次运行状态。
### A.6 Windows PowerShell 最小联调命令
先把 A.2 的 JSON 保存为 `report-test.json`。在 PowerShell 临时设置凭据,执行完成后关闭窗口;不要把真实 Token 写进脚本或提交到 Git:
```powershell
$env:SCRIPT_MONITOR_AGENT_ID = '<本机 Agent ID>'
$env:SCRIPT_MONITOR_AGENT_TOKEN = '<本机 Agent Token>'
$headers = @{
Authorization = "Bearer $env:SCRIPT_MONITOR_AGENT_TOKEN"
'x-script-monitor-agent-id' = $env:SCRIPT_MONITOR_AGENT_ID
}
Invoke-RestMethod `
-Uri 'https://cp.jiansun.vip/api/internal/script-monitor/report' `
-Method Post `
-ContentType 'application/json; charset=utf-8' `
-Headers $headers `
-InFile '.\report-test.json'
```
成功响应中应看到 `success: true`,并返回 `scriptCount`。随后刷新平台页面,机器应在线且显示 JSON 中的脚本。
Python 自定义采集器应按以下方式调用;`payload` 就是 A.2 的对象:
```python
response = requests.post(
"https://cp.jiansun.vip/api/internal/script-monitor/report",
headers={
"Authorization": f"Bearer {agent_token}",
"x-script-monitor-agent-id": agent_id,
"Content-Type": "application/json",
},
json=payload,
timeout=10,
)
print(response.status_code, response.text)
response.raise_for_status()
```
### A.7 一次定位错误
| HTTP/错误 | 含义和处理 |
| --- | --- |
| `404` | 路径错误,通常仍在使用旧的 `/api/agent/...` 或漏写 `/api/internal/script-monitor` |
| `405` | 实际到达服务器的请求方法不是 `POST`,或请求被调用工具/代理改成了 `GET`;记录最终 URL 和最终方法再排查 |
| `400 / VALIDATION_003` | JSON 未通过 A.3 的严格校验;优先检查额外字段、枚举大小写、数字字符串和无时区日期 |
| `401 / AUTH_002` | JSON 格式已通过,但 Agent ID/Token 缺失、无效或不匹配 |
| `403` | 请求被平台安全策略拒绝;保存响应正文和请求 ID,联系平台负责人 |
| `200` | 上报成功;仍需在页面核对脚本数量和状态是否符合真实数据 |
排错时必须让采集器打印以下四项,凭据只保留前后各 2 位并遮盖中间内容:最终请求方法、最终 URL、HTTP 状态码、完整响应正文。不要只打印 `success=False`,否则无法区分路径、校验、认证和安全策略。
### A.8 自定义采集器支持平台手动运行的额外要求
只实现 `/report` 和 `/run`,可以显示机器、脚本、状态和结果,但不能响应平台“立即运行”。如果需要手动触发,自定义采集器还必须:
1. 每 5 秒 `POST /api/internal/script-monitor/commands/claim`,请求体为 `{}`;
2. 收到命令后,只按 `scriptKey` 查找本机预先配置的安全命令,禁止执行平台传来的任意命令文本;
3. 开始和结束时调用 `/run`,并把领取结果中的命令 ID 放入 `commandId`;
4. 在 `/report` 对应脚本中设置 `canManualRun: true`;
5. 对高风险任务默认保持 `canManualRun: false`。
旧项目使用的 WebSocket、`/api/agent/...` 和旧消息格式与当前产品平台不兼容,不能直接复用。可以复用影刀数据采集逻辑,但上报层必须按本附录改造。

View File

@ -1,163 +0,0 @@
# 类目配置取数与筛选逻辑
> 用途:给需要接入类目配置数据的开发人员使用。本文描述当前代码和数据库的真实口径(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

@ -1,99 +0,0 @@
# 内容视频流程看板取数与筛选逻辑
> 用途:给需要接入「新品-视频进度(内容部)」(钉钉内容视频流程)数据的开发人员使用。本文描述当前代码和数据库的真实口径(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

@ -1,188 +0,0 @@
# 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

@ -1,43 +0,0 @@
# 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

@ -78,13 +78,9 @@
# 是否启用 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_CLIENT_ID=
DINGTALK_CLIENT_SECRET=
DINGTALK_CALLBACK_ORIGIN=
# 待办跳转回系统的基础地址

View File

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

View File

@ -1,363 +0,0 @@
# 店铺账号管理取数与筛选逻辑
> 用途:给需要接入店铺账号管理数据的开发人员使用。本文描述当前代码和数据库的真实口径。
>
> 重要:本文不包含数据库连接串、账号、密码、加密密钥或任何业务明文数据。当前数据由本系统人工维护,不直接从外部数据库同步。
## 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

@ -1,289 +0,0 @@
# 物流到货签收时长看板 · 订单明细每日快照
## 为什么要做
看板的全部数据来自远端只读报表视图 `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

@ -0,0 +1,207 @@
# 苹果风格视觉设计标准 (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

@ -1,124 +0,0 @@
# 限价与破价判定规则(业务确认版)
> 适用范围:限价管理 · 破价订单审核(`/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

@ -74,7 +74,6 @@ const contentSecurityPolicy = [
"base-uri 'self'",
"object-src 'none'",
"frame-ancestors 'none'",
`frame-src 'self' ${lodopScriptSources.join(' ')}`,
"form-action 'self'",
"img-src 'self' data: blob: http: https:",
"font-src 'self' data:",
@ -240,7 +239,7 @@ const nextConfig = {
},
// 服务器外部包配置(Next.js 16新配置)
serverExternalPackages: ['sharp', 'three', 'gsap', '@huggingface/transformers', 'onnxruntime-node', 'mssql'],
serverExternalPackages: ['sharp', 'three', 'gsap', '@huggingface/transformers', 'onnxruntime-node'],
// Turbopack配置(Next.js 16默认启用)
turbopack: {},

View File

@ -26,7 +26,6 @@
"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",
@ -43,7 +42,7 @@
"next": "^16.1.6",
"obj2gltf": "^3.2.0",
"pg": "^8.17.2",
"pinyin-pro": "^3.29.5",
"pinyin-pro": "^3.28.1",
"postcss": "^8.5.6",
"print-js": "^1.6.0",
"puppeteer": "^24.40.0",
@ -53,12 +52,10 @@
"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"
@ -69,7 +66,6 @@
"@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",
@ -6149,13 +6145,6 @@
"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",
@ -8884,13 +8873,6 @@
"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",
@ -9355,20 +9337,6 @@
"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",
@ -15537,9 +15505,9 @@
}
},
"node_modules/pinyin-pro": {
"version": "3.29.5",
"resolved": "https://registry.npmjs.org/pinyin-pro/-/pinyin-pro-3.29.5.tgz",
"integrity": "sha512-Fk5YoRmtdHqJWqAuzYVG/NzvzeCbaG+ZEwbY6Nbps3QAIrvQQEtcm49frcyCM8i2b5najnzPDtYqrHRYTTiuwQ==",
"version": "3.28.1",
"resolved": "https://registry.npmmirror.com/pinyin-pro/-/pinyin-pro-3.28.1.tgz",
"integrity": "sha512-oqz8ulwRgtUXRi0vbqEfGNly19zpyCxYrjhkk5TibGcgSW6eNwS5woajCXRwqURi8Ehc2yOFTiB4uNoZ+NJOnA==",
"license": "MIT"
},
"node_modules/pirates": {
@ -16953,12 +16921,6 @@
"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",
@ -18870,16 +18832,16 @@
}
},
"node_modules/uuid": {
"version": "11.1.1",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.1.tgz",
"integrity": "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==",
"version": "10.0.0",
"resolved": "https://registry.npmmirror.com/uuid/-/uuid-10.0.0.tgz",
"integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"bin": {
"uuid": "dist/esm/bin/uuid"
"uuid": "dist/bin/uuid"
}
},
"node_modules/v8-to-istanbul": {

View File

@ -3,9 +3,8 @@
"version": "0.1.0",
"private": true,
"scripts": {
"worker:ai-image": "node scripts/run-ai-image-worker.mjs",
"predev": "npm run prisma:check-client-sync",
"dev": "next dev --webpack",
"dev": "next dev",
"build": "prisma generate && next build",
"start": "next start",
"lint": "eslint",
@ -18,26 +17,11 @@
"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",
@ -47,8 +31,6 @@
"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",
@ -75,7 +57,6 @@
"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",
@ -83,17 +64,12 @@
"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",
@ -146,7 +122,6 @@
"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",
@ -163,7 +138,7 @@
"next": "^16.1.6",
"obj2gltf": "^3.2.0",
"pg": "^8.17.2",
"pinyin-pro": "^3.29.5",
"pinyin-pro": "^3.28.1",
"postcss": "^8.5.6",
"print-js": "^1.6.0",
"puppeteer": "^24.40.0",
@ -173,12 +148,10 @@
"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"
@ -212,7 +185,6 @@
"@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",
@ -235,20 +207,5 @@
"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

@ -1,48 +0,0 @@
CREATE TABLE "SupplierRecommendedProduct" (
"id" TEXT NOT NULL,
"recommendationDate" DATE,
"supplier" TEXT NOT NULL DEFAULT '',
"purchaser" TEXT NOT NULL DEFAULT '',
"productCode" TEXT NOT NULL DEFAULT '',
"imageUrl" TEXT,
"sampleRequest" TEXT NOT NULL DEFAULT '',
"dimensions" TEXT NOT NULL DEFAULT '',
"price" TEXT NOT NULL DEFAULT '',
"material" TEXT NOT NULL DEFAULT '',
"boardGrade" TEXT NOT NULL DEFAULT '',
"weight" TEXT NOT NULL DEFAULT '',
"orderQuantity" INTEGER NOT NULL DEFAULT 0,
"productionCycle" TEXT NOT NULL DEFAULT '',
"shippingMode" TEXT NOT NULL DEFAULT '',
"dropShippingFee" TEXT NOT NULL DEFAULT '',
"createdById" TEXT,
"createdByName" TEXT NOT NULL DEFAULT '',
"updatedById" TEXT,
"updatedByName" TEXT NOT NULL DEFAULT '',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "SupplierRecommendedProduct_pkey" PRIMARY KEY ("id")
);
CREATE TABLE "SupplierRecommendedProductChangeLog" (
"id" TEXT NOT NULL,
"itemId" TEXT NOT NULL,
"operation" TEXT NOT NULL,
"actorId" TEXT,
"actorName" TEXT NOT NULL,
"changes" JSONB NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "SupplierRecommendedProductChangeLog_pkey" PRIMARY KEY ("id")
);
CREATE INDEX "SupplierRecommendedProduct_recommendationDate_idx" ON "SupplierRecommendedProduct"("recommendationDate");
CREATE INDEX "SupplierRecommendedProduct_supplier_idx" ON "SupplierRecommendedProduct"("supplier");
CREATE INDEX "SupplierRecommendedProduct_purchaser_idx" ON "SupplierRecommendedProduct"("purchaser");
CREATE INDEX "SupplierRecommendedProduct_productCode_idx" ON "SupplierRecommendedProduct"("productCode");
CREATE INDEX "SupplierRecommendedProduct_shippingMode_idx" ON "SupplierRecommendedProduct"("shippingMode");
CREATE INDEX "SupplierRecommendedProductChangeLog_itemId_createdAt_idx" ON "SupplierRecommendedProductChangeLog"("itemId", "createdAt");
CREATE INDEX "SupplierRecommendedProductChangeLog_createdAt_idx" ON "SupplierRecommendedProductChangeLog"("createdAt");
ALTER TABLE "SupplierRecommendedProductChangeLog" ADD CONSTRAINT "SupplierRecommendedProductChangeLog_itemId_fkey" FOREIGN KEY ("itemId") REFERENCES "SupplierRecommendedProduct"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -1,8 +0,0 @@
ALTER TABLE "FinishedProductProgressItem" RENAME COLUMN "realtimeProgress" TO "remarks";
ALTER TABLE "FinishedProductProgressItem"
ADD COLUMN "realtimeDate" DATE,
ADD COLUMN "deliveryQuantity" INTEGER NOT NULL DEFAULT 0;
UPDATE "FinishedProductProgressItem"
SET "deliveryQuantity" = GREATEST("orderedQuantity" - "inTransitQuantity", 0);

View File

@ -1,26 +0,0 @@
ALTER TABLE "FinishedProductProgressItem"
ADD COLUMN "contractDateNumber" TEXT NOT NULL DEFAULT '';
WITH contract_first AS (
SELECT
"contractNumber",
MIN("createdAt") AS "firstCreatedAt",
to_char(MIN("createdAt") + interval '8 hours', 'YYMMDD') AS "datePrefix"
FROM "FinishedProductProgressItem"
GROUP BY "contractNumber"
), numbered AS (
SELECT
"contractNumber",
"datePrefix" || lpad(row_number() OVER (
PARTITION BY "datePrefix"
ORDER BY "firstCreatedAt" ASC, "contractNumber" ASC
)::text, 3, '0') AS "contractDateNumber"
FROM contract_first
)
UPDATE "FinishedProductProgressItem" AS item
SET "contractDateNumber" = numbered."contractDateNumber"
FROM numbered
WHERE numbered."contractNumber" = item."contractNumber";
CREATE INDEX "FinishedProductProgressItem_contractDateNumber_idx"
ON "FinishedProductProgressItem"("contractDateNumber");

View File

@ -1,12 +0,0 @@
ALTER TABLE "FinishedProductProgressItem"
ALTER COLUMN "barcode" DROP NOT NULL;
UPDATE "FinishedProductProgressItem"
SET "barcode" = NULL
WHERE "barcode" = '';
DROP INDEX IF EXISTS "FinishedProductProgressItem_contractNumber_barcode_key";
CREATE UNIQUE INDEX "FinishedProductProgressItem_contractNumber_barcode_nonempty_key"
ON "FinishedProductProgressItem"("contractNumber", "barcode")
WHERE "barcode" IS NOT NULL AND "barcode" <> '';

View File

@ -1,51 +0,0 @@
CREATE TABLE "FinishedProductDeliveryRecord" (
"id" TEXT NOT NULL,
"itemId" TEXT NOT NULL,
"deliveryDate" DATE NOT NULL,
"quantity" INTEGER NOT NULL,
"status" TEXT NOT NULL DEFAULT 'ACTIVE',
"createdById" TEXT,
"createdByName" TEXT NOT NULL DEFAULT '',
"updatedById" TEXT,
"updatedByName" TEXT NOT NULL DEFAULT '',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "FinishedProductDeliveryRecord_pkey" PRIMARY KEY ("id")
);
CREATE INDEX "FinishedProductDeliveryRecord_itemId_status_deliveryDate_idx"
ON "FinishedProductDeliveryRecord"("itemId", "status", "deliveryDate");
CREATE INDEX "FinishedProductDeliveryRecord_itemId_createdAt_idx"
ON "FinishedProductDeliveryRecord"("itemId", "createdAt");
ALTER TABLE "FinishedProductDeliveryRecord"
ADD CONSTRAINT "FinishedProductDeliveryRecord_itemId_fkey"
FOREIGN KEY ("itemId") REFERENCES "FinishedProductProgressItem"("id") ON DELETE CASCADE ON UPDATE CASCADE;
INSERT INTO "FinishedProductDeliveryRecord" (
"id",
"itemId",
"deliveryDate",
"quantity",
"createdById",
"createdByName",
"updatedById",
"updatedByName",
"createdAt",
"updatedAt"
)
SELECT
'delivery-' || substr(md5(item."id" || ':' || clock_timestamp()::text), 1, 24),
item."id",
COALESCE(item."realtimeDate", (item."createdAt" AT TIME ZONE 'Asia/Shanghai')::date),
item."deliveryQuantity",
item."createdById",
item."createdByName",
item."updatedById",
item."updatedByName",
item."createdAt",
item."updatedAt"
FROM "FinishedProductProgressItem" AS item
WHERE item."deliveryQuantity" > 0;

View File

@ -1,7 +0,0 @@
ALTER TABLE "FinishedProductProgressItem"
ADD COLUMN "completedAt" TIMESTAMP(3);
UPDATE "FinishedProductProgressItem"
SET "completedAt" = COALESCE("realtimeDate"::timestamp, "updatedAt")
WHERE "orderedQuantity" > 0
AND "deliveryQuantity" >= "orderedQuantity";

View File

@ -1,69 +0,0 @@
BEGIN;
ALTER TABLE "FinishedProductProgressItem"
ADD COLUMN "firstDueDate" DATE;
ALTER TABLE "FinishedProductProgressChangeLog"
ADD COLUMN "changeReason" TEXT;
UPDATE "FinishedProductProgressItem"
SET "firstDueDate" = "orderDueDate"
WHERE "firstDueDate" IS NULL
AND "orderDueDate" IS NOT NULL;
WITH contract_groups AS (
SELECT
"contractNumber",
"contractDateNumber",
MIN("createdAt") AS "firstCreatedAt",
COUNT(*) OVER (PARTITION BY "contractDateNumber") AS "dateNumberContractCount",
ROW_NUMBER() OVER (
PARTITION BY "contractDateNumber"
ORDER BY MIN("createdAt"), "contractNumber"
) AS "contractRank"
FROM "FinishedProductProgressItem"
WHERE trim("contractDateNumber") <> ''
GROUP BY "contractNumber", "contractDateNumber"
), collision_targets AS (
SELECT *
FROM contract_groups
WHERE "dateNumberContractCount" > 1
AND "contractRank" > 1
), prefix_max AS (
SELECT
left("contractDateNumber", 6) AS "prefix",
COALESCE(MAX(
CASE
WHEN right("contractDateNumber", 3) ~ '^[0-9]{3}$' THEN right("contractDateNumber", 3)::integer
ELSE 0
END
), 0) AS "maxSequence"
FROM "FinishedProductProgressItem"
WHERE trim("contractDateNumber") <> ''
GROUP BY left("contractDateNumber", 6)
), assignments AS (
SELECT
target."contractNumber",
left(target."contractDateNumber", 6) AS "prefix",
prefix_max."maxSequence" + ROW_NUMBER() OVER (
PARTITION BY left(target."contractDateNumber", 6)
ORDER BY target."firstCreatedAt", target."contractNumber"
) AS "newSequence"
FROM collision_targets AS target
JOIN prefix_max
ON prefix_max."prefix" = left(target."contractDateNumber", 6)
)
UPDATE "FinishedProductProgressItem" AS item
SET "contractDateNumber" = assignments."prefix" || lpad(assignments."newSequence"::text, 3, '0')
FROM assignments
WHERE item."contractNumber" = assignments."contractNumber";
UPDATE "FinishedProductProgressItem"
SET "currentNode" = CASE
WHEN trim("contractNumber") = '' THEN '合同签订'
WHEN "orderedQuantity" > 0 AND "deliveryQuantity" >= "orderedQuantity" THEN '已完结'
WHEN "deliveryQuantity" > 0 THEN '送货中'
ELSE '采购在途'
END;
COMMIT;

View File

@ -1,2 +0,0 @@
ALTER TABLE "FinishedProductProgressItem"
ADD COLUMN "followUpOwner" TEXT NOT NULL DEFAULT '';

View File

@ -1,5 +0,0 @@
ALTER TABLE "SupplierRecommendedProduct"
ADD COLUMN "category" TEXT NOT NULL DEFAULT '';
CREATE INDEX "SupplierRecommendedProduct_category_idx"
ON "SupplierRecommendedProduct"("category");

View File

@ -1,8 +0,0 @@
ALTER TABLE "SupplierRecommendedProduct"
ADD COLUMN "selectionStatus" TEXT NOT NULL DEFAULT 'UNSELECTED',
ADD COLUMN "selectedById" TEXT,
ADD COLUMN "selectedByName" TEXT NOT NULL DEFAULT '',
ADD COLUMN "selectedAt" TIMESTAMP(3);
CREATE INDEX "SupplierRecommendedProduct_selectionStatus_idx"
ON "SupplierRecommendedProduct"("selectionStatus");

View File

@ -1,18 +0,0 @@
ALTER TABLE "FinishedProductProgressItem"
ADD COLUMN "orderPlacedDate" DATE,
ADD COLUMN "orderSequenceNo" TEXT NOT NULL DEFAULT '';
-- Preserve the legacy date-number column for compatibility, while deriving a
-- real date for rows created before the order identity redesign. Legacy values
-- use YYMMDDNNN (for example 260829001 -> 2026-08-29).
UPDATE "FinishedProductProgressItem"
SET "orderPlacedDate" = to_date('20' || substring("contractDateNumber" from 1 for 6), 'YYYYMMDD')
WHERE "orderPlacedDate" IS NULL
AND "contractDateNumber" ~ '^[0-9]{9}$'
AND substring("contractDateNumber" from 3 for 2)::int BETWEEN 1 AND 12
AND substring("contractDateNumber" from 5 for 2)::int BETWEEN 1 AND 31
AND to_char(to_date('20' || substring("contractDateNumber" from 1 for 6), 'YYYYMMDD'), 'YYYYMMDD')
= '20' || substring("contractDateNumber" from 1 for 6);
CREATE INDEX "FinishedProductProgressItem_orderSequenceNo_idx"
ON "FinishedProductProgressItem"("orderSequenceNo");

View File

@ -1,14 +0,0 @@
-- The legacy index only allowed one row per contract and barcode. That
-- conflicts with the order identity redesign, where order date separates
-- otherwise identical orders and may be left blank for later maintenance.
DROP INDEX IF EXISTS "FinishedProductProgressItem_contractNumber_barcode_nonempty_key";
DROP INDEX IF EXISTS "FinishedProductProgressItem_contractNumber_barcode_key";
-- Dated orders are unique by the complete order identity. Undated rows are
-- intentionally left outside the database constraint so they can be imported
-- before the order date is maintained.
CREATE UNIQUE INDEX "FinishedProductProgressItem_order_identity_nonempty_key"
ON "FinishedProductProgressItem"("contractNumber", "itemNumber", "barcode", "orderPlacedDate")
WHERE "barcode" IS NOT NULL
AND "barcode" <> ''
AND "orderPlacedDate" IS NOT NULL;

View File

@ -1,4 +0,0 @@
-- Supports the bounded retention scan for system-generated product archive
-- field-change logs without changing the audit data itself.
CREATE INDEX "OperationLog_retention_scope_createdAt_idx"
ON "OperationLog"("workspaceCategory", "source", "status", "createdAt");

View File

@ -1,49 +0,0 @@
BEGIN;
SET LOCAL lock_timeout = '5s';
SET LOCAL statement_timeout = '30s';
CREATE TABLE public."StoreAccountPasswordAccessAudit" (
"requestId" UUID PRIMARY KEY,
"requesterId" VARCHAR(128),
"serviceId" VARCHAR(64),
"accountId" VARCHAR(128),
purpose VARCHAR(8) NOT NULL CHECK (purpose IN ('view', 'copy', 'unknown')),
status INTEGER NOT NULL CHECK (status IN (200, 403, 404, 429, 503)),
"createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT clock_timestamp()
);
CREATE INDEX "StoreAccountPasswordAccessAudit_serviceId_createdAt_idx"
ON public."StoreAccountPasswordAccessAudit" ("serviceId", "createdAt");
CREATE INDEX "StoreAccountPasswordAccessAudit_accountId_createdAt_idx"
ON public."StoreAccountPasswordAccessAudit" ("accountId", "createdAt");
REVOKE ALL ON public."StoreAccountPasswordAccessAudit" FROM PUBLIC;
DO $migration$
DECLARE
original_definition TEXT;
BEGIN
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'store_account_reader') THEN
REVOKE ALL ON public."StoreAccountPasswordAccessAudit" FROM store_account_reader;
END IF;
-- The external view is provisioned separately by the DBA. Do not create it or
-- broaden its grants in environments that have not opted into this integration.
IF to_regclass('external_store_accounts.accounts_v1') IS NULL THEN RETURN; END IF;
IF EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_schema = 'external_store_accounts' AND table_name = 'accounts_v1'
AND column_name = 'passwordConfigured') THEN
RAISE EXCEPTION 'External account contract needs preflight review';
END IF;
original_definition := rtrim(pg_get_viewdef('external_store_accounts.accounts_v1'::regclass, true), E';\n\r ');
-- Keep the original query, projection, row filters, duplicates (if any), owner
-- and ACL. EXISTS does not multiply rows. No secret is exposed in this view.
EXECUTE format($view$
CREATE OR REPLACE VIEW external_store_accounts.accounts_v1 AS
SELECT original.*, EXISTS (
SELECT 1 FROM public."StoreAccount" credential
WHERE credential.id = original.id
AND credential."passwordCiphertext" ~ '^v1:[A-Za-z0-9_-]{16}\.[A-Za-z0-9_-]{22}\.[A-Za-z0-9_-]{2,}$'
AND length(split_part(credential."passwordCiphertext", '.', 3)) %% 4 <> 1
)::boolean AS "passwordConfigured"
FROM (%s) original
$view$, original_definition);
END $migration$;
COMMIT;

View File

@ -1,92 +0,0 @@
BEGIN;
SET LOCAL lock_timeout = '5s';
SET LOCAL statement_timeout = '30s';
DO $preflight$
DECLARE
actual_columns TEXT[];
expected_columns CONSTANT TEXT[] := ARRAY[
'id', 'storeName', 'operationOwner', 'operationDepartmentHead',
'operationDirector', 'profitCenter', 'subject', 'accountType', 'account',
'boundPhone', 'verificationContact', 'jstOrderService', 'accountGroupName',
'createdAt', 'updatedAt', 'passwordConfigured'
];
BEGIN
IF to_regclass('external_store_accounts.accounts_v1') IS NULL THEN
RETURN;
END IF;
SELECT array_agg(column_name ORDER BY ordinal_position)
INTO actual_columns
FROM information_schema.columns
WHERE table_schema = 'external_store_accounts'
AND table_name = 'accounts_v1';
IF actual_columns IS DISTINCT FROM expected_columns THEN
RAISE EXCEPTION 'External account contract needs preflight review';
END IF;
END $preflight$;
DO $migration$
DECLARE
original_owner OID;
original_acl ACLITEM[];
BEGIN
IF to_regclass('external_store_accounts.accounts_v1') IS NULL THEN
RETURN;
END IF;
SELECT relowner, relacl
INTO original_owner, original_acl
FROM pg_class
WHERE oid = 'external_store_accounts.accounts_v1'::regclass;
CREATE TEMP TABLE store_account_view_rows_before ON COMMIT DROP AS
SELECT to_jsonb(account_row) - 'passwordConfigured' AS row_value
FROM external_store_accounts.accounts_v1 account_row;
CREATE OR REPLACE VIEW external_store_accounts.accounts_v1 AS
SELECT
source.id,
source."storeName",
source."operationOwner",
source."operationDepartmentHead",
source."operationDirector",
source."profitCenter",
source.subject,
source."accountType",
source.account,
source."boundPhone",
source."verificationContact",
source."jstOrderService",
source."accountGroupName",
source."createdAt",
source."updatedAt",
(source."passwordCiphertext" <> '')::boolean AS "passwordConfigured"
FROM public."StoreAccount" source;
IF EXISTS (
(SELECT row_value FROM store_account_view_rows_before
EXCEPT ALL
SELECT to_jsonb(account_row) - 'passwordConfigured'
FROM external_store_accounts.accounts_v1 account_row)
UNION ALL
(SELECT to_jsonb(account_row) - 'passwordConfigured'
FROM external_store_accounts.accounts_v1 account_row
EXCEPT ALL
SELECT row_value FROM store_account_view_rows_before)
) THEN
RAISE EXCEPTION 'External account rows changed while fixing password status';
END IF;
IF EXISTS (
SELECT 1
FROM pg_class
WHERE oid = 'external_store_accounts.accounts_v1'::regclass
AND (relowner <> original_owner OR relacl IS DISTINCT FROM original_acl)
) THEN
RAISE EXCEPTION 'External account owner or ACL changed';
END IF;
END $migration$;
COMMIT;

View File

@ -1,7 +0,0 @@
-- Persist the Jianshang (JST public available) stock on the archive main table
-- so list filters, page rows and exports all read one source of truth.
ALTER TABLE "SupplyChainProductArchive"
ADD COLUMN "stockJianshang" TEXT NOT NULL DEFAULT '';
CREATE INDEX "SupplyChainProductArchive_stockJianshang_idx"
ON "SupplyChainProductArchive"("stockJianshang");

View File

@ -1,43 +0,0 @@
-- Product archive period-aware sales facts and sync audit.
CREATE TABLE "SupplyChainProductArchiveSalesPerformanceDaily" (
"barcode" VARCHAR(100) NOT NULL,
"statDate" DATE NOT NULL,
"shipAmount" DECIMAL(24,4) NOT NULL,
"returnAmount" DECIMAL(24,4) NOT NULL,
"realPerformance" DECIMAL(24,4) NOT NULL,
"shipQty" BIGINT NOT NULL,
"returnQty" BIGINT NOT NULL,
"clearSalesCost" DECIMAL(24,4) NOT NULL,
"sendFreight" DECIMAL(24,4) NOT NULL,
"platformCommission" DECIMAL(24,4) NOT NULL,
"xyCost" DECIMAL(24,4) NOT NULL,
"afterSaleAmount" DECIMAL(24,4) NOT NULL,
"pushFeeTotal" DECIMAL(24,4) NOT NULL,
"marketingFeeTotal" DECIMAL(24,4) NOT NULL,
"advertisementFeeTotal" DECIMAL(24,4) NOT NULL,
"sharedWarehousingSum" DECIMAL(24,4) NOT NULL,
"otherFee" DECIMAL(24,4) NOT NULL,
"operatingFee" DECIMAL(24,4) NOT NULL,
"sourceRowCount" INTEGER NOT NULL,
"syncedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "SupplyChainProductArchiveSalesPerformanceDaily_pkey" PRIMARY KEY ("barcode", "statDate")
);
CREATE INDEX "SupplyChainProductArchiveSalesPerformanceDaily_statDate_idx" ON "SupplyChainProductArchiveSalesPerformanceDaily"("statDate");
CREATE INDEX "SupplyChainProductArchiveSalesPerformanceDaily_syncedAt_idx" ON "SupplyChainProductArchiveSalesPerformanceDaily"("syncedAt");
CREATE TABLE "SupplyChainProductArchiveSalesSyncRun" (
"id" TEXT NOT NULL,
"startedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"completedAt" TIMESTAMP(3),
"status" VARCHAR(24) NOT NULL,
"rangeStart" DATE NOT NULL,
"rangeEnd" DATE NOT NULL,
"sourceLatestDate" DATE,
"rowsRead" INTEGER NOT NULL DEFAULT 0,
"rowsWritten" INTEGER NOT NULL DEFAULT 0,
"unmatchedBarcodes" INTEGER NOT NULL DEFAULT 0,
"errorMessage" TEXT,
CONSTRAINT "SupplyChainProductArchiveSalesSyncRun_pkey" PRIMARY KEY ("id")
);
CREATE INDEX "SupplyChainProductArchiveSalesSyncRun_startedAt_idx" ON "SupplyChainProductArchiveSalesSyncRun"("startedAt");
CREATE INDEX "SupplyChainProductArchiveSalesSyncRun_status_startedAt_idx" ON "SupplyChainProductArchiveSalesSyncRun"("status", "startedAt");

View File

@ -1,19 +0,0 @@
-- CreateTable
CREATE TABLE "HomeQuickLink" (
"id" TEXT NOT NULL,
"title" TEXT NOT NULL,
"description" TEXT NOT NULL DEFAULT '',
"url" TEXT NOT NULL,
"iconKey" TEXT NOT NULL DEFAULT 'link',
"sortOrder" INTEGER NOT NULL DEFAULT 0,
"enabled" BOOLEAN NOT NULL DEFAULT true,
"createdBy" TEXT NOT NULL,
"updatedBy" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "HomeQuickLink_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "HomeQuickLink_enabled_sortOrder_idx" ON "HomeQuickLink"("enabled", "sortOrder");

View File

@ -1,6 +0,0 @@
-- Keep all existing barcode/day facts and the mainline shipAmount definition.
-- NULL salesAmount explicitly marks history requiring a source backfill.
ALTER TABLE "SupplyChainProductArchiveSalesPerformanceDaily"
ADD COLUMN "skuOperator" TEXT NOT NULL DEFAULT '',
ADD COLUMN "unitCost" DECIMAL(24,4),
ADD COLUMN "salesAmount" DECIMAL(24,4);

View File

@ -1,19 +0,0 @@
-- Operations-maintained checklist per dropship (DF) SPU on the 1688 dropship
-- issues page. Read-only columns always come live from the product archive;
-- only the two operations-maintained fields live here.
CREATE TABLE "SupplyChainDropshipIssue" (
"id" TEXT NOT NULL,
"spu" VARCHAR(120) NOT NULL,
"invoiceType" VARCHAR(16) NOT NULL DEFAULT '',
"returnAddress" TEXT NOT NULL DEFAULT '',
"updatedByName" TEXT NOT NULL DEFAULT '',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "SupplyChainDropshipIssue_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "SupplyChainDropshipIssue_spu_key" ON "SupplyChainDropshipIssue"("spu");
CREATE INDEX "SupplyChainDropshipIssue_updatedAt_idx" ON "SupplyChainDropshipIssue"("updatedAt");
ALTER TYPE "OperationLogAction" ADD VALUE IF NOT EXISTS 'SUPPLY_CHAIN_DROPSHIP_ISSUE_WRITE';

View File

@ -1,15 +0,0 @@
ALTER TABLE "FinishedProductProgressItem"
ADD COLUMN "sourceOpeningCode" TEXT,
ADD COLUMN "sourceProductId" TEXT,
ADD COLUMN "sourceSkuId" TEXT,
ADD COLUMN "sourceGroupKey" TEXT,
ADD COLUMN "sourceSupplierCode" TEXT,
ADD COLUMN "sourceImportedAt" TIMESTAMP(3),
ADD COLUMN "supplierCode" TEXT NOT NULL DEFAULT '';
CREATE UNIQUE INDEX "FinishedProductProgressItem_factory_source_key"
ON "FinishedProductProgressItem" ("sourceOpeningCode", "sourceProductId", "sourceSkuId");
CREATE INDEX "FinishedProductProgressItem_sourceGroupKey_idx"
ON "FinishedProductProgressItem" ("sourceGroupKey");
CREATE INDEX "FinishedProductProgressItem_sourceImportedAt_idx"
ON "FinishedProductProgressItem" ("sourceImportedAt");

View File

@ -1,4 +0,0 @@
-- 1688 dropship issues page: operations now maintain whether the SPU ships
-- assembled (整装, drawing not required) or knocked down (拆装, drawing required).
ALTER TABLE "SupplyChainDropshipIssue"
ADD COLUMN "assemblyType" VARCHAR(16) NOT NULL DEFAULT '';

View File

@ -1,21 +0,0 @@
-- CreateTable
CREATE TABLE IF NOT EXISTS "BomCostingAgentSkuEstimate" (
"id" TEXT NOT NULL,
"caseId" TEXT NOT NULL,
"skuId" TEXT NOT NULL,
"externalSkuId" TEXT NOT NULL DEFAULT '',
"engineVersion" TEXT NOT NULL,
"status" TEXT NOT NULL,
"estimate" JSONB NOT NULL,
"usage" JSONB,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "BomCostingAgentSkuEstimate_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX IF NOT EXISTS "BomCostingAgentSkuEstimate_skuId_engineVersion_key" ON "BomCostingAgentSkuEstimate"("skuId", "engineVersion");
-- CreateIndex
CREATE INDEX IF NOT EXISTS "BomCostingAgentSkuEstimate_caseId_idx" ON "BomCostingAgentSkuEstimate"("caseId");

View File

@ -1,22 +0,0 @@
CREATE TABLE "SupplyChainManagementOwnerConfig" (
"id" TEXT NOT NULL,
"spu" TEXT NOT NULL,
"itemNumber" TEXT NOT NULL,
"productCode" TEXT NOT NULL,
"operationOwner" TEXT NOT NULL DEFAULT '',
"departmentOwner" TEXT NOT NULL DEFAULT '',
"generalOwner" TEXT NOT NULL DEFAULT '',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "SupplyChainManagementOwnerConfig_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "SupplyChainManagementOwnerConfig_spu_itemNumber_productCode_key"
ON "SupplyChainManagementOwnerConfig"("spu", "itemNumber", "productCode");
CREATE INDEX "SupplyChainManagementOwnerConfig_spu_idx"
ON "SupplyChainManagementOwnerConfig"("spu");
CREATE INDEX "SupplyChainManagementOwnerConfig_itemNumber_idx"
ON "SupplyChainManagementOwnerConfig"("itemNumber");
CREATE INDEX "SupplyChainManagementOwnerConfig_productCode_idx"
ON "SupplyChainManagementOwnerConfig"("productCode");

View File

@ -1,9 +0,0 @@
DROP INDEX IF EXISTS "SupplyChainManagementOwnerConfig_spu_itemNumber_productCode_key";
DROP INDEX IF EXISTS "SupplyChainManagementOwnerConfig_spu_idx";
DROP INDEX IF EXISTS "SupplyChainManagementOwnerConfig_itemNumber_idx";
DROP INDEX IF EXISTS "SupplyChainManagementOwnerConfig_productCode_idx";
ALTER TABLE "SupplyChainManagementOwnerConfig"
DROP COLUMN IF EXISTS "spu",
DROP COLUMN IF EXISTS "itemNumber",
DROP COLUMN IF EXISTS "productCode";

View File

@ -1,8 +0,0 @@
ALTER TABLE "Department"
ADD COLUMN "directoryManagerDingtalkUserIds" TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[];
ALTER TABLE "EmployeeDirectory"
ADD COLUMN "managerDingtalkUserId" TEXT;
CREATE INDEX "EmployeeDirectory_managerDingtalkUserId_idx"
ON "EmployeeDirectory"("managerDingtalkUserId");

View File

@ -1,10 +0,0 @@
ALTER TABLE "SupplyChainManagementOwnerConfig"
ADD COLUMN "sourceEmployeeId" TEXT;
CREATE UNIQUE INDEX "SupplyChainManagementOwnerConfig_sourceEmployeeId_key"
ON "SupplyChainManagementOwnerConfig"("sourceEmployeeId");
ALTER TABLE "SupplyChainManagementOwnerConfig"
ADD CONSTRAINT "SupplyChainManagementOwnerConfig_sourceEmployeeId_fkey"
FOREIGN KEY ("sourceEmployeeId") REFERENCES "EmployeeDirectory"("id")
ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -1,6 +0,0 @@
ALTER TABLE "SupplyChainManagementOwnerConfig"
ADD COLUMN "manualOverride" BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN "isHidden" BOOLEAN NOT NULL DEFAULT false;
CREATE INDEX "SupplyChainManagementOwnerConfig_isHidden_updatedAt_idx"
ON "SupplyChainManagementOwnerConfig"("isHidden", "updatedAt");

View File

@ -1,3 +0,0 @@
ALTER TABLE "SupplyChainManagementOwnerConfig"
ADD COLUMN "department" TEXT NOT NULL DEFAULT '',
ADD COLUMN "profitCenter" TEXT NOT NULL DEFAULT '';

View File

@ -1,42 +0,0 @@
CREATE TABLE "Dropship1688SourcingTask" (
"id" TEXT NOT NULL,
"sourceArchiveId" TEXT NOT NULL,
"sourceSpu" TEXT NOT NULL DEFAULT '',
"sourceBarcode" VARCHAR(100) NOT NULL,
"sourceCostPrice" DECIMAL(24,4),
"sourceSkuOperator" TEXT NOT NULL DEFAULT '',
"sourceDateFrom" DATE NOT NULL,
"sourceDateTo" DATE NOT NULL,
"purchaserUserId" TEXT NOT NULL,
"purchaserName" TEXT NOT NULL,
"supplierName" TEXT NOT NULL DEFAULT '',
"quoted_price_tax_included" DECIMAL(24,4),
"invoiceTaxPoint" DECIMAL(8,4),
"sourcingMode" TEXT NOT NULL DEFAULT '',
"jianshangBarcode" TEXT NOT NULL DEFAULT '',
"acceptedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Dropship1688SourcingTask_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "Dropship1688SourcingTask_sourceBarcode_purchaserUserId_key"
ON "Dropship1688SourcingTask"("sourceBarcode", "purchaserUserId");
CREATE INDEX "Dropship1688SourcingTask_sourceArchiveId_idx"
ON "Dropship1688SourcingTask"("sourceArchiveId");
CREATE INDEX "Dropship1688SourcingTask_purchaserName_idx"
ON "Dropship1688SourcingTask"("purchaserName");
CREATE INDEX "Dropship1688SourcingTask_sourceSpu_idx"
ON "Dropship1688SourcingTask"("sourceSpu");
CREATE INDEX "Dropship1688SourcingTask_sourceSkuOperator_idx"
ON "Dropship1688SourcingTask"("sourceSkuOperator");
CREATE INDEX "Dropship1688SourcingTask_purchaserUserId_acceptedAt_idx"
ON "Dropship1688SourcingTask"("purchaserUserId", "acceptedAt");
CREATE INDEX "Dropship1688SourcingTask_acceptedAt_idx"
ON "Dropship1688SourcingTask"("acceptedAt");

View File

@ -1,3 +0,0 @@
-- Store the quantity-one freight estimate separately from the existing total freight fact.
ALTER TABLE "SupplyChainProductArchiveSalesPerformanceDaily"
ADD COLUMN "estimatedFreight" DECIMAL(24,4);

View File

@ -1,32 +0,0 @@
ALTER TABLE "SupplyChainManagementOwnerConfig"
ADD COLUMN "secondaryDepartment" TEXT NOT NULL DEFAULT '';
WITH RECURSIVE department_tree AS (
SELECT "id", "name", "parentId", 0 AS depth
FROM "Department"
WHERE "externalId" = '1'
UNION ALL
SELECT child."id", child."name", child."parentId", parent.depth + 1
FROM "Department" child
INNER JOIN department_tree parent ON child."parentId" = parent."id"
), employee_ancestors AS (
SELECT employee."id" AS "employeeId", department."id" AS "departmentId", department."parentId"
FROM "EmployeeDirectory" employee
INNER JOIN "Department" department
ON department."externalId" = employee."primaryDepartmentExternalId"
UNION ALL
SELECT ancestor."employeeId", parent."id", parent."parentId"
FROM employee_ancestors ancestor
INNER JOIN "Department" parent ON parent."id" = ancestor."parentId"
)
UPDATE "SupplyChainManagementOwnerConfig" config
SET "secondaryDepartment" = department_tree."name"
FROM employee_ancestors
INNER JOIN department_tree
ON department_tree."id" = employee_ancestors."departmentId"
AND department_tree.depth = 3
WHERE config."sourceEmployeeId" = employee_ancestors."employeeId";

View File

@ -1,67 +0,0 @@
ALTER TABLE "SupplyChainProductArchive"
ADD COLUMN "sabcRating" VARCHAR(1) NOT NULL DEFAULT '',
ADD COLUMN "sabcUpdatedAt" TIMESTAMP(3);
CREATE TABLE "SupplyChainProductArchiveSabcResult" (
"id" TEXT NOT NULL,
"archiveId" TEXT NOT NULL,
"barcode" VARCHAR(100) NOT NULL,
"periodStart" DATE NOT NULL,
"periodEnd" DATE NOT NULL,
"eligible" BOOLEAN NOT NULL,
"sabcRating" VARCHAR(1) NOT NULL DEFAULT '',
"category" TEXT NOT NULL DEFAULT '',
"performanceScale" TEXT NOT NULL DEFAULT '',
"salesScale" TEXT NOT NULL DEFAULT '',
"finalScale" TEXT NOT NULL DEFAULT '',
"profitLabel" TEXT NOT NULL DEFAULT '',
"averageDailySales" DECIMAL(24,8),
"feeRatio" DECIMAL(20,8),
"grossMargin" DECIMAL(20,8),
"shipAmount" DECIMAL(24,4) NOT NULL,
"realPerformance" DECIMAL(24,4) NOT NULL,
"shipQty" BIGINT NOT NULL,
"grossProfit" DECIMAL(24,4) NOT NULL,
"afterSaleAmount" DECIMAL(24,4) NOT NULL,
"status" VARCHAR(24) NOT NULL DEFAULT 'CALCULATED',
"reason" TEXT NOT NULL DEFAULT '',
"calculatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "SupplyChainProductArchiveSabcResult_pkey" PRIMARY KEY ("id")
);
CREATE TABLE "SupplyChainProductArchiveSabcRun" (
"id" TEXT NOT NULL,
"periodStart" DATE NOT NULL,
"periodEnd" DATE NOT NULL,
"mode" VARCHAR(16) NOT NULL,
"status" VARCHAR(24) NOT NULL,
"sourceRows" INTEGER NOT NULL DEFAULT 0,
"eligibleRows" INTEGER NOT NULL DEFAULT 0,
"taggedRows" INTEGER NOT NULL DEFAULT 0,
"comparedRows" INTEGER NOT NULL DEFAULT 0,
"mismatchRows" INTEGER NOT NULL DEFAULT 0,
"errorMessage" TEXT,
"startedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"completedAt" TIMESTAMP(3),
CONSTRAINT "SupplyChainProductArchiveSabcRun_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "SupplyChainProductArchiveSabcResult_barcode_periodStart_periodEnd_key"
ON "SupplyChainProductArchiveSabcResult"("barcode", "periodStart", "periodEnd");
CREATE INDEX "SupplyChainProductArchiveSabcResult_periodStart_periodEnd_idx"
ON "SupplyChainProductArchiveSabcResult"("periodStart", "periodEnd");
CREATE INDEX "SupplyChainProductArchiveSabcResult_sabcRating_periodEnd_idx"
ON "SupplyChainProductArchiveSabcResult"("sabcRating", "periodEnd");
CREATE INDEX "SupplyChainProductArchiveSabcResult_archiveId_periodEnd_idx"
ON "SupplyChainProductArchiveSabcResult"("archiveId", "periodEnd");
CREATE UNIQUE INDEX "SupplyChainProductArchiveSabcRun_periodStart_periodEnd_mode_key"
ON "SupplyChainProductArchiveSabcRun"("periodStart", "periodEnd", "mode");
CREATE INDEX "SupplyChainProductArchiveSabcRun_status_startedAt_idx"
ON "SupplyChainProductArchiveSabcRun"("status", "startedAt");
ALTER TABLE "SupplyChainProductArchiveSabcResult"
ADD CONSTRAINT "SupplyChainProductArchiveSabcResult_archiveId_fkey"
FOREIGN KEY ("archiveId") REFERENCES "SupplyChainProductArchive"("id")
ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -1,2 +0,0 @@
-- Add factory stock (scmStock from plm_productDiscontinuation) to the archive main table.
ALTER TABLE "SupplyChainProductArchive" ADD COLUMN "stockFactory" TEXT NOT NULL DEFAULT '';

View File

@ -1,29 +0,0 @@
-- 全局下载中心:后台导出/下载任务
CREATE TABLE "DownloadTask" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"userName" TEXT NOT NULL DEFAULT '',
"type" TEXT NOT NULL,
"title" TEXT NOT NULL DEFAULT '',
"paramsJson" TEXT NOT NULL DEFAULT '{}',
"status" TEXT NOT NULL DEFAULT 'pending',
"progress" INTEGER NOT NULL DEFAULT 0,
"fileName" TEXT NOT NULL DEFAULT '',
"filePath" TEXT NOT NULL DEFAULT '',
"fileSize" INTEGER NOT NULL DEFAULT 0,
"rowCount" INTEGER NOT NULL DEFAULT 0,
"error" TEXT NOT NULL DEFAULT '',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"startedAt" TIMESTAMP(3),
"finishedAt" TIMESTAMP(3),
CONSTRAINT "DownloadTask_pkey" PRIMARY KEY ("id")
);
CREATE INDEX "DownloadTask_userId_createdAt_idx" ON "DownloadTask"("userId", "createdAt" DESC);
CREATE INDEX "DownloadTask_status_idx" ON "DownloadTask"("status");
ALTER TABLE "DownloadTask"
ADD CONSTRAINT "DownloadTask_status_check"
CHECK ("status" IN ('pending', 'processing', 'completed', 'failed'));

View File

@ -1,41 +0,0 @@
ALTER TABLE "WorkRecordScoreRule"
ADD COLUMN "workFrequency" TEXT NOT NULL DEFAULT '',
ADD COLUMN "initiativeType" TEXT NOT NULL DEFAULT '';
CREATE INDEX "WorkRecordScoreRule_workFrequency_sortOrder_idx"
ON "WorkRecordScoreRule"("workFrequency", "sortOrder");
CREATE INDEX "WorkRecordScoreRule_initiativeType_sortOrder_idx"
ON "WorkRecordScoreRule"("initiativeType", "sortOrder");
CREATE TABLE "WorkRecordBusinessOption" (
"id" TEXT NOT NULL,
"type" TEXT NOT NULL,
"value" TEXT NOT NULL,
"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 "WorkRecordBusinessOption_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "WorkRecordBusinessOption_type_value_key"
ON "WorkRecordBusinessOption"("type", "value");
CREATE INDEX "WorkRecordBusinessOption_type_sortOrder_idx"
ON "WorkRecordBusinessOption"("type", "sortOrder");
CREATE INDEX "WorkRecordBusinessOption_type_isActive_sortOrder_idx"
ON "WorkRecordBusinessOption"("type", "isActive", "sortOrder");
INSERT INTO "WorkRecordBusinessOption" ("id", "type", "value", "sortOrder", "isActive", "createdAt", "updatedAt")
VALUES
('employee-hours-category-product', 'BUSINESS_CATEGORY', '产品开发', 1, true, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('employee-hours-category-design', 'BUSINESS_CATEGORY', '设计支持', 2, true, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('employee-hours-category-operation', 'BUSINESS_CATEGORY', '日常运营', 3, true, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('employee-hours-frequency-daily', 'WORK_FREQUENCY', '每日', 1, true, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('employee-hours-frequency-weekly', 'WORK_FREQUENCY', '每周', 2, true, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('employee-hours-frequency-monthly', 'WORK_FREQUENCY', '每月', 3, true, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('employee-hours-frequency-temporary', 'WORK_FREQUENCY', '临时', 4, true, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('employee-hours-initiative-active', 'INITIATIVE_TYPE', '主动', 1, true, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('employee-hours-initiative-passive', 'INITIATIVE_TYPE', '被动', 2, true, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP);

View File

@ -1,2 +0,0 @@
ALTER TABLE "SupplyChainProductArchive"
ADD COLUMN "productAttribute" TEXT NOT NULL DEFAULT '';

View File

@ -1,35 +0,0 @@
CREATE TABLE "EmployeeHoursRecord" (
"id" TEXT NOT NULL,
"workDate" DATE NOT NULL,
"nickname" TEXT NOT NULL,
"businessContent" TEXT NOT NULL,
"businessNote" TEXT NOT NULL DEFAULT '',
"businessCategory" TEXT NOT NULL,
"workFrequency" TEXT NOT NULL,
"businessSource" TEXT NOT NULL,
"startTime" VARCHAR(16) NOT NULL,
"endTime" VARCHAR(16) NOT NULL,
"minutes" INTEGER NOT NULL,
"hours" DECIMAL(10,2) NOT NULL,
"estimatedScore" DECIMAL(12,4) NOT NULL,
"approvedScore" DECIMAL(12,4),
"department" 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 "EmployeeHoursRecord_pkey" PRIMARY KEY ("id")
);
CREATE INDEX "EmployeeHoursRecord_workDate_deletedAt_idx"
ON "EmployeeHoursRecord"("workDate", "deletedAt");
CREATE INDEX "EmployeeHoursRecord_nickname_workDate_idx"
ON "EmployeeHoursRecord"("nickname", "workDate");
CREATE INDEX "EmployeeHoursRecord_businessCategory_workDate_idx"
ON "EmployeeHoursRecord"("businessCategory", "workDate");
CREATE INDEX "EmployeeHoursRecord_department_workDate_idx"
ON "EmployeeHoursRecord"("department", "workDate");

View File

@ -1,3 +0,0 @@
-- 店铺状态(数字码:0=正常、6=已关闭、3=已出售;来源公司报表库 rpt.fys_shopInfo,每日定时同步)
-- NULL 表示尚未同步或在公司报表库中未匹配到对应店铺
ALTER TABLE "StoreAccount" ADD COLUMN "shopState" INTEGER;

View File

@ -1,26 +0,0 @@
-- 类目配置增加「分类」维度,并新增分类规模档位表
ALTER TABLE "SupplyChainProductArchiveCategoryConfig" ADD COLUMN "categoryGroup" TEXT NOT NULL DEFAULT '';
CREATE INDEX "SupplyChainProductArchiveCategoryConfig_categoryGroup_idx" ON "SupplyChainProductArchiveCategoryConfig"("categoryGroup");
-- CreateTable
CREATE TABLE "SupplyChainProductArchiveCategoryGroupTier" (
"id" TEXT NOT NULL,
"categoryGroup" TEXT NOT NULL,
"scaleLabel" TEXT NOT NULL,
"performanceStart" DECIMAL(14,2) NOT NULL,
"performanceEnd" DECIMAL(14,2),
"dailySalesStart" DECIMAL(12,2) NOT NULL,
"dailySalesEnd" DECIMAL(12,2),
"sortOrder" INTEGER NOT NULL DEFAULT 0,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "SupplyChainProductArchiveCategoryGroupTier_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "SupplyChainProductArchiveCategoryGroupTier_categoryGroup_scaleLabel_key" ON "SupplyChainProductArchiveCategoryGroupTier"("categoryGroup", "scaleLabel");
-- CreateIndex
CREATE INDEX "SupplyChainProductArchiveCategoryGroupTier_categoryGroup_idx" ON "SupplyChainProductArchiveCategoryGroupTier"("categoryGroup");

View File

@ -1,6 +0,0 @@
-- 类目配置增加规模标签与业绩/日销阈值字段(终点可空 = 不设上限)
ALTER TABLE "SupplyChainProductArchiveCategoryConfig" ADD COLUMN "scaleLabel" TEXT NOT NULL DEFAULT '';
ALTER TABLE "SupplyChainProductArchiveCategoryConfig" ADD COLUMN "performanceStart" DECIMAL(14,2);
ALTER TABLE "SupplyChainProductArchiveCategoryConfig" ADD COLUMN "performanceEnd" DECIMAL(14,2);
ALTER TABLE "SupplyChainProductArchiveCategoryConfig" ADD COLUMN "dailySalesStart" DECIMAL(12,2);
ALTER TABLE "SupplyChainProductArchiveCategoryConfig" ADD COLUMN "dailySalesEnd" DECIMAL(12,2);

View File

@ -1,19 +0,0 @@
-- 类目规模行表:一行 = 一个二级类目 × 一档规模标签
CREATE TABLE "SupplyChainProductArchiveCategoryScaleRow" (
"id" TEXT NOT NULL,
"categoryL2" TEXT NOT NULL,
"scaleLabel" TEXT NOT NULL,
"performanceStart" DECIMAL(14,2),
"performanceEnd" DECIMAL(14,2),
"dailySalesStart" DECIMAL(12,2),
"dailySalesEnd" DECIMAL(12,2),
"sortOrder" INTEGER NOT NULL DEFAULT 0,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "SupplyChainProductArchiveCategoryScaleRow_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "SupplyChainProductArchiveCategoryScaleRow_categoryL2_scaleLabel_key" ON "SupplyChainProductArchiveCategoryScaleRow"("categoryL2", "scaleLabel");
CREATE INDEX "SupplyChainProductArchiveCategoryScaleRow_categoryL2_idx" ON "SupplyChainProductArchiveCategoryScaleRow"("categoryL2");

View File

@ -1,26 +0,0 @@
CREATE TABLE "EmployeeHoursAccessGrant" (
"userId" TEXT NOT NULL,
"scope" TEXT NOT NULL DEFAULT 'SELF',
"updatedById" TEXT NOT NULL,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "EmployeeHoursAccessGrant_pkey" PRIMARY KEY ("userId"),
CONSTRAINT "EmployeeHoursAccessGrant_scope_check" CHECK ("scope" IN ('SELF', 'DEPARTMENT', 'ALL'))
);
CREATE TABLE "EmployeeHoursAuditLog" (
"id" TEXT NOT NULL,
"area" TEXT NOT NULL,
"action" TEXT NOT NULL,
"status" TEXT NOT NULL,
"actorId" TEXT NOT NULL,
"actorName" TEXT NOT NULL,
"targetId" TEXT,
"message" TEXT NOT NULL,
"details" JSONB,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "EmployeeHoursAuditLog_pkey" PRIMARY KEY ("id")
);
CREATE INDEX "EmployeeHoursAuditLog_area_createdAt_idx" ON "EmployeeHoursAuditLog"("area", "createdAt");
CREATE INDEX "EmployeeHoursAuditLog_actorId_createdAt_idx" ON "EmployeeHoursAuditLog"("actorId", "createdAt");
CREATE INDEX "EmployeeHoursAuditLog_status_createdAt_idx" ON "EmployeeHoursAuditLog"("status", "createdAt");

View File

@ -1,15 +0,0 @@
-- 合并为单表:类目规模行表自带一级类目与分类列,删除分类档位模板表
ALTER TABLE "SupplyChainProductArchiveCategoryScaleRow" ADD COLUMN "categoryL1" TEXT NOT NULL DEFAULT '';
ALTER TABLE "SupplyChainProductArchiveCategoryScaleRow" ADD COLUMN "categoryGroup" TEXT NOT NULL DEFAULT '';
-- 从类目主表回填一级类目与分类
UPDATE "SupplyChainProductArchiveCategoryScaleRow" s
SET "categoryL1" = c."categoryL1",
"categoryGroup" = c."categoryGroup"
FROM "SupplyChainProductArchiveCategoryConfig" c
WHERE s."categoryL2" = c."categoryL2";
CREATE INDEX "SupplyChainProductArchiveCategoryScaleRow_categoryGroup_idx" ON "SupplyChainProductArchiveCategoryScaleRow"("categoryGroup");
-- 删除分类档位模板表(其数据已按类目展开至规模行表)
DROP TABLE "SupplyChainProductArchiveCategoryGroupTier";

View File

@ -1,2 +0,0 @@
ALTER TABLE "EmployeeHoursRecord"
ADD COLUMN "attachment" JSONB;

View File

@ -1,57 +0,0 @@
-- 员工工时独立业务配置(与工作记录 WorkRecordBusinessOption / WorkRecordScoreRule 解耦,空白起始)
CREATE TABLE "EmployeeHoursBusinessOption" (
"id" TEXT NOT NULL,
"type" TEXT NOT NULL,
"value" TEXT NOT NULL,
"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 "EmployeeHoursBusinessOption_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "EmployeeHoursBusinessOption_type_value_key"
ON "EmployeeHoursBusinessOption"("type", "value");
CREATE INDEX "EmployeeHoursBusinessOption_type_sortOrder_idx"
ON "EmployeeHoursBusinessOption"("type", "sortOrder");
CREATE INDEX "EmployeeHoursBusinessOption_type_isActive_sortOrder_idx"
ON "EmployeeHoursBusinessOption"("type", "isActive", "sortOrder");
-- 初始选项与作者环境对齐:业务来源=提前规划/临时任务,工作频率=每日/每周/每月/临时;业务分类留空由页面配置
INSERT INTO "EmployeeHoursBusinessOption" ("id", "type", "value", "sortOrder", "isActive", "createdAt", "updatedAt")
VALUES
('employee-hours-frequency-daily', 'WORK_FREQUENCY', '每日', 1, true, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('employee-hours-frequency-weekly', 'WORK_FREQUENCY', '每周', 2, true, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('employee-hours-frequency-monthly', 'WORK_FREQUENCY', '每月', 3, true, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('employee-hours-frequency-temporary', 'WORK_FREQUENCY', '临时', 4, true, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('employee-hours-initiative-planned', 'INITIATIVE_TYPE', '提前规划', 1, true, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('employee-hours-initiative-temporary-task', 'INITIATIVE_TYPE', '临时任务', 2, true, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP);
CREATE TABLE "EmployeeHoursScoreRule" (
"id" TEXT NOT NULL,
"primary" TEXT NOT NULL,
"secondary" TEXT NOT NULL DEFAULT '',
"workFrequency" TEXT NOT NULL DEFAULT '',
"initiativeType" 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 "EmployeeHoursScoreRule_pkey" PRIMARY KEY ("id")
);
CREATE INDEX "EmployeeHoursScoreRule_primary_sortOrder_idx"
ON "EmployeeHoursScoreRule"("primary", "sortOrder");
CREATE INDEX "EmployeeHoursScoreRule_workFrequency_sortOrder_idx"
ON "EmployeeHoursScoreRule"("workFrequency", "sortOrder");
CREATE INDEX "EmployeeHoursScoreRule_initiativeType_sortOrder_idx"
ON "EmployeeHoursScoreRule"("initiativeType", "sortOrder");
CREATE INDEX "EmployeeHoursScoreRule_isActive_sortOrder_idx"
ON "EmployeeHoursScoreRule"("isActive", "sortOrder");

View File

@ -1,33 +0,0 @@
ALTER TABLE "SupplyChainProductArchiveCategoryConfig"
ADD COLUMN IF NOT EXISTS "categoryGroup" TEXT NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS "scaleLabel" TEXT NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS "performanceStart" DECIMAL(14,2),
ADD COLUMN IF NOT EXISTS "performanceEnd" DECIMAL(14,2),
ADD COLUMN IF NOT EXISTS "dailySalesStart" DECIMAL(12,2),
ADD COLUMN IF NOT EXISTS "dailySalesEnd" DECIMAL(12,2);
CREATE INDEX IF NOT EXISTS "SupplyChainProductArchiveCategoryConfig_categoryGroup_idx"
ON "SupplyChainProductArchiveCategoryConfig"("categoryGroup");
CREATE TABLE IF NOT EXISTS "SupplyChainProductArchiveCategoryScaleRow" (
"id" TEXT NOT NULL,
"categoryL1" TEXT NOT NULL DEFAULT '',
"categoryL2" TEXT NOT NULL,
"categoryGroup" TEXT NOT NULL DEFAULT '',
"scaleLabel" TEXT NOT NULL,
"performanceStart" DECIMAL(14,2) NOT NULL,
"performanceEnd" DECIMAL(14,2),
"dailySalesStart" DECIMAL(12,2) NOT NULL,
"dailySalesEnd" DECIMAL(12,2),
"sortOrder" INTEGER NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "SupplyChainProductArchiveCategoryScaleRow_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX IF NOT EXISTS "SupplyChainProductArchiveCategoryScaleRow_categoryL2_scaleLabel_key"
ON "SupplyChainProductArchiveCategoryScaleRow"("categoryL2", "scaleLabel");
CREATE INDEX IF NOT EXISTS "SupplyChainProductArchiveCategoryScaleRow_categoryL2_idx"
ON "SupplyChainProductArchiveCategoryScaleRow"("categoryL2");
CREATE INDEX IF NOT EXISTS "SupplyChainProductArchiveCategoryScaleRow_categoryGroup_idx"
ON "SupplyChainProductArchiveCategoryScaleRow"("categoryGroup");

View File

@ -1,53 +0,0 @@
-- 物流到货签收时长看板的订单明细日快照(纯新增表,仅本地 Postgres 物化,不改动既有表)。
CREATE TABLE "LogisticsArrivalSigningDurationSnapshotRun" (
"id" TEXT NOT NULL,
"snapshotDate" DATE NOT NULL,
"status" TEXT NOT NULL DEFAULT 'RUNNING',
"windowFrom" TEXT NOT NULL DEFAULT '',
"windowTo" TEXT NOT NULL DEFAULT '',
"rowCount" INTEGER NOT NULL DEFAULT 0,
"message" TEXT NOT NULL DEFAULT '',
"startedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"finishedAt" TIMESTAMP(3),
CONSTRAINT "LogisticsArrivalSigningDurationSnapshotRun_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "LogisticsArrivalSigningDurationSnapshotRun_snapshotDate_key" ON "LogisticsArrivalSigningDurationSnapshotRun"("snapshotDate");
CREATE INDEX "LogisticsArrivalSigningDurationSnapshotRun_status_date_idx" ON "LogisticsArrivalSigningDurationSnapshotRun"("status", "snapshotDate");
CREATE TABLE "LogisticsArrivalSigningDurationSnapshot" (
"id" TEXT NOT NULL,
"snapshotDate" DATE NOT NULL,
"rowKey" TEXT NOT NULL,
"innerOrderId" TEXT NOT NULL,
"soId" TEXT NOT NULL DEFAULT '',
"logisticId" TEXT NOT NULL DEFAULT '',
"logisticCompanyName" TEXT NOT NULL DEFAULT '',
"logisticStatusName" TEXT NOT NULL DEFAULT '',
"logisticReturnFlag" TEXT NOT NULL DEFAULT '',
"orderLabels" TEXT NOT NULL DEFAULT '',
"remark" TEXT NOT NULL DEFAULT '',
"buyerMessage" TEXT NOT NULL DEFAULT '',
"shopName" TEXT NOT NULL DEFAULT '',
"platformName" TEXT NOT NULL DEFAULT '',
"warehouseName" TEXT NOT NULL DEFAULT '',
"receiverAddress" TEXT NOT NULL DEFAULT '',
"payTime" TEXT,
"sendTime" TEXT,
"fetchTime" TEXT,
"signTime" TEXT,
"barcode" TEXT NOT NULL DEFAULT '',
"productName" TEXT NOT NULL DEFAULT '',
"supplier" TEXT NOT NULL DEFAULT '',
"purchaser" TEXT NOT NULL DEFAULT '',
"productItems" JSONB,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "LogisticsArrivalSigningDurationSnapshot_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "LogisticsArrivalSigningDurationSnapshot_date_row_key_key" ON "LogisticsArrivalSigningDurationSnapshot"("snapshotDate", "rowKey");
CREATE INDEX "LogisticsArrivalSigningDurationSnapshot_date_sign_time_idx" ON "LogisticsArrivalSigningDurationSnapshot"("snapshotDate", "signTime");

View File

@ -1,3 +0,0 @@
-- 产品全量档案新增「产品类型」:定厂「产品生产类型」优先,JST rpt.jst_sku.vc_name 兜底。
ALTER TABLE "SupplyChainProductArchive"
ADD COLUMN "productionType" TEXT NOT NULL DEFAULT '';

View File

@ -1,5 +0,0 @@
-- 商品 SABC 月度核算冻结:保存生成时的整行明细快照。
-- 生成后(每月 3 日自动、或管理员手动执行核算)页面只读取该快照,
-- 后续档案销售状态、人员字段等变化不会再改变已生成月份的数据。
ALTER TABLE "SupplyChainProductArchiveSabcResult"
ADD COLUMN IF NOT EXISTS "snapshot" JSONB;

View File

@ -1,86 +0,0 @@
-- 停产需求管理初版:需求单、条码明细行、停产原因分类配置(纯新增表,不改动既有表)。
CREATE TABLE "ProductionStopDemand" (
"id" TEXT NOT NULL,
"demandNo" TEXT NOT NULL,
"requesterId" TEXT NOT NULL,
"requesterName" TEXT NOT NULL,
"department" TEXT NOT NULL DEFAULT '',
"reasonCategory" TEXT NOT NULL,
"reasonDetail" TEXT NOT NULL DEFAULT '',
"status" TEXT NOT NULL DEFAULT 'SUBMITTED',
"submittedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "ProductionStopDemand_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "ProductionStopDemand_demandNo_key" ON "ProductionStopDemand"("demandNo");
CREATE INDEX "ProductionStopDemand_status_submittedAt_idx" ON "ProductionStopDemand"("status", "submittedAt");
CREATE INDEX "ProductionStopDemand_requesterId_submittedAt_idx" ON "ProductionStopDemand"("requesterId", "submittedAt");
CREATE TABLE "ProductionStopDemandItem" (
"id" TEXT NOT NULL,
"demandId" TEXT NOT NULL,
"barcode" TEXT NOT NULL,
"categoryL1" TEXT NOT NULL DEFAULT '',
"categoryL2" TEXT NOT NULL DEFAULT '',
"spu" TEXT NOT NULL DEFAULT '',
"sku" TEXT NOT NULL DEFAULT '',
"productName" TEXT NOT NULL DEFAULT '',
"productStatus" TEXT NOT NULL DEFAULT '',
"supplierName" TEXT NOT NULL DEFAULT '',
"purchaseOwner" TEXT NOT NULL DEFAULT '',
"brand" TEXT NOT NULL DEFAULT '',
"productTag" TEXT NOT NULL DEFAULT '',
"productLabel" TEXT NOT NULL DEFAULT '',
"barcodeCreatedAt" TIMESTAMP(3),
"postStopSkuStatus" TEXT NOT NULL DEFAULT '',
"sales30dWithBig" TEXT NOT NULL DEFAULT '',
"sales30dWithBigSku" TEXT NOT NULL DEFAULT '',
"opsDirector" TEXT NOT NULL DEFAULT '',
"topSalesOpsDirector" TEXT NOT NULL DEFAULT '',
"stopSuggestion" TEXT NOT NULL DEFAULT '',
"replaceType" TEXT NOT NULL DEFAULT '',
"replaceBarcode" TEXT NOT NULL DEFAULT '',
"imageChange" TEXT NOT NULL DEFAULT '',
"modificationPoint" TEXT NOT NULL DEFAULT '',
"responseStatus" TEXT NOT NULL DEFAULT 'PENDING',
"responderId" TEXT,
"responderName" TEXT NOT NULL DEFAULT '',
"responseNote" TEXT NOT NULL DEFAULT '',
"respondedAt" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "ProductionStopDemandItem_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "ProductionStopDemandItem_demandId_barcode_key" ON "ProductionStopDemandItem"("demandId", "barcode");
CREATE INDEX "ProductionStopDemandItem_barcode_idx" ON "ProductionStopDemandItem"("barcode");
CREATE INDEX "ProductionStopDemandItem_sku_idx" ON "ProductionStopDemandItem"("sku");
CREATE INDEX "ProductionStopDemandItem_responseStatus_updatedAt_idx" ON "ProductionStopDemandItem"("responseStatus", "updatedAt");
CREATE INDEX "ProductionStopDemandItem_purchaseOwner_responseStatus_idx" ON "ProductionStopDemandItem"("purchaseOwner", "responseStatus");
ALTER TABLE "ProductionStopDemandItem" ADD CONSTRAINT "ProductionStopDemandItem_demandId_fkey" FOREIGN KEY ("demandId") REFERENCES "ProductionStopDemand"("id") ON DELETE CASCADE ON UPDATE CASCADE;
CREATE TABLE "ProductionStopReasonCategory" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"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 "ProductionStopReasonCategory_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "ProductionStopReasonCategory_name_key" ON "ProductionStopReasonCategory"("name");
CREATE INDEX "ProductionStopReasonCategory_isActive_sortOrder_idx" ON "ProductionStopReasonCategory"("isActive", "sortOrder");

View File

@ -1,2 +0,0 @@
-- 停产需求明细删除支持写入操作日志(新增枚举值,不改动既有数据)。
ALTER TYPE "OperationLogAction" ADD VALUE 'PRODUCTION_STOP_DEMAND_DELETE';

View File

@ -1,12 +0,0 @@
-- 商品SABC打标「各部门数据分类」配置表:品牌范围、负责人归属例外、数据权限例外、目标参数。
CREATE TABLE IF NOT EXISTS "SupplyChainProductArchiveDepartmentSetting" (
"id" TEXT NOT NULL,
"key" VARCHAR(64) NOT NULL,
"value" JSONB NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "SupplyChainProductArchiveDepartmentSetting_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX IF NOT EXISTS "SupplyChainProductArchiveDepartmentSetting_key_key"
ON "SupplyChainProductArchiveDepartmentSetting" ("key");

View File

@ -1,16 +0,0 @@
-- 商品SABC打标「各部门数据分类」月度快照:核算生成后落库,页面/导出直接读结果,不再每次重算。
CREATE TABLE IF NOT EXISTS "SupplyChainProductArchiveDepartmentSnapshot" (
"id" TEXT NOT NULL,
"periodStart" DATE NOT NULL,
"periodEnd" DATE NOT NULL,
"payload" JSONB NOT NULL,
"rowTotal" INTEGER NOT NULL DEFAULT 0,
"generatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "SupplyChainProductArchiveDepartmentSnapshot_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX IF NOT EXISTS "SupplyChainProductArchiveDepartmentSnapshot_periodStart_periodEnd_key"
ON "SupplyChainProductArchiveDepartmentSnapshot" ("periodStart", "periodEnd");
CREATE INDEX IF NOT EXISTS "SupplyChainProductArchiveDepartmentSnapshot_generatedAt_idx"
ON "SupplyChainProductArchiveDepartmentSnapshot" ("generatedAt");

View File

@ -1,172 +0,0 @@
-- CreateTable
CREATE TABLE "InstallationDemoV2Drawing" (
"id" TEXT NOT NULL,
"spuCode" TEXT NOT NULL,
"drawingCode" TEXT NOT NULL,
"createdBy" TEXT NOT NULL,
"parts" JSONB,
"steps" JSONB,
"numbering" JSONB,
"toolIds" JSONB,
"uploadedModel" JSONB,
"explodeTemplate" JSONB,
"sourceDrawingId" TEXT,
"sourceCopiedAt" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "InstallationDemoV2Drawing_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "InstallationDemoV2NumberingConfig" (
"id" TEXT NOT NULL,
"drawingId" TEXT NOT NULL,
"parts" JSONB NOT NULL,
"view" JSONB,
"partsUpdatedAt" TIMESTAMP(3),
"viewUpdatedAt" TIMESTAMP(3),
"updatedById" TEXT,
"updatedByName" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "InstallationDemoV2NumberingConfig_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "InstallationDemoV2NumberingSnapshot" (
"id" TEXT NOT NULL,
"drawingId" TEXT NOT NULL,
"modelUrl" TEXT NOT NULL,
"modelIdentity" TEXT NOT NULL,
"snapshotUrls" JSONB NOT NULL,
"snapshotLineArtUrls" JSONB NOT NULL,
"toolSnapshotUrls" JSONB NOT NULL,
"toolSnapshotLineArtUrls" JSONB NOT NULL,
"savedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "InstallationDemoV2NumberingSnapshot_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "InstallationDemoV2NodeMaterialConfig" (
"id" TEXT NOT NULL,
"drawingId" TEXT NOT NULL,
"nodeMaterialConfigs" JSONB NOT NULL,
"updatedById" TEXT,
"updatedByName" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "InstallationDemoV2NodeMaterialConfig_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "InstallationDemoV2DrawingCapture" (
"id" TEXT NOT NULL,
"drawingId" TEXT NOT NULL,
"manifest" JSONB NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "InstallationDemoV2DrawingCapture_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "InstallationDemoV2DrawingTextAnnotation" (
"id" TEXT NOT NULL,
"drawingId" TEXT NOT NULL,
"annotationsByTargetKey" JSONB NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "InstallationDemoV2DrawingTextAnnotation_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "InstallationDemoV2Tool" (
"id" TEXT NOT NULL,
"toolCode" TEXT NOT NULL,
"name" TEXT NOT NULL,
"modelUrl" TEXT NOT NULL,
"fileName" TEXT NOT NULL,
"mountOffset" JSONB,
"mountRotation" JSONB,
"motionType" TEXT NOT NULL DEFAULT 'spin',
"motionPivot" JSONB,
"motionAxis" JSONB,
"spinTurns" DOUBLE PRECISION NOT NULL DEFAULT 3,
"strikeDistanceRatio" DOUBLE PRECISION NOT NULL DEFAULT 0.2,
"articulationConfig" JSONB,
"sourceToolId" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "InstallationDemoV2Tool_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "InstallationDemoV2Share" (
"id" TEXT NOT NULL,
"token" TEXT NOT NULL,
"drawingId" TEXT NOT NULL,
"createdById" TEXT NOT NULL,
"snapshot" JSONB NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"expiresAt" TIMESTAMP(3),
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "InstallationDemoV2Share_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "InstallationDemoV2Drawing_spuCode_drawingCode_key" ON "InstallationDemoV2Drawing"("spuCode", "drawingCode");
-- CreateIndex
CREATE UNIQUE INDEX "InstallationDemoV2NumberingConfig_drawingId_key" ON "InstallationDemoV2NumberingConfig"("drawingId");
-- CreateIndex
CREATE UNIQUE INDEX "InstallationDemoV2NumberingSnapshot_drawingId_key" ON "InstallationDemoV2NumberingSnapshot"("drawingId");
-- CreateIndex
CREATE UNIQUE INDEX "InstallationDemoV2NodeMaterialConfig_drawingId_key" ON "InstallationDemoV2NodeMaterialConfig"("drawingId");
-- CreateIndex
CREATE UNIQUE INDEX "InstallationDemoV2DrawingCapture_drawingId_key" ON "InstallationDemoV2DrawingCapture"("drawingId");
-- CreateIndex
CREATE UNIQUE INDEX "InstallationDemoV2DrawingTextAnnotation_drawingId_key" ON "InstallationDemoV2DrawingTextAnnotation"("drawingId");
-- CreateIndex
CREATE UNIQUE INDEX "InstallationDemoV2Tool_toolCode_key" ON "InstallationDemoV2Tool"("toolCode");
-- CreateIndex
CREATE UNIQUE INDEX "InstallationDemoV2Tool_sourceToolId_key" ON "InstallationDemoV2Tool"("sourceToolId");
-- CreateIndex
CREATE UNIQUE INDEX "InstallationDemoV2Share_token_key" ON "InstallationDemoV2Share"("token");
-- CreateIndex
CREATE UNIQUE INDEX "InstallationDemoV2Share_createdById_drawingId_key" ON "InstallationDemoV2Share"("createdById", "drawingId");
-- AddForeignKey
ALTER TABLE "InstallationDemoV2NumberingConfig" ADD CONSTRAINT "InstallationDemoV2NumberingConfig_drawingId_fkey" FOREIGN KEY ("drawingId") REFERENCES "InstallationDemoV2Drawing"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "InstallationDemoV2NumberingSnapshot" ADD CONSTRAINT "InstallationDemoV2NumberingSnapshot_drawingId_fkey" FOREIGN KEY ("drawingId") REFERENCES "InstallationDemoV2Drawing"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "InstallationDemoV2NodeMaterialConfig" ADD CONSTRAINT "InstallationDemoV2NodeMaterialConfig_drawingId_fkey" FOREIGN KEY ("drawingId") REFERENCES "InstallationDemoV2Drawing"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "InstallationDemoV2DrawingCapture" ADD CONSTRAINT "InstallationDemoV2DrawingCapture_drawingId_fkey" FOREIGN KEY ("drawingId") REFERENCES "InstallationDemoV2Drawing"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "InstallationDemoV2DrawingTextAnnotation" ADD CONSTRAINT "InstallationDemoV2DrawingTextAnnotation_drawingId_fkey" FOREIGN KEY ("drawingId") REFERENCES "InstallationDemoV2Drawing"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "InstallationDemoV2Share" ADD CONSTRAINT "InstallationDemoV2Share_drawingId_fkey" FOREIGN KEY ("drawingId") REFERENCES "InstallationDemoV2Drawing"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -1,9 +0,0 @@
-- 物流到货签收时长看板的热路径聚合(整区间总览、环节目标、经营对象分组、超时排名)
-- 只需要 4 个时间戳 + 4 个维度列。此前查询 SELECT 全列,Postgres 只能对 900MB 的堆表做
-- Seq Scan(28 天区间实测 6.3 秒),加覆盖索引后可以走 Index Only Scan。
CREATE INDEX IF NOT EXISTS "LogisticsArrivalSigningDurationSnapshot_cover_idx"
ON "LogisticsArrivalSigningDurationSnapshot" ("snapshotDate", "signTime")
INCLUDE ("payTime", "sendTime", "fetchTime", "warehouseName", "platformName", "shopName", "logisticCompanyName");
-- 让计划器立刻用上新索引。
ANALYZE "LogisticsArrivalSigningDurationSnapshot";

View File

@ -1,3 +0,0 @@
-- Add modelCode for V2 three-level catalog (SPU -> model -> drawing)
ALTER TABLE "InstallationDemoV2Drawing" ADD COLUMN "modelCode" TEXT;

View File

@ -1,87 +0,0 @@
-- 停产需求「是否同意停产」多人确认:确认人配置、确认票、需求侧确认状态与截止时间。
ALTER TABLE "ProductionStopDemand"
ADD COLUMN "confirmationDeadline" TIMESTAMP(3),
ADD COLUMN "confirmationStatus" TEXT NOT NULL DEFAULT 'PENDING',
ADD COLUMN "confirmationAt" TIMESTAMP(3),
ADD COLUMN "confirmationReasons" TEXT NOT NULL DEFAULT '',
ADD COLUMN "confirmationPushedAt" TIMESTAMP(3),
ADD COLUMN "confirmationPushMessage" TEXT NOT NULL DEFAULT '';
-- 原「采购反馈」改为需求级「是否同意停产」多人确认,移除按明细的反馈字段。
DROP INDEX "ProductionStopDemand_status_submittedAt_idx";
ALTER TABLE "ProductionStopDemand" DROP COLUMN "status";
DROP INDEX "ProductionStopDemandItem_responseStatus_updatedAt_idx";
DROP INDEX "ProductionStopDemandItem_purchaseOwner_responseStatus_idx";
ALTER TABLE "ProductionStopDemandItem"
DROP COLUMN "responseStatus",
DROP COLUMN "responderId",
DROP COLUMN "responderName",
DROP COLUMN "responseNote",
DROP COLUMN "respondedAt";
CREATE INDEX "ProductionStopDemand_submittedAt_idx" ON "ProductionStopDemand"("submittedAt");
CREATE INDEX "ProductionStopDemandItem_purchaseOwner_idx" ON "ProductionStopDemandItem"("purchaseOwner");
CREATE INDEX "ProductionStopDemandItem_updatedAt_idx" ON "ProductionStopDemandItem"("updatedAt");
CREATE INDEX "ProductionStopDemand_confirmationStatus_confirmationDeadline_idx" ON "ProductionStopDemand"("confirmationStatus", "confirmationDeadline");
-- 历史需求按「当天 15:00 前提交当天 16:00 截止,否则次日 16:00 截止」补齐确认截止时间。
-- 注意:submittedAt / confirmationDeadline 是 timestamp(无时区),Prisma 按 UTC 读写,
-- 因此这里显式按 UTC 解释存储值,结果也写回 UTC,不依赖数据库会话时区。
UPDATE "ProductionStopDemand"
SET "confirmationDeadline" = (
CASE
WHEN (("submittedAt" AT TIME ZONE 'UTC') AT TIME ZONE 'Asia/Shanghai')::time < TIME '15:00'
THEN date_trunc('day', ("submittedAt" AT TIME ZONE 'UTC') AT TIME ZONE 'Asia/Shanghai')
+ INTERVAL '16 hours'
ELSE date_trunc('day', ("submittedAt" AT TIME ZONE 'UTC') AT TIME ZONE 'Asia/Shanghai')
+ INTERVAL '1 day 16 hours'
END
) AT TIME ZONE 'Asia/Shanghai' AT TIME ZONE 'UTC'
WHERE "confirmationDeadline" IS NULL;
CREATE TABLE "ProductionStopConfirmationApprover" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"userName" TEXT NOT NULL,
"dingtalkUnionId" TEXT NOT NULL DEFAULT '',
"isActive" BOOLEAN NOT NULL DEFAULT true,
"sortOrder" INTEGER NOT NULL DEFAULT 0,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "ProductionStopConfirmationApprover_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "ProductionStopConfirmationApprover_userId_key" ON "ProductionStopConfirmationApprover"("userId");
CREATE INDEX "ProductionStopConfirmationApprover_isActive_sortOrder_idx" ON "ProductionStopConfirmationApprover"("isActive", "sortOrder");
CREATE TABLE "ProductionStopDemandConfirmationVote" (
"id" TEXT NOT NULL,
"demandId" TEXT NOT NULL,
"approverId" TEXT NOT NULL,
"approverUserId" TEXT NOT NULL,
"approverName" TEXT NOT NULL,
"decision" TEXT NOT NULL DEFAULT 'AGREED',
"reason" TEXT NOT NULL DEFAULT '',
"source" TEXT NOT NULL DEFAULT 'USER',
"respondedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "ProductionStopDemandConfirmationVote_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "ProductionStopDemandConfirmationVote_demandId_approverId_key" ON "ProductionStopDemandConfirmationVote"("demandId", "approverId");
CREATE INDEX "ProductionStopDemandConfirmationVote_demandId_decision_idx" ON "ProductionStopDemandConfirmationVote"("demandId", "decision");
CREATE INDEX "ProductionStopDemandConfirmationVote_respondedAt_idx" ON "ProductionStopDemandConfirmationVote"("respondedAt");
ALTER TABLE "ProductionStopDemandConfirmationVote" ADD CONSTRAINT "ProductionStopDemandConfirmationVote_demandId_fkey" FOREIGN KEY ("demandId") REFERENCES "ProductionStopDemand"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "ProductionStopDemandConfirmationVote" ADD CONSTRAINT "ProductionStopDemandConfirmationVote_approverId_fkey" FOREIGN KEY ("approverId") REFERENCES "ProductionStopConfirmationApprover"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@ -1,2 +0,0 @@
-- 停产需求修改停产原因支持写入操作日志(新增枚举值,不改动既有数据)。
ALTER TYPE "OperationLogAction" ADD VALUE 'PRODUCTION_STOP_DEMAND_UPDATE';

View File

@ -1,32 +0,0 @@
-- 停产需求状态(待确认/需求驳回/已确认待走流程/已走流程待审批/流程审批通过)与流程负责人名单。
ALTER TABLE "ProductionStopDemand"
ADD COLUMN "demandStatus" TEXT NOT NULL DEFAULT 'PENDING_CONFIRMATION',
ADD COLUMN "demandStatusAt" TIMESTAMP(3),
ADD COLUMN "demandStatusNote" TEXT NOT NULL DEFAULT '';
CREATE INDEX "ProductionStopDemand_demandStatus_updatedAt_idx" ON "ProductionStopDemand"("demandStatus", "updatedAt");
CREATE TABLE "ProductionStopFlowOwner" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"userName" TEXT NOT NULL,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"sortOrder" INTEGER NOT NULL DEFAULT 0,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "ProductionStopFlowOwner_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "ProductionStopFlowOwner_userId_key" ON "ProductionStopFlowOwner"("userId");
CREATE INDEX "ProductionStopFlowOwner_isActive_sortOrder_idx" ON "ProductionStopFlowOwner"("isActive", "sortOrder");
-- 历史需求按当前确认结果回填需求状态(确认通过→已确认待走流程;确认驳回→需求驳回)。
UPDATE "ProductionStopDemand"
SET "demandStatus" = CASE
WHEN "confirmationStatus" = 'AGREED' THEN 'CONFIRMED_WAIT_FLOW'
WHEN "confirmationStatus" = 'REJECTED' THEN 'REJECTED'
ELSE 'PENDING_CONFIRMATION'
END,
"demandStatusAt" = COALESCE("confirmationAt", "createdAt");

View File

@ -1,3 +0,0 @@
-- 按条码拆单后,用批次号把同一次提交的需求单串起来(用于批量确认与推送聚合)。
ALTER TABLE "ProductionStopDemand" ADD COLUMN "batchNo" TEXT NOT NULL DEFAULT '';
CREATE INDEX "ProductionStopDemand_batchNo_idx" ON "ProductionStopDemand"("batchNo");

View File

@ -1,16 +0,0 @@
-- 盘停建议配置:人工批量维护「条码 → 盘停建议」,提单时按条码自动带出。
CREATE TABLE "ProductionStopSuggestionConfig" (
"id" TEXT NOT NULL,
"barcode" TEXT NOT NULL,
"suggestion" TEXT NOT NULL DEFAULT '',
"updatedById" TEXT NOT NULL DEFAULT '',
"updatedByName" TEXT NOT NULL DEFAULT '',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "ProductionStopSuggestionConfig_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "ProductionStopSuggestionConfig_barcode_key" ON "ProductionStopSuggestionConfig"("barcode");
CREATE INDEX "ProductionStopSuggestionConfig_updatedAt_idx" ON "ProductionStopSuggestionConfig"("updatedAt");

View File

@ -1,25 +0,0 @@
-- 简尚「替换款管理」换货规则表(商品SABC打标「同款商品编码」依赖)。
-- 历史遗留:该表一直没进 migrations(本地用 db push 建过),正式环境用 migrate deploy 时缺失,
-- 导致同款商品编码全部为空。这里补上建表语句,生产/本地都可重复执行。
CREATE TABLE IF NOT EXISTS "SupplyChainProductArchiveReplacementRule" (
"id" TEXT NOT NULL,
"sourceId" INTEGER NOT NULL,
"sku" TEXT NOT NULL,
"replaceSku" TEXT NOT NULL DEFAULT '',
"replaceType" INTEGER NOT NULL DEFAULT 0,
"isSwapping" INTEGER NOT NULL DEFAULT 0,
"differentPoints" TEXT NOT NULL DEFAULT '',
"creator" TEXT NOT NULL DEFAULT '',
"sourceCreatedAt" TIMESTAMP(6),
"syncedAt" TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "SupplyChainProductArchiveReplacementRule_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX IF NOT EXISTS "SupplyChainProductArchiveReplacementRule_sourceId_key"
ON "SupplyChainProductArchiveReplacementRule" ("sourceId");
CREATE INDEX IF NOT EXISTS "SupplyChainProductArchiveReplacementRule_sku_idx"
ON "SupplyChainProductArchiveReplacementRule" ("sku");
CREATE INDEX IF NOT EXISTS "SupplyChainProductArchiveReplacementRule_replaceSku_idx"
ON "SupplyChainProductArchiveReplacementRule" ("replaceSku");

View File

@ -1,17 +0,0 @@
-- Retire the independent installation demo V2 experiment.
-- Keep the original migrations unchanged and remove only V2-owned tables.
-- Drop dependents first; do not cascade into unrelated objects.
BEGIN;
SET LOCAL lock_timeout = '5s';
DROP TABLE IF EXISTS "InstallationDemoV2NumberingConfig";
DROP TABLE IF EXISTS "InstallationDemoV2NumberingSnapshot";
DROP TABLE IF EXISTS "InstallationDemoV2NodeMaterialConfig";
DROP TABLE IF EXISTS "InstallationDemoV2DrawingCapture";
DROP TABLE IF EXISTS "InstallationDemoV2DrawingTextAnnotation";
DROP TABLE IF EXISTS "InstallationDemoV2Share";
DROP TABLE IF EXISTS "InstallationDemoV2Tool";
DROP TABLE IF EXISTS "InstallationDemoV2Drawing";
COMMIT;

View File

@ -1,25 +0,0 @@
ALTER TABLE "SupplyChainProductArchive"
ADD COLUMN "sales30d" TEXT NOT NULL DEFAULT '0.00',
ADD COLUMN "sales30dWithBig" TEXT NOT NULL DEFAULT '0.00';
CREATE TABLE "ProductArchiveMarketingSalesState" (
"id" TEXT NOT NULL,
"state" JSONB NOT NULL,
"snapshot" JSONB NOT NULL,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "ProductArchiveMarketingSalesState_pkey" PRIMARY KEY ("id")
);
CREATE TABLE "ProductArchiveMarketingSalesCheck" (
"id" TEXT NOT NULL,
"checkedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"status" TEXT NOT NULL,
"fingerprint" TEXT NOT NULL DEFAULT '',
"sourceDate" TEXT NOT NULL DEFAULT '',
"rowCount" INTEGER NOT NULL DEFAULT 0,
"skuCount" INTEGER NOT NULL DEFAULT 0,
"changedRows" INTEGER NOT NULL DEFAULT 0,
"errorCode" TEXT NOT NULL DEFAULT '',
CONSTRAINT "ProductArchiveMarketingSalesCheck_pkey" PRIMARY KEY ("id")
);
CREATE INDEX "ProductArchiveMarketingSalesCheck_checkedAt_idx" ON "ProductArchiveMarketingSalesCheck"("checkedAt");

View File

@ -1,6 +0,0 @@
-- 记录每批快照实际覆盖到的最大签收时间('YYYY-MM-DD HH:MM:SS')。
-- 快照任务把 windowTo 写成"运行当天",但数据只到运行那一刻;读路径此前只比对声明窗口,
-- 于是当天跑批后看板/导出会把尚未签收完的当天误判为已覆盖(少算当天、导出缺单)。
-- 新增 dataThrough 后,覆盖判定取 min(windowTo - 1, dataThrough 当天),数据没到就回落实时源。
ALTER TABLE "LogisticsArrivalSigningDurationSnapshotRun"
ADD COLUMN IF NOT EXISTS "dataThrough" TEXT NOT NULL DEFAULT '';

View File

@ -1,85 +0,0 @@
BEGIN;
-- Use the same lock as permission configuration writes. Retain every old row.
SELECT pg_advisory_xact_lock(2026081501);
LOCK TABLE "SupplyChainPermissionOverride" IN SHARE ROW EXCLUSIVE MODE;
WITH mappings("source", "target") AS (
VALUES
('businessManagement', 'ownerConfig'),
('businessManagement', 'limitManagement'),
('businessManagement', 'productionStopDemands'),
('businessManagement', 'managementBlankPage1'),
('businessManagement', 'managementBlankPage2'),
('newProductSampleProgress', 'finishedProductProgress')
)
INSERT INTO "SupplyChainPermissionOverride"
("id", "targetKey", "subjectKey", "action", "enabled", "createdBy", "updatedBy", "createdAt", "updatedAt")
SELECT
'scp-' || md5(old."id" || ':' || mappings."target"),
mappings."target", old."subjectKey", old."action", old."enabled",
old."createdBy", old."updatedBy", old."createdAt", old."updatedAt"
FROM "SupplyChainPermissionOverride" AS old
JOIN mappings ON old."targetKey" = mappings."source"
ON CONFLICT ("targetKey", "subjectKey", "action") DO NOTHING;
-- Logistics previously allowed exporting whenever effective view was true.
-- Preserve that ability once, then enforce its independent download permission.
-- Personal download=false used to suppress editing, not the view-based export.
-- Leave inherited personal view/download unset in that case and copy edit=false.
WITH legacy AS (
SELECT
"subjectKey",
bool_or("enabled") FILTER (WHERE "action" = 'view') AS v,
bool_or("enabled") FILTER (WHERE "action" = 'download') AS d,
bool_or("enabled") FILTER (WHERE "action" = 'edit') AS e,
min("id") AS "sourceId",
max("createdBy") AS "createdBy", max("updatedBy") AS "updatedBy",
min("createdAt") AS "createdAt", max("updatedAt") AS "updatedAt"
FROM "SupplyChainPermissionOverride"
WHERE "targetKey" = 'businessManagement'
GROUP BY "subjectKey"
), access AS (
SELECT *,
CASE
WHEN "subjectKey" LIKE 'user@%' THEN
CASE WHEN v IS NOT NULL THEN v
WHEN d IS DISTINCT FROM FALSE AND (d IS TRUE OR e IS TRUE) THEN TRUE
ELSE NULL END
WHEN v IS FALSE THEN FALSE
WHEN d IS FALSE THEN v IS TRUE
ELSE v IS TRUE OR d IS TRUE OR e IS TRUE
END AS "newView",
CASE
WHEN "subjectKey" LIKE 'user@%' THEN
CASE WHEN v IS FALSE THEN FALSE
WHEN v IS TRUE THEN TRUE
WHEN d IS DISTINCT FROM FALSE AND (d IS TRUE OR e IS TRUE) THEN TRUE
ELSE NULL END
WHEN v IS FALSE THEN FALSE
WHEN d IS FALSE THEN v IS TRUE
ELSE v IS TRUE OR d IS TRUE OR e IS TRUE
END AS "newDownload",
CASE
WHEN "subjectKey" LIKE 'user@%' THEN
CASE WHEN v IS FALSE OR d IS FALSE THEN FALSE ELSE e END
ELSE v IS DISTINCT FROM FALSE AND d IS DISTINCT FROM FALSE AND e IS TRUE
END AS "newEdit"
FROM legacy
)
INSERT INTO "SupplyChainPermissionOverride"
("id", "targetKey", "subjectKey", "action", "enabled", "createdBy", "updatedBy", "createdAt", "updatedAt")
SELECT
'scp-' || md5(access."sourceId" || ':logisticsArrivalSigningDuration:' || actions."action"),
'logisticsArrivalSigningDuration', access."subjectKey", actions."action", actions."enabled",
access."createdBy", access."updatedBy", access."createdAt", access."updatedAt"
FROM access
CROSS JOIN LATERAL (VALUES
('view', access."newView"),
('download', access."newDownload"),
('edit', access."newEdit")
) AS actions("action", "enabled")
WHERE actions."enabled" IS NOT NULL
ON CONFLICT ("targetKey", "subjectKey", "action") DO NOTHING;
COMMIT;

View File

@ -1,15 +0,0 @@
CREATE TABLE "DiyScheme" (
"id" TEXT NOT NULL,
"code" TEXT NOT NULL,
"name" TEXT NOT NULL,
"parentCode" TEXT,
"createdBy" TEXT NOT NULL,
"snapshot" JSONB NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "DiyScheme_pkey" PRIMARY KEY ("id"),
CONSTRAINT "DiyScheme_name_not_blank" CHECK (length(btrim("name")) BETWEEN 1 AND 80),
CONSTRAINT "DiyScheme_snapshot_object" CHECK (jsonb_typeof("snapshot") = 'object')
);
CREATE UNIQUE INDEX "DiyScheme_code_key" ON "DiyScheme"("code");
CREATE INDEX "DiyScheme_createdBy_createdAt_id_idx" ON "DiyScheme"("createdBy", "createdAt", "id");
ALTER TABLE "DiyScheme" ADD CONSTRAINT "DiyScheme_createdBy_fkey" FOREIGN KEY ("createdBy") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -1,43 +0,0 @@
CREATE TABLE "DiyCatalog" (
"spu" TEXT PRIMARY KEY, "name" TEXT NOT NULL, "version" INTEGER NOT NULL DEFAULT 1,
"sourceHash" TEXT NOT NULL, "importedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE "DiyAssetFile" (
"id" TEXT PRIMARY KEY, "spu" TEXT NOT NULL REFERENCES "DiyCatalog"("spu") ON DELETE RESTRICT ON UPDATE CASCADE,
"storageKey" TEXT NOT NULL UNIQUE, "url" TEXT NOT NULL, "sourcePath" TEXT NOT NULL,
"sha256" TEXT NOT NULL, "bytes" INTEGER NOT NULL CHECK ("bytes" > 0),
"contentType" TEXT NOT NULL, "kind" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE "DiyCabinetStyle" (
"code" TEXT PRIMARY KEY, "spu" TEXT NOT NULL REFERENCES "DiyCatalog"("spu") ON DELETE RESTRICT ON UPDATE CASCADE,
"name" TEXT NOT NULL, "displaySize" TEXT NOT NULL,
"width" DOUBLE PRECISION NOT NULL CHECK ("width" > 0),
"height" DOUBLE PRECISION NOT NULL CHECK ("height" > 0),
"depth" DOUBLE PRECISION NOT NULL CHECK ("depth" > 0),
"cellCount" INTEGER NOT NULL CHECK ("cellCount" > 0), "panelCount" INTEGER NOT NULL CHECK ("panelCount" > 0),
"panels" JSONB NOT NULL, "cells" JSONB NOT NULL, "sortOrder" INTEGER NOT NULL
);
CREATE TABLE "DiyCabinetVariant" (
"itemCode" TEXT PRIMARY KEY, "styleCode" TEXT NOT NULL REFERENCES "DiyCabinetStyle"("code") ON DELETE RESTRICT ON UPDATE CASCADE,
"name" TEXT NOT NULL, "barcodes" TEXT[] NOT NULL,
"factoryModel" TEXT NOT NULL, "factoryCode" TEXT NOT NULL, "orderQty" INTEGER NOT NULL, "sortOrder" INTEGER NOT NULL,
"thumbnailFileId" TEXT NOT NULL REFERENCES "DiyAssetFile"("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
CREATE TABLE "DiyDoorPattern" (
"itemCode" TEXT PRIMARY KEY, "spu" TEXT NOT NULL REFERENCES "DiyCatalog"("spu") ON DELETE RESTRICT ON UPDATE CASCADE,
"name" TEXT NOT NULL, "barcodes" TEXT[] NOT NULL,
"factoryModel" TEXT NOT NULL, "factoryCode" TEXT NOT NULL, "orderQty" INTEGER NOT NULL, "sortOrder" INTEGER NOT NULL,
"imageFileId" TEXT NOT NULL REFERENCES "DiyAssetFile"("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
CREATE TABLE "DiyMaterial" (
"itemCode" TEXT PRIMARY KEY, "spu" TEXT NOT NULL REFERENCES "DiyCatalog"("spu") ON DELETE RESTRICT ON UPDATE CASCADE,
"name" TEXT NOT NULL, "kind" TEXT NOT NULL CHECK ("kind" IN ('main', 'shelf')), "colorHex" TEXT NOT NULL,
"barcodes" TEXT[] NOT NULL, "factoryModel" TEXT NOT NULL, "factoryCode" TEXT NOT NULL, "sortOrder" INTEGER NOT NULL,
"textureFileId" TEXT REFERENCES "DiyAssetFile"("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
CREATE INDEX "DiyAssetFile_spu_idx" ON "DiyAssetFile"("spu");
CREATE INDEX "DiyCabinetStyle_spu_sortOrder_idx" ON "DiyCabinetStyle"("spu", "sortOrder");
CREATE UNIQUE INDEX "DiyCabinetVariant_styleCode_name_key" ON "DiyCabinetVariant"("styleCode", "name");
CREATE INDEX "DiyDoorPattern_spu_sortOrder_idx" ON "DiyDoorPattern"("spu", "sortOrder");
CREATE UNIQUE INDEX "DiyMaterial_spu_name_key" ON "DiyMaterial"("spu", "name");

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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