Compare commits

...

143 Commits

Author SHA1 Message Date
WuYongJie
c14a90eecb Merge commit '96c0184b3e77ddb281b61df39c87a11a6add5076' into HEAD 2026-11-07 18:12:41 +08:00
WuYongJie
e6ec1bd7ff feat(clearance): 整合人工清货价、审核工作台与小数评分修复 2026-11-07 18:12:41 +08:00
WuYongJie
96c0184b3e fix(installation): replace existing drawing tasks by complete drawing ID 2026-11-07 18:05:30 +08:00
WuYongJie
3ac1331469 fix(store-accounts): include page metadata and source dates in exports 2026-11-07 16:42:48 +08:00
WuYongJie
60fab9516d fix(modular-processing): 以整轮生成状态判定成功并补齐超时回归 2026-11-07 13:41:38 +08:00
apple
e6c25a648e feat(modular-processing): 完善生成监控、异常预警与主表冻结
部件缺口不再阻断整批生成;持久化生成状态并向系统管理员预警;页面展示失败原因与净缺口;冻结条码及之前字段;补充迁移和回归测试。
2026-11-07 13:17:08 +08:00
WuYongJie
a3e1af3bea fix(barcode-print): preserve Gujia 2026 product name position
Cherry-picked from 514c373a5f1b2c13072c4a5352197064f0454f4e; release only the authorized Gujia product name layout change.
2026-11-07 12:09:34 +08:00
WuYongJie
31d030ce24 fix(barcode-print): fit long product names on Gujia certificates
Cherry-picked from a273f7731548a33e2c9e389f15c05d5353eab612; release only the authorized Gujia product name layout change.
2026-11-07 11:01:31 +08:00
WuYongJie
ef2c6aa7f4 feat(store-account): add shop metadata and searchable filters 2026-11-07 10:06:28 +08:00
WuYongJie
b7b8b1ed64 feat(diy): extract editable scene prompts from reference images
(cherry picked from commit 96e92719d2f00c680a9ec33376675bea3b8fc8b5)
2026-11-06 17:39:03 +08:00
居居
6e443caf93 fix(clearance): 限制任务导出字段并禁止客户端图片抓取 2026-11-06 17:34:00 +08:00
居居
34a72da7dd fix(clearance): 任务导出改为当前查询结果快照 2026-11-06 17:09:41 +08:00
居居
489dbd7545 fix(clearance): 统一任务导出与页面发货数量口径 2026-11-06 17:09:41 +08:00
WuYongJie
6a27f3c19d feat(store-account): use source shop dates and add renewal notices
(cherry picked from commit d6456eb07748d6afd9a7797add765ae0d33f6406)
(cherry picked from commit 86c2b6d88aa245674fbe763945029e6597b1b76b)
2026-11-06 16:21:12 +08:00
WuYongJie
84807a89ab fix(intake): unify SKU numbering and two-template imports 2026-11-06 16:00:48 +08:00
居居
331e84cffb fix(clearance): 修正同步状态轮询与请求类型 2026-11-06 15:46:42 +08:00
居居
f57142164a perf(clearance): 拆分发货同步并增加状态反馈 2026-11-06 15:30:12 +08:00
居居
1e798a5aa2 feat(freight): 支持条码固定运费与三方标签计费 2026-11-06 14:15:33 +08:00
居居
072678f2bd fix(clearance): 自动刷新每日发货量并调整分数概览
- 返回每日发货缓存真实截止日并自动轮询刷新\n- 移除清货分数平均分卡片,保留完成率与负责人数量指标
2026-11-06 14:15:33 +08:00
居居
9fa09c7662 feat(clearance): 调整清货分数概览指标 2026-11-06 13:31:49 +08:00
居居
18335f0ab9 feat(clearance): 清货分数增加筛选后合计行 2026-11-06 13:31:49 +08:00
居居
ae4c382789 fix(clearance): 按条码和利润中心分拆任务进度 2026-11-06 13:31:49 +08:00
居居
9b1d7f7cd0 fix(clearance): 修复筛选选项、分数利润中心与全宽布局 2026-11-06 13:31:49 +08:00
居居
7b200dcc27 style(clearance): 清货价推送页面全宽平铺 2026-11-06 13:31:48 +08:00
WuYongJie
212f67c1eb feat(diy): add usage records restricted to Xuxiake account 2026-11-06 13:15:58 +08:00
居居
7e093835e5 fix(clearance): 修正每日发货缓存回补与利润中心兜底 2026-11-06 10:11:35 +08:00
居居
25ede03411 perf(clearance): 按日增量累计清货发货量 2026-11-06 10:11:35 +08:00
WuYongJie
46a2e3418e fix(intake): continue SKU numbering from actual code suffix
Use the largest actual SKU suffix within the SPU for manual additions and workbook imports. Align internal sequences with saved identities, share package sequence numbers, and serialize allocation across product records.
2026-11-06 09:57:03 +08:00
居居
405f217282 fix(clearance): 重试清货价推送连接失败 2026-11-06 09:13:59 +08:00
居居
f7a1b2f3ac fix(discontinuation): never expose unmapped SKU as replacement barcode 2026-11-05 18:09:38 +08:00
WuYongJie
88a6855383 fix(diy): pin support contact notice above AI panel scroll 2026-11-05 17:57:59 +08:00
WuYongJie
4d37d95606 Merge remote-tracking branch 'origin/main' into release/clearance-tasks-62191d50 2026-11-05 17:44:36 +08:00
WuYongJie
1cbf0b38f3 fix(clearance): match resolved shop IDs before paging and export
Use archived shop identities for mapped-ID searches across list, count and export.
Split shop mapping from repository; preserve nonempty historical IDs.
2026-11-05 17:44:33 +08:00
居居
b273c62a4e feat(hr-sync): 人员信息同步纳入快递公司组织树(覆盖全部钉钉部门) 2026-11-05 17:35:32 +08:00
居居
b1154f7b30 feat(clearance): 清货价推送增加利润中心 2026-11-05 17:21:31 +08:00
居居
d4d9e0ba08 fix(clearance): 补齐历史店铺编号并显示清货价说明 2026-11-05 17:21:30 +08:00
WuYongJie
c875549d2f feat(clearance): publish task progress, shipment metrics and price exports
Release the seven reviewed clearance changes through 62191d50 and fix 828883e742e57fae4525888e17974d98a2559af4.
Preserve the applied archive migration; add a separate index migration.
Terminate failed/cancelled workbook downloads and retain frontend retry feedback.

