Compare commits
143 Commits
codex/xuxi
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c14a90eecb | ||
|
|
e6ec1bd7ff | ||
|
|
96c0184b3e | ||
|
|
3ac1331469 | ||
|
|
60fab9516d | ||
|
|
e6c25a648e | ||
|
|
a3e1af3bea | ||
|
|
31d030ce24 | ||
|
|
ef2c6aa7f4 | ||
|
|
b7b8b1ed64 | ||
|
|
6e443caf93 | ||
|
|
34a72da7dd | ||
|
|
489dbd7545 | ||
|
|
6a27f3c19d | ||
|
|
84807a89ab | ||
|
|
331e84cffb | ||
|
|
f57142164a | ||
|
|
1e798a5aa2 | ||
|
|
072678f2bd | ||
|
|
9fa09c7662 | ||
|
|
18335f0ab9 | ||
|
|
ae4c382789 | ||
|
|
9b1d7f7cd0 | ||
|
|
7b200dcc27 | ||
|
|
212f67c1eb | ||
|
|
7e093835e5 | ||
|
|
25ede03411 | ||
|
|
46a2e3418e | ||
|
|
405f217282 | ||
|
|
f7a1b2f3ac | ||
|
|
88a6855383 | ||
|
|
4d37d95606 | ||
|
|
1cbf0b38f3 | ||
|
|
b273c62a4e | ||
|
|
b1154f7b30 | ||
|
|
d4d9e0ba08 | ||
|
|
c875549d2f | ||
|
|
45589b97e0 | ||
|
|
69f1a0c30a | ||
|
|
e38560dfdb | ||
|
|
9930b1ee90 | ||
|
|
b5128e8645 | ||
|
|
9a501745f4 | ||
|
|
d40ba51515 | ||
|
|
aa1671292d | ||
|
|
6e57c8f541 | ||
|
|
03c2cc2396 | ||
|
|
495bd42ac8 | ||
|
|
58fc8bdbee | ||
|
|
f6af1843c2 | ||
|
|
06d61525bb | ||
|
|
57f6de24d0 | ||
|
|
84710c8fa5 | ||
|
|
57ef1ec3ef | ||
|
|
5b17403a8c | ||
|
|
ad7b69c3e4 | ||
|
|
d23590db0a | ||
|
|
56db9d6e93 | ||
|
|
fc885376e5 | ||
|
|
6fd5e989cc | ||
|
|
011bb99278 | ||
|
|
d7b640c482 | ||
|
|
5b329dda80 | ||
|
|
e9a1473348 | ||
|
|
3818b8a65d | ||
|
|
1be13ea777 | ||
|
|
26ff64287b | ||
|
|
11821543ae | ||
|
|
9cfb8c4dfb | ||
|
|
37ff36fe43 | ||
|
|
685a23cc2a | ||
|
|
ae28ff1334 | ||
|
|
fa04954206 | ||
|
|
ee5c4570b4 | ||
|
|
9958a7fbd4 | ||
|
|
677cdfb8e9 | ||
|
|
e0d3ae2c89 | ||
|
|
d2f8a7ca76 | ||
|
|
1cfb598d9e | ||
|
|
05a74eeb15 | ||
|
|
16d0bc90a2 | ||
|
|
a70e5a759e | ||
|
|
7580d9612d | ||
|
|
c0429c23d5 | ||
|
|
c094200ec5 | ||
|
|
c99bff0998 | ||
|
|
9df0e0ac38 | ||
|
|
1f2275333b | ||
|
|
5f29b761e1 | ||
|
|
8f0c3c6d9e | ||
|
|
751f104816 | ||
|
|
2076d0eacf | ||
|
|
e2ff1cab4f | ||
|
|
01f7a4a191 | ||
|
|
6420741003 | ||
|
|
7fae59e687 | ||
|
|
598fb4b97f | ||
|
|
a0707ca369 | ||
|
|
3088716d9a | ||
|
|
6cfdace699 | ||
|
|
95e1a462a8 | ||
|
|
baabe2c3c2 | ||
|
|
940a7d5531 | ||
|
|
747a07d4d6 | ||
|
|
993539eca2 | ||
|
|
4e70404ca6 | ||
|
|
b1b23a0b5e | ||
|
|
2a1ae8640b | ||
|
|
b5fed760cf | ||
|
|
1afd1f7ffa | ||
|
|
cdd3e8cfcb | ||
|
|
c03a9b5e36 | ||
|
|
9cc2aa9ea7 | ||
|
|
49e00b0f93 | ||
|
|
d590b5d6cd | ||
|
|
9ae4ffb44b | ||
|
|
8571b5f649 | ||
|
|
2ea3f22349 | ||
|
|
60c933a14e | ||
|
|
f18a0f3f80 | ||
|
|
ee410934ec | ||
|
|
b936295a7d | ||
|
|
9af1d9efda | ||
|
|
2be511f346 | ||
|
|
2830836b92 | ||
|
|
c169f9392c | ||
|
|
09128489fb | ||
|
|
4fd17c6677 | ||
|
|
66db2a26f6 | ||
|
|
ec8cf4a819 | ||
|
|
416bcd5939 | ||
|
|
e889d53cb6 | ||
|
|
83a3138fe9 | ||
|
|
45387c9003 | ||
|
|
a9f2917c5b | ||
|
|
77665c85b5 | ||
|
|
3f7710ef84 | ||
|
|
281e52bb95 | ||
|
|
2775b4ff83 | ||
|
|
e58f8ba96d | ||
|
|
062666f530 | ||
|
|
ce0aa8f387 | ||
|
|
5c5fef8b8e |
@ -18,6 +18,9 @@ JST_REPORT_DB_USER=
|
||||
JST_REPORT_DB_PASSWORD=
|
||||
JST_REPORT_DB_ENCRYPT=true
|
||||
JST_REPORT_DB_TRUST_SERVER_CERTIFICATE=true
|
||||
# 加工单在途排除的终态;其他状态均按未完成加工单计算 qty-io_qty。
|
||||
MODULAR_PROCESSING_WORKORDER_EXCLUDED_STATUSES=Finished,Cancelled,Canceled,Closed,Completed
|
||||
SUPPLY_CHAIN_LIMIT_SNAPSHOT_DIR=
|
||||
# 换货规则表 app.jst_sku_replace_rule 与 rpt 视图使用独立授权;留空时复用上面的主机、库和账号。
|
||||
JST_SKU_REPLACE_RULE_SCHEMA=app
|
||||
JST_SKU_REPLACE_RULE_DB_USER=
|
||||
@ -29,6 +32,7 @@ JST_DM_MIN_EXISTING_COVERAGE_RATIO=0.5
|
||||
|
||||
# 停产下架服务器 Chrome / RPA。
|
||||
JST_RPA_BROWSER_URL=http://127.0.0.1:9222
|
||||
JST_RPA_BROWSER_SERVICE=furniture-jst-rpa-browser.service
|
||||
JST_RPA_PROFILE_DIR=
|
||||
JST_RPA_CHROME_EXECUTABLE=
|
||||
JST_RPA_HEADLESS=true
|
||||
@ -39,6 +43,17 @@ JST_RPA_LOGIN_ID=
|
||||
JST_RPA_LOGIN_PASSWORD=
|
||||
JST_RPA_ATTEMPTS=3
|
||||
JST_RPA_FALLBACK_BATCH_SIZE=10000
|
||||
# 限价流程历史快照首次构建的追溯起点;后续只按更新时间增量同步。
|
||||
LIMIT_PRICE_HISTORY_SOURCE_START=2026-01-01T00:00:00
|
||||
# 物流规则和省份占比快照目录;正式站建议指向 shared/freight-config,本地留空。
|
||||
FREIGHT_CONFIG_DATA_DIR=
|
||||
# 产品限价页面已确认记录来源库(限价历史抽屉展示来源记录时使用)。
|
||||
PROD_ARCHIVE_DB_HOST=
|
||||
PROD_ARCHIVE_DB_PORT=5432
|
||||
PROD_ARCHIVE_DB_NAME=
|
||||
PROD_ARCHIVE_DB_USER=
|
||||
PROD_ARCHIVE_DB_PASSWORD=
|
||||
PROD_ARCHIVE_DB_SSL=true
|
||||
|
||||
# OA 新品流程看板数据源。默认 mysql;切换到 JSReportReplica 时设为 mssql。
|
||||
OA_BOARD_SOURCE_DRIVER=mysql
|
||||
|
||||
6
furniture-3d-system/.gitignore
vendored
@ -79,3 +79,9 @@ agents/script-monitor/config.json
|
||||
|
||||
# 下载中心本地运行时产物
|
||||
.download-center/
|
||||
|
||||
# 限价每日快照(本机/生产运行产物,不入库)
|
||||
/data/limit-price-snapshots/
|
||||
|
||||
# 产品档案对账诊断报告
|
||||
/data/archive-reconcile-diagnostics/
|
||||
|
||||
@ -0,0 +1,257 @@
|
||||
{
|
||||
"version": 1,
|
||||
"generatedAt": "2026-10-26T08:52:26.453Z",
|
||||
"sourceFile": "各省发货占比.xlsx",
|
||||
"totalShipment": 10983200,
|
||||
"rowCount": 31,
|
||||
"rows": [
|
||||
{
|
||||
"province": "安徽",
|
||||
"shipmentCount": 426181,
|
||||
"totalShipment": 10983200,
|
||||
"shipmentShare": 0.038803,
|
||||
"region": "一区",
|
||||
"regionShare": 0.214
|
||||
},
|
||||
{
|
||||
"province": "江苏",
|
||||
"shipmentCount": 800646,
|
||||
"totalShipment": 10983200,
|
||||
"shipmentShare": 0.072897,
|
||||
"region": "一区",
|
||||
"regionShare": 0.214
|
||||
},
|
||||
{
|
||||
"province": "上海",
|
||||
"shipmentCount": 325564,
|
||||
"totalShipment": 10983200,
|
||||
"shipmentShare": 0.029642,
|
||||
"region": "一区",
|
||||
"regionShare": 0.214
|
||||
},
|
||||
{
|
||||
"province": "浙江",
|
||||
"shipmentCount": 797004,
|
||||
"totalShipment": 10983200,
|
||||
"shipmentShare": 0.072566,
|
||||
"region": "一区",
|
||||
"regionShare": 0.214
|
||||
},
|
||||
{
|
||||
"province": "北京",
|
||||
"shipmentCount": 290861,
|
||||
"totalShipment": 10983200,
|
||||
"shipmentShare": 0.026482,
|
||||
"region": "二区",
|
||||
"regionShare": 0.485
|
||||
},
|
||||
{
|
||||
"province": "福建",
|
||||
"shipmentCount": 467880,
|
||||
"totalShipment": 10983200,
|
||||
"shipmentShare": 0.0426,
|
||||
"region": "二区",
|
||||
"regionShare": 0.485
|
||||
},
|
||||
{
|
||||
"province": "广东",
|
||||
"shipmentCount": 1820992,
|
||||
"totalShipment": 10983200,
|
||||
"shipmentShare": 0.165798,
|
||||
"region": "二区",
|
||||
"regionShare": 0.485
|
||||
},
|
||||
{
|
||||
"province": "河北",
|
||||
"shipmentCount": 424406,
|
||||
"totalShipment": 10983200,
|
||||
"shipmentShare": 0.038641,
|
||||
"region": "二区",
|
||||
"regionShare": 0.485
|
||||
},
|
||||
{
|
||||
"province": "河南",
|
||||
"shipmentCount": 546529,
|
||||
"totalShipment": 10983200,
|
||||
"shipmentShare": 0.04976,
|
||||
"region": "二区",
|
||||
"regionShare": 0.485
|
||||
},
|
||||
{
|
||||
"province": "湖北",
|
||||
"shipmentCount": 378469,
|
||||
"totalShipment": 10983200,
|
||||
"shipmentShare": 0.034459,
|
||||
"region": "二区",
|
||||
"regionShare": 0.485
|
||||
},
|
||||
{
|
||||
"province": "湖南",
|
||||
"shipmentCount": 386336,
|
||||
"totalShipment": 10983200,
|
||||
"shipmentShare": 0.035175,
|
||||
"region": "二区",
|
||||
"regionShare": 0.485
|
||||
},
|
||||
{
|
||||
"province": "江西",
|
||||
"shipmentCount": 271676,
|
||||
"totalShipment": 10983200,
|
||||
"shipmentShare": 0.024736,
|
||||
"region": "二区",
|
||||
"regionShare": 0.485
|
||||
},
|
||||
{
|
||||
"province": "山东",
|
||||
"shipmentCount": 626419,
|
||||
"totalShipment": 10983200,
|
||||
"shipmentShare": 0.057034,
|
||||
"region": "二区",
|
||||
"regionShare": 0.485
|
||||
},
|
||||
{
|
||||
"province": "天津",
|
||||
"shipmentCount": 115475,
|
||||
"totalShipment": 10983200,
|
||||
"shipmentShare": 0.010514,
|
||||
"region": "二区",
|
||||
"regionShare": 0.485
|
||||
},
|
||||
{
|
||||
"province": "广西",
|
||||
"shipmentCount": 397325,
|
||||
"totalShipment": 10983200,
|
||||
"shipmentShare": 0.036176,
|
||||
"region": "三区",
|
||||
"regionShare": 0.251
|
||||
},
|
||||
{
|
||||
"province": "贵州",
|
||||
"shipmentCount": 242963,
|
||||
"totalShipment": 10983200,
|
||||
"shipmentShare": 0.022121,
|
||||
"region": "三区",
|
||||
"regionShare": 0.251
|
||||
},
|
||||
{
|
||||
"province": "黑龙",
|
||||
"shipmentCount": 166557,
|
||||
"totalShipment": 10983200,
|
||||
"shipmentShare": 0.015165,
|
||||
"region": "三区",
|
||||
"regionShare": 0.251
|
||||
},
|
||||
{
|
||||
"province": "吉林",
|
||||
"shipmentCount": 135735,
|
||||
"totalShipment": 10983200,
|
||||
"shipmentShare": 0.012358,
|
||||
"region": "三区",
|
||||
"regionShare": 0.251
|
||||
},
|
||||
{
|
||||
"province": "辽宁",
|
||||
"shipmentCount": 257754,
|
||||
"totalShipment": 10983200,
|
||||
"shipmentShare": 0.023468,
|
||||
"region": "三区",
|
||||
"regionShare": 0.251
|
||||
},
|
||||
{
|
||||
"province": "山西",
|
||||
"shipmentCount": 177480,
|
||||
"totalShipment": 10983200,
|
||||
"shipmentShare": 0.016159,
|
||||
"region": "三区",
|
||||
"regionShare": 0.251
|
||||
},
|
||||
{
|
||||
"province": "陕西",
|
||||
"shipmentCount": 281956,
|
||||
"totalShipment": 10983200,
|
||||
"shipmentShare": 0.025672,
|
||||
"region": "三区",
|
||||
"regionShare": 0.251
|
||||
},
|
||||
{
|
||||
"province": "四川",
|
||||
"shipmentCount": 532179,
|
||||
"totalShipment": 10983200,
|
||||
"shipmentShare": 0.048454,
|
||||
"region": "三区",
|
||||
"regionShare": 0.251
|
||||
},
|
||||
{
|
||||
"province": "云南",
|
||||
"shipmentCount": 341852,
|
||||
"totalShipment": 10983200,
|
||||
"shipmentShare": 0.031125,
|
||||
"region": "三区",
|
||||
"regionShare": 0.251
|
||||
},
|
||||
{
|
||||
"province": "重庆",
|
||||
"shipmentCount": 222288,
|
||||
"totalShipment": 10983200,
|
||||
"shipmentShare": 0.020239,
|
||||
"region": "三区",
|
||||
"regionShare": 0.251
|
||||
},
|
||||
{
|
||||
"province": "甘肃",
|
||||
"shipmentCount": 144419,
|
||||
"totalShipment": 10983200,
|
||||
"shipmentShare": 0.013149,
|
||||
"region": "四区",
|
||||
"regionShare": 0.05
|
||||
},
|
||||
{
|
||||
"province": "海南",
|
||||
"shipmentCount": 148380,
|
||||
"totalShipment": 10983200,
|
||||
"shipmentShare": 0.01351,
|
||||
"region": "四区",
|
||||
"regionShare": 0.05
|
||||
},
|
||||
{
|
||||
"province": "内蒙",
|
||||
"shipmentCount": 166636,
|
||||
"totalShipment": 10983200,
|
||||
"shipmentShare": 0.015172,
|
||||
"region": "四区",
|
||||
"regionShare": 0.05
|
||||
},
|
||||
{
|
||||
"province": "宁夏",
|
||||
"shipmentCount": 47493,
|
||||
"totalShipment": 10983200,
|
||||
"shipmentShare": 0.004324,
|
||||
"region": "四区",
|
||||
"regionShare": 0.05
|
||||
},
|
||||
{
|
||||
"province": "青海",
|
||||
"shipmentCount": 39031,
|
||||
"totalShipment": 10983200,
|
||||
"shipmentShare": 0.003554,
|
||||
"region": "四区",
|
||||
"regionShare": 0.05
|
||||
},
|
||||
{
|
||||
"province": "西藏",
|
||||
"shipmentCount": 441,
|
||||
"totalShipment": 10983200,
|
||||
"shipmentShare": 0.00004,
|
||||
"region": "未分区",
|
||||
"regionShare": 0.0002471046689489402
|
||||
},
|
||||
{
|
||||
"province": "新疆",
|
||||
"shipmentCount": 2273,
|
||||
"totalShipment": 10983200,
|
||||
"shipmentShare": 0.000207,
|
||||
"region": "未分区",
|
||||
"regionShare": 0.0002471046689489402
|
||||
}
|
||||
]
|
||||
}
|
||||
53230
furniture-3d-system/data/freight-config/warehouse-express-rules.json
Normal file
@ -0,0 +1,18 @@
|
||||
[Unit]
|
||||
Description=Furniture JST logistics rule daily snapshot
|
||||
Wants=network-online.target furniture-jst-rpa-browser.service
|
||||
After=network-online.target furniture-jst-rpa-browser.service
|
||||
Conflicts=furniture-discontinuation-daily.service plm-barcode-sync.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
WorkingDirectory=/opt/furniture-3d-system-prod/current/furniture-3d-system
|
||||
Environment=NODE_ENV=production
|
||||
Environment=TZ=Asia/Shanghai
|
||||
Environment=PATH=/usr/local/bin:/usr/bin:/bin
|
||||
Environment=JST_RPA_BROWSER_URL=http://127.0.0.1:9222
|
||||
Environment=FREIGHT_CONFIG_DATA_DIR=/opt/furniture-3d-system-prod/shared/freight-config
|
||||
EnvironmentFile=/opt/furniture-3d-system-prod/shared/.env
|
||||
ExecStart=/usr/bin/env node node_modules/tsx/dist/cli.mjs scripts/sync-jst-logistics-rules.ts
|
||||
TimeoutStartSec=30min
|
||||
Nice=5
|
||||
@ -0,0 +1,11 @@
|
||||
[Unit]
|
||||
Description=Run JST logistics rule snapshot daily
|
||||
|
||||
[Timer]
|
||||
OnCalendar=*-*-* 09:30:00 Asia/Shanghai
|
||||
Persistent=true
|
||||
AccuracySec=30s
|
||||
Unit=furniture-jst-logistics-snapshot.service
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
@ -0,0 +1,19 @@
|
||||
[Unit]
|
||||
Description=Furniture supply-chain daily limit price snapshot
|
||||
Wants=network-online.target furniture-jst-rpa-browser.service
|
||||
After=network-online.target furniture-jst-rpa-browser.service furniture-jst-logistics-snapshot.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
WorkingDirectory=/opt/furniture-3d-system-prod/current/furniture-3d-system
|
||||
Environment=NODE_ENV=production
|
||||
Environment=TZ=Asia/Shanghai
|
||||
Environment=PATH=/usr/local/bin:/usr/bin:/bin
|
||||
Environment=JST_RPA_BROWSER_URL=http://127.0.0.1:9222
|
||||
Environment=FREIGHT_CONFIG_DATA_DIR=/opt/furniture-3d-system-prod/shared/freight-config
|
||||
Environment=SUPPLY_CHAIN_LIMIT_SNAPSHOT_DIR=/opt/furniture-3d-system-prod/shared/limit-price-snapshots
|
||||
Environment=LIMIT_SYNC_SKIP_EXCEL=1
|
||||
EnvironmentFile=/opt/furniture-3d-system-prod/shared/.env
|
||||
ExecStart=/usr/bin/env node scripts/run-limit-price-daily-snapshot.mjs
|
||||
TimeoutStartSec=2h
|
||||
Nice=10
|
||||
@ -0,0 +1,12 @@
|
||||
[Unit]
|
||||
Description=Run furniture supply-chain daily limit price snapshot
|
||||
|
||||
[Timer]
|
||||
OnCalendar=*-*-* 09:30:00 Asia/Shanghai
|
||||
Persistent=true
|
||||
RandomizedDelaySec=60
|
||||
AccuracySec=30s
|
||||
Unit=furniture-limit-price-snapshot.service
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
@ -38,6 +38,34 @@
|
||||
`node_modules/.bin/tsx scripts/jst-shop-goods-rpa.ts --action=login`
|
||||
验证登录态,不执行链接导出。
|
||||
|
||||
## 限价每日快照
|
||||
|
||||
- 生产安装入口:`furniture-3d-system/scripts/install-limit-price-snapshot-systemd-timer.sh`。
|
||||
- systemd 名称:`furniture-limit-price-snapshot.service` / `furniture-limit-price-snapshot.timer`。
|
||||
- 默认每天中国标准时间 `09:30` 执行,可在安装时通过 `LIMIT_PRICE_SNAPSHOT_CALENDAR` 覆盖。
|
||||
- 快照持久目录默认 `/opt/furniture-3d-system-prod/shared/limit-price-snapshots`,安装器会把该路径写入生产 `.env` 的 `SUPPLY_CHAIN_LIMIT_SNAPSHOT_DIR`,避免部署切换版本后丢失。
|
||||
- 任务先更新产品档案、限价、物流规则并完整对账正式产品全量档案,全部成功后才原子切换 `latest.json`。
|
||||
- 安装器会检查数据库和聚水潭报表库环境变量;生产 `.env` 更新后需要重启正式应用进程。
|
||||
- 安装、启用定时器和首次生产快照属于生产操作,需要单独授权。
|
||||
|
||||
## 模块化加工单每日生成
|
||||
|
||||
- 生产目标为 Linux,使用 systemd,不使用 macOS launchd。
|
||||
- 安装入口:`furniture-3d-system/scripts/install-modular-processing-daily-snapshot-timer.sh`。
|
||||
- systemd 名称:`furniture-modular-processing-daily-snapshot.service` / `furniture-modular-processing-daily-snapshot.timer`。
|
||||
- 默认每天中国标准时间 `09:30` 执行,可通过 `MODULAR_PROCESSING_SNAPSHOT_CALENDAR` 覆盖。
|
||||
- 安装器默认在启用 timer 后立即补跑当天快照,避免上线当天仍显示“未生成”;只需安装、不立即执行时设置 `MODULAR_PROCESSING_SNAPSHOT_RUN_NOW=0`。
|
||||
- 安装命令:
|
||||
|
||||
```bash
|
||||
sudo MODULAR_PROCESSING_SNAPSHOT_RUN_NOW=1 \
|
||||
bash /opt/furniture-3d-system-prod/current/furniture-3d-system/scripts/install-modular-processing-daily-snapshot-timer.sh
|
||||
```
|
||||
|
||||
- 运行状态:`systemctl list-timers --all furniture-modular-processing-daily-snapshot.timer --no-pager`
|
||||
- 最近日志:`journalctl -u furniture-modular-processing-daily-snapshot.service -n 100 --no-pager`
|
||||
- 手动补跑:`systemctl start furniture-modular-processing-daily-snapshot.service`
|
||||
|
||||
## 花名册同步
|
||||
|
||||
- 花名册同步属于当前 `furniture-3d-system` 自有能力,不依赖 `shujuku2` 或旧入口机。
|
||||
|
||||
33
furniture-3d-system/docs/使用说明/DIY柜门图案方向修正-发布通知.md
Normal file
@ -0,0 +1,33 @@
|
||||
# DIY 柜门图案方向修正:请核对并替换旧图片
|
||||
|
||||
状态:待发布的更新日志文案。后续正式发布时录入系统“更新日志管理”并发布,本文档本身不会向用户推送通知。
|
||||
|
||||
## 更新日志标题
|
||||
|
||||
DIY 柜门图案方向已修正,请核对并替换旧图片
|
||||
|
||||
## 更新日志摘要
|
||||
|
||||
DIY 柜门图案曾出现左右镜像,现已修正。如果此前使用过 DIY 截图、AI 白底图或场景效果图,请对照柜门原图核对方向;发现镜像的图片请重新出图,并替换已使用的旧图片。
|
||||
|
||||
## 已修正的内容
|
||||
|
||||
DIY 柜门图案在 3D 画面中曾左右镜像,部分图案、文字和动物的朝向可能与原图相反。现已修正柜门贴图方向,横竖切换后也按原图方向显示。
|
||||
|
||||
## 用过旧图片的同事需要处理
|
||||
|
||||
1. 核对修复前生成、下载或发布的 DIY 截图、AI 白底图和场景效果图,重点检查文字及图案的左右方向。
|
||||
2. 如发现镜像,请打开原保存方案,刷新页面后对照柜门原图确认方向,重新截图或生成 AI 图片。
|
||||
3. 将店铺商品图、详情页、方案资料及其他已使用位置中的错误图片替换为核对后的新图片。
|
||||
|
||||
## 已保存的方案和旧图片
|
||||
|
||||
已保存方案可以直接打开并重新出图,无需重新搭建,也不需要为这次显示方向修正重新上传柜门原图。
|
||||
|
||||
旧截图及已生成的 AI 图片不会自动改正,已下载或发布的文件也不会自动替换。请核对重新生成的图片后再使用;原本方向正确的图片可继续保留。
|
||||
|
||||
## 后续发布登记
|
||||
|
||||
- 这条说明需要作为用户可见更新日志发布,仅提交本文档不能代替发布通知。
|
||||
- 发布时使用系统现有“更新日志管理”,覆盖使用 DIY 的同事;如不能准确筛选这批人员,按全员可见发布。
|
||||
- 发布后确认普通使用者可看到标题、摘要及上述替换说明。不要把“修正 3D 预览”写成“旧图片已自动更新”。
|
||||
46
furniture-3d-system/docs/排障记录/2026-10-28-限价运费计算阻塞网页线程.md
Normal file
@ -0,0 +1,46 @@
|
||||
# 2026-10-28 限价运费计算阻塞网页线程
|
||||
|
||||
## 故障与证据
|
||||
|
||||
生产版本 `83a3138fe9704d61f13de9f665779e783931c57d` 的网页进程长时间占用一个 CPU 核心,端口 3001 的监听队列达到 512,本机健康检查无法响应,公网返回 502。穿透进程持续正常,内存和磁盘有余量,故障时没有检出矿工。
|
||||
|
||||
重启前的 CPU 采样主要落在 Unicode 格式转换、字符串替换和去空格。入口访问日志显示,部署后 `/api/supply-chain/limit-management/products` 出现长耗时;17:13 至 19:29 之间有 338 次限价计算请求,其中一次成功请求耗时 397.882 秒。请求时间包含等待与其他请求争用的时间,不能当作单次函数的纯计算耗时。
|
||||
|
||||
故障链路:
|
||||
|
||||
`限价商品列表 / 限价导出 -> calculateRegionFreightsForArchiveRows -> buildFreightCalculation -> 每个快递的每个省份调用 selectLatestApplicableRate -> 重扫并规范化整批费率`
|
||||
|
||||
10 月 28 日新增的区域和全国运费计算,把原来用于单个商品的全表扫描放大到整页商品。算法的重复扫描量随「商品数 × 快递数 × 29 个省份 × 全部费率行数」增长,而且原整批循环没有让出网页线程。并发打开或刷新列表会进一步积压请求;入口 502 和数据库事务超时是下游症状。
|
||||
|
||||
## 隔离复现
|
||||
|
||||
只读取得费率、物流规则和商品字段快照,在本机离线运行,不向生产网页接口重放旧重计算。上游费率视图有 75,630 行;复现的 200 行限价页中有 159 行能进行运费计算,共需 4,465 行费率。
|
||||
|
||||
同一快照、同一计算时间、相同输入与输出顺序,本机 Node.js 24.22.1 的一次带规范化调用计数的对比(计数会影响耗时):
|
||||
|
||||
| 项目 | 修复前 | 修复后 |
|
||||
| --- | ---: | ---: |
|
||||
| 计算耗时 | 16,056 ms | 124 ms |
|
||||
| 字符串规范化调用 | 64,631,978 | 16,030 |
|
||||
| 完整结果 SHA-256 | `a34b18ccfd50ae6053b26d7ca2513fa101368cadfdbee3befaf7f752c360efc6` | 相同 |
|
||||
|
||||
这是离线计算对比,不代表生产端到端请求耗时,也不包含数据库和网络时间。原始商业数据与 CPU 采样只保存在私有排错目录,不进入 Git。
|
||||
|
||||
另用本机独立 HTTP 子进程调用实际批量服务,数据访问以同一只读快照代替,不插入字符串计数。旧版计算用时 5,012 ms,期间并发健康请求最长等待 5,019 ms;修复版计算用时 143 ms,8 次健康请求均为 200,最长等待 29 ms。两次完整服务返回值摘要均为 `ef8a73cec474260611eb74c073d22b32c721b1881644db7ceff22cc83e41d489`。
|
||||
|
||||
## 修复边界
|
||||
|
||||
- 纯计算层按规范化后的快递和省份建立费率索引,并按原来的生效时间和规则 ID 排序。保留有效期、重量/体积/最长边、计泡、取整、失效提示和财务计算规则。
|
||||
- `createFreightCalculator` 在同一批次复用费率索引与发货占比;新的批次重新使用自己的费率快照和计算时间,不增加跨请求的陈旧缓存。
|
||||
- 批量服务超过 8 ms 计算时间片后用 `setImmediate` 让出线程,让健康检查和其他请求有执行机会。
|
||||
- 新增复杂度、报价一致性、有效期和批量让出线程的回归测试。本次生产修复保留现有运费计算公式及基线期望,不带入开发线上尚未发布的运费功能和公式变化。
|
||||
|
||||
不修改数据库结构或业务数据、页面视觉、SSH 密钥、密码、共享账号、全局 Node.js、穿透配置或其他项目服务。
|
||||
|
||||
## 发布核查
|
||||
|
||||
2026-10-29 的真实页面验证发现额外的运行时兼容问题:单商品运费能返回,大页触发让出线程时抛出 `RangeError: Maximum call stack size exceeded`。在生产私有 Node.js 22.23.4 的独立子进程中,仅加载当前 Next.js 16.1.6 的 `fast-set-immediate.external.js` 并调用 `node:timers/promises.setImmediate()` 即可复现;调用栈反复进入框架的 `patchedSetImmediatePromise`,与数据库记录无关。
|
||||
|
||||
统一的 `yieldToEventLoop` 改用 `node:timers` 的回调接口包装 Promise。它仍然把执行机会交还事件循环,不修改框架、全局计时器或其他项目;新增子进程测试加载实际 Next.js 补丁,覆盖生产运行环境,避免只用模拟计时器漏掉这类问题。
|
||||
|
||||
发布需按当前生产部署入口说明执行,保留上一版本,核对目标主机、生产提交、PM2、端口和日志。重点验证真实的限价列表和限价导出,同时并行请求健康检查;不能只验证健康检查或本地测试就认定生产修复完成。其他项目的进程 PID 和重启次数应保持不变。
|
||||
35
furniture-3d-system/docs/排障记录/2026-10-29-限价排序与DNS延迟.md
Normal file
@ -0,0 +1,35 @@
|
||||
# 2026-10-29 限价排序与 DNS 延迟
|
||||
|
||||
## 两种独立的等待
|
||||
|
||||
生产版本 `66db2a26` 已修复运费重复扫描和 Next.js 计时器兼容问题,继续检查时仍出现两类延迟,不能混为同一个业务故障。
|
||||
|
||||
- 本机网页进程:限价排序函数在价格、时间相同的商品之间,反复调用带 `zh-CN` 和 `numeric: true` 参数的 `String.localeCompare`。每次比较都重新准备排序规则,整页取数时需要给五万多条候选商品排序,阻塞网页线程约 3 秒。
|
||||
- 公网检查:分阶段记录 DNS、TCP、TLS 和首字节耗时,慢请求中 DNS 本身耗时约 5.01 秒;同次请求后续连接和网页响应只占约 0.09 秒。保持域名、TLS 验证与目标服务不变,仅诊断时用 `curl --resolve` 指定当前地址后,30 次请求均返回 200,最长约 0.234 秒。该 DNS 等待发生在检查机器上,不能据此认为每位用户的浏览器都有相同延迟。
|
||||
|
||||
当前主机的两个上游 DNS 直接查询都观察到少量超时,本地 DNS stub 的 AAAA 查询也有超时。证据不足以仅凭这些现象断定双宽带负载均衡是唯一原因。本次不改共享主机的 DNS、路由器、网卡、FRP 或其他项目配置。
|
||||
|
||||
## 排序对比与修复范围
|
||||
|
||||
在生产私有 Node.js 22.23.4 的独立进程中,只读取得 54,381 条销售商品候选和 13,537 条货号限价,使用同一批内存数据对比:
|
||||
|
||||
| 指标 | 原比较方式 | 复用 Intl.Collator |
|
||||
| --- | ---: | ---: |
|
||||
| 排序耗时 | 3,318 ms | 212 ms |
|
||||
| 完整有序商品 ID SHA-256 | `8d83f84e3252953e0729d320da3a43ede88fca7cffce6e2f906c9f857a62ff10` | 相同 |
|
||||
|
||||
这是隔离计算耗时,不是整页网络时间。取数约 700 ms,不计入上表。
|
||||
|
||||
打开真实限价页面的同时采样 100 次本机健康请求,全部返回 200,但最长等待 3,076 ms,其中 4 次超过 1 秒。这与隔离复现的同步排序耗时吻合;无用户操作时的 30 次本机检查最长约 9 ms。
|
||||
|
||||
产品档案 Repository 内复用同一个 `Intl.Collator('zh-CN', { numeric: true })`,用于限价列表及筛选候选值的中文数字比较。价格优先、时间优先、条码数字顺序、相同比较值的稳定顺序、空值位置、筛选范围及分页契约保持原样。不增加跨请求业务数据缓存,不修改 SQL、数据库结构、业务数据或页面视觉。
|
||||
|
||||
回归验证覆盖中文与数字/前导零/大小写/全半角的顺序一致性、大批次不重复初始化排序规则、筛选候选值去重与分页。发布后还需在真实页面翻页/刷新并并发检查本机健康响应,核对其他项目进程和凭据指纹。
|
||||
|
||||
## 继续定位查询参数准备
|
||||
|
||||
第一轮上线后,连续翻页和打开筛选仍出现过约 2.1 秒的等待。仅对本项目网页进程短时启用函数采样,发现主线程热点包含 Prisma 的查询模板参数展开函数 `td / Hn / ca / od`,并伴随对象分配和垃圾回收。采样完成后已恢复原 Node.js 启动参数。
|
||||
|
||||
隔离只读查询使用 54,395 条销售商品中的 54,291 个货号,对比 ORM 的逐项 `IN` 参数与 PostgreSQL 的单个 `text[]` 参数:原查询约 958 ms,数组参数约 202 ms;两者均返回 9,552 条,完整货号与限价结果摘要均为 `801e325b6d702880dc7d386f0903b1be073655407bfc43b08a7cdbbf7713df1e`。CPU 时间约为 1,657 ms / 285 ms,包含独立测试进程的各线程。
|
||||
|
||||
限价列表及负责人限价候选值改用参数化 `= ANY($1::text[])`,仍只查询已有筛选候选对应的货号;保留空集合短路、未定价商品、非空价格条件与原始小数精度。没有拼接货号进入 SQL,不扩大读取范围,不修改数据库结构或记录,不新增缓存。新增超过 PostgreSQL 标量参数上限的批次、数据范围、SQL 参数绑定及空价格候选值回归测试。
|
||||
39
furniture-3d-system/docs/排障记录/2026-11-02-SSD写入来源与库存快照差异发布.md
Normal file
@ -0,0 +1,39 @@
|
||||
# SSD 写入来源与库存快照差异发布
|
||||
|
||||
## 现场证据
|
||||
|
||||
2026-11-02 对当前生产机进行只读检查,未调整共享服务、数据库参数、账号或凭据。
|
||||
|
||||
- 最近 8 个完整日整盘写入约 163–270 GB/日;累计通电时长推算的 459 GB/日并不代表当前长期速率。
|
||||
- 本次开机约 14.34 天,cgroup 的物理盘写入计数:MariaDB 约 1132.7 GB,PostgreSQL 约 1045.7 GB,Cronicle 约 163.9 GB,journald 约 32.3 GB,全部 PM2 约 17.5 GB。cgroup 有父子包含关系,不能再与子进程计数相加。
|
||||
- MariaDB 的 `planning_database` 被多个项目共用;`shujuku3_system` 也存在活动连接。未启用 Performance Schema,不能把共享数据库的全部写入归给单个项目,未修改其配置。
|
||||
- PostgreSQL 本项目库累计临时文件写入约 1.04 TB,库存快照约 1.116 亿次插入和删除;这些是不同统计口径,不能直接相加当作 SSD 实际写入。
|
||||
- 库存快照每 15 分钟全量取数。最近 24 小时成功 95 次,读取约 796 万行,每轮约 8.4 万行。旧仓储无条件删除整个快照再插入,重复写入库存未变化的记录及索引。
|
||||
- 120 秒空闲窗口整盘写入约 29 MB,网页进程直接写入约 12 KB;PM2 日志增长很小。后台批处理与持久化是重点,不应以空闲短采样替代完整日统计。
|
||||
|
||||
## 修改边界与契约
|
||||
|
||||
只修改商品库存快照仓储及其同步时间读取;不更改上游查询、库存公式、云仓扣减、每 15 分钟同步频率、表结构、其他项目或共享数据库参数。
|
||||
|
||||
全量来源读取、唯一键暂存、数量校验、独占 advisory lock 保持原样。在同一个发布事务内:
|
||||
|
||||
1. 对 `(barcode, wmsCoId)` 和全部持久化业务字段进行比较;相同行在 INSERT 前跳过,避免仅因冲突检查而锁定、记录 WAL。
|
||||
2. 插入新增行,只更新业务字段变化的行,保留已有行 ID。
|
||||
3. 只删除完整新快照中确实消失的键;空来源、重复键、暂存数量错误均拒绝发布。
|
||||
4. 验证发布后的总行数,再将已有 `ProductInventorySnapshotRun` 标为成功;任一步失败都整体回滚。
|
||||
|
||||
`rowsWritten` 保留“本轮发布总行数”的旧返回契约;运行日志额外输出 `rowsChanged / rowsDeleted / rowsUnchanged`,用于识别实际明细写入。
|
||||
|
||||
明细 `ProductInventorySnapshot.syncedAt` 现在表示该行最近一次写入时间。商品库存页和前置仓页面的整批同步时间统一读最新成功的 `ProductInventorySnapshotRun.syncedAt`,与明细在同一条 SQL、同一个 MVCC 快照内读取。无任何行变化时,页面仍能显示新的成功同步时间。当前未发现依赖此表的数据库视图;技术只读账号若直接读原表,判断整批新鲜度时也应读运行记录。
|
||||
|
||||
## 验证与限制
|
||||
|
||||
- 6 组相关单元测试共 25 项通过,覆盖差异发布、完整性拒绝、发布新鲜度和现有库存页面/前置仓规则。
|
||||
- 独立的本地 PostgreSQL 16 容器验证了未变化行及 ID 不变、整批同步时间推进、新增/修改/删除、相同条码不同仓库、云仓数量,以及在发布提交前注入失败后的数据和时间整体回滚。
|
||||
- 1 万行不变数据:新实现 WAL 约 41,920 字节,旧全删全插约 6,058,184 字节,下降 99.31%。这是隔离环境的数据库日志对比,不能当作整盘或实际生产每日总写入降幅。
|
||||
- 生产首次验证 83,809 行均未变化,全部保留原 ID 和行时间戳,明细写入 0 行,整批成功时间照常推进。前置仓实测发现 PrismaPg 对非 UTC 会话的 `timestamptz` 原生查询结果多偏移 8 小时,已保持原来的 `timestamp` 返回类型;直接 pg 读取则保留 `AT TIME ZONE 'UTC'`。两条读取链路分别验证,不能以 pg 驱动的测试代替 PrismaPg。
|
||||
- 使用实际 PrismaPg 适配器、Asia/Shanghai 数据库会话,在隔离 PostgreSQL 中调用前置仓仓储,验证发布时间与 UTC 期望完全一致、公有可用数不变。
|
||||
- 共享开发基线存在权限类型和 MySQL 参数类型错误,未扩大范围修复;生产发布应从最新 `main` 窄范围回流,并单独完成类型检查与构建。
|
||||
- 尚未归因的共享 MariaDB 和 Cronicle 写入交由对应项目继续核查,不停用其任务、备份或日志。
|
||||
|
||||
规范自审:`passed with warnings`。这是自审,不是独立复审。剩余警告是共享写入尚不能精确分配到各调用项目,及真实长期降幅需要发布后跨完整任务周期观察。
|
||||
@ -92,6 +92,8 @@ node_modules/.bin/tsx scripts/sync-supply-chain-discontinuation-links-from-dm.ts
|
||||
新的服务器方案:
|
||||
|
||||
- Linux 安装器同时安装 `furniture-jst-rpa-browser.service`。
|
||||
- 每日任务在 Linux 上会先重启专用 RPA 浏览器服务,避免长期运行后聚水潭 iframe
|
||||
卡死但仍能通过 CDP 健康检查;服务名可用 `JST_RPA_BROWSER_SERVICE` 覆盖。
|
||||
- Chrome 使用持久化 profile,并通过本机 CDP 端口供 RPA 使用。
|
||||
- 每日入口从服务器 Chrome 导出完整聚水潭数据。
|
||||
- 正常整单导出最多尝试 3 次,任一次成功立即结束;只有全部失败后才按每批
|
||||
|
||||
@ -92,6 +92,8 @@
|
||||
- 组合中任一子商品为停产/退市,且该子商品的简尚库存 + 工厂库存 = 0,则组合商品进入通知下架。
|
||||
- 支持组合嵌套:若子商品本身也是组合,继续向下递归检查,并对循环引用做保护。
|
||||
- 组合商品自身库存仍按组合编码从简尚库存和工厂库存表读取,用于页面展示。
|
||||
- 组合因停产/退市且零库存子商品下架时,若该子商品存在有效替换款,组合处理说明同步为
|
||||
“全渠道替换”或“仅京喜替换”,可替换商品编码固定标记为“运营自行组合替换”。
|
||||
|
||||
### 3.5 可替换商品
|
||||
|
||||
@ -104,7 +106,11 @@
|
||||
- 兼容反向录入:若当前停产条码或其货号等于 `replaceSku`,则反向使用对应 `sku` 作为替换来源。
|
||||
- 同一原编码存在多条规则时,取 `createTime` 最新的一条。
|
||||
- 替换目标能在商品档案映射时,必须展开为具体 barcode;同货号有多个条码时优先销售、有库存、最近更新的条码。
|
||||
- 替换目标完全无法映射时,仍展示规则中的替换编码,但是否换图、区别点和价格差允许为空。
|
||||
- 已映射的替换条码若实时状态为停产/退市,且简尚、工厂、渠道专用仓和京喜库存均为 0,则不作为有效替换款;存在其他可用候选时继续选择下一候选。
|
||||
- 所有已映射候选都不可用且没有未映射替换编码时,可替换商品编码留空,处理说明改为“全渠道下架”或“仅京喜下架”。
|
||||
- 替换目标完全无法映射时,先按实时状态和库存过滤停产/退市且零库存编码;仍可用的才展示
|
||||
规则中的替换编码,是否换图、区别点和价格差允许为空。若无法映射的值是商品档案中的已知
|
||||
货号/SKU,则不得直接写入“可替换商品编码”列;必须展开为具体条码,展开失败时留空。
|
||||
- 是否换图取 `isSwapping`,1 为是。
|
||||
- 区别点取 `differentPoints`。
|
||||
- 价格差沿用现有页面口径:替换商品成本 - 原商品成本。
|
||||
@ -117,6 +123,9 @@
|
||||
- 默认先执行单次全量导出,连续尝试 3 次;第 1、2 次失败后重置页面再重试,
|
||||
任一次成功立即结束;只有三次都失败后才切换为每批 10,000 个编码的分批导出。
|
||||
- 只保留 `online_status='是'` 且 `stock_sync_status='开启同步'` 的导出记录。
|
||||
- 编码维度命中专用仓时,链接维度按专用仓所属部门排除对应链接;例如
|
||||
`简尚-分销部专用仓` 只排除利润中心为“分销部”的链接,其他部门继续通知。
|
||||
同一备注包含多个专用仓时逐个部门排除。
|
||||
- 全部批次成功后,在数据库事务中替换当天链接;失败时保留已有完整链接。
|
||||
- 最终有效写入为 0 时默认回滚;确有业务依据的正常零链接必须显式使用
|
||||
`--allow-zero-links`,避免异常空文件清空当天快照。
|
||||
@ -183,6 +192,12 @@ sudo bash scripts/install-supply-chain-discontinuation-linux-timer.sh
|
||||
任务顺序为“虚拟云仓库存快照 → 编码维度通知快照与库存预警快照 →
|
||||
聚水潭完整链接导出 → 未下架监控与乐捐快照”。
|
||||
|
||||
页面提供“重新生成”按钮,仅经管业务角色可使用,同时仍要求
|
||||
“停产下架管理”的 `edit` 操作权限。点击后由
|
||||
`POST /api/supply-chain/discontinuation/regenerate` 启动后台任务,按上述顺序
|
||||
重新生成四类数据;页面通过 `GET` 同一地址轮询阶段,成功后自动刷新当前页签,
|
||||
任务运行期间禁止重复触发。
|
||||
|
||||
每日任务启动时会先按当前 `prisma/schema.prisma` 重新生成 Prisma Client,
|
||||
并输出各数据源阶段日志,避免加载到旧客户端或无法判断卡在某一张外部表。
|
||||
系统不再依赖个人 Mac;服务器使用独立的 `furniture-jst-rpa-browser.service`
|
||||
@ -224,13 +239,39 @@ pnpm exec tsx scripts/generate-supply-chain-inventory-alert-snapshot.ts
|
||||
- 每日任务固定处理三天前的通知队列:23 日统计 20 日通知、22 日起流入的订单;24 日统计 21 日通知、23 日起流入的订单。
|
||||
- 同一订单命中同一平台店铺商品编码只计一笔;同一订单命中两个链接计两笔。
|
||||
- 乐捐唯一键为“内部订单号 + 平台店铺商品编码”,重复来源数据不会重复计费。
|
||||
- 运营可对自己的待申诉明细上传凭证并发起申诉;有业务管理操作权限的人员可审核通过、驳回或打回。
|
||||
- 只有运营业务身份且拥有操作权限的人员可对自己负责的待申诉明细上传凭证并发起申诉;
|
||||
只有经管业务身份且拥有操作权限的人员可审核通过、驳回或打回。
|
||||
- 申诉原因固定为“商品编码有误”“活动中,无法下架”“其他”三项,接口和页面使用同一份枚举。
|
||||
- 申诉和审核均支持按链接批量处理:同一平台店铺商品编码下符合条件的订单一次覆盖。
|
||||
- 审核通过后原乐捐记录保留,但页面展示与月度汇总按 0 元计算,实现自动减免。
|
||||
- 负责人离职或无法匹配到员工目录在职记录时,状态显示“人员已离职”,乐捐金额按 0 元计算且不能申诉。
|
||||
- “人员已离职”可作为申诉状态筛选;筛选后可批量向部门负责人的钉钉个人工作通知推送处理提醒。
|
||||
- 未下架监控页顶部显示本月有效乐捐合计(¥10/单),审核通过的明细不计入合计。
|
||||
- 未下架监控支持个人列显示/拖拽排序、紧凑展示和筛选字段配置。
|
||||
- 未下架监控列表每次加载 100 条轻量元数据,不返回凭证图片;打开单条申诉后再通过
|
||||
`/api/supply-chain/discontinuation/penalties/:id` 按需加载该条凭证,避免列表载荷过大。
|
||||
- “活动中,无法下架”申诉提交时自动识别凭证中的 SPU ID 和 SPU 报名时间:时间早于通知
|
||||
下架时间且 SPU ID 与订单商品ID一致则自动通过;缺少有效报名时间自动打回
|
||||
“凭证时间不符合要求”,SPU ID 缺失或不一致自动打回“凭证与订单信息不一致”。
|
||||
按链接批量申诉时,系统按每条订单自己的通知时间和商品ID分别判定、逐条更新,并返回
|
||||
通过与打回数量;识图服务超时或异常时保留原申诉状态并提示重试,不执行自动打回。
|
||||
- 用户权限管理使用独立“停产下架管理”节点,可按账号或岗位配置查看、
|
||||
导出和操作权限;个人配置优先覆盖。
|
||||
- 供应商账号不开放停产下架管理页面、编码维度查看和导出权限。
|
||||
- 当前数据口径为所有在职内部账号查看编码维度、链接维度、库存预警和未下架监控的
|
||||
全部数据;供应商账号和外部录入账号不开放。
|
||||
- 系统保留用户数据范围配置能力:跟随人员信息、本组全部、部门全部、利润中心全部、
|
||||
全部数据。当前所有在职内部账号统一配置为“全部数据”,数据范围与操作权限解耦。
|
||||
- 具体用户还可额外选择一组店铺,使用 `platform + shopId` 唯一识别;附加店铺范围与人员
|
||||
默认范围取并集,适用于同时负责运营中心和分销等跨组织店铺的人员。
|
||||
- 各页签只展示当前维度实际支持的筛选字段;商品编码、店铺、商品ID、利润中心、线上商品编码、
|
||||
平台店铺商品编码、内部/交易单号、平台和运营负责人统一使用可搜索下拉,选项来自
|
||||
`/api/supply-chain/discontinuation/filter-options` 并按统计日期收敛。
|
||||
- 所有字段筛选支持多选批量查询,也可粘贴换行、逗号、分号或顿号分隔的多个值后回车添加;
|
||||
列表、分页统计和“导出当前筛选”使用同一组重复查询参数和多值条件。
|
||||
- “操作”权限控制申诉、审核、取消驳回和离职负责人批量推送;申诉校验运营业务身份及本人
|
||||
负责人匹配,审核类操作校验经管业务身份。旧 `discontinuationPenalties` 节点继续兼容审核操作,
|
||||
已有授权不会失效。
|
||||
|
||||
## 4. 业务口径
|
||||
|
||||
|
||||
@ -39,6 +39,11 @@
|
||||
- 清货池「日销」= 产品全量档案 `sales3d × 0.5 + sales7d × 0.3 + sales15d × 0.2`,结果保留两位小数,不取采购日销 `dailySales`;旧数据仅存在 7 日销时回退到 `sales7d`。
|
||||
- 清货池「最近入库时间」在简尚总库存为 0 时显示「简尚无库存」;工厂仍有库存时总库存和工厂库存照常保留,入库天数仍按真实采购入库日期计算。
|
||||
- 清货池生成排除二级分类为空和「其他 - 福利品」的商品,并单独排除编码 `GBGJ-MPWL01N126V01`。
|
||||
- 优先级规则和固定任务规则的条件值从当前清货池读取候选并支持多选;数字字段可同时选择或直接输入任意数值,仅「包含 / 不包含」使用自由文本输入,「为空 / 不为空」无需填写值。
|
||||
- 清货池优先级支持经管人工调整和勾选批量调整;人工值写入清货池快照并标记人工来源,后端仅接受 `clearanceReview.edit` 权限。
|
||||
- 清货批次按月沿用上一月快照:销售转停产/退市进入上一月最大批次加一的新批次,停产/退市转销售恢复“库存积压”;销售且可用天数小于 30 天标记“正常销售”并关闭任务、折扣通道。人工优先级会随快照向后沿用,优先于系统规则。
|
||||
- 人工清货任务按“统计月 + 条码 + 利润中心”区分,任务清货量作为期初任务量;人工清货价独立保存,批次和优先级从对应月清货池映射。
|
||||
- 禁止申请折扣配置使用与优先级规则相同的条件编辑器,可选择任意清货池抬头字段、组合且/或条件并维护多条限制规则;旧商品编码/SPU 配置自动转换为等值条件。
|
||||
- 清货池「停产时间」按商品编码关联 `rpt.plm_productDiscontinuationDetail_flow` 与 `rpt.plm_productDiscontinuation_flow`:仅取已完成的停产流程(`flowtype=14`、`flowStatus=1`)、有效停产明细(`prostatus=1`、`state=0`),以最新流程的 `applyTime` 填入年月日;销售状态为「销售」的商品无论是否存在历史停产流程都清空停产时间。
|
||||
- 清货池「利润中心」按产品档案的运营负责人 `opsHead`,在人员信息表 `SupplyChainManagementOwnerConfig.operationOwner` 中精确匹配后取 `profitCenter`;未匹配时留空。
|
||||
- 清货组合匹配只处理产品全量档案的单双包字段原本为「双包」或「三包」且有库存的商品;「非双包」和空值即使出现在组合明细中,也不改写包型,清货商品编码直接填当前商品编码。
|
||||
@ -192,6 +197,8 @@
|
||||
- 清货任务的 `clearedQty` 已接入本地销售出库汇总;页面实时展示,持久化由 `sync:clearance-progress` 完成,生产定时任务仍需按部署环境配置。
|
||||
- 所有旧导入、重置和库存同步脚本仍会拒绝直接修改已生成月份;更新必须通过经管「清货配置 > 时间窗口」中的生成选项执行。
|
||||
- 清货分数先使用示例权重:完成度 60 + 毛利率 25 + 货值回收 15;历史周期使用复盘快照计算,当月任务只有完成度可计算。
|
||||
- 清货分数档位按完成率上下限区间匹配,完成率允许高于 100%;上下限均可配置为包含或不包含,上限留空表示无上限,区间不可重叠,旧“最低完成率”配置会自动转换为连续区间。
|
||||
- 档位对应分数允许超过 100(最高 99999),用于配置超额完成奖励;完成率和三类任务占比仍限制在 0–100%。
|
||||
- 清货复盘优先使用 `2026年4月清货复盘.xlsx` 导入的历史快照;没有快照的月份由最新一期已确认任务 + 本地销售出库进度自动生成。
|
||||
- 当月自动复盘暂不计算毛利率、结算毛利率和货值回收率,字段保持为空,待结算口径接入后再补齐。
|
||||
- 清货池导出单次最多 5,000 条,按当前筛选范围导出;超出时在「导出说明」中提示实际总数。
|
||||
|
||||
@ -114,6 +114,19 @@
|
||||
- 期初数据更新:把新的 `限价期初数据*.xlsx` 放进 `shared/limit-workflow-sync/期初数据/` 即可,下次同步自动取最新。
|
||||
- `SupplyChainBreachRuleConfig` 不需要灌数:接口读取时用代码内置默认值,首次在「规则配置」保存时落库。
|
||||
|
||||
### 每日聚水潭物流规则快照
|
||||
|
||||
- 正式站采用服务器端 systemd:`npm run install:jst-logistics-systemd`,默认每天 `09:30`(Asia/Shanghai)执行,避开 09:00 的数据库任务,并与 10:30 停产 RPA 任务错开。可通过 `JST_LOGISTICS_CALENDAR` 覆盖。
|
||||
- 开发机示例使用 macOS launchd:`npm run install:jst-logistics-daily`,默认每天 `09:00`。
|
||||
- 正式站快照目录固定为 `$BASE/shared/freight-config`,应用和定时任务共同读取 `FREIGHT_CONFIG_DATA_DIR`,发布切换 release 不会重置。
|
||||
- 本地未配置 `FREIGHT_CONFIG_DATA_DIR` 时继续使用 `data/freight-config`。
|
||||
- 每天最多成功更新一次;若快照的 `generatedAt` 已是北京时间当天,重跑会自动跳过。
|
||||
- 当天确需人工覆盖时执行:在聚水潭 RPA 浏览器保持登录后运行 `npm run sync:jst-logistics-rules -- --force`。
|
||||
- 同步失败时保留上一份可用快照,不写入半成品。
|
||||
- 物流规则同步、停产下架同步和其他聚水潭 RPA 任务不得并发;正式站 unit 已与停产和条码同步任务建立冲突保护。
|
||||
- 商品标签按 `仓库-xxx`、`简尚-xxx`、`三方-xxx` 三种前缀解析发货仓,三者使用同一套运费计算。
|
||||
- 条码命中 `fys_barcodeFreightRate_view` 的有效记录时,优先使用表中绑定的快递和固定单价;固定报价不依赖重量、体积、省份发货占比或仓库通用快递名称过滤,也不参与新快递替换。
|
||||
|
||||
## 五、待办
|
||||
|
||||
- [ ] 限价流程数据接入 + 筛选逻辑(生效时间版本、货号维度)
|
||||
|
||||
254
furniture-3d-system/package-lock.json
generated
@ -58,6 +58,7 @@
|
||||
"ssh2": "^1.17.0",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"three": "^0.182.0",
|
||||
"tsx": "^4.23.15",
|
||||
"uuid": "^11.1.1",
|
||||
"xlsx": "^0.18.5",
|
||||
"zod": "^4.3.6",
|
||||
@ -90,7 +91,6 @@
|
||||
"prisma": "^7.3.0",
|
||||
"supertest": "^7.2.2",
|
||||
"tailwindcss": "^4.1.18",
|
||||
"tsx": "^4.21.0",
|
||||
"typescript": "^5"
|
||||
}
|
||||
},
|
||||
@ -2270,13 +2270,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.27.1",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.27.1.tgz",
|
||||
"integrity": "sha512-HHB50pdsBX6k47S4u5g/CaLjqS3qwaOVE5ILsq64jyzgMhLuCuZ8rGzM9yhsAjfjkbgUPMzZEPa7DAp7yz6vuA==",
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz",
|
||||
"integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@ -2287,13 +2286,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.27.1",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.27.1.tgz",
|
||||
"integrity": "sha512-kFqa6/UcaTbGm/NncN9kzVOODjhZW8e+FRdSeypWe6j33gzclHtwlANs26JrupOntlcWmB0u8+8HZo8s7thHvg==",
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz",
|
||||
"integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@ -2304,13 +2302,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.27.1",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.27.1.tgz",
|
||||
"integrity": "sha512-45fuKmAJpxnQWixOGCrS+ro4Uvb4Re9+UTieUY2f8AEc+t7d4AaZ6eUJ3Hva7dtrxAAWHtlEFsXFMAgNnGU9uQ==",
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz",
|
||||
"integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@ -2321,13 +2318,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.27.1",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.27.1.tgz",
|
||||
"integrity": "sha512-LBEpOz0BsgMEeHgenf5aqmn/lLNTFXVfoWMUox8CtWWYK9X4jmQzWjoGoNb8lmAYml/tQ/Ysvm8q7szu7BoxRQ==",
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz",
|
||||
"integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@ -2338,13 +2334,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.27.1",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.27.1.tgz",
|
||||
"integrity": "sha512-veg7fL8eMSCVKL7IW4pxb54QERtedFDfY/ASrumK/SbFsXnRazxY4YykN/THYqFnFwJ0aVjiUrVG2PwcdAEqQQ==",
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz",
|
||||
"integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@ -2355,13 +2350,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.27.1",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.27.1.tgz",
|
||||
"integrity": "sha512-+3ELd+nTzhfWb07Vol7EZ+5PTbJ/u74nC6iv4/lwIU99Ip5uuY6QoIf0Hn4m2HoV0qcnRivN3KSqc+FyCHjoVQ==",
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz",
|
||||
"integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@ -2372,13 +2366,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.27.1",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.1.tgz",
|
||||
"integrity": "sha512-/8Rfgns4XD9XOSXlzUDepG8PX+AVWHliYlUkFI3K3GB6tqbdjYqdhcb4BKRd7C0BhZSoaCxhv8kTcBrcZWP+xg==",
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz",
|
||||
"integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@ -2389,13 +2382,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.27.1",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.27.1.tgz",
|
||||
"integrity": "sha512-GITpD8dK9C+r+5yRT/UKVT36h/DQLOHdwGVwwoHidlnA168oD3uxA878XloXebK4Ul3gDBBIvEdL7go9gCUFzQ==",
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz",
|
||||
"integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@ -2406,13 +2398,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.27.1",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.27.1.tgz",
|
||||
"integrity": "sha512-ieMID0JRZY/ZeCrsFQ3Y3NlHNCqIhTprJfDgSB3/lv5jJZ8FX3hqPyXWhe+gvS5ARMBJ242PM+VNz/ctNj//eA==",
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz",
|
||||
"integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@ -2423,13 +2414,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.27.1",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.27.1.tgz",
|
||||
"integrity": "sha512-W9//kCrh/6in9rWIBdKaMtuTTzNj6jSeG/haWBADqLLa9P8O5YSRDzgD5y9QBok4AYlzS6ARHifAb75V6G670Q==",
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz",
|
||||
"integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@ -2440,13 +2430,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.27.1",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.27.1.tgz",
|
||||
"integrity": "sha512-VIUV4z8GD8rtSVMfAj1aXFahsi/+tcoXXNYmXgzISL+KB381vbSTNdeZHHHIYqFyXcoEhu9n5cT+05tRv13rlw==",
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz",
|
||||
"integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@ -2457,13 +2446,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.27.1",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.27.1.tgz",
|
||||
"integrity": "sha512-l4rfiiJRN7sTNI//ff65zJ9z8U+k6zcCg0LALU5iEWzY+a1mVZ8iWC1k5EsNKThZ7XCQ6YWtsZ8EWYm7r1UEsg==",
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz",
|
||||
"integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@ -2474,13 +2462,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.27.1",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.27.1.tgz",
|
||||
"integrity": "sha512-U0bEuAOLvO/DWFdygTHWY8C067FXz+UbzKgxYhXC0fDieFa0kDIra1FAhsAARRJbvEyso8aAqvPdNxzWuStBnA==",
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz",
|
||||
"integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@ -2491,13 +2478,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.27.1",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.27.1.tgz",
|
||||
"integrity": "sha512-NzdQ/Xwu6vPSf/GkdmRNsOfIeSGnh7muundsWItmBsVpMoNPVpM61qNzAVY3pZ1glzzAxLR40UyYM23eaDDbYQ==",
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz",
|
||||
"integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@ -2508,13 +2494,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.27.1",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.27.1.tgz",
|
||||
"integrity": "sha512-7zlw8p3IApcsN7mFw0O1Z1PyEk6PlKMu18roImfl3iQHTnr/yAfYv6s4hXPidbDoI2Q0pW+5xeoM4eTCC0UdrQ==",
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz",
|
||||
"integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@ -2525,13 +2510,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.27.1",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.27.1.tgz",
|
||||
"integrity": "sha512-cGj5wli+G+nkVQdZo3+7FDKC25Uh4ZVwOAK6A06Hsvgr8WqBBuOy/1s+PUEd/6Je+vjfm6stX0kmib5b/O2Ykw==",
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz",
|
||||
"integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@ -2542,13 +2526,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.27.1",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.27.1.tgz",
|
||||
"integrity": "sha512-z3H/HYI9MM0HTv3hQZ81f+AKb+yEoCRlUby1F80vbQ5XdzEMyY/9iNlAmhqiBKw4MJXwfgsh7ERGEOhrM1niMA==",
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz",
|
||||
"integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@ -2559,13 +2542,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-arm64": {
|
||||
"version": "0.27.1",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.1.tgz",
|
||||
"integrity": "sha512-wzC24DxAvk8Em01YmVXyjl96Mr+ecTPyOuADAvjGg+fyBpGmxmcr2E5ttf7Im8D0sXZihpxzO1isus8MdjMCXQ==",
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz",
|
||||
"integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@ -2576,13 +2558,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.27.1",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.27.1.tgz",
|
||||
"integrity": "sha512-1YQ8ybGi2yIXswu6eNzJsrYIGFpnlzEWRl6iR5gMgmsrR0FcNoV1m9k9sc3PuP5rUBLshOZylc9nqSgymI+TYg==",
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz",
|
||||
"integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@ -2593,13 +2574,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-arm64": {
|
||||
"version": "0.27.1",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.1.tgz",
|
||||
"integrity": "sha512-5Z+DzLCrq5wmU7RDaMDe2DVXMRm2tTDvX2KU14JJVBN2CT/qov7XVix85QoJqHltpvAOZUAc3ndU56HSMWrv8g==",
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz",
|
||||
"integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@ -2610,13 +2590,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.27.1",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.27.1.tgz",
|
||||
"integrity": "sha512-Q73ENzIdPF5jap4wqLtsfh8YbYSZ8Q0wnxplOlZUOyZy7B4ZKW8DXGWgTCZmF8VWD7Tciwv5F4NsRf6vYlZtqg==",
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz",
|
||||
"integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@ -2627,13 +2606,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openharmony-arm64": {
|
||||
"version": "0.27.1",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.1.tgz",
|
||||
"integrity": "sha512-ajbHrGM/XiK+sXM0JzEbJAen+0E+JMQZ2l4RR4VFwvV9JEERx+oxtgkpoKv1SevhjavK2z2ReHk32pjzktWbGg==",
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz",
|
||||
"integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@ -2644,13 +2622,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.27.1",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.27.1.tgz",
|
||||
"integrity": "sha512-IPUW+y4VIjuDVn+OMzHc5FV4GubIwPnsz6ubkvN8cuhEqH81NovB53IUlrlBkPMEPxvNnf79MGBoz8rZ2iW8HA==",
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz",
|
||||
"integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@ -2661,13 +2638,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.27.1",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.27.1.tgz",
|
||||
"integrity": "sha512-RIVRWiljWA6CdVu8zkWcRmGP7iRRIIwvhDKem8UMBjPql2TXM5PkDVvvrzMtj1V+WFPB4K7zkIGM7VzRtFkjdg==",
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz",
|
||||
"integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@ -2678,13 +2654,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.27.1",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.27.1.tgz",
|
||||
"integrity": "sha512-2BR5M8CPbptC1AK5JbJT1fWrHLvejwZidKx3UMSF0ecHMa+smhi16drIrCEggkgviBwLYd5nwrFLSl5Kho96RQ==",
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz",
|
||||
"integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@ -2695,13 +2670,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.27.1",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.27.1.tgz",
|
||||
"integrity": "sha512-d5X6RMYv6taIymSk8JBP+nxv8DQAMY6A51GPgusqLdK9wBz5wWIXy1KjTck6HnjE9hqJzJRdk+1p/t5soSbCtw==",
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz",
|
||||
"integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@ -9835,10 +9809,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.27.1",
|
||||
"resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.27.1.tgz",
|
||||
"integrity": "sha512-yY35KZckJJuVVPXpvjgxiCuVEJT67F6zDeVTv4rizyPrfGBUpZQsvmxnN+C371c2esD/hNMjj4tpBhuueLN7aA==",
|
||||
"dev": true,
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz",
|
||||
"integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
@ -9848,32 +9821,32 @@
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.27.1",
|
||||
"@esbuild/android-arm": "0.27.1",
|
||||
"@esbuild/android-arm64": "0.27.1",
|
||||
"@esbuild/android-x64": "0.27.1",
|
||||
"@esbuild/darwin-arm64": "0.27.1",
|
||||
"@esbuild/darwin-x64": "0.27.1",
|
||||
"@esbuild/freebsd-arm64": "0.27.1",
|
||||
"@esbuild/freebsd-x64": "0.27.1",
|
||||
"@esbuild/linux-arm": "0.27.1",
|
||||
"@esbuild/linux-arm64": "0.27.1",
|
||||
"@esbuild/linux-ia32": "0.27.1",
|
||||
"@esbuild/linux-loong64": "0.27.1",
|
||||
"@esbuild/linux-mips64el": "0.27.1",
|
||||
"@esbuild/linux-ppc64": "0.27.1",
|
||||
"@esbuild/linux-riscv64": "0.27.1",
|
||||
"@esbuild/linux-s390x": "0.27.1",
|
||||
"@esbuild/linux-x64": "0.27.1",
|
||||
"@esbuild/netbsd-arm64": "0.27.1",
|
||||
"@esbuild/netbsd-x64": "0.27.1",
|
||||
"@esbuild/openbsd-arm64": "0.27.1",
|
||||
"@esbuild/openbsd-x64": "0.27.1",
|
||||
"@esbuild/openharmony-arm64": "0.27.1",
|
||||
"@esbuild/sunos-x64": "0.27.1",
|
||||
"@esbuild/win32-arm64": "0.27.1",
|
||||
"@esbuild/win32-ia32": "0.27.1",
|
||||
"@esbuild/win32-x64": "0.27.1"
|
||||
"@esbuild/aix-ppc64": "0.28.2",
|
||||
"@esbuild/android-arm": "0.28.2",
|
||||
"@esbuild/android-arm64": "0.28.2",
|
||||
"@esbuild/android-x64": "0.28.2",
|
||||
"@esbuild/darwin-arm64": "0.28.2",
|
||||
"@esbuild/darwin-x64": "0.28.2",
|
||||
"@esbuild/freebsd-arm64": "0.28.2",
|
||||
"@esbuild/freebsd-x64": "0.28.2",
|
||||
"@esbuild/linux-arm": "0.28.2",
|
||||
"@esbuild/linux-arm64": "0.28.2",
|
||||
"@esbuild/linux-ia32": "0.28.2",
|
||||
"@esbuild/linux-loong64": "0.28.2",
|
||||
"@esbuild/linux-mips64el": "0.28.2",
|
||||
"@esbuild/linux-ppc64": "0.28.2",
|
||||
"@esbuild/linux-riscv64": "0.28.2",
|
||||
"@esbuild/linux-s390x": "0.28.2",
|
||||
"@esbuild/linux-x64": "0.28.2",
|
||||
"@esbuild/netbsd-arm64": "0.28.2",
|
||||
"@esbuild/netbsd-x64": "0.28.2",
|
||||
"@esbuild/openbsd-arm64": "0.28.2",
|
||||
"@esbuild/openbsd-x64": "0.28.2",
|
||||
"@esbuild/openharmony-arm64": "0.28.2",
|
||||
"@esbuild/sunos-x64": "0.28.2",
|
||||
"@esbuild/win32-arm64": "0.28.2",
|
||||
"@esbuild/win32-ia32": "0.28.2",
|
||||
"@esbuild/win32-x64": "0.28.2"
|
||||
}
|
||||
},
|
||||
"node_modules/escalade": {
|
||||
@ -10950,7 +10923,6 @@
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz",
|
||||
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
@ -18370,14 +18342,12 @@
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/tsx": {
|
||||
"version": "4.21.0",
|
||||
"resolved": "https://registry.npmmirror.com/tsx/-/tsx-4.21.0.tgz",
|
||||
"integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==",
|
||||
"dev": true,
|
||||
"version": "4.23.15",
|
||||
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.15.tgz",
|
||||
"integrity": "sha512-Yiex1Ovn8z2xPpOWckIiysV1SSyRMY9BkLF++q0yKiDxCqRhosKfMg3janKkiLBwZ5c/YryloKwGZcrEmtwxKw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "~0.27.0",
|
||||
"get-tsconfig": "^4.7.5"
|
||||
"esbuild": "~0.28.0"
|
||||
},
|
||||
"bin": {
|
||||
"tsx": "dist/cli.mjs"
|
||||
|
||||
@ -35,6 +35,14 @@
|
||||
"compare:product-sabc-xlsx": "tsx scripts/compare-product-sabc-xlsx.ts",
|
||||
"seed:local-product-sabc-xlsx": "tsx scripts/seed-local-product-sabc-xlsx.ts",
|
||||
"sync:jst-sku-product-archive": "tsx scripts/sync-jst-sku-product-archive.ts",
|
||||
"snapshot:limit-price": "tsx scripts/generate-limit-price-snapshot.ts",
|
||||
"snapshot:limit-price:daily": "node scripts/run-limit-price-daily-snapshot.mjs",
|
||||
"install:limit-price-snapshot-systemd": "bash scripts/install-limit-price-snapshot-systemd-timer.sh",
|
||||
"import:province-shipment-share": "node scripts/import-province-shipment-share.mjs",
|
||||
"sync:jst-logistics-rules": "tsx scripts/sync-jst-logistics-rules.ts",
|
||||
"snapshot:jst-logistics:daily": "tsx scripts/sync-jst-logistics-rules.ts",
|
||||
"install:jst-logistics-daily": "bash scripts/install-jst-logistics-daily-sync.sh",
|
||||
"install:jst-logistics-systemd": "bash scripts/install-jst-logistics-systemd-timer.sh",
|
||||
"sync:bom-cost-reference": "tsx scripts/sync-bom-cost-reference.ts",
|
||||
"backtest:bom-cost-reference": "tsx scripts/backtest-bom-cost-reference.ts",
|
||||
"report:bom-cost-shadow-divergence": "tsx scripts/report-bom-cost-shadow-divergence.ts",
|
||||
@ -78,6 +86,7 @@
|
||||
"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",
|
||||
"check:modular-processing-generation": "tsx scripts/check-modular-processing-generation-health.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",
|
||||
@ -91,6 +100,8 @@
|
||||
"sync:oa-art-flow-raw:apply": "tsx scripts/sync-oa-art-flow-raw.ts --apply",
|
||||
"import:clearance-pool": "tsx scripts/import-clearance-pool-snapshot.ts",
|
||||
"import:clearance-pool-archive": "tsx scripts/import-clearance-pool-archive.ts",
|
||||
"backfill:clearance-task-audit": "tsx scripts/backfill-clearance-task-audit-snapshots.ts",
|
||||
"import:clearance-price-push-manual": "tsx scripts/import-clearance-price-push-manual.ts",
|
||||
"reset:clearance-pool:archive": "tsx scripts/reset-clearance-pool-to-archive.ts",
|
||||
"sync:clearance-pool:discontinued": "tsx scripts/sync-clearance-pool-snapshot.ts",
|
||||
"sync:clearance-stock:stock-monitor": "tsx scripts/sync-clearance-stock-from-stock-monitor.ts",
|
||||
@ -187,6 +198,7 @@
|
||||
"ssh2": "^1.17.0",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"three": "^0.182.0",
|
||||
"tsx": "^4.23.15",
|
||||
"uuid": "^11.1.1",
|
||||
"xlsx": "^0.18.5",
|
||||
"zod": "^4.3.6",
|
||||
@ -242,7 +254,6 @@
|
||||
"prisma": "^7.3.0",
|
||||
"supertest": "^7.2.2",
|
||||
"tailwindcss": "^4.1.18",
|
||||
"tsx": "^4.21.0",
|
||||
"typescript": "^5"
|
||||
},
|
||||
"pnpm": {
|
||||
|
||||
@ -0,0 +1,22 @@
|
||||
CREATE TABLE "SupplyChainManualLimitPriceBatch" (
|
||||
"id" TEXT NOT NULL,
|
||||
"fileName" TEXT NOT NULL,
|
||||
"targetPrice" DECIMAL(18,4),
|
||||
"rowCount" INTEGER NOT NULL,
|
||||
"uniqueCodeCount" INTEGER NOT NULL,
|
||||
"matchedBarcodeCount" INTEGER NOT NULL,
|
||||
"matchedStyleCount" INTEGER NOT NULL,
|
||||
"updatedLimitCount" INTEGER NOT NULL,
|
||||
"codes" JSONB NOT NULL DEFAULT '[]',
|
||||
"unmatchedCodes" JSONB NOT NULL DEFAULT '[]',
|
||||
"invalidRows" JSONB NOT NULL DEFAULT '[]',
|
||||
"conflictingStyles" JSONB NOT NULL DEFAULT '[]',
|
||||
"uploadedById" TEXT NOT NULL DEFAULT '',
|
||||
"uploadedByName" TEXT NOT NULL DEFAULT '',
|
||||
"note" TEXT NOT NULL DEFAULT '',
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT "SupplyChainManualLimitPriceBatch_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE INDEX "SupplyChainManualLimitPriceBatch_createdAt_idx"
|
||||
ON "SupplyChainManualLimitPriceBatch"("createdAt");
|
||||
@ -0,0 +1,22 @@
|
||||
CREATE TABLE "SupplyChainLimitPriceHistorySnapshot" (
|
||||
"id" TEXT NOT NULL,
|
||||
"barcode" TEXT NOT NULL,
|
||||
"sku" TEXT NOT NULL DEFAULT '',
|
||||
"spu" TEXT NOT NULL DEFAULT '',
|
||||
"flows" JSONB NOT NULL DEFAULT '[]',
|
||||
"fields" JSONB NOT NULL DEFAULT '[]',
|
||||
"syncedThrough" TEXT NOT NULL,
|
||||
"lastSyncAttemptAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
CONSTRAINT "SupplyChainLimitPriceHistorySnapshot_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX "SupplyChainLimitPriceHistorySnapshot_barcode_sku_spu_key"
|
||||
ON "SupplyChainLimitPriceHistorySnapshot"("barcode", "sku", "spu");
|
||||
|
||||
CREATE INDEX "SupplyChainLimitPriceHistorySnapshot_barcode_idx"
|
||||
ON "SupplyChainLimitPriceHistorySnapshot"("barcode");
|
||||
|
||||
CREATE INDEX "SupplyChainLimitPriceHistorySnapshot_sku_idx"
|
||||
ON "SupplyChainLimitPriceHistorySnapshot"("sku");
|
||||
@ -0,0 +1,10 @@
|
||||
ALTER TABLE "ModularProcessingManualValue"
|
||||
ADD COLUMN "generationKey" TEXT NOT NULL DEFAULT 'legacy';
|
||||
|
||||
DROP INDEX IF EXISTS "ModularProcessingManualValue_barcode_key";
|
||||
|
||||
CREATE UNIQUE INDEX "ModularProcessingManualValue_generationKey_barcode_key"
|
||||
ON "ModularProcessingManualValue"("generationKey", "barcode");
|
||||
|
||||
CREATE INDEX "ModularProcessingManualValue_generationKey_updatedAt_idx"
|
||||
ON "ModularProcessingManualValue"("generationKey", "updatedAt");
|
||||
@ -0,0 +1,6 @@
|
||||
ALTER TABLE "ModularProcessingManualValue"
|
||||
ADD COLUMN "finalProcessingQuantityIsManual" BOOLEAN NOT NULL DEFAULT true;
|
||||
|
||||
UPDATE "ModularProcessingManualValue"
|
||||
SET "finalProcessingQuantityIsManual" = false
|
||||
WHERE "updatedByName" = '系统按部件库存自动校验';
|
||||
@ -0,0 +1,5 @@
|
||||
ALTER TABLE "DiyCabinetStyle"
|
||||
ADD COLUMN "deletedAt" TIMESTAMP(3),
|
||||
ADD COLUMN "deletedBy" TEXT;
|
||||
|
||||
CREATE INDEX "DiyCabinetStyle_deletedAt_idx" ON "DiyCabinetStyle"("deletedAt");
|
||||
@ -0,0 +1,24 @@
|
||||
CREATE TABLE "SupplyChainDiscontinuationStoreGrant" (
|
||||
"id" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"platform" TEXT NOT NULL DEFAULT '',
|
||||
"shopId" TEXT NOT NULL,
|
||||
"shopName" TEXT NOT NULL DEFAULT '',
|
||||
"createdBy" TEXT,
|
||||
"updatedBy" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
CONSTRAINT "SupplyChainDiscontinuationStoreGrant_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX "SupplyChainDiscontinuationStoreGrant_userId_platform_shopId_key"
|
||||
ON "SupplyChainDiscontinuationStoreGrant"("userId", "platform", "shopId");
|
||||
CREATE INDEX "SupplyChainDiscontinuationStoreGrant_userId_idx"
|
||||
ON "SupplyChainDiscontinuationStoreGrant"("userId");
|
||||
CREATE INDEX "SupplyChainDiscontinuationStoreGrant_platform_shopId_idx"
|
||||
ON "SupplyChainDiscontinuationStoreGrant"("platform", "shopId");
|
||||
|
||||
ALTER TABLE "SupplyChainDiscontinuationStoreGrant"
|
||||
ADD CONSTRAINT "SupplyChainDiscontinuationStoreGrant_userId_fkey"
|
||||
FOREIGN KEY ("userId") REFERENCES "User"("id")
|
||||
ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@ -0,0 +1,5 @@
|
||||
CREATE INDEX "SupplyChainDiscontinuationNoticeLink_platform_shopId_idx"
|
||||
ON "SupplyChainDiscontinuationNoticeLink"("platform", "shopId");
|
||||
|
||||
CREATE INDEX "SupplyChainDiscontinuationPenalty_platform_shopId_idx"
|
||||
ON "SupplyChainDiscontinuationPenalty"("platform", "shopId");
|
||||
@ -0,0 +1,8 @@
|
||||
ALTER TABLE "SupplyChainClearanceTask"
|
||||
ADD COLUMN "profitCenter" TEXT NOT NULL DEFAULT '';
|
||||
|
||||
ALTER TABLE "SupplyChainClearanceTask"
|
||||
ADD COLUMN "clearancePrice" DECIMAL(18,3);
|
||||
|
||||
CREATE INDEX "SupplyChainClearanceTask_profitCenter_idx"
|
||||
ON "SupplyChainClearanceTask"("profitCenter");
|
||||
@ -0,0 +1,43 @@
|
||||
CREATE TABLE "SupplyChainClearancePricePushSnapshot" (
|
||||
"id" TEXT NOT NULL,
|
||||
"periodKey" VARCHAR(7) NOT NULL,
|
||||
"startDate" DATE NOT NULL,
|
||||
"generatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"generatedById" TEXT NOT NULL DEFAULT '',
|
||||
"generatedByName" TEXT NOT NULL DEFAULT '',
|
||||
"rowCount" INTEGER NOT NULL DEFAULT 0,
|
||||
CONSTRAINT "SupplyChainClearancePricePushSnapshot_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE TABLE "SupplyChainClearancePricePushItem" (
|
||||
"id" TEXT NOT NULL,
|
||||
"snapshotId" TEXT NOT NULL,
|
||||
"productCode" TEXT NOT NULL,
|
||||
"costPrice" DECIMAL(18,3),
|
||||
"startDate" DATE NOT NULL,
|
||||
"historicalCostPrice" DECIMAL(18,3),
|
||||
"shopId" TEXT NOT NULL,
|
||||
"shopName" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT "SupplyChainClearancePricePushItem_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX "SupplyChainClearancePricePushSnapshot_periodKey_key"
|
||||
ON "SupplyChainClearancePricePushSnapshot"("periodKey");
|
||||
|
||||
CREATE INDEX "SupplyChainClearancePricePushSnapshot_generatedAt_idx"
|
||||
ON "SupplyChainClearancePricePushSnapshot"("generatedAt");
|
||||
|
||||
CREATE UNIQUE INDEX "SCClearancePricePushItem_snapshot_product_shop_key"
|
||||
ON "SupplyChainClearancePricePushItem"("snapshotId", "productCode", "shopName");
|
||||
|
||||
CREATE INDEX "SupplyChainClearancePricePushItem_productCode_idx"
|
||||
ON "SupplyChainClearancePricePushItem"("productCode");
|
||||
|
||||
CREATE INDEX "SupplyChainClearancePricePushItem_shopId_idx"
|
||||
ON "SupplyChainClearancePricePushItem"("shopId");
|
||||
|
||||
ALTER TABLE "SupplyChainClearancePricePushItem"
|
||||
ADD CONSTRAINT "SupplyChainClearancePricePushItem_snapshotId_fkey"
|
||||
FOREIGN KEY ("snapshotId") REFERENCES "SupplyChainClearancePricePushSnapshot"("id")
|
||||
ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@ -0,0 +1,19 @@
|
||||
-- Keep the already applied archive migration unchanged. Add filtering indexes
|
||||
-- here so existing installations receive the same indexes as new databases.
|
||||
BEGIN;
|
||||
SET LOCAL lock_timeout = '5s';
|
||||
SET LOCAL statement_timeout = '120s';
|
||||
|
||||
CREATE INDEX "SupplyChainClearancePricePushItem_snapshot_product_shop_idx"
|
||||
ON "SupplyChainClearancePricePushItem"("snapshotId", "productCode", "shopId");
|
||||
CREATE INDEX "SupplyChainClearancePricePushItem_snapshot_shopName_idx"
|
||||
ON "SupplyChainClearancePricePushItem"("snapshotId", "shopName");
|
||||
CREATE INDEX "SupplyChainClearancePricePushItem_snapshot_costPrice_idx"
|
||||
ON "SupplyChainClearancePricePushItem"("snapshotId", "costPrice");
|
||||
CREATE INDEX "SupplyChainClearancePricePushItem_snapshot_history_idx"
|
||||
ON "SupplyChainClearancePricePushItem"("snapshotId", "historicalCostPrice");
|
||||
|
||||
DROP INDEX "SupplyChainClearancePricePushItem_productCode_idx";
|
||||
DROP INDEX "SupplyChainClearancePricePushItem_shopId_idx";
|
||||
|
||||
COMMIT;
|
||||
@ -0,0 +1,5 @@
|
||||
ALTER TABLE "SupplyChainClearancePricePushItem"
|
||||
ADD COLUMN "profitCenter" TEXT NOT NULL DEFAULT '';
|
||||
|
||||
CREATE INDEX "SupplyChainClearancePricePushItem_snapshot_profitCenter_idx"
|
||||
ON "SupplyChainClearancePricePushItem"("snapshotId", "profitCenter");
|
||||
@ -0,0 +1,20 @@
|
||||
CREATE TABLE "SupplyChainClearanceTaskShipmentDaily" (
|
||||
"id" TEXT NOT NULL,
|
||||
"taskId" TEXT NOT NULL,
|
||||
"statDate" DATE NOT NULL,
|
||||
"shipQty" INTEGER NOT NULL DEFAULT 0,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
CONSTRAINT "SupplyChainClearanceTaskShipmentDaily_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX "SCClearanceTaskShipmentDaily_task_date_key"
|
||||
ON "SupplyChainClearanceTaskShipmentDaily"("taskId", "statDate");
|
||||
|
||||
CREATE INDEX "SupplyChainClearanceTaskShipmentDaily_statDate_idx"
|
||||
ON "SupplyChainClearanceTaskShipmentDaily"("statDate");
|
||||
|
||||
ALTER TABLE "SupplyChainClearanceTaskShipmentDaily"
|
||||
ADD CONSTRAINT "SupplyChainClearanceTaskShipmentDaily_taskId_fkey"
|
||||
FOREIGN KEY ("taskId") REFERENCES "SupplyChainClearanceTask"("id")
|
||||
ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@ -0,0 +1,10 @@
|
||||
ALTER TABLE "StoreAccount"
|
||||
ADD COLUMN "storeOwnership" VARCHAR(16),
|
||||
ADD COLUMN "storeAttribute" VARCHAR(16),
|
||||
ADD COLUMN "shippingEnabled" BOOLEAN;
|
||||
|
||||
ALTER TABLE "StoreAccount"
|
||||
ADD CONSTRAINT "StoreAccount_storeOwnership_check"
|
||||
CHECK ("storeOwnership" IS NULL OR "storeOwnership" IN ('内部', '外部')),
|
||||
ADD CONSTRAINT "StoreAccount_storeAttribute_check"
|
||||
CHECK ("storeAttribute" IS NULL OR "storeAttribute" IN ('真实店铺', '虚拟店铺'));
|
||||
@ -0,0 +1,28 @@
|
||||
CREATE TABLE "ModularProcessingGenerationRun" (
|
||||
"id" TEXT NOT NULL,
|
||||
"snapshotDate" TEXT NOT NULL,
|
||||
"scheduledFor" TIMESTAMP(3) NOT NULL,
|
||||
"status" TEXT NOT NULL DEFAULT 'RUNNING',
|
||||
"startedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"finishedAt" TIMESTAMP(3),
|
||||
"generatedAt" TIMESTAMP(3),
|
||||
"rowCount" INTEGER,
|
||||
"processingOrderCount" INTEGER,
|
||||
"warnings" JSONB NOT NULL DEFAULT '[]',
|
||||
"failureCode" TEXT,
|
||||
"failureReason" TEXT NOT NULL DEFAULT '',
|
||||
"notificationStatus" TEXT NOT NULL DEFAULT 'PENDING',
|
||||
"notificationError" TEXT NOT NULL DEFAULT '',
|
||||
"notificationAttemptedAt" TIMESTAMP(3),
|
||||
"attemptCount" INTEGER NOT NULL DEFAULT 1,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "ModularProcessingGenerationRun_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX "ModularProcessingGenerationRun_snapshotDate_key"
|
||||
ON "ModularProcessingGenerationRun"("snapshotDate");
|
||||
|
||||
CREATE INDEX "ModularProcessingGenerationRun_status_scheduledFor_idx"
|
||||
ON "ModularProcessingGenerationRun"("status", "scheduledFor");
|
||||
@ -0,0 +1,17 @@
|
||||
CREATE TABLE "SupplyChainClearancePricePushManualItem" (
|
||||
"id" TEXT NOT NULL,
|
||||
"periodKey" VARCHAR(7) NOT NULL,
|
||||
"productCode" TEXT NOT NULL,
|
||||
"clearancePrice" DECIMAL(18, 3) NOT NULL,
|
||||
"costPrice" DECIMAL(18, 3),
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "SupplyChainClearancePricePushManualItem_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX "SCPricePushManual_period_product_key"
|
||||
ON "SupplyChainClearancePricePushManualItem"("periodKey", "productCode");
|
||||
|
||||
CREATE INDEX "SCPricePushManual_period_idx"
|
||||
ON "SupplyChainClearancePricePushManualItem"("periodKey");
|
||||
@ -0,0 +1,35 @@
|
||||
CREATE TABLE "SupplyChainExternalOrderCost" (
|
||||
"id" TEXT NOT NULL,
|
||||
"onlineOrderNo" TEXT NOT NULL DEFAULT '',
|
||||
"internalOrderNo" TEXT NOT NULL DEFAULT '',
|
||||
"orderIdentifierType" VARCHAR(16) NOT NULL,
|
||||
"productCode" TEXT NOT NULL DEFAULT '',
|
||||
"styleCode" TEXT NOT NULL DEFAULT '',
|
||||
"shopProductCode" TEXT NOT NULL DEFAULT '',
|
||||
"shopStyleCode" TEXT NOT NULL DEFAULT '',
|
||||
"productIdentifierType" VARCHAR(24) NOT NULL,
|
||||
"productName" TEXT NOT NULL DEFAULT '',
|
||||
"costPrice" DECIMAL(18, 4) NOT NULL,
|
||||
"freightExpenditure" DECIMAL(18, 4),
|
||||
"remark" TEXT NOT NULL DEFAULT '',
|
||||
"sourceFileName" TEXT NOT NULL DEFAULT '',
|
||||
"importedById" TEXT NOT NULL DEFAULT '',
|
||||
"importedByName" TEXT NOT NULL DEFAULT '',
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "SupplyChainExternalOrderCost_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE INDEX "SCExternalOrderCost_onlineOrder_idx"
|
||||
ON "SupplyChainExternalOrderCost"("onlineOrderNo");
|
||||
CREATE INDEX "SCExternalOrderCost_internalOrder_idx"
|
||||
ON "SupplyChainExternalOrderCost"("internalOrderNo");
|
||||
CREATE INDEX "SCExternalOrderCost_productCode_idx"
|
||||
ON "SupplyChainExternalOrderCost"("productCode");
|
||||
CREATE INDEX "SCExternalOrderCost_styleCode_idx"
|
||||
ON "SupplyChainExternalOrderCost"("styleCode");
|
||||
CREATE INDEX "SCExternalOrderCost_shopProductCode_idx"
|
||||
ON "SupplyChainExternalOrderCost"("shopProductCode");
|
||||
CREATE INDEX "SCExternalOrderCost_shopStyleCode_idx"
|
||||
ON "SupplyChainExternalOrderCost"("shopStyleCode");
|
||||
@ -0,0 +1,2 @@
|
||||
ALTER TABLE "SupplyChainClearanceTask"
|
||||
ADD COLUMN "priorityOverride" VARCHAR(50) NOT NULL DEFAULT '';
|
||||
@ -0,0 +1,21 @@
|
||||
ALTER TABLE "SupplyChainClearanceTask"
|
||||
ADD COLUMN "auditAvailableDays" INTEGER,
|
||||
ADD COLUMN "auditDailySales" DECIMAL(18, 4),
|
||||
ADD COLUMN "auditInboundDays" INTEGER,
|
||||
ADD COLUMN "auditStockUsableDaysScore" INTEGER,
|
||||
ADD COLUMN "auditDailySalesScore" INTEGER,
|
||||
ADD COLUMN "auditInboundDaysScore" INTEGER,
|
||||
ADD COLUMN "auditTotalScore" DECIMAL(6, 2),
|
||||
ADD COLUMN "auditSuggestedDiscount" DECIMAL(4, 2),
|
||||
ADD COLUMN "auditPreviousMonthDiscount" DECIMAL(4, 2),
|
||||
ADD COLUMN "auditScoreVersion" VARCHAR(32) NOT NULL DEFAULT '',
|
||||
ADD COLUMN "auditScoredAt" TIMESTAMP(3);
|
||||
|
||||
CREATE UNIQUE INDEX "SCClearanceTask_period_round_barcode_center_applicant_key"
|
||||
ON "SupplyChainClearanceTask"(
|
||||
"periodKey",
|
||||
"round",
|
||||
"barcode",
|
||||
"profitCenter",
|
||||
"submittedById"
|
||||
);
|
||||
@ -0,0 +1,31 @@
|
||||
ALTER TABLE "SupplyChainClearanceConfig"
|
||||
ADD COLUMN "reviewScoreConfig" JSONB NOT NULL DEFAULT '{}'::jsonb;
|
||||
|
||||
ALTER TABLE "SupplyChainClearancePoolSnapshot"
|
||||
ADD COLUMN "skuProductionStatus" VARCHAR(20) NOT NULL DEFAULT '';
|
||||
|
||||
CREATE INDEX "SupplyChainClearancePoolSnapshot_sku_idx"
|
||||
ON "SupplyChainClearancePoolSnapshot"("sku");
|
||||
|
||||
UPDATE "SupplyChainClearancePoolSnapshot" AS pool
|
||||
SET "skuProductionStatus" = grouped."status"
|
||||
FROM (
|
||||
SELECT
|
||||
UPPER(TRIM("sku")) AS "skuKey",
|
||||
CASE
|
||||
WHEN COUNT(*) FILTER (WHERE "status" = '停产') = COUNT(*) THEN '停产'
|
||||
WHEN COUNT(*) FILTER (WHERE "status" = '销售') > 0 THEN '销售'
|
||||
WHEN COUNT(*) FILTER (WHERE "status" = '退市') > 0 THEN '退市'
|
||||
ELSE ''
|
||||
END AS "status"
|
||||
FROM (
|
||||
SELECT
|
||||
"sku",
|
||||
UPPER(TRIM("productStatus")) AS "status"
|
||||
FROM "SupplyChainProductArchive"
|
||||
WHERE UPPER(TRIM("productStatus")) IN ('销售', '退市', '停产')
|
||||
AND TRIM("sku") <> ''
|
||||
) AS archive_status
|
||||
GROUP BY UPPER(TRIM("sku"))
|
||||
) AS grouped
|
||||
WHERE UPPER(TRIM(pool."sku")) = grouped."skuKey";
|
||||
@ -0,0 +1,7 @@
|
||||
ALTER TABLE "SupplyChainClearanceTask"
|
||||
ALTER COLUMN "auditStockUsableDaysScore" TYPE DECIMAL(8, 2)
|
||||
USING "auditStockUsableDaysScore"::DECIMAL(8, 2),
|
||||
ALTER COLUMN "auditDailySalesScore" TYPE DECIMAL(8, 2)
|
||||
USING "auditDailySalesScore"::DECIMAL(8, 2),
|
||||
ALTER COLUMN "auditInboundDaysScore" TYPE DECIMAL(8, 2)
|
||||
USING "auditInboundDaysScore"::DECIMAL(8, 2);
|
||||
@ -1154,8 +1154,12 @@ model ModularProductProcessingRuleRow {
|
||||
/// 加工单人工维护值:按条码保存最终加工量、加工单备注和人工叫货仓库。
|
||||
model ModularProcessingManualValue {
|
||||
id String @id @default(cuid())
|
||||
barcode String @unique
|
||||
/// 所属生成批次:最新快照 generatedAt 的 ISO 时间;旧数据默认 legacy,不参与新批次。
|
||||
generationKey String @default("legacy")
|
||||
barcode String
|
||||
finalProcessingQuantity Int?
|
||||
/// true = 用户实际编辑/导入;false = 系统公式或库存约束自动调整。
|
||||
finalProcessingQuantityIsManual Boolean @default(true)
|
||||
processingNote String @default("")
|
||||
orderWarehouse String?
|
||||
updatedById String?
|
||||
@ -1163,7 +1167,8 @@ model ModularProcessingManualValue {
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([updatedAt])
|
||||
@@unique([generationKey, barcode])
|
||||
@@index([generationKey, updatedAt])
|
||||
}
|
||||
|
||||
/// 模块化商品加工主表每日快照,用于历史日期查询。
|
||||
@ -1180,6 +1185,34 @@ model ModularProcessingOrderDailySnapshot {
|
||||
@@index([snapshotDate, generatedAt])
|
||||
}
|
||||
|
||||
/// 模块化商品加工单每日生成运行状态:记录定时生成、人工刷新、失败原因和通知结果。
|
||||
model ModularProcessingGenerationRun {
|
||||
id String @id @default(cuid())
|
||||
/// 北京时间日期,YYYY-MM-DD
|
||||
snapshotDate String @unique
|
||||
/// 计划生成时间对应的 UTC 时间
|
||||
scheduledFor DateTime
|
||||
/// RUNNING / SUCCEEDED / FAILED
|
||||
status String @default("RUNNING")
|
||||
startedAt DateTime @default(now())
|
||||
finishedAt DateTime?
|
||||
generatedAt DateTime?
|
||||
rowCount Int?
|
||||
processingOrderCount Int?
|
||||
warnings Json @default("[]")
|
||||
failureCode String?
|
||||
failureReason String @default("")
|
||||
/// PENDING / SENT / PARTIAL / FAILED / NOT_CONFIGURED / NO_RECIPIENT
|
||||
notificationStatus String @default("PENDING")
|
||||
notificationError String @default("")
|
||||
notificationAttemptedAt DateTime?
|
||||
attemptCount Int @default(1)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([status, scheduledFor])
|
||||
}
|
||||
|
||||
/// 模块化商品加工单;每日主表生成后自动生成,人工调整后可覆盖当天明细。
|
||||
model ModularProcessingOrder {
|
||||
id String @id @default(cuid())
|
||||
@ -2936,6 +2969,9 @@ model DiyCabinetStyle {
|
||||
mainNodeNames String[] @default([])
|
||||
shelfNodeNames String[] @default([])
|
||||
modelRotationY Float @default(0)
|
||||
deletedAt DateTime?
|
||||
deletedBy String?
|
||||
@@index([deletedAt])
|
||||
@@index([spu, sortOrder])
|
||||
}
|
||||
|
||||
@ -3565,6 +3601,7 @@ model User {
|
||||
businessRoles BusinessAccountType[] @default([])
|
||||
permissionRoleId String?
|
||||
permissionRole PermissionRole? @relation(fields: [permissionRoleId], references: [id], onDelete: SetNull)
|
||||
discontinuationStoreGrants SupplyChainDiscontinuationStoreGrant[]
|
||||
|
||||
// 组织架构
|
||||
departmentId String?
|
||||
@ -4457,6 +4494,26 @@ model SupplyChainPermissionOverride {
|
||||
@@index([subjectKey])
|
||||
}
|
||||
|
||||
/// 停产下架 · 用户级附加店铺数据范围。与原人员范围取并集。
|
||||
model SupplyChainDiscontinuationStoreGrant {
|
||||
id String @id @default(cuid())
|
||||
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
platform String @default("")
|
||||
shopId String
|
||||
shopName String @default("")
|
||||
|
||||
createdBy String?
|
||||
updatedBy String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@unique([userId, platform, shopId])
|
||||
@@index([userId])
|
||||
@@index([platform, shopId])
|
||||
}
|
||||
|
||||
/// 显式配置主体;个人配置即使没有屏蔽字段,也与「跟随岗位」区分。
|
||||
model ProductArchiveFieldPermissionSubject {
|
||||
subjectKey String @id
|
||||
@ -4501,6 +4558,9 @@ model StoreAccount {
|
||||
jstOrderService String @default("")
|
||||
accountGroupName String @default("")
|
||||
shopState Int?
|
||||
storeOwnership String? @db.VarChar(16)
|
||||
storeAttribute String? @db.VarChar(16)
|
||||
shippingEnabled Boolean?
|
||||
subject String @default("")
|
||||
boundBankAccountNumberCiphertext String @default("") @db.Text
|
||||
createdById String?
|
||||
@ -5438,6 +5498,48 @@ model SupplyChainStyleLimitPrice {
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
/// 限价管理 · 负责人限价历史流程快照。首次查询保存原始流程与表单字段,
|
||||
/// 后续按报表库 updateAt 水位增量刷新。
|
||||
model SupplyChainLimitPriceHistorySnapshot {
|
||||
id String @id @default(cuid())
|
||||
barcode String
|
||||
sku String @default("")
|
||||
spu String @default("")
|
||||
flows Json @default("[]")
|
||||
fields Json @default("[]")
|
||||
/// 本次快照对应的报表库时间,下一次从这里继续增量同步。
|
||||
syncedThrough String
|
||||
lastSyncAttemptAt DateTime @default(now())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@unique([barcode, sku, spu])
|
||||
@@index([barcode])
|
||||
@@index([sku])
|
||||
}
|
||||
|
||||
/// 限价管理 · 人工上传批次。最终生效价格写入 SupplyChainStyleLimitPrice(applyType=人工指定限价)。
|
||||
model SupplyChainManualLimitPriceBatch {
|
||||
id String @id @default(cuid())
|
||||
fileName String
|
||||
targetPrice Decimal? @db.Decimal(18, 4)
|
||||
rowCount Int
|
||||
uniqueCodeCount Int
|
||||
matchedBarcodeCount Int
|
||||
matchedStyleCount Int
|
||||
updatedLimitCount Int
|
||||
codes Json @default("[]")
|
||||
unmatchedCodes Json @default("[]")
|
||||
invalidRows Json @default("[]")
|
||||
conflictingStyles Json @default("[]")
|
||||
uploadedById String @default("")
|
||||
uploadedByName String @default("")
|
||||
note String @default("")
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@index([createdAt])
|
||||
}
|
||||
|
||||
/// 限价流程同步 · 价格授权(同价申请 / 活动限价;供破价判定豁免与超期判定)。
|
||||
model SupplyChainLimitFlowAuth {
|
||||
id String @id @default(cuid())
|
||||
@ -5509,6 +5611,7 @@ model SupplyChainDiscontinuationNoticeLink {
|
||||
@@unique([noticeId, shopId, itemId, shopSkuCode], map: "SupplyChainDiscontinuationNoticeLink_nsis_key")
|
||||
@@index([itemId])
|
||||
@@index([ownerName])
|
||||
@@index([platform, shopId])
|
||||
}
|
||||
|
||||
/// 停产下架 · 乐捐明细(订单+链接唯一,月度账期;运营仅可查看本人记录)
|
||||
@ -5547,6 +5650,7 @@ model SupplyChainDiscontinuationPenalty {
|
||||
@@unique([oId, shopSkuCode])
|
||||
@@index([monthKey, ownerName])
|
||||
@@index([shopSkuCode])
|
||||
@@index([platform, shopId])
|
||||
@@index([payAt])
|
||||
@@index([status])
|
||||
}
|
||||
@ -5700,6 +5804,7 @@ model SupplyChainClearancePoolSnapshot {
|
||||
category String @default("")
|
||||
productStatus String @default("")
|
||||
salesStatus String @default("")
|
||||
skuProductionStatus String @default("")
|
||||
supplierName String @default("")
|
||||
packType String @default("")
|
||||
cooperationStatus String @default("")
|
||||
@ -5722,6 +5827,7 @@ model SupplyChainClearancePoolSnapshot {
|
||||
@@index([statDate])
|
||||
@@index([ownerName])
|
||||
@@index([productStatus])
|
||||
@@index([sku])
|
||||
}
|
||||
|
||||
/// 清货池月度生成记录。经管确认生成后,该月份不再允许变更。
|
||||
@ -5749,6 +5855,7 @@ model SupplyChainClearanceConfig {
|
||||
fixedTaskFallbackFormula String @default("") @db.VarChar(500)
|
||||
discountRestrictions Json @default("[]")
|
||||
scoreConfig Json @default("{}")
|
||||
reviewScoreConfig Json @default("{}")
|
||||
updatedById String @default("")
|
||||
updatedByName String @default("")
|
||||
createdAt DateTime @default(now())
|
||||
@ -5761,6 +5868,8 @@ model SupplyChainClearanceTask {
|
||||
periodKey String @db.VarChar(7)
|
||||
round Int @default(1)
|
||||
barcode String
|
||||
profitCenter String @default("")
|
||||
clearancePrice Decimal? @db.Decimal(18, 3)
|
||||
sku String @default("")
|
||||
productName String @default("")
|
||||
brand String @default("")
|
||||
@ -5774,20 +5883,114 @@ model SupplyChainClearanceTask {
|
||||
submittedById String @default("")
|
||||
submittedByName String @default("")
|
||||
submittedAt DateTime @default(now())
|
||||
auditAvailableDays Int?
|
||||
auditDailySales Decimal? @db.Decimal(18, 4)
|
||||
auditInboundDays Int?
|
||||
auditStockUsableDaysScore Decimal? @db.Decimal(8, 2)
|
||||
auditDailySalesScore Decimal? @db.Decimal(8, 2)
|
||||
auditInboundDaysScore Decimal? @db.Decimal(8, 2)
|
||||
auditTotalScore Decimal? @db.Decimal(6, 2)
|
||||
auditSuggestedDiscount Decimal? @db.Decimal(4, 2)
|
||||
auditPreviousMonthDiscount Decimal? @db.Decimal(4, 2)
|
||||
auditScoreVersion String @default("") @db.VarChar(32)
|
||||
auditScoredAt DateTime?
|
||||
reviewedById String @default("")
|
||||
reviewedByName String @default("")
|
||||
reviewedAt DateTime?
|
||||
rejectReason String @default("") @db.Text
|
||||
priorityOverride String @default("") @db.VarChar(50)
|
||||
clearedQty Int @default(0)
|
||||
lastProgressAt DateTime?
|
||||
revision Int @default(1)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
events SupplyChainClearanceTaskEvent[]
|
||||
shipmentDaily SupplyChainClearanceTaskShipmentDaily[]
|
||||
|
||||
@@index([periodKey, status])
|
||||
@@index([barcode])
|
||||
@@index([profitCenter])
|
||||
@@index([ownerName])
|
||||
@@unique([periodKey, round, barcode, profitCenter, submittedById], map: "SCClearanceTask_period_round_barcode_center_applicant_key")
|
||||
}
|
||||
|
||||
/// 清货价推送月度存档:每月固定一份,重新生成时仅覆盖当月。
|
||||
model SupplyChainClearancePricePushSnapshot {
|
||||
id String @id @default(cuid())
|
||||
periodKey String @unique @db.VarChar(7)
|
||||
startDate DateTime @db.Date
|
||||
generatedAt DateTime @default(now())
|
||||
generatedById String @default("")
|
||||
generatedByName String @default("")
|
||||
rowCount Int @default(0)
|
||||
items SupplyChainClearancePricePushItem[]
|
||||
|
||||
@@index([generatedAt])
|
||||
}
|
||||
|
||||
/// 清货价推送明细。历史成本价字段保存对店铺实际推送的清货价。
|
||||
model SupplyChainClearancePricePushItem {
|
||||
id String @id @default(cuid())
|
||||
snapshotId String
|
||||
snapshot SupplyChainClearancePricePushSnapshot @relation(fields: [snapshotId], references: [id], onDelete: Cascade)
|
||||
productCode String
|
||||
costPrice Decimal? @db.Decimal(18, 3)
|
||||
startDate DateTime @db.Date
|
||||
historicalCostPrice Decimal? @db.Decimal(18, 3)
|
||||
shopId String
|
||||
shopName String
|
||||
profitCenter String @default("")
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@unique([snapshotId, productCode, shopName], map: "SCClearancePricePushItem_snapshot_product_shop_key")
|
||||
@@index([snapshotId, productCode, shopId])
|
||||
@@index([snapshotId, shopName])
|
||||
@@index([snapshotId, costPrice])
|
||||
@@index([snapshotId, historicalCostPrice])
|
||||
@@index([snapshotId, profitCenter])
|
||||
}
|
||||
|
||||
/// 清货任务之外的人工清货价。按月保存,仅进入清货价推送,不生成清货任务。
|
||||
model SupplyChainClearancePricePushManualItem {
|
||||
id String @id @default(cuid())
|
||||
periodKey String @db.VarChar(7)
|
||||
productCode String
|
||||
clearancePrice Decimal @db.Decimal(18, 3)
|
||||
costPrice Decimal? @db.Decimal(18, 3)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@unique([periodKey, productCode], map: "SCPricePushManual_period_product_key")
|
||||
@@index([periodKey], map: "SCPricePushManual_period_idx")
|
||||
}
|
||||
|
||||
/// 外采订单自定义成本。订单号二选一,商品标识四选一,成本价必填。
|
||||
model SupplyChainExternalOrderCost {
|
||||
id String @id @default(cuid())
|
||||
onlineOrderNo String @default("")
|
||||
internalOrderNo String @default("")
|
||||
orderIdentifierType String @db.VarChar(16)
|
||||
productCode String @default("")
|
||||
styleCode String @default("")
|
||||
shopProductCode String @default("")
|
||||
shopStyleCode String @default("")
|
||||
productIdentifierType String @db.VarChar(24)
|
||||
productName String @default("")
|
||||
costPrice Decimal @db.Decimal(18, 4)
|
||||
freightExpenditure Decimal? @db.Decimal(18, 4)
|
||||
remark String @default("") @db.Text
|
||||
sourceFileName String @default("")
|
||||
importedById String @default("")
|
||||
importedByName String @default("")
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([onlineOrderNo], map: "SCExternalOrderCost_onlineOrder_idx")
|
||||
@@index([internalOrderNo], map: "SCExternalOrderCost_internalOrder_idx")
|
||||
@@index([productCode], map: "SCExternalOrderCost_productCode_idx")
|
||||
@@index([styleCode], map: "SCExternalOrderCost_styleCode_idx")
|
||||
@@index([shopProductCode], map: "SCExternalOrderCost_shopProductCode_idx")
|
||||
@@index([shopStyleCode], map: "SCExternalOrderCost_shopStyleCode_idx")
|
||||
}
|
||||
|
||||
/// 清货任务流转记录(提交 / 通过 / 驳回 / 调整 / 运营确认移除)
|
||||
@ -5806,6 +6009,20 @@ model SupplyChainClearanceTaskEvent {
|
||||
@@index([taskId, createdAt])
|
||||
}
|
||||
|
||||
/// 清货任务每日发货缓存。按任务和自然日增量落库,避免每天重算整月出库。
|
||||
model SupplyChainClearanceTaskShipmentDaily {
|
||||
id String @id @default(cuid())
|
||||
taskId String
|
||||
task SupplyChainClearanceTask @relation(fields: [taskId], references: [id], onDelete: Cascade)
|
||||
statDate DateTime @db.Date
|
||||
shipQty Int @default(0)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@unique([taskId, statDate], map: "SCClearanceTaskShipmentDaily_task_date_key")
|
||||
@@index([statDate])
|
||||
}
|
||||
|
||||
/// 清货复盘 · 月度维度快照(运营中心 / 部门负责人 / 优先级 / 折扣 / 整体)
|
||||
model SupplyChainClearanceReviewSnapshot {
|
||||
id String @id @default(cuid())
|
||||
|
||||
BIN
furniture-3d-system/public/diy-guide/ai-render.webp
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
furniture-3d-system/public/diy-guide/barcodes.webp
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
furniture-3d-system/public/diy-guide/batch-render.webp
Normal file
|
After Width: | Height: | Size: 49 KiB |
BIN
furniture-3d-system/public/diy-guide/batch-results.webp
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
furniture-3d-system/public/diy-guide/colors.webp
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
BIN
furniture-3d-system/public/diy-guide/details/ai-reference.webp
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
BIN
furniture-3d-system/public/diy-guide/details/ai-scene.webp
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
furniture-3d-system/public/diy-guide/details/ai-settings.webp
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
furniture-3d-system/public/diy-guide/details/barcode-table.webp
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
furniture-3d-system/public/diy-guide/details/batch-angle.webp
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
furniture-3d-system/public/diy-guide/details/batch-settings.webp
Normal file
|
After Width: | Height: | Size: 6.2 KiB |
BIN
furniture-3d-system/public/diy-guide/details/batch-setup.webp
Normal file
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 5.1 KiB |
BIN
furniture-3d-system/public/diy-guide/details/delete-scheme.webp
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
furniture-3d-system/public/diy-guide/details/door-cells.webp
Normal file
|
After Width: | Height: | Size: 58 KiB |
BIN
furniture-3d-system/public/diy-guide/details/door-controls.webp
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
furniture-3d-system/public/diy-guide/details/door-patterns.webp
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
furniture-3d-system/public/diy-guide/details/image-actions.webp
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
furniture-3d-system/public/diy-guide/details/image-edit.webp
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
furniture-3d-system/public/diy-guide/details/image-layout.webp
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
furniture-3d-system/public/diy-guide/details/image-resize.webp
Normal file
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 15 KiB |
BIN
furniture-3d-system/public/diy-guide/details/image-text.webp
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
furniture-3d-system/public/diy-guide/details/library.webp
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
furniture-3d-system/public/diy-guide/details/model.webp
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
furniture-3d-system/public/diy-guide/details/save-dialog.webp
Normal file
|
After Width: | Height: | Size: 6.9 KiB |
BIN
furniture-3d-system/public/diy-guide/details/scheme-list.webp
Normal file
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
BIN
furniture-3d-system/public/diy-guide/details/toolbar.webp
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
furniture-3d-system/public/diy-guide/details/unsaved-dialog.webp
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
BIN
furniture-3d-system/public/diy-guide/details/view-tools.webp
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
furniture-3d-system/public/diy-guide/doors.webp
Normal file
|
After Width: | Height: | Size: 106 KiB |
BIN
furniture-3d-system/public/diy-guide/flip-door-demo.webm
Normal file
BIN
furniture-3d-system/public/diy-guide/flip-doors.webp
Normal file
|
After Width: | Height: | Size: 104 KiB |
BIN
furniture-3d-system/public/diy-guide/layout.webp
Normal file
|
After Width: | Height: | Size: 106 KiB |
BIN
furniture-3d-system/public/diy-guide/schemes.webp
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
furniture-3d-system/public/diy-guide/series.webp
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
furniture-3d-system/public/diy-guide/workbench.webp
Normal file
|
After Width: | Height: | Size: 108 KiB |
BIN
furniture-3d-system/public/diy/SJ0450/flip-door.glb
Normal file
BIN
furniture-3d-system/public/templates/sku-auto-code-import.xlsx
Normal file
@ -0,0 +1,136 @@
|
||||
import 'dotenv/config'
|
||||
import { prisma } from '../src/lib/prisma'
|
||||
import { calculateClearanceReviewScore } from '../src/lib/clearance-review-score'
|
||||
import { normalizeClearanceReviewScoreConfig } from '../src/lib/clearance-review-score-config'
|
||||
import { supplyChainClearanceRepository } from '../src/server/repositories/supply-chain-clearance.repository'
|
||||
|
||||
function argumentValue(name: string) {
|
||||
const prefix = `--${name}=`
|
||||
return process.argv.find((argument) => argument.startsWith(prefix))?.slice(prefix.length) ?? ''
|
||||
}
|
||||
|
||||
function objectValue(value: unknown) {
|
||||
return value && typeof value === 'object' && !Array.isArray(value)
|
||||
? value as Record<string, unknown>
|
||||
: {}
|
||||
}
|
||||
|
||||
function numeric(value: unknown) {
|
||||
if (value == null || value === '') return null
|
||||
const parsed = Number(String(value).replace(/,/g, '').trim())
|
||||
return Number.isFinite(parsed) ? parsed : null
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const apply = process.argv.includes('--apply')
|
||||
const force = process.argv.includes('--force')
|
||||
const requestedPeriod = argumentValue('period')
|
||||
const periods = requestedPeriod
|
||||
? [requestedPeriod]
|
||||
: await supplyChainClearanceRepository.listTaskPeriods()
|
||||
const reviewScoreConfig = normalizeClearanceReviewScoreConfig(
|
||||
(await supplyChainClearanceRepository.findClearanceConfig())?.reviewScoreConfig,
|
||||
)
|
||||
let scanned = 0
|
||||
let ready = 0
|
||||
let skipped = 0
|
||||
let missingPool = 0
|
||||
|
||||
for (const periodKey of periods) {
|
||||
const tasks = await supplyChainClearanceRepository.listTasks({ periodKey })
|
||||
const targetTasks = tasks.filter((task) => (
|
||||
task.status !== 'REMOVED'
|
||||
&& (force || !task.auditScoredAt)
|
||||
))
|
||||
if (!targetTasks.length) continue
|
||||
const barcodes = Array.from(new Set(targetTasks.map((task) => task.barcode)))
|
||||
const [poolItems, priorDiscountRows] = await Promise.all([
|
||||
supplyChainClearanceRepository.listPoolItemsForTaskPeriod(periodKey, barcodes),
|
||||
supplyChainClearanceRepository.listLatestApprovedTaskDiscountsBefore({
|
||||
periodKey,
|
||||
barcodes,
|
||||
}),
|
||||
])
|
||||
const poolByBarcode = new Map(
|
||||
poolItems.map((item) => [item.barcode.trim().toUpperCase(), item]),
|
||||
)
|
||||
const priorDiscountByKey = new Map<string, number>()
|
||||
for (const row of priorDiscountRows) {
|
||||
const key = `${row.barcode.trim().toUpperCase()}\u0000${row.profitCenter.trim()}`
|
||||
if (priorDiscountByKey.has(key)) continue
|
||||
const discount = numeric(row.discount)
|
||||
if (discount != null) priorDiscountByKey.set(key, discount)
|
||||
}
|
||||
const updates: Array<ReturnType<typeof prisma.supplyChainClearanceTask.update>> = []
|
||||
for (const task of targetTasks) {
|
||||
scanned += 1
|
||||
const poolItem = poolByBarcode.get(task.barcode.trim().toUpperCase())
|
||||
if (!poolItem) {
|
||||
missingPool += 1
|
||||
continue
|
||||
}
|
||||
const profitCenter = task.profitCenter.trim()
|
||||
|| String(objectValue(poolItem.extra).profitCenter ?? '').trim()
|
||||
if (!profitCenter) {
|
||||
skipped += 1
|
||||
continue
|
||||
}
|
||||
const extra = objectValue(poolItem.extra)
|
||||
const score = calculateClearanceReviewScore({
|
||||
availableDays: poolItem.availableDays,
|
||||
dailySales: poolItem.dailySales,
|
||||
inboundDays: extra.inboundDays,
|
||||
}, reviewScoreConfig)
|
||||
const scoredAt = new Date()
|
||||
updates.push(prisma.supplyChainClearanceTask.update({
|
||||
where: { id: task.id },
|
||||
data: {
|
||||
profitCenter,
|
||||
auditAvailableDays: score.auditAvailableDays == null
|
||||
? null
|
||||
: Math.trunc(score.auditAvailableDays),
|
||||
auditDailySales: score.auditDailySales,
|
||||
auditInboundDays: score.auditInboundDays == null
|
||||
? null
|
||||
: Math.trunc(score.auditInboundDays),
|
||||
auditStockUsableDaysScore: score.stockUsableDaysScore,
|
||||
auditDailySalesScore: score.dailySalesScore,
|
||||
auditInboundDaysScore: score.inboundDaysScore,
|
||||
auditTotalScore: score.totalScore,
|
||||
auditSuggestedDiscount: score.suggestedDiscount,
|
||||
auditPreviousMonthDiscount: priorDiscountByKey.get(
|
||||
`${task.barcode.trim().toUpperCase()}\u0000${profitCenter}`,
|
||||
) ?? null,
|
||||
auditScoreVersion: score.scoreVersion,
|
||||
auditScoredAt: scoredAt,
|
||||
},
|
||||
}))
|
||||
ready += 1
|
||||
}
|
||||
if (apply) {
|
||||
for (let index = 0; index < updates.length; index += 100) {
|
||||
await prisma.$transaction(updates.slice(index, index + 100))
|
||||
}
|
||||
}
|
||||
console.log(`${periodKey}: ${targetTasks.length} 条待回填,${updates.length} 条可写入`)
|
||||
}
|
||||
|
||||
console.log(JSON.stringify({
|
||||
mode: apply ? 'apply' : 'dry-run',
|
||||
periods: periods.length,
|
||||
scanned,
|
||||
ready,
|
||||
skipped,
|
||||
missingPool,
|
||||
force,
|
||||
}, null, 2))
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((error) => {
|
||||
console.error(error)
|
||||
process.exitCode = 1
|
||||
})
|
||||
.finally(async () => {
|
||||
await prisma.$disconnect()
|
||||
})
|
||||
@ -0,0 +1,44 @@
|
||||
import { config as loadDotenv } from "dotenv";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
|
||||
loadDotenv({ path: ".env.local", override: false });
|
||||
loadDotenv();
|
||||
|
||||
async function main() {
|
||||
const args = process.argv.slice(2);
|
||||
const file = args.find((arg) => arg.startsWith("--file="))?.slice(7);
|
||||
const output = args.find((arg) => arg.startsWith("--output="))?.slice(9);
|
||||
const apply = args.includes("--apply");
|
||||
if (args.includes("--help")) {
|
||||
console.log("Usage: tsx scripts/backfill-store-account-metadata.ts --file=<xlsx> [--output=<audit.json>] [--apply]");
|
||||
console.log("Preview by default. Fills only matched blank fields. Apply is restricted to a local database.");
|
||||
return;
|
||||
}
|
||||
if (!file || args.some((arg) => !arg.startsWith("--file=") && !arg.startsWith("--output=") && arg !== "--apply")) {
|
||||
throw new Error("请指定 --file=<xlsx>,可选 --output=<audit.json> 和 --apply");
|
||||
}
|
||||
const db = new URL(process.env.DATABASE_URL ?? "");
|
||||
if (!["localhost", "127.0.0.1", "[::1]"].includes(db.hostname)) throw new Error("本地补数入口禁止连接非本机数据库");
|
||||
if (apply && !output) throw new Error("补数必须指定 --output 保存修改前后审计");
|
||||
const { previewStoreAccountMetadataBackfill } = await import("../src/server/services/store-account-metadata-backfill.service");
|
||||
const { storeAccountMetadataRepository } = await import("../src/server/repositories/store-account-metadata.repository");
|
||||
console.log("正在读取表格并核对现有店铺资料……");
|
||||
const plan = await previewStoreAccountMetadataBackfill(file);
|
||||
const report = { apply, source: path.basename(file), database: { host: db.hostname, port: db.port, name: db.pathname.slice(1) },
|
||||
checkedAt: new Date().toISOString(), ...plan, appliedAccounts: 0 };
|
||||
if (output) {
|
||||
await fs.mkdir(path.dirname(path.resolve(output)), { recursive: true });
|
||||
await fs.writeFile(output, JSON.stringify(report, null, 2) + "\n", "utf8");
|
||||
}
|
||||
if (apply) {
|
||||
console.log(`已核对${plan.summary.accounts}条账号,正在补充${plan.updates.length}条……`);
|
||||
report.appliedAccounts = await storeAccountMetadataRepository.applyUpdates(plan.updates, path.basename(file));
|
||||
await fs.writeFile(output!, JSON.stringify(report, null, 2) + "\n", "utf8");
|
||||
}
|
||||
console.log(JSON.stringify({ apply, summary: plan.summary, unmatchedStores: plan.unmatchedStores,
|
||||
appliedAccounts: report.appliedAccounts, failed: 0 }, null, 2));
|
||||
}
|
||||
|
||||
main().catch((error) => { console.error(error instanceof Error ? error.message : String(error)); process.exitCode = 1; })
|
||||
.finally(async () => { const { prisma } = await import("../src/lib/prisma"); await prisma.$disconnect(); });
|
||||
32
furniture-3d-system/scripts/check-modular-processing-generation-health.ts
Executable file
@ -0,0 +1,32 @@
|
||||
/**
|
||||
* 模块化加工单生成健康检查。
|
||||
*
|
||||
* 09:40 后仍未成功生成时,记录失败状态并按日期向所有启用中的系统管理员发送一次预警。
|
||||
* 建议每 5 分钟执行一次;幂等键保证同一天、同一失败类型不会重复发送。
|
||||
*/
|
||||
import { loadEnvConfig } from '@next/env'
|
||||
|
||||
loadEnvConfig(process.cwd())
|
||||
|
||||
async function main() {
|
||||
const snapshotDate = process.argv
|
||||
.find((argument) => argument.startsWith('--date='))
|
||||
?.slice('--date='.length)
|
||||
const { modularProcessingGenerationHealthService } = await import(
|
||||
'@/server/services/modular-processing-generation-health.service'
|
||||
)
|
||||
const { prisma } = await import('@/lib/prisma')
|
||||
try {
|
||||
const health = await modularProcessingGenerationHealthService.checkAndNotify(
|
||||
snapshotDate || undefined,
|
||||
)
|
||||
console.log(JSON.stringify({ health }))
|
||||
} finally {
|
||||
await prisma.$disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
void main().catch((error) => {
|
||||
console.error(error instanceof Error ? error.message : error)
|
||||
process.exitCode = 1
|
||||
})
|
||||
74
furniture-3d-system/scripts/generate-limit-price-snapshot.ts
Normal file
@ -0,0 +1,74 @@
|
||||
import { loadEnvConfig } from '@next/env'
|
||||
|
||||
loadEnvConfig(process.cwd())
|
||||
|
||||
const warnings = process.argv
|
||||
.filter((argument) => argument.startsWith('--warning='))
|
||||
.map((argument) => argument.slice('--warning='.length).trim())
|
||||
.filter(Boolean)
|
||||
const runStartedAt = process.argv
|
||||
.find((argument) => argument.startsWith('--run-started-at='))
|
||||
?.slice('--run-started-at='.length)
|
||||
|
||||
async function main() {
|
||||
const { prisma } = await import('../src/lib/prisma')
|
||||
const { supplyChainLimitPriceSnapshotService } = await import(
|
||||
'../src/server/services/supply-chain-limit-price-snapshot.service'
|
||||
)
|
||||
const { freightConfigRepository } = await import(
|
||||
'../src/server/repositories/freight-config.repository'
|
||||
)
|
||||
const { productArchiveCertificateReportService } = await import(
|
||||
'../src/server/services/product-archive-certificate-report.service'
|
||||
)
|
||||
const fs = await import('node:fs/promises')
|
||||
const path = await import('node:path')
|
||||
const { resolveFreightConfigDataDir } = await import('../src/lib/freight-config-paths')
|
||||
try {
|
||||
// 每日快照必须读取报表库最新费率,不复用页面请求留下的短时缓存。
|
||||
await freightConfigRepository.close()
|
||||
productArchiveCertificateReportService.clearCache()
|
||||
const freightConfigDataDir = resolveFreightConfigDataDir({
|
||||
configuredDir: process.env.FREIGHT_CONFIG_DATA_DIR,
|
||||
})
|
||||
const [archive, sales, limits, logisticsRules, provinceShare] = await Promise.all([
|
||||
prisma.supplyChainProductArchiveSource.aggregate({ _max: { lastSyncedAt: true } }),
|
||||
prisma.supplyChainProductArchiveSalesPerformanceDaily.aggregate({ _max: { syncedAt: true } }),
|
||||
prisma.supplyChainStyleLimitPrice.aggregate({ _max: { updatedAt: true } }),
|
||||
fs.readFile(path.join(freightConfigDataDir, 'warehouse-express-rules.json'), 'utf8')
|
||||
.then((text) => JSON.parse(text) as { generatedAt?: string })
|
||||
.catch(() => null),
|
||||
fs.readFile(path.join(freightConfigDataDir, 'province-shipment-share.json'), 'utf8')
|
||||
.then((text) => JSON.parse(text) as { generatedAt?: string })
|
||||
.catch(() => null),
|
||||
])
|
||||
const manifest = await supplyChainLimitPriceSnapshotService.generate({
|
||||
warnings,
|
||||
sourceTimestamps: {
|
||||
productArchiveSyncedAt: archive._max.lastSyncedAt?.toISOString() ?? null,
|
||||
salesPerformanceSyncedAt: sales._max.syncedAt?.toISOString() ?? null,
|
||||
ownerLimitsSyncedAt: limits._max.updatedAt?.toISOString() ?? null,
|
||||
logisticsRulesGeneratedAt: logisticsRules?.generatedAt ?? null,
|
||||
provinceShareGeneratedAt: provinceShare?.generatedAt ?? null,
|
||||
ratesReadAt: new Date().toISOString(),
|
||||
runStartedAt: runStartedAt ?? null,
|
||||
},
|
||||
onProgress(processedRows, totalRows) {
|
||||
const progress = totalRows > 0 ? Math.round(processedRows / totalRows * 100) : 0
|
||||
console.log(JSON.stringify({ stage: 'snapshot', processedRows, totalRows, progress }))
|
||||
},
|
||||
})
|
||||
console.log(JSON.stringify({ stage: 'snapshot-complete', manifest }, null, 2))
|
||||
} finally {
|
||||
await Promise.allSettled([
|
||||
prisma.$disconnect(),
|
||||
freightConfigRepository.close(),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((error) => {
|
||||
console.error(`限价快照生成失败:${error instanceof Error ? error.message : String(error)}`)
|
||||
process.exitCode = 1
|
||||
})
|
||||
@ -19,8 +19,25 @@ async function main() {
|
||||
const { modularProductProcessingOrdersService } = await import(
|
||||
'@/server/services/modular-product-processing-orders.service'
|
||||
)
|
||||
const result = await modularProductProcessingOrdersService.generateDailySnapshot(dateArg)
|
||||
console.log(JSON.stringify({ snapshot: result }))
|
||||
const [
|
||||
{ prisma },
|
||||
{ closeModularProcessingUpstreamPools },
|
||||
{ closeSupplyChainArchiveReadPool },
|
||||
] = await Promise.all([
|
||||
import('@/lib/prisma'),
|
||||
import('@/server/repositories/modular-processing-upstream.repository'),
|
||||
import('@/server/repositories/supply-chain-archive-read-pool'),
|
||||
])
|
||||
try {
|
||||
const result = await modularProductProcessingOrdersService.generateDailySnapshot(dateArg)
|
||||
console.log(JSON.stringify({ snapshot: result }))
|
||||
} finally {
|
||||
await Promise.allSettled([
|
||||
prisma.$disconnect(),
|
||||
closeModularProcessingUpstreamPools(),
|
||||
closeSupplyChainArchiveReadPool(),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
void main().catch((error) => {
|
||||
|
||||
@ -60,13 +60,24 @@ async function main() {
|
||||
const sheet = workbook.Sheets['清货池']
|
||||
if (!sheet) throw new Error('Excel 中缺少「清货池」工作表')
|
||||
const excelRows = XLSX.utils.sheet_to_json<Record<string, unknown>>(sheet, { defval: '' })
|
||||
const backupSheet = workbook.Sheets['备份']
|
||||
const backupRows = backupSheet
|
||||
? XLSX.utils.sheet_to_json<Record<string, unknown>>(backupSheet, { defval: '' })
|
||||
: []
|
||||
const overlayByBarcode = new Map<string, Record<string, unknown>>()
|
||||
const backupByBarcode = new Map<string, Record<string, unknown>>()
|
||||
for (const row of excelRows) {
|
||||
for (const key of ['商品编码', '清货商品编码']) {
|
||||
const barcode = text(row[key]).toUpperCase()
|
||||
if (barcode) overlayByBarcode.set(barcode, row)
|
||||
}
|
||||
}
|
||||
for (const row of backupRows) {
|
||||
for (const key of ['商品编码', '清货商品编码']) {
|
||||
const barcode = text(row[key]).toUpperCase()
|
||||
if (barcode && !backupByBarcode.has(barcode)) backupByBarcode.set(barcode, row)
|
||||
}
|
||||
}
|
||||
|
||||
const previousSnapshot = await prisma.supplyChainClearancePoolSnapshot.findFirst({
|
||||
orderBy: [{ statDate: 'desc' }, { statTime: 'desc' }],
|
||||
@ -127,6 +138,7 @@ async function main() {
|
||||
const data = archiveRows.map((row) => {
|
||||
const barcode = row.barcode.trim().toUpperCase()
|
||||
const overlay = overlayByBarcode.get(barcode)
|
||||
const backup = backupByBarcode.get(barcode)
|
||||
if (overlay) overlayMatched += 1
|
||||
const previousStock = previousStockByBarcode.get(barcode)
|
||||
const stockJianshang = numberValue(overlay?.['简尚总库存(优先清货)'])
|
||||
@ -195,6 +207,12 @@ async function main() {
|
||||
profitCenter: text(overlay?.['利润中心']),
|
||||
fixedTaskQty: numberValue(overlay?.['固定任务量']),
|
||||
specialInboundRemaining: numberValue(overlay?.['特殊入库剩余量']),
|
||||
stockUsableDaysScore: numberValue(backup?.['库存可使用天数分数']),
|
||||
dailySalesScore: numberValue(backup?.['日销分数']),
|
||||
inboundDaysScore: numberValue(backup?.['入库天数分数']),
|
||||
totalScore: numberValue(backup?.['总分数']),
|
||||
suggestedDiscount: numberValue(backup?.['建议折扣']),
|
||||
previousMonthDiscount: text(backup?.['上月折扣']),
|
||||
applicant: text(overlay?.['流程申请人']),
|
||||
ownerProfitCenter: text(overlay?.['所属利润中心']),
|
||||
inventoryInboundAt: text(overlay?.['库存入库时间']),
|
||||
|
||||
@ -0,0 +1,148 @@
|
||||
import { basename, resolve } from 'node:path'
|
||||
import ExcelJS from 'exceljs'
|
||||
import { loadEnvConfig } from '@next/env'
|
||||
|
||||
loadEnvConfig(process.cwd())
|
||||
|
||||
let prisma: typeof import('../src/lib/prisma').prisma | undefined
|
||||
|
||||
function argument(name: string) {
|
||||
const prefix = `--${name}=`
|
||||
return process.argv.find((value) => value.startsWith(prefix))?.slice(prefix.length)
|
||||
}
|
||||
|
||||
function cellText(value: ExcelJS.CellValue) {
|
||||
if (value == null) return ''
|
||||
if (value instanceof Date) return value.toISOString().slice(0, 7)
|
||||
if (typeof value === 'object') {
|
||||
if ('text' in value && typeof value.text === 'string') return value.text.trim()
|
||||
if ('result' in value && value.result != null) return String(value.result).trim()
|
||||
throw new Error('单元格包含公式或富文本,请先粘贴为值')
|
||||
}
|
||||
return String(value).trim()
|
||||
}
|
||||
|
||||
function periodOf(value: ExcelJS.CellValue) {
|
||||
if (value instanceof Date) return value.toISOString().slice(0, 7)
|
||||
if (typeof value === 'number') {
|
||||
return new Date(Date.UTC(1899, 11, 30) + Math.trunc(value) * 86_400_000)
|
||||
.toISOString()
|
||||
.slice(0, 7)
|
||||
}
|
||||
const text = cellText(value)
|
||||
if (/^\d{4}-(0[1-9]|1[0-2])$/.test(text)) return text
|
||||
if (/^\d{5}$/.test(text)) {
|
||||
return new Date(Date.UTC(1899, 11, 30) + Number(text) * 86_400_000)
|
||||
.toISOString()
|
||||
.slice(0, 7)
|
||||
}
|
||||
throw new Error(`统计月格式不正确:${text || '空值'}`)
|
||||
}
|
||||
|
||||
async function readWorkbook(filePath: string) {
|
||||
const workbook = new ExcelJS.Workbook()
|
||||
await workbook.xlsx.readFile(filePath)
|
||||
const worksheet = workbook.getWorksheet('Sheet1') ?? workbook.worksheets[0]
|
||||
if (!worksheet) throw new Error('Excel 中没有可读取的工作表')
|
||||
|
||||
const headers = new Map<string, number>()
|
||||
worksheet.getRow(1).eachCell((cell, columnNumber) => {
|
||||
headers.set(cellText(cell.value), columnNumber)
|
||||
})
|
||||
for (const required of ['统计月', '条码', '清货价']) {
|
||||
if (!headers.has(required)) throw new Error(`Excel 缺少“${required}”列`)
|
||||
}
|
||||
const periodColumn = headers.get('统计月')!
|
||||
const barcodeColumn = headers.get('条码')!
|
||||
const priceColumn = headers.get('清货价')!
|
||||
const rows = new Map<string, { periodKey: string; productCode: string; clearancePrice: number }>()
|
||||
worksheet.eachRow((row, rowNumber) => {
|
||||
if (rowNumber === 1) return
|
||||
const periodKey = periodOf(row.getCell(periodColumn).value)
|
||||
const productCode = cellText(row.getCell(barcodeColumn).value).toUpperCase()
|
||||
const priceText = cellText(row.getCell(priceColumn).value)
|
||||
if (!productCode && !priceText) return
|
||||
if (!productCode) throw new Error(`第 ${rowNumber} 行条码为空`)
|
||||
if (!priceText) return
|
||||
const clearancePrice = Number(priceText)
|
||||
if (!Number.isFinite(clearancePrice) || clearancePrice <= 0) {
|
||||
throw new Error(`第 ${rowNumber} 行清货价必须为正数`)
|
||||
}
|
||||
const current = rows.get(productCode)
|
||||
if (!current || clearancePrice < current.clearancePrice) {
|
||||
rows.set(productCode, { periodKey, productCode, clearancePrice })
|
||||
}
|
||||
})
|
||||
return Array.from(rows.values())
|
||||
}
|
||||
|
||||
async function main() {
|
||||
prisma = (await import('../src/lib/prisma')).prisma
|
||||
const filePath = resolve(argument('file')?.trim() || '')
|
||||
if (!argument('file')) throw new Error('请通过 --file 指定 Excel 文件')
|
||||
const parsedRows = await readWorkbook(filePath)
|
||||
const periods = Array.from(new Set(parsedRows.map((row) => row.periodKey)))
|
||||
if (periods.length !== 1) throw new Error('Excel 必须只包含一个统计月')
|
||||
const periodKey = argument('period')?.trim() || periods[0]
|
||||
if (periodKey !== periods[0]) throw new Error(`Excel 统计月为 ${periods[0]},与 --period=${periodKey} 不一致`)
|
||||
|
||||
const taskCodes = new Set(
|
||||
(await prisma.supplyChainClearanceTask.findMany({
|
||||
where: { periodKey, status: 'APPROVED' },
|
||||
select: { barcode: true },
|
||||
})).map((task) => task.barcode.trim().toUpperCase()),
|
||||
)
|
||||
const rows = parsedRows.filter((row) => !taskCodes.has(row.productCode))
|
||||
const archives = await prisma.supplyChainProductArchive.findMany({
|
||||
where: { barcode: { in: rows.map((row) => row.productCode) } },
|
||||
select: { barcode: true, costPrice: true },
|
||||
})
|
||||
const costByBarcode = new Map(
|
||||
archives.map((archive) => [archive.barcode.trim().toUpperCase(), Number(archive.costPrice)]),
|
||||
)
|
||||
const data = rows.map((row) => {
|
||||
const costPrice = costByBarcode.get(row.productCode)
|
||||
if (!costPrice || !Number.isFinite(costPrice)) {
|
||||
throw new Error(`人工清货价编码 ${row.productCode} 无法匹配成本价`)
|
||||
}
|
||||
return {
|
||||
periodKey,
|
||||
productCode: row.productCode,
|
||||
clearancePrice: row.clearancePrice,
|
||||
costPrice,
|
||||
}
|
||||
})
|
||||
|
||||
const apply = process.argv.includes('--apply')
|
||||
if (apply && argument('confirm') !== 'import-clearance-price-manual') {
|
||||
throw new Error('正式导入必须同时传入 --confirm=import-clearance-price-manual')
|
||||
}
|
||||
if (apply) {
|
||||
await prisma.$transaction(async (tx) => {
|
||||
await tx.supplyChainClearancePricePushManualItem.deleteMany({ where: { periodKey } })
|
||||
for (let start = 0; start < data.length; start += 500) {
|
||||
await tx.supplyChainClearancePricePushManualItem.createMany({
|
||||
data: data.slice(start, start + 500),
|
||||
})
|
||||
}
|
||||
}, { maxWait: 10_000, timeout: 120_000 })
|
||||
}
|
||||
|
||||
console.log(JSON.stringify({
|
||||
file: basename(filePath),
|
||||
periodKey,
|
||||
apply,
|
||||
sourcePricedCodes: parsedRows.length,
|
||||
excludedTaskCodes: parsedRows.length - rows.length,
|
||||
manualPriceCodes: data.length,
|
||||
}, null, 2))
|
||||
}
|
||||
|
||||
void main()
|
||||
.catch((error) => {
|
||||
console.error(error instanceof Error ? error.message : String(error))
|
||||
process.exitCode = 1
|
||||
})
|
||||
.finally(async () => {
|
||||
await prisma?.$disconnect()
|
||||
})
|
||||
@ -4,6 +4,7 @@ import sql from 'mssql'
|
||||
import { Client } from 'pg'
|
||||
import {
|
||||
runDiscontinuationLinkImportTransaction,
|
||||
shouldExcludeDedicatedWarehouseLink,
|
||||
type DiscontinuationLinkImportStats,
|
||||
} from '@/server/services/supply-chain-discontinuation-link-import'
|
||||
|
||||
@ -21,6 +22,7 @@ function code(value: unknown) {
|
||||
type NoticeRecord = {
|
||||
id: string
|
||||
barcode: string
|
||||
specialWarehouseNote: string
|
||||
}
|
||||
|
||||
type OwnerRecord = {
|
||||
@ -126,13 +128,13 @@ async function main() {
|
||||
const statTime = new Date()
|
||||
try {
|
||||
const noticeRows = (await db.query<NoticeRecord>(
|
||||
`SELECT id, barcode
|
||||
`SELECT id, barcode, "specialWarehouseNote"
|
||||
FROM "SupplyChainDiscontinuationNotice"
|
||||
WHERE "statDate" = $1::date`,
|
||||
[statDate],
|
||||
)).rows
|
||||
const noticeByCode = new Map(
|
||||
noticeRows.map((row) => [code(row.barcode), row.id]),
|
||||
noticeRows.map((row) => [code(row.barcode), row]),
|
||||
)
|
||||
if (!noticeByCode.size) throw new Error(`${statDate} 没有通知下架编码`)
|
||||
|
||||
@ -154,23 +156,31 @@ async function main() {
|
||||
let rawRows = 0
|
||||
let skippedWithoutNotice = 0
|
||||
let skippedNotOnline = 0
|
||||
let skippedByDedicatedWarehouse = 0
|
||||
let writtenRows = 0
|
||||
|
||||
const insertBatch = async (rows: Array<Record<string, string>>) => {
|
||||
if (!rows.length) return
|
||||
const columns = 16
|
||||
const values: unknown[] = []
|
||||
const tuples = rows.map((row, rowIndex) => {
|
||||
const noticeId = noticeByCode.get(code(row.system_sku_code))
|
||||
if (!noticeId) return ''
|
||||
const tuples = rows.map((row) => {
|
||||
const notice = noticeByCode.get(code(row.system_sku_code))
|
||||
if (!notice) return ''
|
||||
const shopId = String(row.shop_id ?? '').trim()
|
||||
const itemId = code(row.product_id)
|
||||
const shopSkuCode = code(row.shop_sku_code)
|
||||
const owner = ownerByItemId.get(itemId)
|
||||
const offset = rowIndex * columns
|
||||
if (shouldExcludeDedicatedWarehouseLink(
|
||||
notice.specialWarehouseNote,
|
||||
owner?.profitCenter,
|
||||
)) {
|
||||
skippedByDedicatedWarehouse += 1
|
||||
return ''
|
||||
}
|
||||
const offset = values.length
|
||||
values.push(
|
||||
crypto.randomUUID(),
|
||||
noticeId,
|
||||
notice.id,
|
||||
`|${code(row.system_sku_code)}|`,
|
||||
shopId,
|
||||
itemId,
|
||||
@ -215,7 +225,7 @@ async function main() {
|
||||
writtenRows += tuples.length
|
||||
}
|
||||
|
||||
await runDiscontinuationLinkImportTransaction({
|
||||
const storedStats = await runDiscontinuationLinkImportTransaction({
|
||||
begin: () => db.query('BEGIN'),
|
||||
deleteExisting: () => db.query(
|
||||
`DELETE FROM "SupplyChainDiscontinuationNoticeLink"
|
||||
@ -269,39 +279,73 @@ async function main() {
|
||||
writtenRows,
|
||||
skippedWithoutNotice,
|
||||
skippedNotOnline,
|
||||
skippedByDedicatedWarehouse,
|
||||
allowZeroLinks,
|
||||
} satisfies DiscontinuationLinkImportStats
|
||||
})
|
||||
|
||||
const blankItems = (await db.query<{ itemId: string }>(
|
||||
`SELECT DISTINCT l."itemId"
|
||||
FROM "SupplyChainDiscontinuationNoticeLink" l
|
||||
INNER JOIN "SupplyChainDiscontinuationNotice" n ON n.id = l."noticeId"
|
||||
WHERE n."statDate" = $1::date
|
||||
AND l."ownerName" = ''`,
|
||||
[statDate],
|
||||
)).rows.map((row) => code(row.itemId)).filter(Boolean)
|
||||
const remoteOwners = await loadRemoteOwners(blankItems)
|
||||
for (const owner of remoteOwners.values()) {
|
||||
await db.query(
|
||||
`UPDATE "SupplyChainDiscontinuationNoticeLink" l
|
||||
SET "ownerName" = $1,
|
||||
"deptHead" = $2,
|
||||
"profitCenter" = $3
|
||||
FROM "SupplyChainDiscontinuationNotice" n
|
||||
WHERE l."noticeId" = n.id
|
||||
AND n."statDate" = $4::date
|
||||
AND l."itemId" = $5
|
||||
}, async (initialStats) => {
|
||||
const blankItems = (await db.query<{ itemId: string }>(
|
||||
`SELECT DISTINCT l."itemId"
|
||||
FROM "SupplyChainDiscontinuationNoticeLink" l
|
||||
INNER JOIN "SupplyChainDiscontinuationNotice" n ON n.id = l."noticeId"
|
||||
WHERE n."statDate" = $1::date
|
||||
AND l."ownerName" = ''`,
|
||||
[
|
||||
owner.ownerName,
|
||||
owner.deptHead,
|
||||
owner.profitCenter,
|
||||
statDate,
|
||||
owner.itemId,
|
||||
],
|
||||
)
|
||||
}
|
||||
[statDate],
|
||||
)).rows.map((row) => code(row.itemId)).filter(Boolean)
|
||||
const remoteOwners = await loadRemoteOwners(blankItems)
|
||||
for (const owner of remoteOwners.values()) {
|
||||
await db.query(
|
||||
`UPDATE "SupplyChainDiscontinuationNoticeLink" l
|
||||
SET "ownerName" = $1,
|
||||
"deptHead" = $2,
|
||||
"profitCenter" = $3
|
||||
FROM "SupplyChainDiscontinuationNotice" n
|
||||
WHERE l."noticeId" = n.id
|
||||
AND n."statDate" = $4::date
|
||||
AND l."itemId" = $5
|
||||
AND l."ownerName" = ''`,
|
||||
[
|
||||
owner.ownerName,
|
||||
owner.deptHead,
|
||||
owner.profitCenter,
|
||||
statDate,
|
||||
owner.itemId,
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
const dedicatedWarehouseLinks = (await db.query<{
|
||||
id: string
|
||||
specialWarehouseNote: string
|
||||
profitCenter: string
|
||||
}>(
|
||||
`SELECT l.id, n."specialWarehouseNote", l."profitCenter"
|
||||
FROM "SupplyChainDiscontinuationNoticeLink" l
|
||||
INNER JOIN "SupplyChainDiscontinuationNotice" n ON n.id = l."noticeId"
|
||||
WHERE n."statDate" = $1::date
|
||||
AND n."specialWarehouseNote" <> ''
|
||||
AND l."profitCenter" <> ''`,
|
||||
[statDate],
|
||||
)).rows
|
||||
const excludedIds = dedicatedWarehouseLinks
|
||||
.filter((row) => shouldExcludeDedicatedWarehouseLink(
|
||||
row.specialWarehouseNote,
|
||||
row.profitCenter,
|
||||
))
|
||||
.map((row) => row.id)
|
||||
for (const ids of chunk(excludedIds, 1_000)) {
|
||||
await db.query(
|
||||
`DELETE FROM "SupplyChainDiscontinuationNoticeLink"
|
||||
WHERE id = ANY($1::text[])`,
|
||||
[ids],
|
||||
)
|
||||
}
|
||||
return {
|
||||
...initialStats,
|
||||
writtenRows: Math.max(0, initialStats.writtenRows - excludedIds.length),
|
||||
skippedByDedicatedWarehouse:
|
||||
(initialStats.skippedByDedicatedWarehouse ?? 0) + excludedIds.length,
|
||||
} satisfies DiscontinuationLinkImportStats
|
||||
})
|
||||
|
||||
const stored = await db.query<{ count: number }>(
|
||||
`SELECT COUNT(*)::int AS count
|
||||
@ -315,7 +359,8 @@ async function main() {
|
||||
rawRows,
|
||||
skippedWithoutNotice,
|
||||
skippedNotOnline,
|
||||
writtenRows,
|
||||
skippedByDedicatedWarehouse: storedStats.skippedByDedicatedWarehouse ?? 0,
|
||||
writtenRows: storedStats.writtenRows,
|
||||
storedRows: stored.rows[0]?.count ?? 0,
|
||||
allowZeroLinks,
|
||||
}, null, 2))
|
||||
|
||||
@ -0,0 +1,96 @@
|
||||
#!/usr/bin/env node
|
||||
import fs from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import XLSX from 'xlsx'
|
||||
|
||||
const inputPath = process.argv[2]?.trim()
|
||||
if (!inputPath) throw new Error('请传入各省发货占比 Excel 路径')
|
||||
|
||||
const workbook = XLSX.readFile(path.resolve(inputPath), { cellDates: false })
|
||||
const sheetName = workbook.SheetNames[0]
|
||||
const worksheet = workbook.Sheets[sheetName]
|
||||
const rawRows = XLSX.utils.sheet_to_json(worksheet, { header: 1, defval: null })
|
||||
const headers = rawRows[0]?.map((value) => String(value ?? '').trim()) ?? []
|
||||
const expectedHeaders = ['省份', '发货量', '总量', '发货占比', '区域', '区域占比']
|
||||
if (expectedHeaders.some((header, index) => headers[index] !== header)) {
|
||||
throw new Error(`Excel 表头不符合预期:${expectedHeaders.join('、')}`)
|
||||
}
|
||||
|
||||
const rows = rawRows
|
||||
.slice(1)
|
||||
.filter((row) => String(row[0] ?? '').trim())
|
||||
.map((row) => {
|
||||
const rawRegion = String(row[4] ?? '').trim()
|
||||
return {
|
||||
province: String(row[0]).trim(),
|
||||
shipmentCount: Number(row[1]),
|
||||
totalShipment: Number(row[2]),
|
||||
shipmentShare: Number(row[3]),
|
||||
region: !rawRegion || rawRegion.startsWith('#') ? '未分区' : rawRegion,
|
||||
regionShare: row[5] === null || row[5] === undefined || row[5] === ''
|
||||
? null
|
||||
: Number(row[5]),
|
||||
}
|
||||
})
|
||||
|
||||
if (!rows.length) throw new Error('Excel 中没有省份数据')
|
||||
const totalShipments = rows.reduce((sum, row) => sum + row.shipmentCount, 0)
|
||||
const totalShares = rows.reduce((sum, row) => sum + row.shipmentShare, 0)
|
||||
const declaredTotals = new Set(rows.map((row) => row.totalShipment))
|
||||
if (declaredTotals.size !== 1 || !rows.every((row) => Number.isFinite(row.shipmentCount))) {
|
||||
throw new Error('各省明细的总量或发货量不一致')
|
||||
}
|
||||
const declaredTotal = [...declaredTotals][0]
|
||||
if (totalShipments !== declaredTotal || Math.abs(totalShares - 1) > 0.000001) {
|
||||
throw new Error(`明细校验失败:发货量 ${totalShipments},占比 ${totalShares}`)
|
||||
}
|
||||
|
||||
const regionShipmentTotals = new Map()
|
||||
const declaredRegionShares = new Map()
|
||||
for (const row of rows) {
|
||||
regionShipmentTotals.set(
|
||||
row.region,
|
||||
(regionShipmentTotals.get(row.region) ?? 0) + row.shipmentCount,
|
||||
)
|
||||
if (row.regionShare === null) continue
|
||||
if (!Number.isFinite(row.regionShare) || row.regionShare < 0 || row.regionShare > 1) {
|
||||
throw new Error(`区域占比不正确:${row.region} ${row.regionShare}`)
|
||||
}
|
||||
const existing = declaredRegionShares.get(row.region)
|
||||
if (existing !== undefined && Math.abs(existing - row.regionShare) > 0.0005) {
|
||||
throw new Error(`同一区域存在不同占比:${row.region} ${existing} / ${row.regionShare}`)
|
||||
}
|
||||
declaredRegionShares.set(row.region, row.regionShare)
|
||||
}
|
||||
|
||||
for (const row of rows) {
|
||||
row.regionShare = declaredRegionShares.get(row.region)
|
||||
?? (regionShipmentTotals.get(row.region) ?? 0) / declaredTotal
|
||||
}
|
||||
|
||||
const snapshot = {
|
||||
version: 1,
|
||||
generatedAt: new Date().toISOString(),
|
||||
sourceFile: path.basename(inputPath),
|
||||
totalShipment: declaredTotal,
|
||||
rowCount: rows.length,
|
||||
rows,
|
||||
}
|
||||
|
||||
const outputPath = path.join(
|
||||
process.cwd(),
|
||||
'data',
|
||||
'freight-config',
|
||||
'province-shipment-share.json',
|
||||
)
|
||||
await fs.mkdir(path.dirname(outputPath), { recursive: true })
|
||||
const temporaryPath = `${outputPath}.tmp`
|
||||
await fs.writeFile(temporaryPath, `${JSON.stringify(snapshot, null, 2)}\n`, 'utf8')
|
||||
await fs.rename(temporaryPath, outputPath)
|
||||
|
||||
console.log(JSON.stringify({
|
||||
outputPath,
|
||||
rowCount: snapshot.rowCount,
|
||||
totalShipment: snapshot.totalShipment,
|
||||
totalShare: totalShares,
|
||||
}, null, 2))
|
||||
90
furniture-3d-system/scripts/install-jst-logistics-daily-sync.sh
Executable file
@ -0,0 +1,90 @@
|
||||
#!/usr/bin/env bash
|
||||
# 安装/卸载聚水潭物流匹配规则每日快照任务(macOS launchd)。
|
||||
# 默认每天 09:00(Asia/Shanghai)执行;当天已成功生成快照时自动跳过。
|
||||
set -euo pipefail
|
||||
|
||||
BASE="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
LABEL="${JST_LOGISTICS_TIMER_LABEL:-com.furniture3d.jst-logistics-daily}"
|
||||
PLIST="$HOME/Library/LaunchAgents/$LABEL.plist"
|
||||
LOG="$HOME/Library/Logs/furniture3d-jst-logistics-daily.log"
|
||||
HOUR="${JST_LOGISTICS_TIMER_HOUR:-9}"
|
||||
MINUTE="${JST_LOGISTICS_TIMER_MINUTE:-0}"
|
||||
TSX_CLI="$BASE/node_modules/tsx/dist/cli.mjs"
|
||||
SYNC_SCRIPT="$BASE/scripts/sync-jst-logistics-rules.ts"
|
||||
|
||||
NODE_BIN="${NODE_BIN:-}"
|
||||
if [ -z "$NODE_BIN" ]; then
|
||||
if command -v node >/dev/null 2>&1; then
|
||||
NODE_BIN="$(command -v node)"
|
||||
elif [ -x "$HOME/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/bin/node" ]; then
|
||||
NODE_BIN="$HOME/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/bin/node"
|
||||
else
|
||||
echo "找不到 node,请安装 Node.js 或设置 NODE_BIN。" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ ! -f "$TSX_CLI" ]; then
|
||||
echo "找不到 tsx:$TSX_CLI" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "${1:-}" = "--uninstall" ]; then
|
||||
launchctl bootout "gui/$(id -u)/$LABEL" 2>/dev/null || launchctl unload "$PLIST" 2>/dev/null || true
|
||||
rm -f "$PLIST"
|
||||
echo "已卸载 $LABEL"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
mkdir -p "$HOME/Library/LaunchAgents" "$HOME/Library/Logs"
|
||||
|
||||
cat >"$PLIST" <<PLIST_EOF
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<string>$LABEL</string>
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>$NODE_BIN</string>
|
||||
<string>$TSX_CLI</string>
|
||||
<string>$SYNC_SCRIPT</string>
|
||||
</array>
|
||||
<key>WorkingDirectory</key>
|
||||
<string>$BASE</string>
|
||||
<key>EnvironmentVariables</key>
|
||||
<dict>
|
||||
<key>PATH</key>
|
||||
<string>$(dirname "$NODE_BIN"):/usr/local/bin:/usr/bin:/bin</string>
|
||||
</dict>
|
||||
<key>StartCalendarInterval</key>
|
||||
<dict>
|
||||
<key>Hour</key>
|
||||
<integer>$HOUR</integer>
|
||||
<key>Minute</key>
|
||||
<integer>$MINUTE</integer>
|
||||
</dict>
|
||||
<key>StandardOutPath</key>
|
||||
<string>$LOG</string>
|
||||
<key>StandardErrorPath</key>
|
||||
<string>$LOG</string>
|
||||
<key>RunAtLoad</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
PLIST_EOF
|
||||
|
||||
chmod 600 "$PLIST"
|
||||
launchctl bootout "gui/$(id -u)/$LABEL" 2>/dev/null || true
|
||||
launchctl bootstrap "gui/$(id -u)" "$PLIST" 2>/dev/null || launchctl load "$PLIST"
|
||||
|
||||
if launchctl list | grep -q "$LABEL"; then
|
||||
echo "已安装 ${LABEL}:每天 $(printf '%02d' "$HOUR"):$(printf '%02d' "$MINUTE")(Asia/Shanghai)执行"
|
||||
echo "日志:${LOG}"
|
||||
echo "手动触发:launchctl kickstart -k gui/$(id -u)/$LABEL"
|
||||
echo "强制刷新当天快照:npm run sync:jst-logistics-rules -- --force"
|
||||
else
|
||||
echo "安装失败,请检查 $PLIST" >&2
|
||||
exit 1
|
||||
fi
|
||||
100
furniture-3d-system/scripts/install-jst-logistics-systemd-timer.sh
Executable file
@ -0,0 +1,100 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
BASE="${FURNITURE_PROD_BASE:-/opt/furniture-3d-system-prod}"
|
||||
APP_ROOT="$BASE/current/furniture-3d-system"
|
||||
APP_ENV="$BASE/shared/.env"
|
||||
CONFIG_DIR="$BASE/shared/freight-config"
|
||||
SERVICE_NAME="${JST_LOGISTICS_SERVICE_NAME:-furniture-jst-logistics-snapshot}"
|
||||
SERVICE_FILE="/etc/systemd/system/$SERVICE_NAME.service"
|
||||
TIMER_FILE="/etc/systemd/system/$SERVICE_NAME.timer"
|
||||
CALENDAR="${JST_LOGISTICS_CALENDAR:-*-*-* 09:30:00 Asia/Shanghai}"
|
||||
SOURCE_DIR="$(cd "$(dirname "$0")/.." && pwd)/deploy/systemd"
|
||||
|
||||
if [ "${1:-}" = "--uninstall" ]; then
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
echo "This uninstaller requires root." >&2
|
||||
exit 1
|
||||
fi
|
||||
systemctl disable --now "$SERVICE_NAME.timer" 2>/dev/null || true
|
||||
rm -f "$SERVICE_FILE" "$TIMER_FILE"
|
||||
systemctl daemon-reload
|
||||
echo "已卸载 $SERVICE_NAME"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
echo "This installer requires root." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ ! -d "$APP_ROOT" ]; then
|
||||
echo "Missing app root: $APP_ROOT" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ ! -f "$APP_ENV" ]; then
|
||||
echo "Missing production env file: $APP_ENV" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ ! -f "$APP_ROOT/node_modules/tsx/dist/cli.mjs" ]; then
|
||||
echo "Missing tsx runtime under $APP_ROOT/node_modules" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ ! -f "$SOURCE_DIR/furniture-jst-logistics-snapshot.service" ] \
|
||||
|| [ ! -f "$SOURCE_DIR/furniture-jst-logistics-snapshot.timer" ]; then
|
||||
echo "Missing systemd templates under $SOURCE_DIR" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
CONFIGURED_DIR="$(sed -n 's/^FREIGHT_CONFIG_DATA_DIR=//p' "$APP_ENV" | tail -n 1)"
|
||||
if [ -n "$CONFIGURED_DIR" ]; then
|
||||
CONFIG_DIR="$CONFIGURED_DIR"
|
||||
fi
|
||||
case "$CONFIG_DIR" in
|
||||
/*) ;;
|
||||
*)
|
||||
echo "FREIGHT_CONFIG_DATA_DIR must be an absolute path: $CONFIG_DIR" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
mkdir -p "$CONFIG_DIR"
|
||||
for file in warehouse-express-rules.json province-shipment-share.json; do
|
||||
if [ ! -f "$CONFIG_DIR/$file" ] && [ -f "$APP_ROOT/data/freight-config/$file" ]; then
|
||||
install -m 0644 "$APP_ROOT/data/freight-config/$file" "$CONFIG_DIR/$file"
|
||||
fi
|
||||
done
|
||||
|
||||
ENV_CHANGED=0
|
||||
if ! grep -q '^FREIGHT_CONFIG_DATA_DIR=' "$APP_ENV"; then
|
||||
{
|
||||
printf '\n# Persistent freight snapshots shared across releases.\n'
|
||||
printf 'FREIGHT_CONFIG_DATA_DIR=%s\n' "$CONFIG_DIR"
|
||||
} >>"$APP_ENV"
|
||||
ENV_CHANGED=1
|
||||
elif [ -z "$CONFIGURED_DIR" ]; then
|
||||
sed -i "s|^FREIGHT_CONFIG_DATA_DIR=.*|FREIGHT_CONFIG_DATA_DIR=$CONFIG_DIR|" "$APP_ENV"
|
||||
ENV_CHANGED=1
|
||||
fi
|
||||
|
||||
sed \
|
||||
-e "s|/opt/furniture-3d-system-prod/shared/freight-config|$CONFIG_DIR|g" \
|
||||
-e "s|/opt/furniture-3d-system-prod|$BASE|g" \
|
||||
"$SOURCE_DIR/furniture-jst-logistics-snapshot.service" \
|
||||
>"$SERVICE_FILE"
|
||||
sed "s|^OnCalendar=.*|OnCalendar=$CALENDAR|" \
|
||||
"$SOURCE_DIR/furniture-jst-logistics-snapshot.timer" \
|
||||
>"$TIMER_FILE"
|
||||
chmod 0644 "$SERVICE_FILE" "$TIMER_FILE"
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl enable --now "$SERVICE_NAME.timer"
|
||||
systemctl list-timers --all "$SERVICE_NAME.timer" --no-pager
|
||||
|
||||
echo "快照目录:$CONFIG_DIR"
|
||||
echo "调度时间:$CALENDAR"
|
||||
if [ "$ENV_CHANGED" -eq 1 ]; then
|
||||
echo "已写入 $APP_ENV;请重启正式应用进程,使页面读取 shared 快照目录。" >&2
|
||||
fi
|
||||
if ! curl --fail --silent --show-error http://127.0.0.1:9222/json/version >/dev/null; then
|
||||
echo "警告:当前 127.0.0.1:9222 不可用,请确认 furniture-jst-rpa-browser 已启动并保持登录。" >&2
|
||||
fi
|
||||
111
furniture-3d-system/scripts/install-limit-price-snapshot-systemd-timer.sh
Executable file
@ -0,0 +1,111 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
BASE="${FURNITURE_PROD_BASE:-/opt/furniture-3d-system-prod}"
|
||||
APP_ROOT="$BASE/current/furniture-3d-system"
|
||||
APP_ENV="$BASE/shared/.env"
|
||||
SNAPSHOT_DIR="$BASE/shared/limit-price-snapshots"
|
||||
SERVICE_NAME="${LIMIT_PRICE_SNAPSHOT_SERVICE_NAME:-furniture-limit-price-snapshot}"
|
||||
SERVICE_FILE="/etc/systemd/system/$SERVICE_NAME.service"
|
||||
TIMER_FILE="/etc/systemd/system/$SERVICE_NAME.timer"
|
||||
CALENDAR="${LIMIT_PRICE_SNAPSHOT_CALENDAR:-*-*-* 09:30:00 Asia/Shanghai}"
|
||||
SOURCE_DIR="$(cd "$(dirname "$0")/.." && pwd)/deploy/systemd"
|
||||
|
||||
if [ "${1:-}" = "--uninstall" ]; then
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
echo "This uninstaller requires root." >&2
|
||||
exit 1
|
||||
fi
|
||||
systemctl disable --now "$SERVICE_NAME.timer" 2>/dev/null || true
|
||||
rm -f "$SERVICE_FILE" "$TIMER_FILE"
|
||||
systemctl daemon-reload
|
||||
echo "已卸载 $SERVICE_NAME"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
echo "This installer requires root." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ ! -d "$APP_ROOT" ]; then
|
||||
echo "Missing app root: $APP_ROOT" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ ! -f "$APP_ENV" ]; then
|
||||
echo "Missing production env file: $APP_ENV" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ ! -f "$APP_ROOT/node_modules/tsx/dist/cli.mjs" ]; then
|
||||
echo "Missing tsx runtime under $APP_ROOT/node_modules" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ ! -f "$SOURCE_DIR/furniture-limit-price-snapshot.service" ] \
|
||||
|| [ ! -f "$SOURCE_DIR/furniture-limit-price-snapshot.timer" ]; then
|
||||
echo "Missing systemd templates under $SOURCE_DIR" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for key in \
|
||||
DATABASE_URL \
|
||||
PROD_ARCHIVE_DB_HOST PROD_ARCHIVE_DB_NAME PROD_ARCHIVE_DB_USER PROD_ARCHIVE_DB_PASSWORD \
|
||||
JST_REPORT_DB_HOST JST_REPORT_DB_NAME JST_REPORT_DB_USER JST_REPORT_DB_PASSWORD; do
|
||||
if ! grep -Eq "^${key}=.+$" "$APP_ENV"; then
|
||||
echo "Missing $key in $APP_ENV" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
CONFIGURED_DIR="$(sed -n 's/^SUPPLY_CHAIN_LIMIT_SNAPSHOT_DIR=//p' "$APP_ENV" | tail -n 1)"
|
||||
if [ -n "$CONFIGURED_DIR" ]; then
|
||||
SNAPSHOT_DIR="$CONFIGURED_DIR"
|
||||
fi
|
||||
case "$SNAPSHOT_DIR" in
|
||||
/*) ;;
|
||||
*)
|
||||
echo "SUPPLY_CHAIN_LIMIT_SNAPSHOT_DIR must be an absolute path: $SNAPSHOT_DIR" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
mkdir -p "$SNAPSHOT_DIR"
|
||||
if [ -d "$APP_ROOT/data/limit-price-snapshots" ] \
|
||||
&& [ -z "$(find "$SNAPSHOT_DIR" -mindepth 1 -maxdepth 1 -print -quit)" ]; then
|
||||
cp -R "$APP_ROOT/data/limit-price-snapshots/." "$SNAPSHOT_DIR/"
|
||||
fi
|
||||
|
||||
ENV_CHANGED=0
|
||||
if ! grep -q '^SUPPLY_CHAIN_LIMIT_SNAPSHOT_DIR=' "$APP_ENV"; then
|
||||
{
|
||||
printf '\n# Persistent daily limit snapshots shared across releases.\n'
|
||||
printf 'SUPPLY_CHAIN_LIMIT_SNAPSHOT_DIR=%s\n' "$SNAPSHOT_DIR"
|
||||
} >>"$APP_ENV"
|
||||
ENV_CHANGED=1
|
||||
elif [ -z "$CONFIGURED_DIR" ]; then
|
||||
sed -i "s|^SUPPLY_CHAIN_LIMIT_SNAPSHOT_DIR=.*|SUPPLY_CHAIN_LIMIT_SNAPSHOT_DIR=$SNAPSHOT_DIR|" "$APP_ENV"
|
||||
ENV_CHANGED=1
|
||||
fi
|
||||
|
||||
sed \
|
||||
-e "s|/opt/furniture-3d-system-prod/shared/limit-price-snapshots|$SNAPSHOT_DIR|g" \
|
||||
-e "s|/opt/furniture-3d-system-prod|$BASE|g" \
|
||||
"$SOURCE_DIR/furniture-limit-price-snapshot.service" \
|
||||
>"$SERVICE_FILE"
|
||||
sed \
|
||||
-e "s|^OnCalendar=.*|OnCalendar=$CALENDAR|" \
|
||||
-e "s|Unit=furniture-limit-price-snapshot.service|Unit=$SERVICE_NAME.service|" \
|
||||
"$SOURCE_DIR/furniture-limit-price-snapshot.timer" \
|
||||
>"$TIMER_FILE"
|
||||
chmod 0644 "$SERVICE_FILE" "$TIMER_FILE"
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl enable --now "$SERVICE_NAME.timer"
|
||||
systemctl list-timers --all "$SERVICE_NAME.timer" --no-pager
|
||||
|
||||
echo "快照目录:$SNAPSHOT_DIR"
|
||||
echo "调度时间:$CALENDAR"
|
||||
if [ "$ENV_CHANGED" -eq 1 ]; then
|
||||
echo "已写入 $APP_ENV;请重启正式应用进程,使页面和下载任务读取 shared 快照目录。" >&2
|
||||
fi
|
||||
if ! curl --fail --silent --show-error http://127.0.0.1:9222/json/version >/dev/null; then
|
||||
echo "警告:当前 127.0.0.1:9222 不可用,请确认 furniture-jst-rpa-browser 已启动并保持登录。" >&2
|
||||
fi
|
||||
@ -1,10 +1,10 @@
|
||||
#!/usr/bin/env bash
|
||||
# 安装/卸载本地每日同步任务(macOS launchd)。
|
||||
# 默认每天 08:30 运行 scripts/local-sync-archive.mjs:
|
||||
# 聚水潭档案同步 → 销售日数据同步 → 正式库对齐(状态/创建时间)→ 上月 SABC 重算。
|
||||
# 默认每天 09:30 运行 scripts/run-limit-price-daily-snapshot.mjs:
|
||||
# 聚水潭档案同步 → 销售日数据同步 → 正式库对齐 → 负责人限价同步 → 限价全量快照。
|
||||
# 用法:
|
||||
# bash scripts/install-local-daily-sync.sh # 安装(每天 08:30)
|
||||
# LOCAL_SYNC_HOUR=7 LOCAL_SYNC_MINUTE=0 bash scripts/install-local-daily-sync.sh
|
||||
# bash scripts/install-local-daily-sync.sh # 安装(每天 09:30)
|
||||
# LOCAL_SYNC_HOUR=9 LOCAL_SYNC_MINUTE=30 bash scripts/install-local-daily-sync.sh
|
||||
# bash scripts/install-local-daily-sync.sh --uninstall
|
||||
set -euo pipefail
|
||||
|
||||
@ -12,7 +12,7 @@ BASE="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
LABEL="${LOCAL_SYNC_LABEL:-com.furniture3d.daily-sync}"
|
||||
PLIST="$HOME/Library/LaunchAgents/$LABEL.plist"
|
||||
LOG="$HOME/Library/Logs/furniture3d-daily-sync.log"
|
||||
HOUR="${LOCAL_SYNC_HOUR:-8}"
|
||||
HOUR="${LOCAL_SYNC_HOUR:-9}"
|
||||
MINUTE="${LOCAL_SYNC_MINUTE:-30}"
|
||||
|
||||
NODE_BIN="${NODE_BIN:-}"
|
||||
@ -46,7 +46,7 @@ cat >"$PLIST" <<PLIST_EOF
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>$NODE_BIN</string>
|
||||
<string>$BASE/scripts/local-sync-archive.mjs</string>
|
||||
<string>$BASE/scripts/run-limit-price-daily-snapshot.mjs</string>
|
||||
</array>
|
||||
<key>WorkingDirectory</key>
|
||||
<string>$BASE</string>
|
||||
|
||||
@ -0,0 +1,74 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
BASE="${FURNITURE_PROD_BASE:-/opt/furniture-3d-system-prod}"
|
||||
APP_ROOT="$BASE/current/furniture-3d-system"
|
||||
SERVICE_NAME="${MODULAR_PROCESSING_SNAPSHOT_SERVICE_NAME:-furniture-modular-processing-daily-snapshot}"
|
||||
CALENDAR="${MODULAR_PROCESSING_SNAPSHOT_CALENDAR:-*-*-* 09:30:00 Asia/Shanghai}"
|
||||
RUN_NOW="${MODULAR_PROCESSING_SNAPSHOT_RUN_NOW:-1}"
|
||||
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
echo "This installer requires root." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ ! -f "$APP_ROOT/scripts/generate-modular-processing-daily-snapshot.ts" ]; then
|
||||
echo "Missing deployed snapshot script: $APP_ROOT/scripts/generate-modular-processing-daily-snapshot.ts" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ ! -f "$BASE/shared/.env" ]; then
|
||||
echo "Missing production env file: $BASE/shared/.env" >&2
|
||||
exit 1
|
||||
fi
|
||||
if ! command -v systemctl >/dev/null 2>&1; then
|
||||
echo "This installer requires systemd (production Linux)." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cat >"/etc/systemd/system/$SERVICE_NAME.service" <<EOF
|
||||
[Unit]
|
||||
Description=Generate modular product processing orders at Beijing 09:30
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
[Service]
|
||||
Type=oneshot
|
||||
WorkingDirectory=$APP_ROOT
|
||||
Environment=NODE_ENV=production
|
||||
Environment=TZ=Asia/Shanghai
|
||||
EnvironmentFile=$BASE/shared/.env
|
||||
UMask=0077
|
||||
Nice=10
|
||||
ExecStart=/bin/bash -lc 'exec ./node_modules/.bin/tsx scripts/generate-modular-processing-daily-snapshot.ts'
|
||||
TimeoutStartSec=2h
|
||||
Restart=on-failure
|
||||
RestartSec=15min
|
||||
EOF
|
||||
|
||||
cat >"/etc/systemd/system/$SERVICE_NAME.timer" <<EOF
|
||||
[Unit]
|
||||
Description=Generate modular product processing orders daily at Beijing 09:30
|
||||
[Timer]
|
||||
OnCalendar=$CALENDAR
|
||||
Persistent=true
|
||||
AccuracySec=1min
|
||||
Unit=$SERVICE_NAME.service
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
EOF
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl enable --now "$SERVICE_NAME.timer"
|
||||
systemctl reset-failed "$SERVICE_NAME.service" 2>/dev/null || true
|
||||
|
||||
if [ "$RUN_NOW" = "1" ]; then
|
||||
echo "Running the current-day snapshot once..."
|
||||
systemctl start "$SERVICE_NAME.service"
|
||||
fi
|
||||
|
||||
echo "Timer status:"
|
||||
systemctl list-timers --all "$SERVICE_NAME.timer" --no-pager
|
||||
systemctl show "$SERVICE_NAME.service" \
|
||||
--property=Result \
|
||||
--property=ExecMainStatus \
|
||||
--property=ActiveEnterTimestamp \
|
||||
--property=InactiveEnterTimestamp \
|
||||
--no-pager
|
||||
@ -0,0 +1,51 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
BASE="${FURNITURE_PROD_BASE:-/opt/furniture-3d-system-prod}"
|
||||
APP_ROOT="$BASE/current/furniture-3d-system"
|
||||
SERVICE_NAME="${MODULAR_PROCESSING_HEALTH_SERVICE_NAME:-furniture-modular-processing-generation-health}"
|
||||
CALENDAR="${MODULAR_PROCESSING_HEALTH_CALENDAR:-*-*-* *:0/5:00}"
|
||||
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
echo "This installer requires root." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ ! -f "$APP_ROOT/scripts/check-modular-processing-generation-health.ts" ]; then
|
||||
echo "Missing deployed health-check script: $APP_ROOT/scripts/check-modular-processing-generation-health.ts" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ ! -f "$BASE/shared/.env" ]; then
|
||||
echo "Missing production env file: $BASE/shared/.env" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cat >"/etc/systemd/system/$SERVICE_NAME.service" <<EOF
|
||||
[Unit]
|
||||
Description=Check modular product processing order generation health
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
[Service]
|
||||
Type=oneshot
|
||||
WorkingDirectory=$APP_ROOT
|
||||
Environment=NODE_ENV=production
|
||||
Environment=TZ=Asia/Shanghai
|
||||
EnvironmentFile=$BASE/shared/.env
|
||||
ExecStart=/bin/bash -lc 'exec ./node_modules/.bin/tsx scripts/check-modular-processing-generation-health.ts'
|
||||
TimeoutStartSec=10min
|
||||
EOF
|
||||
|
||||
cat >"/etc/systemd/system/$SERVICE_NAME.timer" <<EOF
|
||||
[Unit]
|
||||
Description=Check modular product processing order generation health every five minutes
|
||||
[Timer]
|
||||
OnCalendar=$CALENDAR
|
||||
Persistent=true
|
||||
AccuracySec=30s
|
||||
Unit=$SERVICE_NAME.service
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
EOF
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl enable --now "$SERVICE_NAME.timer"
|
||||
systemctl list-timers --all "$SERVICE_NAME.timer" --no-pager
|
||||
@ -13,7 +13,7 @@ chmod 600 "$PERSONAL_ENV"
|
||||
mkdir -p "$BASE/shared/logs/marketing-sales-job"
|
||||
cat >"/etc/systemd/system/$SERVICE.service" <<EOF
|
||||
[Unit]
|
||||
Description=Synchronize product archive marketing daily sales once a day
|
||||
Description=Synchronize product archive marketing daily sales with hourly retries
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
[Service]
|
||||
@ -30,9 +30,9 @@ KillMode=control-group
|
||||
EOF
|
||||
cat >"/etc/systemd/system/$SERVICE.timer" <<EOF
|
||||
[Unit]
|
||||
Description=Marketing daily sales once a day at Beijing 08:00
|
||||
Description=Marketing daily sales hourly retries from Beijing 08:00 through 14:00
|
||||
[Timer]
|
||||
OnCalendar=*-*-* 08:00:00 Asia/Shanghai
|
||||
OnCalendar=*-*-* 08..14:00:00 Asia/Shanghai
|
||||
OnBootSec=1min
|
||||
AccuracySec=1s
|
||||
Persistent=true
|
||||
|
||||
@ -0,0 +1,63 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Install only after this script and the runner have been released from main.
|
||||
# Sends failed store-account notifications to the named group every five minutes.
|
||||
# It does not retry personal deliveries or invoke the general retry endpoint.
|
||||
BASE="${FURNITURE_PROD_BASE:-/opt/furniture-3d-system-prod}"
|
||||
APP_ROOT="$BASE/current/furniture-3d-system"
|
||||
APP_ENV="$BASE/shared/.env"
|
||||
SERVICE_NAME="furniture-store-account-group-notification-retry"
|
||||
CALENDAR='*-*-* *:0/5:00 Asia/Shanghai'
|
||||
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
echo 'This installer requires root.' >&2
|
||||
exit 1
|
||||
fi
|
||||
for required in "$APP_ENV" "$APP_ROOT/scripts/retry-store-account-group-notifications.ts" "$APP_ROOT/node_modules/.bin/tsx"; do
|
||||
if [ ! -f "$required" ]; then
|
||||
echo "Missing required file: $required" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
if ! grep -Eq '^DINGTALK_STORE_ACCOUNT_GROUP_WEBHOOK_URL=.+' "$APP_ENV"; then
|
||||
echo 'Store-account group webhook is not configured.' >&2
|
||||
exit 1
|
||||
fi
|
||||
command -v flock >/dev/null
|
||||
systemd-analyze calendar "$CALENDAR" >/dev/null
|
||||
|
||||
cat >"/etc/systemd/system/$SERVICE_NAME.service" <<EOF
|
||||
[Unit]
|
||||
Description=Retry failed store-account notifications to Mengxi/Yangling group
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
WorkingDirectory=$APP_ROOT
|
||||
Environment=NODE_ENV=production
|
||||
EnvironmentFile=$APP_ENV
|
||||
ExecStart=/usr/bin/flock --nonblock --conflict-exit-code 0 /run/lock/$SERVICE_NAME.lock $APP_ROOT/node_modules/.bin/tsx scripts/retry-store-account-group-notifications.ts --apply
|
||||
TimeoutStartSec=20min
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
EOF
|
||||
|
||||
cat >"/etc/systemd/system/$SERVICE_NAME.timer" <<EOF
|
||||
[Unit]
|
||||
Description=Check failed store-account group notifications every five minutes (Asia/Shanghai)
|
||||
|
||||
[Timer]
|
||||
OnCalendar=$CALENDAR
|
||||
Persistent=true
|
||||
RandomizedDelaySec=15
|
||||
Unit=$SERVICE_NAME.service
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
EOF
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl enable --now "$SERVICE_NAME.timer"
|
||||
systemctl list-timers --all "$SERVICE_NAME.timer" --no-pager
|
||||
@ -0,0 +1,49 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
BASE="${FURNITURE_PROD_BASE:-/opt/furniture-3d-system-prod}"
|
||||
SERVICE_NAME="${STORE_ACCOUNT_RENEWAL_NOTICE_SERVICE_NAME:-furniture-store-account-renewal-notice}"
|
||||
APP_ROOT="$BASE/current/furniture-3d-system"
|
||||
APP_ENV="$BASE/shared/.env"
|
||||
SCHEDULE="${STORE_ACCOUNT_RENEWAL_NOTICE_SCHEDULE:-*-*-* 11:10:00 Asia/Shanghai}"
|
||||
|
||||
if [ "$(id -u)" -ne 0 ]; then echo "This installer must run as root." >&2; exit 1; fi
|
||||
if [ ! -d "$APP_ROOT" ] || [ ! -f "$APP_ENV" ]; then echo "Missing app root or environment." >&2; exit 1; fi
|
||||
if ! grep -Eq '^JST_REPORT_DB_HOST=.+' "$APP_ENV" || ! grep -Eq '^YYD_NOTIFICATION_CENTER_URL=.+' "$APP_ENV"; then
|
||||
echo "Report source and notification center must be configured." >&2; exit 1
|
||||
fi
|
||||
if command -v systemd-analyze >/dev/null 2>&1; then systemd-analyze calendar "$SCHEDULE" >/dev/null; fi
|
||||
|
||||
cat >"/etc/systemd/system/$SERVICE_NAME.service" <<EOF
|
||||
[Unit]
|
||||
Description=Furniture normal-store renewal due notice
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
WorkingDirectory=$APP_ROOT
|
||||
Environment=NODE_ENV=production
|
||||
EnvironmentFile=$APP_ENV
|
||||
ExecStart=/bin/bash -lc 'set -euo pipefail; cd "$APP_ROOT"; ./node_modules/.bin/tsx --conditions=react-server scripts/push-store-account-renewal-notice.ts --apply'
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
EOF
|
||||
|
||||
cat >"/etc/systemd/system/$SERVICE_NAME.timer" <<EOF
|
||||
[Unit]
|
||||
Description=Check store renewal dates daily
|
||||
|
||||
[Timer]
|
||||
OnCalendar=$SCHEDULE
|
||||
Persistent=true
|
||||
RandomizedDelaySec=120
|
||||
Unit=$SERVICE_NAME.service
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
EOF
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl enable --now "$SERVICE_NAME.timer"
|
||||
systemctl list-timers --all "$SERVICE_NAME.timer" --no-pager
|
||||
@ -104,6 +104,7 @@ Environment=TZ=Asia/Shanghai
|
||||
Environment=PATH=/usr/local/bin:/usr/bin:/bin
|
||||
EnvironmentFile=$ENV_FILE
|
||||
Environment=JST_RPA_PYTHON=$PYTHON_BIN
|
||||
Environment=JST_RPA_BROWSER_SERVICE=$BROWSER_SERVICE_NAME.service
|
||||
ExecStart=/usr/bin/env node scripts/run-supply-chain-discontinuation-daily.mjs
|
||||
TimeoutStartSec=2h
|
||||
Restart=on-failure
|
||||
|
||||
@ -11,7 +11,7 @@ SERVICE_FILE="/etc/systemd/system/$SERVICE_NAME.service"
|
||||
TIMER_FILE="/etc/systemd/system/$SERVICE_NAME.timer"
|
||||
SYNC_ON_BOOT_SEC="${SUPPLY_CHAIN_ARCHIVE_SYNC_ON_BOOT_SEC:-3min}"
|
||||
SYNC_INTERVAL="${SUPPLY_CHAIN_ARCHIVE_SYNC_INTERVAL:-15min}"
|
||||
SYNC_CALENDAR="${SUPPLY_CHAIN_ARCHIVE_SYNC_CALENDAR:-*-*-* 08:10:00}"
|
||||
SYNC_CALENDAR="${SUPPLY_CHAIN_ARCHIVE_SYNC_CALENDAR:-*-*-* 08:10:00 Asia/Shanghai}"
|
||||
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
echo "This installer must run as root." >&2
|
||||
@ -65,7 +65,7 @@ EOF
|
||||
|
||||
cat >"$TIMER_FILE" <<EOF
|
||||
[Unit]
|
||||
Description=Sync product archive source tables daily at 08:10
|
||||
Description=Sync product archive source tables daily at 08:10 Asia/Shanghai
|
||||
|
||||
[Timer]
|
||||
OnCalendar=$SYNC_CALENDAR
|
||||
|
||||