Source commits:
976fc50be6c25c9e83dca1c29410a7ad8ee40b7a perf(clearance): 异步刷新任务出货进度
4c1cd5527b4c54403ad985c8e807dbe695316309 perf(clearance): 解耦任务首屏与进度刷新
72fb742a3d56beceecaec485782258a7ccff214d style(clearance): 缩短历史成本价备注
9e064c4b4268b361bdc24c47b2bd50ba163f3e1e fix(clearance): 从聚水潭店铺表映射店铺编号
75b6d6b08d0b4dc97926171600793dd5c0dbcd76 feat(clearance): 增加清货价推送筛选与导出
05e4509f0d443829872b0d8ed6c2c2a9f6b8ede3 fix(clearance): 保留真实发货量并补齐任务池字段
62191d50ab5f96868232331810961d74ad2fc34b style(clearance): 精简清货价推送筛选条件
2026-11-05 16:02:25 +08:00
WuYongJie
45589b97e0 feat(1688): display and filter product suppliers 2026-11-05 15:02:57 +08:00
WuYongJie
69f1a0c30a feat(clearance): add monthly price push archive
Integrate clearance feature from shared commit 6a61f375182ae35fe5b87da23ab459c89544e3f8. Includes full profit-center shop coverage and actual product-code mapping; excludes unrelated shared-branch changes.
2026-11-05 14:32:14 +08:00
WuYongJie
e38560dfdb fix(intake): complete inventory tasks when either flow succeeds
Cherry-picked from f79dfd05a92b6d904a3906101bb1702bf4a0107f; release only the authorized inventory task and alert fix.
2026-11-05 14:06:57 +08:00
居居
9930b1ee90 feat(hr-sync): 人员信息同步纳入供应商组织树(仅排除快递公司) 2026-11-05 11:57:10 +08:00
WuYongJie
b5128e8645 feat(discontinuation): release corrected activity appeal review
Integrate only activity appeal functionality and fixes from 708d6e74fbcd449babb69e60a9b2bb6d7e80b37b, preserving current main permissions and evidence loading guards.
2026-11-05 11:39:19 +08:00
居居
9a501745f4 fix(supply-chain): include snapshot images in limit export 2026-11-05 11:13:40 +08:00
居居
d40ba51515 fix(discontinuation): lazy load appeal evidence with loading protection
Source: ff6406d1e0cd7729528e09a6a704d2f371f0d63f; loading fix: fc28557c3de09606fc058a469c8e3cfba1c555c4. Preserve current main permission documentation.
2026-11-05 10:00:33 +08:00
WuYongJie
aa1671292d test(auth): preserve current supply-chain exclusions during release 2026-11-04 14:51:59 +08:00
WuYongJie
6e57c8f541 fix(auth): honor personal supply-chain grants for external intake users 2026-11-04 14:51:59 +08:00
居居
03c2cc2396 fix(discontinuation): restart stale RPA browser before sync 2026-11-04 13:29:10 +08:00
WuYongJie
495bd42ac8 fix(product-archive): retry marketing sales hourly until 14:00 2026-11-04 10:33:10 +08:00
WuYongJie
58fc8bdbee fix(intake): show successful inventory channel exclusively
Cherry-picked from efcba62d3b1e50deaf34c8df54fbb0fe4228dfb5; release only the authorized inventory status display fix.
2026-11-04 09:51:07 +08:00
居居
f6af1843c2 fix(clearance): 优化任务加载与分销任务计算 2026-11-03 18:06:13 +08:00
WuYongJie
06d61525bb fix(diy): avoid reserved layout filename in guide content 2026-11-03 17:59:48 +08:00
WuYongJie
57f6de24d0 feat(diy): add illustrated operation guide with searchable chapters
Provide concise instructions for SJ0380 and SJ0450, real interface screenshots, and a flip-door demo. Place the guide capsule after the scheme name and open the manual in a separate tab.
2026-11-03 17:44:34 +08:00
居居
84710c8fa5 feat(discontinuation): mark combo replacements 2026-11-03 17:18:05 +08:00
居居
57ef1ec3ef fix(discontinuation): hide unavailable replacement aliases 2026-11-03 17:18:05 +08:00
WuYongJie
5b17403a8c fix(intake): keep inventory status labels on one line
Cherry-picked from b11d2efbe109d46614d94eba916cea27833f8c36
2026-11-03 16:02:43 +08:00
居居
ad7b69c3e4 feat(clearance): 完善任务映射、计算与配置交互 2026-11-03 15:54:03 +08:00
居居
d23590db0a feat(clearance): 支持人工任务按利润中心导入 2026-11-03 15:54:03 +08:00
居居
56db9d6e93 feat(clearance): 接入人工批次与月度批次规则 2026-11-03 15:54:03 +08:00
居居
fc885376e5 feat(clearance): 支持经管人工调整优先级 2026-11-03 15:54:03 +08:00
居居
6fd5e989cc feat(discontinuation): support batch filter queries 2026-11-03 15:54:03 +08:00
WuYongJie
011bb99278 fix(intake): reliably enqueue factory virtual inventory
Cherry-picked from 4ed8082fba353c1ad18662509e3a6c12780ece30; only the authorized inventory fix is released.
2026-11-03 15:29:07 +08:00
居居
d7b640c482 feat(discontinuation): suppress dedicated warehouse links 2026-11-03 15:27:29 +08:00
居居
5b329dda80 fix(supply-chain): exclude owner-adjusted price from limit exports 2026-11-03 15:17:31 +08:00
居居
e9a1473348 fix(supply-chain): hide owner-adjusted price column in limit UI
Keep the owner-adjusted price in calculations, snapshots, and exports while excluding it from the limit table, filters, and column settings.
2026-11-03 15:17:31 +08:00
WuYongJie
3818b8a65d fix(product-archive): sync Jianshang actual stock daily
Use inventory management warehouse scope and sum finished goods qty by barcode.
Refresh source metadata and support a targeted stock-only refresh with backups.
Make the existing daily 08:10 schedule explicitly use Asia/Shanghai.

(cherry picked from commit 824603764b8892689c7043f0f02c0613ed4fed82)
2026-11-03 13:35:36 +08:00
WuYongJie
1be13ea777 fix(product-archive): prioritize warehouse storage configuration 2026-11-03 12:15:24 +08:00
居居
26ff64287b fix(discontinuation): apply all link filters 2026-11-03 11:29:10 +08:00
居居
11821543ae feat(discontinuation): simplify appeal reason options 2026-11-03 11:21:15 +08:00
居居
9cfb8c4dfb feat(discontinuation): separate appeal and review roles 2026-11-03 11:21:15 +08:00
居居
37ff36fe43 feat(discontinuation): add additional store data scopes 2026-11-03 11:21:14 +08:00
居居
685a23cc2a fix(discontinuation): retain saved data scope overrides 2026-11-03 11:00:19 +08:00
WuYongJie
ae28ff1334 fix(layout): hide external AI navigation for supplier accounts 2026-11-03 09:54:12 +08:00
WuYongJie
fa04954206 fix(diy): highlight new SJ0450 series without changing names 2026-11-03 09:40:04 +08:00
居居
ee5c4570b4 fix(discontinuation): resolve regeneration runner from release root 2026-11-03 09:37:36 +08:00
居居
9958a7fbd4 fix(discontinuation): preserve underlying regeneration errors 2026-11-03 09:13:09 +08:00
WuYongJie
677cdfb8e9 fix(diy): load uncompressed SJ0450 door without wasm decoders
(cherry picked from commit 00eddb201d88eaa7f4aae2f150bd849b27a050c0)
(cherry picked from commit 01e1f61f91919a39a4e5942d169a80063a0f8e7d)
2026-11-02 19:04:36 +08:00
WuYongJie
e0d3ae2c89 feat(diy): add independent SJ0450 catalog and flip door hardware
(cherry picked from commit be3e390be0e0360a0149daf08e67affa9aa454ff)
(cherry picked from commit f3a123def54064fdc604e5c6ff50e1ac7cd1be42)
2026-11-02 18:50:18 +08:00
居居
d2f8a7ca76 fix(discontinuation): bundle runtime tools and report child errors 2026-11-02 18:25:44 +08:00
WuYongJie
1cfb598d9e feat(distribution): show archive SKU before barcode
Add a goods code column using the product archive SKU. Match only the paged barcode summaries and display a dash for missing codes; keep listing counts, detail expansion, exports and synchronization unchanged.
2026-11-02 18:00:49 +08:00
居居
05a74eeb15 fix(discontinuation): run regeneration outside standalone runtime 2026-11-02 17:59:02 +08:00
WuYongJie
16d0bc90a2 fix(inventory): preserve Prisma snapshot timestamp semantics 2026-11-02 17:43:23 +08:00
居居
a70e5a759e fix(discontinuation): surface regeneration stage errors 2026-11-02 17:39:34 +08:00
WuYongJie
7580d9612d perf(inventory): publish only changed snapshot rows 2026-11-02 17:25:56 +08:00
居居
c0429c23d5 feat(discontinuation): add management-only snapshot regeneration 2026-11-02 17:18:06 +08:00
居居
c094200ec5 feat(discontinuation): add one-click filter reset 2026-11-02 16:41:57 +08:00
居居
c99bff0998 feat(discontinuation): add filter options and configurable data scopes 2026-11-02 16:41:56 +08:00
居居
9df0e0ac38 feat(clearance): 数字条件支持自定义输入 2026-11-02 16:05:30 +08:00
ZCode
1f2275333b revert(discontinuation): drop department permission dimension
User decision 2026-11-02: the department permission config introduced with the discontinuation permission wiring had wrong logic and is removed entirely. Drops department@ subject keys, the department override layer in access resolution, the DepartmentPermissionPanel UI, its wiring in UserPermissionCenter/Tabs, related tests and doc lines. Account and role based configuration for the discontinuation management node stays; the new owner-scope data access from b87d7428 is unaffected.
2026-11-02 15:50:37 +08:00
居居
5f29b761e1 feat(discontinuation): enforce owner scopes and valid replacements 2026-11-02 15:44:47 +08:00
ZCode
8f0c3c6d9e fix(supply-chain): typecheck department permission subject keys
resolveSupplyChainPermissionSubjectKeys returned IntakeNodePermissionRoleKey[]
2026-11-02 14:34:06 +08:00
居居
751f104816 feat(discontinuation): 接入用户和部门权限配置 2026-11-02 14:20:21 +08:00
WuYongJie
2076d0eacf fix(supply-chain): restore modular processing page tab 2026-11-02 13:39:47 +08:00
WuYongJie
e2ff1cab4f feat(diy): keep solid door colors consistent in AI renders
Share verified color standards across single and batch renders, add conservative result color review, and support signed development reference URLs.

(cherry picked from commit 56e24b0b5f21c233cf5525c9839762bf6dc51133)
2026-11-02 11:45:55 +08:00
WuYongJie
01f7a4a191 fix: retry failed store account group notifications 2026-11-02 11:42:00 +08:00
居居
6420741003 feat(discontinuation): 完善顶部标签与按日按月筛选 2026-11-02 10:31:49 +08:00
居居
7fae59e687 feat(clearance): 条件规则支持优先级字段 2026-11-02 10:31:49 +08:00
居居
598fb4b97f fix(clearance): 接入顶部标签路由标题 2026-11-02 10:31:49 +08:00
居居
a0707ca369 feat(clearance): 折扣限制支持自定义条件规则 2026-11-02 10:31:49 +08:00
居居
3088716d9a feat(clearance): 完成率允许超过一百分 2026-11-02 10:31:49 +08:00
居居
6cfdace699 feat(clearance): 允许档位分数超过一百分 2026-11-02 10:31:49 +08:00
apple
95e1a462a8 fix(modular-processing): install daily timer during production deploy 2026-11-02 10:11:05 +08:00
WuYongJie
baabe2c3c2 feat(diy): add batch image editing and selective downloads 2026-11-02 10:04:45 +08:00
apple
940a7d5531 feat(modular-processing): merge complete management source 2026-11-02 09:25:37 +08:00
WuYongJie
747a07d4d6 fix(intake): clearly label products successfully pushed to Jushuitan
(cherry picked from commit 6568bcb349991a41a14cb663c6aa1b4bb4c02254)
2026-10-30 18:37:26 +08:00
WuYongJie
993539eca2 feat(diy): simplify batch records and name downloads after schemes 2026-10-30 17:57:20 +08:00
居居
4e70404ca6 feat(clearance): 支持无上限完成率档位 2026-10-30 17:49:49 +08:00
居居
b1b23a0b5e ops(supply-chain): add production limit snapshot timer
Install a production systemd timer at 09:30 Asia/Shanghai, persist daily snapshots under the shared release directory, and validate required production data sources before enabling the schedule.
2026-10-30 16:53:19 +08:00
居居
2a1ae8640b feat(clearance): 完成率档位支持自定义区间 2026-10-30 16:14:21 +08:00
居居
b5fed760cf feat(clearance): 规则条件值改为动态多选 2026-10-30 16:14:20 +08:00
居居
1afd1f7ffa feat(supply-chain): add daily limit snapshot workflow
Generate and export an atomic daily limit snapshot from the full product archive, reconcile all limit-dependent archive fields before publication, and apply owner-adjusted pricing without the gross-margin floor when an owner limit exists.
2026-10-30 16:14:20 +08:00
WuYongJie
cdd3e8cfcb feat(diy): add shared-angle batch AI rendering and refine scheme dialogs 2026-10-30 15:22:32 +08:00
WuYongJie
c03a9b5e36 fix(product-archive): unify list and detail image sources 2026-10-30 13:05:57 +08:00
居居
9cc2aa9ea7 feat(freight): 增加物流快照正式站定时调度 2026-10-30 11:14:45 +08:00
居居
49e00b0f93 feat(freight): 每日九点生成聚水潭物流规则快照 2026-10-30 10:35:40 +08:00
居居
d590b5d6cd fix(freight): 拦截说明文字作为商品编码提交
原文 cherry-pick 自主线 cd094bc90;因 main 上 calculateByBarcodes 为索引化融合版(不走单条方法),补充同样的 isFreightBarcodeInput 格式拦截与改进后的“不在产品全量档案中”文案到批量循环,并更新批量测试(FAIL 新文案 + 说明文字拦截用例)。
2026-10-30 10:14:08 +08:00
WuYongJie
9ae4ffb44b fix(diy): correct mirrored door texture orientation 2026-10-30 10:10:49 +08:00
居居
8571b5f649 feat(freight): 支持条码固定价和体积运费测算 2026-10-29 17:44:03 +08:00
居居
2ea3f22349 feat(freight): 支持连续对话修正与部分报价计算 2026-10-29 14:48:55 +08:00
WuYongJie
60c933a14e fix(intake): reuse empty SKU placeholders and normalize imported dimensions
Preserve factory specification names; match equivalent size separators.

Task: sku-import-empty-placeholder-20261029-01a18026
Source-commit: afcede79a052c0d478866f71c59fd15f4c64d3ef
2026-10-29 14:43:43 +08:00
WuYongJie
f18a0f3f80 fix(store-account): support JD profiles in account editor 2026-10-29 14:36:55 +08:00
WuYongJie
ee410934ec fix(intake): update children furniture execution standard to GB 28007-2024 2026-10-29 13:52:20 +08:00
WuYongJie
b936295a7d feat(intake): preserve factory specification names in SKU imports 2026-10-29 11:40:50 +08:00
WuYongJie
9af1d9efda fix(limit-management): bind large style sets as a PostgreSQL array 2026-10-29 10:56:34 +08:00
WuYongJie
2be511f346 perf(freight): index batch barcode calculations with shared calculator
calculateByBarcodes previously called calculateByBarcode per item, re-reading rates, shipment share and courier rules and re-scanning the full rate catalogue for every barcode. Switch it to the shared createFreightCalculator snapshot: batch archive read via findProductArchiveSnapshots, courier rules fetched once per warehouse, rates and province shipment share fetched once, and the calculation loop yields to the event loop every 8ms like the archive page path. Per-item validation messages and result/error shapes are unchanged.
2026-10-29 10:46:16 +08:00
WuYongJie
2830836b92 test(freight): align release fixture with published region formula 2026-10-29 10:22:51 +08:00
WuYongJie
c169f9392c fix(limit-management): reuse locale comparator for archive sorting 2026-10-29 10:20:35 +08:00
居居
09128489fb feat(freight-maintenance): 完善批量测算与对话式比价 2026-10-29 10:13:38 +08:00
WuYongJie
4fd17c6677 fix(intake): convert WebP installation drawings and verify image delivery
Isolated release of 423d8bc7e663788d4bd2f3c7a41d6f3a1b1089d1.
2026-10-29 10:08:56 +08:00
WuYongJie
66db2a26f6 fix(freight): use framework-compatible event loop scheduling 2026-10-29 09:49:12 +08:00
WuYongJie
ec8cf4a819 feat(material-library): export all boards and normalize surface types 2026-10-29 09:19:36 +08:00
WuYongJie
416bcd5939 test(freight): preserve production formulas in scoped release 2026-10-28 20:18:20 +08:00
WuYongJie
e889d53cb6 fix(freight): index batch rates and yield the web event loop 2026-10-28 20:10:56 +08:00
WuYongJie
83a3138fe9 fix(intake): use configured vision model for reference cost estimates 2026-10-28 17:06:19 +08:00
居居
45387c9003 fix(limit-management): include freight in limit exports 2026-10-28 16:36:28 +08:00
WuYongJie
a9f2917c5b style(intake): align SKU toolbar actions by task 2026-10-28 16:16:29 +08:00
WuYongJie
77665c85b5 feat(intake): add product notification delivery history 2026-10-28 16:03:41 +08:00
居居
3f7710ef84 feat(limit-management): add regional and national freight averages 2026-10-28 15:56:14 +08:00
居居
281e52bb95 feat(supply-chain): 完善快递维护与AI运费比价 2026-10-28 15:56:14 +08:00
WuYongJie
2775b4ff83 feat(intake): import SKU templates with automatic coding 2026-10-28 14:36:34 +08:00
WuYongJie
e58f8ba96d fix(limit-management): restore mysql2 query call compatible with locked 3.15.3 typings
75adc3f7 imported QueryValues from mysql2/promise, which only exists in newer mysql2 typings than the package-lock version (3.15.3), breaking tsc on a clean install. Restore the previous equivalent call form (options object + values argument), preserving runtime behavior exactly.
2026-10-28 14:04:01 +08:00
居居
062666f530 feat(limit-management): consolidate limit and freight maintenance 2026-10-28 13:59:30 +08:00
居居
ce0aa8f387 fix(supply-chain): 修复运费配置权限并过滤过期费率 2026-10-28 13:59:30 +08:00
居居
5c5fef8b8e feat(supply-chain): 新增运费配置查询页面 2026-10-28 13:59:30 +08:00
788 changed files with 114396 additions and 5179 deletions

View File

@ -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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

@ -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` 或旧入口机。

View File

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

View File

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

View File

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

View File

@ -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`。这是自审,不是独立复审。剩余警告是共享写入尚不能精确分配到各调用项目,及真实长期降幅需要发布后跨完整任务周期观察。

View File

@ -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 次,任一次成功立即结束;只有全部失败后才按每批

View File

@ -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. 业务口径

View File

@ -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 条,按当前筛选范围导出;超出时在「导出说明」中提示实际总数。

View File

@ -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` 的有效记录时,优先使用表中绑定的快递和固定单价;固定报价不依赖重量、体积、省份发货占比或仓库通用快递名称过滤,也不参与新快递替换。
## 五、待办
- [ ] 限价流程数据接入 + 筛选逻辑(生效时间版本、货号维度)

View File

@ -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"

View File

@ -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": {

View File

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

View File

@ -0,0 +1,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");

View File

@ -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");

View File

@ -0,0 +1,6 @@
ALTER TABLE "ModularProcessingManualValue"
ADD COLUMN "finalProcessingQuantityIsManual" BOOLEAN NOT NULL DEFAULT true;
UPDATE "ModularProcessingManualValue"
SET "finalProcessingQuantityIsManual" = false
WHERE "updatedByName" = '系统按部件库存自动校验';

View File

@ -0,0 +1,5 @@
ALTER TABLE "DiyCabinetStyle"
ADD COLUMN "deletedAt" TIMESTAMP(3),
ADD COLUMN "deletedBy" TEXT;
CREATE INDEX "DiyCabinetStyle_deletedAt_idx" ON "DiyCabinetStyle"("deletedAt");

View File

@ -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;

View File

@ -0,0 +1,5 @@
CREATE INDEX "SupplyChainDiscontinuationNoticeLink_platform_shopId_idx"
ON "SupplyChainDiscontinuationNoticeLink"("platform", "shopId");
CREATE INDEX "SupplyChainDiscontinuationPenalty_platform_shopId_idx"
ON "SupplyChainDiscontinuationPenalty"("platform", "shopId");

View File

@ -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");

View File

@ -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;

View File

@ -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;

View File

@ -0,0 +1,5 @@
ALTER TABLE "SupplyChainClearancePricePushItem"
ADD COLUMN "profitCenter" TEXT NOT NULL DEFAULT '';
CREATE INDEX "SupplyChainClearancePricePushItem_snapshot_profitCenter_idx"
ON "SupplyChainClearancePricePushItem"("snapshotId", "profitCenter");

View File

@ -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;

View File

@ -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 ('真实店铺', '虚拟店铺'));

View File

@ -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");

View File

@ -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");

View File

@ -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");

View File

@ -0,0 +1,2 @@
ALTER TABLE "SupplyChainClearanceTask"
ADD COLUMN "priorityOverride" VARCHAR(50) NOT NULL DEFAULT '';

View File

@ -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"
);

View File

@ -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";

View File

@ -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);

View File

@ -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())

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

View 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()
})

View File

@ -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(); });

View 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
})

View 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
})

View File

@ -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) => {

View File

@ -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?.['库存入库时间']),

View File

@ -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()
})

View File

@ -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))

View File

@ -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))

View 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

View 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

View 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

View File

@ -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>

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

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