Compare commits

...
2081 Commits
Author SHA1 Message Date
ouyangxq 5cc6f617f5 fix: complete GCP synthetic single-order acceptance and align booking collation [skip ci] 2026-10-24 19:20:44 +08:00
ouyangxq 493656e677 Merge remote-tracking branch 'origin/test' into dev 2026-10-23 21:02:36 +08:00
ouyangxq a8192f51fe fix(lastleg): generate sequenced outbound task numbers 2026-10-23 18:45:47 +08:00
ouyangxq 880c53ab43 Merge latest test updates into local dev [skip ci] 2026-10-23 15:30:53 +08:00
ouyangxq 7ef8411ea4 fix(dispatch): publish capacity demands only during creation 2026-10-23 12:42:20 +08:00
ouyangxq ab70c63988 fix(dispatch): calculate received pallet cargo from current stock 2026-10-23 12:14:14 +08:00
ouyangxq ecc76d4f91 test(loading): scope non-ISO HTTP verification without removal probes 2026-10-23 11:19:36 +08:00
ouyangxq 9c19980736 fix(loading): remove ISO container validation across PDA fulfillment 2026-10-23 11:12:36 +08:00
ouyangxq c38f1758c5 fix(firstleg): remove booking document prerequisites from release and trucking 2026-10-23 10:56:37 +08:00
ouyangxq c53992ad59 Merge latest test updates into dev [skip ci] 2026-10-23 09:41:44 +08:00
ouyangxq 3616e9db04 test(acceptance): recover completed-plan rejection probe after verified connection loss 2026-10-23 05:36:21 +08:00
ouyangxq 2b54e709f8 test(acceptance): verify native outbound ownership and exact relabel carton quantities 2026-10-23 05:21:30 +08:00
ouyangxq 18ef338036 fix(lastleg): retain physical carton labels from frozen relabel contributions 2026-10-23 05:19:54 +08:00
ouyangxq 139f6b1932 test(acceptance): reuse fixed customers and align native fulfillment flows 2026-10-23 04:48:02 +08:00
ouyangxq b92f7fe991 fix(lastleg): group same-code FBA cargo with optional internal warehouse reference 2026-10-23 04:47:33 +08:00
ouyangxq f6c83125c9 fix(lastleg): preserve source validation during partial dispatch picking 2026-10-23 04:42:16 +08:00
ouyangxq f946285bcc fix: keep fleet booking timestamps stable across persistent replay 2026-10-23 04:25:47 +08:00
ouyangxq 5c5514436e fix: retain source plan identity in mixed-pallet BOL 2026-10-23 03:40:02 +08:00
ouyangxq a7f680d69d fix: restore transfer creation replay before carton validation 2026-10-23 02:59:48 +08:00
ouyangxq 85c7cde040 fix: bind received dispatches to current per-order inventory 2026-10-23 02:24:49 +08:00
ouyangxq 7e1703bde9 test: prepare measurement review fixtures without approval 2026-10-23 02:13:47 +08:00
ouyangxq 6cd27658f7 fix: preserve native tail order facts in combined lists 2026-10-23 01:44:07 +08:00
ouyangxq d60a35b367 test: align prearrangement and dispatch update contracts 2026-10-23 01:27:08 +08:00
ouyangxq 2dbf0cb248 test: prepare destination appointments in dispatch review fixtures 2026-10-23 01:08:49 +08:00
ouyangxq eb82401afa test: prepare unpicked dispatch drafts for browser acceptance 2026-10-23 01:04:27 +08:00
ouyangxq 0fd523e740 fix: finish received transfers after relabel settlement 2026-10-23 00:44:42 +08:00
ouyangxq 40d1b06312 fix: count split picking facts in trip operations 2026-10-23 00:38:30 +08:00
ouyangxq f74861baea test: align live acceptance with dispatch photos and return counting 2026-10-22 23:50:23 +08:00
ouyangxq daa5d30d16 test: align review evidence fixtures and verify stale request rejection 2026-10-22 23:21:31 +08:00
ouyangxq a16a85e14a fix: complete October 22 workflow feedback and request failure recovery 2026-10-22 23:06:00 +08:00
ouyangxq f701456de1 docs(booking): restore required arrival rule 2026-10-22 18:50:01 +08:00
ouyangxq ef0f691845 fix(booking): require arrival time before release 2026-10-22 18:47:58 +08:00
ouyangxq bfeeef130e fix(appointments): expose dispatch cabinet short numbers 2026-10-22 18:03:45 +08:00
ouyangxq 9e0266b7c3 fix(booking): allow release without estimated arrival 2026-10-22 16:43:46 +08:00
ouyangxq c385b7543b fix(dispatch): confirm pallet quantities at inbound receipt 2026-10-22 15:52:29 +08:00
ouyangxq 080ca600e6 test: extend eight-business acceptance flows and offline contracts 2026-10-22 15:41:50 +08:00
ouyangxq 86889d7e0e fix(dispatch): limit appointment prerequisite to inbound cargo 2026-10-22 15:39:33 +08:00
ouyangxq 56f2853fb7 test(dispatch): align read boundary with capacity demand endpoint 2026-10-22 15:13:57 +08:00
ouyangxq fc25507915 fix(dispatch): enforce confirmed FBA appointment before scheduling 2026-10-22 14:55:56 +08:00
ouyangxq 760bf8daa1 优化 2026-10-22 14:37:59 +08:00
ouyangxq 3c57a19bab Merge latest test updates into dev [skip ci] 2026-10-22 12:11:44 +08:00
ouyangxq de96e47d74 优化 2026-10-22 11:56:47 +08:00
liuyihe 5d71af1ac9 节点修改 2026-10-22 10:28:38 +08:00
ouyangxq 98939d9314 Merge precise cancellation release ancestry while retaining development changes 2026-10-22 09:08:27 +08:00
ouyangxq e60890e63e fix(dispatch): release original planned transport claims on cancellation
Exact deployed baseline plus four reviewed files. Focused cancellation tests: 40 passed.
2026-10-22 09:08:27 +08:00
ouyangxq 0c05c15e5a fix(dispatch): cancel planned cargo without loading readiness 2026-10-22 09:08:26 +08:00
ouyangxq 74f510a49b fix(lastleg): repair first appointment execution scope on validated release baseline 2026-10-22 08:16:52 +08:00
ouyangxq 5783610154 Merge validated appointment repair release candidate 2026-10-22 08:16:52 +08:00
ouyangxq 8adaea75fa fix(lastleg): allow appointment registration before dispatch assignment 2026-10-22 08:16:51 +08:00
ouyangxq 738b778edf Merge remote-tracking branch 'origin/test' into test 2026-10-21 23:23:15 +08:00
ouyangxq 2c3435adaf 修复费用记录、装柜完成及海外履约资料 2026-10-21 23:20:09 +08:00
liuyihe bbf8f2f792 尾程订单列表查询修改 2026-10-21 21:49:48 +08:00
ouyangxq 5356a8c898 优化 2026-10-21 21:30:24 +08:00
ouyangxq 234216b3eb fix(dispatch): 在审核及拣货前核对预约与运输时间 2026-10-21 19:42:32 +08:00
ouyangxq e2ad7857b4 feat(dispatch): 从当前排车提供内部散板需求池 2026-10-21 18:50:56 +08:00
ouyangxq 632d35faaf docs(api): describe unknown actual loading measurements 2026-10-21 18:45:06 +08:00
ouyangxq 538a63c635 fix(dispatch): confirm scanned loading without repeated measurements 2026-10-21 18:44:26 +08:00
ouyangxq 6088ff10c2 perf(ci): scope Lastleg tests to affected changes 2026-10-21 18:28:15 +08:00
liuyihe 6d04659ced 提交 2026-10-21 18:00:13 +08:00
liuyihe 044812cd90 提交 2026-10-21 17:52:28 +08:00
liuyihe ae361879e5 Merge remote-tracking branch 'origin/test' into test 2026-10-21 17:27:17 +08:00
liuyihe c442c9eaa7 提交 2026-10-21 17:26:43 +08:00
ouyangxq 1ea7b4ff12 优化 2026-10-21 17:26:41 +08:00
ouyangxq 0976b6ef0d 优化 2026-10-21 17:22:19 +08:00
ouyangxq 0c6c55f6f1 优化 2026-10-21 17:08:52 +08:00
ouyangxq 4a0e86bd42 优化 2026-10-21 16:58:30 +08:00
ouyangxq 55436e3a0a fix(firstleg): align warehouse arrival sources on latest observed port fact 2026-10-21 16:45:42 +08:00
ouyangxq c962f614e6 优化 2026-10-21 16:40:18 +08:00
ouyangxq 090591076f 优化 2026-10-21 15:53:33 +08:00
ouyangxq 94595c810b fix(ci): retain configured image repository defaults 2026-10-21 15:28:44 +08:00
ouyangxq 6fe632f28e 优化 2026-10-21 15:10:44 +08:00
ouyangxq 40a03978cd 优化 2026-10-21 14:30:41 +08:00
ouyangxq 0af977bdef fix(fleet): compare pooled cargo identifiers without collation conflicts 2026-10-21 14:16:17 +08:00
ouyangxq be79b90a4a fix(fleet): hash pooled dispatch source keys without object cast 2026-10-21 13:45:21 +08:00
ouyangxq 337da6d67c 优化 2026-10-21 13:22:35 +08:00
ouyangxq b4ff441a70 fix(numbering): expose coload and settlement payment business numbers 2026-10-21 11:43:55 +08:00
ouyangxq 9fb39604c6 fix(calendar): include dispatch container numbers in calendar 2026-10-21 11:38:18 +08:00
ouyangxq 6b2df287bd test(return): align parcel counts and physical carton assertions 2026-10-21 11:18:02 +08:00
ouyangxq 7a9ab27fd2 Merge remote-tracking branch 'origin/test' into test 2026-10-21 11:05:59 +08:00
ouyangxq 4d12b56cac test(numbering): align return fixtures with receipt contract 2026-10-21 11:01:24 +08:00
ouyangxq 91b6574d53 fix(finance): canonicalize wallet and reconciliation command hashes 2026-10-21 10:56:06 +08:00
liuyihe e5f9cdcf01 提交 2026-10-21 10:52:17 +08:00
ouyangxq b072c0a884 fix(finance): stabilize settlement idempotency request hashes 2026-10-21 10:41:38 +08:00
ouyangxq f00d0e16cf 优化 2026-10-21 07:53:31 +08:00
ouyangxq af5cf1e3bd 优化 2026-10-20 23:49:55 +08:00
ouyangxq 35a1b835fe Merge branch 'test' of http://8.163.30.128:10082/java-project/baiyue-firstleg into test 2026-10-20 23:35:38 +08:00
ouyangxq 9f6e43ef3f 优化 2026-10-20 23:34:22 +08:00
liuyihe 868557d79d 提交 2026-10-20 21:48:47 +08:00
liuyihe 137efc6809 提交 2026-10-20 21:47:51 +08:00
liuyihe 34b90f311a 提交 2026-10-20 18:44:23 +08:00
liuyihe 6771eba2d4 提交 2026-10-20 18:16:29 +08:00
ouyangxq 0dd9c8db07 优化 2026-10-20 16:45:04 +08:00
ouyangxq 0ff1eefff6 优化 2026-10-20 15:48:38 +08:00
liuyihe da6b4685d7 Merge remote-tracking branch 'origin/test' into test
# Conflicts:
#	lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchBolMapper.java
2026-10-20 14:35:19 +08:00
ouyangxq e0c0316487 优化 2026-10-20 14:22:55 +08:00
ouyangxq bf2508b6d6 优化 2026-10-20 14:22:53 +08:00
liuyihe a870b920fe 提交 2026-10-20 14:22:31 +08:00
ouyangxq 19745e46c1 docs(agents): pointerize duplicated DB rules and add index width guard
18 个服务与公共模块重复复制的 314 字符数据库访问约束改为指向后端根唯一定义,并给索引门禁增加防退化校验。

- 18 处重复段落合并为一句指针,规则正文只在后端根 AGENTS.md 维护
- 门禁新增路由表行宽上限(根/Admin/Client/后端根/海外 PDA),防止业务规则重新回流表格
- 同步包含此前未提交的链接与锚点、表格存在性校验
- 仅索引文档与门禁脚本,未改动业务代码
2026-10-20 12:45:41 +08:00
liuyihe e829519044 提交 2026-10-20 11:36:30 +08:00
liuyihe 6fa5d281ec 提交 2026-10-20 11:33:20 +08:00
liuyihe 16ac817524 提交 2026-10-20 11:30:34 +08:00
ouyangxq c13175bf77 优化 2026-10-20 10:35:52 +08:00
liuyihe ce189f062f 提交 2026-10-19 17:59:34 +08:00
ouyangxq dcfba1053c 优化 2026-10-19 17:56:36 +08:00
ouyangxq bb917c954c fix(order): persist cleared courier tracking numbers 2026-10-19 17:48:34 +08:00
ouyangxq 766e199c9b Merge remote-tracking branch 'origin/test' into test 2026-10-19 17:41:05 +08:00
ouyangxq 5368da03d8 feat(order): accept optional courier tracking on customer drafts 2026-10-19 17:39:54 +08:00
liuyihe 920a7e083d 提交 2026-10-19 17:02:59 +08:00
ouyangxq a563f63857 feat(appointments): expose destination warehouse options 2026-10-19 17:02:25 +08:00
liuyihe e0677e7ae7 提交 2026-10-19 16:41:20 +08:00
liuyihe 44c76d8953 提交 2026-10-19 15:54:19 +08:00
ouyangxq eb167347f6 test(transfer): prepare source snapshot before foreign relabel ownership check 2026-10-19 15:19:09 +08:00
ouyangxq c8608d50ba merge: retain current overseas inventory fixes with looseboard demand 2026-10-19 15:11:45 +08:00
ouyangxq dff040b509 feat(looseboard): require route contacts and pickup delivery times 2026-10-19 15:11:15 +08:00
liuyihe 26a203b4eb 提交 2026-10-19 15:00:25 +08:00
ouyangxq 21128b895f feat(trucking): refine truck demand contacts vehicle and service times 2026-10-19 14:34:33 +08:00
ouyangxq fafcf75014 fix(appointments): exclude pickup trips from delivery calendar 2026-10-19 12:21:57 +08:00
ouyangxq aa195cef66 feat(appointments): add four-type calendar sources and align release contracts 2026-10-19 12:15:43 +08:00
liuyihe fe1b2a6a46 提交 2026-10-19 11:54:29 +08:00
liuyihe e8f6bb32dc 提交 2026-10-19 10:23:09 +08:00
ouyangxq 45fbd6eff8 fix(lastleg): reconfirm received bookings after releasing all own cargo 2026-10-18 19:53:35 +08:00
ouyangxq 2c330bf9df test(lastleg): derive merchant relabel scope from profile receipt 2026-10-18 19:29:35 +08:00
ouyangxq 4bef8577fa test(lastleg): verify dispatch repair with current-run HTTP evidence 2026-10-18 19:21:12 +08:00
ouyangxq db4354bda8 fix(lastleg): align return receipt fixtures and schema totals 2026-10-18 18:45:47 +08:00
ouyangxq aa33514ec6 fix(lastleg): repair dispatch cancellation and return execution contracts 2026-10-18 16:34:14 +08:00
ouyangxq 66a0beb653 test(lastleg): mark diagnostic fixture mapper for registration contract 2026-10-18 11:49:47 +08:00
ouyangxq 5cd108b39a chore(lastleg): instrument appointment latency from restored 662dead baseline 2026-10-18 11:43:10 +08:00
ouyangxq b0acbfbb34 perf(lastleg): batch appointment requirement generation reads 2026-10-17 12:54:39 +08:00
ouyangxq 662dead053 chore(lastleg): diagnose work fact synchronization phases 2026-10-17 11:37:57 +08:00
ouyangxq 2971d6f7f0 perf(lastleg): reuse warehouse directory within sync generation 2026-10-17 09:57:54 +08:00
ouyangxq 81e4123b10 perf(lastleg): limit transport reconciliation facts and trace sync phases 2026-10-17 09:11:00 +08:00
ouyangxq 732cf99007 perf(lastleg): reuse work items within synchronization batch 2026-10-17 08:30:11 +08:00
ouyangxq 5fd74e9c8c perf(lastleg): batch receiving candidates and trace slow work invalidation 2026-10-17 07:49:56 +08:00
ouyangxq af97cd17d0 Merge production prebuild test fixes into frozen release 2026-10-16 18:44:42 +08:00
ouyangxq 5f5133bd68 test(lastleg): separate stable carton marks from return numbers 2026-10-16 18:44:32 +08:00
ouyangxq 339c70bb56 test(lastleg): isolate SQL semantic tests in bounded CI 2026-10-16 18:41:25 +08:00
ouyangxq e4bf573146 test: integrate production preflight contract correction 2026-10-16 18:14:49 +08:00
ouyangxq 4348296daf test(lastleg): validate shared appointment contract references 2026-10-16 18:14:37 +08:00
liuyihe 9ea03eb540 提交 2026-10-16 17:43:07 +08:00
liuyihe c9186c37a5 提交 2026-10-16 16:56:33 +08:00
liuyihe 6211d4ae37 提交 2026-10-16 16:53:32 +08:00
ouyangxq fee0268391 fix(appointment): link customer work to original batch requirement 2026-10-16 16:38:33 +08:00
ouyangxq 38a1f1079a Merge remote-tracking branch 'origin/test' into test 2026-10-16 16:21:51 +08:00
ouyangxq 58685945b1 fix(appointment): exclude cancelled batches without active allocations 2026-10-16 16:21:51 +08:00
liuyihe ecded9e58b 提交 2026-10-16 16:18:39 +08:00
liuyihe 488c0318ff 提交 2026-10-16 16:18:28 +08:00
ouyangxq 000125c3ec chore(sql): normalize merged initialization ending 2026-10-16 15:45:18 +08:00
ouyangxq 0708bbad2e Merge remote-tracking branch 'origin/test' into test
# Conflicts:
#	deploy/sql/README.md
#	deploy/sql/海外区域统一增量-MySQL8.sql
2026-10-16 15:44:59 +08:00
ouyangxq 30f48bd926 feat(appointments): support scoped receiving and stable trip continuation 2026-10-16 15:43:04 +08:00
liuyihe 358feb5567 提交 2026-10-16 13:57:39 +08:00
liuyihe 5855529e97 提交 2026-10-16 11:41:24 +08:00
ouyangxq 4e2ea89403 fix(selfop): retry continuous outbound failures with bounded diagnostics 2026-10-16 11:05:37 +08:00
ouyangxq a3a5862f89 fix(delivery): use frozen relabel destination in SKU details 2026-10-15 21:48:18 +08:00
ouyangxq b8739eebf0 fix(lastleg): preserve return relabel delivery metadata and measurements 2026-10-15 21:40:37 +08:00
ouyangxq 607aaf74dc fix(lastleg): route all relabel pickups directly to outbound and verify final schemas 2026-10-15 21:05:34 +08:00
ouyangxq 4277601999 perf(firstleg): scope appointment fact source aggregation by container 2026-10-15 20:03:41 +08:00
ouyangxq ae33780dae fix(order): project exact customer fulfillment progress in detail 2026-10-15 19:46:34 +08:00
ouyangxq 097bec33b1 perf(lastleg): batch live appointment work facts and filter candidates 2026-10-15 19:10:57 +08:00
ouyangxq abf4e45e39 perf(firstleg): batch appointment source and execution facts 2026-10-15 19:08:18 +08:00
ouyangxq 2631bd3af4 fix(dispatch): use supported capacity for default draft groups 2026-10-15 18:45:05 +08:00
liuyihe cda5f3dc32 提交 2026-10-15 18:25:53 +08:00
ouyangxq caff693ad8 test(appointments): verify stable batches and current HTTP contracts 2026-10-15 18:13:52 +08:00
ouyangxq 204ae10737 fix(dispatch): persist and reuse the resolved draft group 2026-10-15 18:12:55 +08:00
ouyangxq 23ce486539 fix(firstleg): reuse source manifest within work fact batches 2026-10-15 17:52:56 +08:00
ouyangxq 994215d981 fix(appointment): keep source inspection in work transaction 2026-10-15 17:34:45 +08:00
ouyangxq 42679d4d54 fix(appointment): retain unbookable cargo as blocked work 2026-10-15 17:28:22 +08:00
ouyangxq 14b8cc625b fix(appointment): retain safe synchronization failure diagnostics 2026-10-15 17:18:09 +08:00
ouyangxq 4f9a63fb01 merge: integrate TEST updates and verify appointment release 2026-10-15 17:08:30 +08:00
ouyangxq a1d5ebdab2 feat(appointment): integrate work calendar and stable cargo batches 2026-10-15 16:55:57 +08:00
liuyihe 628b1a5732 提交 2026-10-15 16:30:37 +08:00
liuyihe c6bd6af5ce 提交 2026-10-15 15:42:10 +08:00
liuyihe 095a95e64a Merge remote-tracking branch 'origin/test' into test
# Conflicts:
#	deploy/sql/README.md
2026-10-15 15:02:34 +08:00
ouyangxq dbb93a06c2 feat(selfop): provide frozen turnover batches for backend Excel exports 2026-10-15 15:00:03 +08:00
ouyangxq 22d79e08f9 fix(selfop): restore unique same-second inventory chains 2026-10-15 14:13:41 +08:00
ouyangxq 235b9469d9 fix(selfop): remove obsolete execution binding on config refresh 2026-10-15 13:48:49 +08:00
ouyangxq 99215bbb79 feat(selfop): reconstruct historical inventory and publish qualified turnover 2026-10-15 13:30:37 +08:00
ouyangxq d81d9cbda4 feat(selfop): add bounded inventory flow diagnostic through account egress 2026-10-15 11:37:49 +08:00
ouyangxq e19d303a76 feat(selfop): filter turnover by multiple owners across all aggregates 2026-10-15 11:12:44 +08:00
ouyangxq 72d7ed6962 feat(selfop): hide known zero inventory before turnover pagination 2026-10-15 10:29:42 +08:00
liuyihe d663d53747 Merge remote-tracking branch 'origin/test' into test 2026-10-14 20:27:06 +08:00
ouyangxq b7fb3a5054 feat(selfop): add owner inventory volume top ten chart 2026-10-14 18:17:09 +08:00
ouyangxq 7079455eb0 fix(selfop): support inventory and outbound volume sorting 2026-10-14 18:11:02 +08:00
liuyihe 471e50ffe1 提交 2026-10-14 18:01:25 +08:00
ouyangxq 8bc8518496 feat(selfop): calculate inventory and outbound volume by owner and SKU 2026-10-14 17:48:52 +08:00
ouyangxq 814eb11d2f fix(selfop): avoid loading routing manifests during idle planning 2026-10-14 17:26:41 +08:00
ouyangxq ab903fe60f Merge latest test into dev [skip ci]
# Conflicts:
#	deploy/sql/README.md
#	deploy/sql/海外区域统一增量-MySQL8.sql
2026-10-14 17:26:37 +08:00
ouyangxq 1112d11c80 fix(lastleg): pace master data pages without holding collection leases 2026-10-14 16:56:40 +08:00
ouyangxq 63883f4ffd fix(integration): read master data source pagination fields 2026-10-14 16:43:11 +08:00
ouyangxq d311d9dc8d feat(selfop): sync versioned owner and product directories 2026-10-14 16:15:39 +08:00
ouyangxq f4fd56f1ce Merge latest test into dev preserving initialization fixes [skip ci] 2026-10-14 15:55:04 +08:00
ouyangxq 2b3b13913b feat(self-operated): expose and sort transit stock in turnover views 2026-10-14 14:08:52 +08:00
ouyangxq 5ace2fb584 Merge remote-tracking branch 'origin/test' into test 2026-10-14 14:01:40 +08:00
ouyangxq 6e7d922853 fix(self-operated): align turnover sales eligibility and known zero stock 2026-10-14 14:01:39 +08:00
liuyihe 1425b284dd 提交 2026-10-14 13:57:04 +08:00
liuyihe d017f5d881 提交 2026-10-14 13:42:58 +08:00
ouyangxq bc9c78122b feat(self-operated): support selected-day analytics and global quantity sorting 2026-10-14 12:44:15 +08:00
liuyihe 287edcf32b 提交 2026-10-14 12:13:26 +08:00
liuyihe 166280da5a 贴标申请,仓储转运 2026-10-14 11:37:38 +08:00
liuyihe e14bdb4f49 贴标申请,仓储转运 2026-10-14 11:36:35 +08:00
ouyangxq 83dc6dea3f 优化 2026-10-14 11:03:02 +08:00
ouyangxq 6193043880 perf(selfop): batch list checkpoints and publish each completed day 2026-10-14 10:09:16 +08:00
ouyangxq 674914c651 fix(selfop): allow explicit retry of failed continuous inventory roots 2026-10-14 09:46:49 +08:00
ouyangxq 2e2a354ba6 fix(selfop): retain turnover precision and scope reused publication runs 2026-10-14 09:37:00 +08:00
ouyangxq 6024faa9aa fix(selfop): isolate inventory paging and bound unstable scan retries 2026-10-14 09:36:52 +08:00
ouyangxq 0aa188445b fix(selfop): allow controlled continuous inventory retry in test 2026-10-14 09:29:08 +08:00
ouyangxq c0e1d42b62 perf(selfop): route collected orders once after resolution 2026-10-14 08:55:58 +08:00
ouyangxq c640fa2d0d feat(selfop): prefer list SKU quantities with targeted detail fallback 2026-10-14 08:15:16 +08:00
ouyangxq 4ae158c93e fix(selfop): preserve exact diagnostic decimals in JSON evidence 2026-10-14 05:05:23 +08:00
ouyangxq 913b9e9262 fix(selfop): persist bounded next-root quantity diagnostics 2026-10-14 04:59:05 +08:00
ouyangxq 765b6e6208 perf(lastleg): reduce inventory turnover query aggregation work 2026-10-14 03:36:54 +08:00
ouyangxq 16d0b9c80f fix: finalize inventory turnover queries and reuse snapshot aggregation 2026-10-14 01:51:51 +08:00
ouyangxq d029f9a19e 优化 2026-10-14 00:07:12 +08:00
ouyangxq 3d3507a770 feat(selfop): add bounded independent quantity diagnostic capture 2026-10-13 23:40:44 +08:00
ouyangxq 54f79b3902 feat(self-operated): expose private runtime configuration snapshots 2026-10-13 22:23:48 +08:00
ouyangxq 1d0b0918ba Merge commit 'b033a89aaf0af286cdf60cb2cdc2d51fd02deab1' into test 2026-10-13 21:51:00 +08:00
ouyangxq f1fb5662ca fix(self-operated): remove business collection deadline limits 2026-10-13 21:39:51 +08:00
ouyangxq 242a76221e feat(self-operated): persist continuous collection and frozen coverage 2026-10-13 21:13:31 +08:00
liuyihe b033a89aaf 贴标申请,仓储转运 2026-10-13 20:51:44 +08:00
ouyangxq c2bb769676 fix(selfop): remove business request count quotas 2026-10-13 20:02:07 +08:00
ouyangxq f151e74f5c fix(self-operated): rank hot SKUs for selected dates 2026-10-13 18:44:49 +08:00
liuyihe 38958c891a 贴标申请,仓储转运 2026-10-13 18:05:30 +08:00
liuyihe c07131b26f 贴标申请,仓储转运 2026-10-13 17:42:54 +08:00
ouyangxq ec7e9b36ec Merge remote-tracking branch 'origin/test' into test 2026-10-13 16:53:38 +08:00
ouyangxq 146d468ff4 feat: expose reliable collection activity and stage estimates 2026-10-13 16:50:53 +08:00
liuyihe 55764497c8 贴标申请,仓储转运 2026-10-13 16:48:54 +08:00
liuyihe 03601ab0aa 贴标申请,仓储转运 2026-10-13 15:55:31 +08:00
liuyihe c84b5f460a 贴标申请,仓储转运 2026-10-13 14:46:22 +08:00
liuyihe 89a05a2348 贴标申请,仓储转运 2026-10-13 13:46:16 +08:00
ouyangxq cebcdfb66e fix: preserve complete historical dashboard days 2026-10-13 13:43:13 +08:00
ouyangxq 496fc11b0a feat(selfop): support complete inventory sales windows 2026-10-13 12:35:39 +08:00
ouyangxq 39f914b3b5 fix(lingxing): allow bounded configurable business request pacing 2026-10-13 11:46:57 +08:00
ouyangxq 6e9466d0b4 Merge remote-tracking branch 'origin/test' into test 2026-10-13 11:19:19 +08:00
ouyangxq ed8c9b1543 test(selfop): verify every coverage bucket identity and pagination 2026-10-13 11:18:40 +08:00
liuyihe ce4eb4e8a2 Merge remote-tracking branch 'origin/test' into test 2026-10-13 11:00:37 +08:00
liuyihe 3b86a6d779 贴标申请,仓储转运 2026-10-13 11:00:31 +08:00
liuyihe c920d954ef 贴标申请,仓储转运 2026-10-13 10:47:58 +08:00
liuyihe f7a871e3e4 贴标申请,仓储转运 2026-10-13 10:47:52 +08:00
ouyangxq c13bf96cb9 feat(selfop): expose opt-in test query timing headers 2026-10-13 10:42:19 +08:00
ouyangxq 4c9878c338 feat(selfop): add opt-in test query timing diagnostics 2026-10-13 10:31:46 +08:00
ouyangxq d08ed2f2ca perf(selfop): seek latest outbound date for inventory page 2026-10-13 10:11:06 +08:00
ouyangxq 2d7b6e4381 test(selfop): verify warehouse summary build transactions 2026-10-13 10:04:06 +08:00
ouyangxq 25db631ff7 perf(selfop): build versioned warehouse daily sales summaries 2026-10-13 09:40:55 +08:00
ouyangxq ca4e7086e9 perf(selfop): skip unavailable dashboard aggregates 2026-10-13 03:45:01 +08:00
ouyangxq 7ae91eb276 fix(lastleg): persist bounded test collection session budgets 2026-10-13 03:08:36 +08:00
ouyangxq 1fdd978084 perf(selfop): paginate sales summary before day coverage enrichment 2026-10-13 02:46:20 +08:00
ouyangxq 341b7635c7 perf(selfop): materialize sales bounds without redundant deduplication 2026-10-13 02:31:46 +08:00
ouyangxq afd3bbaa3d perf(selfop): page stock keys before sales enrichment 2026-10-13 02:13:56 +08:00
ouyangxq 2c8c26490a perf(selfop): stabilize profile candidate join order 2026-10-13 02:00:14 +08:00
ouyangxq bf693fab6b perf(selfop): index staged observation joins and verify candidate builds 2026-10-13 01:47:17 +08:00
ouyangxq d689112656 perf(selfop): share dashboard aggregates and cover daily trend reads 2026-10-13 01:29:14 +08:00
ouyangxq 99d206eb00 perf(selfop): cover range reads and stream yearly SKU aggregation 2026-10-13 01:05:52 +08:00
ouyangxq a41c271a67 feat(selfop): add warehouse dashboard and exact inventory drilldowns 2026-10-13 00:45:39 +08:00
ouyangxq a84a9b730e fix(selfop): publish proven manual sales windows for configured warehouses 2026-10-13 00:30:48 +08:00
ouyangxq ae1a809357 fix(selfop): publish complete single-pass inventory observations 2026-10-13 00:10:12 +08:00
ouyangxq 51c0d77fd4 fix(selfop): stabilize inventory and order quantity fingerprints 2026-10-12 23:50:39 +08:00
ouyangxq 86e05a77e6 fix(selfop): allow controlled manual collection in test 2026-10-12 23:21:57 +08:00
ouyangxq 6e9f4b7932 perf(selfop): aggregate daily facts before SKU coverage calculations 2026-10-12 23:06:45 +08:00
ouyangxq cbc316b34d fix(selfop): expose scoped validation tasks without published scopes 2026-10-12 22:57:18 +08:00
ouyangxq b71380de31 fix(selfop): discover warehouses with bounded validation and optimize queries 2026-10-12 22:22:08 +08:00
ouyangxq e42d358e71 Merge remote-tracking branch 'origin/test' into test 2026-10-12 21:25:28 +08:00
ouyangxq 06de9b1c93 test(selfop): verify publication transactions against opt-in TEST MySQL 2026-10-12 21:24:56 +08:00
liuyihe fb98b1a8ee 贴标申请,仓储转运 2026-10-12 21:18:29 +08:00
ouyangxq 8526697b08 fix: quote inline descriptions in Lingxing OpenAPI contract 2026-10-12 21:10:18 +08:00
ouyangxq b702ed9f45 test: verify self-operated analytics through TEST Gateway and synthetic query fixtures 2026-10-12 20:59:54 +08:00
ouyangxq d4dc4f875a fix: register current synchronous service dependencies and guard callback boundaries 2026-10-12 20:58:16 +08:00
ouyangxq d782b2cd24 fix: reject unavailable self-operated collection commands before initialization 2026-10-12 20:45:31 +08:00
ouyangxq 3b137f8946 Merge remote-tracking branch 'origin/test' into test 2026-10-12 20:27:50 +08:00
ouyangxq a1f1d93902 feat: add self-operated analytics and enforce Lastleg module boundaries 2026-10-12 20:27:35 +08:00
liuyihe 0eb58f9dd1 贴标申请,仓储转运 2026-10-12 18:19:08 +08:00
liuyihe 9b333df720 贴标申请,仓储转运 2026-10-12 18:11:23 +08:00
liuyihe 46507e41c6 贴标申请,仓储转运 2026-10-12 18:01:16 +08:00
ouyangxq c06e788b0f fix: complete single-order acceptance and add Lingxing WMS integration 2026-10-12 16:09:21 +08:00
liuyihe 6c4ee2bfde 贴标申请,仓储转运 2026-10-12 15:57:47 +08:00
liuyihe a1cd47dabd 贴标申请,仓储转运 2026-10-12 13:42:23 +08:00
liuyihe ea3aae6093 Merge remote-tracking branch 'origin/test' into test 2026-10-12 13:42:13 +08:00
ouyangxq c545a77190 test(firstleg): keep sailing copy dates in the future 2026-10-12 11:48:49 +08:00
ouyangxq 3cb4a5676a fix(firstleg): use a single cabinet business number 2026-10-12 11:44:58 +08:00
liuyihe b4b00ae3dd 贴标申请,仓储转运 2026-10-12 11:18:58 +08:00
ouyangxq 89c15a7e60 docs: record verified initialization seed counts [skip ci] 2026-10-12 10:35:06 +08:00
ouyangxq b216246335 fix: finalize empty database initialization baseline [skip ci] 2026-10-12 10:20:55 +08:00
ouyangxq 29b562ebf5 fix: require dedicated appointment cancellation command 2026-10-11 14:14:51 +08:00
ouyangxq 43494cbfa3 fix: retain appointment cancellation reasons and query history 2026-10-11 13:26:00 +08:00
ouyangxq d4b72594ab test(procurement): reject receipt corrections after shipment 2026-10-10 19:52:36 +08:00
ouyangxq 15ab355dc2 fix(appointments): include destination timezone in batch preview 2026-10-10 18:37:38 +08:00
liuyihe c6471d06ff 贴标申请,仓储转运 2026-10-10 16:58:33 +08:00
liuyihe 05302490fb 贴标申请,仓储转运 2026-10-10 16:15:17 +08:00
ouyangxq 8e36bd90ef Merge remote-tracking branch 'origin/test' into test 2026-10-10 15:34:19 +08:00
ouyangxq 0ac2b849c3 feat(appointments): track forecast impacts and handling history 2026-10-10 15:33:13 +08:00
liuyihe 84864c2ecf Merge remote-tracking branch 'origin/test' into test 2026-10-10 14:47:42 +08:00
liuyihe d1b4cc9c0f 贴标申请,仓储转运 2026-10-10 14:47:31 +08:00
ouyangxq 331f750871 fix(procurement): reject unsupported purchase payload fields 2026-10-10 14:44:23 +08:00
ouyangxq 25aae44918 Merge remote-tracking branch 'origin/test' into test 2026-10-10 14:19:42 +08:00
ouyangxq 4b5b639adc feat(appointments): expose authoritative arrival forecast observations 2026-10-10 14:10:17 +08:00
ouyangxq 71111bf21b feat(appointments): discover external booking candidates through fleet boundary 2026-10-10 14:03:42 +08:00
ouyangxq 369613a2e7 test(procurement): verify pending commands and late successful receipts 2026-10-10 14:01:50 +08:00
liuyihe 12a5093b41 贴标申请,仓储转运 2026-10-10 13:48:25 +08:00
ouyangxq 721d92ac7b fix: include overlapping overnight FBA appointments in calendar 2026-10-10 12:40:35 +08:00
ouyangxq 6e1d90aafe test: cover fleet calendar authorization in integrated contracts 2026-10-10 12:33:39 +08:00
ouyangxq 3a252dfc70 feat(fleet): expose bounded origin-aware trip calendar 2026-10-10 12:15:02 +08:00
ouyangxq 83049ed9e5 feat(warehouse): provide bounded internal historical metadata batch 2026-10-10 11:52:29 +08:00
ouyangxq 46ae8ab1c6 feat(appointments): expose bounded terminal pickup calendar 2026-10-10 11:43:47 +08:00
ouyangxq 47dc9e8bf6 fix(warehouse): use location time zones for appointment scheduling 2026-10-10 11:02:49 +08:00
ouyangxq c4c1bddeec feat(appointments): unify pre-inbound planning and confirmed FBA bookings 2026-10-10 10:38:06 +08:00
ouyangxq 24f5ccf7ff feat(appointment): share pickup and receiving followups by container 2026-10-10 08:42:01 +08:00
ouyangxq 885cac13a4 test(fleet): exercise simplified intake HTTP workflow 2026-10-10 08:07:27 +08:00
ouyangxq 2c970f252d fix(lastleg): snapshot customer name for incoming cargo 2026-10-10 08:05:23 +08:00
ouyangxq 373f1b2c18 test(procurement): verify fee source recovery without duplicate debit 2026-10-10 03:43:53 +08:00
ouyangxq c0fbe2698b fix(procurement): read MySQL fulfillment timestamps with JDBC types 2026-10-10 02:42:59 +08:00
ouyangxq a973be2103 feat(procurement): implement sample fulfillment and settled fee workflow 2026-10-10 02:19:38 +08:00
ouyangxq 040f3bbd93 fix(lastleg): preserve followup timestamp on replay 2026-10-09 22:31:51 +08:00
ouyangxq 0d75e311d0 feat(lastleg): connect appointment followups and fleet intake 2026-10-09 22:04:49 +08:00
ouyangxq 1202cafadf feat(firstleg): count actionable customer customs cabinets 2026-10-09 18:14:17 +08:00
ouyangxq d11abb78ac Merge branch 'test' of http://8.163.30.128:10082/java-project/baiyue-firstleg into test 2026-10-09 17:36:25 +08:00
ouyangxq 261cdc87cb 优化 2026-10-09 17:36:24 +08:00
liuyihe 7e37770f26 贴标申请,仓储转运 2026-10-09 17:27:02 +08:00
ouyangxq 74bf0e73c5 test: prepare isolated gray domestic PDA H5 tasks without auto-picking 2026-10-09 17:21:56 +08:00
ouyangxq 023b28e31f fix(lastleg): normalize source key comparisons across MySQL collations 2026-10-09 16:31:19 +08:00
ouyangxq f6605a1ab0 fix(sql): keep return counting in clean initialization definitions 2026-10-09 16:20:50 +08:00
ouyangxq c1aaaf207a Merge remote-tracking branch 'origin/test' into test
# Conflicts:
#	lastleg-service/src/main/resources/openapi/overseas-business-v1.yaml
2026-10-09 16:13:28 +08:00
ouyangxq 3b5454fb99 feat(lastleg): query commercial sources independently of fulfillment types 2026-10-09 16:12:02 +08:00
liuyihe bac7e0b214 贴标申请,仓储转运 2026-10-09 15:53:30 +08:00
liuyihe 1e15a2838a 贴标申请,仓储转运 2026-10-09 15:25:00 +08:00
liuyihe b4b3ae452c 贴标申请,仓储转运 2026-10-09 15:20:15 +08:00
ouyangxq c4a7c2a22d feat(fleet): match cargo stops when binding dispatches 2026-10-09 15:04:51 +08:00
liuyihe 2ec5414b39 贴标申请,仓储转运 2026-10-09 11:49:17 +08:00
ouyangxq 7fd7699f6e test: verify customer isolation on shared appointment items 2026-10-09 11:10:34 +08:00
ouyangxq fddae546a0 fix: avoid existing test permission IDs for appointment menu 2026-10-09 11:05:24 +08:00
ouyangxq e94271706c feat: support shared FBA appointments and arrival calendar 2026-10-09 11:01:39 +08:00
ouyangxq 4f9433c39a test(pda): bind receiving fixtures to exact source container booking 2026-10-08 21:36:53 +08:00
ouyangxq 6b6d217774 Merge branch 'test' of http://8.163.30.128:10082/java-project/baiyue-firstleg into test 2026-10-08 20:16:30 +08:00
ouyangxq ed99ecba1c 优化 2026-10-08 19:09:30 +08:00
liuyihe 315a510969 贴标申请,仓储转运 2026-10-08 18:27:52 +08:00
ouyangxq 2c0f55d9b7 fix(config): restore required IAM Nacos imports 2026-10-08 18:18:33 +08:00
liuyihe a24a223a35 贴标申请,仓储转运 2026-10-08 18:10:06 +08:00
ouyangxq 3a9ee57e53 fix(lastleg): restore fulfillment safeguards and reconcile return regressions 2026-10-08 18:08:42 +08:00
ouyangxq c1ddb658bd 优化 2026-10-08 17:07:57 +08:00
liuyihe 16d2d9111a 贴标申请,仓储转运 2026-10-08 15:55:47 +08:00
ouyangxq e09f8785e1 fix(lastleg): export all pages when pagination caps batch size 2026-10-08 15:31:56 +08:00
ouyangxq d89198ee21 fix(lastleg): classify all first-leg orders by sailing stage 2026-10-08 15:23:34 +08:00
ouyangxq 27a89a6ff3 fix(lastleg): filter domestic first-leg orders by sailing stage 2026-10-08 15:12:47 +08:00
ouyangxq 136a2bc3b5 fix(lastleg): expose dispatch reviews and filter available capacity offers 2026-10-08 14:11:55 +08:00
ouyangxq 1cb1f2d0c0 fix(schema): allow warehouse transfer inbound plan source 2026-10-08 08:45:56 +08:00
ouyangxq c216f393a7 fix(fleet): preserve unknown commercial facts for pooled freight 2026-10-07 19:38:56 +08:00
ouyangxq cfdedd8861 test(fleet): close pool cancellation capacity and isolation matrices 2026-10-07 18:11:55 +08:00
ouyangxq dbdd7bb066 test(fleet): isolate pick commands per customer dispatch 2026-10-07 17:21:58 +08:00
ouyangxq 27c0fe80f6 fix(fleet): align customer identity collation for fulfillment joins 2026-10-07 17:16:50 +08:00
ouyangxq 5c6cd6af44 test(fleet): use local wall time for planned delivery fixture 2026-10-07 16:54:05 +08:00
ouyangxq 001219460f feat(fleet): add internal capacity pool and warehouse fulfillment bridge 2026-10-07 16:46:56 +08:00
ouyangxq 564cfa107c fix(audit): expose real actuator health status 2026-10-07 11:48:05 +08:00
ouyangxq 7ff6512864 test(deploy): verify unified TEST and gray environment contracts 2026-10-07 11:17:28 +08:00
ouyangxq ec0c06ff88 fix(deploy): publish unified services to temporary gray environment 2026-10-07 11:02:47 +08:00
ouyangxq 6346e7bd95 fix(lastleg): use reassigned FBA destination in inbound plans 2026-10-06 10:07:23 +08:00
ouyangxq bc8c1467ee fix(lastleg): return persisted grouped dispatch replay facts 2026-10-06 00:52:48 +08:00
ouyangxq 60543adf63 fix(lastleg): prevent transfer business number collisions 2026-10-06 00:20:43 +08:00
ouyangxq 4bde1a8352 fix(firstleg): avoid shared lock upgrades when initializing customs 2026-10-05 17:03:01 +08:00
ouyangxq 261c720c77 fix(firstleg): read concurrent customs initialization with current row lock 2026-10-05 16:35:22 +08:00
ouyangxq 090f57bfb1 fix(firstleg): stop retrying deterministic completion preflight conflicts 2026-10-05 16:05:06 +08:00
ouyangxq adaac7aca4 fix(finance): include billed sources in trip financial attribution 2026-10-05 12:35:38 +08:00
ouyangxq f13d340620 fix(finance): retain trip receipts after partial and full payment 2026-10-05 12:22:18 +08:00
ouyangxq 8ac2400043 docs(discard): align confirmation semantics and current coordination checks 2026-10-05 12:07:02 +08:00
ouyangxq 0c26e1e99d test(discard): expect approval to reserve stock until PDA confirmation 2026-10-05 12:00:09 +08:00
ouyangxq 7577650eee feat(fleet): read attributable trip receivables and receipts from finance 2026-10-05 11:51:47 +08:00
ouyangxq 113d2b1fec fix(discard): recover completed PDA confirmation without duplicate writes 2026-10-05 08:10:43 +08:00
ouyangxq 5864e62443 test(lastleg): include transfer reservations in hold concurrency fixture 2026-10-05 07:38:43 +08:00
ouyangxq 5b4d9624a9 fix(inventory): enforce warehouse transfer reservation ownership 2026-10-05 07:24:53 +08:00
ouyangxq 548a133de6 fix(transfer): preserve inbound ownership and expose durable receipt recovery 2026-10-05 06:34:23 +08:00
ouyangxq 47acfc991b fix(lastleg): keep unconfirmed container resources recoverable 2026-10-05 05:26:01 +08:00
ouyangxq 3afb3bcfd1 fix(test): stream resource fault configuration through stdin 2026-10-05 04:52:38 +08:00
ouyangxq 5ea1c277bc fix(test): retain safe inbound resource fault diagnostics 2026-10-05 04:11:33 +08:00
ouyangxq 6fc32574c7 fix(lastleg): exclude cancelled dispatches from inbound status 2026-10-05 04:09:15 +08:00
ouyangxq d7981f7575 test(inbound): add recoverable scoped resource fault runtime 2026-10-05 03:48:29 +08:00
ouyangxq cc1fa74c63 test(dispatch): register grouped draft in browser API boundary 2026-10-05 03:45:05 +08:00
ouyangxq c194dd2862 fix(transfer): reserve receipt resources with durable recovery intents 2026-10-05 03:37:13 +08:00
ouyangxq 3f8ac23aba feat(transfer): persist receipt resource intents in schema baseline 2026-10-05 03:13:31 +08:00
ouyangxq 8e46ba6985 fix(dispatch): preserve loading groups in atomic draft pre-arrangement 2026-10-05 03:12:44 +08:00
ouyangxq e1cb01e6fb test(transfer): preserve rejected bind evidence and enforce completed unit facts 2026-10-05 02:50:29 +08:00
ouyangxq b371fab53d fix(transfer): settle linked relabel fees after final receipt 2026-10-05 02:47:22 +08:00
ouyangxq 9e4a838ba1 test(inbound): add scoped one-shot resource confirmation fault proxy 2026-10-05 02:45:27 +08:00
ouyangxq 49ac9222b5 fix(shipping): replay immutable carrier facts without updates 2026-10-05 02:32:06 +08:00
ouyangxq a0a4e03da9 fix(inbound): expose original command for resource confirmation recovery 2026-10-05 02:13:22 +08:00
ouyangxq b000b056d1 feat(orders): expose customer-owned loaded transport facts 2026-10-05 01:06:11 +08:00
ouyangxq 5ad3fec449 test(firstleg): preserve partial and skipped loading lifecycle regression 2026-10-05 00:44:28 +08:00
ouyangxq 67c528c3e2 test(fba): add scoped transfer lifecycle and remaining-node evidence helpers 2026-10-05 00:37:18 +08:00
ouyangxq 4bded73040 fix(iam): register stock transfer execution permission 2026-10-04 22:11:31 +08:00
ouyangxq ea9732749a fix(firstleg): finish only actual picked cargo owners 2026-10-04 20:45:01 +08:00
ouyangxq 1302275b71 docs(lastleg): group transfer operations with platform contracts 2026-10-04 20:29:30 +08:00
ouyangxq 76f02d8d45 fix(lastleg): complete stock transfer through linked relabel 2026-10-04 20:21:16 +08:00
ouyangxq 7319fba35a fix(firstleg): settle and validate actual partial loading cargo 2026-10-04 19:01:40 +08:00
ouyangxq 22133fe604 fix(firstleg): project completed loading lists from actual picked cargo 2026-10-04 18:04:21 +08:00
ouyangxq cbd5dc0ca2 perf(test): normalize report secrets once per redaction 2026-10-04 15:00:24 +08:00
ouyangxq 055cd29e29 fix(test): resolve loading customer credentials per run 2026-10-04 14:40:40 +08:00
ouyangxq 791d1a650f test(fleet): verify same-route costs from three real pallet sources 2026-10-04 12:07:37 +08:00
ouyangxq dc993c9713 feat(lastleg): retain receipt and redispatch history on exceptions 2026-10-04 10:45:05 +08:00
ouyangxq 446fad33d0 fix(test): require precise database lock conflict during rollback 2026-10-04 10:44:33 +08:00
ouyangxq ba4179b28c fix(test): wait for database timeout before judging fleet rollback 2026-10-04 10:37:43 +08:00
ouyangxq 8a735020e3 test(fleet): verify two-customer departure rollback in TEST 2026-10-04 10:32:26 +08:00
ouyangxq c46a68e32b fix(delivery): project current dispatched cargo instead of whole order SKU quantities 2026-10-04 02:54:52 +08:00
ouyangxq 9cd709edee test(lastleg): verify MySQL hold and full PDA picking races 2026-10-04 01:58:02 +08:00
ouyangxq e8cf5e70ba test(order): verify warehouse faults and await recovery projection 2026-10-04 00:04:36 +08:00
ouyangxq 3f6083ff02 fix(acceptance): keep safe fleet metadata clear of sensitive report fields 2026-10-03 23:58:47 +08:00
ouyangxq 19e9913f30 fix(acceptance): target deployed TEST Firstleg container for read-only audits 2026-10-03 23:39:06 +08:00
ouyangxq 1f7ddf7bc9 fix(delivery): expose signed quantity and publish correct signature instant 2026-10-03 23:09:24 +08:00
ouyangxq 0c8ede3889 feat(test): continue reviewed return source sample with pre-signature audit 2026-10-03 22:13:15 +08:00
ouyangxq 6dbc899626 fix(lastleg): resolve mixed-pallet return sources by dispatch snapshots 2026-10-03 21:54:07 +08:00
ouyangxq 48faf43e37 fix(test): bind fleet statistics evidence without optional remarks 2026-10-03 21:26:46 +08:00
ouyangxq 3987562890 feat(test): support scoped Firstleg read-only receiver evidence 2026-10-03 21:24:00 +08:00
ouyangxq 18b39b4378 fix(fleet): reuse reviewed handover vehicle and check formal responses 2026-10-03 21:00:07 +08:00
ouyangxq 99a469e6cf fix(fleet): retain unavailable source cost shares for multi-stop trips 2026-10-03 20:36:09 +08:00
ouyangxq f2e25ec34b fix(fleet): verify dispatch cargo and retain checked costs on continuation 2026-10-03 20:24:56 +08:00
ouyangxq f56c68d952 test(fleet): isolate recovery actor phone generation 2026-10-03 20:13:50 +08:00
ouyangxq 6d922516f6 test(fleet): resume reviewed post-departure acceptance safely 2026-10-03 20:06:02 +08:00
ouyangxq bab967acce test(fleet): preserve unavailable allocations for multi-stop trips 2026-10-03 19:51:29 +08:00
ouyangxq e5af1ba758 test(fleet): handle PDFs from direct print requests 2026-10-03 19:27:03 +08:00
ouyangxq dbc7688516 test(fleet): preserve PDF and direct fetch prerequisite evidence 2026-10-03 19:19:16 +08:00
ouyangxq c516347565 test(fleet): verify booking quote source linkage in TEST 2026-10-03 19:09:22 +08:00
ouyangxq 3bf2612470 test(fleet): journal prerequisite writes and stop on unknown results 2026-10-03 19:07:58 +08:00
ouyangxq 499354ffd2 test(firstleg): align initialization table count with current baseline 2026-10-03 18:43:25 +08:00
ouyangxq 4ff55ba3e0 fix(ci): bound Maven resources and serialize shared builder jobs 2026-10-03 17:52:47 +08:00
ouyangxq 78a4d4a089 test(order): verify rejected XLSX imports preserve scoped database facts 2026-10-03 17:12:49 +08:00
ouyangxq 14696fb1a1 test(order): verify foreign customer addresses rejected in XLSX imports 2026-10-03 17:08:15 +08:00
ouyangxq e49ad96cc4 feat(fleet): attach scoped evidence to measured trip records 2026-10-03 16:38:37 +08:00
ouyangxq d05ff1cd26 test(fleet): cover partial returns and redispatch through real API flow 2026-10-03 16:11:52 +08:00
ouyangxq 8700fc3423 feat(lastleg): redispatch returned cargo and reconcile source signatures 2026-10-03 16:06:54 +08:00
ouyangxq e740047e03 test(order): prove foreign customer address rejection through real API probes 2026-10-03 16:03:34 +08:00
ouyangxq 8f356b1337 test(order): compare import templates across independent test customers 2026-10-03 15:57:36 +08:00
ouyangxq 18935f1379 test(order): verify live import template directories without business writes 2026-10-03 15:53:55 +08:00
ouyangxq bf8a985f22 test(order): add readonly merchant workbench pagination acceptance 2026-10-03 15:46:42 +08:00
ouyangxq dd52bfd043 test(fleet): verify operations from immutable current-run evidence 2026-10-03 15:00:12 +08:00
ouyangxq 780d37e1d2 fix(order): paginate merchant workbench with scoped queue counts 2026-10-03 14:57:51 +08:00
ouyangxq 7d9ee18196 test(fleet): persist independent acceptance requests and current-trip evidence 2026-10-03 14:52:04 +08:00
ouyangxq a6fd0ba3c5 feat(fleet): bind quote versions and reconcile actual return quantities 2026-10-03 13:32:55 +08:00
ouyangxq cb6c54ffe5 feat(fleet): integrate costs quotes operations and in-transit handovers 2026-10-03 12:38:04 +08:00
ouyangxq 274ecac474 fix(scripts): validate current vehicle task registry 2026-10-03 12:15:12 +08:00
ouyangxq eadb431f33 fix(lastleg): read committed order command replays after lock waits 2026-10-03 12:13:34 +08:00
ouyangxq 34cac9c42a test(fleet): verify driver qualification lifecycle 2026-10-03 11:53:58 +08:00
ouyangxq 65c12eaf5e fix: expose the selected container schedule for editing 2026-10-03 11:09:52 +08:00
ouyangxq ba7dcd39f2 test(fleet): verify vehicle care lifecycle independently 2026-10-03 10:54:03 +08:00
ouyangxq d4d5d79070 fix(fleet): compare customer identities without collation conflicts 2026-10-03 10:18:48 +08:00
ouyangxq 30559fd4eb feat(fleet): query vehicle delivery and reassignment history 2026-10-03 10:10:55 +08:00
ouyangxq d93185e750 fix: schedule manual outbox retries in UTC 2026-10-03 09:27:46 +08:00
ouyangxq 588227f181 test: match non-null fee source hash serialization 2026-10-03 09:10:34 +08:00
ouyangxq 4968745764 test: verify fee void ACK loss with original event recovery 2026-10-03 09:02:25 +08:00
ouyangxq 912e69aeb2 test: isolate fee void ACK loss and preserve wallet delivery 2026-10-03 08:54:52 +08:00
ouyangxq 933dd4cdef test(fba): retain parent proof across isolated fragment contexts 2026-10-03 08:20:27 +08:00
ouyangxq 51bf4e5558 test(fba): verify signed delivery ACK loss and original event recovery 2026-10-03 08:13:04 +08:00
ouyangxq ef1eef3278 test(fleet): close shared trips and verify resource release 2026-10-03 07:51:17 +08:00
ouyangxq 2992871975 test(fleet): resolve raw position ownership through its balance 2026-10-03 07:44:17 +08:00
ouyangxq 18d22418ca test(fleet): cover separate customers sharing one physical trip 2026-10-03 07:42:18 +08:00
ouyangxq 4e129a7f4a test(fleet): read complete customer inventory snapshots 2026-10-03 07:25:37 +08:00
ouyangxq d858357c5d test(fba): preserve verified fleet and fulfillment acceptance workflows 2026-10-03 06:26:33 +08:00
ouyangxq c55bd7f4b6 fix(test): align order dependency fault preflight with Nacos v3 2026-10-03 06:21:57 +08:00
ouyangxq bde1cd532e test(order): add isolated dependency failure and recovery verification 2026-10-03 05:25:30 +08:00
ouyangxq 5e683da3b9 feat(fleet): enforce reviewed trips and original fee coverage for dispatch 2026-10-03 04:18:52 +08:00
ouyangxq c50e9f97d1 fix(tooling): validate explicitly registered TEST task ownership 2026-10-03 03:21:14 +08:00
ouyangxq dc05eb3f8a test(lastleg): verify hold cycles against immutable receipt stock 2026-10-03 02:12:42 +08:00
ouyangxq 375e355faf fix(iam): define and grant existing cargo discard approval permission 2026-10-03 01:57:11 +08:00
ouyangxq 7530a95fbd fix(lastleg): scope inventory holds by cycle and align pick lock ordering 2026-10-03 01:34:14 +08:00
ouyangxq 1db6e744c8 test(order): distinguish publication and final price book states 2026-10-03 00:50:52 +08:00
ouyangxq 0319bb51a0 test(order): verify channel removal using customer-owned price books 2026-10-03 00:46:00 +08:00
ouyangxq 4cd51a4955 fix(firstleg): compare stable carrier facts separately from response audit hashes 2026-10-03 00:39:57 +08:00
ouyangxq 9803dcfd2c test(order): verify qualification and isolated negative balance gates 2026-10-03 00:27:45 +08:00
ouyangxq c0c6e21833 style(tests): normalize platform command fixture line endings 2026-10-02 23:37:38 +08:00
ouyangxq d1a13cdc80 test(order): verify platform submit and cancel command boundaries 2026-10-02 23:37:15 +08:00
ouyangxq c55ecabbfb fix(order): enforce versioned platform commands and submissions 2026-10-02 23:14:24 +08:00
ouyangxq 25580dab86 test(order): assert multipart rejection for oversized imports 2026-10-02 21:53:29 +08:00
ouyangxq afb02a0a1b test(order): compare Excel error row as numeric cell 2026-10-02 21:49:48 +08:00
ouyangxq 8c1905418e test(order): accept omitted identifiers on failed import rows 2026-10-02 21:46:39 +08:00
ouyangxq 58dfcc21f6 fix(order): make merchant draft edits and cancellations versioned and idempotent 2026-10-02 21:33:39 +08:00
ouyangxq 4b531c7e22 test: integrate reliable event recovery and receiving evidence 2026-10-02 20:14:17 +08:00
ouyangxq d0df738bba fix(firstleg): keep loading evidence replacement transactional 2026-10-02 19:50:47 +08:00
ouyangxq c9a1e93299 test(firstleg): retry lost evidence uploads with the same command 2026-10-02 19:32:57 +08:00
ouyangxq fa51d9235c test(lastleg): verify return outbound stock and replay invariants 2026-10-02 18:21:55 +08:00
ouyangxq 4655090f76 fix(lastleg): account return carton outbound stock atomically 2026-10-02 17:47:11 +08:00
ouyangxq e15bec3052 fix(lastleg): expose and validate individual return carton picks 2026-10-02 16:59:16 +08:00
ouyangxq 195446e532 fix(order): preserve tax mode in remainder child orders 2026-10-02 15:03:09 +08:00
ouyangxq f532e5ee7e fix(orders): filter tax mode before unified order pagination 2026-10-02 14:44:51 +08:00
ouyangxq 2d73ce9f37 fix(firstleg): preserve confirmed loading history in departure gates 2026-10-02 13:36:39 +08:00
ouyangxq 0b34f99be5 fix(firstleg): validate public port schedule queries with zero scope 2026-10-02 11:16:15 +08:00
ouyangxq 8b9b135ab5 fix(lastleg): align dispatch candidates with relabel reservation gate 2026-10-02 10:34:00 +08:00
ouyangxq a291bd6d45 fix(firstleg): permit platform public port schedule scope 2026-09-30 18:18:57 +08:00
ouyangxq 8a957790fc test(firstleg): cover public schedule dispatch fallback 2026-09-30 17:58:36 +08:00
ouyangxq 8d9d38ed4c fix(firstleg): allow public sailing schedule dispatch fallback 2026-09-30 17:53:12 +08:00
ouyangxq 36a74eb58e fix(firstleg): support platform scope port schedules 2026-09-30 17:41:16 +08:00
ouyangxq 32fb9ec7b3 优化 2026-09-30 17:13:11 +08:00
ouyangxq babc7de4b6 fix(order): require dropship SKU before submission 2026-09-30 16:51:49 +08:00
ouyangxq 94881d23fd test(firstleg): verify ship exception persistence and rollback 2026-09-30 16:35:05 +08:00
ouyangxq b436f3cf1f fix(lastleg): retain location code in immutable stock receipts 2026-09-30 16:35:03 +08:00
ouyangxq 1a7e4800f5 fix(firstleg): derive remainder pallet labels from frozen allocation facts 2026-09-30 15:28:50 +08:00
ouyangxq 71202bce70 fix(order): remove manual completion bypass of fulfillment gate 2026-09-30 15:26:35 +08:00
ouyangxq 22584e7fde 优化 2026-09-30 15:16:39 +08:00
ouyangxq 0919a61661 优化 2026-09-30 14:27:45 +08:00
ouyangxq 39af04f169 优化 2026-09-30 14:01:52 +08:00
ouyangxq 041a27c325 test(fulfillment): reconcile frozen PDA sources and dispatch departure gate 2026-09-30 13:50:13 +08:00
ouyangxq 13028d8a5a fix(order): complete root fulfillment only after effective remainders 2026-09-30 13:49:40 +08:00
ouyangxq bd73c7a326 fix(lastleg): expose authoritative outbound type to PDA 2026-09-30 13:33:53 +08:00
ouyangxq 7f3c0690cd test(shipping): validate platform customer scope separately from business IDs 2026-09-30 13:30:27 +08:00
ouyangxq 74f0d823da test(fulfillment): reconcile exact source positions and inventory balances 2026-09-30 13:15:20 +08:00
ouyangxq eae630e60d fix(shipping): allow platform scope tracking resources 2026-09-30 13:03:33 +08:00
ouyangxq d320e78bbc fix(inventory): resolve returns from immutable shipment SKU facts 2026-09-30 13:00:40 +08:00
ouyangxq 80632ba409 test: strengthen customs and fulfillment acceptance boundaries 2026-09-30 12:58:15 +08:00
ouyangxq 125ca34ba4 fix(outbound): atomically confirm platform mixed-container dispatch and tracking 2026-09-30 12:37:00 +08:00
ouyangxq 5cb2a5145f fix(inventory): normalize FBA return SKU and match durable shipment command identity 2026-09-30 12:32:59 +08:00
ouyangxq 2f0b202590 fix(test): assign authenticated PDA operator in pallet matrix 2026-09-30 12:22:47 +08:00
ouyangxq 4a14001a09 fix(lastleg): restore dispatch returns from immutable pick source and expose customer receipts 2026-09-30 12:09:19 +08:00
ouyangxq c7f880300c fix(customs): keep declaration pending while exceptions remain 2026-09-30 11:48:14 +08:00
ouyangxq 6731be1a6b fix(auth): avoid gap locks when creating accounts 2026-09-30 11:29:02 +08:00
ouyangxq c7945ad8fd fix(api): preserve null data in response envelopes 2026-09-30 11:18:40 +08:00
ouyangxq e5832d5c67 fix(firstleg): refresh inbound picking progress after task completion 2026-09-30 11:03:47 +08:00
ouyangxq f87caf9dd1 fix(lastleg): retry shipping nodes until loading handover exists 2026-09-30 00:23:06 +08:00
ouyangxq 221227204d docs: record deployed test stack and remaining acceptance gates 2026-09-29 23:54:02 +08:00
ouyangxq 88c7fcaca9 优化 2026-09-29 20:17:36 +08:00
ouyangxq 6c0a026cc4 完成单机TEST统一入口与可靠投递回归收尾 2026-09-29 19:48:34 +08:00
ouyangxq 64a70c9f28 优化 2026-09-29 18:48:58 +08:00
ouyangxq 7bc4c52ba4 优化 2026-09-29 18:48:57 +08:00
ouyangxq a530205d2b 优化 2026-09-29 18:47:49 +08:00
ouyangxq c2927ab9a6 优化 2026-09-29 18:47:37 +08:00
ouyangxq 2c60a29e0f 优化 2026-09-29 18:06:18 +08:00
ouyangxq 54ad26c607 优化 2026-09-29 17:55:54 +08:00
ouyangxq 41d8387cd5 test: align acceptance contracts with current initialization and service clients 2026-09-29 15:06:42 +08:00
ouyangxq 60f61d7e0a test: verify virtual receiving location rejects pallet restore 2026-09-29 15:02:15 +08:00
ouyangxq cb8e5fb145 fix(overseas): preserve shared overflow locations during pallet moves 2026-09-29 14:39:11 +08:00
ouyangxq 098cc785d8 Merge remote-tracking branch 'origin/test' into hotfix 2026-09-29 14:18:20 +08:00
ouyangxq 8df0f4e823 fix(overseas): repair pallet transitions and transfer queries 2026-09-29 14:18:20 +08:00
ouyangxq 875d2497f4 docs(firstleg): document price book channel query 2026-09-29 04:27:52 +01:00
ouyangxq 3f00896411 test(firstleg): active price book sailing channels 2026-09-29 04:21:46 +01:00
ouyangxq ed36c43af4 fix(firstleg): source sailing channels from active price book 2026-09-29 04:17:44 +01:00
liuyihe dd792cd792 贴标申请,仓储转运 2026-09-28 17:34:38 +08:00
admin a07bcda2c9 Merge pull request 'Test' (#18) from test into main
Reviewed-on: #18
2026-09-28 17:21:15 +08:00
ouyangxq 5816cd0958 优化 2026-09-28 17:20:39 +08:00
ouyangxq 6a3b6e22e6 优化 2026-09-28 17:17:28 +08:00
admin 3f65b5f0ae Merge pull request 'Test' (#17) from test into main
Reviewed-on: #17
2026-09-28 16:00:14 +08:00
ouyangxq 6954932ce3 fix(db): guard iam permission id conflicts 2026-09-28 15:45:45 +08:00
ouyangxq 08bf8f9a5a feat(db): add production migration chain 2026-09-28 15:39:15 +08:00
ouyangxq 7a4486b1bc test(db): align customs baseline and relabel contracts 2026-09-28 15:06:01 +08:00
ouyangxq f380d4ecb0 Merge remote-tracking branch 'origin/main' into test 2026-09-28 14:47:19 +08:00
ouyangxq 01efb9f157 docs: refresh repository guidance 2026-09-28 14:46:26 +08:00
ouyangxq 736e94c808 fix(firstleg): allow optional sailing booking details 2026-09-28 14:46:19 +08:00
ouyangxq e63cb2b29e fix(customs): add broker submission and receipt action history 2026-09-28 13:21:37 +08:00
ouyangxq ab64699245 fix(firstleg): preserve sailing edit metadata 2026-09-28 12:32:22 +08:00
liuyihe 582ea098f9 Merge remote-tracking branch 'origin/test' into test 2026-09-28 11:59:16 +08:00
liuyihe 2bb98d2047 贴标申请,仓储转运 2026-09-28 11:59:08 +08:00
ouyangxq f6e70ea5d0 feat(firstleg): expose customs receipt history 2026-09-28 09:49:53 +08:00
ouyangxq cf76f7d6d3 fix(firstleg): make sailing copy idempotent 2026-09-27 23:22:56 +08:00
ouyangxq 465d26ba42 feat(firstleg): project sailing display metrics read-only 2026-09-27 22:07:54 +08:00
ouyangxq 2c4f1ea37b fix(firstleg): project latest port schedule in container reads 2026-09-27 22:05:56 +08:00
ouyangxq a6ca9a8a18 fix(firstleg): project latest port schedule in platform containers 2026-09-27 21:56:04 +08:00
ouyangxq f1218cef02 fix(firstleg): avoid limit in latest schedule projection 2026-09-27 21:33:30 +08:00
ouyangxq ac3acfce6a fix(firstleg): project latest port schedule to merchant transport 2026-09-27 21:27:57 +08:00
ouyangxq 4bec639a23 fix(firstleg): open declaration batch after broker execution 2026-09-27 20:48:18 +08:00
ouyangxq 95da66c763 fix(firstleg): align declaration batch routes 2026-09-27 20:31:45 +08:00
ouyangxq ed692ee3bb feat(firstleg): model declaration batches and customs results 2026-09-27 20:23:53 +08:00
ouyangxq 69f2a18a44 fix(firstleg): restore sailing channel duplicate lookup 2026-09-27 19:56:23 +08:00
ouyangxq 48de46739c fix(firstleg): preserve sailing forecast batch facts 2026-09-27 19:23:56 +08:00
ouyangxq 4777f29c5b fix(firstleg): publish sailing edit and history paths 2026-09-27 19:20:10 +08:00
ouyangxq cdff6ba69d test(firstleg): wire sailing facts into measurement fixtures 2026-09-27 19:01:26 +08:00
ouyangxq 4525790534 feat(firstleg): add sailing calendar facts and APIs 2026-09-27 18:55:19 +08:00
ouyangxq 93e2e8fb44 test(customs): align receipt and final file contracts 2026-09-27 08:29:53 +08:00
ouyangxq 9f025edc96 fix(file): allow customs broker receipt files 2026-09-27 01:01:40 +08:00
ouyangxq 703b0f63ce fix(firstleg): restrict broker submission to tax refund cargo 2026-09-26 18:01:40 +08:00
ouyangxq 5f7df21f9c fix(firstleg): include receipt version in confirmation idempotency 2026-09-26 17:54:19 +08:00
ouyangxq 5927034b1c fix(firstleg): freeze customs receipt after broker submission 2026-09-26 17:45:41 +08:00
ouyangxq 5d365f9040 docs(firstleg): expose broker submission timestamp 2026-09-26 17:38:47 +08:00
ouyangxq c266cdee9a feat(firstleg): expose broker submission timestamp 2026-09-26 14:22:29 +08:00
ouyangxq 7680c3e5c2 docs(firstleg): align customs baseline schema 2026-09-26 14:11:28 +08:00
ouyangxq 12f87537d7 test(firstleg): preserve customs container row fixtures 2026-09-26 14:01:39 +08:00
ouyangxq e46d541780 fix(firstleg): map broker submission fields in customs container 2026-09-26 13:57:53 +08:00
ouyangxq ff2386e5bf feat(firstleg): split customs receipt and final document flow 2026-09-26 13:28:18 +08:00
ouyangxq 5a895bf214 test(firstleg): verify manual rollover schedule edit 2026-09-26 12:08:29 +08:00
ouyangxq 8940c4bcff test: align customs acceptance with receipt confirmation 2026-09-25 23:00:00 +08:00
ouyangxq bdf14dffb5 chore: rename Mojia permissions to weighing machine 2026-09-25 21:40:59 +08:00
ouyangxq 4e9663e852 test: cover persisted mismatched Mojia uploads 2026-09-24 17:24:01 +08:00
ouyangxq b31405af43 优化 2026-09-24 17:07:21 +08:00
ouyangxq 0e62b2f45b feat: release empty outbound pallet locations 2026-09-24 16:47:50 +08:00
ouyangxq f7b009bcdf feat: auto assign overseas pallet locations 2026-09-24 16:37:13 +08:00
ouyangxq 91464e031f 修正回执流程测试契约 2026-09-24 16:34:24 +08:00
ouyangxq 2486a01225 完善退税报关回执确认流程 2026-09-24 16:28:58 +08:00
liuyihe bcf44affd7 贴标申请,仓储转运 2026-09-24 14:42:24 +08:00
liuyihe 56f4e6e35c 贴标申请,仓储转运 2026-09-24 14:09:50 +08:00
liuyihe d03862d47e 贴标申请,仓储转运 2026-09-24 11:39:15 +08:00
liuyihe b991eac3c3 Merge branch 'dev' into test
# Conflicts:
#	lastleg-service/src/test/java/com/baiyue/lastleg/transfer/service/WarehouseTransferServiceTest.java
2026-09-24 09:26:50 +08:00
liuyihe b89400e6dc 贴标申请,仓储转运 2026-09-24 09:13:02 +08:00
liuyihe 186b6be7d8 贴标申请,仓储转运 2026-09-24 09:01:14 +08:00
ouyangxq 49e63eca9f 优化 2026-09-24 08:54:26 +08:00
ouyangxq 66c2651be7 test: map PDA fixtures by completed container 2026-09-24 05:53:55 +08:00
ouyangxq c346c6b1ba test: preserve PDA producer identity for fixture handoff 2026-09-24 05:42:16 +08:00
ouyangxq 8f1fafe572 test: isolate overseas PDA operations fixture 2026-09-24 05:08:46 +08:00
ouyangxq eb8662070a test: harden admin acceptance fixtures and transport retries 2026-09-24 04:38:38 +08:00
ouyangxq f5e53db24f test: retry client order submit transport failures 2026-09-24 03:41:12 +08:00
ouyangxq d2fb8bb372 test: choose FBA fixture with spare relabel inventory 2026-09-24 03:23:34 +08:00
ouyangxq 7e0498af23 test: prefer overseas stock for PDA relabel 2026-09-24 02:58:40 +08:00
ouyangxq 83f78a4ecb test: combine same-destination PDA relabel sources 2026-09-24 02:49:32 +08:00
ouyangxq ca2d896403 test: preserve dispatch stock during PDA relabel 2026-09-24 02:37:39 +08:00
ouyangxq b78b878250 test: keep dispatch FBA inventory available 2026-09-24 02:04:00 +08:00
ouyangxq 4a5267d332 test: isolate overseas PDA relabel fixture 2026-09-24 01:48:24 +08:00
ouyangxq a0cf38f7b8 test: harden admin acceptance fixtures 2026-09-24 01:24:22 +08:00
ouyangxq ead27a6a11 test: retry transient phase3 PDA requests 2026-09-24 00:57:47 +08:00
ouyangxq d5d3364275 test: stabilize admin full acceptance flows 2026-09-24 00:04:03 +08:00
ouyangxq 7a8f9c8968 use idempotency key for finance export replay 2026-09-23 23:32:56 +08:00
ouyangxq 3a2a8d8f79 update PDA producer contract 2026-09-23 23:01:35 +08:00
ouyangxq 95537d2d8d include standard FBA producers in overseas PDA 2026-09-23 23:01:04 +08:00
ouyangxq e19a30cc16 derive primary FBA destination from producer matrix 2026-09-23 22:41:14 +08:00
ouyangxq 85379df182 exclude primary FBA destination for second producer 2026-09-23 22:20:18 +08:00
ouyangxq b1f97bef0f stabilize full admin fixture transport and state checks 2026-09-23 22:00:00 +08:00
ouyangxq 9ad0b9f4c3 dedupe FBA destinations before route selection 2026-09-23 21:58:01 +08:00
ouyangxq 98b7d89ab8 rotate second FBA producer destination 2026-09-23 21:29:21 +08:00
ouyangxq 2b71a00303 align transfer review fixtures with service states 2026-09-23 20:58:57 +08:00
ouyangxq a29f4fc749 retry merchant transfer submission on transport failure 2026-09-23 20:50:07 +08:00
ouyangxq 39cdfe1c8f submit merchant transfer drafts before review 2026-09-23 20:37:00 +08:00
ouyangxq 9a2b9c5d11 stabilize overseas admin acceptance fixtures 2026-09-23 20:28:26 +08:00
ouyangxq f3755b1348 fix overseas PDA producer fixture selection 2026-09-23 19:48:03 +08:00
ouyangxq 9c91bc0faa match PDA pending tasks by current container number 2026-09-23 19:39:31 +08:00
ouyangxq 9a29c1aeb0 retry PDA pending lookup without keyword 2026-09-23 19:36:45 +08:00
ouyangxq 99b3096633 fix pending task response syntax 2026-09-23 19:34:10 +08:00
ouyangxq e5952d79a7 read pending PDA tasks from paged response 2026-09-23 19:33:53 +08:00
ouyangxq 54a281b780 use PDA identity across downstream loading checks 2026-09-23 19:31:50 +08:00
ouyangxq b3b8ccaa18 keep loading task reassignment on admin identity 2026-09-23 19:11:39 +08:00
ouyangxq 8baefb0511 fix PDA identity handling in phase3 loading 2026-09-23 19:09:26 +08:00
ouyangxq 78bed591e0 use PDA token for evidence downloads 2026-09-23 18:50:06 +08:00
ouyangxq ed8efc9a35 use PDA identity for loading evidence acceptance 2026-09-23 18:48:27 +08:00
ouyangxq 7525754e03 preserve loading mutation contract for assigned pending tasks 2026-09-23 18:43:23 +08:00
ouyangxq d34bf88b7f fix PDA transport replacement and evidence auth 2026-09-23 18:40:32 +08:00
ouyangxq e58f087e97 同步Admin PDA全量验收协议 2026-09-23 18:32:23 +08:00
ouyangxq 5a6bfb2a30 优化 2026-09-23 18:26:04 +08:00
ouyangxq 6f36bf680f 修复预装确认后的PDA任务状态读取 2026-09-23 18:02:31 +08:00
ouyangxq 962dbe7c02 优化 2026-09-23 17:47:50 +08:00
ouyangxq 10a2ab3e42 优化 2026-09-23 17:45:54 +08:00
ouyangxq 0d145b84db test: align warehouse transfer destination contract 2026-09-23 13:10:04 +08:00
ouyangxq 64fbde2c90 test: mock domestic PDA operator in IAM controller contract 2026-09-23 12:01:01 +08:00
ouyangxq aa02ec9df8 test: align PDA operator warehouse contract 2026-09-23 10:56:42 +08:00
ouyangxq 267e32ba0f test: align domestic PDA operator mapper contract 2026-09-23 10:47:04 +08:00
ouyangxq 52fad8505b 优化 2026-09-23 09:59:39 +08:00
ouyangxq c26b68c53a Merge branch 'dev' of http://git.buggoglobal.com/java-project/baiyue-firstleg into dev 2026-09-23 08:58:58 +08:00
ouyangxq 5c01b34503 优化 2026-09-23 08:58:56 +08:00
liuyihe b21690d943 贴标申请,仓储转运 2026-09-23 08:41:53 +08:00
liuyihe 2555eda797 货柜pda 添加操作人 2026-09-22 17:27:32 +08:00
ouyangxq abca6882c3 fix: handle empty mojia sku sync filter 2026-09-22 14:36:07 +08:00
ouyangxq 81517945f6 优化 2026-09-22 14:26:46 +08:00
ouyangxq 2813be3581 test: verify lingxing track key vector 2026-09-22 13:33:43 +08:00
ouyangxq ab68334af3 feat: add read-only lingxing connection test 2026-09-22 11:44:06 +08:00
ouyangxq 212262e300 feat: control lingxing sku sync with nacos 2026-09-22 11:32:37 +08:00
ouyangxq 0c7ff73e2d docs: document lingxing new product measurement api 2026-09-22 10:16:31 +08:00
ouyangxq 16849c2d6e fix: register mojia sku sync gateway route 2026-09-22 10:01:32 +08:00
ouyangxq e696cf33a6 fix: cover A0 schema comment migration 2026-09-22 09:42:41 +08:00
ouyangxq 1ebc128c59 feat: support overseas warehouse locations and A0 overflow 2026-09-22 09:38:49 +08:00
ouyangxq d3ce268ca3 Merge remote-tracking branch 'origin/test' into dev 2026-09-22 09:33:00 +08:00
liuyihe 9a8412279c 贴标申请,仓储转运 2026-09-22 08:49:21 +08:00
ouyangxq 1ec8ef4e2b feat: add mojia sku sync permissions 2026-09-21 23:15:43 +08:00
ouyangxq 914c6fbebf feat: add mojia sku sync platform api 2026-09-21 23:07:21 +08:00
ouyangxq 74ed71bfc0 fix: derive platform qualification file names 2026-09-21 22:54:22 +08:00
ouyangxq cf09720beb fix: preserve qualification file names across uploads 2026-09-21 22:44:01 +08:00
ouyangxq 8d66610964 fix: wire lingxing auth components for spring 2026-09-21 22:12:37 +08:00
ouyangxq 03374c83f6 feat: prepare lingxing browser auth and sku measurement inbox 2026-09-21 21:20:32 +08:00
ouyangxq 656f197737 fix: fallback customer name for dimension initialization 2026-09-21 20:06:46 +08:00
ouyangxq 36f5aa4ae3 fix: allow measurement draft during processing 2026-09-21 19:58:01 +08:00
ouyangxq 9147190d67 fix: support manual FBA quote fallback 2026-09-21 10:44:23 +08:00
ouyangxq 8f2f485402 drain old web slot before blue-green shutdown 2026-09-21 09:26:50 +08:00
ouyangxq 3b4e288564 优化 2026-09-21 07:59:48 +08:00
ouyangxq 92286e18de disable blue-green deployment in test environment 2026-09-21 00:09:59 +08:00
admin d4919fe78b Merge pull request 'fix: keep BaoTa proxies on stable ports' (#16) from test into main 2026-09-20 23:41:50 +08:00
ouyangxq 2f0b421131 fix: keep BaoTa reverse proxies on stable ports 2026-09-20 23:39:39 +08:00
admin af37c150c0 Merge pull request 'fix: complete blue-green regression and test rollout' (#15) from test into main 2026-09-20 22:36:45 +08:00
ouyangxq c1983d47a5 fix: align full regression and enable test blue-green 2026-09-20 22:34:19 +08:00
admin 1eb4aa19df Merge pull request 'feat: production blue-green deployment for all business services' (#14) from test into main
Reviewed-on: #14
2026-09-20 22:22:40 +08:00
ouyangxq 9805bc7acc 优化 2026-09-20 22:21:02 +08:00
ouyangxq 2aa80f08d0 优化 2026-09-20 22:18:59 +08:00
ouyangxq 2bf3a1c8eb fix: bound blue-green worker memory 2026-09-20 22:00:10 +08:00
ouyangxq ea37015c80 优化 2026-09-20 21:52:07 +08:00
ouyangxq b4824ddf71 fix: harden blue-green routing and gateway migration 2026-09-20 21:49:30 +08:00
ouyangxq b50d59a058 feat: complete production blue-green deployment 2026-09-20 21:44:27 +08:00
ouyangxq e3415a8778 优化 2026-09-20 21:16:15 +08:00
ouyangxq f87fcf65de 优化 2026-09-20 20:53:20 +08:00
ouyangxq 799aa45a66 优化 2026-09-20 20:50:32 +08:00
ouyangxq 2213647db4 优化 2026-09-20 20:40:21 +08:00
ouyangxq 088c219c20 fix(acceptance): reuse approved carton mark during relabel rework 2026-09-20 20:20:49 +08:00
admin f09aa0923e Merge pull request 'Test' (#13) from test into main
Reviewed-on: #13
2026-09-20 20:00:22 +08:00
ouyangxq bd03ed5671 优化 2026-09-20 19:57:47 +08:00
ouyangxq db07111e63 优化 2026-09-20 19:48:49 +08:00
admin 4c7325e701 Merge pull request 'Test' (#12) from test into main
Reviewed-on: #12
2026-09-20 19:48:44 +08:00
ouyangxq 93b58fdd6d 优化 2026-09-20 19:45:24 +08:00
ouyangxq 80778e017e 优化 2026-09-20 19:33:51 +08:00
ouyangxq 6211230564 fix(lastleg): preserve mixed SKU balances when splitting return relabel stock 2026-09-20 19:11:53 +08:00
ouyangxq bf99bf9048 优化 2026-09-20 19:08:42 +08:00
ouyangxq 8ada2ef673 fix(lastleg): bind merged return inventory to reusable target pallet 2026-09-20 18:54:40 +08:00
ouyangxq 417cbc6363 优化 2026-09-20 18:51:10 +08:00
ouyangxq 20a748cf7f 优化 2026-09-20 18:25:12 +08:00
ouyangxq d8e368bf55 test(pda): update historical photo paths 2026-09-20 18:20:28 +08:00
ouyangxq 0fef9a8133 fix(pda): allow receiving photo lookup by container 2026-09-20 18:12:48 +08:00
liuyihe 82ff6589cb 贴标申请,仓储转运 2026-09-20 16:55:55 +08:00
ouyangxq 2ce2fff8d7 fix: map sealed pallet cartons to physical pallet 2026-09-20 16:10:01 +08:00
ouyangxq cb008ad64d test(firstleg): cover printable pallet without nickname 2026-09-20 15:57:51 +08:00
ouyangxq 0dec690235 优化 2026-09-20 15:49:29 +08:00
ouyangxq 75c2bd092f 优化 2026-09-20 15:39:37 +08:00
liuyihe 11e5c6b769 Merge remote-tracking branch 'origin/test' into test 2026-09-20 15:07:01 +08:00
liuyihe de5b1fa889 贴标申请,仓储转运 2026-09-20 15:06:56 +08:00
liuyihe 02844da360 贴标申请,仓储转运 2026-09-20 15:06:17 +08:00
ouyangxq 0e52f4e11e 优化x 2026-09-20 15:03:24 +08:00
ouyangxq a7d458cddd 优化 2026-09-20 14:43:26 +08:00
ouyangxq 398f4e6983 test: align database initialization contracts 2026-09-20 14:36:22 +08:00
ouyangxq 99b1156f96 优化 2026-09-20 14:34:02 +08:00
ouyangxq 784f7ea6cd 优化 2026-09-20 14:28:25 +08:00
ouyangxq b12e5c6ffa 优化 2026-09-20 14:26:49 +08:00
ouyangxq b3162c6d69 优化 2026-09-20 14:26:03 +08:00
ouyangxq b9e0c8ea2f 优化 2026-09-20 14:22:06 +08:00
ouyangxq 493d62e04c fix: keep pallet labels on A4 templates 2026-09-20 14:05:00 +08:00
ouyangxq ee97565f67 fix: paginate dropship pallet print details 2026-09-20 13:51:06 +08:00
admin 55eb30f065 Merge pull request 'fix(print): remove SKU from carton labels while retaining dropship A4 details' (#11) from test into main
Reviewed-on: #11
2026-09-20 11:24:49 +08:00
ouyangxq bbf000a38a fix(print): remove SKU from carton labels while retaining dropship A4 details 2026-09-20 11:22:29 +08:00
admin b7dc7ebbb0 Merge pull request '优化' (#10) from test into main
Reviewed-on: #10
2026-09-20 11:11:08 +08:00
ouyangxq 78a36d1a39 优化 2026-09-20 11:09:52 +08:00
admin 9360f973ef Merge pull request '优化' (#9) from test into main
Reviewed-on: #9
2026-09-20 10:48:10 +08:00
ouyangxq c909d9044e 优化 2026-09-20 10:45:27 +08:00
admin b5b57f6cc0 Merge pull request '优化' (#8) from test into main
Reviewed-on: #8
2026-09-20 10:35:17 +08:00
ouyangxq 507763a0d6 优化 2026-09-20 10:06:14 +08:00
admin fcff45d8a0 Merge pull request '优化' (#7) from test into main
Reviewed-on: #7
2026-09-20 09:19:57 +08:00
ouyangxq d5c4059942 优化 2026-09-20 09:17:47 +08:00
admin b4182f838c Merge pull request 'Test' (#6) from test into main
Reviewed-on: #6
2026-09-19 22:59:43 +08:00
ouyangxq aa2953172b 优化 2026-09-19 17:30:20 +08:00
ouyangxq 9a7b1bb2b8 优化 2026-09-19 17:02:04 +08:00
ouyangxq 4b69f13d76 优化 2026-09-18 18:50:45 +08:00
ouyangxq 28d3d6b8b6 优化 2026-09-18 18:50:40 +08:00
admin a519942cb3 Merge pull request 'Test' (#5) from test into main
Reviewed-on: #5
2026-09-18 18:04:39 +08:00
ouyangxq 1fb02db516 优化 2026-09-18 18:02:27 +08:00
ouyangxq aedfcc3abf 优化 2026-09-18 16:49:25 +08:00
ouyangxq fd27a0ed21 优化 2026-09-18 16:48:30 +08:00
ouyangxq 7d6ca17c02 fix(firstleg): execute order remarks sync SQL 2026-09-18 15:24:09 +08:00
ouyangxq 98d15c30bb fix: carry partial dropship measurements to outbound 2026-09-18 15:23:36 +08:00
ouyangxq c8a99f9fff fix: complete order measurement downstream flow 2026-09-18 14:58:40 +08:00
ouyangxq 02d2474141 test(lastleg): align merchant warehouse contract wording 2026-09-18 14:21:05 +08:00
ouyangxq 1b7ef17c89 test(lastleg): keep pallet regression fixture self-contained 2026-09-18 14:16:19 +08:00
ouyangxq 895d0f7639 fix(lastleg): accept cabinet pallet identities 2026-09-18 14:14:10 +08:00
ouyangxq 2d902308b5 fix(firstleg): align measurement freeze contract 2026-09-18 13:42:49 +08:00
ouyangxq b0c2e1d967 fix(firstleg): use short cabinet pallet identities 2026-09-18 13:37:46 +08:00
wujinliang 674cac8ec2 Merge branch 'test' of http://8.163.30.128:10082/java-project/baiyue-firstleg into test 2026-09-18 11:41:13 +08:00
wujinliang 1fc3e04723 1 2026-09-18 11:41:04 +08:00
ouyangxq 387cd46a6b Merge branch 'test' of http://8.163.30.128:10082/java-project/baiyue-firstleg into test 2026-09-18 09:59:08 +08:00
ouyangxq 4f59f7bd28 优化 2026-09-18 09:59:07 +08:00
ouyangxq 13a8dde0a5 Merge branch 'test' of http://8.163.30.128:10082/java-project/baiyue-firstleg into test 2026-09-18 09:59:04 +08:00
wujinliang 263068ac85 Merge branch 'test' of http://8.163.30.128:10082/java-project/baiyue-firstleg into test 2026-09-18 09:16:10 +08:00
wujinliang 6be4e764a9 1 2026-09-18 09:16:02 +08:00
ouyangxq 27d281d4b3 优化 2026-09-17 18:25:05 +08:00
liuyihe 29f05b7b09 贴标申请,仓储转运 2026-09-17 17:37:29 +08:00
admin e7436263d7 Merge pull request 'ci: remove duplicate production deploy approval' (#4) from test into main
Reviewed-on: #4
2026-09-17 16:02:46 +08:00
ouyangxq 3b8b6f5aa2 ci: remove duplicate production deploy approval 2026-09-17 16:01:00 +08:00
admin 662667aba3 Merge pull request 'Test' (#3) from test into main
Reviewed-on: #3
2026-09-17 15:27:43 +08:00
ouyangxq 9dc344bdef test(sql): align contracts with clean initialization 2026-09-17 15:10:27 +08:00
ouyangxq 1fe99eb6d0 fix(iam): grant merchant return inventory permission 2026-09-17 15:10:27 +08:00
wujinliang 522afd0185 1 2026-09-17 15:05:02 +08:00
wujinliang 66c68cdc89 1 2026-09-17 14:40:02 +08:00
admin e147bec604 Merge pull request 'test(auth): align SQL contracts with clean initialization' (#2) from test into main
Reviewed-on: #2
2026-09-17 14:00:50 +08:00
ouyangxq 6ffd56a0a3 test(auth): align SQL contracts with clean initialization 2026-09-17 13:59:31 +08:00
admin eed95a384a Merge pull request 'Test' (#1) from test into main
Reviewed-on: #1
2026-09-17 11:52:43 +08:00
ouyangxq cd16687d25 ci: 收口生产 main 分支部署流程 2026-09-17 11:49:00 +08:00
ouyangxq bf8256a817 test: 标记物理箱全链路验收完成 2026-09-17 10:21:46 +08:00
ouyangxq 9ab1c328ba fix: 支持多SKU共箱整托装柜 2026-09-16 21:22:18 +08:00
ouyangxq 1cd10a9d3b test: 对齐海外空库初始化合同 2026-09-16 21:06:00 +08:00
ouyangxq c6797a8aad fix: 恢复海外服务 Nacos 凭据注入 2026-09-16 20:58:52 +08:00
ouyangxq 7ce662949b 优化 2026-09-16 20:50:10 +08:00
ouyangxq 545ad2083d fix: 按物理箱贯通一件代发履约箱数 2026-09-16 20:49:35 +08:00
ouyangxq 8797c0bef9 优化 2026-09-16 19:32:52 +08:00
ouyangxq dfcd280dd7 优化 2026-09-16 17:52:02 +08:00
ouyangxq 053e59bec1 优化 2026-09-16 17:49:27 +08:00
ouyangxq 8488d4d989 优化 2026-09-16 17:49:20 +08:00
ouyangxq 392f5b7386 优化 2026-09-16 17:15:18 +08:00
ouyangxq 854d484142 优化 2026-09-16 16:42:05 +08:00
ouyangxq b79cd9ec42 优化 2026-09-16 16:17:38 +08:00
ouyangxq 4571e3ac16 优化 2026-09-16 15:57:37 +08:00
ouyangxq 8f0622058d 优化 2026-09-16 15:21:08 +08:00
ouyangxq 912feca56f 优化 2026-09-16 12:02:38 +08:00
ouyangxq de29e639e4 fix: check released bookings only for active loading allocations 2026-09-16 11:50:30 +08:00
ouyangxq 35c6c6ec03 优化 2026-09-16 11:43:02 +08:00
ouyangxq dcd8cc2c85 test: cover loading reassignment and PDA completion gates 2026-09-16 11:40:57 +08:00
ouyangxq 011aa80870 fix: ignore empty loading links during completion 2026-09-16 11:33:16 +08:00
ouyangxq a975b07968 优化 2026-09-16 11:14:32 +08:00
ouyangxq a098c4377c 优化 2026-09-16 11:06:33 +08:00
ouyangxq a4c659f4c6 fix: count distinct SKU codes across shared pallet inbounds 2026-09-16 10:18:56 +08:00
ouyangxq c099b19176 完善一件代发整托拣货回归 2026-09-15 21:05:08 +08:00
ouyangxq 8b19085bc4 优化 2026-09-15 20:52:02 +08:00
ouyangxq 4b671bb0ab 优化 2026-09-15 18:17:06 +08:00
ouyangxq 45018987ef fix(return): replay concurrent merges and extend integrity acceptance 2026-09-15 18:00:08 +08:00
ouyangxq 6b0de57a4b test(return): cover isolation and concurrency; fix receipt and packing races 2026-09-15 17:05:07 +08:00
ouyangxq 28fa2a001d 优化 2026-09-15 16:39:44 +08:00
ouyangxq c207a4d3b7 优化 2026-09-15 16:10:55 +08:00
liuyihe 506525524b Merge remote-tracking branch 'origin/test' into test 2026-09-15 15:28:40 +08:00
ouyangxq fd86c1b801 优化 2026-09-15 15:25:05 +08:00
liuyihe cadc5a175e 提交 2026-09-15 15:15:46 +08:00
ouyangxq 6d5cc03741 优化 2026-09-15 15:13:15 +08:00
ouyangxq 7edea222d8 test: synchronize initialization and dependency contracts 2026-09-15 14:58:26 +08:00
ouyangxq c65389b2c2 fix: preserve receiving session on duplicate pallet scans 2026-09-15 14:44:10 +08:00
ouyangxq 791918c0dc 修正排车路线字段合同 2026-09-15 14:36:43 +08:00
ouyangxq d1a00b8e01 优化 2026-09-15 14:30:47 +08:00
ouyangxq a474ca4d93 优化 2026-09-15 14:18:18 +08:00
ouyangxq 6a2075fe4b 优化 2026-09-15 13:43:12 +08:00
ouyangxq f9649c44e9 优化 2026-09-15 13:36:58 +08:00
ouyangxq 1bb69698de fix: expose completed unified order status 2026-09-15 13:12:17 +08:00
ouyangxq 960b5d6b0a test: retain a second FBA fixture for delivery split 2026-09-15 13:01:44 +08:00
ouyangxq 9914d40593 test: prove dispatch split across FBA destinations 2026-09-15 12:54:09 +08:00
ouyangxq a7a6d78568 fix: persist overseas delivery completion history 2026-09-15 12:47:45 +08:00
ouyangxq 13ff7e1513 test: use supported delivery vehicle size 2026-09-15 12:34:47 +08:00
ouyangxq 2c183f0def test: use supported dispatch vehicle capacity 2026-09-15 12:29:47 +08:00
ouyangxq 9e53a0d11f test: use channel groups in booking fixtures 2026-09-15 12:24:28 +08:00
ouyangxq 9888072d8c fix: limit dispatch vehicles to 15 and 30 pallets 2026-09-15 12:17:16 +08:00
ouyangxq 37c3e676a4 test: remove manual delivery completion permission contract 2026-09-15 12:12:57 +08:00
ouyangxq b2c5bafb8e fix: remove unused pallet carton mark override 2026-09-15 12:08:11 +08:00
ouyangxq a400563552 test: honor runtime inbound code policy in HTTP order fixtures 2026-09-15 12:07:43 +08:00
ouyangxq 6e35993e61 更新派送自动完成测试验收 2026-09-15 12:07:10 +08:00
ouyangxq c1f01e1e87 优化 2026-09-15 11:54:14 +08:00
ouyangxq 3fb9909dd3 test(lastleg): align relabel upload fixture and PDA status comment 2026-09-15 11:45:40 +08:00
ouyangxq 3ebe254c45 fix(dropship): serialize outbound creation and pick exact carton subsets 2026-09-15 11:42:19 +08:00
ouyangxq 88e2fb0eb1 优化 2026-09-15 10:37:34 +08:00
liuyihe 419950482f Merge remote-tracking branch 'origin/test' into test 2026-09-14 22:44:54 +08:00
ouyangxq 4f13ee2ec3 fix(dropship): preserve carton quantities and shared pallet outbound ownership 2026-09-14 20:55:09 +08:00
liuyihe 70145e6b3b 废弃 ,换标文件上传 ,排车托盘锁粒度 2026-09-14 20:21:07 +08:00
ouyangxq 6dbcb2128c 优化 2026-09-14 20:20:57 +08:00
ouyangxq ebc299d58a 优化 2026-09-14 20:03:22 +08:00
ouyangxq d53de922db 优化 2026-09-14 18:34:37 +08:00
ouyangxq d9a4384585 fix(firstleg): align measurement package validation 2026-09-14 18:32:32 +08:00
ouyangxq 3910b8fcce feat: finalize dropship pallet print documents 2026-09-14 17:52:01 +08:00
ouyangxq b3d74c5441 test: align full-chain fixture invariants 2026-09-14 17:45:56 +08:00
ouyangxq e26b39ba0a test: use current loading remarks endpoint 2026-09-14 17:36:52 +08:00
ouyangxq a9959620f1 test: target unified order remarks endpoint 2026-09-14 17:31:06 +08:00
ouyangxq 6f917f9b2a Merge remote-tracking branch 'origin/test' into test 2026-09-14 17:29:27 +08:00
ouyangxq 4c048d0760 test: use current order remarks contract 2026-09-14 17:29:03 +08:00
liuyihe dbc4926d14 Merge remote-tracking branch 'origin/test' into test 2026-09-14 17:26:01 +08:00
ouyangxq 64bddb1f81 fix: scope smart loading recommendations 2026-09-14 17:22:09 +08:00
ouyangxq 8a1e587ee4 fix(order): align FBA submit and fee confirmation 2026-09-14 17:19:06 +08:00
liuyihe 57358d7665 Merge branch 'test' into dev 2026-09-14 17:04:24 +08:00
ouyangxq e0af45db02 feat: add editable order collaboration remarks 2026-09-14 16:53:09 +08:00
liuyihe 57f62ae97b 废弃 ,换标文件上传 ,排车托盘锁粒度 2026-09-14 16:49:08 +08:00
liuyihe 78bbd69a26 废弃 ,换标文件上传 ,排车托盘锁粒度 2026-09-14 16:49:00 +08:00
ouyangxq 26c37deb55 fix: enforce dropship pallet count for inbound plans 2026-09-14 16:38:50 +08:00
ouyangxq 99f2c730e0 优化 2026-09-14 15:53:49 +08:00
ouyangxq f9fa859391 优化 2026-09-14 15:51:52 +08:00
ouyangxq 13d2fe8e3a fix: merge multi-pallet dropship handover fragments 2026-09-14 15:44:51 +08:00
ouyangxq fe888f9278 fix(iam): scope customer nickname to customer management 2026-09-14 15:38:29 +08:00
ouyangxq 059757507a 优化 2026-09-14 15:33:45 +08:00
ouyangxq cdac78e4f7 Merge branch 'test' of http://8.163.30.128:10082/java-project/baiyue-firstleg into test 2026-09-14 15:33:30 +08:00
ouyangxq 46b4d14898 优化 2026-09-14 15:33:29 +08:00
liuyihe cd578a8615 优化提交 2026-09-14 15:26:00 +08:00
liuyihe 60ebc2e376 优化提交 2026-09-14 15:03:04 +08:00
ouyangxq febe265c50 fix: tolerate stale pallets in smart recommendation 2026-09-14 15:01:05 +08:00
ouyangxq 1d258ab27b fix: keep dropship pallets atomic in smart loading 2026-09-14 14:35:29 +08:00
ouyangxq de9516d19e test: align client registration replay with current auth contract 2026-09-14 13:49:35 +08:00
ouyangxq d8254c4f1d 优化 2026-09-14 13:48:21 +08:00
ouyangxq d8e2dc59ae fix: validate transfer rejection and align dispatch HTTP contracts 2026-09-14 13:46:32 +08:00
ouyangxq a2ea0ba9e6 fix: align HTTP acceptance fixtures and pallet permissions 2026-09-14 12:40:40 +08:00
ouyangxq e55ab73e61 优化 2026-09-14 12:00:48 +08:00
ouyangxq fb66d9200a fix: give warehouse transfer approval a distinct permission id 2026-09-14 11:35:14 +08:00
ouyangxq 54f5bdfb1f fix: align overseas administrator authority assertion with merged baseline 2026-09-14 11:26:16 +08:00
ouyangxq 035537e5cc 优化 2026-09-14 11:12:29 +08:00
ouyangxq 481ab348ff Merge latest origin/dev into test retaining verified acceptance workflows 2026-09-14 11:05:33 +08:00
ouyangxq d4fec8294d Merge origin/dev into test preserving warehouse and PDA workflows 2026-09-14 11:03:47 +08:00
liuyihe 6c7bc1dc97 Merge test into dev and resolve conflicts 2026-09-14 11:02:37 +08:00
liuyihe f7ef8be18f 优化提交 2026-09-14 10:37:23 +08:00
liuyihe 8cf7c4b9c9 优化提交 2026-09-14 09:58:30 +08:00
ouyangxq cc2aed72d9 优化 2026-09-12 18:34:23 +08:00
ouyangxq f26e4b2972 fix(lastleg): trace return carton sources and generate scannable labels 2026-09-12 16:16:38 +08:00
ouyangxq e467d60e8f fix(lastleg): allow edits with historical procurement warehouse 2026-09-12 16:14:50 +08:00
ouyangxq d33922ad35 fix overseas return inventory flow 2026-09-12 13:55:48 +08:00
ouyangxq 2191bacbfe fix qualification upload contract and test baseline 2026-09-12 12:27:56 +08:00
ouyangxq dcd079b5b7 优化 2026-09-11 18:43:57 +08:00
ouyangxq 06b9f3eda7 fix: persist PDA picking progress incrementally 2026-09-11 18:24:18 +08:00
ouyangxq 019db2900a 优化 2026-09-11 18:14:29 +08:00
ouyangxq 30d6896c8a docs: clarify scan-first pallet workflow 2026-09-11 17:29:05 +08:00
ouyangxq 63ce4c48a7 优化 2026-09-11 17:27:27 +08:00
ouyangxq 458a4cfa50 优化 2026-09-11 17:20:56 +08:00
ouyangxq c8e8245853 feat: allow shared dropship pallets 2026-09-11 17:12:03 +08:00
ouyangxq 1683977e0b 优化 2026-09-11 17:01:56 +08:00
ouyangxq b6c66565b9 fix: unify same-carton order metrics 2026-09-11 16:50:10 +08:00
ouyangxq 87ae82a114 test: support manual dropship count fixtures 2026-09-11 16:34:40 +08:00
ouyangxq 010475a4c3 fix: prepare mixed-SKU dropship acceptance data 2026-09-11 16:21:14 +08:00
ouyangxq 51ce609ab1 优化 2026-09-11 16:17:16 +08:00
ouyangxq 2d928d667b fix: resolve cross-plan pallet allocations 2026-09-11 16:10:49 +08:00
ouyangxq 64fa620c19 test: add full return receipt acceptance 2026-09-11 16:02:10 +08:00
ouyangxq e1319cb389 fix: keep unified SQL pass marker final 2026-09-11 15:50:17 +08:00
ouyangxq 071f807b87 fix: aggregate mixed cartons by fulfillment type 2026-09-11 15:45:05 +08:00
ouyangxq 774f0472e8 feat: explain supplemental pallet counts 2026-09-11 15:39:38 +08:00
ouyangxq cc7c2f0954 feat: support mixed-SKU dropship cartons 2026-09-11 15:38:19 +08:00
ouyangxq 0db11517db fix: persist return forecast tail references 2026-09-11 15:10:01 +08:00
ouyangxq 183b242d45 test: cover PDA return receipt lifecycle 2026-09-11 15:01:17 +08:00
ouyangxq 3b97eedc3d fix: block dropship work until pallets are ready 2026-09-11 13:54:06 +08:00
ouyangxq 137f8173ab fix: keep full-chain acceptance aligned with current APIs 2026-09-11 12:36:12 +08:00
ouyangxq 1f20516d23 docs: update dropship pallet acceptance flow 2026-09-11 12:27:41 +08:00
ouyangxq b57ca7def7 fix: align dropship acceptance with precreated pallets 2026-09-11 12:26:52 +08:00
ouyangxq e61050c490 feat: precreate dropship pallet codes after measurement 2026-09-11 12:12:41 +08:00
ouyangxq 69076ea712 fix: print overseas warehouse address on BOL 2026-09-11 11:55:46 +08:00
ouyangxq 749d403ed4 优化 2026-09-11 11:52:41 +08:00
ouyangxq d2cff49b9e 优化 2026-09-11 11:51:26 +08:00
ouyangxq d60689ce02 feat: match warehouse BOL print layout 2026-09-11 11:35:37 +08:00
ouyangxq 7fd2e70024 优化 2026-09-11 11:15:01 +08:00
ouyangxq a4b7247c57 fix: expose overseas receiving business facts 2026-09-11 10:32:30 +08:00
ouyangxq b2f85501ac fix: connect overseas lastleg to print service 2026-09-11 10:24:08 +08:00
ouyangxq 220d159cec feat: generate dispatch BOL PDFs 2026-09-11 10:09:06 +08:00
ouyangxq 81f4f9163c fix: link terminal drayage carrier events 2026-09-11 02:20:50 +08:00
ouyangxq fe278f2ba6 test: sequence terminal drayage around unloading 2026-09-11 01:50:46 +08:00
ouyangxq abbacc683e test: align acceptance and dependency contracts 2026-09-11 01:31:14 +08:00
ouyangxq 543ebee5cc test: refresh merchant token before PDA handoff 2026-09-11 01:24:00 +08:00
ouyangxq 4f27e33daf test: align booking channel group contract 2026-09-11 01:20:21 +08:00
ouyangxq 3209b2e647 test: follow complete relabel lifecycle 2026-09-11 01:18:01 +08:00
ouyangxq 7dff9b57ed fix: reuse overseas receiving resources in acceptance 2026-09-11 00:48:21 +08:00
ouyangxq 9abb0004dd test: align overseas PDA group receipt semantics 2026-09-11 00:24:35 +08:00
ouyangxq 2bd12f8a5f fix: converge inbound pallets and retries 2026-09-11 00:06:57 +08:00
ouyangxq ed12769858 fix: converge dropship manifest schema 2026-09-10 23:48:48 +08:00
ouyangxq 4294a5c3db 优化 2026-09-10 23:27:49 +08:00
ouyangxq 89573fd1f0 fix: make unified database migrations converge 2026-09-10 23:12:37 +08:00
ouyangxq 56635ad4e4 feat: complete finance and overseas receiving workflow 2026-09-10 22:43:11 +08:00
ouyangxq e531ae9407 fix(acceptance): require FBA pallet label configuration 2026-09-10 21:53:15 +08:00
ouyangxq 6fa2260572 fix(acceptance): derive split assertions from real cargo 2026-09-10 21:29:40 +08:00
ouyangxq e3dc5329aa fix(acceptance): reuse configured merchant credentials 2026-09-10 21:26:52 +08:00
ouyangxq 6cf144e53d feat: expose dispatch BOL lines 2026-09-10 21:22:06 +08:00
ouyangxq 800ebb875f fix(acceptance): enforce immutable confirmed fees 2026-09-10 21:18:52 +08:00
ouyangxq 7b35373da3 fix(firstleg): enforce PDA task warehouse scope 2026-09-10 20:47:23 +08:00
ouyangxq 47a70406f6 fix(acceptance): reuse initialized pallet fixtures 2026-09-10 20:47:15 +08:00
ouyangxq 69e80a736e fix(sql): make domestic migration replay-safe 2026-09-10 20:19:11 +08:00
ouyangxq 2b73245c96 test(firstleg): align workbench channel group contract 2026-09-10 19:38:40 +08:00
ouyangxq 07b5f6e077 test(iam): align migration guide contract 2026-09-10 19:34:40 +08:00
ouyangxq c480f957c5 优化 2026-09-10 19:27:09 +08:00
ouyangxq 155dd9d77d feat(firstleg): align simulated containers with channel groups 2026-09-10 19:24:49 +08:00
ouyangxq 5ffd6c0118 fix: require configured overseas service tokens 2026-09-10 18:22:42 +08:00
ouyangxq 959d43e308 Merge remote-tracking branch 'origin/dev' into dev 2026-09-10 18:19:10 +08:00
ouyangxq 2fc2fca9fd Merge test fixes into dev dropship flow 2026-09-10 18:18:41 +08:00
liuyihe 72e62c0532 Merge remote-tracking branch 'origin/dev' into dev
# Conflicts:
#	deploy/sql/海外区域统一增量-MySQL8.sql
#	lastleg-service/src/main/java/com/baiyue/lastleg/relabel/service/RelabelApplicationService.java
#	lastleg-service/src/main/resources/db/lastleg.sql
#	lastleg-service/src/main/resources/openapi/overseas-business-v1.yaml
#	lastleg-service/src/test/java/com/baiyue/lastleg/relabel/service/RelabelApplicationServiceTest.java
#	scripts/README.md
2026-09-10 17:25:07 +08:00
ouyangxq 56d555668d 优化 2026-09-10 17:13:02 +08:00
liuyihe 2e6a18c58e 优化 2026-09-10 17:11:33 +08:00
ouyangxq d37e6cf147 fix: auto materialize completed container plans 2026-09-10 17:05:40 +08:00
ouyangxq 343c6e278a fix: block receiving before pallet materialization 2026-09-10 16:43:08 +08:00
ouyangxq 1ce99e710d fix: materialize signed manual pallet plans 2026-09-10 16:33:32 +08:00
ouyangxq f5d6c37080 fix: deduplicate phase5 overseas schema baseline 2026-09-10 15:49:20 +08:00
ouyangxq f0b8514890 fix: materialize container pallet plans 2026-09-10 15:40:55 +08:00
ouyangxq a5db6fbbb6 优化 2026-09-10 15:14:30 +08:00
ouyangxq 2963acc32b fix(lastleg): return empty container customer options 2026-09-10 14:41:03 +08:00
ouyangxq bf7579f93b 优化 2026-09-10 14:38:22 +08:00
ouyangxq 6d0cd3e301 优化 2026-09-10 14:16:24 +08:00
ouyangxq 0dc0053787 优化 2026-09-10 14:15:41 +08:00
ouyangxq 3e4d6b76aa fix(events): allow container nodes before ocean booking 2026-09-10 14:10:02 +08:00
ouyangxq c026edbb2f fix(firstleg): allow customs view for empty loading plan 2026-09-10 14:02:26 +08:00
ouyangxq a3890e7a55 fix(events): allow manual nodes without tracking query 2026-09-10 13:56:07 +08:00
ouyangxq c051be3a67 fix(gateway): route container shipping nodes 2026-09-10 13:45:41 +08:00
ouyangxq 98f61ba3ed 优化 2026-09-10 12:18:33 +08:00
ouyangxq 443b01c8a8 fix(firstleg): decouple manual container nodes from tracking 2026-09-10 12:17:45 +08:00
ouyangxq c1a36c6f40 fix(firstleg): enable manual nodes for consolidated containers 2026-09-10 11:55:09 +08:00
ouyangxq 2821c5e9c7 fix(firstleg): update submitted outer dimensions safely 2026-09-10 11:42:05 +08:00
ouyangxq e8890389ff fix(firstleg): keep PDA photos accessible after departure 2026-09-10 11:37:51 +08:00
ouyangxq 1ef289cd09 优化 2026-09-10 11:25:16 +08:00
ouyangxq e9aa2ac047 优化 2026-09-10 11:18:31 +08:00
ouyangxq 8731efcbea fix(firstleg): allow fee revision until final fulfillment lock 2026-09-10 11:02:45 +08:00
ouyangxq a926a23b87 fix: allow overseas container detail before carrier tracking 2026-09-10 10:59:09 +08:00
ouyangxq 630150a7c7 fix: allow unassigned overseas receiving warehouse 2026-09-10 10:34:24 +08:00
ouyangxq e72e74b21e fix: stabilize overseas initialization and PDA acceptance 2026-09-10 10:25:16 +08:00
ouyangxq 37a28b9c11 Merge remote-tracking branch 'origin/test' into dev
# Conflicts:
#	lastleg-service/src/main/resources/db/lastleg.sql
2026-09-10 10:01:35 +08:00
ouyangxq 87e65043f3 fix: treat overseas unload photos as platform evidence 2026-09-10 00:18:17 +08:00
ouyangxq 3e4b9d0a9a 优化 2026-09-09 23:46:21 +08:00
ouyangxq 8487eeb1b0 merge: sync latest test environment changes 2026-09-09 22:45:42 +08:00
ouyangxq 107374bec5 fix: align overseas container scope migration 2026-09-09 21:37:01 +08:00
ouyangxq a05b263ca8 fix: support multi-customer overseas receiving containers 2026-09-09 21:30:39 +08:00
ouyangxq c8ab4d3de5 merge: sync test and stabilize full acceptance 2026-09-09 21:14:25 +08:00
ouyangxq 6737c4c327 优化 2026-09-09 20:53:24 +08:00
ouyangxq eac6875877 test: stabilize full dev acceptance orchestration 2026-09-09 20:36:53 +08:00
ouyangxq 71604c9c39 fix: deliver actual destuff pallet fees 2026-09-09 20:00:44 +08:00
ouyangxq b3e0ea9199 fix: complete dropship container after pallet receipt 2026-09-09 19:41:29 +08:00
ouyangxq f3bbe0e15f Merge remote-tracking branch 'origin/test' into dev 2026-09-09 19:40:30 +08:00
ouyangxq 5ccce6af71 test: carry sealed pallet into overseas acceptance 2026-09-09 19:25:13 +08:00
ouyangxq 4cc3e5372b fix: allow native pallets for overseas destinations 2026-09-09 19:24:10 +08:00
ouyangxq e0d8b4f76a Merge remote-tracking branch 'origin/test' into dev
# Conflicts:
#	scripts/admin-test-env-modules/phase4-tail-order-controls.mjs
2026-09-09 19:23:57 +08:00
ouyangxq 90a0b009da test: keep overseas acceptance fixtures independent 2026-09-09 19:16:18 +08:00
ouyangxq e30255ff58 fix: complete pallet code cross-region flow 2026-09-09 19:14:47 +08:00
ouyangxq 30b9db9628 test: align tail fee acceptance with current contract 2026-09-09 19:11:37 +08:00
ouyangxq 1e66fc6c22 fix: enforce dropship whole-pallet receiving 2026-09-09 19:07:13 +08:00
ouyangxq 3e01dec4f8 Merge remote-tracking branch 'origin/test' into dev 2026-09-09 19:05:05 +08:00
ouyangxq 7678fd85ff test: cover customer inbound fee confirmation 2026-09-09 18:43:08 +08:00
ouyangxq d9603aa7da 修复统一增量中的IAM权限区块边界 2026-09-09 18:38:22 +08:00
ouyangxq 1c4f35f61c test: verify manual inbound unit price flow 2026-09-09 18:34:07 +08:00
ouyangxq 0d016bc5fc 适配财务验收FBA内部编号 2026-09-09 18:33:39 +08:00
ouyangxq 45e7c98b3f feat: receive dropship inbound by source pallet 2026-09-09 18:33:17 +08:00
ouyangxq c8ae626e87 Merge remote-tracking branch 'origin/test' into dev 2026-09-09 18:31:14 +08:00
ouyangxq acf9bd7135 优化 2026-09-09 18:29:42 +08:00
ouyangxq 1dfb19276c Merge remote-tracking branch 'origin/test' into dev
# Conflicts:
#	lastleg-service/src/main/java/com/baiyue/lastleg/service/LoadingCompletedConsumer.java
#	lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/pda/gateway/HttpWarehouseResourceGatewayTest.java
#	warehouse-overseas-service/src/main/java/com/baiyue/warehouse/service/WarehousePalletBatchService.java
2026-09-09 18:20:40 +08:00
ouyangxq 312501cc70 feat: use FBA internal code in pallet labels 2026-09-09 18:19:58 +08:00
ouyangxq 2a9f79604b 修复装柜完成托盘标签事务 2026-09-09 18:18:26 +08:00
ouyangxq 3b02affcb3 优化 2026-09-09 17:58:07 +08:00
ouyangxq 4fee718424 修复仓库内部编号增量迁移 2026-09-09 17:55:46 +08:00
ouyangxq fc5ce1f45f 完善财务P1验收客户规则快照 2026-09-09 17:46:36 +08:00
ouyangxq be882d53d1 优化 2026-09-09 17:42:44 +08:00
ouyangxq 88b489eb77 优化 2026-09-09 17:42:26 +08:00
ouyangxq 2b0e66ca7e test: cover pallet label loading-list paths 2026-09-09 17:35:47 +08:00
ouyangxq de1f7e8ddd feat: freeze and propagate first-leg pallet labels 2026-09-09 17:27:00 +08:00
ouyangxq 6f0622ed36 fix: close development regression gaps 2026-09-09 17:16:34 +08:00
ouyangxq 7c6fa248ac Merge remote-tracking branch 'origin/test' into dev
# Conflicts:
#	lastleg-service/src/main/resources/openapi/overseas-business-v1.yaml
2026-09-09 17:07:34 +08:00
ouyangxq 48bc8bf0c1 Merge remote-tracking branch 'origin/test' into dev
# Conflicts:
#	lastleg-service/src/main/java/com/baiyue/lastleg/relabel/service/RelabelApplicationService.java
#	lastleg-service/src/test/java/com/baiyue/lastleg/relabel/service/RelabelApplicationServiceTest.java
2026-09-09 16:49:46 +08:00
ouyangxq 83e95d764e 修复海外业务接口契约格式 2026-09-09 16:49:01 +08:00
ouyangxq 7113703e3c 更新财务费用状态合同 2026-09-09 16:46:01 +08:00
ouyangxq 17b9f3eb73 统一财务费用来源入池链路 2026-09-09 16:39:20 +08:00
ouyangxq 95828a0ec6 Merge remote-tracking branch 'origin/test' into dev 2026-09-09 15:41:50 +08:00
ouyangxq 9fdb199972 修复导出基础设施测试的三次重启策略断言 2026-09-09 15:41:32 +08:00
ouyangxq 2980308f4d 优化 2026-09-09 15:27:10 +08:00
liuyihe 1ee32b7a6f 优化 2026-09-09 15:14:15 +08:00
ouyangxq 5f671e164f 优化 2026-09-09 14:31:59 +08:00
ouyangxq 250b623f3a 限制容器异常退出后最多自动重启三次 2026-09-09 14:26:34 +08:00
ouyangxq 3727ff141c 补齐码头车队邮件表字段中文注释 2026-09-09 14:00:03 +08:00
ouyangxq f31a049444 优化 2026-09-09 13:54:47 +08:00
ouyangxq 1a092bc9d7 优化 2026-09-09 13:51:01 +08:00
liuyihe 90a2eda86a 优化 2026-09-09 13:41:24 +08:00
ouyangxq 748855793d fix(dropship): project sealed carton identities into overseas receiving 2026-09-09 13:20:11 +08:00
ouyangxq 90973532f0 Merge remote-tracking branch 'origin/test' into dev 2026-09-09 13:13:51 +08:00
ouyangxq a3354c92c2 优化 2026-09-09 12:57:59 +08:00
ouyangxq 655237e37f fix(lastleg): complete relabel document contract and regression gates 2026-09-09 12:54:14 +08:00
ouyangxq ac3ce3703e fix(firstleg): preserve sealed pallet identity in atomic PDA picking 2026-09-09 12:33:31 +08:00
ouyangxq 44ac24a9e2 Merge remote-tracking branch 'origin/dev' into dev 2026-09-09 12:20:05 +08:00
ouyangxq 0d8539e72f Merge remote-tracking branch 'origin/test' into dev 2026-09-09 12:19:17 +08:00
ouyangxq 5fb147f7aa fix(dropship): generate pallet numbers atomically and extend full regressions 2026-09-09 12:19:16 +08:00
liuyihe c3ae15abfe 优化 2026-09-09 12:02:12 +08:00
ouyangxq e5d826b4a2 优化 2026-09-09 11:55:06 +08:00
ouyangxq 465d9dfc10 feat(lastleg): manage three delivery document categories 2026-09-09 11:22:47 +08:00
ouyangxq f37e9ce555 fix(firstleg): allow optional ocean booking customer 2026-09-09 11:21:37 +08:00
ouyangxq 81b729929c fix(file): converge delivery documents to three categories 2026-09-09 11:21:36 +08:00
ouyangxq 156520b560 fix(file): make ocean booking documents platform owned 2026-09-09 11:16:28 +08:00
ouyangxq 3627239708 feat(file): support seven delivery document categories 2026-09-09 11:11:32 +08:00
ouyangxq cde8acf111 feat(iam): add delivery document management permission 2026-09-09 11:09:51 +08:00
ouyangxq 0d157e4dd9 test(firstleg): verify overseas list return action projection 2026-09-09 10:52:53 +08:00
ouyangxq 5ecee65720 test(finance): cover overseas fee replay and frozen lifecycle 2026-09-09 10:20:56 +08:00
ouyangxq 87d6469f02 优化 2026-09-09 10:18:04 +08:00
ouyangxq 7e2626513e fix(finance): preserve container identity and source version order 2026-09-09 10:14:42 +08:00
ouyangxq 4379b721bc 优化 2026-09-09 09:52:46 +08:00
ouyangxq 9d492cb5ed fix(overseas): restore dispatch delivery returns 2026-09-09 04:00:24 +08:00
ouyangxq 646221d133 test(overseas): use authoritative fulfillment fixtures 2026-09-09 03:42:48 +08:00
ouyangxq bfcec93017 Merge remote-tracking branch 'origin/test' into dev 2026-09-09 03:40:54 +08:00
ouyangxq 68a5612598 fix(firstleg): preserve dropship palletization state during pricing 2026-09-09 03:22:07 +08:00
ouyangxq a99a54493b test(overseas): close fulfillment HTTP gaps 2026-09-09 03:18:59 +08:00
ouyangxq 47774ea8fe Merge remote-tracking branch 'origin/test' into dev 2026-09-09 03:16:56 +08:00
ouyangxq bf89a04883 fix(firstleg): price dropship before preload confirmation 2026-09-09 03:11:41 +08:00
ouyangxq cda6521078 test(overseas): align relabel and pallet acceptance 2026-09-09 03:02:36 +08:00
ouyangxq 71580da8d7 Merge remote-tracking branch 'origin/test' into dev
# Conflicts:
#	lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/gateway/WarehouseResourceGateway.java
#	scripts/admin-test-env-modules/phase3-container-documents-contract.test.mjs
#	scripts/admin-test-env-modules/phase4-overseas-pda.mjs
2026-09-09 03:00:48 +08:00
ouyangxq c5862db98d fix(lastleg): complete generated pallet receiving flow 2026-09-09 02:54:09 +08:00
ouyangxq 045d0d815b test(overseas): cover split relabel inventory 2026-09-09 02:48:10 +08:00
ouyangxq 41225248d0 test(acceptance): reconcile dropship runtime facts 2026-09-09 02:40:42 +08:00
ouyangxq 4ccdae0665 fix(overseas): price generated receiving pallets 2026-09-09 02:17:38 +08:00
ouyangxq 66ba59a51a test(firstleg): accept pallet pick audit action 2026-09-09 01:57:15 +08:00
ouyangxq 96b5f66d5c Merge remote-tracking branch 'origin/test' into dev
# Conflicts:
#	scripts/admin-test-env-modules/phase3-container-loading.mjs
#	scripts/admin-test-env-modules/phase3-loading.mjs
2026-09-09 01:56:29 +08:00
ouyangxq 86646ccd69 test(admin): scope domestic container lookup 2026-09-09 01:54:54 +08:00
ouyangxq 04c056f744 test(admin): unwrap container workbench details 2026-09-09 01:52:59 +08:00
ouyangxq 74fa4789f5 fix(acceptance): permit dropship pallet pick audit 2026-09-09 01:50:19 +08:00
ouyangxq c5d73b8e8f test(admin): use canonical container workbench routes 2026-09-09 01:49:52 +08:00
ouyangxq decfe4ab1f Merge remote-tracking branch 'origin/test' into dev
# Conflicts:
#	scripts/admin-test-env-modules/inbound-fees-contract.test.mjs
2026-09-09 01:48:32 +08:00
ouyangxq 859fba4ecc test(admin): confirm fee adjustment tolerance 2026-09-09 01:46:11 +08:00
ouyangxq a0b1c761b7 fix(firstleg): complete inbound fee deployment schema 2026-09-09 01:33:03 +08:00
ouyangxq f7e3863898 fix(acceptance): follow reserved pallet workflows 2026-09-09 01:30:22 +08:00
ouyangxq 3cfd45f4bb fix(lastleg): preserve generated pallet FBA destination 2026-09-09 01:29:38 +08:00
ouyangxq b11ad31d5f Merge remote-tracking branch 'origin/test' into dev 2026-09-09 01:28:48 +08:00
ouyangxq b797af706a test(admin): sync fee item baseline 2026-09-09 01:27:44 +08:00
ouyangxq 8452f1410f fix(firstleg): hide provider framework errors 2026-09-09 01:20:08 +08:00
ouyangxq 2abbb3cc8f fix(acceptance): preserve calculated dropship fees 2026-09-09 01:09:31 +08:00
ouyangxq 0d73e185c0 fix(acceptance): align latest overseas contracts 2026-09-09 00:53:40 +08:00
ouyangxq 5da3417429 Merge remote-tracking branch 'origin/test' into dev
# Conflicts:
#	deploy/sql/海外区域统一增量-MySQL8.sql
#	lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/gateway/HttpWarehouseResourceGateway.java
#	lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/gateway/WarehouseResourceGateway.java
#	lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/service/ReceivingContainerService.java
#	lastleg-service/src/main/resources/db/lastleg.sql
#	lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/pda/gateway/HttpWarehouseResourceGatewayTest.java
2026-09-09 00:41:47 +08:00
ouyangxq e7cbe98766 fix(lastleg): restore pallet validation structure 2026-09-09 00:39:38 +08:00
ouyangxq df0088750b fix(lastleg): restore receiving service structure 2026-09-09 00:37:42 +08:00
ouyangxq 2ba5f0c307 fix(lastleg): harden container plan saga recovery 2026-09-09 00:31:50 +08:00
ouyangxq 990b74f3e8 fix(lastleg): confirm pre-reserved pallets on PDA scan 2026-09-09 00:25:57 +08:00
ouyangxq 7e76fed728 fix(acceptance): complete overseas fulfillment closure 2026-09-09 00:25:34 +08:00
ouyangxq 094d9c1e81 Merge remote-tracking branch 'origin/test' into dev 2026-09-09 00:19:29 +08:00
ouyangxq 7b47de47c6 feat(lastleg): reserve pallets for container plans 2026-09-09 00:15:23 +08:00
ouyangxq cf8053b7ce fix(warehouse-overseas): keep confirmed pallet locks active 2026-09-09 00:11:46 +08:00
ouyangxq 93704a24ce fix(warehouse-overseas): avoid pallet batch gap lock 2026-09-08 23:59:50 +08:00
ouyangxq 5086de23a7 fix(acceptance): close overseas dropship pallet chain 2026-09-08 23:56:54 +08:00
ouyangxq fa9888d0e1 Merge remote-tracking branch 'origin/test' into dev
# Conflicts:
#	scripts/db/incremental.sql
2026-09-08 23:54:12 +08:00
ouyangxq a88a04fd24 feat(warehouse-overseas): reserve inbound pallet batches 2026-09-08 23:52:36 +08:00
ouyangxq 4c1e9aa714 feat(lastleg): add container plan preview 2026-09-08 23:45:30 +08:00
ouyangxq 0c984e8f5f fix(lastleg): inspect occupied pallets for PDA queries 2026-09-08 23:34:12 +08:00
ouyangxq 3e15ad6b5b fix(scripts): align dropship container acceptance 2026-09-08 23:26:09 +08:00
ouyangxq 2121743753 fix(scripts): scope container workbench queries 2026-09-08 23:16:52 +08:00
ouyangxq 7d64135e79 fix(scripts): read domestic container list payload 2026-09-08 23:15:13 +08:00
ouyangxq 735b5abdcc fix(scripts): read container workbench payload 2026-09-08 23:11:49 +08:00
ouyangxq 00990fedcc fix(scripts): use domestic container detail route 2026-09-08 23:06:07 +08:00
ouyangxq f7f4c80806 fix(firstleg): sync container pickup code schema 2026-09-08 22:51:02 +08:00
ouyangxq 0b9cab7c1a fix(firstleg): align dropship pallet sealing contract 2026-09-08 22:34:58 +08:00
ouyangxq 4bbcda2a75 Merge remote-tracking branch 'origin/test' into dev
# Conflicts:
#	deploy/sql/海外区域统一增量-MySQL8.sql
2026-09-08 22:34:48 +08:00
ouyangxq ae44b82a85 fix(lastleg): align overseas schema upgrades 2026-09-08 22:17:36 +08:00
ouyangxq 8e3e94b286 fix(lastleg): sync relabel inventory snapshot schema 2026-09-08 22:12:34 +08:00
ouyangxq a060f8dca8 fix(overseas): support projected authorization headers 2026-09-08 21:59:13 +08:00
ouyangxq 55fda1f616 Merge remote-tracking branch 'origin/test' into dev 2026-09-08 21:59:03 +08:00
ouyangxq e3a5ea7341 feat(lastleg): add overseas container fee sources 2026-09-08 21:45:57 +08:00
ouyangxq 0c4d7c644e fix(gateway): forward standard internal trust header 2026-09-08 21:38:12 +08:00
ouyangxq e9ad7e333a fix(security): distinguish gateway context failures 2026-09-08 21:29:23 +08:00
ouyangxq 781669dfaf Merge remote-tracking branch 'origin/test' into dev 2026-09-08 21:29:21 +08:00
ouyangxq 1cf6722d6e fix(finance): key container finance by booking 2026-09-08 21:27:23 +08:00
ouyangxq 55c36cf290 fix(firstleg): publish fees by container booking 2026-09-08 21:24:49 +08:00
ouyangxq d0c4d5f997 test(security): cover gateway token authentication 2026-09-08 21:22:24 +08:00
ouyangxq 23ac00eb61 Merge remote-tracking branch 'origin/dev' into dev 2026-09-08 21:05:33 +08:00
ouyangxq 4a403aaa30 fix(security): accept overseas token subject identity 2026-09-08 21:05:15 +08:00
ouyangxq 1fb139bc71 test(finance): sync formal fee item baseline 2026-09-08 21:05:15 +08:00
ouyangxq 8e0001dc56 Merge remote-tracking branch 'origin/test' into dev 2026-09-08 21:05:05 +08:00
ouyangxq 2e2ca3bef4 feat(finance): add container settlement aggregate 2026-09-08 21:03:47 +08:00
liuyihe f81fac8f6b Merge remote-tracking branch 'origin/dev' into dev 2026-09-08 20:52:55 +08:00
ouyangxq aa7f067e54 test(firstleg): confirm fee adjustment tolerance 2026-09-08 20:51:08 +08:00
liuyihe c6156771b4 Merge remote-tracking branch 'origin/dev' into dev 2026-09-08 20:50:41 +08:00
liuyihe 4d4008cdb3 Merge origin/dev and resolve backend conflicts 2026-09-08 20:50:28 +08:00
ouyangxq 93bf192e3d fix(lastleg): sync delivery lifecycle schema 2026-09-08 20:37:41 +08:00
ouyangxq cafa66fd37 Merge remote-tracking branch 'origin/test' into dev 2026-09-08 20:37:19 +08:00
ouyangxq fc079fcdc0 fix(firstleg): allow domestic history container details 2026-09-08 20:29:05 +08:00
liuyihe 249bf0ab4b 修改提交 2026-09-08 20:25:05 +08:00
ouyangxq da5cf0eb64 fix(deploy): sync inbound fee pricing columns 2026-09-08 20:23:02 +08:00
ouyangxq 11076cef00 Merge remote-tracking branch 'origin/test' into dev 2026-09-08 20:22:49 +08:00
ouyangxq b19b828861 fix(sql): sync phase5 delivery permissions 2026-09-08 20:18:35 +08:00
ouyangxq 2f01fadeeb chore(sql): sync container workspace permissions 2026-09-08 20:05:17 +08:00
ouyangxq d94c902d75 feat(firstleg): split domestic and overseas container workbenches 2026-09-08 19:48:16 +08:00
ouyangxq 19d9a89651 feat(iam): split domestic and overseas container permissions 2026-09-08 19:11:29 +08:00
ouyangxq b0c81e337d feat(firstleg): expose batch carrier actual facts 2026-09-08 19:09:08 +08:00
ouyangxq 75e67fa980 feat(iam): add exception order process permission 2026-09-08 18:51:22 +08:00
ouyangxq d1090671ce fix(lastleg): enforce delivery and return workflows 2026-09-08 18:49:52 +08:00
ouyangxq d42629d289 feat(lastleg): add direct inventory balance detail 2026-09-08 18:47:15 +08:00
ouyangxq 82337da0cc Merge remote-tracking branch 'origin/test' into dev 2026-09-08 18:45:13 +08:00
ouyangxq 6514634dcb fix(lastleg): align receiving warehouse checks 2026-09-08 18:41:00 +08:00
ouyangxq 3e7fa40a99 Merge remote-tracking branch 'origin/test' into dev
# Conflicts:
#	lastleg-service/src/main/java/com/baiyue/lastleg/inventory/command/service/InventoryCommandService.java
2026-09-08 18:39:34 +08:00
ouyangxq 47419aad00 Merge remote-tracking branch 'origin/test' into dev
# Conflicts:
#	firstleg/src/main/java/com/baiyue/firstleg/service/SmartLoadingService.java
2026-09-08 18:37:23 +08:00
ouyangxq f6b0bca2bf test(firstleg): mock container tier mapper 2026-09-08 18:32:59 +08:00
ouyangxq db98da5689 fix(lastleg): restore returned inventory from shipment source 2026-09-08 18:30:04 +08:00
ouyangxq 047ed674b8 feat(file): support overseas exception evidence 2026-09-08 18:26:26 +08:00
ouyangxq 3b88e5f6ab fix(firstleg): reprice container fee on loading changes 2026-09-08 18:24:04 +08:00
ouyangxq cd47317e8a test(firstleg): provide container pricing mapper fixture 2026-09-08 18:21:46 +08:00
ouyangxq 6ac80bf659 Merge remote-tracking branch 'origin/test' into dev 2026-09-08 18:21:46 +08:00
ouyangxq 1fd2b544a5 优化 2026-09-08 18:18:51 +08:00
ouyangxq fcb4c055e2 优化 2026-09-08 18:16:54 +08:00
ouyangxq c6657929da Merge remote-tracking branch 'origin/test' into dev 2026-09-08 18:16:53 +08:00
ouyangxq 1a1a19dea7 feat(firstleg): tier container customer freight 2026-09-08 18:04:14 +08:00
ouyangxq 3aaba2d6a3 fix: order dropship schema convergence before assertions 2026-09-08 18:02:47 +08:00
ouyangxq 13d8640b4c Merge remote-tracking branch 'origin/test' into dev 2026-09-08 17:59:53 +08:00
liuyihe 2a27e28f26 修改提交 2026-09-08 17:58:42 +08:00
ouyangxq f3a0a45e59 fix(lastleg): stock receipts into scanned locations 2026-09-08 17:55:29 +08:00
ouyangxq b0fd797cd2 feat(firstleg): support customer price books 2026-09-08 17:42:20 +08:00
ouyangxq 751bdbf880 fix: converge dropship development schema 2026-09-08 17:38:43 +08:00
ouyangxq 0a9c798580 Merge remote-tracking branch 'origin/test' into dev
# Conflicts:
#	firstleg/src/main/java/com/baiyue/firstleg/client/OrderCargoClient.java
#	order/src/main/java/com/baiyue/order/controller/InternalOrderCargoController.java
#	order/src/test/java/com/baiyue/order/controller/InternalOrderCargoControllerTest.java
2026-09-08 17:34:12 +08:00
ouyangxq 60a41ec3ea fix: reserve stable inbound code for draft label printing 2026-09-08 17:33:44 +08:00
ouyangxq eead11e99d feat(order): add batch review preview 2026-09-08 17:32:56 +08:00
ouyangxq 442518c382 feat(firstleg): add private warehouse delivery fee 2026-09-08 17:27:20 +08:00
ouyangxq 6c2d169798 feat: complete dropship palletization workflow 2026-09-08 17:24:48 +08:00
ouyangxq ba81dece5e feat(firstleg): expose authoritative measurement identifiers 2026-09-08 17:24:29 +08:00
ouyangxq e597bebafd feat(order): expose cargo shipment binding 2026-09-08 17:17:57 +08:00
ouyangxq 2e60f2cd77 feat(firstleg): enforce pricing adjustment bounds 2026-09-08 17:13:43 +08:00
ouyangxq 78a73a1b0e feat(firstleg): expose loading item tax mode 2026-09-08 17:07:06 +08:00
ouyangxq e9182e8cfe Merge remote-tracking branch 'origin/test' into dev
# Conflicts:
#	AGENTS.md
2026-09-08 17:01:59 +08:00
ouyangxq 40fe0ba0e1 fix(lastleg): keep inventory warehouse snapshots canonical 2026-09-08 16:16:20 +08:00
ouyangxq 6908969307 fix(lastleg): reject unavailable receiving pallets 2026-09-08 14:52:08 +08:00
ouyangxq a0ad3382af fix(warehouse-overseas): enforce pallet location invariant 2026-09-08 14:47:35 +08:00
ouyangxq 07688005a1 优化 2026-09-08 14:07:31 +08:00
ouyangxq d318f4a450 fix(lastleg): create remaining container plans 2026-09-08 13:52:37 +08:00
ouyangxq 3c05525e27 fix(lastleg): search frozen warehouse code and name 2026-09-08 13:14:13 +08:00
ouyangxq 096ab8d48b fix(test): align receiving pallets with FBA destinations 2026-09-08 12:45:46 +08:00
ouyangxq 847df6a85f fix(lastleg): keep completed containers editable 2026-09-08 11:51:04 +08:00
ouyangxq cc6df5993f fix(lastleg): enforce container warehouse plan facts 2026-09-08 11:00:39 +08:00
ouyangxq 61ffff1be0 fix: keep terminal drayage actions executable 2026-09-08 07:39:17 +08:00
ouyangxq 287c0f3ce8 fix: publish holds for local overseas orders 2026-09-08 00:51:32 +08:00
ouyangxq bb68e8a580 fix: route terminal drayage through gateway 2026-09-08 00:23:40 +08:00
ouyangxq eeb087e453 fix: make terminal drayage migration restartable 2026-09-08 00:11:31 +08:00
ouyangxq e32fe65fa8 feat: complete tail order controls and terminal drayage 2026-09-07 23:58:18 +08:00
ouyangxq f018ca85a9 test(overseas): retain marker merchant fixture 2026-09-07 21:26:03 +08:00
ouyangxq ac18f69ec5 fix(lastleg): preserve dropship source pallets 2026-09-07 21:10:43 +08:00
ouyangxq 98dafc13ca fix(lastleg): align outbound pallet workflows 2026-09-07 20:51:03 +08:00
ouyangxq 4895c4f738 优化 2026-09-07 19:59:31 +08:00
ouyangxq df8610f458 fix(lastleg): refresh received pallets before delivery transfer 2026-09-07 19:16:30 +08:00
ouyangxq e0bb7de0cf fix: isolate invalid container progress snapshots 2026-09-07 16:36:21 +08:00
ouyangxq d622ddd37f fix: isolate invalid receiving candidates 2026-09-07 16:06:08 +08:00
ouyangxq ff2b02b102 fix: finalize smart dispatch drafts 2026-09-07 15:54:10 +08:00
ouyangxq f0939c91c3 优化 2026-09-07 15:34:36 +08:00
ouyangxq 1b47158770 优化 2026-09-07 15:07:34 +08:00
ouyangxq 693e10188c fix: refresh overseas authorization on user status change 2026-09-07 14:22:26 +08:00
ouyangxq 2c5fda33e7 fix: complete overseas fulfillment acceptance 2026-09-07 14:03:00 +08:00
ouyangxq fcf1771967 fix: restore overseas list queries and identifiers 2026-09-07 12:30:17 +08:00
ouyangxq 30e883fe0c test: complete held binding fixture 2026-09-07 12:17:56 +08:00
ouyangxq 51e5c6dc64 Merge remote-tracking branch 'origin/test' into dev 2026-09-07 12:17:01 +08:00
ouyangxq e281a8f20b fix: preserve hold binding replays 2026-09-07 12:16:55 +08:00
ouyangxq 0aecc90d11 test: cover concurrent inbound hold 2026-09-07 12:16:19 +08:00
ouyangxq a077f828ae feat: return authorization snapshot outside jwt 2026-09-07 12:14:48 +08:00
ouyangxq c6b598daa7 Merge remote-tracking branch 'origin/test' into dev 2026-09-07 12:10:19 +08:00
ouyangxq 41582fe17b fix: close inbound hold bypass paths 2026-09-07 12:10:11 +08:00
ouyangxq a99ca36316 fix: align overseas outbound schema entry 2026-09-07 11:48:49 +08:00
ouyangxq 4963889024 fix: project overseas authorization outside jwt 2026-09-07 11:37:06 +08:00
ouyangxq 6b479d1a68 test: expand inbound hold acceptance 2026-09-07 11:22:57 +08:00
ouyangxq 0c8f6fe2b3 feat: add inbound hold lifecycle 2026-09-07 11:20:10 +08:00
ouyangxq e52ecb9315 merge: sync test into dev before hold flow 2026-09-07 10:37:14 +08:00
liuyihe 9d213b3843 修改提交 2026-09-06 16:02:11 +08:00
liuyihe 3ae8964fe5 修改提交 2026-09-06 15:02:33 +08:00
ouyangxq 7fdc58380f test: include relabel charge amount 2026-09-06 12:24:42 +08:00
ouyangxq 1a7d4bb39f fix: order wallet database assertions 2026-09-06 12:02:11 +08:00
ouyangxq 42ddc19826 test: deduplicate acceptance coverage gaps 2026-09-06 11:41:46 +08:00
ouyangxq 54af43a99d test: align wallet ledger assertions with contract 2026-09-06 11:15:48 +08:00
ouyangxq 0fbab09890 test: preserve uploaded file ids in wallet flow 2026-09-06 11:09:53 +08:00
ouyangxq 20b3074cb1 test: automate overseas wallet and pallet cost closure 2026-09-06 11:00:06 +08:00
ouyangxq 64bf5aa0fe fix: accept wallet result event hashes 2026-09-05 23:32:31 +08:00
ouyangxq 81917423eb fix: relay wallet result events 2026-09-05 23:24:34 +08:00
ouyangxq af7852416a fix: accept prefixed wallet event hashes 2026-09-05 23:13:24 +08:00
ouyangxq 3c535731f7 fix: configure finance wallet event delivery 2026-09-05 22:59:08 +08:00
ouyangxq 718a54ec9e fix: bound wallet event metric labels 2026-09-05 22:34:56 +08:00
ouyangxq 5fbd2bbd04 feat: close overseas wallet and pallet cost flow 2026-09-05 22:26:25 +08:00
ouyangxq f62283fb7f test(finance): cover internal wallet token boundary 2026-09-05 16:46:40 +08:00
ouyangxq 8400636df4 test(finance): expand wallet credit boundary coverage 2026-09-05 16:43:33 +08:00
ouyangxq ede63fac24 fix(finance): align permission migration totals 2026-09-05 12:16:57 +08:00
ouyangxq ba4407eb0d feat(finance): add credit and wallet hold lifecycle 2026-09-05 11:54:55 +08:00
ouyangxq 6f2b2babc6 fix(web): map missing multipart parts to bad request 2026-09-05 10:44:13 +08:00
ouyangxq 37183ac2f0 fix(deploy): identify development wallet runtime 2026-09-05 09:01:43 +08:00
ouyangxq 3df6fb511f fix(finance): expose sample recharge QR in dev 2026-09-05 08:56:02 +08:00
ouyangxq cf955477d8 fix(finance): enable development recharge channels 2026-09-05 08:50:39 +08:00
ouyangxq b4fff67303 chore(finance): enable development recharge sample 2026-09-05 08:41:24 +08:00
ouyangxq ad101ea42e fix(sql): preserve quoted audit comments 2026-09-05 08:39:24 +08:00
ouyangxq 5792630231 feat(finance): add customer wallet recharge flow 2026-09-05 08:34:40 +08:00
ouyangxq 6002e4223b test: align smart loading mapper signature 2026-09-04 22:18:19 +08:00
ouyangxq 7bfccc87fc test: guard unified sql comment ordering 2026-09-04 18:54:12 +08:00
ouyangxq 77817b4c94 优化 2026-09-04 18:47:57 +08:00
ouyangxq 7c5ed85a36 test: select live unpaid settlement fixture 2026-09-04 18:16:27 +08:00
ouyangxq 991bab9cb1 test: stabilize full development acceptance 2026-09-04 17:17:08 +08:00
ouyangxq 61b7b3fb02 test: align task registry index contract 2026-09-04 16:43:51 +08:00
ouyangxq 07bcf316e8 Merge remote-tracking branch 'origin/test' into dev
# Conflicts:
#	scripts/acceptance-finance-full-contract.test.mjs
#	scripts/admin-test-env-acceptance.mjs
2026-09-04 16:43:43 +08:00
ouyangxq 1e980638c4 优化 2026-09-04 16:23:38 +08:00
ouyangxq 23bb915d92 test: preserve direct paid finance ordering 2026-09-04 16:20:09 +08:00
ouyangxq d284fc0637 优化 2026-09-04 16:06:47 +08:00
ouyangxq b8a649e705 test: retry transient unauthenticated upload probe 2026-09-04 16:04:12 +08:00
ouyangxq e477f73983 Merge remote-tracking branch 'origin/test' into dev
# Conflicts:
#	AGENTS.md
#	scripts/AGENTS.md
#	scripts/admin-test-env-modules/README.md
#	scripts/agents-index-contract.ps1
2026-09-04 16:03:09 +08:00
ouyangxq 56a6e690ed 优化 2026-09-04 16:02:05 +08:00
ouyangxq 1f1a7825e3 优化 2026-09-04 16:02:03 +08:00
ouyangxq c6282e7e2f test: align smart loading limit error code 2026-09-04 15:56:17 +08:00
ouyangxq 6cd09ed2fa 优化 2026-09-04 15:55:43 +08:00
ouyangxq dc6a0debca fix: deduplicate unified order locations 2026-09-04 15:54:53 +08:00
ouyangxq c3f45c6bff fix: refresh dimension config response 2026-09-04 15:44:00 +08:00
ouyangxq eba86bb666 Merge remote-tracking branch 'origin/test' into dev
# Conflicts:
#	scripts/admin-test-env-modules/phase3-producer-chain-contract.test.mjs
#	scripts/admin-test-env-modules/phase4-overseas-warehouse-contract.test.mjs
2026-09-04 15:41:18 +08:00
ouyangxq 15b16634db test: cover smart loading quantity operations 2026-09-04 15:36:56 +08:00
ouyangxq 9c96bdfaa8 test: require incremental pallet growth 2026-09-04 15:30:59 +08:00
ouyangxq 7babcb9e0d test: expand truck attachment acceptance contract 2026-09-04 15:29:43 +08:00
ouyangxq c8c402ffc5 test: detect shared overflow evidence 2026-09-04 15:29:06 +08:00
ouyangxq c55f2aed11 test: add pallet location http matrix 2026-09-04 15:24:45 +08:00
ouyangxq 4aed53a8af 优化 2026-09-04 15:19:45 +08:00
ouyangxq 1c8d058d71 test: avoid duplicate overseas PDA lock assertion 2026-09-04 15:18:57 +08:00
ouyangxq 21796cb83d Merge remote-tracking branch 'origin/test' into dev 2026-09-04 15:16:16 +08:00
ouyangxq 2c21d27f61 fix: keep location guidance non-blocking 2026-09-04 15:15:12 +08:00
ouyangxq e23c5dcb7b 优化 2026-09-04 15:06:36 +08:00
ouyangxq 8a0366af5b fix: reserve locations from measured volume 2026-09-04 15:05:44 +08:00
ouyangxq f94fbaa9b4 Merge remote-tracking branch 'origin/test' into dev
# Conflicts:
#	AGENTS.md
#	scripts/agents-index-contract.ps1
2026-09-04 14:50:25 +08:00
ouyangxq 5e013489e0 优化 2026-09-04 14:41:23 +08:00
ouyangxq 760c7b561d fix: stabilize smart loading plan reads 2026-09-04 14:28:25 +08:00
ouyangxq 2c94e212ca 优化 2026-09-04 14:17:34 +08:00
ouyangxq 053c538f94 fix: preserve pallet snapshots across partial pick 2026-09-04 14:09:41 +08:00
ouyangxq 3399ae7488 fix: return A0404 for missing PDA pallets 2026-09-04 14:07:18 +08:00
ouyangxq f6247ce232 Merge remote-tracking branch 'origin/test' into dev 2026-09-04 14:06:02 +08:00
ouyangxq b79d136925 优化 2026-09-04 14:04:52 +08:00
ouyangxq f70d38a034 优化 2026-09-04 14:04:23 +08:00
ouyangxq b428ac1c6c Merge remote-tracking branch 'origin/test' into dev 2026-09-04 14:04:07 +08:00
ouyangxq 93f06f8ff8 fix: fall back to order declared volume 2026-09-04 14:03:04 +08:00
ouyangxq a08559e163 test: use original token for dynamic stale snapshot 2026-09-04 13:50:11 +08:00
ouyangxq 7feb80689b test: verify pallet block settlement over HTTP 2026-09-04 13:48:03 +08:00
ouyangxq 7d9b2749d5 Merge remote-tracking branch 'origin/test' into dev 2026-09-04 13:47:47 +08:00
ouyangxq 660e284e25 fix: reserve pallet block from declared volume 2026-09-04 13:28:57 +08:00
ouyangxq 944ecb2bf3 fix: allocate pallet locations on weight confirmation 2026-09-04 13:21:37 +08:00
ouyangxq 81470359f2 fix: include customer cross-border authorities overseas 2026-09-04 13:15:28 +08:00
ouyangxq 54cb9ceade feat: complete overseas customer and PDA workflows 2026-09-04 12:59:37 +08:00
ouyangxq a274d029bb Merge remote-tracking branch 'origin/test' into dev 2026-09-04 12:55:52 +08:00
ouyangxq d6956d396c fix: remove legacy PDA release columns 2026-09-04 12:51:17 +08:00
ouyangxq 58bf9a3503 docs: register pallet guide acceptance 2026-09-04 12:49:15 +08:00
ouyangxq f556d3ca7e test: cover pallet location guide over HTTP 2026-09-04 12:48:55 +08:00
ouyangxq db6ed676ac test: align pallet block contracts 2026-09-04 12:35:55 +08:00
ouyangxq 209255f17e 优化 2026-09-04 12:31:52 +08:00
ouyangxq 9bf0b4d331 Merge remote-tracking branch 'origin/test' into dev
# Conflicts:
#	order/AGENTS.md
2026-09-04 12:29:57 +08:00
ouyangxq 1407f616a1 Merge remote-tracking branch 'origin/test' into test 2026-09-04 12:29:25 +08:00
ouyangxq 8099343fca fix: align PDA inventory with pallet location blocks 2026-09-04 12:28:47 +08:00
liuyihe 5c094fd0fd 修改提交 2026-09-04 12:23:26 +08:00
ouyangxq b697e4c9f0 优化 2026-09-04 12:18:36 +08:00
ouyangxq 566b07be62 Merge remote-tracking branch 'origin/test' into test 2026-09-04 12:03:17 +08:00
ouyangxq b971ca2f7c test: expand domestic container acceptance coverage 2026-09-04 12:02:38 +08:00
liuyihe 6f32971d79 修改提交 2026-09-04 11:50:01 +08:00
ouyangxq 55cad93be3 Merge branch 'test' of http://8.163.30.128:10082/java-project/baiyue-firstleg into test 2026-09-04 11:35:21 +08:00
ouyangxq f23f3ea26d 优化 2026-09-04 11:35:19 +08:00
liuyihe 451c791f10 修改提交 2026-09-04 11:34:25 +08:00
ouyangxq c3729249c4 Merge branch 'test' of http://8.163.30.128:10082/java-project/baiyue-firstleg into test 2026-09-04 10:51:26 +08:00
ouyangxq 7a0e158b51 优化 2026-09-04 10:51:25 +08:00
ouyangxq da23683440 Merge remote-tracking branch 'origin/test' into dev 2026-09-04 10:37:19 +08:00
ouyangxq a0ba933db3 Merge remote-tracking branch 'origin/test' into dev
# Conflicts:
#	firstleg/src/test/java/com/baiyue/firstleg/service/CartonMeasurementServiceTest.java
#	firstleg/src/test/java/com/baiyue/firstleg/service/CustomerDimensionConfigServiceTest.java
#	scripts/admin-test-env-modules/inbound-measurement.mjs
2026-09-04 10:37:12 +08:00
liuyihe e6859c470c 修改提交 2026-09-04 10:34:35 +08:00
liuyihe 4ca5a57e6a 修改提交 2026-09-04 10:13:46 +08:00
ouyangxq edadca18c6 perf(firstleg): streamline smart loading assignments 2026-09-04 09:57:39 +08:00
ouyangxq b5be60140e test(firstleg): align full acceptance contracts 2026-09-04 00:41:34 +08:00
ouyangxq ce3cd7e80f fix(firstleg): preserve coload groups in filtered candidates 2026-09-04 00:33:07 +08:00
ouyangxq 65b29adb01 修复智能装柜日期筛选调用 2026-09-03 22:05:27 +08:00
ouyangxq d3eaa901f5 优化 2026-09-03 21:50:21 +08:00
ouyangxq b8826c3e92 优化 2026-09-03 21:48:46 +08:00
ouyangxq eca2c94aa3 优化 2026-09-03 21:48:40 +08:00
ouyangxq 2b461cc6e8 Merge branch 'test' of http://8.163.30.128:10082/java-project/baiyue-firstleg into test 2026-09-03 21:34:28 +08:00
ouyangxq a5aecf6171 优化 2026-09-03 21:34:27 +08:00
liuyihe da4cb16d52 Merge remote-tracking branch 'origin/test' into test 2026-09-03 21:33:33 +08:00
liuyihe bfb139c7c7 修改提交 2026-09-03 21:33:23 +08:00
ouyangxq effab80db9 Merge branch 'test' into dev 2026-09-03 21:30:08 +08:00
ouyangxq 26c2d7aacd 修复设备测量箱规计算编译错误 2026-09-03 21:29:51 +08:00
ouyangxq 2baf1b1460 Merge branch 'test' of http://8.163.30.128:10082/java-project/baiyue-firstleg into test 2026-09-03 21:25:54 +08:00
ouyangxq dd1682a83b 优化 2026-09-03 21:25:53 +08:00
ouyangxq 1a3f94dfcb Merge remote-tracking branch 'origin/test' into dev
# Conflicts:
#	firstleg/src/test/java/com/baiyue/firstleg/service/CartonMeasurementServiceTest.java
2026-09-03 21:23:35 +08:00
liuyihe baabfdabba 修改提交 2026-09-03 21:09:22 +08:00
ouyangxq 0ad8bb66cc test: bound reused price fixture names 2026-09-03 21:00:11 +08:00
ouyangxq 47a043324f Merge remote-tracking branch 'origin/test' into dev 2026-09-03 20:59:58 +08:00
ouyangxq 171f450e59 优化 2026-09-03 20:38:47 +08:00
ouyangxq a24d55f526 优化 2026-09-03 20:24:52 +08:00
ouyangxq 8b2c2b32b2 test: align tally permission total 2026-09-03 19:41:25 +08:00
ouyangxq 2063d29d16 fix: complete development acceptance paths 2026-09-03 19:10:18 +08:00
ouyangxq 4df0ae4811 Merge remote-tracking branch 'origin/test' into dev 2026-09-03 19:09:04 +08:00
ouyangxq acdc0943c0 feat(order): derive batch customer context 2026-09-03 18:22:44 +08:00
ouyangxq 514a585de2 Merge remote-tracking branch 'origin/test' into dev
# Conflicts:
#	AGENTS.md
#	firstleg/AGENTS.md
#	scripts/agents-index-contract.ps1
2026-09-03 18:22:05 +08:00
ouyangxq ca288dec72 优化 2026-09-03 18:13:00 +08:00
ouyangxq 66ad9f5c85 优化 2026-09-03 18:05:34 +08:00
ouyangxq 61adfc7818 feat(order): refine batch import workbook 2026-09-03 18:03:42 +08:00
ouyangxq 880f923462 Merge remote-tracking branch 'origin/dev' into dev 2026-09-03 17:52:38 +08:00
ouyangxq 03591d6fbd test(firstleg): 覆盖超限设备测量实际尺寸 2026-09-03 17:52:31 +08:00
ouyangxq 1d8432b8ef Merge remote-tracking branch 'origin/test' into dev 2026-09-03 17:48:01 +08:00
ouyangxq d7132536a8 Merge branch 'test' into dev 2026-09-03 17:44:23 +08:00
ouyangxq 8911d4a242 固定 Jenkins Job 部署环境 2026-09-03 17:43:52 +08:00
ouyangxq 6130a13b6b 修复超限单箱测量尺寸计算 2026-09-03 17:17:57 +08:00
ouyangxq b8bdff2f1f fix(lastleg): preserve readable exception operators 2026-09-03 17:17:10 +08:00
ouyangxq 997419ea6b Merge remote-tracking branch 'origin/test' into dev 2026-09-03 17:14:16 +08:00
ouyangxq 253ca82b99 fix(order): load all warehouse template options 2026-09-03 17:04:57 +08:00
ouyangxq 3664f036cc feat(order): localize batch import workbook 2026-09-03 16:33:33 +08:00
ouyangxq 5c524fb92c test: stabilize phase5 acceptance inventory fixture 2026-09-03 16:20:17 +08:00
ouyangxq a66ecfab98 Merge remote-tracking branch 'origin/test' into dev
# Conflicts:
#	deploy/sql/头程数据库统一增量-MySQL8.sql
2026-09-03 16:19:48 +08:00
ouyangxq dac02e5253 优化 2026-09-03 16:09:22 +08:00
ouyangxq 0307acf859 Merge branch 'test' of http://8.163.30.128:10082/java-project/baiyue-firstleg into test 2026-09-03 15:35:28 +08:00
ouyangxq 36d35276b7 优化 2026-09-03 15:35:26 +08:00
ouyangxq 904b495613 Merge remote-tracking branch 'origin/test' into dev
# Conflicts:
#	deploy/maven/settings-cn.xml
2026-09-03 15:23:55 +08:00
liuyihe 29ec202563 修改提交 2026-09-03 15:11:40 +08:00
liuyihe e29695eb3f 修改提交 2026-09-03 15:10:35 +08:00
ouyangxq 6eff5a4b9b 优化 2026-09-03 15:00:55 +08:00
ouyangxq 55592a1f08 Merge branch 'test' of http://8.163.30.128:10082/java-project/baiyue-firstleg into test 2026-09-03 13:49:41 +08:00
ouyangxq a927903a66 优化 2026-09-03 13:49:38 +08:00
ouyangxq 2041819df5 优化 2026-09-03 13:35:17 +08:00
ouyangxq 732fc66670 Merge remote-tracking branch 'origin/test' into dev 2026-09-03 12:01:00 +08:00
ouyangxq 0d52ee5948 修复订单申报更新测试依赖装配 2026-09-03 11:59:25 +08:00
liuyihe fb91168614 Merge remote-tracking branch 'origin/test' into test 2026-09-03 11:59:15 +08:00
liuyihe f561e6bb09 修改提交 2026-09-03 11:59:06 +08:00
ouyangxq 8de52f65bd Merge remote-tracking branch 'origin/test' into dev 2026-09-03 11:55:55 +08:00
ouyangxq bdcc32a095 Merge remote-tracking branch 'origin/test' into dev
# Conflicts:
#	deploy/sql/头程数据库统一增量-MySQL8.sql
2026-09-03 11:55:46 +08:00
ouyangxq 59b42c7579 优化 2026-09-03 11:54:17 +08:00
ouyangxq fd3dbb2fc6 优化 2026-09-03 11:47:28 +08:00
ouyangxq b15d003217 Merge remote-tracking branch 'origin/test' into dev 2026-09-03 11:34:52 +08:00
ouyangxq 757f6ef24f 修复头程测量与计费测试门禁 2026-09-03 11:32:08 +08:00
ouyangxq d56af7e036 Merge remote-tracking branch 'origin/test' into dev
# Conflicts:
#	firstleg/src/main/java/com/baiyue/firstleg/service/MeasurementBatchService.java
#	lastleg-service/src/main/resources/db/lastleg.sql
2026-09-03 11:03:25 +08:00
liuyihe a3715f618f 一件代发拣货 2026-09-03 10:44:34 +08:00
ouyangxq cf77e19765 feat(file): expedite pending export retries 2026-09-03 10:40:21 +08:00
ouyangxq 6b2339e50a feat(file): allow immediate retry during export backoff 2026-09-03 10:39:55 +08:00
ouyangxq 16a60a9175 fix(file): align order export batch limit 2026-09-03 10:23:01 +08:00
ouyangxq 12b749d7d8 fix(iam): grant domestic order export permission 2026-09-03 10:12:08 +08:00
ouyangxq 7359fbc093 fix: hide completed receiving containers 2026-09-03 10:08:57 +08:00
ouyangxq 59add49408 fix: synchronize order declaration schema 2026-09-02 20:49:26 +08:00
ouyangxq 11d89435f5 Merge remote-tracking branch 'origin/test' into dev
# Conflicts:
#	scripts/admin-test-env-acceptance.mjs
#	scripts/agents-index-contract.ps1
#	scripts/full-regression-entry-contract.test.mjs
2026-09-02 20:32:23 +08:00
ouyangxq 6f5398770b feat(finance): expose inbound code on bill lines 2026-09-02 20:25:54 +08:00
ouyangxq e4ee999098 优化 2026-09-02 20:14:47 +08:00
ouyangxq fa9a98f078 优化 2026-09-02 19:53:07 +08:00
ouyangxq 71f6dfa1eb test: extend finance and cross-region acceptance 2026-09-02 19:29:23 +08:00
ouyangxq 4521090967 Merge remote-tracking branch 'origin/test' into dev 2026-09-02 18:40:20 +08:00
ouyangxq 9adbdfbcc1 统一财务事件投递端口 2026-09-02 18:28:39 +08:00
ouyangxq 4e2703295c fix: use published fees for client phase2 acceptance 2026-09-02 18:25:42 +08:00
ouyangxq b998a0c073 Merge remote-tracking branch 'origin/test' into dev 2026-09-02 18:25:19 +08:00
ouyangxq 30090e741f 修复财务事件桥投递地址 2026-09-02 18:24:39 +08:00
ouyangxq eef45eda65 fix: synchronize unified firstleg migration 2026-09-02 18:17:00 +08:00
ouyangxq 8246093af1 Merge remote-tracking branch 'origin/test' into dev
# Conflicts:
#	auth/AGENTS.md
#	auth/src/main/java/com/baiyue/auth/service/OverseasTokenService.java
#	auth/src/test/java/com/baiyue/auth/service/OverseasTokenServiceTest.java
2026-09-02 18:14:50 +08:00
ouyangxq 596a5c925c 支持柜费按订单归属分摊 2026-09-02 18:14:25 +08:00
ouyangxq 62855915fc 补齐海外财务权限令牌 2026-09-02 18:09:43 +08:00
ouyangxq 6be5e0d81f 优化 2026-09-02 18:03:36 +08:00
ouyangxq 6296f032f0 fix: retain scanned carton traceability in measurement drafts 2026-09-02 17:57:35 +08:00
ouyangxq 2a0f942234 Merge remote-tracking branch 'origin/test' into dev
# Conflicts:
#	deploy/sql/头程数据库统一增量-MySQL8.sql
#	event-bridge-service/src/main/java/com/baiyue/eventbridge/metrics/EventBridgeMetrics.java
#	firstleg/src/main/resources/db/phase3-container-customs-schema.sql
#	order/src/main/resources/db/order.sql
#	scripts/db/incremental.sql
2026-09-02 17:56:20 +08:00
ouyangxq 18c673811f 补齐财务事件监控标签 2026-09-02 17:45:17 +08:00
ouyangxq 5099a46581 补齐统一数据库中文注释 2026-09-02 17:36:37 +08:00
ouyangxq 3bdaba2c60 完善财务费用来源与柜级分摊 2026-09-02 17:07:03 +08:00
ouyangxq c5c80d5ab6 fix(lastleg): align deployment schema contracts 2026-09-02 16:55:38 +08:00
ouyangxq e06c12c263 fix(lastleg): resolve exceptions by inbound relation 2026-09-02 16:48:45 +08:00
ouyangxq 6cd477a037 修正四期全量验收夹具 2026-09-02 16:48:05 +08:00
ouyangxq a8c71fef12 fix(acceptance): select booking agents by carrier 2026-09-02 16:38:44 +08:00
ouyangxq 1b1f2718b3 fix(firstleg): sync finance allocation schema 2026-09-02 16:24:47 +08:00
ouyangxq 42021b351e Merge remote-tracking branch 'origin/test' into dev
# Conflicts:
#	common-event-contract/src/main/java/com/baiyue/common/event/EventTypes.java
2026-09-02 16:19:06 +08:00
ouyangxq 03d04431f2 优化 2026-09-02 16:17:41 +08:00
ouyangxq 12fbf2aa67 隔离头程定时任务执行线程 2026-09-02 16:10:52 +08:00
ouyangxq 7eabbdcd45 fix(firstleg): include declaration sync schema 2026-09-02 16:07:32 +08:00
ouyangxq 57d0073b40 Merge remote-tracking branch 'origin/test' into dev
# Conflicts:
#	scripts/admin-test-env-modules/phase4-overseas-pda.mjs
2026-09-02 16:04:08 +08:00
ouyangxq 601ae83504 收敛海外PDA回归夹具选择 2026-09-02 16:01:58 +08:00
ouyangxq cc04a5570c fix(acceptance): update declaration before fulfillment convergence 2026-09-02 15:59:13 +08:00
ouyangxq 337033b350 fix(acceptance): read nested order declaration details 2026-09-02 15:53:11 +08:00
ouyangxq afdee17ec0 Merge remote-tracking branch 'origin/test' into dev 2026-09-02 15:53:11 +08:00
ouyangxq 5f688d6131 修正海外PDA实装箱唛验收夹具 2026-09-02 15:47:13 +08:00
ouyangxq feda5cd061 Merge remote-tracking branch 'origin/test' into dev
# Conflicts:
#	lastleg-service/src/main/java/com/baiyue/lastleg/pda/move/service/PdaInventoryMoveService.java
#	lastleg-service/src/main/java/com/baiyue/lastleg/pda/stocktake/service/PdaStocktakeService.java
#	lastleg-service/src/test/java/com/baiyue/lastleg/palletops/service/PdaPalletOperationServiceTest.java
#	lastleg-service/src/test/java/com/baiyue/lastleg/pda/move/service/PdaInventoryMoveServiceTest.java
#	lastleg-service/src/test/java/com/baiyue/lastleg/pda/stocktake/service/PdaStocktakeServiceTest.java
2026-09-02 15:46:10 +08:00
ouyangxq ef74579f2c test(overseas): align phase four to five acceptance contracts 2026-09-02 15:43:11 +08:00
ouyangxq 137e4e7c60 Merge remote-tracking branch 'origin/test' into dev
# Conflicts:
#	scripts/admin-test-env-modules/phase4-overseas-pda.mjs
2026-09-02 15:43:00 +08:00
ouyangxq ecf4eb1211 优化 2026-09-02 15:42:22 +08:00
ouyangxq 2902ab6077 fix(deploy): prioritize BaoTa-visible container logs 2026-09-02 15:39:58 +08:00
ouyangxq 25c4dd50b3 fix(deploy): bound infrastructure container logs 2026-09-02 15:30:29 +08:00
ouyangxq 9e0d2f6cdb 修正海外PDA候选验收门禁判断 2026-09-02 15:25:30 +08:00
ouyangxq e946d99ea3 docs: clarify inbound plan pending filter 2026-09-02 15:24:56 +08:00
ouyangxq 0c36ea0e12 docs: align repository guidance with test contracts 2026-09-02 15:22:16 +08:00
ouyangxq eab8a1ae5f Merge remote-tracking branch 'origin/test' into dev
# Conflicts:
#	scripts/admin-test-env-modules/phase4-overseas-pda.mjs
#	scripts/agents-index-contract.ps1
2026-09-02 15:21:20 +08:00
ouyangxq 70d66c2fae 优化 2026-09-02 15:15:41 +08:00
ouyangxq b65f98451d 同步海外PDA柜级转换验收契约 2026-09-02 15:15:07 +08:00
ouyangxq e48e8dca42 fix(deploy): eliminate duplicate container log writes 2026-09-02 15:14:24 +08:00
ouyangxq 8da57487c0 修复一件代发多票事件幂等冲突 2026-09-02 15:04:15 +08:00
ouyangxq 3d81b22850 Merge branch 'test' of http://git.buggoglobal.com/java-project/baiyue-firstleg into dev
# Conflicts:
#	scripts/admin-test-env-modules/phase4-carrier-nodes.mjs
2026-09-02 14:46:15 +08:00
ouyangxq 19a6af1c6d 优化 2026-09-02 14:45:34 +08:00
ouyangxq 9ea4ccb9b1 优化 2026-09-02 14:39:28 +08:00
ouyangxq b6800a48c6 test(acceptance): follow automatic departure flow 2026-09-02 14:16:44 +08:00
ouyangxq afb38f3c3f fix(dispatch): expose PDA departure gate 2026-09-02 14:00:54 +08:00
ouyangxq 25ffd62e14 Merge remote-tracking branch 'origin/test' into dev
# Conflicts:
#	firstleg/src/main/java/com/baiyue/firstleg/loading/service/CabinetLoadingEvidenceService.java
#	firstleg/src/test/java/com/baiyue/firstleg/loading/service/CabinetLoadingEvidenceServiceTest.java
#	scripts/admin-test-env-modules/ocean-bookings.mjs
#	scripts/admin-test-env-modules/phase3-container-customs-fixture.mjs
#	scripts/admin-test-env-modules/phase3-container-documents-contract.test.mjs
#	scripts/admin-test-env-modules/phase3-container-loading.mjs
#	scripts/admin-test-env-modules/phase3-customs.mjs
#	scripts/admin-test-env-modules/phase3-loading-finalize.mjs
#	scripts/admin-test-env-modules/phase3-loading.mjs
#	scripts/admin-test-env-modules/phase3-port-schedules.mjs
#	scripts/admin-test-env-modules/service-center-flow.mjs
2026-09-02 14:00:44 +08:00
ouyangxq 5d098c9762 test(acceptance): align retained state fixtures 2026-09-02 13:59:05 +08:00
ouyangxq f4c139815c 优化 2026-09-02 13:38:44 +08:00
ouyangxq 69ab3e9a90 优化 2026-09-02 13:34:53 +08:00
ouyangxq 68340eb616 test(acceptance): align current gateway contracts 2026-09-02 13:26:24 +08:00
ouyangxq f7f8ff53c0 test(acceptance): align automatic preload workflows 2026-09-02 13:03:03 +08:00
ouyangxq 275d4df35d fix(firstleg): make loading evidence upload replayable 2026-09-02 12:53:18 +08:00
ouyangxq e3fd48c3b9 fix(firstleg): restore smart loading candidate projection 2026-09-02 12:33:26 +08:00
ouyangxq a4d8fa9f8b fix(order): preserve submitted review status 2026-09-02 12:19:46 +08:00
ouyangxq 1574263836 fix(lastleg): validate dropship SKU for PDA outbound 2026-09-02 12:09:14 +08:00
ouyangxq 08be508501 Merge remote-tracking branch 'origin/test' into dev
# Conflicts:
#	order/AGENTS.md
#	scripts/agents-index-contract.ps1
2026-09-02 12:08:08 +08:00
ouyangxq 9e70f4339a fix(file): allow standalone legal-person ID upload 2026-09-02 12:07:32 +08:00
ouyangxq f0da6e5110 优化 2026-09-02 11:59:54 +08:00
ouyangxq d786b88030 fix(firstleg): map declaration inbound projection 2026-09-02 11:51:56 +08:00
ouyangxq 6acb5297e5 test(firstleg): lock internal declaration endpoints 2026-09-02 10:45:09 +08:00
ouyangxq bb9fb9ffe6 fix(firstleg): retain declaration event idempotency history 2026-09-02 10:44:17 +08:00
ouyangxq 8e7835fe86 优化 2026-09-02 10:41:32 +08:00
ouyangxq d0d0b2cdda Merge branch 'test' of http://git.buggoglobal.com/java-project/baiyue-firstleg into dev 2026-09-02 10:34:02 +08:00
ouyangxq 31996491be test(firstleg): bind channel change hash to full payload 2026-09-02 10:29:24 +08:00
ouyangxq e0a1f15777 优化 2026-09-02 10:27:47 +08:00
ouyangxq 371aa3dbcb 优化 2026-09-02 10:26:40 +08:00
ouyangxq 5f8b7baa06 优化 2026-09-02 10:26:37 +08:00
ouyangxq 37c37c75c8 优化 2026-09-02 10:26:34 +08:00
ouyangxq 70bbdd5b7b fix(auth): decouple managed login phones from company contacts 2026-09-02 10:11:11 +08:00
ouyangxq da56f261cd 优化 2026-09-02 10:05:56 +08:00
ouyangxq 8a66af81b1 优化 2026-09-02 10:04:54 +08:00
ouyangxq e83dff6ac7 优化 2026-09-02 10:04:18 +08:00
ouyangxq 79c390ad58 test: close phase five inventory snapshot coverage 2026-09-02 03:51:07 +08:00
ouyangxq a4a4306154 test: align phase five dev acceptance flows 2026-09-02 03:32:35 +08:00
ouyangxq f3be06a3e0 test: cover merchant phase 5 delivery 2026-09-02 02:51:58 +08:00
ouyangxq b5d7be7d28 fix: replay completed dispatch picks 2026-09-02 02:03:15 +08:00
ouyangxq 666add1cf9 fix: complete overseas PDA inventory ledgers 2026-09-02 01:47:00 +08:00
ouyangxq cc02037f78 fix: preserve dropship pallets in container plans 2026-09-02 01:17:00 +08:00
ouyangxq fd9a070efc fix: settle whole dropship pallets by sku 2026-09-02 00:27:04 +08:00
ouyangxq 7e6c7a4b9d test: preserve whole dropship pallets in producer flow 2026-09-02 00:16:53 +08:00
ouyangxq cd749733de fix: preserve dropship sku loading details 2026-09-02 00:01:39 +08:00
ouyangxq 571a5544a3 test: pick complete dropship pallet fixtures 2026-09-01 23:47:18 +08:00
ouyangxq fa7a7b8630 fix: enforce pickup-ready receiving candidates 2026-09-01 23:35:23 +08:00
ouyangxq f4a3b58959 fix: validate overseas handover before settlement 2026-09-01 23:20:34 +08:00
ouyangxq e19224c4b7 fix: make loading evidence replay idempotent 2026-09-01 22:59:50 +08:00
ouyangxq d965a82cfd test: accept automatic outbound confirmation 2026-09-01 22:54:02 +08:00
ouyangxq 7f9649f309 test: align outbound producer with container customs 2026-09-01 22:52:39 +08:00
ouyangxq 20eb524a51 test: complete dropship palletization before smart loading 2026-09-01 22:38:16 +08:00
ouyangxq bc596878d6 fix: keep order declaration schema in unified baseline 2026-09-01 22:30:15 +08:00
ouyangxq aa77cece23 fix: align smart loading candidate mapping 2026-09-01 22:09:01 +08:00
ouyangxq ad5e0812bf fix: restore phase five development acceptance chain 2026-09-01 21:55:27 +08:00
ouyangxq 04e0111756 Merge remote-tracking branch 'origin/test' into dev 2026-09-01 21:51:52 +08:00
ouyangxq b85e021cbe test(order): cover declaration field clearing 2026-09-01 21:11:37 +08:00
ouyangxq e82cefe493 优化 2026-09-01 21:09:33 +08:00
ouyangxq 47723c7c0f feat(file): register domestic order exports 2026-09-01 21:03:57 +08:00
ouyangxq b9cb5555e2 fix(firstleg): isolate declarations from measurement facts 2026-09-01 20:59:30 +08:00
ouyangxq 858061452c feat(order): formalize domestic order export contract 2026-09-01 20:48:44 +08:00
ouyangxq 83f4e1a3de test: align loading tests with latest test contract 2026-09-01 20:47:27 +08:00
ouyangxq b2ec00754a Merge remote-tracking branch 'origin/test' into dev 2026-09-01 20:46:48 +08:00
ouyangxq 94c67b5c51 优化 2026-09-01 20:41:22 +08:00
ouyangxq 6449daf823 fix: allow optional phase5 custom admin role 2026-09-01 20:36:24 +08:00
ouyangxq 974d7b1e43 fix: complete phase5 overseas deployment contract 2026-09-01 20:20:25 +08:00
ouyangxq 8e17401f37 优化 2026-09-01 20:07:38 +08:00
ouyangxq 539030d2b6 fix(file): return controlled missing package errors 2026-09-01 20:01:14 +08:00
ouyangxq d88e5cffce fix: align phase5 overseas permissions 2026-09-01 20:00:49 +08:00
ouyangxq a5d787b279 Merge remote-tracking branch 'origin/test' into dev 2026-09-01 19:59:24 +08:00
ouyangxq b4e496e9cb fix(file): support optional legal ID and order import files 2026-09-01 19:52:42 +08:00
ouyangxq 9fc20d5482 fix: keep ambiguous legacy phones unbound 2026-09-01 19:34:17 +08:00
ouyangxq 271a77fd86 Merge remote-tracking branch 'origin/test' into dev 2026-09-01 19:32:20 +08:00
ouyangxq 2ec8fcb886 fix(order): route customer addresses through gateway 2026-09-01 19:32:01 +08:00
ouyangxq 405d6b94d7 fix(auth): reject placeholder mobile numbers 2026-09-01 19:27:46 +08:00
ouyangxq 176d146b8e fix: preserve merchant warehouse authorities overseas 2026-09-01 19:25:48 +08:00
ouyangxq 28526bf43e Merge remote-tracking branch 'origin/test' into dev 2026-09-01 19:25:16 +08:00
ouyangxq caffcb90a9 feat(order): add customer order foundations 2026-09-01 19:17:43 +08:00
ouyangxq 222c3790dc feat(auth): support username or phone login 2026-09-01 19:15:32 +08:00
ouyangxq b6e81bab6b test: align inbound status contract with dropship palletizing 2026-09-01 19:11:36 +08:00
ouyangxq 1c26f7db9b Merge remote-tracking branch 'origin/test' into dev 2026-09-01 19:00:36 +08:00
ouyangxq 0ba3df2fce test(firstleg): cover booking-level truck lifecycle 2026-09-01 16:36:41 +08:00
ouyangxq 9098d8dfdf test(firstleg): decouple PDA completion from truck 2026-09-01 16:33:57 +08:00
ouyangxq 20d0d84240 fix: preserve lastleg comments in overseas migration 2026-09-01 16:30:41 +08:00
ouyangxq eb385fe906 test(firstleg): align outbound truck lookup contract 2026-09-01 16:24:10 +08:00
ouyangxq ecc83ed64a fix(firstleg): scope truck appointments by booking 2026-09-01 16:21:27 +08:00
ouyangxq c319986738 fix: order lastleg delivery baseline for clean database 2026-09-01 16:18:38 +08:00
ouyangxq a8f8ff2b6d Merge remote-tracking branch 'origin/test' into dev 2026-09-01 15:59:15 +08:00
ouyangxq ca8127ce83 test(firstleg): formalize late container gate acceptance 2026-09-01 15:53:12 +08:00
ouyangxq e02c463be7 优化 2026-09-01 15:46:30 +08:00
ouyangxq aa828ca1eb 优化 2026-09-01 15:43:19 +08:00
ouyangxq df1ffbc6bc fix(firstleg): filter truck booking candidates 2026-09-01 15:41:36 +08:00
ouyangxq 8b2a964bfb fix(firstleg): allow late initial container fill 2026-09-01 15:38:33 +08:00
ouyangxq 07ffd8ddce fix(firstleg): finalize late container gate 2026-09-01 15:27:34 +08:00
ouyangxq d1225845ec 优化 2026-09-01 15:25:15 +08:00
ouyangxq 4d7ea28a61 feat(firstleg): capture container number at pickup 2026-09-01 15:18:18 +08:00
liuyihe 8a94c541cb Merge remote-tracking branch 'origin/dev' into dev 2026-09-01 15:14:24 +08:00
ouyangxq b223ce82d6 Merge remote-tracking branch 'origin/test' into dev 2026-09-01 15:11:52 +08:00
ouyangxq 1e8f504f15 fix(firstleg): allow booking link before container pickup 2026-09-01 15:06:03 +08:00
ouyangxq 94395faeef fix: make phase 5 permission validation mysql compatible 2026-09-01 15:00:38 +08:00
ouyangxq 579c61879d fix: include phase 3 customs baseline in unified sql 2026-09-01 14:54:47 +08:00
ouyangxq 41837180f8 Merge remote-tracking branch 'origin/test' into dev 2026-09-01 14:49:41 +08:00
ouyangxq a10e32a012 test(firstleg): finish task evidence migration 2026-09-01 14:48:51 +08:00
ouyangxq 980d37b9c2 Merge remote-tracking branch 'origin/test' into dev 2026-09-01 14:48:33 +08:00
ouyangxq f2958bef6e 优化 2026-09-01 14:47:45 +08:00
ouyangxq 0e1ad4ed45 优化 2026-09-01 14:47:43 +08:00
ouyangxq 76d2da5bf9 test(firstleg): use task-scoped loading evidence 2026-09-01 14:46:48 +08:00
ouyangxq 46f5a73c47 优化 2026-09-01 14:46:44 +08:00
ouyangxq b610a128de Merge branch 'test' of http://git.buggoglobal.com/java-project/baiyue-firstleg into dev 2026-09-01 14:46:36 +08:00
ouyangxq f0ac0c42c9 test(firstleg): use valid PDF truck fixtures 2026-09-01 14:44:30 +08:00
ouyangxq 105660bd09 test(firstleg): scope loading evidence by cabinet 2026-09-01 14:37:02 +08:00
ouyangxq 2ccfc38cdf test(firstleg): seed booking file before loading release 2026-09-01 14:31:01 +08:00
ouyangxq f551cd8c7a 优化 2026-09-01 14:27:16 +08:00
ouyangxq d2092bf0b6 优化 2026-09-01 14:27:04 +08:00
ouyangxq 97168b19d8 feat: complete phase 5 overseas fulfillment contracts 2026-09-01 14:24:59 +08:00
liuyihe 8b0c99593e 一件代发拣货 2026-09-01 13:41:00 +08:00
ouyangxq 9e32efdd90 test(firstleg): harden measurement demo package 2026-09-01 12:03:21 +08:00
ouyangxq 6eaf484fcd test(firstleg): package carrier node demo checks 2026-09-01 12:02:57 +08:00
ouyangxq 53078b4281 test(firstleg): extend shipment demo regression 2026-09-01 12:02:47 +08:00
ouyangxq 49d216cc13 test(order): harden demo order lifecycle fixture 2026-09-01 12:02:35 +08:00
ouyangxq 9a3b408de0 优化 2026-09-01 11:38:42 +08:00
ouyangxq 68b584128c fix(firstleg): surface existing truck appointment 2026-09-01 11:30:35 +08:00
ouyangxq b5bc77b153 fix(firstleg): atomically finalize PDA completion 2026-09-01 11:11:43 +08:00
ouyangxq ffed9bf898 Merge branch 'dev-lyhfive' of http://git.buggoglobal.com/java-project/baiyue-firstleg into dev 2026-09-01 11:09:14 +08:00
ouyangxq de0eddeee8 修复尾程货主快照与短单号合同 2026-09-01 11:06:26 +08:00
ouyangxq 4991536065 test(firstleg): harden pallet option page contract 2026-09-01 11:03:40 +08:00
ouyangxq 7ace1ded56 优化 2026-09-01 11:02:10 +08:00
ouyangxq 5af79df4bd test(firstleg): align trucking attachment regressions 2026-09-01 11:01:34 +08:00
ouyangxq 6eb53751a3 test(pda): keep overseas acceptance business-scoped 2026-09-01 11:01:20 +08:00
ouyangxq 09e81b429c test(firstleg): align fulfillment matrix with container customs 2026-09-01 10:48:33 +08:00
ouyangxq 179a20fc38 refactor(firstleg): keep trucking payable only 2026-09-01 10:38:15 +08:00
liuyihe f86fab838a 一件代发拣货 2026-09-01 10:35:54 +08:00
ouyangxq 6a0f51f136 优化 2026-09-01 10:29:14 +08:00
ouyangxq 7d9eaa7f43 test(firstleg): verify trucking detail after dispatch 2026-09-01 10:26:22 +08:00
ouyangxq bfc2a4dbd4 test(firstleg): scope trucking warehouse lookup 2026-09-01 10:22:15 +08:00
ouyangxq 0613d1095e test(firstleg): resolve trucking fixtures from active catalogs 2026-09-01 10:21:37 +08:00
ouyangxq 3d7990250f fix(finance): align operating fund export labels 2026-09-01 10:19:35 +08:00
ouyangxq 2c3efad70a test: bound A0 location isolation writes 2026-09-01 10:18:19 +08:00
ouyangxq cdcf3e0f4f test(firstleg): parse platform warehouse scope explicitly 2026-09-01 10:17:41 +08:00
ouyangxq 3d6216910f test(finance): align full acceptance with current workflows 2026-09-01 10:17:03 +08:00
ouyangxq 505188c3fd test(firstleg): make trucking acceptance self-contained 2026-09-01 10:16:07 +08:00
ouyangxq 477971dda1 优化 2026-09-01 10:00:13 +08:00
liuyihe 2f1bda8369 一件代发拣货 2026-09-01 09:57:10 +08:00
ouyangxq 249b182e7f refactor(firstleg): remove customer scope from trucking 2026-09-01 09:51:59 +08:00
ouyangxq 936412f351 test(file-print): extend booking document regression 2026-08-31 20:56:52 +08:00
ouyangxq 0cc4eae209 feat(firstleg): enforce booking-owned truck workflow 2026-08-31 19:55:58 +08:00
ouyangxq f2a980e5c1 fix(file): allow arbitrary ocean booking documents 2026-08-31 19:54:05 +08:00
ouyangxq 47ce84d122 feat(file-print): support ocean booking documents 2026-08-31 19:30:37 +08:00
ouyangxq 01385c4bc7 优化 2026-08-31 18:55:03 +08:00
ouyangxq 9b09663b2e fix(firstleg): allow corrective evidence after completion failure 2026-08-31 18:42:08 +08:00
ouyangxq 838bd93bc4 test(firstleg): align cabinet evidence SQL contracts 2026-08-31 18:31:39 +08:00
ouyangxq 60d45613c7 fix(firstleg): import multipart request annotation 2026-08-31 18:24:58 +08:00
ouyangxq 4ddfc6fb99 chore(firstleg): remove legacy inbound evidence schema 2026-08-31 18:19:25 +08:00
ouyangxq df0fbc1c7f fix(firstleg): scope loading evidence to cabinet tasks 2026-08-31 18:14:36 +08:00
ouyangxq 4795ab958c fix(file): require Firstleg for PDA evidence access 2026-08-31 17:42:19 +08:00
ouyangxq 1288c8937c fix(file): bind loading evidence to cabinet 2026-08-31 17:33:11 +08:00
ouyangxq ba8e3595e4 fix(firstleg): explain coload group eligibility 2026-08-31 15:40:24 +08:00
ouyangxq 17d81b0b94 feat(order): expose unified order channel code 2026-08-31 15:38:18 +08:00
ouyangxq c9792b1f63 fix(firstleg): resolve loading list lifecycle by plan 2026-08-31 15:26:47 +08:00
liuyihe a213be67c5 尾程暂扣回传 2026-08-31 15:22:32 +08:00
ouyangxq 0122ad314b feat(firstleg): expose smart-loading channel directory 2026-08-31 14:54:00 +08:00
ouyangxq ea615b864a fix(firstleg): cancel coload groups by inbound code 2026-08-31 14:53:44 +08:00
liuyihe a4189a0074 提交 2026-08-31 14:39:40 +08:00
ouyangxq 314ce09b5d fix(firstleg): load container overview transport history 2026-08-31 12:39:24 +08:00
ouyangxq 942dbace19 feat: enforce canonical container identity across regions 2026-08-31 12:13:20 +08:00
ouyangxq b386f35ab4 优化 2026-08-31 10:31:51 +08:00
ouyangxq d6f47e650c 优化 2026-08-31 10:19:48 +08:00
ouyangxq bef91151a3 优化 2026-08-31 10:17:42 +08:00
ouyangxq bf1dc1f209 优化 2026-08-31 10:17:03 +08:00
ouyangxq aa583efa4a 优化 2026-08-31 10:09:14 +08:00
liuyihe a4429f95b7 提交 2026-08-30 18:49:15 +08:00
ouyangxq ff6a8498a6 fix(lastleg): resolve actual overseas receiving warehouse 2026-08-30 18:07:38 +08:00
ouyangxq ca9e7b60e8 优化 2026-08-30 17:02:19 +08:00
ouyangxq 0aa77327d5 test(firstleg): stabilize coload smart and short-pick checks 2026-08-30 16:14:07 +08:00
ouyangxq 5c83b81e8a test(firstleg): harden coload capacity acceptance 2026-08-30 16:07:12 +08:00
ouyangxq c9ee319e9d fix(firstleg): refresh recommendation capacity 2026-08-30 16:06:20 +08:00
ouyangxq 4581c8a294 test(firstleg): assert atomic smart coload recommendation 2026-08-30 15:58:14 +08:00
ouyangxq 9a15e78b2e test(firstleg): extend coload group acceptance 2026-08-30 15:55:48 +08:00
ouyangxq bfcb80b326 fix(firstleg): align coload group eligibility 2026-08-29 12:44:42 +08:00
ouyangxq 0cf76df168 fix(firstleg): align coload loading route gates 2026-08-29 12:36:07 +08:00
ouyangxq ba641754de test(firstleg): document coload PDA acceptance 2026-08-29 11:59:53 +08:00
ouyangxq 81ef43c9fd 优化 2026-08-29 11:59:11 +08:00
ouyangxq 0605c32829 fix(firstleg): persist pricing conversion metadata 2026-08-29 11:48:19 +08:00
ouyangxq 66816d5341 优化 2026-08-29 11:42:52 +08:00
ouyangxq de101621dd fix(firstleg): align merchant billing with sent fees 2026-08-29 11:13:35 +08:00
ouyangxq b4db37392d fix(firstleg): complete coload SQL comments 2026-08-29 11:13:10 +08:00
ouyangxq b1ab909184 优化 2026-08-29 11:10:28 +08:00
ouyangxq 775dd71227 fix(firstleg): keep published billing visible after loading 2026-08-29 11:00:19 +08:00
ouyangxq 3339ec91f9 feat(firstleg): enforce indivisible coload loading 2026-08-29 10:59:38 +08:00
ouyangxq ac6d3289a6 feat(firstleg): add indivisible coload groups 2026-08-29 10:51:50 +08:00
ouyangxq e6ee9cb442 fix(firstleg): recover multi-customer PDA completion 2026-08-29 08:25:42 +08:00
ouyangxq da32a67d7f 优化 2026-08-28 23:01:52 +08:00
ouyangxq 969ff05815 test(firstleg): correct mixed customs completion probe 2026-08-28 22:54:23 +08:00
ouyangxq 7f565a24d6 test(firstleg): fix mixed customs permission probe 2026-08-28 22:50:55 +08:00
ouyangxq 1e389d95dd fix(firstleg): serialize concurrent customs start replay 2026-08-28 22:44:37 +08:00
ouyangxq f53ff76143 test(firstleg): add mixed container customs acceptance 2026-08-28 22:39:18 +08:00
ouyangxq f87647d4eb fix(firstleg): allow customs uploads up to 50MB 2026-08-28 22:15:24 +08:00
ouyangxq 833f928a30 fix(file): align customs download permission contract 2026-08-28 22:12:29 +08:00
ouyangxq 621aec9b2b fix(firstleg): replay completed customs uploads 2026-08-28 21:42:51 +08:00
ouyangxq f72238e43f fix(firstleg): map customs document versions explicitly 2026-08-28 21:20:43 +08:00
ouyangxq 7a15481e2d fix(firstleg): resolve merchant customs plan lookup 2026-08-28 17:28:39 +08:00
ouyangxq 2dca7dfdb2 优化 2026-08-28 17:10:12 +08:00
ouyangxq 1c51373704 fix(firstleg): register container customs mapper 2026-08-28 17:10:07 +08:00
ouyangxq 9664aa4bf0 test(firstleg): align customs contract assertions 2026-08-28 16:59:43 +08:00
ouyangxq 603bd29cc2 优化 2026-08-28 16:54:21 +08:00
ouyangxq 0afe75379e test(order): align batch draft service mocks 2026-08-28 16:53:28 +08:00
ouyangxq 0ea4aeecc9 feat(firstleg): implement container customs workflow 2026-08-28 16:48:50 +08:00
liuyihe 5ddaa4d51f 提交 2026-08-28 15:25:50 +08:00
liuyihe 8a7c92bb0f 提交 2026-08-28 15:16:36 +08:00
liuyihe 4f2b670ebf 暂扣 标记合托功能 2026-08-28 14:55:18 +08:00
liuyihe 66b9c3aa77 暂扣 标记合托功能 2026-08-28 14:20:48 +08:00
ouyangxq bb9673358d 优化 2026-08-28 13:58:49 +08:00
ouyangxq 6b5f2f0565 优化 2026-08-28 13:54:32 +08:00
ouyangxq fe68551124 优化 2026-08-28 12:33:14 +08:00
ouyangxq f998bed09c 优化 2026-08-28 11:28:30 +08:00
ouyangxq 1e5ce54858 优化 2026-08-28 10:38:58 +08:00
ouyangxq b34fbb2bd1 fix(ci): ignore stale Surefire reports 2026-08-28 10:32:41 +08:00
ouyangxq a6f266687e 优化 2026-08-28 10:03:32 +08:00
liuyihe 6a83008ba6 提交 2026-08-28 09:59:17 +08:00
ouyangxq dd117b8fc2 Merge branch 'test' of http://8.163.30.128:10082/java-project/baiyue-firstleg into test 2026-08-28 09:19:19 +08:00
ouyangxq a719a903cd 优化 2026-08-28 09:19:18 +08:00
wujinliang 3b1b4ab9b2 Merge branch 'test' of http://8.163.30.128:10082/java-project/baiyue-firstleg into test 2026-08-27 21:49:00 +08:00
wujinliang 9d54c57b7c 1 2026-08-27 21:48:47 +08:00
ouyangxq e72ac14425 fix: map warehouse pallet lookup failure 2026-08-27 19:31:38 +08:00
ouyangxq ec8f84c1c0 fix: reject missing receiving pallet 2026-08-27 19:27:59 +08:00
ouyangxq 2b7f66cb04 fix: reclaim newly added container plans 2026-08-27 19:14:54 +08:00
ouyangxq c7061b8a74 优化 2026-08-27 18:18:14 +08:00
ouyangxq 5250f304be 优化 2026-08-27 18:11:57 +08:00
ouyangxq f5f72d681d 优化 2026-08-27 18:05:02 +08:00
ouyangxq c226b6a99b 优化 2026-08-27 17:49:02 +08:00
ouyangxq 600b2a2dc4 优化 2026-08-27 17:31:19 +08:00
liuyihe 5f341d6fb5 一件代发 2026-08-27 17:23:18 +08:00
ouyangxq 76c24867a4 优化 2026-08-27 17:16:33 +08:00
liuyihe 8d39c61863 提交 2026-08-27 16:54:03 +08:00
ouyangxq 61a97cd057 优化 2026-08-27 16:42:27 +08:00
ouyangxq ecf48886d5 优化 2026-08-27 16:34:30 +08:00
ouyangxq 1db3e48506 优化 2026-08-27 16:17:11 +08:00
ouyangxq 0f2e201c38 fix(lastleg): unblock inbound plan handoff 2026-08-27 15:56:14 +08:00
ouyangxq adabec13d3 fix(print): include Spring DAO exceptions at runtime 2026-08-27 15:21:28 +08:00
ouyangxq 0fb51211aa 优化 2026-08-27 15:02:37 +08:00
ouyangxq d7bff980ca 优化 2026-08-27 14:53:27 +08:00
ouyangxq ed3c1dafcc 优化 2026-08-27 14:30:33 +08:00
ouyangxq 0a3ec0b8a6 优化 2026-08-27 14:30:23 +08:00
ouyangxq 3b38fdd33d 优化 2026-08-27 13:50:33 +08:00
liuyihe 7e0911951d Merge branch 'dev' into dev-lyhfive
# Conflicts:
#	deploy/sql/头程数据库统一增量-MySQL8.sql
#	scripts/admin-test-env-modules/phase3-container-loading.mjs
#	scripts/admin-test-env-modules/phase3-loading.mjs
2026-08-27 13:40:26 +08:00
liuyihe a79999d97c 提交 2026-08-27 13:35:52 +08:00
ouyangxq daf0ee2acd 优化 2026-08-27 11:45:52 +08:00
ouyangxq 086a0f9c89 优化 2026-08-27 11:42:05 +08:00
ouyangxq c80307a838 优化 2026-08-27 11:39:32 +08:00
ouyangxq 575cee7a86 优化 2026-08-27 11:38:53 +08:00
ouyangxq c1b136c3d6 优化 2026-08-27 11:09:51 +08:00
ouyangxq 4612181ab0 优化 2026-08-27 11:07:57 +08:00
ouyangxq b4c46280de test(acceptance): cover booking agent carrier matrix 2026-08-27 10:38:55 +08:00
ouyangxq 15b563610c fix(firstleg): validate booking carrier associations 2026-08-27 10:30:38 +08:00
ouyangxq 079ff43f73 feat(warehouse): support supplier carrier associations 2026-08-27 10:29:28 +08:00
ouyangxq 554b6c93b7 优化 2026-08-27 09:21:01 +08:00
ouyangxq 817e356c21 Merge remote-tracking branch 'origin/dev' into test 2026-08-26 21:17:25 +08:00
ouyangxq 9b6e257f8d fix(db): support clean test database initialization 2026-08-26 21:17:14 +08:00
ouyangxq ca12f58d38 chore: remove tracked runtime artifacts 2026-08-26 20:51:42 +08:00
ouyangxq 8fb63e9adc Merge remote-tracking branch 'origin/dev' into test
# Conflicts:
#	warehouse/src/main/java/com/baiyue/warehouse/dto/WarehouseRequests.java
#	warehouse/src/main/java/com/baiyue/warehouse/service/WarehouseImportService.java
#	warehouse/src/main/java/com/baiyue/warehouse/service/WarehouseSaveNormalizer.java
#	warehouse/src/main/resources/db/warehouse.sql
2026-08-26 20:51:27 +08:00
ouyangxq 0103ea96c8 test: lock overseas PDA pallet routing 2026-08-26 20:39:03 +08:00
liuyihe 3975759458 提交 2026-08-26 19:46:14 +08:00
wujinliang d736fdba1b 1 2026-08-26 18:12:55 +08:00
ouyangxq e9b03a3d35 test: allow inbound operation descriptions 2026-08-26 18:07:39 +08:00
ouyangxq 29c27d2fd7 fix: resolve FBA PDA carton by inbound prefix 2026-08-26 18:03:17 +08:00
ouyangxq cbcbb9c764 fix(ci): make AUTO deployment incremental 2026-08-26 18:01:47 +08:00
ouyangxq 64047df5b3 优化 2026-08-26 17:50:56 +08:00
ouyangxq aad8dd6344 优化 2026-08-26 17:31:12 +08:00
ouyangxq 9df87b17ac 优化 2026-08-26 17:31:09 +08:00
ouyangxq 2280154773 优化 2026-08-26 17:29:07 +08:00
ouyangxq 05e3aa4e8d 优化 2026-08-26 17:27:09 +08:00
ouyangxq ef45325643 Merge branch 'test' of http://git.buggoglobal.com/java-project/baiyue-firstleg into dev 2026-08-26 17:27:05 +08:00
ouyangxq 92d7c0a7b0 优化 2026-08-26 17:23:38 +08:00
ouyangxq 795e852944 优化 2026-08-26 17:09:02 +08:00
ouyangxq 6c4543c18c 优化 2026-08-26 16:50:22 +08:00
liuyihe 63182a2b88 Merge branch 'dev' into dev-lyhfive 2026-08-26 16:07:27 +08:00
liuyihe b04c58a76a Merge branch 'dev' into dev-lyhfive
# Conflicts:
#	deploy/sql/头程数据库统一增量-MySQL8.sql
#	deploy/sql/海外区域统一增量-MySQL8.sql
#	lastleg-service/src/main/java/com/baiyue/lastleg/service/LoadingCompletedConsumer.java
#	lastleg-service/src/main/resources/db/lastleg.sql
#	lastleg-service/src/test/java/com/baiyue/lastleg/service/LoadingCompletedConsumerTest.java
2026-08-26 16:05:24 +08:00
ouyangxq fd1a81594a 优化 2026-08-26 15:58:54 +08:00
liuyihe 5c6c0ef5ca 提交 2026-08-26 15:51:46 +08:00
ouyangxq 57a2f6ef28 fix(firstleg): enforce strict manual node progression 2026-08-26 15:44:05 +08:00
ouyangxq 2ebea9e789 fix(firstleg): allow audited carrier node time corrections 2026-08-26 15:41:03 +08:00
wujinliang f2877df697 1 2026-08-26 15:32:10 +08:00
ouyangxq 4ae236fe11 优化 2026-08-26 15:24:48 +08:00
liuyihe 93e1540e3f 提交 2026-08-26 15:02:14 +08:00
ouyangxq 3f9ee28d22 优化 2026-08-26 14:56:33 +08:00
ouyangxq 36534fe9ce fix(warehouse): align partner import carrier columns 2026-08-26 14:55:43 +08:00
ouyangxq 1f98c5ab9c 优化 2026-08-26 14:49:11 +08:00
ouyangxq bf960f10d1 优化 2026-08-26 14:48:43 +08:00
liuyihe e31fd8caa2 提交 2026-08-26 14:36:04 +08:00
ouyangxq 0f4524c795 优化 2026-08-26 14:30:55 +08:00
ouyangxq 14126dce5b 优化 2026-08-26 14:30:35 +08:00
ouyangxq dc564c9530 优化 2026-08-26 14:12:05 +08:00
ouyangxq 7f05183904 优化 2026-08-26 14:08:41 +08:00
ouyangxq 4398e8b347 优化 2026-08-26 13:58:33 +08:00
liuyihe 394c690601 提交 2026-08-26 13:51:19 +08:00
ouyangxq fc0a6ea7ca fix(firstleg): expose container documents and aggregate departure gates 2026-08-26 13:07:30 +08:00
ouyangxq 1584c7a0a3 fix(firstleg): make truck status informational for departure 2026-08-26 12:36:14 +08:00
ouyangxq 8eeb52ba77 feat(firstleg): confirm domestic departure by container 2026-08-26 12:05:42 +08:00
ouyangxq 9ea2aa46ec Merge branch 'test' of http://8.163.30.128:10082/java-project/baiyue-firstleg into dev 2026-08-26 11:11:03 +08:00
ouyangxq cf5d603a21 优化 2026-08-26 11:10:42 +08:00
ouyangxq 15421db421 fix(event-bridge): restore overseas event delivery 2026-08-26 11:06:24 +08:00
liuyihe 270372f56a 提交 2026-08-26 11:05:26 +08:00
wujinliang 897ddd476f 1 2026-08-25 22:08:44 +08:00
ouyangxq 44fee01761 test: align active booking allocation contract 2026-08-25 20:21:42 +08:00
ouyangxq 5d2b6c22e6 test: freeze server-filtered booking candidates 2026-08-25 20:17:12 +08:00
ouyangxq ead9565a37 fix: align ocean booking link eligibility 2026-08-25 20:13:48 +08:00
liuyihe 3bf68c1083 提交 2026-08-25 19:58:08 +08:00
ouyangxq 6e55d94f33 fix: add ocean booking link candidates query 2026-08-25 19:44:10 +08:00
ouyangxq 7d36759f3f 优化 2026-08-25 19:07:34 +08:00
ouyangxq 65c01f8b3d test: isolate phase4 carrier fixtures 2026-08-25 18:32:12 +08:00
ouyangxq b6226f1ca6 fix: replay completed pallet operations 2026-08-25 18:21:39 +08:00
ouyangxq ca69434698 fix: persist PDA movement state semantics 2026-08-25 18:11:29 +08:00
ouyangxq 335ad8269f fix: execute overseas pallet transitions 2026-08-25 18:07:48 +08:00
ouyangxq 7471f7e81f Merge branch 'test' of http://8.163.30.128:10082/java-project/baiyue-firstleg into dev 2026-08-25 17:55:27 +08:00
ouyangxq 62407610cb 优化 2026-08-25 17:54:50 +08:00
wujinliang 0def6114ee 1 2026-08-25 17:42:38 +08:00
ouyangxq 25cb3bfa54 test: await PDA projection and pending receipts 2026-08-25 17:25:38 +08:00
ouyangxq 4e8c15dbfa test: wait for customer dimension initialization 2026-08-25 17:17:32 +08:00
ouyangxq 3ab1a9d173 fix(lastleg): remove duplicate vehicle repository mapper 2026-08-25 17:01:52 +08:00
ouyangxq 505d448850 优化 2026-08-25 16:41:02 +08:00
ouyangxq 2dcfdd5fc8 优化 2026-08-25 16:40:54 +08:00
ouyangxq eb1a8895ac 优化 2026-08-25 16:40:50 +08:00
ouyangxq 87c5e18ca6 优化 2026-08-25 16:40:48 +08:00
ouyangxq 1f6f1a0677 test: complete phase4 PDA shipping prerequisites 2026-08-25 15:27:26 +08:00
ouyangxq fa92936406 fix(deploy): bind development gateways to loopback 2026-08-25 15:16:41 +08:00
ouyangxq 44198866d9 优化 2026-08-25 15:08:34 +08:00
ouyangxq e815abcbd7 fix(firstleg): retain dropship carton label pool 2026-08-25 15:07:06 +08:00
liuyihe 749a0d40a7 Merge branch 'dev' into dev-lyhfive
# Conflicts:
#	firstleg/src/main/java/com/baiyue/firstleg/service/LoadingCompletedOutboxService.java
#	firstleg/src/test/java/com/baiyue/firstleg/service/LoadingCompletedOutboxServiceTest.java
#	lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/controller/InboundPdaController.java
#	lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/dto/PdaResponses.java
#	lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/service/InboundPdaWorkflowService.java
#	lastleg-service/src/main/java/com/baiyue/lastleg/service/LoadingCompletedConsumer.java
#	lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/pda/service/InboundPdaWorkflowServiceTest.java
#	lastleg-service/src/test/java/com/baiyue/lastleg/service/LoadingCompletedConsumerTest.java
2026-08-25 15:06:11 +08:00
ouyangxq 39988cbaa5 fix(firstleg): publish only actually loaded items 2026-08-25 15:00:03 +08:00
ouyangxq a067a9f056 Merge branch 'test' of http://8.163.30.128:10082/java-project/baiyue-firstleg into dev
# Conflicts:
#	firstleg/src/main/java/com/baiyue/firstleg/service/LoadingPickService.java
2026-08-25 14:53:27 +08:00
ouyangxq f325d57fc1 优化 2026-08-25 14:46:06 +08:00
ouyangxq 5abc7543ae fix(firstleg): align loading completion trigger 2026-08-25 14:41:48 +08:00
ouyangxq 121e8ea619 fix(firstleg): validate settled partial pick totals 2026-08-25 14:33:38 +08:00
ouyangxq 7eca39e4fd fix(firstleg): honor terminal partial pick outcomes 2026-08-25 14:24:59 +08:00
ouyangxq f1af749616 test: target dynamic loading detail precisely 2026-08-25 14:18:13 +08:00
ouyangxq 5fb80d7624 fix(firstleg): reuse running preload lifecycle 2026-08-25 14:07:52 +08:00
ouyangxq 9093550615 test: verify complete PDA batch history 2026-08-25 13:59:58 +08:00
ouyangxq 98766e2b1a fix(firstleg): continue same-plan loading allocations 2026-08-25 13:59:03 +08:00
ouyangxq 72f3f9f971 fix(firstleg): execute carrier schedule due queries 2026-08-25 13:46:33 +08:00
ouyangxq 0302985f21 fix(firstleg): settle zero-package measurements safely 2026-08-25 13:43:22 +08:00
ouyangxq 6d8e4c85c5 test(lastleg): align rest client gateway setup 2026-08-25 13:26:05 +08:00
ouyangxq 2cfaf8e51b fix(lastleg): isolate shipping node version stream 2026-08-25 13:23:05 +08:00
ouyangxq 79eb3efe61 fix(lastleg): align overseas business incremental schema 2026-08-25 13:07:01 +08:00
ouyangxq 995c1833f8 test: align phase3 and overseas business acceptance 2026-08-25 13:06:09 +08:00
ouyangxq d686b05966 fix(firstleg): expose event bridge relay failures 2026-08-25 13:02:26 +08:00
ouyangxq 4cca194eee 优化 2026-08-25 12:30:08 +08:00
ouyangxq 1382902f86 优化 2026-08-25 12:30:07 +08:00
ouyangxq 12dad6673d fix(lastleg): register PDA operation mappers 2026-08-25 12:29:47 +08:00
ouyangxq 4b5e5a1e4e 优化 2026-08-25 12:27:47 +08:00
ouyangxq 0e0b6f8264 test: add architecture and OpenAPI validation tooling 2026-08-25 12:22:14 +08:00
ouyangxq 24df851acf fix(lastleg): register shipping mapper on startup 2026-08-25 12:18:19 +08:00
ouyangxq d9c8cc478e 优化 2026-08-25 11:54:19 +08:00
ouyangxq 322de03312 fix(ci): serialize deployments per service 2026-08-25 11:53:59 +08:00
ouyangxq 4fbf8bfb53 优化 2026-08-25 11:52:25 +08:00
ouyangxq d64706e97b fix: keep development gateways reachable 2026-08-25 11:44:38 +08:00
ouyangxq d066381ae2 fix: expose failed deployment container logs 2026-08-25 11:41:25 +08:00
ouyangxq 6ba4537d3d fix(deploy): bind development ports to loopback 2026-08-25 11:39:12 +08:00
ouyangxq 68bdc3e6bb test: cover overseas pda warehouse operation lifecycles 2026-08-25 11:25:13 +08:00
ouyangxq 042d0b738a feat: complete overseas pda warehouse operations 2026-08-25 11:20:36 +08:00
ouyangxq 45a3b9adfd test: verify pda warehouse queries after receiving 2026-08-25 10:50:58 +08:00
ouyangxq 2a16c91c89 test: sequence pickup readiness before overseas receiving 2026-08-25 10:48:10 +08:00
ouyangxq a6be840588 优化 2026-08-25 10:39:43 +08:00
ouyangxq 52b9fc382f feat: gate overseas receiving and harden phase4 workflows 2026-08-25 10:31:04 +08:00
ouyangxq cb78bc6b41 test: cover partial and concurrent pda completion 2026-08-25 09:58:59 +08:00
ouyangxq b67217332a 优化 2026-08-25 09:52:49 +08:00
ouyangxq b211121365 test: expose acceptance coverage metrics 2026-08-25 09:50:02 +08:00
ouyangxq 46dd0b54eb test: isolate pda receipt idempotency keys 2026-08-25 09:27:45 +08:00
ouyangxq df4b7a1f6e test: carry frozen sku receipts into pda acceptance 2026-08-25 09:23:28 +08:00
ouyangxq 29b2f080d8 test: preserve dropship producer fixture type 2026-08-25 09:16:00 +08:00
ouyangxq 4839243f21 feat: add dropship fulfillment producer chain 2026-08-25 08:40:27 +08:00
ouyangxq c218bfad6b fix: expose warehouse transfer application status 2026-08-25 08:21:47 +08:00
ouyangxq 85a288e4f2 fix: preserve overseas pallet identity 2026-08-25 08:12:58 +08:00
ouyangxq 73d206e3f3 test: complete phase3 phase4 acceptance coverage 2026-08-25 00:24:43 +08:00
ouyangxq 84ad5e779a test: make price-book acceptance portable 2026-08-25 00:08:59 +08:00
ouyangxq 24149c062a fix: migrate event bridge customer columns 2026-08-25 00:02:45 +08:00
ouyangxq e94f8629d2 fix: retain event bridge rejection evidence 2026-08-24 23:56:52 +08:00
ouyangxq 1c40548986 fix: isolate domestic bridge target variable 2026-08-24 23:52:38 +08:00
ouyangxq 55a9d1af09 fix: prioritize domestic bridge endpoint 2026-08-24 23:49:07 +08:00
ouyangxq a22a284e7d fix: route unload events to domestic bridge 2026-08-24 23:43:08 +08:00
ouyangxq 058bf9f4e6 test: complete phase4 cross-region handoff 2026-08-24 23:35:26 +08:00
ouyangxq 615ecb4ba1 test: complete overseas carton and sailing fixtures 2026-08-24 23:28:24 +08:00
ouyangxq 985d952e54 test: provision canonical carrier fixture 2026-08-24 23:19:27 +08:00
ouyangxq bf4fd95841 fix: align phase4 receiving and carrier fixtures 2026-08-24 23:12:04 +08:00
ouyangxq 1e8d85c714 fix: support id-only overseas destinations 2026-08-24 22:51:33 +08:00
ouyangxq 0e73a79479 fix: serialize phase4 inventory and outbound gates 2026-08-24 22:32:37 +08:00
ouyangxq 3fbb7653f7 fix: complete phase4 receiving acceptance chain 2026-08-24 22:14:41 +08:00
ouyangxq 8cc4c6f6da fix: bind overseas receiving to shared warehouse 2026-08-24 21:46:19 +08:00
ouyangxq 3607bebbfd fix: close phase4 overseas acceptance gaps 2026-08-24 21:00:22 +08:00
ouyangxq 015fc09091 补充PDA装柜完成阶段诊断 2026-08-24 20:47:13 +08:00
ouyangxq 005c3f54e2 test: align full acceptance contracts 2026-08-24 20:14:40 +08:00
ouyangxq 5edc2fc1bd fix(lastleg): restore overseas receiving build 2026-08-24 20:14:39 +08:00
ouyangxq c93fdc44d5 1 2026-08-24 19:58:35 +08:00
ouyangxq 9e0f41e78b test: strengthen phase4 overseas business acceptance 2026-08-24 19:45:22 +08:00
ouyangxq a811c2ca06 test: harden carrier node HTTP acceptance 2026-08-24 19:44:28 +08:00
ouyangxq ac806704be chore(test): keep master-data changes out of event gate 2026-08-24 19:42:20 +08:00
ouyangxq bffe7d3b23 test: strengthen overseas warehouse PDA contracts 2026-08-24 19:41:16 +08:00
ouyangxq 1e4b216afa test(events): add cross-region HTTP acceptance gates 2026-08-24 19:40:46 +08:00
ouyangxq 2f986cf5e3 优化 2026-08-24 19:35:01 +08:00
ouyangxq 89bd746dbd 优化 2026-08-24 19:29:39 +08:00
ouyangxq 5c54785d77 优化 2026-08-24 19:28:32 +08:00
ouyangxq aa3e72af28 Merge origin/test into dev 2026-08-24 19:27:54 +08:00
ouyangxq 551560efda 优化 2026-08-24 19:23:20 +08:00
ouyangxq 7e080622f3 fix(ci): isolate event bridge region parameter 2026-08-24 19:15:15 +08:00
ouyangxq 360115de35 修复确认预装后重新分配失败 2026-08-24 18:18:08 +08:00
ouyangxq 111500d010 test(lastleg): align receiving completion fixtures 2026-08-24 18:17:44 +08:00
ouyangxq 4f2abf2894 test(lastleg): type receiving mapper inserts 2026-08-24 18:10:31 +08:00
liuyihe 5c331f173d 提交 2026-08-24 17:58:47 +08:00
ouyangxq 52e4b914b1 fix(lastleg): complete receiving by container 2026-08-24 17:56:43 +08:00
ouyangxq f4a1ec92de 优化 2026-08-24 17:51:11 +08:00
ouyangxq 4893542ec4 1 2026-08-24 17:48:08 +08:00
ouyangxq 633cec7c68 fix(lastleg): disambiguate consumer test fixtures 2026-08-24 17:43:03 +08:00
ouyangxq a320198b47 fix(ci): keep Jenkins boolean continuations parseable 2026-08-24 17:37:36 +08:00
ouyangxq bb74a05562 feat(lastleg): harden container receiving contract 2026-08-24 17:37:15 +08:00
ouyangxq be1eb4f45c fix(ci): carry dual bridge intent through stable parameters 2026-08-24 17:34:11 +08:00
ouyangxq 9d09b77442 fix(ci): bridge BOTH through legacy downstream choices 2026-08-24 17:23:05 +08:00
ouyangxq ce82916036 fix(lastleg): align inbound snapshot with schema 2026-08-24 17:22:18 +08:00
ouyangxq 5a4bcf130a feat(ci): deploy both event bridge regions by default 2026-08-24 17:19:36 +08:00
ouyangxq 3d8a1fac20 优化 2026-08-24 17:15:48 +08:00
liuyihe 01675b8220 提交 2026-08-24 17:12:30 +08:00
ouyangxq 132d96a99b 修复海外报价合作仓候选缓存刷新 2026-08-24 17:10:39 +08:00
ouyangxq 8708904dab 修复装柜清单托盘数快照 2026-08-24 17:03:29 +08:00
ouyangxq 3019f5da77 fix(event-bridge): retain downstream business errors 2026-08-24 16:59:02 +08:00
ouyangxq 8e12dd9a38 fix(test): select container-backed PDA fixtures 2026-08-24 16:40:25 +08:00
ouyangxq 6aca41427c feat(phase4): automate overseas PDA inbound lifecycle 2026-08-24 15:38:28 +08:00
ouyangxq ef9f253f47 feat(lastleg): expose inbound PDA work instructions 2026-08-24 15:25:34 +08:00
ouyangxq 95b72afdcc fix(phase4): enforce customer and pallet versions 2026-08-24 15:14:18 +08:00
ouyangxq e773816c98 fix(db): upgrade overseas pallet FBA field 2026-08-24 15:10:10 +08:00
ouyangxq e11a528dfe test(phase4): provision overseas lifecycle fixtures 2026-08-24 15:07:17 +08:00
liuyihe ce9f08c756 提交 2026-08-24 14:52:50 +08:00
ouyangxq cd97229791 Merge branch 'test' of http://8.163.30.128:10082/java-project/baiyue-firstleg into dev 2026-08-24 14:47:54 +08:00
ouyangxq c88d99a593 修复订单列表托数汇总 2026-08-24 14:42:38 +08:00
ouyangxq a8326455fc fix(firstleg): allocate handover carton marks per inbound 2026-08-24 14:40:51 +08:00
ouyangxq 2f32f1d827 test(phase3): finalize loading before split acceptance 2026-08-24 14:34:01 +08:00
ouyangxq e781cbe9cb fix(firstleg): normalize split carton label pools 2026-08-24 14:20:59 +08:00
ouyangxq 8e61cfd202 fix(firstleg): transfer carton labels to remainder inbound 2026-08-24 14:08:17 +08:00
ouyangxq 506cc69808 test(phase3): provision dispatch and handoff evidence 2026-08-24 14:01:02 +08:00
ouyangxq f041cf524b test(phase3): complete remaining hard-gate fixtures 2026-08-24 13:57:02 +08:00
ouyangxq 23335ee8d0 test(phase3): satisfy finalization fixture gates 2026-08-24 13:53:10 +08:00
ouyangxq 98ec62f66e test(phase3): align full acceptance fixtures 2026-08-24 13:48:16 +08:00
ouyangxq b012d64b54 perf(ci): run selected tests once before service builds 2026-08-24 13:43:38 +08:00
ouyangxq 9dd2fcb7e5 test(phase3): validate multi-owner loaded slices 2026-08-24 13:41:59 +08:00
liuyihe 6a86dbcb37 Merge remote-tracking branch 'origin/dev-lyhfive' into dev-lyhfive 2026-08-24 13:41:54 +08:00
ouyangxq 6ddf0d823b fix(firstleg): share container evidence across loading owners 2026-08-24 13:37:47 +08:00
ouyangxqandliuyihe b72dd54b80 fix container customer snapshot schema migration 2026-08-24 13:36:38 +08:00
ouyangxq 92dbeb1676 fix(firstleg): finalize loading before customs acceptance 2026-08-24 12:52:23 +08:00
ouyangxq 4377a7acd9 fix(firstleg): allow confirmed preload reassign during loading 2026-08-24 12:34:34 +08:00
ouyangxq 2389a63858 fix IAM security event correlation id 2026-08-24 12:30:18 +08:00
ouyangxq ae3026dfdb Merge branch 'test' of http://8.163.30.128:10082/java-project/baiyue-firstleg into dev 2026-08-24 12:01:44 +08:00
ouyangxq f1639b9fe3 优化 2026-08-24 12:01:37 +08:00
ouyangxq b3695276b3 Merge branch 'test' of http://8.163.30.128:10082/java-project/baiyue-firstleg into dev
# Conflicts:
#	firstleg/src/test/java/com/baiyue/firstleg/repository/SmartLoadingSqlContractTest.java
2026-08-24 11:58:22 +08:00
ouyangxq 739c20cb3d fix lastleg order detail customer schema 2026-08-24 11:55:50 +08:00
ouyangxq d59fccc731 优化 2026-08-24 11:52:32 +08:00
ouyangxq 4977b09af9 fix phase3 loading acceptance sequence 2026-08-24 11:49:31 +08:00
ouyangxq 257c019b36 fix phase3 acceptance roots and payable validation 2026-08-24 11:48:16 +08:00
ouyangxq 044678f65c fix carrier history action schema upgrade 2026-08-24 11:36:25 +08:00
ouyangxq 287591c75f fix container customer snapshot schema migration 2026-08-24 11:19:33 +08:00
ouyangxq fe4eb0b593 perf(ci): allow two concurrent non-production deploys 2026-08-24 11:17:17 +08:00
ouyangxq 9198ce02d9 fix(ci): isolate Docker registry credentials 2026-08-24 11:00:03 +08:00
ouyangxq a4a693837e perf(ci): acquire deploy lock before target agent 2026-08-24 10:56:33 +08:00
ouyangxq c8574326dd fix smart loading candidate result mapping 2026-08-24 10:51:49 +08:00
ouyangxq 938c5771b3 Merge branch 'test' of http://8.163.30.128:10082/java-project/baiyue-firstleg into dev
# Conflicts:
#	Jenkinsfile
#	Jenkinsfile.service
#	deploy/scripts/deploy-built-service.sh
#	scripts/test-environment-pipeline-contract.test.mjs
2026-08-24 10:49:08 +08:00
ouyangxq 0fbba13714 优化 2026-08-24 10:35:40 +08:00
ouyangxq ccfd024aef fix phase3 phase4 development acceptance blockers 2026-08-24 10:33:05 +08:00
ouyangxq 81899015cf 优化 2026-08-24 09:29:26 +08:00
ouyangxq ae9238b3cf feat(ci): rollback unhealthy deployments 2026-08-22 18:26:47 +08:00
ouyangxq 657f3553d8 fix(ci): retain service source workspaces 2026-08-22 18:01:20 +08:00
ouyangxq abab08e879 perf(ci): reuse Git workspaces across builds 2026-08-22 18:01:15 +08:00
ouyangxq 52b673b878 perf(ci): release coordinator builder executor 2026-08-22 18:01:10 +08:00
ouyangxq d3e16bf29f fix(ci): deploy on target environment agents 2026-08-22 18:01:10 +08:00
ouyangxq 1df87e5d17 fix(ci): retain service source workspaces 2026-08-22 17:55:50 +08:00
ouyangxq 46e5c14b86 perf(ci): reuse Git workspaces across builds 2026-08-22 17:53:18 +08:00
ouyangxq 309900de82 perf(ci): release coordinator builder executor 2026-08-22 17:46:11 +08:00
ouyangxq 232c2657f1 fix(ci): deploy on target environment agents 2026-08-22 17:43:20 +08:00
liuyihe e398f1f7e0 修改提交 2026-08-22 12:35:32 +08:00
ouyangxq e92cfe18ad ci: use internal Java base image 2026-08-22 12:22:04 +08:00
ouyangxq 996102e77d ci: use internal Java base image 2026-08-22 12:20:26 +08:00
ouyangxq 668668b505 fix(ci): schedule coordinator on builder agents 2026-08-22 12:07:35 +08:00
ouyangxq fd1c582d45 ci: serialize registry-based environment deployments 2026-08-22 12:07:35 +08:00
ouyangxq 0f296e016e ci: use dedicated registry credentials 2026-08-22 12:07:35 +08:00
ouyangxq a7ec6ed285 ci: publish development and test images to registry 2026-08-22 12:07:35 +08:00
ouyangxq 0d3a27f4ba build(docker): layer Spring Boot service images 2026-08-22 12:07:35 +08:00
ouyangxq dc131f26af Merge branch 'test' of http://8.163.30.128:10082/java-project/baiyue-firstleg into dev 2026-08-22 11:47:12 +08:00
ouyangxq 775b2c4863 优化 2026-08-22 11:46:41 +08:00
ouyangxq cee3ee07a8 fix(ci): schedule coordinator on builder agents 2026-08-22 11:45:14 +08:00
ouyangxq 5a78e9ca54 ci: default development coordinator to dev jobs 2026-08-22 11:14:28 +08:00
ouyangxq aa80ff4b2e fix(ci): use compatible Jenkins job copy API 2026-08-22 11:13:09 +08:00
ouyangxq 7aebc3c0cb ci: add safe dev job provisioning script 2026-08-22 11:08:07 +08:00
ouyangxq ba8a6b7832 ci: serialize registry-based environment deployments 2026-08-22 11:01:06 +08:00
ouyangxq ff9c1e2bc1 ci: use dedicated registry credentials 2026-08-22 10:50:33 +08:00
ouyangxq 3b3d672fb1 ci: publish development and test images to registry 2026-08-22 10:38:40 +08:00
ouyangxq b25087e60b build(docker): layer Spring Boot service images 2026-08-22 10:27:28 +08:00
ouyangxq 85173b608d Merge branch 'dev' of http://8.163.30.128:10082/java-project/baiyue-firstleg into dev 2026-08-22 10:20:39 +08:00
ouyangxq 1f2c4cd8bb 优化 2026-08-22 10:20:38 +08:00
liuyihe 93feb7da67 修改提交 2026-08-22 10:16:45 +08:00
ouyangxq 76c3662dda 优化 2026-08-22 10:02:10 +08:00
ouyangxq af8724dd70 优化 2026-08-22 09:56:26 +08:00
liuyihe ab2f3ed72e Merge remote-tracking branch 'origin/dev' into dev 2026-08-22 09:52:04 +08:00
liuyihe 15636a1600 修改提交 2026-08-22 09:51:41 +08:00
ouyangxq ff926d5b74 Merge branch 'dev' of http://8.163.30.128:10082/java-project/baiyue-firstleg into dev 2026-08-22 09:51:03 +08:00
ouyangxq 988d1bee7c 1 2026-08-22 09:51:01 +08:00
liuyihe 76b41c0812 Merge remote-tracking branch 'origin/dev' into dev 2026-08-21 18:12:25 +08:00
liuyihe 4f14367eba 修改提交 2026-08-21 18:12:07 +08:00
ouyangxq 1a67b3404d Merge branch 'test' of http://8.163.30.128:10082/java-project/baiyue-firstleg into dev 2026-08-21 17:59:21 +08:00
ouyangxq 72b050af6d Merge branch 'dev' of http://8.163.30.128:10082/java-project/baiyue-firstleg into dev 2026-08-21 17:58:55 +08:00
ouyangxq 24e4a402a9 优化 2026-08-21 17:58:52 +08:00
liuyihe d1d269e96f 修改提交 2026-08-21 17:38:14 +08:00
liuyihe bbd303cd65 修改提交 2026-08-21 17:21:26 +08:00
ouyangxq af97217184 优化 2026-08-21 17:19:49 +08:00
ouyangxq 598749d8e4 优化 2026-08-21 17:07:34 +08:00
ouyangxq 8c280e19c4 优化 2026-08-21 17:05:52 +08:00
ouyangxq cd477fe40a 优化 2026-08-21 16:15:02 +08:00
ouyangxq 8e07342fa1 Merge branch 'dev' of http://8.163.30.128:10082/java-project/baiyue-firstleg into dev 2026-08-21 16:04:19 +08:00
ouyangxq 0e7df9052a 优化 2026-08-21 16:04:16 +08:00
liuyihe e534784052 提交 2026-08-21 14:42:40 +08:00
liuyihe 5fd00d3151 提交 2026-08-21 11:43:03 +08:00
ouyangxq dceec6cbed 优化 2026-08-21 10:56:51 +08:00
liuyihe e17f03e596 提交 2026-08-21 10:43:25 +08:00
ouyangxq d5e9ba6d4c 优化 2026-08-21 10:38:28 +08:00
ouyangxq 361e6be3be 优化 2026-08-21 09:31:29 +08:00
ouyangxq 96feb8faff 优化 2026-08-20 20:54:30 +08:00
ouyangxq acc5bec3b1 优化 2026-08-20 20:38:46 +08:00
ouyangxq 5400a8e8e7 优化 2026-08-20 20:29:51 +08:00
ouyangxq 11cfba880d 优化 2026-08-20 20:14:21 +08:00
ouyangxq 893d1801b5 优化 2026-08-20 19:55:21 +08:00
ouyangxq d170505327 Merge branch 'test' of http://8.163.30.128:10082/java-project/baiyue-firstleg into dev 2026-08-20 19:11:43 +08:00
ouyangxq 34fb91baf6 优化 2026-08-20 19:10:16 +08:00
ouyangxq 8095a99afa Merge branch 'test' of http://8.163.30.128:10082/java-project/baiyue-firstleg into dev
# Conflicts:
#	scripts/admin-test-env-modules/orders.mjs
#	scripts/admin-test-env-modules/phase3-container-loading.mjs
2026-08-20 18:24:23 +08:00
ouyangxq b0dcbf77e5 Merge branch 'dev' of http://8.163.30.128:10082/java-project/baiyue-firstleg into dev 2026-08-20 18:22:20 +08:00
ouyangxq 4256e74c11 优化 2026-08-20 18:21:04 +08:00
ouyangxq 12604afa36 优化 2026-08-20 18:17:32 +08:00
ouyangxq 382ffc7f55 Merge branch 'dev' of http://8.163.30.128:10082/java-project/baiyue-firstleg into dev
# Conflicts:
#	config/nacos/cn/event-bridge-service.yml
#	config/nacos/overseas/lastleg-service.yml
#	deploy/sql/头程数据库统一增量-MySQL8.sql
#	event-bridge-service/src/main/resources/application.yml
#	iam/src/main/resources/db/lastleg-permissions.sql
#	lastleg-service/AGENTS.md
#	lastleg-service/src/main/java/com/baiyue/lastleg/BaiyueLastlegApplication.java
#	lastleg-service/src/main/java/com/baiyue/lastleg/controller/LastlegOrderController.java
#	lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/controller/InboundPlanController.java
#	lastleg-service/src/main/java/com/baiyue/lastleg/inventory/command/controller/InventoryCommandController.java
#	lastleg-service/src/main/java/com/baiyue/lastleg/inventory/query/controller/InventoryQueryController.java
#	lastleg-service/src/main/java/com/baiyue/lastleg/procurement/controller/PlatformOverseasProcurementController.java
#	lastleg-service/src/main/java/com/baiyue/lastleg/procurement/service/OverseasProcurementService.java
#	lastleg-service/src/main/java/com/baiyue/lastleg/relabel/controller/RelabelApplicationController.java
#	lastleg-service/src/main/java/com/baiyue/lastleg/trucking/controller/PlatformUsTruckOrderController.java
#	lastleg-service/src/main/java/com/baiyue/lastleg/trucking/service/UsTruckOrderService.java
#	lastleg-service/src/main/resources/application.yml
#	lastleg-service/src/main/resources/db/lastleg.sql
#	scripts/db/incremental.sql
#	warehouse-overseas-service/src/main/java/com/baiyue/warehouse/controller/PlatformWarehouseController.java
#	warehouse-overseas-service/src/main/java/com/baiyue/warehouse/controller/PlatformWarehouseResourceController.java
2026-08-20 18:14:55 +08:00
liuyihe a38396ce3f 提交 2026-08-20 18:06:07 +08:00
ouyangxq d202ca85ef 优化 2026-08-20 18:05:24 +08:00
ouyangxq 1cd8b6a473 优化 2026-08-20 18:02:38 +08:00
ouyangxq b1655a1841 优化 2026-08-20 18:02:20 +08:00
ouyangxq 1e8577dc4e 低碳经济 2026-08-20 17:07:51 +08:00
ouyangxq 5b9ae25f29 优化 2026-08-20 17:00:05 +08:00
liuyihe c9a05b108b 桥接事件 2026-08-20 16:45:47 +08:00
ouyangxq 09f6e28c1f 优化 2026-08-20 16:13:51 +08:00
liuyihe 1f0c0f7751 提交 2026-08-20 15:59:45 +08:00
liuyihe 2847ad1b2d 提交 2026-08-20 15:58:25 +08:00
liuyihe cd34cd426e 提交 2026-08-20 15:25:40 +08:00
liuyihe b55b049e77 优化 2026-08-20 15:20:05 +08:00
ouyangxq 6c7a670f1e 优化 2026-08-20 14:43:07 +08:00
ouyangxq 50d9803566 Merge branch 'test' of http://8.163.30.128:10082/java-project/baiyue-firstleg into dev 2026-08-20 13:49:08 +08:00
ouyangxq 164d862588 优化 2026-08-20 13:42:55 +08:00
liuyihe 802893b2ff Merge remote-tracking branch 'origin/dev' into dev 2026-08-20 13:34:55 +08:00
liuyihe b139255d3b 入库修改 2026-08-20 13:34:34 +08:00
ouyangxq d3183d8f2c 优化 2026-08-20 13:34:29 +08:00
ouyangxq 3e517ee74e Merge branch 'test' of http://8.163.30.128:10082/java-project/baiyue-firstleg into dev 2026-08-20 13:34:04 +08:00
ouyangxq d340c418e8 优化 2026-08-20 13:07:40 +08:00
ouyangxq 3b19a1bc16 Merge branch 'test' into dev 2026-08-20 13:00:49 +08:00
ouyangxq 15d3ffbc0a 优化 2026-08-20 13:00:39 +08:00
ouyangxq b060be18a9 Merge branch 'test' into dev 2026-08-20 12:50:05 +08:00
ouyangxq b029f4ce28 优化 2026-08-20 12:49:54 +08:00
ouyangxq fd1da73fa8 Merge branch 'test' into dev 2026-08-20 12:34:49 +08:00
ouyangxq c83620a86a 优化 2026-08-20 12:34:10 +08:00
ouyangxq b464de1edb Merge branch 'test' into dev 2026-08-20 10:39:13 +08:00
ouyangxq 3d57abf260 优化 2026-08-20 10:37:32 +08:00
ouyangxq 0384f54da7 Merge branch 'test' into dev 2026-08-20 10:28:57 +08:00
ouyangxq bdf545ee0a 优化 2026-08-20 10:28:45 +08:00
ouyangxq 0575ea247f Merge branch 'test' into dev 2026-08-20 10:23:00 +08:00
ouyangxq 7a5719b5a1 优化 2026-08-20 10:22:51 +08:00
ouyangxq d36e480413 优化 2026-08-20 10:17:39 +08:00
ouyangxq e469749a98 Merge branch 'test' into dev 2026-08-20 09:50:48 +08:00
ouyangxq be5d89a4e6 优化 2026-08-20 09:49:28 +08:00
ouyangxq 7cf4a8d562 优化 2026-08-20 09:43:31 +08:00
ouyangxq cf2356bccb Merge branch 'dev' of http://8.163.30.128:10082/java-project/baiyue-firstleg into dev 2026-08-20 09:10:32 +08:00
ouyangxq f227cf05a3 优化 2026-08-20 09:10:30 +08:00
liuyihe f03ea227ed 暂存提交 2026-08-19 17:23:54 +08:00
liuyihe 26eba227eb 暂存提交 2026-08-19 17:22:03 +08:00
ouyangxq 064247a8c9 Merge branch 'dev' of http://8.163.30.128:10082/java-project/baiyue-firstleg into dev 2026-08-19 17:08:42 +08:00
ouyangxq f78b44297d 优化 2026-08-19 17:08:41 +08:00
liuyihe 88100f3536 Merge remote-tracking branch 'origin/dev' into dev 2026-08-19 16:49:58 +08:00
liuyihe 63618c20dc 暂存提交 2026-08-19 16:49:41 +08:00
ouyangxq a4ec4e8e99 Merge branch 'dev' of http://8.163.30.128:10082/java-project/baiyue-firstleg into dev 2026-08-19 16:45:45 +08:00
ouyangxq 3b8934fc60 优化 2026-08-19 16:45:44 +08:00
liuyihe 6acb6178da 暂存提交 2026-08-19 16:24:02 +08:00
ouyangxq f5c4bf130a Merge branch 'test' of http://8.163.30.128:10082/java-project/baiyue-firstleg into dev 2026-08-19 14:14:04 +08:00
ouyangxq f25e270f22 优化 2026-08-19 14:13:47 +08:00
ouyangxq 000fd10b6a fix(sql): complete IAM security event comments 2026-08-19 14:04:16 +08:00
ouyangxq 23e78bb33f Merge branch 'test' of http://8.163.30.128:10082/java-project/baiyue-firstleg into dev 2026-08-19 14:02:11 +08:00
ouyangxq 41d70841fc 优化 2026-08-19 09:58:43 +08:00
ouyangxq 008d7ba574 优化 2026-08-19 09:57:50 +08:00
ouyangxq b908d9f248 优化 2026-08-19 09:56:17 +08:00
ouyangxq 80527fa15f 优化 2026-08-19 09:55:29 +08:00
ouyangxq a381d443da 优化 2026-08-19 09:44:18 +08:00
ouyangxq b5864bfcda 优化 2026-08-19 09:42:07 +08:00
ouyangxq ff1d330c2d 优化 2026-08-19 09:14:30 +08:00
ouyangxq 8845a66749 优化 2026-08-18 19:04:22 +08:00
ouyangxq 688e5649b0 优化 2026-08-18 17:40:32 +08:00
ouyangxq d70c08a378 优化 2026-08-18 16:42:01 +08:00
ouyangxq a9061eca54 优化 2026-08-18 16:39:39 +08:00
ouyangxq 7fe5b4e31f 优化 2026-08-18 16:10:44 +08:00
ouyangxq fa77170304 优化 2026-08-18 16:09:52 +08:00
ouyangxq 82ee80226e 优化 2026-08-18 16:03:42 +08:00
ouyangxq 12e9f3746e 优化 2026-08-18 15:49:52 +08:00
ouyangxq 6d5ec5759b 优化 2026-08-18 15:49:37 +08:00
ouyangxq 9db371ab0e 伦 2026-08-18 15:47:44 +08:00
ouyangxq 81e6610eb8 傣给 2026-08-18 15:46:56 +08:00
ouyangxq b26e64bb8d 优化 2026-08-18 15:46:52 +08:00
ouyangxq 4b0b3e9172 优化 2026-08-18 15:43:58 +08:00
ouyangxq 4e4d047862 优化 2026-08-18 11:46:10 +08:00
ouyangxq cad9a81d21 优化 2026-08-18 11:44:39 +08:00
ouyangxq ec4b1e0fe5 优化 2026-08-18 11:41:10 +08:00
ouyangxq 3e1010fa26 优化 2026-08-18 11:32:55 +08:00
ouyangxq ea26eecc50 优化 2026-08-18 09:09:28 +08:00
ouyangxq 092c003e3a 优化 2026-08-17 20:21:44 +08:00
ouyangxq b8c7f9fea2 优化 2026-08-17 20:01:48 +08:00
ouyangxq 45b51415cf 优化 2026-08-17 19:51:54 +08:00
ouyangxq cddd23e4c7 优化 2026-08-17 19:13:08 +08:00
ouyangxq 5199146e97 优化 2026-08-17 18:32:38 +08:00
ouyangxq 5c22018edd 优化 2026-08-17 18:04:04 +08:00
ouyangxq fd4afd6ae6 优化 2026-08-17 18:00:38 +08:00
ouyangxq 7c66097672 优化 2026-08-17 17:49:03 +08:00
ouyangxq c71db4b481 优化 2026-08-17 16:45:43 +08:00
ouyangxq 592da4d7e2 优化 2026-08-17 16:17:20 +08:00
ouyangxq 75580c3585 优化 2026-08-17 16:07:20 +08:00
ouyangxq 68d9216cb8 优化 2026-08-17 15:55:11 +08:00
ouyangxq 5f79616623 优化 2026-08-17 15:41:42 +08:00
ouyangxq 4abcb2b80f 优化 2026-08-17 14:50:00 +08:00
ouyangxq a34b5d5ba0 优化 2026-08-17 14:46:44 +08:00
ouyangxq b914bb7532 优化 2026-08-17 13:47:20 +08:00
ouyangxq 6739c83467 优化 2026-08-17 13:24:48 +08:00
ouyangxq ac6cf9c904 优化 2026-08-17 12:31:24 +08:00
ouyangxq 8566c6ea9e 优化 2026-08-17 11:23:35 +08:00
ouyangxq 6b416621bb 优化 2026-08-17 11:23:28 +08:00
ouyangxq d51e054def 优化 2026-08-17 10:54:08 +08:00
ouyangxq 7a9c72a6a2 优化 2026-08-17 09:41:08 +08:00
ouyangxq 20d836ab3d test: preserve fee item identifiers 2026-08-15 18:07:23 +08:00
ouyangxq f2a49d032a test: map finance supplier category from fee type 2026-08-15 18:03:47 +08:00
ouyangxq 2cec065582 test: provision finance fixture prerequisites 2026-08-15 17:58:15 +08:00
ouyangxq 0e2a291e7a test: use active suppliers for finance fixtures 2026-08-15 17:52:46 +08:00
ouyangxq 5e1e49085d test: normalize finance container allocation sources 2026-08-15 17:45:51 +08:00
ouyangxq 28daf15082 test: confirm plans before PDA acceptance 2026-08-15 17:40:01 +08:00
ouyangxq 05921eca13 test: stabilize preload push replay assertions 2026-08-15 17:32:16 +08:00
ouyangxq 8350bcdf00 fix: replay preload confirmation idempotently 2026-08-15 17:27:26 +08:00
ouyangxq a35794e918 feat: close preload confirmation and loading flow 2026-08-15 17:04:00 +08:00
ouyangxq 5b8e7d6989 fix: harden smart loading push idempotency 2026-08-15 12:39:30 +08:00
ouyangxq 0ddffeddc8 优化 2026-08-15 12:33:23 +08:00
ouyangxq 3a9558bd1c 优化 2026-08-15 12:12:34 +08:00
ouyangxq 08187363a1 优化 2026-08-15 11:51:15 +08:00
ouyangxq 46cb95db15 优化 2026-08-15 11:36:53 +08:00
ouyangxq 282eb73371 优化 2026-08-15 11:20:45 +08:00
ouyangxq 0f915d7b24 优化 2026-08-15 11:19:10 +08:00
ouyangxq c544b1bc39 优化 2026-08-15 11:19:08 +08:00
ouyangxq 792965c822 优化 2026-08-15 11:14:57 +08:00
ouyangxq dfa3e8cc53 优化 2026-08-15 11:14:50 +08:00
ouyangxq fdea2a6f31 优化 2026-08-15 11:14:47 +08:00
ouyangxq c4adba57db 优化 2026-08-15 11:12:19 +08:00
ouyangxq 557ab75319 优化 2026-08-15 11:05:58 +08:00
ouyangxq c4282020b5 优化 2026-08-15 10:35:54 +08:00
ouyangxq 555acca779 优化 2026-08-15 10:32:39 +08:00
ouyangxq 9e7cc55eab 优化 2026-08-15 10:27:15 +08:00
ouyangxq c6d7fb0d84 优化 2026-08-15 10:09:04 +08:00
ouyangxq 233f8f312d 优化 2026-08-15 10:05:03 +08:00
ouyangxq bafabf7217 优化 2026-08-15 09:57:46 +08:00
ouyangxq e7c0788a0b 优化 2026-08-15 09:49:13 +08:00
ouyangxq c07af65c3b 优化 2026-08-15 09:49:09 +08:00
ouyangxq 2bf3ff9d8e 优化 2026-08-15 09:49:00 +08:00
ouyangxq 20907e4685 优化 2026-08-15 09:47:17 +08:00
ouyangxq 1982a7dd69 test: align ocean booking fixtures with direct confirmed flow 2026-08-15 09:35:52 +08:00
ouyangxq e4d5e6d631 test: release split bookings before pda 2026-08-14 23:21:47 +08:00
ouyangxq 5bb8f4db6d test: align manual booking list dependencies 2026-08-14 23:14:11 +08:00
ouyangxq 32e9b69556 test(finance): complete automatic fee fixture workflow 2026-08-14 23:03:36 +08:00
ouyangxq 1db7c3b3cf test: align customer archive acceptance status 2026-08-14 22:59:57 +08:00
ouyangxq b89f14217d test: bind real containers before pda push 2026-08-14 22:59:10 +08:00
ouyangxq 0c845874f4 test: distinguish active and void overview photos 2026-08-14 22:43:07 +08:00
ouyangxq 011c30cc09 test: align container flow acceptance identities 2026-08-14 22:30:28 +08:00
ouyangxq 8aa58a4f72 fix(firstleg): hide void photo download paths 2026-08-14 22:29:50 +08:00
ouyangxq b2a093b31b test: release bookings before opening PDA 2026-08-14 22:21:24 +08:00
ouyangxq 010098c4af fix(firstleg): require real container before pda push 2026-08-14 22:03:21 +08:00
ouyangxq fdd011bc57 test: correct container loading acceptance order 2026-08-14 21:45:54 +08:00
ouyangxq dbca4a1403 test: stop container acceptance after failed booking link 2026-08-14 21:37:02 +08:00
ouyangxq 9bb046063c fix(firstleg): execute container customer scope query 2026-08-14 21:31:46 +08:00
ouyangxq 7cb5da753e fix(firstleg): keep truck links on container ownership 2026-08-14 21:14:42 +08:00
ouyangxq a4a4374c67 test(firstleg): cover simulated container booking links 2026-08-14 21:00:20 +08:00
ouyangxq 7184f5083a feat(firstleg): link simulated containers to real bookings 2026-08-14 20:59:03 +08:00
ouyangxq 993b3dfe03 优化 2026-08-14 19:59:26 +08:00
ouyangxq 7d4256dbd2 优化 2026-08-14 19:09:51 +08:00
ouyangxq 10d3390bb1 优化 2026-08-14 18:50:07 +08:00
ouyangxq 9b36978448 修复开发部署 SSH 指纹读取 2026-08-14 17:54:53 +08:00
ouyangxq e33b2c8645 优化 2026-08-14 17:53:09 +08:00
ouyangxq 2752cfb5b2 支持开发环境远程部署 2026-08-14 17:45:43 +08:00
ouyangxq feced8eb92 优化 2026-08-14 17:37:59 +08:00
ouyangxq 87e919f3c4 优化 2026-08-14 16:06:27 +08:00
ouyangxq 87eb1aa72f 优化 2026-08-14 14:54:48 +08:00
ouyangxq 36a048a2bc 优化 2026-08-14 14:50:39 +08:00
ouyangxq 261a8bf2df 优化 2026-08-14 14:26:56 +08:00
ouyangxq 06c264db0a 优化 2026-08-14 13:03:09 +08:00
ouyangxq ce6963a25e 优化 2026-08-14 12:50:37 +08:00
ouyangxq 91e781b3ff 优化 2026-08-14 10:57:05 +08:00
ouyangxq 66089f9b99 优化 2026-08-14 10:09:46 +08:00
ouyangxq 0bc752a2be fix: seed IT customer qualifications 2026-08-14 09:46:50 +08:00
4383 changed files with 503501 additions and 33219 deletions
+3
View File
@@ -1,4 +1,5 @@
target/ target/
node_modules/
.docker-context/ .docker-context/
*.log *.log
.idea/ .idea/
@@ -13,6 +14,7 @@ logs/
data/ data/
.env .env
.env.* .env.*
.env.development
!.env.example !.env.example
!.env.*.example !.env.*.example
application-local.yml application-local.yml
@@ -27,5 +29,6 @@ reports/
uploads/ uploads/
.codex-temp* .codex-temp*
.codex-tmp* .codex-tmp*
.runtime-*/
.codex-*.out .codex-*.out
.codex-*.err .codex-*.err
+56 -2
View File
@@ -13,6 +13,55 @@ cache:
paths: paths:
- .m2/repository/ - .m2/repository/
Java拓扑合同:
stage: verify
image: node:22-alpine
cache: []
script:
- node --test scripts/java-topology-contract.test.mjs
OpenAPI有效性:
stage: verify
image: node:22-alpine
cache: []
script:
- npm ci --ignore-scripts --prefix scripts/openapi-validation
- npm test --prefix scripts/openapi-validation
- npm run validate --prefix scripts/openapi-validation
静态分析报告:
stage: verify
allow_failure: true
script:
- mvn $MAVEN_CLI_OPTS -Pstatic-analysis-report -DskipTests verify
artifacts:
when: always
expire_in: 7 days
paths:
- "**/target/spotbugs*.xml"
- "**/target/site/spotbugs.html"
质量摘要:
stage: verify
image: node:22-alpine
cache: []
allow_failure: true
needs:
- job: OpenAPI有效性
artifacts: false
- job: 校验与测试
artifacts: true
- job: 静态分析报告
artifacts: true
script:
- node --test scripts/java-quality-summary.test.mjs
- node scripts/java-quality-summary.mjs --output target/java-quality-summary.json --require-jacoco --require-spotbugs
artifacts:
when: always
expire_in: 7 days
paths:
- target/java-quality-summary.json
校验与测试: 校验与测试:
stage: verify stage: verify
script: script:
@@ -25,10 +74,14 @@ cache:
- "**/target/surefire-reports/TEST-*.xml" - "**/target/surefire-reports/TEST-*.xml"
paths: paths:
- "**/target/surefire-reports/" - "**/target/surefire-reports/"
- "**/target/site/jacoco/"
- "**/target/jacoco.exec"
打包服务: 打包服务:
stage: package stage: package
needs: needs:
- Java拓扑合同
- OpenAPI有效性
- 校验与测试 - 校验与测试
script: script:
- mvn $MAVEN_CLI_OPTS -DskipTests package - mvn $MAVEN_CLI_OPTS -DskipTests package
@@ -40,10 +93,11 @@ cache:
- iam/target/*.jar - iam/target/*.jar
- order/target/*.jar - order/target/*.jar
- firstleg/target/*.jar - firstleg/target/*.jar
- finance/target/*.jar
- warehouse/target/*.jar - warehouse/target/*.jar
- warehouse-overseas-service/target/*.jar
- lastleg-service/target/*.jar
- file/target/*.jar - file/target/*.jar
- print/target/*.jar - print/target/*.jar
- audit-service/target/*.jar - audit-service/target/*.jar
- integration-service/target/*.jar - integration-service/target/*.jar
- event-bridge-service/target/*.jar
@@ -0,0 +1,23 @@
{
"deploy/sql/README.md": "4c7475566d5b3ab7ebaeb26a9369c3b2daa2911adf648bd9df1cae348de9f5c1",
"deploy/sql/头程数据库统一增量-MySQL8.sql": "55c9736c6998f59d48a76c5ba60e2a801dd0b8acbf209fcf0dae4830fb3fb485",
"deploy/sql/海外区域统一增量-MySQL8.sql": "3fda99ff623735dc42844a50d9a80090ab478aaf916ebdf5fa21e5a6d054117a",
"firstleg/src/main/java/com/baiyue/firstleg/terminaldrayage/controller/ContainerCalendarSourceController.java": "74f1f56fb224798004382e0f4b36140cf81a073f50a1ac212bc393e0d2dde159",
"firstleg/src/main/java/com/baiyue/firstleg/terminaldrayage/dto/ContainerCalendarSource.java": "58edea28fff5a8357aa662b794d23983d7da987f7f11f9b4bc64a8b406e466c5",
"firstleg/src/main/java/com/baiyue/firstleg/terminaldrayage/mapper/ContainerCalendarSourceMapper.java": "76556dc0b615c19d8b3aa8c4453af56dce72bb8782343fce6a09b8a36163a467",
"firstleg/src/main/java/com/baiyue/firstleg/terminaldrayage/repository/ContainerCalendarSourceRepository.java": "881f000a5f9a2ebe10858ee4ab97fb2595189aef641bb90dc73b9c37bc6d1d7b",
"firstleg/src/main/java/com/baiyue/firstleg/terminaldrayage/service/ContainerCalendarSourceService.java": "44c3927abb813c5d6f5c8d553795c8bf50fb945e9862984f1fe011da20d3695d",
"firstleg/src/main/resources/openapi/appointment-calendar-sources-v1.yaml": "8fffd13df663c6eaaf7eaffaa063b24d531f33241d7adb94850ee146af6d22dd",
"firstleg/src/test/java/com/baiyue/firstleg/terminaldrayage/mapper/ContainerCalendarSourceMapperTest.java": "f38e11e2509a030854c0b6f40f8f9577ed729bcbbff0666b180504f38aa93c0b",
"firstleg/src/test/java/com/baiyue/firstleg/terminaldrayage/service/ContainerCalendarSourceServiceTest.java": "87a0fbe6f7b9a271e331641c7c8da27282a6f1df2ae9b3167d543bf78119f7e6",
"lastleg-service/src/main/java/com/baiyue/lastleg/calendar/WarehouseCalendarController.java": "17102d4a32640ccfd92a7e7e738a248d4c1f5ae8906b8b965f455eb9be572603",
"lastleg-service/src/main/java/com/baiyue/lastleg/calendar/WarehouseCalendarMapper.java": "80f3a96bee0fa7630afff100ddb051977e283902c4e7ef247b8152fb37c2400f",
"lastleg-service/src/main/java/com/baiyue/lastleg/calendar/WarehouseCalendarModels.java": "88824f1a88d24b9bd34c9e7023aaa00d2653849a9131bf619d7df0f5fd3a7a0f",
"lastleg-service/src/main/java/com/baiyue/lastleg/calendar/WarehouseCalendarRepository.java": "4395cce32c574ebee0b4437f6ec8bbe0a5aee62c19a6a451ee0f243eab7be558",
"lastleg-service/src/main/java/com/baiyue/lastleg/calendar/WarehouseCalendarService.java": "886e9e5460e47962f5701ceeb5e94ba9d2a123c4c83f2791b739178c8bfe874a",
"lastleg-service/src/main/resources/openapi/appointment-calendar-warehouse-v1.yaml": "06965771d34d761b869f90ee6ff3f2dca7589121c67a5adbe2081677ce5a563c",
"lastleg-service/src/test/java/com/baiyue/lastleg/calendar/WarehouseCalendarMapperTest.java": "a17e359b39000036fd9bd80d6b0744018541fc7bbe846a0585ece629ff09dc6c",
"lastleg-service/src/test/java/com/baiyue/lastleg/calendar/WarehouseCalendarServiceTest.java": "9ed34bea7163166d0f79f882f12b23033dae13ad13bd4eedc7e202aed5defb8f",
"lastleg-service/src/test/java/com/baiyue/lastleg/fleet/trip/FleetTripApiContractTest.java": "c3a4cedac09f16f37442a6709b9d704a607f2e0238e9997687e44153c378f27a",
"lastleg-service/src/test/java/com/baiyue/lastleg/fleet/trip/FleetTripAuthorizationTest.java": "5d9e8aded16aa4de3338d65edd1a2e6b4e42961498f22d99c702e0f8d491bcd6"
}
@@ -0,0 +1,10 @@
import subprocess, shutil, xml.etree.ElementTree as ET
from pathlib import Path
root=Path.cwd()
report=root/'lastleg-service/target/surefire-reports/TEST-com.baiyue.lastleg.dispatch.service.DispatchAggregatePersistenceTest.xml'
properties=ET.parse(report).getroot().find('properties')
classpath=next(item.attrib['value'] for item in properties if item.attrib['name']=='java.class.path')
sources=[str(root/'lastleg-service/src/test/java/com/baiyue/lastleg/fleet/trip'/name) for name in ['FleetTripCalendarServiceTest.java','FleetTripCalendarMapperTest.java']]
subprocess.run(['javac','-encoding','UTF-8','--release','17','-cp',classpath,'-d',str(root/'lastleg-service/target/test-classes'),*sources],check=True)
result=subprocess.run([shutil.which('mvn'),' -T'.strip(),'1','-DforkCount=1','-DreuseForks=true','-pl','lastleg-service','surefire:test','-Dtest=FleetTripCalendarServiceTest,FleetTripCalendarMapperTest'],check=False)
raise SystemExit(result.returncode)
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+256
View File
@@ -0,0 +1,256 @@
{
"capturedAt": "2026-10-19T23:34:24.6121293+08:00",
"frozenManifest": ".logs/dispatch-merged-final-inputs.json",
"frozenManifestSha256": "55373c9dd7d5a2cc9ab0756893650ab27d5691c6b7cc4086c03140b76b86c136",
"firstPostRunAudit": {
"files": 65,
"unchanged": 65,
"drift": 0
},
"currentAudit": {
"files": 65,
"unchanged": 64,
"drift": [
{
"path": "lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/service/FbaAppointmentRepartitionPersistenceTest.java",
"frozen": "66071beb3fe6787894055c74f236fb4c72fffdb75565ed0ccb2932f546903cda",
"actual": "f2f7174dd6e52f5d9caae36f8fc24aa2dc78145c1c390e8e16857bb066948a86"
}
]
},
"affectedRun": {
"finishedAt": "2026-10-19T23:01:03+08:00",
"log": ".logs/dispatch-merged-final-affected-consumers.maven.log",
"total": 111,
"passed": 110,
"failures": 1,
"errors": 0,
"skipped": 0,
"result": "FAILURE",
"classes": [
{
"class": "com.baiyue.lastleg.controller.Phase4BrowserApiBoundaryTest",
"total": 7,
"failures": 0,
"errors": 0,
"skipped": 0
},
{
"class": "com.baiyue.lastleg.dispatch.service.DispatchMergedRelabelPersistenceTest",
"total": 11,
"failures": 0,
"errors": 0,
"skipped": 0
},
{
"class": "com.baiyue.lastleg.dispatch.service.DispatchRepairWorkflowTest",
"total": 9,
"failures": 0,
"errors": 0,
"skipped": 0
},
{
"class": "com.baiyue.lastleg.dispatch.service.DispatchServiceTest",
"total": 5,
"failures": 0,
"errors": 0,
"skipped": 0
},
{
"class": "com.baiyue.lastleg.dispatch.service.ReturnDispatchCandidatePersistenceTest",
"total": 1,
"failures": 0,
"errors": 0,
"skipped": 0
},
{
"class": "com.baiyue.lastleg.inboundplan.service.FbaAppointmentRepartitionPersistenceTest",
"total": 8,
"failures": 1,
"errors": 0,
"skipped": 0
},
{
"class": "com.baiyue.lastleg.inboundplan.service.InboundPlanPaginationTest",
"total": 3,
"failures": 0,
"errors": 0,
"skipped": 0
},
{
"class": "com.baiyue.lastleg.inboundplan.work.AppointmentWorkDomainMapperTest",
"total": 1,
"failures": 0,
"errors": 0,
"skipped": 0
},
{
"class": "com.baiyue.lastleg.relabel.service.RelabelDispatchSourcePersistenceTest",
"total": 16,
"failures": 0,
"errors": 0,
"skipped": 0
},
{
"class": "com.baiyue.lastleg.transfer.service.WarehouseTransferServiceTest",
"total": 50,
"failures": 0,
"errors": 0,
"skipped": 0
}
],
"failure": "FbaAppointmentRepartitionPersistenceTest.existingTransportAndConcurrentRepartitionCannotDuplicateWork",
"rootCause": "Fixture allocation dispatch_item_id=11/plan_pallet_id=12 did not match inserted item id=1/plan_pallet_id=NULL; current production join correctly requires matching dispatch, item, customer and plan pallet. Parent owns correction; original assertions retained."
},
"priorFullModule": {
"total": 3075,
"passed": 3056,
"failures": 6,
"errors": 4,
"skipped": 9,
"log": ".logs/dispatch-merged-final-full-module.maven.log"
},
"pending": {
"nativeCancellation": "Local backend implementation and directly affected regressions complete: 94/94 directed; Lastleg full 3072 pass, 9 conditional skips, 0 failure/error; File full 242/242",
"environment": "Deployment, test database, real HTTP and four UI browser acceptance newly authorized; parent coordinates execution, no environment verification performed by this chat"
},
"cancellationAffectedRun": {
"finishedAt": "2026-10-19T23:18:32+08:00",
"log": ".logs/dispatch-native-cancellation-affected.maven.log",
"result": "SUCCESS",
"total": 94,
"passed": 94,
"failures": 0,
"errors": 0,
"skipped": 0,
"inputManifest": ".logs/dispatch-native-cancellation-inputs.json",
"frozenFiles": 74,
"postRunDrift": 0,
"classes": [
{
"class": "DispatchDeliveryArrangementPersistenceTest",
"total": 3
},
{
"class": "DispatchDeliveryFeesTest",
"total": 6
},
{
"class": "DispatchDeliveryServiceTest",
"total": 6
},
{
"class": "DeliveryTransferServiceTest",
"total": 59
},
{
"class": "ReturnOutboundClosureTest",
"total": 11
},
{
"class": "DispatchDeliveryCompletionWorkerTest",
"total": 1
},
{
"class": "FbaAppointmentRepartitionPersistenceTest",
"total": 8
}
],
"inputManifestSha256": "187ef121ef3c75686717c2dc009604943462889dff63b9b2ac4441e7e765591b"
},
"finalFullModule": {
"result": "SUCCESS",
"finishedAt": "2026-10-19T23:25:33+08:00",
"log": ".logs/dispatch-final-full-module-closed.maven.log",
"inputManifest": ".logs/dispatch-final-full-reactor-inputs.json",
"frozenFiles": 1706,
"preRunDrift": 0,
"postRunDrift": 0,
"total": 3081,
"passed": 3072,
"failures": 0,
"errors": 0,
"skipped": 9,
"skippedCases": [
{
"class": "com.baiyue.lastleg.discard.CargoDiscardMysqlAcceptanceTest",
"name": "createApprovalRejectionPendingAndInsufficientStockScenarios",
"reason": "System property [cargo.discard.acceptance] does not exist"
},
{
"class": "com.baiyue.lastleg.service.TaillegRealConsumerAcceptanceTest",
"name": "consumesRealOutboxAndCreatesEveryTaillegOrderType",
"reason": "System property [tailleg.real.consumer] does not exist"
},
{
"class": "com.baiyue.lastleg.selfoperated.analytics.outbound.OutboundCandidateBuildMySqlAcceptanceTest",
"name": "realCandidateBuildOnIndependentSyntheticFacts",
"reason": "Environment variable [SELFOP_TEST_MYSQL_CANDIDATE_BUILD] does not exist"
},
{
"class": "com.baiyue.lastleg.selfoperated.analytics.inventory.InventoryPublicationMySqlAcceptanceTest",
"name": "crossMidnightPublicationHasNoDailyMarkerAndCannotBackfill",
"reason": "Environment variable [SELFOP_TEST_MYSQL_PUBLICATION] does not exist"
},
{
"class": "com.baiyue.lastleg.selfoperated.analytics.inventory.InventoryPublicationMySqlAcceptanceTest",
"name": "successfulPublicationAndPublishedRecoveryKeepExactCommittedResult",
"reason": "Environment variable [SELFOP_TEST_MYSQL_PUBLICATION] does not exist"
},
{
"class": "com.baiyue.lastleg.selfoperated.analytics.inventory.InventoryPublicationMySqlAcceptanceTest",
"name": "laterSameDayPublicationPreservesFirstRepresentativeAndItsRows",
"reason": "Environment variable [SELFOP_TEST_MYSQL_PUBLICATION] does not exist"
},
{
"class": "com.baiyue.lastleg.selfoperated.analytics.inventory.InventoryPublicationMySqlAcceptanceTest",
"name": "pointerRunTaskSupersededAndDailyElectionRollBackAfterRealSqlFault",
"reason": "Environment variable [SELFOP_TEST_MYSQL_PUBLICATION] does not exist"
},
{
"class": "com.baiyue.lastleg.returnreceipt.service.ReturnSkuConversionPersistenceTest",
"name": "mysqlPageLocksConvertedSkuUntilConfirmationWithoutChangingPhysicalUnit(String)",
"reason": "System property [returnSku.test.mysql] does not exist"
},
{
"class": "com.baiyue.lastleg.returnreceipt.service.ReturnSkuConversionPersistenceTest",
"name": "mysqlPageCollatesCaseAndAccentsWithoutJoiningOtherCustomerWarehouseOrSku(String)",
"reason": "System property [returnSku.test.mysql] does not exist"
}
],
"inputManifestSha256": "4ac18b73c5b9e6599a8c7f9729ab4a2a34f8145aeddb6d72c8935efaf7287cc2"
},
"fileProviderRun": {
"result": "SUCCESS",
"finishedAt": "2026-10-19T23:33:52+08:00",
"log": ".logs/dispatch-evidence-file-final-full.maven.log",
"inputManifest": ".logs/dispatch-evidence-file-final-inputs.json",
"inputManifestSha256": "f49a0707b7ea2241d7c11c90311d25b9a50eec0310fc192d445bc351c9ba78ae",
"frozenFiles": 289,
"preRunDrift": 0,
"postRunDrift": 0,
"total": 242,
"passed": 242,
"failures": 0,
"errors": 0,
"skipped": 0,
"scope": [
{
"path": "file/AGENTS.md",
"sha256": "c3236c7e89151f8f7b1da540ed8b591982e1200aafb89f45cb49230815d320c7"
},
{
"path": "file/src/main/java/com/baiyue/file/service/BusinessFileCategory.java",
"sha256": "b274532a0e4a263c56558eae68eb5717182791dec8e399e21bd459f6dffd32ff"
},
{
"path": "file/src/main/resources/openapi/dispatch-evidence-v1.yaml",
"sha256": "b3bc23d9f25513b258f0282886743e4323cb65b89c0203d4d2921ac89da08d14"
},
{
"path": "file/src/test/java/com/baiyue/file/service/DispatchEvidenceFileTest.java",
"sha256": "bba87946457fb1290960218127087552de95b59f443eb6d5dacf354084d04a19"
}
]
}
}
+339
View File
@@ -0,0 +1,339 @@
{
"capturedAt": "2026-10-19T22:57:12.4378746+08:00",
"log": ".logs/dispatch-merged-final-affected-consumers.maven.log",
"fullModulePrior": {
"total": 3075,
"passed": 3056,
"failures": 6,
"errors": 4,
"skipped": 9,
"log": ".logs/dispatch-merged-final-full-module.maven.log"
},
"files": [
{
"group": "dispatch",
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/port/DispatchPlanningModels.java",
"sha256": "47489d22ab81913ab23c319c04561371cbdf1e1c43034fe5b1d24fe94038137e"
},
{
"group": "dispatch",
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/port/DispatchViews.java",
"sha256": "d34d5a810ea7658ffd2623f53970435ecee116663bd969339de84432e38cd573"
},
{
"group": "dispatch",
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/port/DispatchContracts.java",
"sha256": "5086523d9df7c7d00840d391e9608b1685e4edb71d19154b1608339afe7421e2"
},
{
"group": "dispatch",
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/entity/DispatchPalletLock.java",
"sha256": "2988eee05074b78728878fef6dd45d0df3dba8cc7c219ebfd583bf7650a379b3"
},
{
"group": "dispatch",
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/entity/DispatchPhysicalPalletLock.java",
"sha256": "3044dc6d8780f96b4ff7d6303a2b15ed458c201496788a98c3b28cad7449e0f8"
},
{
"group": "dispatch",
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/entity/DispatchItemSource.java",
"sha256": "02dc868bef1646a02143969f7aad13b4e92cd04d4a8d69c8c71ffc07f7a50360"
},
{
"group": "dispatch",
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchSourceMapper.java",
"sha256": "1bec0687e9d1d3a2404d25a37aad092d18b7134eab605594f352a5432360ac76"
},
{
"group": "dispatch",
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchPhysicalClaimMapper.java",
"sha256": "9ae01c8669beb2fcf5e359d2e26e13e07c2a496bbe22036a408f3dd49d2ec2fa"
},
{
"group": "dispatch",
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchItemSourceMapper.java",
"sha256": "8d6b4bf84e27c18b3c8faad15dbf73018485996ca5d4dbb00bdf9ba2ab158d2d"
},
{
"group": "dispatch",
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchExecutionReadMapper.java",
"sha256": "54666dbd5968ebd9d2cb8fc9678a8a385e60591f62c90a850be513f93546906d"
},
{
"group": "dispatch",
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchCandidateMapper.java",
"sha256": "7c0269c3e5179add0ebb772cd7674ddf899039afad179bff06f7524017934573"
},
{
"group": "dispatch",
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/ReturnDispatchCandidateMapper.java",
"sha256": "a99080e851a6d22c5ecb1229e761382d846d1f5e4b92a3c69d30716af125746d"
},
{
"group": "dispatch",
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/repository/DispatchSourceRepository.java",
"sha256": "04b5d5160614b17a31030f23167d9e32aa89e13be7ef2e33c9d91359ef684348"
},
{
"group": "dispatch",
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/repository/DispatchPhysicalClaimRepository.java",
"sha256": "27f37b5e683b92d82d9cf63a88b2135ada5f597e652474eef52d7c985b870b31"
},
{
"group": "dispatch",
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/repository/DispatchItemSourceRepository.java",
"sha256": "2bcef5d9952750235ecabcf429e4eab9b9a9c0a3cac0b3e02b083120f85df28e"
},
{
"group": "dispatch",
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchService.java",
"sha256": "c95a0d4075df7628e4443f87be4d2d906aee5cbb95148d5fd3869d6ec7932465"
},
{
"group": "dispatch",
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchAggregateService.java",
"sha256": "ec0ce406d4244a0abf34ee9ede97b788675b39234fbb8e6d0ff9579dde360145"
},
{
"group": "dispatch",
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchPickingService.java",
"sha256": "17a461a599cc0702b519d1333c5e136860eff7b638d739dde7e0bd203bcfc4ec"
},
{
"group": "dispatch",
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchPdaQueryService.java",
"sha256": "0f59acc9c3ed6fd2995c1ed59810146c0056d1462e4e23d85f52198a1ffe0296"
},
{
"group": "dispatch",
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchDepartureService.java",
"sha256": "fc9440c6755316c58572f3fed5a576c5721098a3d5d7a036eecd3d6d752f1ea0"
},
{
"group": "dispatch",
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchPhysicalClaimService.java",
"sha256": "063b557cd488bf2281516df744f12123473269be12f56a5155facad018e6408e"
},
{
"group": "dispatch",
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchItemSourceService.java",
"sha256": "ee42766e7f86f896df1839e15987382c8aebc3484449b2104312378eaababbba"
},
{
"group": "dispatch",
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/pda/entity/DispatchPdaPickOrder.java",
"sha256": "3d66df6566b467580c89aa5f9f585aba610b05ca2de07ad28a3d6a6f30ab10e3"
},
{
"group": "dispatch",
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/pda/service/DispatchPickOrderAttributionService.java",
"sha256": "83947030468892ee1d4a02b354e855d7977f55fcc17de440f324cf24940a10d8"
},
{
"group": "dispatch",
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/pda/mapper/DispatchWholePdaReadMapper.java",
"sha256": "4df97c47b2ca481eb5f8f32e7735bb15a80aa4e561bea7ab349f09352ca9db1e"
},
{
"group": "sharedDraft",
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchDraftService.java",
"sha256": "a2682e87f8bbf47a799d8d54ae05b8acdef52414016cab116eb9a40fe1364e04"
},
{
"group": "sharedDraft",
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/repository/DispatchDraftRepository.java",
"sha256": "f28b7aff08afdf634387e6abb7919a807fd6a2ff54ff7869b3e1e4d453f2c92b"
},
{
"group": "sharedDraft",
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchDraftClaimMapper.java",
"sha256": "32dd25dc737d7c36f3a35f1a97bd2865cf433368aaccce2162585a30d6b07641"
},
{
"group": "tests",
"path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchMergedRelabelPersistenceTest.java",
"sha256": "1c36d64a389bf12e55cfb8cbb29b7dd333bc952f66e8bb88d441ddf42f83c653"
},
{
"group": "tests",
"path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchAggregatePersistenceTest.java",
"sha256": "8f5907aa180db30a2b56fb6b251f9ae7a94fa550666239947b660cb5431fe978"
},
{
"group": "tests",
"path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchPickingPersistenceTest.java",
"sha256": "0971e32ae147a03a730c16f1a75f0576e3a9122f92c5c65608db0cb9a76a3510"
},
{
"group": "tests",
"path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchPhysicalClaimPersistenceTest.java",
"sha256": "02bd34b71fdb237566e99fab0b748c3ed333ace4ae65042aaf315b06695299f6"
},
{
"group": "tests",
"path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchWholeSourcePersistenceTest.java",
"sha256": "0143045a52df440dc8be72706d69cbc45a9d210ddfb4ce77762423533dcc1a55"
},
{
"group": "tests",
"path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchPdaReadPersistenceTest.java",
"sha256": "9b4402d0dd3f0eefa8b6b9bfe65e20f42f3e60619ac6989b18c9a74cbebad0ad"
},
{
"group": "tests",
"path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchAppointmentCancellationPersistenceTest.java",
"sha256": "0be4d1f344374147149f248bbc2aaac486484ad4a0e9ba1681295aa6fd5ae21d"
},
{
"group": "tests",
"path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/pda/service/DispatchPickOrderAttributionServiceTest.java",
"sha256": "ec3ab8341cec7ae692501ba264178d5d520923b7c2dcf20dfaa620313c9d03d4"
},
{
"group": "tests",
"path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchDraftPersistenceTest.java",
"sha256": "35b7c411e452dce2189c22f8cf31d77083f8854062435a9223e306a1a9d44a6a"
},
{
"group": "tests",
"path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchItemSourcePersistenceTest.java",
"sha256": "24e1b3f0dba4d4f9c1f724840c7a263ff0d1561cb5cb509fb0c410be8187afb0"
},
{
"group": "tests",
"path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchRepairWorkflowTest.java",
"sha256": "4a4d01424e91fd1b55b0af4721e02c4acad065e958ff4a0b9726cf726691fbdf"
},
{
"group": "related",
"path": "lastleg-service/src/main/resources/db/lastleg.sql",
"sha256": "61946a41d773e856e3d3f9463f7ff5bfe0e904856598e4a64c8cbd0936ef55b5"
},
{
"group": "related",
"path": "deploy/sql/海外区域统一增量-MySQL8.sql",
"sha256": "4f31c8731dbb54955555d13f027c24a537aee8a317f744f7bf50b59cb22694b1"
},
{
"group": "related",
"path": "lastleg-service/src/test/java/com/baiyue/lastleg/inventory/command/InventoryHoldConcurrencyPersistenceTest.java",
"sha256": "e146136299db24b1cf618faf2161fa284d9eec15a26a75e8352548cd7dbe7e94"
},
{
"group": "related",
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/palletops/service/PdaPalletOperationService.java",
"sha256": "c3a4ceba06b722d5a0bba810eaa948bd364a09ff5599c3176207d5c28b39b7fc"
},
{
"group": "related",
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/palletops/mapper/PdaPalletOperationMapper.java",
"sha256": "960bb99f8432524e5a4bc76a61ccde8273929ad9de5e21ae0b44d941edeb6c9d"
},
{
"group": "related",
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/palletops/repository/PdaPalletOperationRepository.java",
"sha256": "55d243f6f419f112d3f89005ce37af0d6446e7b21d3b00f5931b56f3ea556302"
},
{
"group": "related",
"path": "lastleg-service/src/test/java/com/baiyue/lastleg/palletops/service/PdaPalletOperationServiceTest.java",
"sha256": "eae67e13f9d1d3e8ff8a76b30a8a72d445e54b3cd6a30394c5ea41f763f48a7c"
},
{
"group": "related",
"path": "lastleg-service/src/test/java/com/baiyue/lastleg/palletops/service/PdaPalletDispatchFreezePersistenceTest.java",
"sha256": "a8ef7e364e4d0fb97f83515dd1d3b983cdac4f367a9335fe181c39a47719720f"
},
{
"group": "related",
"path": "../docs/海外仓业务/排车列表接口与数据设计.md",
"sha256": "521169378de4ee51c9af4f8cbe331284f35bf60f137253d9751f2ba3480e95ea"
},
{
"group": "final-related",
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/relabel/port/RelabelDispatchSourcePort.java",
"sha256": "af771fd474faf6c411543b077c646479e6368b98ab3cab875c4cd6c0b3b24978"
},
{
"group": "final-related",
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/relabel/service/RelabelDispatchSourceService.java",
"sha256": "949ea59036d710af0cc0b81848a77e17da0da3de049e44124a36be141ee81fe0"
},
{
"group": "final-related",
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/relabel/repository/RelabelDispatchSourceRepository.java",
"sha256": "12b9577391f0908d94e4e2b307f7572e4825f556307145f8a83d0294516a2c46"
},
{
"group": "final-related",
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/relabel/mapper/RelabelDispatchSourceMapper.java",
"sha256": "6dd3902b7f64d40f8db5ae1f3dd3640e91a69cadd6c895b1f3a81ac48b93ebf0"
},
{
"group": "final-related",
"path": "lastleg-service/src/test/java/com/baiyue/lastleg/relabel/service/RelabelDispatchSourcePersistenceTest.java",
"sha256": "6eb43b35de92364884e9ffbd36c18c3921ac8c21c959602383f9ca9cc8c4ece5"
},
{
"group": "final-related",
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/inventory/port/PhysicalPalletSerialPort.java",
"sha256": "4aa97a0c9bf0f4a9138ec15ac32e512ff13224e9a6f0e0817c5b0852f9635746"
},
{
"group": "final-related",
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/inventory/service/PhysicalPalletSerialService.java",
"sha256": "4876ed0fc6267d9b5986fde0f2e8096ec01db35cd2f0cadbaeb3ebad2e03b76b"
},
{
"group": "final-related",
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/inventory/repository/PhysicalPalletSerialRepository.java",
"sha256": "0489e8fa5806fd0faa4a61c4a0c7a9bf89d086537752f218dcbcaed08475487e"
},
{
"group": "final-related",
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/inventory/mapper/PhysicalPalletSerialMapper.java",
"sha256": "237fe76c88d572b0d70eb25d84d191dcc0d2fdbc3a031e728e6bbbf88a8aa25f"
},
{
"group": "final-related",
"path": "lastleg-service/src/test/java/com/baiyue/lastleg/inventory/service/PhysicalPalletSerialPersistenceTest.java",
"sha256": "444bd8d361f4d90fbdaf1e4d1d205906b5452096670f57bd3459906716e36c7e"
},
{
"group": "final-related",
"path": "lastleg-service/src/test/java/com/baiyue/lastleg/controller/Phase4BrowserApiBoundaryTest.java",
"sha256": "4fbf6bab69cfaff409d0d228a1169c63db0f092e9c1c6e2f70bb956f1d3ed70f"
},
{
"group": "final-related",
"path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchServiceTest.java",
"sha256": "a9a2e25ff22e326925104e48ff1cfed34fec3350dfb569f1e7f19a4e186925c7"
},
{
"group": "final-related",
"path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/ReturnDispatchCandidatePersistenceTest.java",
"sha256": "4170a35b6f57d9058577941ef665130e0f3a3faeef37c92c9e93a69dd9f0f60a"
},
{
"group": "final-related",
"path": "lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/service/FbaAppointmentRepartitionPersistenceTest.java",
"sha256": "66071beb3fe6787894055c74f236fb4c72fffdb75565ed0ccb2932f546903cda"
},
{
"group": "final-related",
"path": "lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/service/InboundPlanPaginationTest.java",
"sha256": "b9196d489830c2f4bf24ebcaf3e94adf4e0484f414f373c300b0f4a2a538679b"
},
{
"group": "final-related",
"path": "lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/work/AppointmentWorkDomainMapperTest.java",
"sha256": "5fbe3b3be8be194d1f31006e4e05187cd629236c376e01da2d291a2593e70e0a"
},
{
"group": "final-related",
"path": "lastleg-service/src/test/java/com/baiyue/lastleg/transfer/service/WarehouseTransferServiceTest.java",
"sha256": "787e78907d019458e3533325ed2bfe8cc09eb8de7ca1db2c0b98a54239a56fe5"
}
]
}
+247
View File
@@ -0,0 +1,247 @@
{
"capturedAt": "2026-10-19T22:39:26.3372438+08:00",
"command": "mvn -pl lastleg-service -am test",
"log": ".logs/dispatch-merged-final-full-module.maven.log",
"files": [
{
"group": "dispatch",
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/port/DispatchPlanningModels.java",
"sha256": "47489d22ab81913ab23c319c04561371cbdf1e1c43034fe5b1d24fe94038137e"
},
{
"group": "dispatch",
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/port/DispatchViews.java",
"sha256": "d34d5a810ea7658ffd2623f53970435ecee116663bd969339de84432e38cd573"
},
{
"group": "dispatch",
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/port/DispatchContracts.java",
"sha256": "5086523d9df7c7d00840d391e9608b1685e4edb71d19154b1608339afe7421e2"
},
{
"group": "dispatch",
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/entity/DispatchPalletLock.java",
"sha256": "2988eee05074b78728878fef6dd45d0df3dba8cc7c219ebfd583bf7650a379b3"
},
{
"group": "dispatch",
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/entity/DispatchPhysicalPalletLock.java",
"sha256": "3044dc6d8780f96b4ff7d6303a2b15ed458c201496788a98c3b28cad7449e0f8"
},
{
"group": "dispatch",
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/entity/DispatchItemSource.java",
"sha256": "02dc868bef1646a02143969f7aad13b4e92cd04d4a8d69c8c71ffc07f7a50360"
},
{
"group": "dispatch",
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchSourceMapper.java",
"sha256": "1bec0687e9d1d3a2404d25a37aad092d18b7134eab605594f352a5432360ac76"
},
{
"group": "dispatch",
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchPhysicalClaimMapper.java",
"sha256": "9ae01c8669beb2fcf5e359d2e26e13e07c2a496bbe22036a408f3dd49d2ec2fa"
},
{
"group": "dispatch",
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchItemSourceMapper.java",
"sha256": "8d6b4bf84e27c18b3c8faad15dbf73018485996ca5d4dbb00bdf9ba2ab158d2d"
},
{
"group": "dispatch",
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchExecutionReadMapper.java",
"sha256": "54666dbd5968ebd9d2cb8fc9678a8a385e60591f62c90a850be513f93546906d"
},
{
"group": "dispatch",
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchCandidateMapper.java",
"sha256": "7c0269c3e5179add0ebb772cd7674ddf899039afad179bff06f7524017934573"
},
{
"group": "dispatch",
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/ReturnDispatchCandidateMapper.java",
"sha256": "a99080e851a6d22c5ecb1229e761382d846d1f5e4b92a3c69d30716af125746d"
},
{
"group": "dispatch",
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/repository/DispatchSourceRepository.java",
"sha256": "04b5d5160614b17a31030f23167d9e32aa89e13be7ef2e33c9d91359ef684348"
},
{
"group": "dispatch",
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/repository/DispatchPhysicalClaimRepository.java",
"sha256": "27f37b5e683b92d82d9cf63a88b2135ada5f597e652474eef52d7c985b870b31"
},
{
"group": "dispatch",
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/repository/DispatchItemSourceRepository.java",
"sha256": "2bcef5d9952750235ecabcf429e4eab9b9a9c0a3cac0b3e02b083120f85df28e"
},
{
"group": "dispatch",
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchService.java",
"sha256": "c95a0d4075df7628e4443f87be4d2d906aee5cbb95148d5fd3869d6ec7932465"
},
{
"group": "dispatch",
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchAggregateService.java",
"sha256": "ec0ce406d4244a0abf34ee9ede97b788675b39234fbb8e6d0ff9579dde360145"
},
{
"group": "dispatch",
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchPickingService.java",
"sha256": "17a461a599cc0702b519d1333c5e136860eff7b638d739dde7e0bd203bcfc4ec"
},
{
"group": "dispatch",
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchPdaQueryService.java",
"sha256": "0f59acc9c3ed6fd2995c1ed59810146c0056d1462e4e23d85f52198a1ffe0296"
},
{
"group": "dispatch",
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchDepartureService.java",
"sha256": "fc9440c6755316c58572f3fed5a576c5721098a3d5d7a036eecd3d6d752f1ea0"
},
{
"group": "dispatch",
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchPhysicalClaimService.java",
"sha256": "063b557cd488bf2281516df744f12123473269be12f56a5155facad018e6408e"
},
{
"group": "dispatch",
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchItemSourceService.java",
"sha256": "ee42766e7f86f896df1839e15987382c8aebc3484449b2104312378eaababbba"
},
{
"group": "dispatch",
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/pda/entity/DispatchPdaPickOrder.java",
"sha256": "3d66df6566b467580c89aa5f9f585aba610b05ca2de07ad28a3d6a6f30ab10e3"
},
{
"group": "dispatch",
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/pda/service/DispatchPickOrderAttributionService.java",
"sha256": "83947030468892ee1d4a02b354e855d7977f55fcc17de440f324cf24940a10d8"
},
{
"group": "dispatch",
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/pda/mapper/DispatchWholePdaReadMapper.java",
"sha256": "4df97c47b2ca481eb5f8f32e7735bb15a80aa4e561bea7ab349f09352ca9db1e"
},
{
"group": "sharedDraft",
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchDraftService.java",
"sha256": "a2682e87f8bbf47a799d8d54ae05b8acdef52414016cab116eb9a40fe1364e04"
},
{
"group": "sharedDraft",
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/repository/DispatchDraftRepository.java",
"sha256": "f28b7aff08afdf634387e6abb7919a807fd6a2ff54ff7869b3e1e4d453f2c92b"
},
{
"group": "sharedDraft",
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchDraftClaimMapper.java",
"sha256": "32dd25dc737d7c36f3a35f1a97bd2865cf433368aaccce2162585a30d6b07641"
},
{
"group": "tests",
"path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchMergedRelabelPersistenceTest.java",
"sha256": "1c36d64a389bf12e55cfb8cbb29b7dd333bc952f66e8bb88d441ddf42f83c653"
},
{
"group": "tests",
"path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchAggregatePersistenceTest.java",
"sha256": "8f5907aa180db30a2b56fb6b251f9ae7a94fa550666239947b660cb5431fe978"
},
{
"group": "tests",
"path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchPickingPersistenceTest.java",
"sha256": "0971e32ae147a03a730c16f1a75f0576e3a9122f92c5c65608db0cb9a76a3510"
},
{
"group": "tests",
"path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchPhysicalClaimPersistenceTest.java",
"sha256": "02bd34b71fdb237566e99fab0b748c3ed333ace4ae65042aaf315b06695299f6"
},
{
"group": "tests",
"path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchWholeSourcePersistenceTest.java",
"sha256": "0143045a52df440dc8be72706d69cbc45a9d210ddfb4ce77762423533dcc1a55"
},
{
"group": "tests",
"path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchPdaReadPersistenceTest.java",
"sha256": "9b4402d0dd3f0eefa8b6b9bfe65e20f42f3e60619ac6989b18c9a74cbebad0ad"
},
{
"group": "tests",
"path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchAppointmentCancellationPersistenceTest.java",
"sha256": "0be4d1f344374147149f248bbc2aaac486484ad4a0e9ba1681295aa6fd5ae21d"
},
{
"group": "tests",
"path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/pda/service/DispatchPickOrderAttributionServiceTest.java",
"sha256": "ec3ab8341cec7ae692501ba264178d5d520923b7c2dcf20dfaa620313c9d03d4"
},
{
"group": "tests",
"path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchDraftPersistenceTest.java",
"sha256": "35b7c411e452dce2189c22f8cf31d77083f8854062435a9223e306a1a9d44a6a"
},
{
"group": "tests",
"path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchItemSourcePersistenceTest.java",
"sha256": "24e1b3f0dba4d4f9c1f724840c7a263ff0d1561cb5cb509fb0c410be8187afb0"
},
{
"group": "tests",
"path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchRepairWorkflowTest.java",
"sha256": "4a4d01424e91fd1b55b0af4721e02c4acad065e958ff4a0b9726cf726691fbdf"
},
{
"group": "related",
"path": "lastleg-service/src/main/resources/db/lastleg.sql",
"sha256": "61946a41d773e856e3d3f9463f7ff5bfe0e904856598e4a64c8cbd0936ef55b5"
},
{
"group": "related",
"path": "deploy/sql/海外区域统一增量-MySQL8.sql",
"sha256": "4f31c8731dbb54955555d13f027c24a537aee8a317f744f7bf50b59cb22694b1"
},
{
"group": "related",
"path": "lastleg-service/src/test/java/com/baiyue/lastleg/inventory/command/InventoryHoldConcurrencyPersistenceTest.java",
"sha256": "e146136299db24b1cf618faf2161fa284d9eec15a26a75e8352548cd7dbe7e94"
},
{
"group": "related",
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/palletops/service/PdaPalletOperationService.java",
"sha256": "c3a4ceba06b722d5a0bba810eaa948bd364a09ff5599c3176207d5c28b39b7fc"
},
{
"group": "related",
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/palletops/mapper/PdaPalletOperationMapper.java",
"sha256": "960bb99f8432524e5a4bc76a61ccde8273929ad9de5e21ae0b44d941edeb6c9d"
},
{
"group": "related",
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/palletops/repository/PdaPalletOperationRepository.java",
"sha256": "55d243f6f419f112d3f89005ce37af0d6446e7b21d3b00f5931b56f3ea556302"
},
{
"group": "related",
"path": "lastleg-service/src/test/java/com/baiyue/lastleg/palletops/service/PdaPalletOperationServiceTest.java",
"sha256": "eae67e13f9d1d3e8ff8a76b30a8a72d445e54b3cd6a30394c5ea41f763f48a7c"
},
{
"group": "related",
"path": "lastleg-service/src/test/java/com/baiyue/lastleg/palletops/service/PdaPalletDispatchFreezePersistenceTest.java",
"sha256": "a8ef7e364e4d0fb97f83515dd1d3b983cdac4f367a9335fe181c39a47719720f"
},
{
"group": "related",
"path": "../docs/海外仓业务/排车列表接口与数据设计.md",
"sha256": "e3986c47f1b0a093a9dfd53038328211d96194eeb0abf0374df0a45d3090ebe6"
}
]
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,302 @@
{
"capturedAt": "2026-10-19T23:12:11.1283856+08:00",
"log": ".logs/dispatch-native-cancellation-affected.maven.log",
"files": [
{
"path": "../docs/海外仓业务/排车列表接口与数据设计.md",
"sha256": "521169378de4ee51c9af4f8cbe331284f35bf60f137253d9751f2ba3480e95ea"
},
{
"path": "deploy/sql/海外区域统一增量-MySQL8.sql",
"sha256": "4f31c8731dbb54955555d13f027c24a537aee8a317f744f7bf50b59cb22694b1"
},
{
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/repository/DeliveryCancellationRepository.java",
"sha256": "52409141461491b48ac5828c3d85a8ee5d578678d2d670c87cc73a707225bcdb"
},
{
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/service/DeliveryTransferService.java",
"sha256": "bb036a0cfb01324eba14bb36b623047367bf31c2727be82b6d3728635fb591ce"
},
{
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/entity/DispatchItemSource.java",
"sha256": "02dc868bef1646a02143969f7aad13b4e92cd04d4a8d69c8c71ffc07f7a50360"
},
{
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/entity/DispatchPalletLock.java",
"sha256": "2988eee05074b78728878fef6dd45d0df3dba8cc7c219ebfd583bf7650a379b3"
},
{
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/entity/DispatchPhysicalPalletLock.java",
"sha256": "3044dc6d8780f96b4ff7d6303a2b15ed458c201496788a98c3b28cad7449e0f8"
},
{
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchCandidateMapper.java",
"sha256": "7c0269c3e5179add0ebb772cd7674ddf899039afad179bff06f7524017934573"
},
{
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchDraftClaimMapper.java",
"sha256": "32dd25dc737d7c36f3a35f1a97bd2865cf433368aaccce2162585a30d6b07641"
},
{
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchExecutionReadMapper.java",
"sha256": "54666dbd5968ebd9d2cb8fc9678a8a385e60591f62c90a850be513f93546906d"
},
{
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchItemSourceMapper.java",
"sha256": "8d6b4bf84e27c18b3c8faad15dbf73018485996ca5d4dbb00bdf9ba2ab158d2d"
},
{
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchPhysicalClaimMapper.java",
"sha256": "9ae01c8669beb2fcf5e359d2e26e13e07c2a496bbe22036a408f3dd49d2ec2fa"
},
{
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchSourceMapper.java",
"sha256": "1bec0687e9d1d3a2404d25a37aad092d18b7134eab605594f352a5432360ac76"
},
{
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/ReturnDispatchCandidateMapper.java",
"sha256": "a99080e851a6d22c5ecb1229e761382d846d1f5e4b92a3c69d30716af125746d"
},
{
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/pda/entity/DispatchPdaPickOrder.java",
"sha256": "3d66df6566b467580c89aa5f9f585aba610b05ca2de07ad28a3d6a6f30ab10e3"
},
{
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/pda/mapper/DispatchWholePdaReadMapper.java",
"sha256": "4df97c47b2ca481eb5f8f32e7735bb15a80aa4e561bea7ab349f09352ca9db1e"
},
{
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/pda/service/DispatchPickOrderAttributionService.java",
"sha256": "83947030468892ee1d4a02b354e855d7977f55fcc17de440f324cf24940a10d8"
},
{
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/port/DispatchContracts.java",
"sha256": "5086523d9df7c7d00840d391e9608b1685e4edb71d19154b1608339afe7421e2"
},
{
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/port/DispatchPlanningModels.java",
"sha256": "47489d22ab81913ab23c319c04561371cbdf1e1c43034fe5b1d24fe94038137e"
},
{
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/port/DispatchViews.java",
"sha256": "d34d5a810ea7658ffd2623f53970435ecee116663bd969339de84432e38cd573"
},
{
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/repository/DispatchDraftRepository.java",
"sha256": "f28b7aff08afdf634387e6abb7919a807fd6a2ff54ff7869b3e1e4d453f2c92b"
},
{
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/repository/DispatchItemSourceRepository.java",
"sha256": "2bcef5d9952750235ecabcf429e4eab9b9a9c0a3cac0b3e02b083120f85df28e"
},
{
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/repository/DispatchPhysicalClaimRepository.java",
"sha256": "27f37b5e683b92d82d9cf63a88b2135ada5f597e652474eef52d7c985b870b31"
},
{
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/repository/DispatchSourceRepository.java",
"sha256": "04b5d5160614b17a31030f23167d9e32aa89e13be7ef2e33c9d91359ef684348"
},
{
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchAggregateService.java",
"sha256": "ec0ce406d4244a0abf34ee9ede97b788675b39234fbb8e6d0ff9579dde360145"
},
{
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchDepartureService.java",
"sha256": "fc9440c6755316c58572f3fed5a576c5721098a3d5d7a036eecd3d6d752f1ea0"
},
{
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchDraftService.java",
"sha256": "a2682e87f8bbf47a799d8d54ae05b8acdef52414016cab116eb9a40fe1364e04"
},
{
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchItemSourceService.java",
"sha256": "ee42766e7f86f896df1839e15987382c8aebc3484449b2104312378eaababbba"
},
{
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchPdaQueryService.java",
"sha256": "0f59acc9c3ed6fd2995c1ed59810146c0056d1462e4e23d85f52198a1ffe0296"
},
{
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchPhysicalClaimService.java",
"sha256": "063b557cd488bf2281516df744f12123473269be12f56a5155facad018e6408e"
},
{
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchPickingService.java",
"sha256": "17a461a599cc0702b519d1333c5e136860eff7b638d739dde7e0bd203bcfc4ec"
},
{
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchService.java",
"sha256": "c95a0d4075df7628e4443f87be4d2d906aee5cbb95148d5fd3869d6ec7932465"
},
{
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/inventory/mapper/PhysicalPalletSerialMapper.java",
"sha256": "237fe76c88d572b0d70eb25d84d191dcc0d2fdbc3a031e728e6bbbf88a8aa25f"
},
{
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/inventory/port/PhysicalPalletSerialPort.java",
"sha256": "4aa97a0c9bf0f4a9138ec15ac32e512ff13224e9a6f0e0817c5b0852f9635746"
},
{
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/inventory/repository/PhysicalPalletSerialRepository.java",
"sha256": "0489e8fa5806fd0faa4a61c4a0c7a9bf89d086537752f218dcbcaed08475487e"
},
{
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/inventory/service/PhysicalPalletSerialService.java",
"sha256": "4876ed0fc6267d9b5986fde0f2e8096ec01db35cd2f0cadbaeb3ebad2e03b76b"
},
{
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/palletops/mapper/PdaPalletOperationMapper.java",
"sha256": "960bb99f8432524e5a4bc76a61ccde8273929ad9de5e21ae0b44d941edeb6c9d"
},
{
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/palletops/repository/PdaPalletOperationRepository.java",
"sha256": "55d243f6f419f112d3f89005ce37af0d6446e7b21d3b00f5931b56f3ea556302"
},
{
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/palletops/service/PdaPalletOperationService.java",
"sha256": "c3a4ceba06b722d5a0bba810eaa948bd364a09ff5599c3176207d5c28b39b7fc"
},
{
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/relabel/mapper/RelabelDispatchSourceMapper.java",
"sha256": "6dd3902b7f64d40f8db5ae1f3dd3640e91a69cadd6c895b1f3a81ac48b93ebf0"
},
{
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/relabel/port/RelabelDispatchSourcePort.java",
"sha256": "af771fd474faf6c411543b077c646479e6368b98ab3cab875c4cd6c0b3b24978"
},
{
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/relabel/repository/RelabelDispatchSourceRepository.java",
"sha256": "12b9577391f0908d94e4e2b307f7572e4825f556307145f8a83d0294516a2c46"
},
{
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/relabel/service/RelabelDispatchSourceService.java",
"sha256": "949ea59036d710af0cc0b81848a77e17da0da3de049e44124a36be141ee81fe0"
},
{
"path": "lastleg-service/src/main/resources/db/lastleg.sql",
"sha256": "61946a41d773e856e3d3f9463f7ff5bfe0e904856598e4a64c8cbd0936ef55b5"
},
{
"path": "lastleg-service/src/test/java/com/baiyue/lastleg/controller/Phase4BrowserApiBoundaryTest.java",
"sha256": "4fbf6bab69cfaff409d0d228a1169c63db0f092e9c1c6e2f70bb956f1d3ed70f"
},
{
"path": "lastleg-service/src/test/java/com/baiyue/lastleg/delivery/DeliveryDispatchFixtures.java",
"sha256": "1dc488f6d614d0038a54ea81ecf1ca15c40887bcf951f5f96df436511a8cec86"
},
{
"path": "lastleg-service/src/test/java/com/baiyue/lastleg/delivery/DispatchDeliveryArrangementPersistenceTest.java",
"sha256": "b989d8db720bd10aa4c928297ed387486353a06922d043d298b234bbc3c26b29"
},
{
"path": "lastleg-service/src/test/java/com/baiyue/lastleg/delivery/DispatchDeliveryFeesTest.java",
"sha256": "57928866a589890689ff729a234a5a1b1a93e39e2ad6a11dae7c05bb312261e4"
},
{
"path": "lastleg-service/src/test/java/com/baiyue/lastleg/delivery/DispatchDeliveryServiceTest.java",
"sha256": "cd2b7753a44aeb34f5b2f88bae8349ecb8eeea5c96ecd3796391570a8662b9ca"
},
{
"path": "lastleg-service/src/test/java/com/baiyue/lastleg/delivery/service/DeliveryTransferServiceTest.java",
"sha256": "6e37ddaf197138e06b6b0d2d3ca908c58e23f8a2e01fb18791cb38b8ff1fd5be"
},
{
"path": "lastleg-service/src/test/java/com/baiyue/lastleg/delivery/service/ReturnOutboundClosureTest.java",
"sha256": "178d6781adf4c6f08be8569ec869566ea97391ead064ca460b9b3e812ca9e585"
},
{
"path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/pda/service/DispatchPickOrderAttributionServiceTest.java",
"sha256": "ec3ab8341cec7ae692501ba264178d5d520923b7c2dcf20dfaa620313c9d03d4"
},
{
"path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchAggregatePersistenceTest.java",
"sha256": "8f5907aa180db30a2b56fb6b251f9ae7a94fa550666239947b660cb5431fe978"
},
{
"path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchAppointmentCancellationPersistenceTest.java",
"sha256": "0be4d1f344374147149f248bbc2aaac486484ad4a0e9ba1681295aa6fd5ae21d"
},
{
"path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchDeliveryCompletionWorkerTest.java",
"sha256": "cf2812520b6c1025b3b8ab8084b9d21064427811dd62f12a734cf90c4c035c12"
},
{
"path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchDraftPersistenceTest.java",
"sha256": "35b7c411e452dce2189c22f8cf31d77083f8854062435a9223e306a1a9d44a6a"
},
{
"path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchItemSourcePersistenceTest.java",
"sha256": "24e1b3f0dba4d4f9c1f724840c7a263ff0d1561cb5cb509fb0c410be8187afb0"
},
{
"path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchMergedRelabelPersistenceTest.java",
"sha256": "1c36d64a389bf12e55cfb8cbb29b7dd333bc952f66e8bb88d441ddf42f83c653"
},
{
"path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchPdaReadPersistenceTest.java",
"sha256": "9b4402d0dd3f0eefa8b6b9bfe65e20f42f3e60619ac6989b18c9a74cbebad0ad"
},
{
"path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchPhysicalClaimPersistenceTest.java",
"sha256": "02bd34b71fdb237566e99fab0b748c3ed333ace4ae65042aaf315b06695299f6"
},
{
"path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchPickingPersistenceTest.java",
"sha256": "0971e32ae147a03a730c16f1a75f0576e3a9122f92c5c65608db0cb9a76a3510"
},
{
"path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchRepairWorkflowTest.java",
"sha256": "4a4d01424e91fd1b55b0af4721e02c4acad065e958ff4a0b9726cf726691fbdf"
},
{
"path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchServiceTest.java",
"sha256": "a9a2e25ff22e326925104e48ff1cfed34fec3350dfb569f1e7f19a4e186925c7"
},
{
"path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchWholeSourcePersistenceTest.java",
"sha256": "0143045a52df440dc8be72706d69cbc45a9d210ddfb4ce77762423533dcc1a55"
},
{
"path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/ReturnDispatchCandidatePersistenceTest.java",
"sha256": "4170a35b6f57d9058577941ef665130e0f3a3faeef37c92c9e93a69dd9f0f60a"
},
{
"path": "lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/service/FbaAppointmentRepartitionPersistenceTest.java",
"sha256": "f2f7174dd6e52f5d9caae36f8fc24aa2dc78145c1c390e8e16857bb066948a86"
},
{
"path": "lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/service/InboundPlanPaginationTest.java",
"sha256": "b9196d489830c2f4bf24ebcaf3e94adf4e0484f414f373c300b0f4a2a538679b"
},
{
"path": "lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/work/AppointmentWorkDomainMapperTest.java",
"sha256": "5fbe3b3be8be194d1f31006e4e05187cd629236c376e01da2d291a2593e70e0a"
},
{
"path": "lastleg-service/src/test/java/com/baiyue/lastleg/inventory/command/InventoryHoldConcurrencyPersistenceTest.java",
"sha256": "e146136299db24b1cf618faf2161fa284d9eec15a26a75e8352548cd7dbe7e94"
},
{
"path": "lastleg-service/src/test/java/com/baiyue/lastleg/inventory/service/PhysicalPalletSerialPersistenceTest.java",
"sha256": "444bd8d361f4d90fbdaf1e4d1d205906b5452096670f57bd3459906716e36c7e"
},
{
"path": "lastleg-service/src/test/java/com/baiyue/lastleg/palletops/service/PdaPalletDispatchFreezePersistenceTest.java",
"sha256": "a8ef7e364e4d0fb97f83515dd1d3b983cdac4f367a9335fe181c39a47719720f"
},
{
"path": "lastleg-service/src/test/java/com/baiyue/lastleg/palletops/service/PdaPalletOperationServiceTest.java",
"sha256": "eae67e13f9d1d3e8ff8a76b30a8a72d445e54b3cd6a30394c5ea41f763f48a7c"
},
{
"path": "lastleg-service/src/test/java/com/baiyue/lastleg/relabel/service/RelabelDispatchSourcePersistenceTest.java",
"sha256": "6eb43b35de92364884e9ffbd36c18c3921ac8c21c959602383f9ca9cc8c4ece5"
},
{
"path": "lastleg-service/src/test/java/com/baiyue/lastleg/transfer/service/WarehouseTransferServiceTest.java",
"sha256": "787e78907d019458e3533325ed2bfe8cc09eb8de7ca1db2c0b98a54239a56fe5"
}
]
}
+167
View File
@@ -0,0 +1,167 @@
{
"createdAtUtc": "2026-10-19T11:12:23.053116+00:00",
"scope": "C52 route pruning only: actual origin, supported old stops retain relative order, new cargo stops append, active external booking stops required; invalid commitments fail atomically",
"sources": [
{
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetBookingReservations.java",
"sha256": "84906c28ccaf94125a41c03a30c9df392400af74569ed26c2da8abd5c116144c",
"modifiedAtUtc": "2026-10-19T11:06:24.471699+00:00"
},
{
"path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetTripService.java",
"sha256": "28eca09b87337b07a04955039a50c3c55805289955c24db75fe9df6fbf0b3bcc",
"modifiedAtUtc": "2026-10-19T11:06:24.472698+00:00"
},
{
"path": "lastleg-service/src/test/java/com/baiyue/lastleg/fleet/trip/FleetDispatchRoutePersistenceTest.java",
"sha256": "687248640255a40456f4197cab28dd32e8a16f97a833858fc868e171dcf2b62c",
"modifiedAtUtc": "2026-10-19T11:08:07.884938+00:00"
}
],
"aggregateMethod": "normalized path + space + lowercase SHA256, Unicode lexicographic path sort, LF joined, no trailing LF",
"aggregateSha256": "9fab733033d4fffaeb716b4cb2720dd72b26c14ece1b97d6bd6d2e30db502aa2",
"compileCommand": [
"./scripts/local-check.ps1",
"-WorkingDirectory",
".",
"-Label",
"Dispatch route test-compile",
"-Command",
[
"mvn",
"-pl",
"lastleg-service",
"-am",
"-DskipTests",
"test-compile"
]
],
"testCommand": [
"./scripts/local-check.ps1",
"-WorkingDirectory",
".",
"-Label",
"Dispatch route regression",
"-Command",
[
"mvn",
"-pl",
"lastleg-service",
"-am",
"-Dtest=FleetDispatchRoutePersistenceTest,DispatchAggregatePersistenceTest,FleetTripPersistenceTest,FleetReceiptCapacityTest,FleetTripMatchedStopsTest",
"-Dsurefire.failIfNoSpecifiedTests=false",
"test"
]
],
"compileExit": 0,
"testExit": 0,
"jvmPolicy": "local-check defaults; JAVA_TOOL_OPTIONS ActiveProcessorCount=2 Xmx1536m; no memory override",
"logs": [
{
"path": ".logs/dispatch-route-test-compile.log",
"sha256": "d52d00bd3b3d16b0ced516f4649367a94b012fb8ed831a1ca933b1d4ef6acb39"
},
{
"path": ".logs/dispatch-route-test-compile.maven.log",
"sha256": "9071174d9e9248a6655a1ca815043d17e607adb1d4f1ab8b5f914c2e51fc9d4b"
},
{
"path": ".logs/dispatch-route-regression.log",
"sha256": "1edb3d4afb3b03e35f4192b855ec80c607b4ae5a88e9261a431a79e97dbaa68a"
},
{
"path": ".logs/dispatch-route-regression.maven-tail.log",
"sha256": "b17e27d2466c4b63d9c4a976bea598b6af45a50f2b53fb387bb8fdbbb48bdfab"
}
],
"results": [
{
"class": "com.baiyue.lastleg.dispatch.service.DispatchAggregatePersistenceTest",
"tests": 11,
"failures": 0,
"errors": 0,
"skipped": 0,
"xmlSha256": "edb055fa5c2d9ddaa446aec702a00f17db76e8e95c3c5d4ddb411e0df40e2573",
"testCases": [
"incompleteAttributionCannotPassLoadingThroughMatchingHeaderTotals",
"reviewsUseActualRootRoundAndImmutableCommandsBeforeAndAfterLoading",
"operationItemsAndRouteUseActualIdsAndPersistedStopOrder",
"emptyCreateHasOneRealTruckAndNoCustomerHeader",
"multiCustomerSelectionCreatesOneTruckAndKeepsAllDocumentsAndBatchReads",
"realPickBlocksReturnEvenWhenHeaderCountersAreZero",
"laterCargoConflictRollsBackTruckResourcesEarlierLocksAndItems",
"loadingUsesActualSourcesMeasurementsAndNeverAutoCompletesFromCounters",
"cancelledOutboundHistoryDoesNotBlockZeroPickCancellationButAnActiveDocumentDoes",
"updateSavesCurrentVehiclePlanAndReplayWithoutAnyVehicleAppointmentField",
"removalCancellationAndOldCommandRecoveryNeverReleaseNewTruckClaims"
]
},
{
"class": "com.baiyue.lastleg.fleet.trip.FleetDispatchRoutePersistenceTest",
"tests": 7,
"failures": 0,
"errors": 0,
"skipped": 0,
"xmlSha256": "12404cd4a8e98eecd01e18b7ea12f915032a4f5f175d2d1e0b26daebc4766b8d",
"testCases": [
"failureAfterTripRouteWriteRollsBackMainClaimsQuantitiesAndLedgerThenTheSameKeyCanSucceed",
"missingRequiredBookingStopRejectsTheWholeRemovalAndDoesNotInventAStop",
"lastPalletRemovalPrunesOnlyItsStopPreservesOrderAndAppendsNewDestinations",
"activeBookingKeepsItsOriginalStopAndCancelledBookingsDoNotLeaveGhostStops(String)[1]",
"activeBookingKeepsItsOriginalStopAndCancelledBookingsDoNotLeaveGhostStops(String)[2]",
"activeBookingKeepsItsOriginalStopAndCancelledBookingsDoNotLeaveGhostStops(String)[3]",
"anotherPalletAtTheSameDestinationKeepsTheStopAndAnEmptyTruckKeepsOnlyItsOrigin"
]
},
{
"class": "com.baiyue.lastleg.fleet.trip.FleetReceiptCapacityTest",
"tests": 5,
"failures": 0,
"errors": 0,
"skipped": 0,
"xmlSha256": "a86f409a17999c90c88791bbc64e98c0e6fcd69db6e1f52c5aada0676ab8b53b",
"testCases": [
"firstReceiptOrMissingPreviousReceiptStillRequiresValidOwnPlan",
"receivedReconfirmationFitsWithoutCreatingAnOwnCargoPlan",
"otherConfirmedReservationsStillConsumeCapacity",
"nonemptyStaleOwnPlanRemainsBlocked",
"remeasuredExternalCargoStillCannotExceedVehicleWeight"
]
},
{
"class": "com.baiyue.lastleg.fleet.trip.FleetTripMatchedStopsTest",
"tests": 3,
"failures": 0,
"errors": 0,
"skipped": 0,
"xmlSha256": "0f1d101f1d479c0f8d86fd3ec0d06649addc999bacfb3fa43c39361a42691b4c",
"testCases": [
"rejectsMissingIdentityWithoutUsingDisplayNameAsKey",
"replacesOnlyInitialPlaceholderAndDeduplicatesByIdentity",
"preservesExistingStopIndexesAndAppendsDifferentIdentityEvenWhenNamesMatch"
]
},
{
"class": "com.baiyue.lastleg.fleet.trip.FleetTripPersistenceTest",
"tests": 7,
"failures": 0,
"errors": 0,
"skipped": 0,
"xmlSha256": "2f5eba7a2776a9c19829fba55ed379a84e19d6a5d7095ec7f2ec6f4dfccfba67",
"testCases": [
"capacityFailureOrStaleFingerprintDoesNotWritePlanOrHistory",
"boundCorePlanAndResourcesMustUseTheCommonMainVersionAndReview",
"externalVehicleCapacityPlanUsesActualWholeTruckStopsAndSnapshots",
"oneWholeTruckProjectsBothReviewsFromTheRealMain",
"laterFullSignatureReturnDoesNotReopenOriginalTransport",
"partialSignatureCannotCloseCargoOrEndExternalTasks",
"externalCargoCompletionAndTwoTaskEndsAreIndependentAndReplayable"
]
}
],
"tests": 33,
"failures": 0,
"errors": 0,
"skipped": 0,
"limitations": "H2 service/mapper/transaction regression with controlled catalogue/source providers and seeded persisted bookings; no environment HTTP, production SQL, deployment, UI or real PDA acceptance; no 501 rerun"
}
File diff suppressed because it is too large Load Diff
+134 -33
View File
@@ -1,67 +1,168 @@
# 国内头程后端项目指引 # 头程与海外仓后端项目指引
> 当前为单 TEST、统一 Gateway/Auth 会话/Nacos 与业务 Outbox 直接投递。架构规则及部署/验收状态唯一见 `../docs/测试环境单机部署与配置契约.md`、`../docs/统一认证与服务通信契约.md`。模块旧双区域说明不能覆盖这两份契约;构建、部署与整体验收分别核对。
根 POM 当前聚合 20 个模块(7 个公共/依赖模块、13 个服务),部署映射见 `deploy/service-map.yml`。`gateway-overseas/` 与 `event-bridge-service/` 为退役待清理源码,不是当前构建或部署单元。
## 适用范围与定位 ## 适用范围与定位
- 本文件适用于 `baiyue-firstleg/` Maven 多模块仓库;进入具体服务后继续读取该服务的 `AGENTS.md`。 - 本文件适用于 `baiyue-firstleg/` Maven 多模块仓库,包含国内头程与海外区域服务;进入具体服务后继续读取该服务的 `AGENTS.md`。
- 本层只负责:服务边界、跨服务契约、数据库部署总入口、阶段路由和通用验证。Controller、Service、DTO、SQL 细节以模块级指引为准。 - 本层只负责:服务边界、跨服务契约、数据库部署总入口、阶段路由和通用验证。Controller、Service、DTO、SQL 细节以模块级指引为准。
- 普通任务先读本文件和目标模块指引;聚合构建/架构问题再读 `README.md`、`pom.xml`、`docs/头程项目目录结构与服务说明.md`。 - 普通任务先读本文件和目标模块指引;聚合构建/架构问题再读 `README.md`、`pom.xml`、`docs/头程项目目录结构与服务说明.md`。
- 平台/客户层级与权限公共语义见 `../docs/平台客户层级与权限契约.md`;统一 Token、API 和可靠直接投递公共契约见 `../docs/统一认证与服务通信契约.md`。本文件只保留后端执行所需摘要。
## 不可突破的产品与数据边界 ## 不可突破的产品与数据边界
- 当前产品只有“平台—客户”两级。`tenant_id=0` 是平台技术作用域;`tenant_id>0` 是客户隔离键,不得新增租户切换、租户选择或租户级 RBAC。 - 当前产品只有“平台—客户”两级。`tenant_id=0` 是平台技术作用域;`tenant_id>0` 是客户隔离键,不得新增租户切换、租户选择或租户级 RBAC。
- 公共仓库、往来单位、快递、托盘、库位统一归属 `tenant_id=0`;订单、文件、费用等客户数据按客户 `tenant_id>0` 隔离。不得跨库 Join 或信任客户端客户 ID。 - 公共仓库、往来单位、快递、托盘、库位统一归属 `tenant_id=0`;订单、文件、费用等客户数据按客户 `tenant_id>0` 隔离。不得跨库 Join 或信任客户端客户 ID。
- 订单交易状态归 `order`,履约/入库状态归 `firstleg`,财务账期、账单、收款、成本与核销归 `finance`,仓储资源归 `warehouse`,文件对象归 `file`,OCR 适配归 `integration-service`;通过内部客户端、DTO 和服务令牌协作。 - 国内订单交易状态归 `order`,头程履约/入库状态归 `firstleg`,财务账期、账单、收款、成本与核销归 `finance`;全部仓库 ID、类型、状态、地址和能力主档归国内 `warehouse`,海外 `warehouse-overseas-service` 只维护引用同一国内 warehouseId 的库位、托盘和资源锁。海外尾程订单、入仓计划、库存业务与海外本土业务归 `lastleg-service`,所有端公网入口归 `gateway`。文件对象归 `file`,第三方适配归 `integration-service`;跨服务通过内部客户端、DTO、事件和服务令牌协作。
- 普通业务成功/失败使用 HTTP 200 + body `code`(成功为字符串 `"200"`);认证、基础设施和网关拒绝可使用真实 401/403/503。 - 普通业务成功/失败使用 HTTP 200 + body `code`(成功为字符串 `"200"`);认证、基础设施和网关拒绝可使用真实 401/403/503。
## 服务路由(按关键词定位) ## 服务路由(按关键词定位)
| 任务关键词 | 首选服务 | 先看入口 | 本表只保留定位入口;命中业务后用 `rg -n -F '关键词' docs/backend-business-index.md` 读取对应行/段的详细约束,再沿代码引用追踪;不默认全文加载详细索引。已知目标文件时直接定位,并定向核对该业务约束。
|---|---|---|
| 登录、Token、密码、注册 | `auth/` | `auth/AGENTS.md` | ### 后端任务最小读取集
| 客户档案、成员、审核、RBAC、授权快照 | `iam/` | `iam/AGENTS.md` |
| 消息中心、站内通知、消息模板、收件箱 | `iam/` | `iam/AGENTS.md`,再按 `src/main/java/com/baiyue/iam/notification/` 定位;业务事件生产者回到所属服务 | - 单服务 Controller/Service/Mapper 任务:本文件、目标服务 `AGENTS.md`、目标类及其直接调用点。
| 订单、货物、快照、状态、幂等、Outbox | `order/` | `order/AGENTS.md` | - 跨服务 API/事件任务:在上述基础上增加直接调用方、被调用方及受影响的 OpenAPI/事件契约;公开路径、访问域或入口鉴权变化才增加 Gateway,内部调用和事件不经公网 Gateway。不因业务关键词自动加载整个业务专题。
| 履约分配、入库、测量、审核、FBA 计费 | `firstleg/` | `firstleg/AGENTS.md` | - 只有状态、权限、数据库或验收边界无法从上述入口确定时,才读取 `docs/业务快速索引.md`、专题 README 或完整契约。
| 分柜、柜次、智能排柜、预装/装柜清单、PDA | `firstleg/` | `firstleg/AGENTS.md`,再按 `split/`、`loading/`、`outbound/` 定位 | - 读取预算遵循工作区根指引:超过约 250 行或 8000 字符时先读职责、硬边界和验证章节,再检索任务命中段;不得仅因行数少而全文加载长业务表。聚合构建或架构任务才读取相关 `README.md`、`pom.xml` 和目录结构文档,已知单模块任务不增加概览文档。
| 订舱、船期、拖车、报关 | `firstleg/` | `oceanbooking/`、`portschedule/`、`outbound/dispatch/`、`customs/` |
| 客户账期、费用池、账单、收款、成本、核销 | `finance/` | `finance/AGENTS.md`;费用来源和柜次成本快照再联查 `firstleg/AGENTS.md`,凭证再联查 `file/AGENTS.md` | | 任务关键词 | 首选服务 |
| 仓库、往来单位、快递、托盘、库位 | `warehouse/` | `warehouse/AGENTS.md` | |---|---|
| 供应商、国内整柜库存与出入库 | `warehouse/` | `warehouse/AGENTS.md`;OpenAPI `warehouse/src/main/resources/openapi/supplier-v1.yaml`、`domestic-container-v1.yaml` | | 登录、Token、密码、注册与顶层授权快照 | `auth/` |
| 客户运营统计、在途柜量 | `firstleg/` | `firstleg/AGENTS.md`;OpenAPI `firstleg/src/main/resources/openapi/customer-statistics-v1.yaml` | | 客户档案、成员、审核、RBAC、授权快照 | `iam/` |
| 平台操作日志与审计查询 | `audit-service/` | `audit-service/AGENTS.md`;平台查询接口 `/api/platform/v1/audit/logs` | | 客户建档默认箱规与权重系数初始化及补偿 | `iam/`、`firstleg/` |
| 资质、导出、测量图片、文件下载 | `file/` | `file/AGENTS.md` | | 消息中心、站内通知、消息模板、收件箱 | `iam/` |
| 路由、CORS、JWT 入口、访问域 | `gateway/` | `gateway/AGENTS.md` | | 订单、货物、快照、状态、幂等、Outbox | `order/` |
| 百度/Amazon/OCR/第三方适配 | `integration-service/` | `integration-service/AGENTS.md` | | 客户头程订单中文批量导入 | `order/` |
| PDF 标签、审计、事件 | `print/`、`audit-service/`、`event-bridge-service/` | 各服务指引 | | 平台修改客户原始申报、履约渠道变更与跨服务同步 | `order/`、`firstleg/` |
| 平台订单批量审核预览 | `order/` |
| 履约分配、入库、测量、箱规算法、审核、FBA 计费 | `firstleg/` |
| 入库单扣货、解除、凭证与履约门禁 | `firstleg/`、`file/` |
| 称重确认、整票库位块与 PDA 资源结算 | `firstleg/`、`warehouse/` |
| 客户专属价格簿、柜级阶梯运费与受控调价 | `firstleg/` |
| Firstleg 内部模块化与依赖基线 | `firstleg/` |
| Mapper 新增、移动、Bean 缺失与扫描遗漏 | 目标持久化服务 |
| 脚本管理、全量验证、测试造数与诊断 | 仓库根 `scripts/`;服务专属脚本留在 `<service>/scripts/` |
| 分柜、柜次、智能排柜、预装/装柜清单、真实柜号后补、PDA | `firstleg/` |
| 合柜报关 / 同柜不可拆组、跨客户合柜、整组分配/移除 | `firstleg/` |
| 全局智能装柜候选、柜次、占位与移除 | `firstleg/`、`order/` |
| 客户入库结果与头程费用确认 | `firstleg/` |
| 订舱资料/打印、船期、订舱级拖车、报关 | `firstleg/` |
| 目的港码头拖车与还空预警 | `firstleg/` |
| 客户账期、费用池、账单、入仓码联查、收款、成本、核销 | `finance/` |
| 客户资金账户、充值、授信、冻结、结算与跨区资金指令 | `finance/`、`lastleg-service/`、`common-event-contract/` |
| 柜级公共费用跨客户分摊 | `firstleg/`、`finance/` |
| 海外业务费用来源跨区入池 | `lastleg-service/`、`common-event-contract/`、`finance/` |
| 入库/柜次应付、直接已付、付款流水与凭证 | `firstleg/`、`finance/`、`file/` |
| 仓库、往来单位、快递、托盘、库位 | `warehouse/` |
| 供应商、国内整柜库存与出入库 | `warehouse/` |
| 客户运营统计、在途柜量 | `firstleg/` |
| 平台操作日志与审计查询 | `audit-service/` |
| 资质、导出、导出退避重试、测量图片、文件下载 | `file/` |
| 路由、CORS、JWT 入口、访问域 | `gateway/` |
| 百度/Amazon/OCR/第三方适配 | `integration-service/` |
| 统一 API 路由、Token、Audience/JWKS | `gateway/`、`auth/`、`common-security/` |
| 统一会话与账号/客户权限撤销 | `auth/`、`iam/`、`common-security/` |
| 尾程订单、海外入仓计划、换标、库存、采购、本土卡车 | `lastleg-service/` |
| 尾程订单控制、柜级预览与入仓计划转换 | `lastleg-service/`、`warehouse-overseas-service/` |
| 海外入仓分页、FBA 预约、PDA 柜号签收与入仓 | `lastleg-service/` |
| 海外仓排车查询、候选与智能预排 | `lastleg-service/` |
| 海外排车执行、PDA 出库、派送与一件代发 | `lastleg-service/` |
| 海外异常退回、库存恢复与客户回访 | `lastleg-service/`、`file/` |
| 一件代发 SKU/箱唛与暂扣跨区协同 | `order/`、`firstleg/`、`common-event-contract/`、`lastleg-service/` |
| 客户海外仓贴标、转运、废弃与退货换标 | `lastleg-service/` |
| 海外仓 PDA 盘点、移库、托盘、换标与查询 | `lastleg-service/`、`warehouse-overseas-service/` |
| 国内船节点向海外接柜门禁事件 | `firstleg/`、`common-event-contract/`、`lastleg-service/` |
| 海外仓库目录、海外库位、海外托盘、资源锁 | 国内仓库目录归 `warehouse/`,海外资源归 `warehouse-overseas-service/` |
| Lastleg 内部模块化与依赖基线 | `lastleg-service/` |
| 自营领星采集、经营统计、SKU 日销量 | `lastleg-service/selfoperated/` 查询与采集编排;领星签名/分页/外部接口查 `integration-service/`,不因采集页面任务同时读取两服务 |
| 跨客户同仓预约、预约跟进、到港日历 | `lastleg-service/inboundplan/`、`appointmentplanning/`;到港事实查 `firstleg/portschedule/` |
| 车辆、司机证照、车次、外接预订、车队报价、出车记录 | `lastleg-service/fleet/`;先读 `lastleg-service/AGENTS.md` 对应入口,客户排车关联再沿 `dispatch/fleet/` 实际调用 |
| PDF 标签、审计、业务事件直投 | `print/`、`audit-service/`、`common-event-contract/` |
## 公共模块快速索引
| 关键词 | 模块与入口 |
|---|---|
| 公共依赖版本 | `platform-bom/pom.xml`;统一管理公共组件版本,不放业务实现 |
| 通用响应、分页、异常、基础实体与导出契约 | `common-core/`;从 `ApiResponse.java`、`PageResult.java`、`BizException.java`、`ExportContracts.java` 定位 |
| Web 异常、请求关联/Trace、同步出站传播、Long ID 序列化 | `common-web/`;从 `GlobalExceptionHandler.java`、`CorrelationIdFilter.java`、`TraceParent.java`、`FeignTraceRequestInterceptor.java`、`RestClientTraceInterceptor.java`、`LongIdJacksonConfiguration.java` 定位 |
| JWT、JWKS、会话校验与认证用户 | `common-security/`;从 `JwtAuthenticationFilter.java`、`JwtClaimsMapper.java`、`AccessTokenSessionValidator.java` 定位 |
| Springdoc/OpenAPI 自动配置 | `common-openapi/`;从 `BaiyueOpenApiAutoConfiguration.java`、`OpenApiProperties.java` 定位 |
| 业务审计注解、脱敏、Outbox 与投递 | `common-audit/`;从 `BusinessAudit.java`、`BusinessAuditAspect.java`、`SensitiveDataSanitizer.java`、`AuditOutboxPublisher.java` 定位 |
| 跨区域事件信封、Payload 与哈希 | `common-event-contract/`;继续读该目录 `AGENTS.md` |
## 全量验收路由 ## 全量验收路由
- 当前一期、二期、三期及财务统一进入全量验收;按本次链路读取对应阶段/专题文档,不以历史阶段终点阻断跨服务联调或回归。 - 当前订单与客户、入库测量与计费、国内货柜履约、海外仓业务及财务统一进入全量验收;按本次链路读取对应业务域/专题文档,不以历史规划终点阻断跨服务联调或回归。
- `auth`、`iam`、`warehouse`、`order`、`firstleg`、`file`、`finance`、`integration-service` 及出仓相关能力按各自模块契约协作;状态、权限、数据库和 OpenAPI 以当前实现及已确认契约为准。 - `auth`、`iam`、`warehouse`、`order`、`firstleg`、`file`、`finance`、`integration-service` 及出仓相关能力按各自模块契约协作;状态、权限、数据库和 OpenAPI 以当前实现及已确认契约为准。
## 国内头程与海外仓协同
- Auth 是唯一用户账号与 Token 签发方;目标为各端共用 Gateway 和 Redis 会话,保留 RS256/JWKS、客户隔离、权限撤销、PDA 仓绑定及服务身份校验。当前源码已移除旧海外会话验证与 Lastleg 安全投影,改为 Auth 权威会话校验;本地验证不等于测试环境部署验收。
- 装柜、船节点、卸柜、暂扣、派送、费用与资金按统一通信契约的逐事件表直接投递。保留本地 Outbox、消费幂等、重试与失败处置,不携带用户 JWT;旧 Bridge 源码已退出聚合和发布清单,退役待清理;实际停机仍须替代链验证与授权,不得据源码状态认定已部署。
- `lastleg-service` 不读取头程数据库,`warehouse-overseas-service` 不读取尾程订单库。共用测试 MySQL 实例不改变服务数据归属。准确路由、ACK、撤销矩阵和切换门槛只维护在新的两份主契约中。
## 数据库访问与中文代码注释(所有服务及公共模块强制)
- 本节适用于全部后端服务和公共模块;新增代码遵守下列规范,存量分层仅在影响本次正确性或用户明确要求时整改,不因局部修改迁移整条链路;以上为目标规范,不代表存量均已达标。
- 生产代码的关系型数据库读写统一经 MyBatis/MyBatis-Plus Mapper 执行;Controller、Service、定时任务、消费者、编号生成器和公共基础组件不得直接使用 `JdbcTemplate`、`NamedParameterJdbcTemplate`、原生 JDBC/Connection/Statement 或其他通道执行 SQL。仅改成 Repository 包装 JDBC 不算整改,SQL 必须进入 Mapper;无数据库职责的模块不得为满足本条新增数据库访问。
- 正常业务数据库访问路径统一为 `Controller → Service → Repository → Mapper`,四层职责明确,不得省略 Repository,也不得由 Controller 直接调用 Repository/Mapper,或由 Service 直接调用 Mapper。定时任务、消费者等非 HTTP 入口从 Service 进入,同样经 Repository → Mapper 完成持久化访问。
- Controller 负责请求接收、参数校验、入口权限和响应转换;Service 负责业务规则、编排及业务事务;Repository 负责领域数据访问封装、查询组合与持久化模型转换,对 Service 提供有业务含义的数据操作;Mapper 负责 SQL 执行及数据库记录映射。Repository 不承载业务状态流转,不直接用 JDBC 执行 SQL;Service 不继承 MyBatis-Plus `ServiceImpl` 等方式绕过 Repository。
- 复杂查询、批量处理、原子计数、锁定读取、Outbox 和数据库监控查询同样经 Repository → Mapper,不以 SQL 特殊或实现方便为由跳层。公共模块需要数据库能力时,按现有模块边界定义 Repository/持久化接口,由所属服务的 Repository → Mapper 实现,禁止借此跨服务读库。
- 迁移数据库操作须保留客户隔离条件、事务传播、行锁、幂等和并发语义;`LAST_INSERT_ID()` 等连接级操作必须在同一事务连接完成,并明确关闭相关读取缓存,不能仅机械搬移 SQL。Mapper 注册及扫描仍遵守下文统一规范。
- 测试代码可用 `JdbcTemplate` 创建隔离测试表、准备数据及核对结果;被测生产路径仍必须调用实际 Mapper,不得把测试造数做法带入生产。数据库基线和迁移脚本按现有部署契约管理。
- 本次新增或修改的手写方法及构造器须有简短中文业务注释,通常 1—3 行;接口与 Mapper 方法优先用中文 Javadoc,覆盖方法同样适用,关键规则就地说明。不补齐未修改或自动生成方法的注释。
- 核心业务、复杂 SQL、状态流转、资金与库存变更、权限隔离、事务、锁、幂等、并发、重试和补偿逻辑,须在关键代码前增加适量中文注释,解释业务规则、处理原因、约束边界及失败影响;不逐行翻译代码,不添加无信息量或与实现不符的注释。
- 新增和修改的代码说明注释统一使用中文;必要的技术标识、协议名、类名可保留原文,第三方许可证和生成标记保持原样。修改方法时同步更新相关存量英文或过期说明;审查时同时核对 Controller → Service → Repository → Mapper 分层及中文方法、核心逻辑注释。
## 契约、数据库与验证入口 ## 契约、数据库与验证入口
- 对外 Controller/API 新增、改名或删除时,同时核对调用端、统一 Gateway、OpenAPI 和权限;提交前运行 `node --test scripts/gateway-route-contract.test.mjs`,自动发现生产 Controller 并校验网关目标服务与内部路径隔离。失败不得提交或标记完成;网关配置变化必须列入部署清单,不以业务服务单测代替入口检查。
- 跨服务修改先检查调用方 `client/`、被调用方 Controller/DTO、`src/main/resources/openapi/` 和双方契约测试;禁止共享领域 Entity。 - 跨服务修改先检查调用方 `client/`、被调用方 Controller/DTO、`src/main/resources/openapi/` 和双方契约测试;禁止共享领域 Entity。
- 平台后台存在正式列表页时,`*:list` 同时授权列表、详情及只读辅助接口;`create`、`edit`、`delete`、`status`、`approve`、`review`、`export`、`upload`、`complete` 等操作保持独立。当前未上线,不兼容旧独立 `*:view`,Controller 不得使用 `hasAnyAuthority(list, view)`,OpenAPI 和角色初始化只保留 `*:list`。 - 修改 Java 方法、构造器、Controller、Client、DTO 或 Mapper 参数签名时,先用 `rg` 定向查找符号及旧参数形态,并同步更新 `src/main/`、`src/test/`、Mock、Fixture、契约测试和脚本中的全部调用点。新增可选参数也必须补齐旧测试调用,不能因生产调用已更新就视为完成。
- 所有生产 MyBatis Mapper 接口必须显式标记 `@Mapper`;每个持久化服务只允许在启动类使用一次服务根包 `@MapperScan(..., annotationClass = Mapper.class)`,不得新增子域包白名单或分散的 `*MapperConfiguration`。新增、移动 Mapper 或修改扫描配置后运行 `node scripts/mapper-scan-contract.mjs`;全量后端回归已在 Maven 前执行该门禁时复用结果,输入未变不重复执行。
- 平台后台存在正式列表页时,`*:list` 同时授权列表、详情及只读辅助接口;`create`、`edit`、`delete`、`status`、`approve`、`review`、`export`、`upload`、`complete` 等操作保持独立。当前正式权限契约不保留旧独立 `*:view`;Controller 不得使用 `hasAnyAuthority(list, view)`,OpenAPI 和角色初始化只保留 `*:list`。若外部已有不可变权限契约,先按当前对话建立单独兼容规则。
- 供应商查询接口固定只使用 `platform:supplier:list`;IAM 中 `platform:supplier:list` 是正式菜单资源,`platform:supplier:view` 必须停用/移除,不得重新提升或兼容。 - 供应商查询接口固定只使用 `platform:supplier:list`;IAM 中 `platform:supplier:list` 是正式菜单资源,`platform:supplier:view` 必须停用/移除,不得重新提升或兼容。
- 权限语义变更必须同步 Controller `@PreAuthorize`、OpenAPI、模块权限 SQL、IAM 菜单资源、统一增量脚本和契约测试。并行模块任务不得覆盖菜单与权限总控已写入共享 SQL 或权限契约的改动。 - 权限语义变更必须同步 Controller `@PreAuthorize`、OpenAPI、模块权限 SQL、IAM 菜单资源、统一增量脚本和契约测试。并行模块任务不得覆盖菜单与权限总控已写入共享 SQL 或权限契约的改动。
- 数据库只访问所属服务库。模块迁移位于 `<module>/src/main/resources/db/`;测试/存量环境唯一部署入口是 `deploy/sql/头程数据库统一增量-MySQL8.sql`,说明见 `deploy/sql/README.md`,历史日期脚本仅追溯。 - 数据库只访问所属服务库。模块表结构位于 `<module>/src/main/resources/db/`;开发环境按最新模块基线和 `deploy/sql/头程数据库统一增量-MySQL8.sql` 收敛,不为历史开发数据新增兼容迁移。已有名称含 `migration` 的脚本仅作结构来源和追溯,部署说明见 `deploy/sql/README.md`;生产升级另按明确发布规则处理。
- 通用配置在 `config/nacos/{local,cn}/`;服务日志、上传文件、`target/`、`reports/` 不属于源码。 - 后端同样遵守根指引中的“开发数据和上传文件可丢弃”边界:不得为现有开发库孤儿文件、旧文件归属或历史部分失败新增删除补偿接口、兼容复用、回填或清理链路;只修复当前正式上传与持久化路径,既有开发文件由人工重新上传。
- 全量验证:`mvn -DskipTests validate`、`mvn test`;单服务优先 `mvn -pl <module> -am test`。不要在本地启动 Java/Nacos/数据库,除非用户明确要求。 - 通用配置在 `config/nacos/test/`;服务日志、上传文件、`target/`、`reports/` 不属于源码。
- 后端本地验证默认使用 `scripts/backend-check.ps1`,必须显式指定 `-Modules`,不传模块直接拒绝;命令及参数见 `scripts/README.md` 的“后端分层检查”。入口自动经 `local-check.ps1` 限流,`-DryRun` 只检查选择范围,不代表测试通过。
- 局部逻辑使用 `-Mode Focused -Tests <精确测试类名>`,覆盖修改行为及必要边界即可交付,不因收尾再跑 `Module`。Java 签名变化须覆盖生产与测试源码编译;定向测试已编译时不重复跑 `-Mode Compile`。只有影响扩及服务内多个行为且定向覆盖不足才选 `Module`,先说明原因。跨服务 DTO、接口或事件变化显式列入生产者和实际消费者;`-am` 仅补上游构建依赖,不自动包含下游或 HTTP/事件消费者。
- 仅注释/文档调整不触发 Java 回归;脚本调整执行受影响脚本测试和登记合同。API、Mapper、AGENTS 等静态合同仍按各自触发条件执行。纯重构也应覆盖受影响行为,不能只用编译宣称回归通过。`-Dmaven.test.skip=true` 跳过测试编译,不得作为签名变更交付验证。
- 全仓回归仅用于已确认影响广泛且定向覆盖不足的公共依赖/聚合拓扑/认证/序列化/基础模型变化,或用户要求的全量/跨域集成验收;先说明具体影响,不能因未查清范围而全跑。根 POM、依赖或测试配置改动先按实际影响选编译或测试,不一律全仓。入口为 `scripts/backend-full-regression.ps1`,CI 全量保留。普通提交、单服务任务或对话收尾不触发;有效结果复用,输入变化只补跑受影响范围。
- 不以缩短耗时为由删除金额、库存、权限隔离、状态流转、幂等和并发测试;冗余测试须根据重复场景及维护成本单独评估。不要在本地启动 Java/Nacos/数据库,除非用户明确要求。
- 同一批 `AGENTS.md` 索引修改完成后从后端根统一运行一次 `./scripts/agents-index-contract.ps1`;脚本只读源码目录和指定指引,不访问 HTTP、数据库或部署环境。
- Mapper 注册静态合同为 `scripts/mapper-scan-contract.mjs`、`mapper-scan-contract.test.mjs`:自动发现聚合模块、生产 `*Mapper` 接口、服务启动类和分散扫描配置,不启动 Spring、不连接数据库;遗漏注解、扫描根漂移或重新引入子域扫描时直接失败并输出类与文件路径。
- 单服务镜像构建/部署从 `Jenkinsfile.service`、`deploy/docker/Dockerfile`、`deploy/scripts/deploy-built-service.sh` 进入;新镜像启动失败或健康检查失败时只允许回滚到同服务先前镜像,并保持任务失败,离线合同见 `scripts/deploy-built-service-rollback.test.mjs`。实际部署仍须当前对话明确授权。
## 全量 HTTP 验收脚本路由 ## 全量 HTTP 验收脚本路由
- 全量验收唯一指向测试环境 HTTP:平台端执行 `scripts/admin-test-env-acceptance.mjs`,客户端执行 `scripts/client-test-env-acceptance.mjs`;二期兼容场景才单独执行 `scripts/client-phase2-test-env-acceptance.mjs`。 - 脚本管理的唯一总索引是 `scripts/README.md`,最近执行规则是 `scripts/AGENTS.md`。仓库级脚本、Admin 内部模块、服务局部合同和部署脚本的目录归属以这两个文件为准,不再通过无范围搜索猜测入口。
- 全量验收唯一指向测试环境 HTTP:平台端执行 `scripts/admin-test-env-acceptance.mjs`,客户端执行 `scripts/client-test-env-acceptance.mjs`;入库测量与计费专项场景才单独执行 `scripts/client-phase2-test-env-acceptance.mjs`,不替代客户端全量入口。
- Client 快速下单定向验收使用 `scripts/client-quick-order-test-env-acceptance.mjs`,只验证批量草稿接口、逐行隔离和幂等行为,不提交、不审核,也不替代 Client 全量入口。
- 固定客户待审核订单使用 `scripts/create-pending-review-orders.mjs`;固定“IT测试”与“义乌仓”、随机真实线路并推进至 `WAIT_SPLIT` 使用 `scripts/it-test-wait-split-bulk.mjs`。二者都是造数入口,不属于全量验证;会写目标环境且不自动清理成功数据。
- Admin 总入口默认按模块列表覆盖平台全链路;需要定位单个业务时使用 `--modules <name>`,模块名称和静态合同先查 `scripts/admin-test-env-modules/README.md`。 - Admin 总入口默认按模块列表覆盖平台全链路;需要定位单个业务时使用 `--modules <name>`,模块名称和静态合同先查 `scripts/admin-test-env-modules/README.md`。
- 这些脚本会创建、更新或最终停用测试数据,必须在当前对话明确授权测试环境验收后执行,并保存 JSON 报告;未授权不得请求 Gateway。 - 订舱与拖车跨服务完整状态链由 Admin 注册模块 `service-center-flow` 验证,必须复用本轮已放舱订舱并由总入口展开依赖,不得直接执行模块文件或借历史数据。
- 尾程公网控制由 `phase4-tail-order-controls` 在海外 PDA 前验证整柜接柜仓变更与恢复、单/批目的仓变更、单/批合托标记、CSV 导出、订单费用、暂扣/解除、八个人工节点和逐票人工计划;目的港码头拖车由 `phase5-terminal-drayage` 在卸柜后验证选车队、预约提柜、实际提柜和实际还柜,`phase4-carrier-nodes` 只读核验最终船节点终态。
- 这些脚本会创建、更新或最终停用测试数据;指定测试环境和服务后,本轮非破坏性写入复用一次授权,删除、重置、SQL、部署或发布仍逐项授权,并保存 JSON 报告;未授权不得请求 Gateway。
- 后端 Maven、前端 `pnpm test/typecheck`、PDA `node --test` 和数据库 Dry-run 都是本地质量检查,不属于全量 HTTP 验收。 - 后端 Maven、前端 `pnpm test/typecheck`、PDA `node --test` 和数据库 Dry-run 都是本地质量检查,不属于全量 HTTP 验收。
- Java 模块、OpenAPI 覆盖、JaCoCo 与 SpotBugs 聚合摘要使用 `node scripts/java-quality-summary.mjs`;默认仅报告存量缺口,是否阻断由显式 `--require-*` 参数决定,输出位于 `target/` 时不得作为业务事实或提交物。
- HTTP 全量通过后,才进入 Admin/Client 真实页面调用、浏览器样式和 PDA 页面/设备验收;自动化 HTTP 通过不代表视觉验收完成。 - HTTP 全量通过后,才进入 Admin/Client 真实页面调用、浏览器样式和 PDA 页面/设备验收;自动化 HTTP 通过不代表视觉验收完成。
- 修改脚本目录、入口或说明后,至少运行 `node --test scripts/script-documentation-contract.test.mjs`;修改 `AGENTS.md` 后继续运行 `./scripts/agents-index-contract.ps1`。两者均为离线只读合同,不能代替真实 HTTP 验收。
## 阶段文档入口 ## 业务域文档入口
- 一期:`../docs/一期/README.md`、`../docs/一期/一期头程订单闭环设计.md` - 订单与客户:`../docs/订单与客户/README.md`、`../docs/订单与客户/订单与客户头程订单闭环设计.md`
- 二期:`../docs/二期/README.md`、`../docs/二期/二期需求优化版-v2-测量计费入库主流程.md`、需求确认清单及联调报告 - 入库测量与计费:`../docs/入库测量与计费/README.md`;流程细节按 `../docs/头程到尾程/README.md` 指向的当前阶段文档读取,需求确认和联调报告仅在历史追溯或验收证据任务中读取
- 三期:`../docs/三期/README.md`、`../docs/三期/三期开工门禁.md`、`../docs/三期/三期开发规划.md`、`../docs/三期/三期需求确认表.md`、页面与原型清单 - 国内货柜履约:`../docs/国内货柜履约/README.md`,报关规则变化再查 `../docs/国内货柜履约/报关资料与船期价格接入梳理.md`。规划、确认与验收材料按归档位置及文档角色判断,当前入口指定的有效规则仍按需核对,历史材料只用于追溯或证据任务
- 海外仓业务:`../docs/海外仓业务/README.md`;统一入口 `gateway/`,尾程与入仓库存 `lastleg-service/`,海外仓储资源 `warehouse-overseas-service/`
- 海外仓全流程:以正式 Controller、前端路由、权限、SQL 与跨区事件契约为范围依据;海外接柜、入仓、库存、库内服务、排车、出库、派送、一件代发、异常退回和客户回访进入 `lastleg-service/`,国内 SKU/箱唛及暂扣协同进入 `order/`、`firstleg/`、`common-event-contract/`
- 财务:`../docs/财务管理/README.md` 中已确认的范围、对象、状态和接口约束 - 财务:`../docs/财务管理/README.md` 中已确认的范围、对象、状态和接口约束
全量验收任务再读取对应阶段进度表、验收报告和证据矩阵;普通代码定位只读取当前链路涉及的范围、契约、模块指引和代码。 全量验收需要证据时再读取对应阶段报告和证据矩阵;普通代码定位只读取当前链路涉及的范围、契约、模块指引和代码。
Vendored
+281 -59
View File
@@ -1,6 +1,6 @@
def serviceModules = [ def serviceModules = [
'gateway', 'auth', 'iam', 'firstleg', 'finance', 'order', 'warehouse', 'file', 'print', 'gateway', 'auth', 'iam', 'firstleg', 'finance', 'order', 'warehouse', 'file', 'print',
'integration-service', 'event-bridge-service', 'audit-service' 'integration-service', 'lastleg-service', 'warehouse-overseas-service', 'audit-service'
] ]
def serviceCheckboxes = [ def serviceCheckboxes = [
@@ -14,23 +14,149 @@ def serviceCheckboxes = [
'file': 'SERVICE_FILE', 'file': 'SERVICE_FILE',
'print': 'SERVICE_PRINT', 'print': 'SERVICE_PRINT',
'integration-service': 'SERVICE_INTEGRATION', 'integration-service': 'SERVICE_INTEGRATION',
'event-bridge-service': 'SERVICE_EVENT_BRIDGE', 'lastleg-service': 'SERVICE_LASTLEG',
'warehouse-overseas-service': 'SERVICE_WAREHOUSE_OVERSEAS',
'audit-service': 'SERVICE_AUDIT' 'audit-service': 'SERVICE_AUDIT'
] ]
def commonPaths = [ def fullRebuildPaths = [
'pom.xml', 'platform-bom/', 'common-core/', 'common-web/', 'common-security/', 'pom.xml', 'platform-bom/', 'common-core/', 'deploy/docker/',
'common-openapi/', 'common-audit/', 'common-event-contract/', 'Jenkinsfile', 'deploy/docker-compose.yml', 'deploy/service-map.yml', 'config/nacos/test/', 'deploy/scripts/', 'Jenkinsfile'
'Jenkinsfile.service', 'deploy/' ]
def commonPathServices = [
'common-web/': [
'auth', 'iam', 'firstleg', 'finance', 'order', 'warehouse', 'file', 'print',
'integration-service', 'lastleg-service', 'warehouse-overseas-service',
'audit-service'
],
'common-security/': [
'auth', 'iam', 'firstleg', 'finance', 'order', 'warehouse', 'file',
'lastleg-service', 'warehouse-overseas-service', 'audit-service'
],
'common-openapi/': [
'auth', 'iam', 'firstleg', 'finance', 'order', 'warehouse', 'file', 'print',
'integration-service', 'lastleg-service', 'warehouse-overseas-service',
'audit-service'
],
'common-audit/': [
'auth', 'iam', 'firstleg', 'finance', 'order', 'warehouse', 'file',
'warehouse-overseas-service', 'audit-service'
],
'common-event-contract/': [
'firstleg', 'finance', 'lastleg-service'
],
'deploy/docker-compose.audit.yml': ['audit-service'],
] ]
def databaseMigrationPaths = ['deploy/sql/'] def databaseMigrationPaths = ['deploy/sql/']
// 暂停数据库结构只读门禁;保留实现,完成 Credentials 配置后改为 true。
def databaseSchemaContractEnabled = false
def downstreamResults = [:] def downstreamResults = [:]
def downstreamBuildUrls = [:]
def downstreamBuildNumbers = [:]
def parseAutoBuildState(String description, List<String> allowedServices) {
if (!description?.startsWith('AUTO ')) return null
int markerIndex = description.indexOf(' | ')
if (markerIndex < 0) return null
String commit = description.substring('AUTO '.length(), markerIndex).trim()
if (!(commit ==~ /[0-9a-fA-F]{7,40}/)) return null
String state = description.substring(markerIndex + ' | '.length()).trim()
List<String> failedServices = []
if (state.startsWith('RESULTS')) {
String summary = state.substring('RESULTS'.length()).trim()
int metadataIndex = summary.indexOf(' | ')
if (metadataIndex >= 0) summary = summary.substring(0, metadataIndex).trim()
summary.split(',').collect { it.trim() }.findAll { it }.each { item ->
int separator = item.indexOf('=')
if (separator > 0) {
String service = item.substring(0, separator).trim()
String result = item.substring(separator + 1).trim()
if (allowedServices.contains(service) && result != 'SUCCESS') failedServices << service
}
}
} else if (state.startsWith('PENDING')) {
// 上一轮若在统一测试、排队或并行部署期间失败/取消,description 仍是 PENDING。
// 将该轮已选服务作为精确补跑范围,同时让 Git 基线前进到该轮提交,
// 避免回退到很早的成功构建后把历史公共文件再次解释成全量变更。
String selected = state.substring('PENDING'.length()).trim()
int suffixIndex = selected.indexOf(' | ')
if (suffixIndex >= 0) selected = selected.substring(0, suffixIndex).trim()
failedServices.addAll(selected.split(',').collect { it.trim() }
.findAll { allowedServices.contains(it) })
} else {
return null
}
return [commit: commit, failedServices: failedServices.unique()]
}
def previousAutoBuildState(def previousBuild, List<String> allowedServices) {
def candidate = previousBuild
int inspected = 0
while (candidate != null && inspected < 30) {
def state = parseAutoBuildState(candidate.description, allowedServices)
// 完成的 RESULTS 和中断留下的 PENDING 都能推进 AUTO 基线;PENDING 中的服务会被精确补跑。
if (state != null) return state
candidate = candidate.previousBuild
inspected++
}
return null
}
// 测试范围必须相对成功门禁,而非失败/取消轮次;否则补跑可能遗漏前次未通过的变更。
def previousSuccessfulTestCommit(def previousBuild, List<String> requiredServices) {
def candidate = previousBuild
int inspected = 0
while (candidate != null && inspected < 30) {
String description = candidate.description ?: ''
String marker = ' | TESTS=PASSED '
int markerIndex = description.lastIndexOf(marker)
if (candidate.result == 'SUCCESS' && markerIndex >= 0) {
List metadata = description.substring(markerIndex + marker.length()).trim().split(' ')
if (metadata.size() == 2 && metadata[0] ==~ /[0-9a-fA-F]{7,40}/ &&
requiredServices.every { metadata[1].split(',').contains(it) }) return metadata[0]
}
candidate = candidate.previousBuild
inspected++
}
return ''
}
def environmentChoicesForJob(String jobBaseName) {
return [fixedEnvironmentForJob(jobBaseName) ?: 'test']
}
def fixedEnvironmentForJob(String jobBaseName) {
switch (jobBaseName) {
case 'backend-java':
return 'test'
case 'backend-java-prod':
return 'production'
default:
return null
}
}
def fixedDownstreamJobPrefixForJob(String jobBaseName) {
switch (jobBaseName) {
case 'backend-java':
return 'baiyue-firstleg-'
case 'backend-java-prod':
return 'baiyue-firstleg-prod-'
default:
return null
}
}
def downstreamJobPrefixChoicesForJob(String jobBaseName) {
return [fixedDownstreamJobPrefixForJob(jobBaseName) ?: 'baiyue-firstleg-']
}
pipeline { pipeline {
// 总控流水线只识别变更并调度独立的服务 Job;实际 Maven、镜像和部署在 Jenkinsfile.service 中完成。 // 总控流水线只识别变更并调度独立的服务 Job;实际 Maven、镜像和部署在 Jenkinsfile.service 中完成。
agent any agent none
options { options {
disableConcurrentBuilds() disableConcurrentBuilds()
@@ -42,7 +168,7 @@ pipeline {
choice(name: 'BUILD_MODE', choices: ['AUTO', 'SELECT', 'ALL'], description: 'AUTO 按 Git 变更识别;SELECT 勾选服务;ALL 构建全部服务') choice(name: 'BUILD_MODE', choices: ['AUTO', 'SELECT', 'ALL'], description: 'AUTO 按 Git 变更识别;SELECT 勾选服务;ALL 构建全部服务')
booleanParam(name: 'SERVICE_GATEWAY', defaultValue: false, description: 'Gateway 网关服务(gateway)') booleanParam(name: 'SERVICE_GATEWAY', defaultValue: false, description: 'Gateway 网关服务(gateway)')
booleanParam(name: 'SERVICE_AUTH', defaultValue: false, description: '统一认证服务(auth)') booleanParam(name: 'SERVICE_AUTH', defaultValue: false, description: '统一认证服务(auth)')
booleanParam(name: 'SERVICE_IAM', defaultValue: false, description: '租户与权限服务(iam)') booleanParam(name: 'SERVICE_IAM', defaultValue: false, description: '客户与权限服务(iam)')
booleanParam(name: 'SERVICE_FIRSTLEG', defaultValue: false, description: '头程履约服务(firstleg)') booleanParam(name: 'SERVICE_FIRSTLEG', defaultValue: false, description: '头程履约服务(firstleg)')
booleanParam(name: 'SERVICE_FINANCE', defaultValue: false, description: '财务服务(finance)') booleanParam(name: 'SERVICE_FINANCE', defaultValue: false, description: '财务服务(finance)')
booleanParam(name: 'SERVICE_ORDER', defaultValue: false, description: '订单服务(order)') booleanParam(name: 'SERVICE_ORDER', defaultValue: false, description: '订单服务(order)')
@@ -50,43 +176,59 @@ pipeline {
booleanParam(name: 'SERVICE_FILE', defaultValue: false, description: '文件中心(file)') booleanParam(name: 'SERVICE_FILE', defaultValue: false, description: '文件中心(file)')
booleanParam(name: 'SERVICE_PRINT', defaultValue: false, description: '打印服务(print)') booleanParam(name: 'SERVICE_PRINT', defaultValue: false, description: '打印服务(print)')
booleanParam(name: 'SERVICE_INTEGRATION', defaultValue: false, description: '第三方集成服务(integration-service)') booleanParam(name: 'SERVICE_INTEGRATION', defaultValue: false, description: '第三方集成服务(integration-service)')
booleanParam(name: 'SERVICE_EVENT_BRIDGE', defaultValue: false, description: '跨区域事件桥(event-bridge-service)') booleanParam(name: 'SERVICE_LASTLEG', defaultValue: false, description: '海外尾程与入仓服务(lastleg-service)')
booleanParam(name: 'SERVICE_WAREHOUSE_OVERSEAS', defaultValue: false, description: '海外仓资源服务(warehouse-overseas-service)')
booleanParam(name: 'SERVICE_AUDIT', defaultValue: false, description: '审计服务(audit-service)') booleanParam(name: 'SERVICE_AUDIT', defaultValue: false, description: '审计服务(audit-service)')
choice(name: 'ENVIRONMENT', choices: ['test', 'staging', 'production'], description: '部署环境') choice(name: 'ENVIRONMENT', choices: environmentChoicesForJob(env.JOB_BASE_NAME), description: '部署环境')
booleanParam(name: 'DEPLOY', defaultValue: false, description: '各服务构建成功后是否独立部署') booleanParam(name: 'DEPLOY', defaultValue: false, description: '各服务构建成功后是否独立部署')
booleanParam(name: 'INITIALIZE_TEST_DATABASE', defaultValue: false, description: '仅 test:调度服务前幂等初始化测试数据库') booleanParam(name: 'SKIP_TESTS', defaultValue: false, description: '是否跳过测试执行(仍编译测试源码)')
booleanParam(name: 'SKIP_TESTS', defaultValue: false, description: '是否跳过测试;生产构建不建议勾选') choice(name: 'TEST_SCOPE', choices: ['AUTO', 'MODULE'], description: 'AUTO 对已支持的局部变更执行关联测试;公共/未知变更自动完整模块回归;MODULE 显式完整模块回归')
string(name: 'IMAGE_REPOSITORY', defaultValue: '8.163.30.128:10082/java-project/baiyue-firstleg', description: '预发布/生产镜像仓库地址;test 环境自动使用本机镜像') // 地址由当前 Job 配置提供;参数声明刷新时保留已配置值,不将环境地址写入源码。
string(name: 'DOCKER_BASE_IMAGE', defaultValue: 'docker.m.daocloud.io/library/eclipse-temurin:17-jre-alpine', description: 'Java 17 基础镜像') string(name: 'IMAGE_REPOSITORY', defaultValue: params.IMAGE_REPOSITORY ?: '', description: '沿用当前 Job 已配置的镜像仓库地址;首次配置或更换仓库时填写 host[:port]/owner/image')
string(name: 'DOWNSTREAM_JOB_PREFIX', defaultValue: 'baiyue-firstleg-', description: '单服务 Jenkins Job 名称前缀,例如 baiyue-firstleg-auth') string(name: 'DOCKER_BASE_IMAGE', defaultValue: 'eclipse-temurin:17-jre-alpine', description: 'Java 17 基础镜像')
choice(name: 'DOWNSTREAM_JOB_PREFIX', choices: downstreamJobPrefixChoicesForJob(env.JOB_BASE_NAME), description: '由当前总控 Job 固定的单服务 Job 前缀')
} }
stages { stages {
stage('确定构建范围') { stage('确定构建范围') {
agent { label 'java-builder' }
steps { steps {
script { script {
String requestedRepository String resolvedEnvironment = fixedEnvironmentForJob(env.JOB_BASE_NAME)
if (params.ENVIRONMENT == 'test') { String resolvedJobPrefix = fixedDownstreamJobPrefixForJob(env.JOB_BASE_NAME)
requestedRepository = 'baiyue-firstleg-test' if (!resolvedEnvironment || !resolvedJobPrefix) {
echo '测试环境使用 Jenkins 节点本机镜像,不登录、不推送远程 Registry' error("未配置总控 Job ${env.JOB_BASE_NAME} 的固定部署环境,拒绝继续")
} else {
requestedRepository = params.IMAGE_REPOSITORY?.trim()
if (!requestedRepository || requestedRepository == 'registry.example.com/your-group/baiyue-firstleg') {
error('预发布/生产环境必须填写真实镜像仓库地址')
}
if (requestedRepository.contains('://') || !requestedRepository.contains('/')) {
error('IMAGE_REPOSITORY 格式错误:请填写 host[:port]/owner/image,不要包含 http:// 或 https://')
}
} }
if (params.ENVIRONMENT != resolvedEnvironment ||
params.DOWNSTREAM_JOB_PREFIX?.trim() != resolvedJobPrefix) {
error('部署环境和下游前缀必须与当前 Job 固定映射一致')
}
env.RESOLVED_ENVIRONMENT = resolvedEnvironment
echo "Job ${env.JOB_BASE_NAME} 使用部署环境: ${env.RESOLVED_ENVIRONMENT}"
// 先记录当前提交,使后续生产校验失败时通知仍能定位版本。
env.SOURCE_COMMIT = sh(script: 'git rev-parse HEAD', returnStdout: true).trim()
env.IMAGE_TAG = sh(script: 'git rev-parse --short=12 HEAD', returnStdout: true).trim()
String requestedRepository = params.IMAGE_REPOSITORY?.trim()
if (!requestedRepository) {
error('必须填写真实镜像仓库地址')
}
if (requestedRepository.tokenize('/')[0] ==~ /(?i)(?:[a-z0-9-]+\.)*example\.(?:com|net|org)(?::[0-9]+)?/) {
error('IMAGE_REPOSITORY 不能使用 example 示例域名,请填写已核实的镜像仓库地址')
}
if (requestedRepository.contains('://') || !requestedRepository.contains('/')) {
error('IMAGE_REPOSITORY 格式错误:请填写 host[:port]/owner/image,不要包含 http:// 或 https://')
}
echo "${env.RESOLVED_ENVIRONMENT} 环境使用共享镜像仓库: ${requestedRepository}"
env.RESOLVED_IMAGE_REPOSITORY = requestedRepository env.RESOLVED_IMAGE_REPOSITORY = requestedRepository
String jobPrefix = params.DOWNSTREAM_JOB_PREFIX?.trim() env.RESOLVED_JOB_PREFIX = resolvedJobPrefix
if (!jobPrefix) {
error('DOWNSTREAM_JOB_PREFIX 不能为空')
}
env.RESOLVED_JOB_PREFIX = jobPrefix
env.DATABASE_CHANGE_DETECTED = 'false' env.DATABASE_CHANGE_DETECTED = 'false'
List selected = [] List selected = []
if (params.BUILD_MODE == 'ALL') { if (params.BUILD_MODE == 'ALL') {
selected = serviceModules selected = serviceModules
@@ -98,9 +240,18 @@ pipeline {
error('SELECT 模式至少需要勾选一个服务') error('SELECT 模式至少需要勾选一个服务')
} }
} else { } else {
String base = env.GIT_PREVIOUS_SUCCESSFUL_COMMIT?.trim() def autoState = previousAutoBuildState(currentBuild.previousBuild, serviceModules)
String base = autoState?.commit?.trim()
List retryServices = autoState?.failedServices ?: []
if (!base) {
// 首次启用或旧构建记录已淘汰时,只比较当前提交的直接父提交。
// 需要初始化全部服务时应显式使用 ALL,不能因历史成功基线过旧而意外全量部署。
base = sh(script: 'git rev-parse HEAD^ 2>/dev/null || true', returnStdout: true).trim()
if (base) echo "AUTO 未找到历史记录,本次使用直接父提交作为基线: ${base}"
}
if (!base || sh(script: "git cat-file -e ${base}^{commit} 2>/dev/null", returnStatus: true) != 0) { if (!base || sh(script: "git cat-file -e ${base}^{commit} 2>/dev/null", returnStatus: true) != 0) {
selected = serviceModules selected = serviceModules
echo 'AUTO 未找到可用基线,安全回退为全量构建'
} else { } else {
List changed = sh(script: "git diff --name-only ${base} HEAD", returnStdout: true) List changed = sh(script: "git diff --name-only ${base} HEAD", returnStdout: true)
.trim().split('\\n').collect { it.trim() }.findAll { it } .trim().split('\\n').collect { it.trim() }.findAll { it }
@@ -108,13 +259,27 @@ pipeline {
databaseMigrationPaths.any { prefix -> path.startsWith(prefix) } databaseMigrationPaths.any { prefix -> path.startsWith(prefix) }
} }
env.DATABASE_CHANGE_DETECTED = databaseChanged ? 'true' : 'false' env.DATABASE_CHANGE_DETECTED = databaseChanged ? 'true' : 'false'
boolean commonChanged = changed.any { path -> List serviceRelevantChanges = changed.findAll { path ->
boolean databaseMigration = databaseMigrationPaths.any { prefix -> path.startsWith(prefix) } !databaseMigrationPaths.any { prefix -> path.startsWith(prefix) }
!databaseMigration && commonPaths.any { prefix -> path == prefix || path.startsWith(prefix) }
} }
selected = commonChanged ? serviceModules : serviceModules.findAll { module -> boolean fullRebuild = serviceRelevantChanges.any { path ->
changed.any { it == module || it.startsWith("${module}/") } fullRebuildPaths.any { prefix -> path == prefix || path.startsWith(prefix) }
} }
if (fullRebuild) {
selected = serviceModules
} else {
selected = serviceModules.findAll { module ->
serviceRelevantChanges.any { it == module || it.startsWith("${module}/") }
}
commonPathServices.each { prefix, impactedServices ->
if (serviceRelevantChanges.any { path -> path == prefix || path.startsWith(prefix) }) {
selected.addAll(impactedServices)
}
}
}
selected.addAll(retryServices)
echo "AUTO 基线: ${base};变更选中: ${selected.unique().join(',') ?: '无'};" +
"补跑失败服务: ${retryServices.join(',') ?: '无'}"
} }
} }
@@ -124,17 +289,21 @@ pipeline {
} }
selected = selected.unique() selected = selected.unique()
env.SELECTED_SERVICES = selected.join(',') env.SELECTED_SERVICES = selected.join(',')
env.TEST_BASE_COMMIT = previousSuccessfulTestCommit(currentBuild.previousBuild, selected)
env.NO_SERVICE_CHANGE = selected ? 'false' : 'true' env.NO_SERVICE_CHANGE = selected ? 'false' : 'true'
if (!selected) { if (!selected) {
currentBuild.description = env.DATABASE_CHANGE_DETECTED == 'true' currentBuild.description = params.BUILD_MODE == 'AUTO'
? '仅数据库迁移变更 | 需单独审核执行' ? "AUTO ${env.IMAGE_TAG} | RESULTS"
: '没有需要构建的业务服务' : (env.DATABASE_CHANGE_DETECTED == 'true'
? '仅数据库迁移变更 | 需单独审核执行'
: '没有需要构建的业务服务')
} else { } else {
env.SOURCE_COMMIT = sh(script: 'git rev-parse HEAD', returnStdout: true).trim()
env.IMAGE_TAG = sh(script: 'git rev-parse --short=12 HEAD', returnStdout: true).trim()
String databaseSuffix = env.DATABASE_CHANGE_DETECTED == 'true' ? ' | DB migration pending' : '' String databaseSuffix = env.DATABASE_CHANGE_DETECTED == 'true' ? ' | DB migration pending' : ''
currentBuild.description = "${env.SELECTED_SERVICES} | ${env.IMAGE_TAG}${databaseSuffix}" currentBuild.description = params.BUILD_MODE == 'AUTO'
? "AUTO ${env.IMAGE_TAG} | PENDING ${env.SELECTED_SERVICES}${databaseSuffix}"
: "${env.SELECTED_SERVICES} | ${env.IMAGE_TAG}${databaseSuffix}"
echo "本次独立调度服务: ${env.SELECTED_SERVICES}" echo "本次独立调度服务: ${env.SELECTED_SERVICES}"
} }
if (env.DATABASE_CHANGE_DETECTED == 'true') { if (env.DATABASE_CHANGE_DETECTED == 'true') {
@@ -144,25 +313,73 @@ pipeline {
} }
} }
stage('生产环境部署确认(仅 production 执行)') { stage('Gateway 路由静态门禁') {
when { when {
allOf { expression { env.NO_SERVICE_CHANGE != 'true' }
expression { params.DEPLOY && params.ENVIRONMENT == 'production' && env.NO_SERVICE_CHANGE != 'true' }
}
} }
agent { label 'java-builder' }
steps { steps {
input message: "确认将 ${env.SELECTED_SERVICES} 的镜像 ${env.IMAGE_TAG} 分别部署到生产环境?", ok: '确认部署' sh 'node --test scripts/gateway-route-contract.test.mjs'
} }
} }
stage('测试数据库初始化(可选)') { stage('数据库结构只读门禁') {
when { when {
allOf { expression { databaseSchemaContractEnabled }
expression { params.ENVIRONMENT == 'test' && params.INITIALIZE_TEST_DATABASE } }
agent { label 'java-builder' }
steps {
script {
String credentialId = "firstleg-${env.RESOLVED_ENVIRONMENT}-schema-readonly"
withCredentials([file(credentialsId: credentialId, variable: 'SCHEMA_CHECK_ENV_FILE')]) {
sh '''
set -eu
set -a
. "$SCHEMA_CHECK_ENV_FILE"
set +a
test "${SCHEMA_CHECK_READ_ONLY:-}" = "true"
node scripts/database-schema-contract.mjs --region BOTH
'''
}
} }
} }
}
stage('统一 Maven 测试门禁') {
when {
expression { env.NO_SERVICE_CHANGE != 'true' && !params.SKIP_TESTS }
}
agent { label 'java-builder' }
tools { maven 'maven-3.8.8' }
steps { steps {
sh 'sh deploy/scripts/initialize-test-databases.sh' script {
String selectedModules = env.SELECTED_SERVICES.split(',')
.collect { it.trim() }
.findAll { it }
.join(',')
echo "统一测试本次选中的服务及其公共依赖: ${selectedModules}"
String testPlan = "${pwd(tmp: true)}/ci-test-plan.json"
lock(resource: 'firstleg-test-maven') {
timeout(time: 50, unit: 'MINUTES') {
sh """
test \"\$(git rev-parse HEAD)\" = \"${env.SOURCE_COMMIT}\"
find . -type f -path '*/target/surefire-reports/TEST-*.xml' -delete
node deploy/scripts/ci-test-scope.mjs plan --modules '${selectedModules}' --base '${env.TEST_BASE_COMMIT ?: ''}' --mode '${env.RESOLVED_ENVIRONMENT == 'test' ? (params.TEST_SCOPE ?: 'AUTO') : 'MODULE'}' --plan '${testPlan}'
node deploy/scripts/ci-test-scope.mjs checks --plan '${testPlan}'
test_arguments=\"\$(node deploy/scripts/ci-test-scope.mjs args --plan '${testPlan}')\"
sh deploy/scripts/run-ci-maven.sh --settings deploy/maven/settings-ci.xml --batch-mode --errors -pl ${selectedModules} -am test \$test_arguments
node deploy/scripts/ci-test-scope.mjs verify --plan '${testPlan}'
"""
}
}
echo "统一测试门禁通过: ${env.SOURCE_COMMIT};下游单服务任务不再重复执行测试"
env.TEST_GATE_PASSED = 'true'
}
}
post {
always {
junit allowEmptyResults: true, testResults: '**/target/surefire-reports/TEST-*.xml'
}
} }
} }
@@ -183,14 +400,17 @@ pipeline {
parameters: [ parameters: [
string(name: 'SOURCE_COMMIT', value: env.SOURCE_COMMIT), string(name: 'SOURCE_COMMIT', value: env.SOURCE_COMMIT),
string(name: 'IMAGE_TAG', value: env.IMAGE_TAG), string(name: 'IMAGE_TAG', value: env.IMAGE_TAG),
string(name: 'ENVIRONMENT', value: params.ENVIRONMENT), string(name: 'ENVIRONMENT', value: env.RESOLVED_ENVIRONMENT),
booleanParam(name: 'DEPLOY', value: params.DEPLOY), booleanParam(name: 'DEPLOY', value: params.DEPLOY),
booleanParam(name: 'SKIP_TESTS', value: params.SKIP_TESTS), // 总控已完成统一测试,或用户已明确选择跳过;下游仅负责编译、打包和部署。
booleanParam(name: 'SKIP_TESTS', value: true),
string(name: 'IMAGE_REPOSITORY', value: env.RESOLVED_IMAGE_REPOSITORY), string(name: 'IMAGE_REPOSITORY', value: env.RESOLVED_IMAGE_REPOSITORY),
string(name: 'DOCKER_BASE_IMAGE', value: params.DOCKER_BASE_IMAGE) string(name: 'DOCKER_BASE_IMAGE', value: params.DOCKER_BASE_IMAGE)
] ]
) )
downstreamResults[service] = downstreamBuild.result ?: 'UNKNOWN' downstreamResults[service] = downstreamBuild.result ?: 'UNKNOWN'
downstreamBuildUrls[service] = downstreamBuild.absoluteUrl ?: ''
downstreamBuildNumbers[service] = downstreamBuild.number?.toString() ?: ''
echo "${service} 执行结果: ${downstreamResults[service]} (${downstreamBuild.absoluteUrl})" echo "${service} 执行结果: ${downstreamResults[service]} (${downstreamBuild.absoluteUrl})"
} }
} }
@@ -207,7 +427,12 @@ pipeline {
.collect { service, result -> "${service}=${result}" } .collect { service, result -> "${service}=${result}" }
String summary = downstreamResults.collect { service, result -> "${service}=${result}" }.join(', ') String summary = downstreamResults.collect { service, result -> "${service}=${result}" }.join(', ')
echo "单服务执行汇总: ${summary}" echo "单服务执行汇总: ${summary}"
currentBuild.description = "${env.IMAGE_TAG} | ${summary}" currentBuild.description = params.BUILD_MODE == 'AUTO'
? "AUTO ${env.IMAGE_TAG} | RESULTS ${summary}"
: "${env.IMAGE_TAG} | ${summary}"
if (env.TEST_GATE_PASSED == 'true') {
currentBuild.description += " | TESTS=PASSED ${env.SOURCE_COMMIT} ${env.SELECTED_SERVICES}"
}
if (failed) { if (failed) {
unstable("部分服务执行失败,但其他服务已独立完成:${failed.join(', ')}") unstable("部分服务执行失败,但其他服务已独立完成:${failed.join(', ')}")
} }
@@ -216,7 +441,4 @@ pipeline {
} }
} }
post {
always { cleanWs(deleteDirs: true, notFailBuild: true) }
}
} }
+188 -107
View File
@@ -1,6 +1,6 @@
def serviceModules = [ def serviceModules = [
'gateway', 'auth', 'iam', 'firstleg', 'finance', 'order', 'warehouse', 'file', 'print', 'gateway', 'auth', 'iam', 'firstleg', 'finance', 'order', 'warehouse', 'file', 'print',
'integration-service', 'event-bridge-service', 'audit-service' 'integration-service', 'lastleg-service', 'warehouse-overseas-service', 'audit-service'
] ]
def serviceJars = [ def serviceJars = [
@@ -14,17 +14,26 @@ def serviceJars = [
'file': 'file/target/file-service-1.0.0-SNAPSHOT.jar', 'file': 'file/target/file-service-1.0.0-SNAPSHOT.jar',
'print': 'print/target/print-service-1.0.0-SNAPSHOT.jar', 'print': 'print/target/print-service-1.0.0-SNAPSHOT.jar',
'integration-service': 'integration-service/target/integration-service-1.0.0-SNAPSHOT.jar', 'integration-service': 'integration-service/target/integration-service-1.0.0-SNAPSHOT.jar',
'event-bridge-service': 'event-bridge-service/target/event-bridge-service-1.0.0-SNAPSHOT.jar', 'lastleg-service': 'lastleg-service/target/lastleg-service-1.0.0-SNAPSHOT.jar',
'warehouse-overseas-service': 'warehouse-overseas-service/target/warehouse-overseas-service-1.0.0-SNAPSHOT.jar',
'audit-service': 'audit-service/target/audit-service-1.0.0-SNAPSHOT.jar' 'audit-service': 'audit-service/target/audit-service-1.0.0-SNAPSHOT.jar'
] ]
pipeline { def fixedEnvironmentForServiceJob(String jobBaseName) {
// 每个 Jenkins 子 Job 使用自己的 workspace,因此不同服务可以并行执行 mvn clean。 if (jobBaseName?.startsWith('baiyue-firstleg-prod-')) return 'production'
agent any if (jobBaseName?.startsWith('baiyue-firstleg-') && !jobBaseName.startsWith('baiyue-firstleg-dev-') && !jobBaseName.startsWith('baiyue-firstleg-prod-')) {
return 'test'
tools {
maven 'maven-3.8.8'
} }
return null
}
def environmentChoicesForServiceJob(String jobBaseName) {
return [fixedEnvironmentForServiceJob(jobBaseName) ?: 'test']
}
pipeline {
// 构建在任意 java-builder 节点执行,部署再回到目标环境节点。
agent none
options { options {
skipDefaultCheckout(true) skipDefaultCheckout(true)
@@ -36,117 +45,153 @@ pipeline {
parameters { parameters {
string(name: 'SOURCE_COMMIT', defaultValue: '', description: '必须构建的 Git commit;通常由总控 Job 传入') string(name: 'SOURCE_COMMIT', defaultValue: '', description: '必须构建的 Git commit;通常由总控 Job 传入')
string(name: 'IMAGE_TAG', defaultValue: '', description: '不可变镜像标签;通常使用 Git commit 前 12 位') string(name: 'IMAGE_TAG', defaultValue: '', description: '不可变镜像标签;通常使用 Git commit 前 12 位')
choice(name: 'ENVIRONMENT', choices: ['test', 'staging', 'production'], description: '部署环境') choice(name: 'ENVIRONMENT', choices: environmentChoicesForServiceJob(env.JOB_BASE_NAME), description: '由当前单服务 Job 固定的部署环境')
booleanParam(name: 'ALLOW_SAME_CONTRACT_ROLLBACK', defaultValue: false, description: '仅确认相同协议与Schema时允许回滚;架构切换保持关闭')
booleanParam(name: 'DEPLOY', defaultValue: false, description: '构建成功后是否部署本服务') booleanParam(name: 'DEPLOY', defaultValue: false, description: '构建成功后是否部署本服务')
booleanParam(name: 'SKIP_TESTS', defaultValue: false, description: '是否跳过测试') booleanParam(name: 'SKIP_TESTS', defaultValue: false, description: '是否跳过测试')
string(name: 'IMAGE_REPOSITORY', defaultValue: 'baiyue-firstleg-test', description: '镜像仓库基础路径,不包含服务名和标签') // 独立构建沿用本 Job 配置,总控调度时保留其明确传入的地址。
string(name: 'DOCKER_BASE_IMAGE', defaultValue: 'docker.m.daocloud.io/library/eclipse-temurin:17-jre-alpine', description: 'Java 17 基础镜像') string(name: 'IMAGE_REPOSITORY', defaultValue: params.IMAGE_REPOSITORY ?: '', description: '沿用当前 Job 已配置的镜像仓库基础路径,不包含服务名和标签;首次配置或更换仓库时填写')
string(name: 'DOCKER_BASE_IMAGE', defaultValue: 'eclipse-temurin:17-jre-alpine', description: 'Java 17 基础镜像')
} }
environment { environment {
REGISTRY_CREDENTIALS = 'gitea-test-token' REGISTRY_CREDENTIALS = 'gitea-registry-token'
MAVEN_SETTINGS = 'deploy/maven/settings-cn.xml' MAVEN_SETTINGS = 'deploy/maven/settings-ci.xml'
MAVEN_OPTS = '-Dfile.encoding=UTF-8 -Dmaven.wagon.http.retryHandler.count=3'
} }
stages { stages {
stage('校验参数并检出指定提交') { stage('构建并发布单服务') {
steps { agent { label 'java-builder' }
checkout scm tools { maven 'maven-3.8.8' }
stages {
stage('校验参数并检出指定提交') {
steps {
checkout scm
script { script {
String service = serviceModules.find { candidate -> String service = serviceModules.find { candidate ->
env.JOB_BASE_NAME?.endsWith("-${candidate}") env.JOB_BASE_NAME?.endsWith("-${candidate}")
} }
String sourceCommit = params.SOURCE_COMMIT?.trim() String sourceCommit = params.SOURCE_COMMIT?.trim()
String imageTag = params.IMAGE_TAG?.trim() String imageTag = params.IMAGE_TAG?.trim()
String imageRepository = params.IMAGE_REPOSITORY?.trim() String imageRepository = params.IMAGE_REPOSITORY?.trim()
String resolvedEnvironment = fixedEnvironmentForServiceJob(env.JOB_BASE_NAME)
if (!sourceCommit) { if (!sourceCommit) {
sourceCommit = sh(script: 'git rev-parse HEAD', returnStdout: true).trim() sourceCommit = sh(script: 'git rev-parse HEAD', returnStdout: true).trim()
} }
if (!imageTag) { if (!imageTag) {
imageTag = sh(script: 'git rev-parse --short=12 HEAD', returnStdout: true).trim() imageTag = sh(script: 'git rev-parse --short=12 HEAD', returnStdout: true).trim()
} }
if (!serviceModules.contains(service)) { if (!serviceModules.contains(service)) {
error("无法从 Job 名称 ${env.JOB_BASE_NAME} 识别服务;名称必须以以下服务之一结尾: ${serviceModules.join(', ')}") error("无法从 Job 名称 ${env.JOB_BASE_NAME} 识别服务;名称必须以以下服务之一结尾: ${serviceModules.join(', ')}")
} }
if (!sourceCommit || !(sourceCommit ==~ /(?i)[0-9a-f]{7,40}/)) { if (!resolvedEnvironment) {
error('SOURCE_COMMIT 必须是 7 到 40 位十六进制 Git commit') error("未配置单服务 Job ${env.JOB_BASE_NAME} 的固定部署环境,拒绝继续")
} }
if (!imageTag || !(imageTag ==~ /[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}/)) { if (params.ENVIRONMENT != resolvedEnvironment) {
error('IMAGE_TAG 不是合法的 Docker 镜像标签') error('部署环境必须与当前单服务 Job 固定映射一致')
} }
if (!imageRepository || imageRepository.contains('://')) { if (!sourceCommit || !(sourceCommit ==~ /(?i)[0-9a-f]{7,40}/)) {
error('IMAGE_REPOSITORY 不能为空且不能包含 http:// 或 https://') error('SOURCE_COMMIT 必须是 7 到 40 位十六进制 Git commit')
} }
if (params.ENVIRONMENT != 'test' && !imageRepository.contains('/')) { if (!imageTag || !(imageTag ==~ /[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}/)) {
error('预发布/生产环境 IMAGE_REPOSITORY 必须使用 host[:port]/owner/image 格式') error('IMAGE_TAG 不是合法的 Docker 镜像标签')
} }
if (!imageRepository || imageRepository.contains('://')) {
error('IMAGE_REPOSITORY 不能为空且不能包含协议前缀')
}
if (imageRepository.tokenize('/')[0] ==~ /(?i)(?:[a-z0-9-]+\.)*example\.(?:com|net|org)(?::[0-9]+)?/) {
error('IMAGE_REPOSITORY 不能使用 example 示例域名,请填写已核实的镜像仓库地址')
}
if (!imageRepository.contains('/')) {
error('IMAGE_REPOSITORY 必须使用 host[:port]/owner/image 格式')
}
env.RESOLVED_SERVICE = service env.RESOLVED_SERVICE = service
env.RESOLVED_SOURCE_COMMIT = sourceCommit env.RESOLVED_ENVIRONMENT = resolvedEnvironment
env.RESOLVED_IMAGE_TAG = imageTag env.RESOLVED_SOURCE_COMMIT = sourceCommit
env.RESOLVED_IMAGE_REPOSITORY = imageRepository env.RESOLVED_IMAGE_TAG = imageTag
env.RESOLVED_IMAGE_REPOSITORY = imageRepository
}
sh '''
if ! git cat-file -e "${RESOLVED_SOURCE_COMMIT}^{commit}" 2>/dev/null; then
git fetch --no-tags origin
fi
git cat-file -e "${RESOLVED_SOURCE_COMMIT}^{commit}"
git checkout --detach "${RESOLVED_SOURCE_COMMIT}"
test "$(git rev-parse HEAD)" = "$(git rev-parse "${RESOLVED_SOURCE_COMMIT}^{commit}")"
if [ "$RESOLVED_ENVIRONMENT" = "production" ]; then
git fetch --no-tags origin +refs/heads/main:refs/remotes/origin/main
git merge-base --is-ancestor "$RESOLVED_SOURCE_COMMIT" origin/main || {
echo "灰度发布提交必须已经合并到 main" >&2
exit 1
}
fi
'''
script {
currentBuild.description = "${env.RESOLVED_SERVICE} | ${env.RESOLVED_IMAGE_TAG} | ${env.RESOLVED_ENVIRONMENT} | ${env.NODE_NAME}"
echo "独立构建服务: ${env.RESOLVED_SERVICE},提交: ${env.RESOLVED_SOURCE_COMMIT},节点: ${env.NODE_NAME}"
}
}
} }
sh ''' stage('Maven 测试与打包') {
if ! git cat-file -e "${RESOLVED_SOURCE_COMMIT}^{commit}" 2>/dev/null; then steps {
git fetch --no-tags origin script {
fi boolean coordinatorPinnedCommit = params.SOURCE_COMMIT?.trim() as boolean
git cat-file -e "${RESOLVED_SOURCE_COMMIT}^{commit}"
git checkout --detach "${RESOLVED_SOURCE_COMMIT}"
test "$(git rev-parse HEAD)" = "$(git rev-parse "${RESOLVED_SOURCE_COMMIT}^{commit}")"
'''
script { boolean skipTestExecution = params.SKIP_TESTS
currentBuild.description = "${env.RESOLVED_SERVICE} | ${env.RESOLVED_IMAGE_TAG} | ${params.ENVIRONMENT}" String testOption = skipTestExecution ? '-DskipTests' : ''
echo "独立构建服务: ${env.RESOLVED_SERVICE},提交: ${env.RESOLVED_SOURCE_COMMIT}" if (params.SKIP_TESTS && coordinatorPinnedCommit) {
} echo "总控已处理提交 ${env.RESOLVED_SOURCE_COMMIT} 的测试门禁,本单服务仅编译测试源码并打包"
} }
} lock(resource: 'firstleg-test-maven') {
timeout(time: 50, unit: 'MINUTES') {
stage('Maven 测试与打包') { sh "sh deploy/scripts/run-ci-maven.sh --settings ${env.MAVEN_SETTINGS} --batch-mode --errors -pl ${env.RESOLVED_SERVICE} -am clean package ${testOption}"
steps { }
script { }
if (params.ENVIRONMENT == 'production' && params.SKIP_TESTS) { }
error('生产环境禁止跳过测试')
} }
boolean skipTestExecution = params.ENVIRONMENT == 'test' || params.SKIP_TESTS post {
String testOption = skipTestExecution ? '-DskipTests' : '' always {
if (params.ENVIRONMENT == 'test') { junit allowEmptyResults: true, testResults: '**/target/surefire-reports/TEST-*.xml'
echo '测试环境快速部署:编译主代码和测试代码,但不重复执行单元测试;完整测试由提交门禁和预发布执行' }
} }
sh "mvn --settings ${env.MAVEN_SETTINGS} --batch-mode --errors -pl ${env.RESOLVED_SERVICE} -am clean package ${testOption}"
} }
}
post {
always {
junit allowEmptyResults: true, testResults: '**/target/surefire-reports/TEST-*.xml'
}
}
}
stage('构建并发布单服务镜像') { stage('构建并发布单服务镜像') {
steps { steps {
script { script {
String image = "${env.RESOLVED_IMAGE_REPOSITORY}/${env.RESOLVED_SERVICE}:${env.RESOLVED_IMAGE_TAG}" String image = "${env.RESOLVED_IMAGE_REPOSITORY}/${env.RESOLVED_SERVICE}:${env.RESOLVED_IMAGE_TAG}"
String contextDir = ".docker-context/${env.RESOLVED_SERVICE}" String contextDir = ".docker-context/${env.RESOLVED_SERVICE}"
String jar = serviceJars[env.RESOLVED_SERVICE] String jar = serviceJars[env.RESOLVED_SERVICE]
sh "mkdir -p '${contextDir}' && cp -f '${jar}' '${contextDir}/app.jar'" String registryHost = env.RESOLVED_IMAGE_REPOSITORY.tokenize('/')[0]
sh "docker build -f deploy/docker/Dockerfile --build-arg BASE_IMAGE=${params.DOCKER_BASE_IMAGE} --build-arg JAR_FILE=app.jar -t ${image} '${contextDir}'" String dockerConfigDir = "${pwd(tmp: true)}/docker-auth"
sh "mkdir -p '${contextDir}' && cp -f '${jar}' '${contextDir}/app.jar'"
if (params.ENVIRONMENT == 'test') { echo "推送共享镜像: ${image}"
echo "测试镜像已保存在当前 Docker 节点:${image}" withEnv(["DOCKER_CONFIG=${dockerConfigDir}"]) {
} else { sh 'mkdir -p "$DOCKER_CONFIG"'
String registryHost = env.RESOLVED_IMAGE_REPOSITORY.tokenize('/')[0] withCredentials([usernamePassword(credentialsId: env.REGISTRY_CREDENTIALS, usernameVariable: 'REGISTRY_USER', passwordVariable: 'REGISTRY_PASSWORD')]) {
withCredentials([usernamePassword(credentialsId: env.REGISTRY_CREDENTIALS, usernameVariable: 'REGISTRY_USER', passwordVariable: 'REGISTRY_PASSWORD')]) { try {
try { sh 'printf %s "$REGISTRY_PASSWORD" | docker login -u "$REGISTRY_USER" --password-stdin "' + registryHost + '"'
sh 'printf %s "$REGISTRY_PASSWORD" | docker login -u "$REGISTRY_USER" --password-stdin "' + registryHost + '"' sh "docker build -f deploy/docker/Dockerfile --build-arg BASE_IMAGE=${params.DOCKER_BASE_IMAGE} --build-arg JAR_FILE=app.jar -t ${image} '${contextDir}'"
sh "docker push ${image}" sh "docker push ${image}"
} finally { } finally {
sh "docker logout ${registryHost} || true" sh "docker logout ${registryHost} || true"
sh 'rm -rf "$DOCKER_CONFIG"'
}
}
}
if (params.DEPLOY) {
stash name: 'deployment-files', includes: 'deploy/**', useDefaultExcludes: true
} }
} }
} }
@@ -154,25 +199,61 @@ pipeline {
} }
} }
stage('部署单个服务') { stage('部署 TEST') {
when { expression { params.DEPLOY } } when { expression { params.DEPLOY && env.RESOLVED_ENVIRONMENT == 'test' } }
agent none
steps { steps {
script { script {
String credentialId = "firstleg-${params.ENVIRONMENT}-env" lock(resource: "firstleg-test-${env.RESOLVED_SERVICE}-deploy", inversePrecedence: true) {
withCredentials([file(credentialsId: credentialId, variable: 'DEPLOY_ENV_FILE')]) { node('java-test') {
sh "sh deploy/scripts/deploy-services.sh '${params.ENVIRONMENT}' '${env.RESOLVED_IMAGE_REPOSITORY}' '${env.RESOLVED_IMAGE_TAG}' '${env.RESOLVED_SERVICE}' \"\$DEPLOY_ENV_FILE\"" deleteDir()
unstash 'deployment-files'
withCredentials([
file(credentialsId: 'firstleg-test-env', variable: 'DEPLOY_ENV_FILE'),
usernamePassword(credentialsId: env.REGISTRY_CREDENTIALS, usernameVariable: 'REGISTRY_USER', passwordVariable: 'REGISTRY_PASSWORD')
]) {
withEnv(['TARGET_IS_LOCAL=true', "ALLOW_SAME_CONTRACT_ROLLBACK=${params.ALLOW_SAME_CONTRACT_ROLLBACK}"]) {
sh 'sh deploy/scripts/deploy-built-service.sh test "$RESOLVED_IMAGE_REPOSITORY" "$RESOLVED_IMAGE_TAG" "$RESOLVED_SERVICE" "$DEPLOY_ENV_FILE"'
}
}
}
} }
} }
} }
} }
stage('部署临时灰度') {
when { expression { params.DEPLOY && env.RESOLVED_ENVIRONMENT == 'production' } }
agent none
steps {
script {
lock(resource: "firstleg-production-${env.RESOLVED_SERVICE}-deploy", inversePrecedence: true) {
node('java-builder') {
deleteDir()
unstash 'deployment-files'
withCredentials([
file(credentialsId: 'firstleg-production-env', variable: 'DEPLOY_ENV_FILE'),
usernamePassword(credentialsId: 'gitea-production-pull-token', usernameVariable: 'REGISTRY_USER', passwordVariable: 'REGISTRY_PASSWORD'),
sshUserPrivateKey(credentialsId: 'firstleg-production-ssh', keyFileVariable: 'TARGET_SSH_KEY', usernameVariable: 'TARGET_SSH_USER'),
string(credentialsId: 'firstleg-production-host', variable: 'TARGET_SSH_HOST'),
file(credentialsId: 'firstleg-production-known-hosts', variable: 'TARGET_SSH_KNOWN_HOSTS')
]) {
withEnv(['TARGET_IS_LOCAL=false', 'TARGET_SSH_TARGET=' + env.TARGET_SSH_USER + '@' + env.TARGET_SSH_HOST,
"ALLOW_SAME_CONTRACT_ROLLBACK=${params.ALLOW_SAME_CONTRACT_ROLLBACK}"]) {
sh 'sh deploy/scripts/deploy-built-service.sh production "$RESOLVED_IMAGE_REPOSITORY" "$RESOLVED_IMAGE_TAG" "$RESOLVED_SERVICE" "$DEPLOY_ENV_FILE"'
}
}
}
}
}
}
}
} }
post { post {
success { success {
echo "${env.RESOLVED_SERVICE} 已独立完成构建${params.DEPLOY ? '和部署' : ''}" echo "${env.RESOLVED_SERVICE} 已独立完成构建${params.DEPLOY ? '和部署' : ''}"
} }
always {
cleanWs(deleteDirs: true, notFailBuild: true)
}
} }
} }
+36 -10
View File
@@ -1,37 +1,61 @@
# 柏悦头程平台 # 柏悦头程平台
本仓库承载国内头程业务,负责统一登录、客户与企业成员、角色、菜单、按钮权限、订单、国内仓储、头程履约以及国内公共能力。尾程业务不进入本仓库,通过版本化跨区域事件与尾程仓库协作。 本仓库承载头程与海外仓后端。当前只有一个测试环境,目标为所有服务同机部署,统一 Gateway、Auth 会话及 Nacos 命名空间,业务服务继续按职责拥有独立 Schema。部署与切换见[测试环境单机部署与配置契约](../docs/测试环境单机部署与配置契约.md),路由、认证和事件投递见[统一认证与服务通信契约](../docs/统一认证与服务通信契约.md)。
根 POM 与发布清单已收敛为 20 个模块、13 个服务;本地配置变化尚未经过真实 TEST 部署验收。`gateway-overseas/` 与 `event-bridge-service/` 为退役待清理源码,已不参与当前聚合构建和发布。
> 当前主体模型:业务单租户,产品只包含“平台—客户”两级。现有 `tenant_id > 0`、`tenantId` 和 `Tenant*` 是历史兼容命名,当前承载客户归属与隔离;`tenant_id = 0` 是平台账号、平台 RBAC 和平台公共主数据的技术作用域,不是可管理租户。平台公共仓库、往来单位、快递公司、托盘、库位等归属 `0`,客户订单、文件和费用归属客户 ID。未经过独立迁移设计,不得新增真实租户层或把现有客户隔离字段改义为上级租户。 > 当前主体模型:业务单租户,产品只包含“平台—客户”两级。现有 `tenant_id > 0`、`tenantId` 和 `Tenant*` 是历史兼容命名,当前承载客户归属与隔离;`tenant_id = 0` 是平台账号、平台 RBAC 和平台公共主数据的技术作用域,不是可管理租户。平台公共仓库、往来单位、快递公司、托盘、库位等归属 `0`,客户订单、文件和费用归属客户 ID。未经过独立迁移设计,不得新增真实租户层或把现有客户隔离字段改义为上级租户。
## 模块 ## 模块
- `gateway`:四端统一 API 入口。
- `finance`:财务账单、费用、充值、授信与资金核销。
- `auth`:统一登录、令牌签发与刷新。 - `auth`:统一登录、令牌签发与刷新。
- `iam`:客户主档、用户、企业成员、角色、菜单、按钮权限和数据范围;部分路径、表和类保留 `tenant` 历史命名。 - `iam`:客户主档、用户、企业成员、角色、菜单、按钮权限和数据范围;部分路径、表和类保留 `tenant` 历史命名。
- `order`:订单主数据及订单状态编排。 - `order`:订单主数据及订单状态编排。
- `firstleg`:头程揽收、入仓、装载、发运和交接。 - `firstleg`:头程揽收、入仓、装载、发运和交接。
- `warehouse`:国内仓储能力。 - `warehouse`:国内仓储能力。
- `file`、`print`、`audit-service`、`integration-service`:国内公共业务能力;Print 调用方接入见 `print/README.md`。 - `file`、`print`、`audit-service`、`integration-service`:国内公共业务能力;Print 调用方接入见 `print/README.md`。
- `event-bridge-service`:跨区域事件接收幂等和投递记录基础骨架;真实跨区发送、重试调度和死信补投仍需完成。 - `lastleg-service`、`warehouse-overseas-service`:海外尾程/入仓/出库/派送和海外仓储资源,统一会话鉴权。
- `common-*`、`common-event-contract`:由本仓维护并以 `com.baiyue.shared` 发布的公共制品。 - `common-*`、`common-event-contract`:由本仓维护并以 `com.baiyue.shared` 发布的公共制品。
- `platform-bom`:发布 `com.baiyue.shared:baiyue-platform-bom:1.0.0-SNAPSHOT`,尾程仓只通过固定版本引用。 - `platform-bom`:统一管理仓库内公共依赖版本。
## 本地构建 ## 本地构建
```powershell ```powershell
mvn -DskipTests validate # 日常按受影响模块检查;跨服务变更显式列入消费者。
mvn test .\scripts\backend-check.ps1 -Modules order -Mode Compile
.\scripts\backend-check.ps1 -Modules order
# 根构建/拓扑变化等全量触发条件见 AGENTS.md。
.\scripts\local-check.ps1 -Command @('node', '--test', 'scripts/java-topology-contract.test.mjs')
.\scripts\backend-full-regression.ps1
``` ```
2026-07-30 一期前端联调缺口已完成真实回归:19 个 Reactor 项目构建成功,75 项单元及组件测试、68 项认证与 IAM 真实 API、37 项订单与仓库真实 API 全部通过。详细证据见 `docs/phase1-business-maturity-regression-report.md`。 开发中可通过 `backend-check.ps1 -Mode Focused -Modules <模块> -Tests <精确测试类名>` 快速反馈;参数、DryRun 和结果边界见 [后端分层检查](scripts/README.md#后端分层检查)。单服务修改收尾执行模块回归,不默认执行上面的全仓命令。
本地一期完整联调启动顺序为:IAM、Auth、Firstleg、Order、Warehouse、File、Print,最后启动 Gateway。往来单位不是独立服务,由 `warehouse-service` 承载并保存到 `baiyue_warehouse.wh_partner`。 GitLab 在独立 Node 镜像中自动执行拓扑合同,随后才允许服务制品打包。Maven `verify` 会为执行过测试且产生覆盖率数据的模块生成 `target/site/jacoco/index.html` 与 `jacoco.xml`;当前只生成报告,不设置覆盖率阈值,也不改变测试选择。Jenkins 的 `java-builder` 尚未在仓库中声明 Node 运行时,因此暂不重复执行该 Node 合同,以 GitLab 门禁和上述本地命令为准。
国内 Nacos 配置位于 `config/nacos/cn`。数据库、Redis、Nacos 和 RocketMQ 的生产密码必须通过环境变量或配置中心密文注入,禁止提交明文密码。 2026-07-30 订单与客户前端联调缺口已完成真实回归:19 个 Reactor 项目构建成功,75 项单元及组件测试、68 项认证与 IAM 真实 API、37 项订单与仓库真实 API 全部通过。详细证据见 `docs/phase1-business-maturity-regression-report.md`。
唯一发布配置为 `deploy/docker-compose.yml`、`deploy/env/.env.test.example` 和 `config/nacos/test/`;所有保留服务共用实际 Namespace ID、Auth/Redis 会话和单机内部网络,各服务 Schema 保留。往来单位由 `warehouse-service` 承载并保存到 `baiyue_warehouse.wh_partner`。发布、健康检查和失败恢复见 `deploy/TEST单机部署执行清单.md`。真实凭据不得提交;本地编译不代表另有开发环境。
## 容器日志
所有服务只向标准输出和标准错误写日志,不在容器内生成应用日志文件,也不把应用日志目录挂载到宿主机。统一业务及基础设施 Compose 默认使用宝塔可见的 Docker `json-file` 日志驱动,并设置强制轮转;仍可通过 `docker logs` 排查。文件服务的 `/data/files` 是业务文件目录,不属于日志目录,必须继续持久化。
单容器默认保留 `50m × 3`,可在 TEST 部署变量中覆盖:
| 环境 | `DOCKER_LOG_DRIVER` | `DOCKER_LOG_MAX_SIZE` | `DOCKER_LOG_MAX_FILE` | 长期留存 |
|---|---|---:|---:|---|
| 测试 | `json-file` | `50m` | `3` | 不要求 |
当前优先保证宝塔可观测性,因此不启用 `json-file` 压缩或非阻塞投递,避免面板无法读取轮转文件或在日志压力下静默丢弃。接入集中日志系统后,可通过 `DOCKER_LOG_DRIVER` 切换驱动,并由集中系统承担长期检索与留存。
调整日志策略后必须重新创建容器;只重启既有容器不会改变其日志驱动。历史 `/data/logs/baiyue-firstleg` 文件和旧容器日志不自动删除,确认无需保留后再单独清理。
## 网关跨域配置 ## 网关跨域配置
本地默认允许商户端和后台端从 `localhost/127.0.0.1` 的 `5173/5174` 端口访问网关。部署到测试或生产环境时,必须通过 `CORS_ALLOWED_ORIGINS` 配置两个前端的明确地址,多个地址用英文逗号分隔,例如: 本地默认允许商户端和后台端从 `localhost/127.0.0.1` 的 `5173/5174` 端口访问网关。部署到 TEST 时,必须通过 `CORS_ALLOWED_ORIGINS` 配置两个前端的明确地址,多个地址用英文逗号分隔,例如:
```text ```text
CORS_ALLOWED_ORIGINS=https://merchant.example.com,https://admin.example.com CORS_ALLOWED_ORIGINS=https://merchant.example.com,https://admin.example.com
@@ -41,4 +65,6 @@ CORS_ALLOWED_ORIGINS=https://merchant.example.com,https://admin.example.com
## 仓库边界 ## 仓库边界
仓库边界、服务调用和拆分规则见 `docs/头程项目目录结构与服务说明.md` 的“项目定位”“主要调用关系”和“开发和拆分约束”。本仓不得引入尾程源码,不允许跨区域共享数据库、Redis 或 Nacos,也不允许通过公网直接调用对方内部服务。 仓库结构入口见 `docs/头程项目目录结构与服务说明.md`。服务保持数据归属、禁止跨库直接查询其他服务业务表;同机可共用 MySQL 实例、Redis 和 Nacos。内部调用不通过公网,部署及通信规则以新的两份主契约为准。
前端 phase4 定向离线检查入口为 `apps/admin-web/scripts/phase4-test-frontend-acceptance.mjs`(工作区相对路径),由 Admin 的 `pnpm test:phase4:test` 使用 `vitest.phase4-test.config.ts` 执行;不替代真实HTTP、页面或PDA验收。
+6 -1
View File
@@ -1,7 +1,12 @@
# Audit Service 模块指引 # Audit Service 模块指引
## 数据库访问与中文注释约束
- 强制继承 `../AGENTS.md` 的“数据库访问与中文代码注释”全部规则;该节是分层、SQL 执行和方法/核心逻辑中文注释的唯一维护处,本模块不另设例外。
## 职责边界 ## 职责边界
- Mapper 只由 `BaiyueAuditApplication.java` 对 `com.baiyue.audit` 根包按 `@Mapper` 过滤扫描;新增或移动 Mapper 后从后端根运行 `node scripts/mapper-scan-contract.mjs`,不得增加子域扫描配置。
- 负责接收 `common-audit` 产生的审计事件并持久化到 `baiyue_audit`。 - 负责接收 `common-audit` 产生的审计事件并持久化到 `baiyue_audit`。
- 审计注解、切面、脱敏和发送策略在 `common-audit/`;本服务不复制业务服务的审计采集逻辑。 - 审计注解、切面、脱敏和发送策略在 `common-audit/`;本服务不复制业务服务的审计采集逻辑。
@@ -16,7 +21,7 @@
| 入库编排 | `service/AuditIngestService.java` | | 入库编排 | `service/AuditIngestService.java` |
| 持久化 | `entity/AuditLogEntity.java`、`mapper/AuditLogMapper.java`、`repository/AuditEventRepository.java` | | 持久化 | `entity/AuditLogEntity.java`、`mapper/AuditLogMapper.java`、`repository/AuditEventRepository.java` |
| 表结构 | `src/main/resources/db/audit.sql` | | 表结构 | `src/main/resources/db/audit.sql` |
| 本地/Nacos 配置 | `src/main/resources/application.yml`、`../config/nacos/{local,cn}/audit-service.yml` | | 本地/Nacos 配置 | `src/main/resources/application.yml`、`../config/nacos/test/audit-service.yml` |
## 修改与验证 ## 修改与验证
+4
View File
@@ -53,6 +53,10 @@
<groupId>com.alibaba.cloud</groupId> <groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId> <artifactId>spring-boot-starter-test</artifactId>
@@ -1,15 +1,17 @@
package com.baiyue.audit; package com.baiyue.audit;
import com.baiyue.common.core.mybatis.MybatisPlusConfiguration;
import org.apache.ibatis.annotations.Mapper;
import org.mybatis.spring.annotation.MapperScan; import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.context.annotation.Import;
/** /**
* 审计微服务启动入口,初始化 Spring Boot 应用并扫描审计日志 Mapper。 * 审计微服务启动入口,初始化 Spring Boot 应用并扫描审计日志 Mapper。
*/ */
@MapperScan("com.baiyue.audit.mapper") @MapperScan(basePackages = "com.baiyue.audit", annotationClass = Mapper.class)
@EnableScheduling @Import(MybatisPlusConfiguration.class)
@SpringBootApplication @SpringBootApplication
public class BaiyueAuditApplication { public class BaiyueAuditApplication {
@@ -24,6 +24,7 @@ public final class AuditLogResponses {
String username, String username,
String appCode, String appCode,
String clientType, String clientType,
String clientIp,
String requestId, String requestId,
String traceId, String traceId,
String serviceName, String serviceName,
@@ -1,10 +1,13 @@
package com.baiyue.audit.mapper; package com.baiyue.audit.mapper;
import org.apache.ibatis.annotations.Mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baiyue.audit.entity.AuditLogEntity; import com.baiyue.audit.entity.AuditLogEntity;
/** /**
* 审计日志 MyBatis-Plus Mapper,提供 {@code sys_audit_log} 表的单表写入和查询能力。 * 审计日志 MyBatis-Plus Mapper,提供 {@code sys_audit_log} 表的单表写入和查询能力。
*/ */
@Mapper
public interface AuditLogMapper extends BaseMapper<AuditLogEntity> { public interface AuditLogMapper extends BaseMapper<AuditLogEntity> {
} }
@@ -41,7 +41,7 @@ public class AuditLogQueryService {
return new AuditLogResponses.ListItem( return new AuditLogResponses.ListItem(
entity.getId(), entity.getEventId(), entity.getEventType(), entity.getResourceType(), entity.getResourceId(), entity.getId(), entity.getEventId(), entity.getEventType(), entity.getResourceType(), entity.getResourceId(),
entity.getDescription(), entity.getTenantId(), entity.getOperatorTenantId(), entity.getUserId(), entity.getDescription(), entity.getTenantId(), entity.getOperatorTenantId(), entity.getUserId(),
entity.getUsername(), entity.getAppCode(), entity.getClientType(), entity.getRequestId(), entity.getUsername(), entity.getAppCode(), entity.getClientType(), entity.getClientIp(), entity.getRequestId(),
entity.getTraceId(), entity.getServiceName(), entity.getSuccess(), entity.getErrorCode(), entity.getTraceId(), entity.getServiceName(), entity.getSuccess(), entity.getErrorCode(),
entity.getErrorMessage(), entity.getOccurredAt(), entity.getCreatedAt()); entity.getErrorMessage(), entity.getOccurredAt(), entity.getCreatedAt());
} }
@@ -1,6 +0,0 @@
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: ${AUDIT_DB_URL:jdbc:mysql://127.0.0.1:3306/baiyue_audit?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true}
username: ${AUDIT_DB_USERNAME:root}
password: ${AUDIT_DB_PASSWORD:123456}
@@ -1,6 +0,0 @@
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: ${AUDIT_TEST_DB_URL:jdbc:mysql://${TEST_DB_HOST:8.163.30.128}:${TEST_DB_PORT:3306}/baiyue_audit?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true&rewriteBatchedStatements=true}
username: ${AUDIT_TEST_DB_USERNAME:${TEST_DB_USERNAME:root}}
password: ${AUDIT_TEST_DB_PASSWORD:${TEST_DB_PASSWORD}}
@@ -3,27 +3,27 @@ server:
spring: spring:
profiles: profiles:
active: ${SPRING_PROFILES_ACTIVE:dev} active: ${SPRING_PROFILES_ACTIVE:test}
application: application:
name: audit-service name: audit-service
config: config:
import: import:
- optional:nacos:baiyue-common.yml?group=BAIYUE&refreshEnabled=true - nacos:baiyue-common.yml?group=${NACOS_GROUP:BAIYUE}&refreshEnabled=true
- optional:nacos:${spring.application.name}.yml?group=BAIYUE&refreshEnabled=true - nacos:${spring.application.name}.yml?group=${NACOS_GROUP:BAIYUE}&refreshEnabled=true
cloud: cloud:
nacos: nacos:
server-addr: ${NACOS_SERVER_ADDR:127.0.0.1:8848} server-addr: ${NACOS_SERVER_ADDR}
username: ${NACOS_USERNAME:nacos} username: ${NACOS_USERNAME}
password: ${NACOS_PASSWORD:nacos} password: ${NACOS_PASSWORD}
discovery: discovery:
server-addr: ${NACOS_SERVER_ADDR:127.0.0.1:8848} server-addr: ${NACOS_SERVER_ADDR}
enabled: ${NACOS_DISCOVERY_ENABLED:true} enabled: ${NACOS_DISCOVERY_ENABLED:true}
register-enabled: ${NACOS_REGISTER_ENABLED:true} register-enabled: ${NACOS_REGISTER_ENABLED:true}
group: ${NACOS_GROUP:BAIYUE} group: ${NACOS_GROUP:BAIYUE}
namespace: ${NACOS_NAMESPACE:public} namespace: ${NACOS_NAMESPACE}
config: config:
server-addr: ${NACOS_SERVER_ADDR:127.0.0.1:8848} server-addr: ${NACOS_SERVER_ADDR}
namespace: ${NACOS_NAMESPACE:public} namespace: ${NACOS_NAMESPACE}
baiyue.audit.enabled: false baiyue.audit.enabled: false
baiyue: baiyue:
@@ -40,14 +40,3 @@ baiyue:
rocketmq: rocketmq:
name-server: ${ROCKETMQ_NAME_SERVER:127.0.0.1:9876} name-server: ${ROCKETMQ_NAME_SERVER:127.0.0.1:9876}
management.endpoints.web.exposure.include: health,info,prometheus management.endpoints.web.exposure.include: health,info,prometheus
logging:
file:
name: "${LOGGING_FILE_NAME:${LOG_PATH:./logs}/${spring.application.name}.log}"
logback:
rollingpolicy:
file-name-pattern: "${LOGGING_ROLLING_FILE_NAME_PATTERN:${LOG_PATH:./logs}/${spring.application.name}.%d{yyyy-MM-dd}.%i.log.gz}"
max-file-size: "${LOGGING_ROLLING_MAX_FILE_SIZE:100MB}"
max-history: "${LOGGING_ROLLING_MAX_HISTORY:7}"
total-size-cap: "${LOGGING_ROLLING_TOTAL_SIZE_CAP:5GB}"
clean-history-on-start: "${LOGGING_ROLLING_CLEAN_HISTORY_ON_START:false}"
@@ -0,0 +1,51 @@
package com.baiyue.audit;
import com.baiyue.common.web.CommonWebAutoConfiguration;
import org.junit.jupiter.api.Test;
import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.web.server.ManagementContextAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.health.HealthEndpointAutoConfiguration;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration;
import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration;
import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration;
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
class AuditHealthEndpointTest {
private final WebApplicationContextRunner context = new WebApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(
JacksonAutoConfiguration.class, HttpMessageConvertersAutoConfiguration.class,
WebMvcAutoConfiguration.class, DispatcherServletAutoConfiguration.class, EndpointAutoConfiguration.class,
HealthEndpointAutoConfiguration.class, WebEndpointAutoConfiguration.class,
ManagementContextAutoConfiguration.class, CommonWebAutoConfiguration.class))
.withPropertyValues("management.endpoints.web.exposure.include=health");
@Test
void actuatorHealthReturnsUpInsteadOfBusinessResourceNotFound() {
context.withBean("dependencyHealthIndicator", HealthIndicator.class, () -> () -> Health.up().build())
.run(application -> MockMvcBuilders.webAppContextSetup(application).build()
.perform(get("/actuator/health"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.status").value("UP"))
.andExpect(jsonPath("$.code").doesNotExist()));
}
@Test
void failedHealthContributorReturnsDownAndServiceUnavailable() {
context.withBean("dependencyHealthIndicator", HealthIndicator.class, () -> () -> Health.down().build())
.run(application -> MockMvcBuilders.webAppContextSetup(application).build()
.perform(get("/actuator/health"))
.andExpect(status().isServiceUnavailable())
.andExpect(jsonPath("$.status").value("DOWN"))
.andExpect(jsonPath("$.code").doesNotExist()));
}
}
@@ -0,0 +1,17 @@
package com.baiyue.audit;
import com.baiyue.common.core.mybatis.MybatisPlusConfiguration;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.Import;
import static org.assertj.core.api.Assertions.assertThat;
class BaiyueAuditApplicationContractTest {
@Test
void importsSharedMybatisPaginationConfiguration() {
Import imported = BaiyueAuditApplication.class.getAnnotation(Import.class);
assertThat(imported).isNotNull();
assertThat(imported.value()).contains(MybatisPlusConfiguration.class);
}
}
@@ -10,6 +10,7 @@ import jakarta.annotation.Resource;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import org.springframework.security.access.prepost.PreAuthorize;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
@@ -73,6 +74,18 @@ class PlatformAuditLogControllerContractTest {
verify(service).detail(8L); verify(service).detail(8L);
} }
@Test
void listAndDetailUseTheSamePlatformAuditPermission() throws NoSuchMethodException {
String expected = "hasAuthority('platform:audit:list')";
assertThat(PlatformAuditLogController.class.getMethod("page", AuthenticatedUser.class,
long.class, long.class, Long.class, Long.class, String.class, String.class, String.class,
Boolean.class, java.time.Instant.class, java.time.Instant.class, String.class)
.getAnnotation(PreAuthorize.class).value()).isEqualTo(expected);
assertThat(PlatformAuditLogController.class.getMethod("detail", AuthenticatedUser.class, Long.class)
.getAnnotation(PreAuthorize.class).value()).isEqualTo(expected);
}
private static AuthenticatedUser user(String appCode) { private static AuthenticatedUser user(String appCode) {
return new AuthenticatedUser(7L, 0L, 0L, 1L, "管理员", appCode, "WEB", Set.of("platform:audit:list")); return new AuthenticatedUser(7L, 0L, 0L, 1L, "管理员", appCode, "WEB", Set.of("platform:audit:list"));
} }
@@ -45,6 +45,19 @@ class AuditLogQueryServiceTest {
.doesNotContain("beforeData", "afterData"); .doesNotContain("beforeData", "afterData");
} }
@Test
void pagePreservesTotalAndPagesForCustomerLogPagination() {
AuditLogQuery query = new AuditLogQuery(10001L, null, null, "TENANT", null, null,
null, null, null);
when(repository.page(query, 1, 20)).thenReturn(new PageResult<>(List.of(log(8L)), 1, 20, 13, 1));
PageResult<AuditLogResponses.ListItem> result = service.page(query, 1, 20);
assertThat(result.records()).hasSize(1);
assertThat(result.total()).isEqualTo(13);
assertThat(result.pages()).isEqualTo(1);
}
@Test @Test
void detailReturnsSanitizedSnapshots() { void detailReturnsSanitizedSnapshots() {
when(repository.findById(8L)).thenReturn(log(8L)); when(repository.findById(8L)).thenReturn(log(8L));
+16 -3
View File
@@ -1,9 +1,15 @@
# Auth Service 模块指引 # Auth Service 模块指引
## 数据库访问与中文注释约束
- 强制继承 `../AGENTS.md` 的“数据库访问与中文代码注释”全部规则;该节是分层、SQL 执行和方法/核心逻辑中文注释的唯一维护处,本模块不另设例外。
## 职责与边界 ## 职责与边界
- Mapper 只由 `BaiyueAuthApplication.java` 对 `com.baiyue.auth` 根包按 `@Mapper` 过滤扫描;新增或移动 Mapper 后从后端根运行 `node scripts/mapper-scan-contract.mjs`,不得增加子域扫描配置。
- 负责登录、注册、账号凭据、Access/Refresh Token、退出、密码和账号归属范围。 - 负责登录、注册、账号凭据、Access/Refresh Token、退出、密码和账号归属范围。
- 客户主档、成员、角色权限归 `iam/`;Auth 通过 IAM 客户端获取授权快照,不跨库写 IAM。 - 客户主档、成员、角色权限归 `iam/`;Auth 通过 IAM 客户端获取授权快照,不跨库写 IAM。
- 修改统一 Access/Refresh Token、Audience、JWKS、Redis 会话或权限撤销前,先读 `../../docs/统一认证与服务通信契约.md` 的认证章节;平台、客户及两类 PDA 保持各自访问域和仓绑定边界。
- `tenant_id=0` 为平台作用域,`tenant_id>0` 为客户 ID;不得引入租户管理或切换。 - `tenant_id=0` 为平台作用域,`tenant_id>0` 为客户 ID;不得引入租户管理或切换。
## 模块入口 ## 模块入口
@@ -12,7 +18,11 @@
|---|---| |---|---|
| 登录/刷新/退出/改密 | `src/main/java/com/baiyue/auth/controller/AuthController.java` | | 登录/刷新/退出/改密 | `src/main/java/com/baiyue/auth/controller/AuthController.java` |
| 内部账号管理与 scope | `controller/InternalAccountController.java`、`service/AccountManagementService.java` | | 内部账号管理与 scope | `controller/InternalAccountController.java`、`service/AccountManagementService.java` |
| 小 JWT 签发 | `service/AuthService.java`;JWT 只承载稳定身份、应用域和版本声明,角色/权限随 Redis 会话加载 | | 登录标识与企业联系人解耦 | `service/AccountManagementService.java`、`dto/AccountManagementDtos.java`、`service/MerchantRegistrationService.java`;注册手机号只在创建时同时初始化首位管理员登录手机号和公司联系电话,创建后二者独立维护;账号手机号变更需撤销该账号全部会话 |
| 统一 JWT 签发与 JWKS | `config/AuthSecurityConfig.java`、`config/AuthSecurityProperties.java`、`service/AuthService.java`;Auth 是国内与海外仓共用的唯一用户 Token 签发方,JWKS 公钥发布按认证接口契约实现 |
| 统一小 JWT 与会话 | `service/AuthService.java`、`service/TokenSessionStore.java`;浏览器登录 Token 只承载稳定身份、应用域和版本声明,各业务服务结合 Redis 会话校验 |
| 统一 RS256 签发 | `config/UnifiedJwtConfiguration.java`、`config/UnifiedJwtProperties.java`;登录、刷新、代登录使用唯一 Access Token,`JWT_RSA_JWK_SET` 和 `JWT_CURRENT_KID` 仅配置在 Auth;不再提供海外 Token 兑换 |
| 统一会话权威校验 | `controller/InternalAccountController.java`、`service/SessionStateService.java`;内部 `/internal/auth/accounts/sessions/validate` 接收 jwtId/userId,核对 Redis Access 与状态指纹,再查询当前 Auth/IAM;内部服务令牌保护,不接收或转发用户 JWT |
| Token 会话与权限快照 | `service/TokenSessionStore.java`、`client/AuthorizationLoader.java`、`client/IamAuthorizationClient.java`;消费端映射见 `../common-security/src/main/java/com/baiyue/common/security/RedisAccessTokenSessionValidator.java`、`JwtClaimsMapper.java` | | Token 会话与权限快照 | `service/TokenSessionStore.java`、`client/AuthorizationLoader.java`、`client/IamAuthorizationClient.java`;消费端映射见 `../common-security/src/main/java/com/baiyue/common/security/RedisAccessTokenSessionValidator.java`、`JwtClaimsMapper.java` |
| 注册协作 | `service/MerchantRegistrationService.java` | | 注册协作 | `service/MerchantRegistrationService.java` |
| IAM 授权客户端 | `client/IamAuthorizationClient.java`、`client/IamOnboardingClient.java` | | IAM 授权客户端 | `client/IamAuthorizationClient.java`、`client/IamOnboardingClient.java` |
@@ -22,10 +32,13 @@
## 修改约束 ## 修改约束
- 密码、Token、敏感 Claim 不得写日志;小 JWT 不嵌入角色和权限全集,实时授权来自 Redis 会话中的 IAM 授权快照。修改 Claim、Redis Key、权限快照或过期策略时联查 `common-security/`、`gateway/`、`iam/`。 - 密码、Token、敏感 Claim 不得写日志;统一 JWT 使用 RS256,私钥只留在 Auth,JWKS 只发布公钥。统一 Token 不携带角色或权限集,权限来自共享 Redis 会话;用户、客户、权限和仓绑定变更必须撤销受影响会话。本地源码已采用统一权威状态校验并移除安全投影,不能将本地改造视为测试环境已部署。修改登录响应、Claim、Audience、JWKS、权限快照或过期策略时联查 Admin/Client 统一认证层、`common-security/`、`gateway/`、`iam/` 和海外服务消费者。
- 登录、刷新和账号安全修改通过账号行锁串行化;IAM 身份、权限、客户版本、仓绑定由单条 SQL 快照组成指纹,Access/Refresh 均绑定指纹。Gateway 与业务服务逐请求在线核对,无正结果缓存;提交后开始的请求不得接受旧指纹,提交前已经完成认证的在途业务请求不追溯中断。撤销失败使变更失败或回滚,丢失撤销响应也不得恢复旧会话。
- 校验链为 Gateway/业务服务 → Auth → IAM,不转发 Bearer、无递归。统一 `baiyue.security.session-validation-uri` 默认 `http://auth:9100/internal/auth/accounts/sessions/validate`;外层请求总超时 4 秒,Auth→IAM 连接 1 秒/读取 2 秒且不自动重试。Auth/IAM/Redis 任一不可用均失败关闭;真实吞吐与耗时仍需测试环境验证。
- 注册跨 Auth/IAM 必须保持幂等和失败补偿;内部接口仅接受服务令牌。 - 注册跨 Auth/IAM 必须保持幂等和失败补偿;内部接口仅接受服务令牌。
- 登录账号的 username、phone、email 唯一性只约束 Auth 账号;IAM 公司联系电话/联系邮箱是企业资料,不得反向覆盖账号登录标识,也不得因其他企业使用相同联系电话而拒绝。成员登录手机号变更只能由 IAM 账号协作入口同步并撤销该账号全部会话。
- 业务错误保持 HTTP 200 + body code;Security 认证失败保留真实 401/403。 - 业务错误保持 HTTP 200 + body code;Security 认证失败保留真实 401/403。
## 全量验收导航 ## 全量验收导航
一期、二期、三期及财务全量验收均复用本模块认证能力;按本次链路读取对应阶段/专题文档,重点核对 Token、应用域、客户隔离、失效和跨 Auth/IAM 补偿,不因历史阶段划分排除其他消费者。 订单与客户、入库测量与计费、国内货柜履约、海外仓业务及财务全量验收均复用本模块认证能力;按本次链路读取对应业务域/专题文档,重点核对 Token、应用域、客户隔离、失效和跨 Auth/IAM 补偿,不因历史规划边界排除其他消费者。
+1
View File
@@ -22,6 +22,7 @@
<dependency><groupId>com.mysql</groupId><artifactId>mysql-connector-j</artifactId><scope>runtime</scope></dependency> <dependency><groupId>com.mysql</groupId><artifactId>mysql-connector-j</artifactId><scope>runtime</scope></dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency> <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency>
<dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-test</artifactId><scope>test</scope></dependency> <dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-test</artifactId><scope>test</scope></dependency>
<dependency><groupId>com.h2database</groupId><artifactId>h2</artifactId><scope>test</scope></dependency>
</dependencies> </dependencies>
<build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build> <build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build>
</project> </project>
@@ -1,5 +1,6 @@
package com.baiyue.auth; package com.baiyue.auth;
import org.apache.ibatis.annotations.Mapper;
import org.mybatis.spring.annotation.MapperScan; import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
@@ -8,7 +9,7 @@ import org.springframework.cloud.openfeign.EnableFeignClients;
/** /**
* 认证服务启动入口,负责装配统一登录、令牌签发与会话管理相关组件。 * 认证服务启动入口,负责装配统一登录、令牌签发与会话管理相关组件。
*/ */
@MapperScan("com.baiyue.auth.mapper") @MapperScan(basePackages = "com.baiyue.auth", annotationClass = Mapper.class)
@EnableFeignClients(basePackages = "com.baiyue.auth.client") @EnableFeignClients(basePackages = "com.baiyue.auth.client")
@SpringBootApplication(scanBasePackages = "com.baiyue") @SpringBootApplication(scanBasePackages = "com.baiyue")
public class BaiyueAuthApplication { public class BaiyueAuthApplication {
@@ -0,0 +1,14 @@
package com.baiyue.auth.client;
import feign.Request;
import feign.Retryer;
import org.springframework.context.annotation.Bean;
import java.util.concurrent.TimeUnit;
/** Only the IAM authorization client uses this bounded, single-attempt chain. */
public class AuthorizationFeignConfiguration {
@Bean public Request.Options authorizationRequestOptions() {
return new Request.Options(1, TimeUnit.SECONDS, 2, TimeUnit.SECONDS, false);
}
@Bean public Retryer authorizationRetryer() { return Retryer.NEVER_RETRY; }
}
@@ -21,4 +21,28 @@ public class AuthorizationSnapshot {
private Set<String> roles; private Set<String> roles;
private Set<String> permissions; private Set<String> permissions;
private Long permissionVersion; private Long permissionVersion;
private Long overseasWarehouseId;
private Long domesticWarehouseId;
private Long tenantVersion;
public AuthorizationSnapshot(Long userId, Long tenantId, Long membershipId, String appCode,
String clientType, Set<String> roles, Set<String> permissions, Long permissionVersion,
Long overseasWarehouseId, Long domesticWarehouseId) {
this(userId, tenantId, membershipId, appCode, clientType, roles, permissions,
permissionVersion, overseasWarehouseId, domesticWarehouseId, 0L);
}
public AuthorizationSnapshot(Long userId, Long tenantId, Long membershipId, String appCode,
String clientType, Set<String> roles, Set<String> permissions,
Long permissionVersion) {
this(userId, tenantId, membershipId, appCode, clientType, roles, permissions,
permissionVersion, null, null);
}
public AuthorizationSnapshot(Long userId, Long tenantId, Long membershipId, String appCode,
String clientType, Set<String> roles, Set<String> permissions,
Long permissionVersion, Long overseasWarehouseId) {
this(userId, tenantId, membershipId, appCode, clientType, roles, permissions,
permissionVersion, overseasWarehouseId, null);
}
} }
@@ -9,7 +9,7 @@ import org.springframework.web.bind.annotation.RequestHeader;
/** /**
* 认证服务访问 IAM 内部授权接口的声明式客户端,用于加载用户在指定应用和租户下的权限快照。 * 认证服务访问 IAM 内部授权接口的声明式客户端,用于加载用户在指定应用和租户下的权限快照。
*/ */
@FeignClient(name = "iam-service", contextId = "iamAuthorizationClient", @FeignClient(name = "iam-service", contextId = "iamAuthorizationClient", configuration = AuthorizationFeignConfiguration.class,
url = "${baiyue.clients.iam.url:}", path = "/internal/iam") url = "${baiyue.clients.iam.url:}", path = "/internal/iam")
public interface IamAuthorizationClient { public interface IamAuthorizationClient {
@PostMapping("/authorization-snapshot") @PostMapping("/authorization-snapshot")
@@ -5,6 +5,7 @@ import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.cloud.openfeign.FeignClient; import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestHeader;
@@ -16,14 +17,20 @@ public interface IamOnboardingClient {
@RequestHeader("X-Internal-Service-Token") String internalToken, @RequestHeader("X-Internal-Service-Token") String internalToken,
@RequestBody OnboardingRequest request); @RequestBody OnboardingRequest request);
@GetMapping("/merchant-registrations/{authUserId}")
ApiResponse<OnboardingResponse> registration(
@RequestHeader("X-Internal-Service-Token") String internalToken,
@PathVariable("authUserId") Long authUserId);
@DeleteMapping("/merchant-registrations/{authUserId}/compensation") @DeleteMapping("/merchant-registrations/{authUserId}/compensation")
ApiResponse<Void> compensate( ApiResponse<Void> compensate(
@RequestHeader("X-Internal-Service-Token") String internalToken, @RequestHeader("X-Internal-Service-Token") String internalToken,
@PathVariable("authUserId") Long authUserId); @PathVariable("authUserId") Long authUserId);
record OnboardingRequest(Long authUserId, String displayName, String companyName, record OnboardingRequest(Long authUserId, String displayName, String companyName,
String legalPerson, String legalPhone, String contactEmail, String legalPerson, String administratorPhone, String contactEmail,
String countryCode, String address) { } String countryCode, String address) { }
record OnboardingResponse(Long tenantId, Long membershipId, String tenantStatus) { } record OnboardingResponse(Long tenantId, Long membershipId, String tenantStatus,
String dimensionInitializationStatus) { }
} }
@@ -8,7 +8,7 @@ import java.time.Duration;
* 认证安全配置属性,承载 JWT 密钥、签发方及访问令牌、刷新令牌有效期。 * 认证安全配置属性,承载 JWT 密钥、签发方及访问令牌、刷新令牌有效期。
*/ */
@ConfigurationProperties(prefix = "baiyue.security") @ConfigurationProperties(prefix = "baiyue.security")
public record AuthSecurityProperties(String jwtSecret, String issuer, String audience, public record AuthSecurityProperties(String issuer, String audience,
Duration accessTokenTtl, Duration refreshTokenTtl, Duration accessTokenTtl, Duration refreshTokenTtl,
int maxLoginFailures, Duration lockDuration) { int maxLoginFailures, Duration lockDuration) {
} }
@@ -1,22 +0,0 @@
package com.baiyue.auth.config;
import com.nimbusds.jose.jwk.source.ImmutableSecret;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.security.oauth2.jwt.JwtEncoder;
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
import java.nio.charset.StandardCharsets;
/** 国内现有 HS256 编码器,始终为认证服务主编码器。 */
@Configuration(proxyBeanMethods = false)
public class DomesticJwtConfiguration {
@Bean("jwtEncoder")
@Primary
JwtEncoder jwtEncoder(AuthSecurityProperties properties) {
byte[] secret = properties.jwtSecret().getBytes(StandardCharsets.UTF_8);
if (secret.length < 32) throw new IllegalStateException("JWT_SECRET 至少需要 32 字节");
return new NimbusJwtEncoder(new ImmutableSecret<>(secret));
}
}
@@ -1,10 +0,0 @@
package com.baiyue.auth.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.time.Duration;
/** 海外尾程 RS256 短时令牌配置,默认关闭。 */
@ConfigurationProperties(prefix = "baiyue.security.overseas-jwt")
public record OverseasJwtProperties(boolean enabled, String issuer, String audience,
String rsaJwkSet, String currentKid, Duration accessTokenTtl) { }
@@ -3,7 +3,6 @@ package com.baiyue.auth.config;
import com.nimbusds.jose.jwk.JWKSet; import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey; import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.ImmutableJWKSet; import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
@@ -11,27 +10,26 @@ import org.springframework.security.oauth2.jwt.JwtEncoder;
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder; import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
@Configuration(proxyBeanMethods = false) @Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(OverseasJwtProperties.class) @EnableConfigurationProperties(UnifiedJwtProperties.class)
@ConditionalOnProperty(name = "baiyue.security.overseas-jwt.enabled", havingValue = "true") public class UnifiedJwtConfiguration {
public class OverseasJwtConfiguration { @Bean("jwtEncoder")
@Bean("overseasJwtEncoder") JwtEncoder jwtEncoder(UnifiedJwtProperties properties) {
JwtEncoder overseasJwtEncoder(OverseasJwtProperties properties) {
try { try {
JWKSet set = JWKSet.parse(properties.rsaJwkSet()); JWKSet set = JWKSet.parse(properties.rsaJwkSet());
RSAKey current = set.getKeyByKeyId(properties.currentKid()) instanceof RSAKey rsa ? rsa : null; RSAKey current = set.getKeyByKeyId(properties.currentKid()) instanceof RSAKey rsa ? rsa : null;
if (current == null || !current.isPrivate()) { if (current == null || !current.isPrivate()) {
throw new IllegalStateException("海外 current-kid 必须指向包含私钥的 RSA 密钥"); throw new IllegalStateException("统一 current-kid 必须指向包含私钥的 RSA 密钥");
} }
if (set.getKeys().stream().anyMatch(jwk -> !(jwk instanceof RSAKey))) { if (set.getKeys().stream().anyMatch(jwk -> !(jwk instanceof RSAKey))) {
throw new IllegalStateException("海外 JWK Set 只能包含 RSA 密钥"); throw new IllegalStateException("统一 JWK Set 只能包含 RSA 密钥");
} }
if (set.getKeys().stream().anyMatch(jwk -> !properties.currentKid().equals(jwk.getKeyID()) if (set.getKeys().stream().anyMatch(jwk -> !properties.currentKid().equals(jwk.getKeyID())
&& jwk instanceof RSAKey rsa && rsa.isPrivate())) { && jwk instanceof RSAKey rsa && rsa.isPrivate())) {
throw new IllegalStateException("海外非当前密钥只能配置公钥"); throw new IllegalStateException("统一非当前密钥只能配置公钥");
} }
return new NimbusJwtEncoder(new ImmutableJWKSet<>(set)); return new NimbusJwtEncoder(new ImmutableJWKSet<>(set));
} catch (java.text.ParseException exception) { } catch (java.text.ParseException exception) {
throw new IllegalStateException("海外 RSA JWK Set 配置无效", exception); throw new IllegalStateException("统一 RSA JWK Set 配置无效", exception);
} }
} }
} }
@@ -0,0 +1,7 @@
package com.baiyue.auth.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
/** Unified RS256 signing key configuration. Private keys remain in Auth. */
@ConfigurationProperties(prefix = "baiyue.security.jwt")
public record UnifiedJwtProperties(String rsaJwkSet, String currentKid) { }
@@ -23,8 +23,6 @@ import com.baiyue.auth.dto.AccountManagementDtos.ChangePasswordRequest;
import com.baiyue.auth.dto.AccountManagementDtos.ForcedPasswordChangeRequest; import com.baiyue.auth.dto.AccountManagementDtos.ForcedPasswordChangeRequest;
import com.baiyue.auth.dto.AccountManagementDtos; import com.baiyue.auth.dto.AccountManagementDtos;
import com.baiyue.auth.service.AccountManagementService; import com.baiyue.auth.service.AccountManagementService;
import com.baiyue.auth.service.OverseasTokenService;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
/** /**
@@ -40,19 +38,6 @@ public class AuthController {
private MerchantRegistrationService registrationService; private MerchantRegistrationService registrationService;
@Resource @Resource
private AccountManagementService accountManagementService; private AccountManagementService accountManagementService;
@Resource
private ObjectProvider<OverseasTokenService> overseasTokens;
@PostMapping("/overseas/token")
@PreAuthorize("isAuthenticated()")
@Operation(summary = "兑换海外尾程访问令牌", description = "使用已通过国内会话校验的身份兑换 15 分钟 RS256 海外令牌。")
public ApiResponse<OverseasTokenService.OverseasTokenResponse> exchangeOverseasToken(
@AuthenticationPrincipal AuthenticatedUser user) {
OverseasTokenService service = overseasTokens.getIfAvailable();
if (service == null) throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "海外认证未启用");
return ApiResponse.success(service.exchange(user.userId()));
}
@PutMapping("/password") @PutMapping("/password")
@PreAuthorize("isAuthenticated()") @PreAuthorize("isAuthenticated()")
@Operation(summary = "修改当前用户密码") @Operation(summary = "修改当前用户密码")
@@ -77,7 +62,7 @@ public class AuthController {
@SecurityRequirements @SecurityRequirements
@PostMapping("/register") @PostMapping("/register")
@Operation(summary = "Merchant registration / 客户注册", @Operation(summary = "Merchant registration / 客户注册",
description = "Create a login account and an enterprise profile awaiting the required business license and legal-person ID; other qualification files are optional.") description = "Create a login account and enterprise profile. The supplied phone initializes both the first administrator login phone and company contact phone; they are maintained independently after creation. Only the business license and unified social credit code are required for review; legal-person ID is optional.")
public ApiResponse<MerchantRegisterResponse> register(@Valid @RequestBody MerchantRegisterRequest request) { public ApiResponse<MerchantRegisterResponse> register(@Valid @RequestBody MerchantRegisterRequest request) {
return ApiResponse.success(registrationService.register(request)); return ApiResponse.success(registrationService.register(request));
} }
@@ -9,6 +9,7 @@ import com.baiyue.common.core.exception.BizException;
import jakarta.annotation.PostConstruct; import jakarta.annotation.PostConstruct;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
@@ -18,11 +19,13 @@ import java.security.MessageDigest;
/** 仅供可信内部服务调用的账号管理接口。 */ /** 仅供可信内部服务调用的账号管理接口。 */
@RestController @RestController
@RequestMapping("/internal/auth/accounts") @RequestMapping("/internal/auth/accounts")
@Tag(name = "Internal Auth / 内部认证", description = "Service-token protected internal account and unified session APIs")
public class InternalAccountController { public class InternalAccountController {
@Resource @Resource
private AccountManagementService service; private AccountManagementService service;
@Resource @Resource
private AuthService authService; private AuthService authService;
@Resource private com.baiyue.auth.service.SessionStateService sessionStateService;
@Value("${baiyue.security.internal-service-token}") @Value("${baiyue.security.internal-service-token}")
private String configuredToken; private String configuredToken;
@@ -64,11 +67,36 @@ public class InternalAccountController {
@Valid @RequestBody RevokeAccountSessionsRequest request) { @Valid @RequestBody RevokeAccountSessionsRequest request) {
verify(token); service.revokeSessions(request.accountIds()); return ApiResponse.success(null); verify(token); service.revokeSessions(request.accountIds()); return ApiResponse.success(null);
} }
public record SessionValidationRequest(@jakarta.validation.constraints.NotBlank String jwtId,
@jakarta.validation.constraints.NotNull Long userId) { }
@PostMapping("/sessions/validate")
public ApiResponse<Boolean> validateSession(
@RequestHeader(value="X-Internal-Service-Token", required=false) String token,
@Valid @RequestBody SessionValidationRequest request) {
verify(token);
return ApiResponse.success(sessionStateService.validate(request.jwtId(), request.userId()));
}
@PutMapping("/{id}/profile") @PutMapping("/{id}/profile")
public ApiResponse<Void> profile(@RequestHeader(value="X-Internal-Service-Token", required=false) String token, public ApiResponse<Void> profile(@RequestHeader(value="X-Internal-Service-Token", required=false) String token,
@PathVariable Long id, @Valid @RequestBody UpdateAccountProfileRequest request) { @PathVariable Long id, @Valid @RequestBody UpdateAccountProfileRequest request) {
verify(token); service.updateProfile(id, request); return ApiResponse.success(null); verify(token); service.updateProfile(id, request); return ApiResponse.success(null);
} }
@PutMapping("/{id}/login-identifiers")
public ApiResponse<AccountSummary> loginIdentifiers(
@RequestHeader(value="X-Internal-Service-Token", required=false) String token,
@PathVariable Long id, @Valid @RequestBody ChangeLoginIdentifiersRequest request) {
verify(token);
return ApiResponse.success(service.changeLoginIdentifiers(id, request));
}
@PutMapping("/{id}/phone")
public ApiResponse<Void> phone(@RequestHeader(value="X-Internal-Service-Token", required=false) String token,
@PathVariable Long id, @Valid @RequestBody UpdateAccountPhoneRequest request) {
verify(token);
service.updatePhone(id, request);
return ApiResponse.success(null);
}
@PutMapping("/{id}/password") @PutMapping("/{id}/password")
public ApiResponse<Void> reset(@RequestHeader(value="X-Internal-Service-Token", required=false) String token, public ApiResponse<Void> reset(@RequestHeader(value="X-Internal-Service-Token", required=false) String token,
@PathVariable Long id, @Valid @RequestBody ResetPasswordRequest request) { @PathVariable Long id, @Valid @RequestBody ResetPasswordRequest request) {
@@ -1,10 +1,9 @@
package com.baiyue.auth.controller; package com.baiyue.auth.controller;
import com.baiyue.auth.config.OverseasJwtProperties; import com.baiyue.auth.config.UnifiedJwtProperties;
import com.nimbusds.jose.jwk.JWKSet; import com.nimbusds.jose.jwk.JWKSet;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.security.SecurityRequirements; import io.swagger.v3.oas.annotations.security.SecurityRequirements;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.http.CacheControl; import org.springframework.http.CacheControl;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
@@ -13,17 +12,16 @@ import org.springframework.web.bind.annotation.RestController;
import java.util.Map; import java.util.Map;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
/** 仅在海外 JWT 显式启用后发布 RSA 公钥。 */ /** 发布统一 JWT 的 RSA 公钥。 */
@RestController @RestController
@ConditionalOnProperty(name = "baiyue.security.overseas-jwt.enabled", havingValue = "true")
public class JwksController { public class JwksController {
private final OverseasJwtProperties properties; private final UnifiedJwtProperties properties;
public JwksController(OverseasJwtProperties properties) { this.properties = properties; } public JwksController(UnifiedJwtProperties properties) { this.properties = properties; }
@GetMapping("/api/auth/.well-known/jwks.json") @GetMapping("/api/auth/.well-known/jwks.json")
@SecurityRequirements @SecurityRequirements
@Operation(summary = "获取海外 JWT 公钥集", description = "仅发布当前和轮换期上一把 RSA 公钥,不包含私钥。") @Operation(summary = "获取统一 JWT 公钥集", description = "仅发布当前和轮换期上一把 RSA 公钥,不包含私钥。")
public ResponseEntity<Map<String, Object>> jwks() throws Exception { public ResponseEntity<Map<String, Object>> jwks() throws Exception {
return ResponseEntity.ok().cacheControl(CacheControl.maxAge(5, TimeUnit.MINUTES).cachePublic()) return ResponseEntity.ok().cacheControl(CacheControl.maxAge(5, TimeUnit.MINUTES).cachePublic())
.body(JWKSet.parse(properties.rsaJwkSet()).toPublicJWKSet().toJSONObject()); .body(JWKSet.parse(properties.rsaJwkSet()).toPublicJWKSet().toJSONObject());
@@ -21,7 +21,8 @@ public final class AccountManagementDtos {
@Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d).+$", message = "密码必须同时包含字母和数字") @Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d).+$", message = "密码必须同时包含字母和数字")
String newPassword) { } String newPassword) { }
public record CreateAccountRequest(@NotNull Long accountId, public record CreateAccountRequest(@NotNull Long accountId,
@NotBlank @Pattern(regexp = "^[A-Za-z0-9_.@-]{4,64}$") String username, @NotBlank @Pattern(regexp = "^[A-Za-z][A-Za-z0-9_.@-]{3,63}$") String username,
@Pattern(regexp = "^1[3-9]\\d{9}$") String phone,
@NotBlank @Size(min = 8, max = 64) @NotBlank @Size(min = 8, max = 64)
@Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d).+$", message = "密码必须同时包含字母和数字") String password, @Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d).+$", message = "密码必须同时包含字母和数字") String password,
@NotBlank String displayName, @NotNull Long tenantId, @NotBlank String displayName, @NotNull Long tenantId,
@@ -32,9 +33,19 @@ public final class AccountManagementDtos {
String accountStatus) { String accountStatus) {
public CreateAccountRequest(Long accountId, String username, String password, String displayName, public CreateAccountRequest(Long accountId, String username, String password, String displayName,
Long tenantId, Long membershipId, String appCode) { Long tenantId, Long membershipId, String appCode) {
this(accountId, username, password, displayName, tenantId, membershipId, appCode, "ACTIVE"); this(accountId, username, null, password, displayName, tenantId, membershipId, appCode, "ACTIVE");
}
public CreateAccountRequest(Long accountId, String username, String password, String displayName,
Long tenantId, Long membershipId, String appCode, String accountStatus) {
this(accountId, username, null, password, displayName, tenantId, membershipId, appCode, accountStatus);
} }
} }
public record ChangeLoginIdentifiersRequest(
@NotBlank @Pattern(regexp = "^[A-Za-z][A-Za-z0-9_.@-]{3,63}$") String username,
@NotBlank @Pattern(regexp = "^1[3-9]\\d{9}$") String phone,
@NotBlank String currentPassword) { }
public record UpdateAccountPhoneRequest(
@Pattern(regexp = "^1[3-9]\\d{9}$") String phone) { }
public record UpdateAccountProfileRequest(@NotBlank @Size(max = 64) String displayName) { } public record UpdateAccountProfileRequest(@NotBlank @Size(max = 64) String displayName) { }
public record UpdateAccountStatusRequest(@NotBlank String status) { } public record UpdateAccountStatusRequest(@NotBlank String status) { }
public record RevokeAccountSessionsRequest( public record RevokeAccountSessionsRequest(
@@ -56,10 +67,10 @@ public final class AccountManagementDtos {
public record ImpersonationExchangeRequest(@NotBlank String code) { } public record ImpersonationExchangeRequest(@NotBlank String code) { }
public record UpdateAccountScopeRequest(@NotNull Long tenantId, @NotNull Long membershipId, public record UpdateAccountScopeRequest(@NotNull Long tenantId, @NotNull Long membershipId,
@NotBlank @Pattern(regexp = "MERCHANT|PLATFORM") String appCode) { } @NotBlank @Pattern(regexp = "MERCHANT|PLATFORM") String appCode) { }
public record AccountSummary(Long accountId, String username, String displayName, String status, public record AccountSummary(Long accountId, String username, String phone, String displayName, String status,
LocalDateTime lastLoginAt, Long tenantId, Long membershipId, String appCode) { LocalDateTime lastLoginAt, Long tenantId, Long membershipId, String appCode) {
public AccountSummary(Long accountId, String username, String displayName, String status) { public AccountSummary(Long accountId, String username, String displayName, String status) {
this(accountId, username, displayName, status, null, null, null, null); this(accountId, username, null, displayName, status, null, null, null, null);
} }
} }
} }
@@ -2,13 +2,15 @@ package com.baiyue.auth.dto;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
/** /**
* 登录请求契约,接收账号、密码和目标应用域等统一登录参数。 * 登录请求契约,接收账号、密码和目标应用域等统一登录参数。
*/ */
@Schema(name = "LoginRequest", description = "Unified login credentials / 统一登录凭据") @Schema(name = "LoginRequest", description = "Unified login credentials / 统一登录凭据")
public record LoginRequest( public record LoginRequest(
@Schema(description = "Login username / 登录用户名", example = "merchant_admin") @NotBlank String username, @Schema(description = "Username or mainland China mobile number / 用户名或中国大陆手机号", example = "merchant_admin")
@NotBlank @Size(max = 64) String loginId,
@Schema(description = "Login password / 登录密码", example = "ChangeMe123!", format = "password") @NotBlank String password @Schema(description = "Login password / 登录密码", example = "ChangeMe123!", format = "password") @NotBlank String password
) { ) {
} }
@@ -8,12 +8,13 @@ import jakarta.validation.constraints.Size;
@Schema(name = "MerchantRegisterRequest", description = "Phase-one merchant account registration") @Schema(name = "MerchantRegisterRequest", description = "Phase-one merchant account registration")
public record MerchantRegisterRequest( public record MerchantRegisterRequest(
@NotBlank @Pattern(regexp = "[A-Za-z0-9_]{4,64}") String username, @NotBlank @Pattern(regexp = "^[A-Za-z][A-Za-z0-9_.@-]{3,63}$") String username,
@NotBlank @Size(min = 8, max = 72) String password, @NotBlank @Size(min = 8, max = 72) String password,
@NotBlank @Size(max = 64) String displayName, @NotBlank @Size(max = 64) String displayName,
@NotBlank @Size(max = 128) String companyName, @NotBlank @Size(max = 128) String companyName,
@NotBlank @Size(max = 64) String legalPerson, @Size(max = 64) String legalPerson,
@NotBlank @Size(max = 32) String legalPhone, @Schema(description = "首个企业管理员的登录手机号;注册时同时初始化企业联系电话,创建后两者独立维护")
@NotBlank @Pattern(regexp = "^1[3-9]\\d{9}$") String phone,
@Email @Size(max = 128) String contactEmail, @Email @Size(max = 128) String contactEmail,
@Size(min = 2, max = 2) String countryCode, @Size(min = 2, max = 2) String countryCode,
@Size(max = 255) String address @Size(max = 255) String address
@@ -5,5 +5,6 @@ public record MerchantRegisterResponse(
Long tenantId, Long tenantId,
Long membershipId, Long membershipId,
String tenantStatus, String tenantStatus,
String dimensionInitializationStatus,
String nextStep String nextStep
) { } ) { }
@@ -2,6 +2,8 @@ package com.baiyue.auth.dto;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import java.util.Set;
/** /**
* 认证令牌响应契约,返回访问令牌、刷新令牌及其有效期等会话信息。 * 认证令牌响应契约,返回访问令牌、刷新令牌及其有效期等会话信息。
*/ */
@@ -11,6 +13,12 @@ public record TokenResponse(
@Schema(description = "JWT access token / JWT 访问令牌", format = "password") String accessToken, @Schema(description = "JWT access token / JWT 访问令牌", format = "password") String accessToken,
@Schema(description = "Access token lifetime in seconds / 访问令牌有效秒数", example = "1800") long expiresIn, @Schema(description = "Access token lifetime in seconds / 访问令牌有效秒数", example = "1800") long expiresIn,
@Schema(description = "Refresh token / 刷新令牌", format = "password") String refreshToken, @Schema(description = "Refresh token / 刷新令牌", format = "password") String refreshToken,
@Schema(description = "Refresh token lifetime in seconds / 刷新令牌有效秒数", example = "604800") long refreshExpiresIn @Schema(description = "Refresh token lifetime in seconds / 刷新令牌有效秒数", example = "604800") long refreshExpiresIn,
@Schema(description = "Current role snapshot returned outside JWT / JWT 外返回的当前角色快照") Set<String> roles,
@Schema(description = "Current authority snapshot returned outside JWT / JWT 外返回的当前权限快照") Set<String> authorities
) { ) {
public TokenResponse {
roles = roles == null ? Set.of() : Set.copyOf(roles);
authorities = authorities == null ? Set.of() : Set.copyOf(authorities);
}
} }
@@ -18,6 +18,7 @@ import java.time.LocalDateTime;
public class AuthAccount { public class AuthAccount {
@TableId private Long id; @TableId private Long id;
private String username; private String username;
private String phone;
private String passwordHash; private String passwordHash;
private String displayName; private String displayName;
private String accountStatus; private String accountStatus;
@@ -1,9 +1,12 @@
package com.baiyue.auth.mapper; package com.baiyue.auth.mapper;
import org.apache.ibatis.annotations.Mapper;
import com.baiyue.auth.entity.AuthAccount; import com.baiyue.auth.entity.AuthAccount;
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper;
/** /**
* 认证账号 MyBatis-Plus 数据访问接口,负责 {@code auth_account} 表的基础持久化操作。 * 认证账号 MyBatis-Plus 数据访问接口,负责 {@code auth_account} 表的基础持久化操作。
*/ */
@Mapper
public interface AuthAccountMapper extends BaseMapper<AuthAccount> { } public interface AuthAccountMapper extends BaseMapper<AuthAccount> { }
@@ -1,9 +1,12 @@
package com.baiyue.auth.mapper; package com.baiyue.auth.mapper;
import org.apache.ibatis.annotations.Mapper;
import com.baiyue.auth.entity.AuthRefreshToken; import com.baiyue.auth.entity.AuthRefreshToken;
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper;
/** /**
* 刷新令牌 MyBatis-Plus 数据访问接口,负责 {@code auth_refresh_token} 表的基础持久化操作。 * 刷新令牌 MyBatis-Plus 数据访问接口,负责 {@code auth_refresh_token} 表的基础持久化操作。
*/ */
@Mapper
public interface AuthRefreshTokenMapper extends BaseMapper<AuthRefreshToken> { } public interface AuthRefreshTokenMapper extends BaseMapper<AuthRefreshToken> { }
@@ -19,7 +19,13 @@ public class AuthAccountRepository {
public AuthAccount findByUsername(String username) { public AuthAccount findByUsername(String username) {
return mapper.selectOne(new LambdaQueryWrapper<AuthAccount>() return mapper.selectOne(new LambdaQueryWrapper<AuthAccount>()
.eq(AuthAccount::getUsername, username) .eq(AuthAccount::getUsername, username)
.last("LIMIT 1")); .last("LIMIT 1 FOR UPDATE"));
}
public AuthAccount findByPhone(String phone) {
return mapper.selectOne(new LambdaQueryWrapper<AuthAccount>()
.eq(AuthAccount::getPhone, phone)
.last("LIMIT 1 FOR UPDATE"));
} }
public boolean existsByUsername(String username) { public boolean existsByUsername(String username) {
@@ -27,16 +33,35 @@ public class AuthAccountRepository {
.eq(AuthAccount::getUsername, username)) > 0; .eq(AuthAccount::getUsername, username)) > 0;
} }
public boolean existsByPhone(String phone) {
return mapper.selectCount(new LambdaQueryWrapper<AuthAccount>()
.eq(AuthAccount::getPhone, phone)) > 0;
}
public boolean existsByUsernameForOtherAccount(String username, Long accountId) {
return mapper.selectCount(new LambdaQueryWrapper<AuthAccount>()
.eq(AuthAccount::getUsername, username)
.ne(AuthAccount::getId, accountId)) > 0;
}
public boolean existsByPhoneForOtherAccount(String phone, Long accountId) {
return mapper.selectCount(new LambdaQueryWrapper<AuthAccount>()
.eq(AuthAccount::getPhone, phone)
.ne(AuthAccount::getId, accountId)) > 0;
}
public void insert(AuthAccount account) { mapper.insert(account); } public void insert(AuthAccount account) { mapper.insert(account); }
public AuthAccount findById(Long id) { public AuthAccount findById(Long id) {
return mapper.selectById(id); return mapper.selectOne(new LambdaQueryWrapper<AuthAccount>().eq(AuthAccount::getId, id).last("FOR UPDATE"));
} }
public AuthAccount findCurrentById(Long id) { return mapper.selectById(id); }
public List<AuthAccount> findMerchantByTenant(Long tenantId) { public List<AuthAccount> findMerchantByTenant(Long tenantId) {
return mapper.selectList(new LambdaQueryWrapper<AuthAccount>() return mapper.selectList(new LambdaQueryWrapper<AuthAccount>()
.eq(AuthAccount::getDefaultTenantId, tenantId) .eq(AuthAccount::getDefaultTenantId, tenantId)
.eq(AuthAccount::getDefaultAppCode, "MERCHANT")); .eq(AuthAccount::getDefaultAppCode, "MERCHANT").orderByAsc(AuthAccount::getId).last("FOR UPDATE"));
} }
public void save(AuthAccount account) { public void save(AuthAccount account) {
@@ -9,6 +9,7 @@ import jakarta.annotation.Resource;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.dao.DuplicateKeyException;
import com.baiyue.common.audit.BusinessAudit; import com.baiyue.common.audit.BusinessAudit;
import java.util.Locale; import java.util.Locale;
@@ -63,7 +64,9 @@ public class AccountManagementService {
String appCode = request.appCode() == null ? "MERCHANT" String appCode = request.appCode() == null ? "MERCHANT"
: request.appCode().trim().toUpperCase(Locale.ROOT); : request.appCode().trim().toUpperCase(Locale.ROOT);
validateScope(appCode, request.tenantId()); validateScope(appCode, request.tenantId());
AuthAccount existing = repository.findById(request.accountId()); // Locking a missing key takes an InnoDB gap lock and can deadlock
// unrelated account inserts. The unique indexes arbitrate creation.
AuthAccount existing = repository.findCurrentById(request.accountId());
if (existing != null) { if (existing != null) {
if (sameIdentity(existing, request, appCode)) return summary(existing); if (sameIdentity(existing, request, appCode)) return summary(existing);
throw new BizException(ResultCode.CONFLICT, "认证账号标识已被其他成员使用"); throw new BizException(ResultCode.CONFLICT, "认证账号标识已被其他成员使用");
@@ -71,9 +74,13 @@ public class AccountManagementService {
if (repository.existsByUsername(request.username())) { if (repository.existsByUsername(request.username())) {
throw new BizException(ResultCode.CONFLICT, "登录账号已存在"); throw new BizException(ResultCode.CONFLICT, "登录账号已存在");
} }
if (request.phone() != null && repository.existsByPhone(request.phone())) {
throw new BizException(ResultCode.CONFLICT, "手机号已存在");
}
AuthAccount account = new AuthAccount(); AuthAccount account = new AuthAccount();
account.setId(request.accountId()); account.setId(request.accountId());
account.setUsername(request.username()); account.setUsername(request.username());
account.setPhone(request.phone());
account.setPasswordHash(passwordEncoder.encode(request.password())); account.setPasswordHash(passwordEncoder.encode(request.password()));
account.setDisplayName(request.displayName()); account.setDisplayName(request.displayName());
account.setAccountStatus(request.accountStatus() == null ? "ACTIVE" : request.accountStatus()); account.setAccountStatus(request.accountStatus() == null ? "ACTIVE" : request.accountStatus());
@@ -84,7 +91,13 @@ public class AccountManagementService {
account.setDefaultMembershipId(request.membershipId()); account.setDefaultMembershipId(request.membershipId());
account.setDefaultAppCode(appCode); account.setDefaultAppCode(appCode);
account.setDeleted(0); account.setDeleted(0);
repository.insert(account); try {
repository.insert(account);
} catch (DuplicateKeyException duplicate) {
AuthAccount winner = repository.findById(request.accountId());
if (winner != null && sameIdentity(winner, request, appCode)) return summary(winner);
throw new BizException(ResultCode.CONFLICT, "认证账号标识或登录账号已被使用");
}
return summary(account); return summary(account);
} }
@@ -104,7 +117,7 @@ public class AccountManagementService {
resourceId = "'RBAC_CHANGE'", description = "RBAC 变更批量撤销账号会话", resourceId = "'RBAC_CHANGE'", description = "RBAC 变更批量撤销账号会话",
captureBefore = false, captureAfter = false) captureBefore = false, captureAfter = false)
public void revokeSessions(Set<Long> accountIds) { public void revokeSessions(Set<Long> accountIds) {
accountIds.forEach(accountId -> bumpVersion(required(accountId))); accountIds.stream().sorted().forEach(accountId -> bumpVersion(required(accountId)));
} }
@Transactional @Transactional
@@ -117,6 +130,45 @@ public class AccountManagementService {
bumpVersion(account); bumpVersion(account);
} }
@Transactional
@BusinessAudit(event = "ACCOUNT_LOGIN_IDENTIFIERS_CHANGE", resourceType = "ACCOUNT", resourceId = "#accountId",
description = "修改本人用户名和手机号", captureAfter = false)
public AccountSummary changeLoginIdentifiers(Long accountId, ChangeLoginIdentifiersRequest request) {
AuthAccount account = required(accountId);
if (!passwordEncoder.matches(request.currentPassword(), account.getPasswordHash())) {
throw new BizException(ResultCode.UNAUTHORIZED, "当前密码错误");
}
String username = request.username().trim();
String phone = request.phone().trim();
if (repository.existsByUsernameForOtherAccount(username, accountId)) {
throw new BizException(ResultCode.CONFLICT, "用户名已存在");
}
if (repository.existsByPhoneForOtherAccount(phone, accountId)) {
throw new BizException(ResultCode.CONFLICT, "手机号已存在");
}
if (Objects.equals(account.getUsername(), username) && Objects.equals(account.getPhone(), phone)) {
return summary(account);
}
account.setUsername(username);
account.setPhone(phone);
bumpVersion(account);
return summary(account);
}
@Transactional
@BusinessAudit(event = "ACCOUNT_PHONE_SYNC", resourceType = "ACCOUNT", resourceId = "#accountId",
description = "同步受管账号手机号", captureAfter = false)
public void updatePhone(Long accountId, UpdateAccountPhoneRequest request) {
AuthAccount account = required(accountId);
String phone = request.phone() == null ? null : request.phone().trim();
if (Objects.equals(account.getPhone(), phone)) return;
if (phone != null && repository.existsByPhoneForOtherAccount(phone, accountId)) {
throw new BizException(ResultCode.CONFLICT, "手机号已存在");
}
account.setPhone(phone);
bumpVersion(account);
}
@Transactional @Transactional
@BusinessAudit(event = "PASSWORD_RESET", resourceType = "ACCOUNT", resourceId = "#accountId", description = "管理员重置密码", captureAfter = false) @BusinessAudit(event = "PASSWORD_RESET", resourceType = "ACCOUNT", resourceId = "#accountId", description = "管理员重置密码", captureAfter = false)
public void resetPassword(Long accountId, ResetPasswordRequest request, boolean forceChangePassword) { public void resetPassword(Long accountId, ResetPasswordRequest request, boolean forceChangePassword) {
@@ -185,7 +237,7 @@ public class AccountManagementService {
} }
private AccountSummary summary(AuthAccount a) { private AccountSummary summary(AuthAccount a) {
return new AccountSummary(a.getId(), a.getUsername(), a.getDisplayName(), a.getAccountStatus(), return new AccountSummary(a.getId(), a.getUsername(), a.getPhone(), a.getDisplayName(), a.getAccountStatus(),
a.getLastLoginAt(), a.getDefaultTenantId(), a.getDefaultMembershipId(), a.getDefaultAppCode()); a.getLastLoginAt(), a.getDefaultTenantId(), a.getDefaultMembershipId(), a.getDefaultAppCode());
} }
@@ -201,6 +253,7 @@ public class AccountManagementService {
private boolean sameIdentity(AuthAccount account, CreateAccountRequest request, String appCode) { private boolean sameIdentity(AuthAccount account, CreateAccountRequest request, String appCode) {
return Objects.equals(account.getUsername(), request.username()) return Objects.equals(account.getUsername(), request.username())
&& Objects.equals(account.getPhone(), request.phone())
&& Objects.equals(account.getDefaultTenantId(), request.tenantId()) && Objects.equals(account.getDefaultTenantId(), request.tenantId())
&& Objects.equals(account.getOperatorTenantId(), request.tenantId()) && Objects.equals(account.getOperatorTenantId(), request.tenantId())
&& Objects.equals(account.getDefaultMembershipId(), request.membershipId()) && Objects.equals(account.getDefaultMembershipId(), request.membershipId())
@@ -16,13 +16,14 @@ import com.baiyue.common.core.exception.BizException;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.jose.jws.MacAlgorithm; import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
import org.springframework.security.oauth2.jwt.JwtClaimsSet; import org.springframework.security.oauth2.jwt.JwtClaimsSet;
import org.springframework.security.oauth2.jwt.JwtEncoder; import org.springframework.security.oauth2.jwt.JwtEncoder;
import org.springframework.security.oauth2.jwt.JwtEncoderParameters; import org.springframework.security.oauth2.jwt.JwtEncoderParameters;
import org.springframework.security.oauth2.jwt.JwsHeader; import org.springframework.security.oauth2.jwt.JwsHeader;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import com.baiyue.auth.config.UnifiedJwtProperties;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.time.Instant; import java.time.Instant;
@@ -49,15 +50,20 @@ public class AuthService {
private AuthorizationLoader authorizationLoader; private AuthorizationLoader authorizationLoader;
@Resource @Resource
private TokenSessionStore tokenSessionStore; private TokenSessionStore tokenSessionStore;
@Resource
private UnifiedJwtProperties signingProperties;
private final SecureRandom secureRandom = new SecureRandom(); private final SecureRandom secureRandom = new SecureRandom();
@Transactional @Transactional
@BusinessAudit(event = "AUTH_LOGIN", resourceType = "ACCOUNT", resourceId = "#request.username", @BusinessAudit(event = "AUTH_LOGIN", resourceType = "ACCOUNT", resourceId = "#request.loginId",
description = "User login / 用户登录", captureAfter = false) description = "User login / 用户登录", captureAfter = false)
public TokenResponse login(LoginRequest request) { public TokenResponse login(LoginRequest request) {
AuthAccount account = accountRepository.findByUsername(request.username()); String loginId = request.loginId().trim();
AuthAccount account = loginId.matches("^1[3-9]\\d{9}$")
? accountRepository.findByPhone(loginId)
: accountRepository.findByUsername(loginId);
if (account == null) throw invalidCredentials(); if (account == null) throw invalidCredentials();
LocalDateTime now = LocalDateTime.now(); LocalDateTime now = LocalDateTime.now();
if (!"ACTIVE".equals(account.getAccountStatus())) if (!"ACTIVE".equals(account.getAccountStatus()))
@@ -86,7 +92,8 @@ public class AuthService {
AuthAccount account = accountRepository.findById(session.userId()); AuthAccount account = accountRepository.findById(session.userId());
if (account == null || !"ACTIVE".equals(account.getAccountStatus())) if (account == null || !"ACTIVE".equals(account.getAccountStatus()))
throw new BizException(ResultCode.UNAUTHORIZED, "账号不可用"); throw new BizException(ResultCode.UNAUTHORIZED, "账号不可用");
return issue(account); if (session.fingerprint() == null) throw new BizException(ResultCode.UNAUTHORIZED, "刷新会话缺少授权状态");
return issue(account, session.fingerprint());
} }
@Transactional @Transactional
@@ -103,17 +110,23 @@ public class AuthService {
accountRepository.save(account); accountRepository.save(account);
} }
private TokenResponse issue(AuthAccount account) { private TokenResponse issue(AuthAccount account) { return issue(account, null); }
private TokenResponse issue(AuthAccount account, String expectedState) {
AuthorizationSnapshot authorization = authorizationLoader.load(account); AuthorizationSnapshot authorization = authorizationLoader.load(account);
if (expectedState != null && !expectedState.equals(SessionStateService.fingerprint(account, authorization))) {
throw new BizException(ResultCode.UNAUTHORIZED, "授权状态已变更,请重新登录");
}
Instant issuedAt = Instant.now(); Instant issuedAt = Instant.now();
String jwtId = UUID.randomUUID().toString(); String jwtId = UUID.randomUUID().toString();
Instant expiresAt = issuedAt.plus(properties.accessTokenTtl()); Instant expiresAt = issuedAt.plus(properties.accessTokenTtl());
JwtClaimsSet claims = JwtClaimsSet.builder() JwtClaimsSet.Builder claimsBuilder = JwtClaimsSet.builder()
.issuer(properties.issuer()).audience(List.of(properties.audience())) .issuer(properties.issuer()).audience(List.of(properties.audience()))
.id(jwtId).issuedAt(issuedAt).expiresAt(expiresAt) .id(jwtId).issuedAt(issuedAt).expiresAt(expiresAt)
.subject(account.getId().toString()) .subject(account.getId().toString())
.claim("user_id", account.getId()) .claim("user_id", account.getId())
.claim("username", account.getUsername()) .claim("username", account.getUsername())
.claim("display_name", account.getDisplayName() == null ? "" : account.getDisplayName())
.claim("tenant_id", account.getDefaultTenantId()) .claim("tenant_id", account.getDefaultTenantId())
.claim("operator_tenant_id", account.getOperatorTenantId()) .claim("operator_tenant_id", account.getOperatorTenantId())
.claim("membership_id", account.getDefaultMembershipId()) .claim("membership_id", account.getDefaultMembershipId())
@@ -124,18 +137,26 @@ public class AuthService {
.claim("userId", account.getId()) .claim("userId", account.getId())
.claim("tenantId", account.getDefaultTenantId()) .claim("tenantId", account.getDefaultTenantId())
.claim("appCode", account.getDefaultAppCode()) .claim("appCode", account.getDefaultAppCode())
.claim("permissionVersion", account.getTokenVersion()) .claim("permissionVersion", account.getTokenVersion());
.build(); if (authorization.getOverseasWarehouseId() != null) {
claimsBuilder.claim("overseas_warehouse_id", authorization.getOverseasWarehouseId());
}
if (authorization.getDomesticWarehouseId() != null) {
claimsBuilder.claim("domestic_warehouse_id", authorization.getDomesticWarehouseId());
}
JwtClaimsSet claims = claimsBuilder.build();
String accessToken = jwtEncoder.encode(JwtEncoderParameters.from( String accessToken = jwtEncoder.encode(JwtEncoderParameters.from(
signingHeader(), claims)).getTokenValue(); signingHeader(), claims)).getTokenValue();
byte[] random = new byte[48]; byte[] random = new byte[48];
secureRandom.nextBytes(random); secureRandom.nextBytes(random);
String rawRefresh = Base64.getUrlEncoder().withoutPadding().encodeToString(random); String rawRefresh = Base64.getUrlEncoder().withoutPadding().encodeToString(random);
tokenSessionStore.store(jwtId, account.getId(), authorization.getRoles(), authorization.getPermissions(), rawRefresh, tokenSessionStore.store(jwtId, account.getId(), authorization.getRoles(), authorization.getPermissions(),
SessionStateService.fingerprint(account, authorization), rawRefresh,
properties.accessTokenTtl(), properties.refreshTokenTtl()); properties.accessTokenTtl(), properties.refreshTokenTtl());
return new TokenResponse("Bearer", accessToken, properties.accessTokenTtl().toSeconds(), return new TokenResponse("Bearer", accessToken, properties.accessTokenTtl().toSeconds(),
rawRefresh, properties.refreshTokenTtl().toSeconds()); rawRefresh, properties.refreshTokenTtl().toSeconds(),
authorization.getRoles(), authorization.getPermissions());
} }
@Transactional @Transactional
@@ -176,7 +197,7 @@ public class AuthService {
JwtClaimsSet claims = JwtClaimsSet.builder().issuer(properties.issuer()) JwtClaimsSet claims = JwtClaimsSet.builder().issuer(properties.issuer())
.audience(List.of(properties.audience())).id(jwtId) .audience(List.of(properties.audience())).id(jwtId)
.issuedAt(issuedAt).expiresAt(expiresAt).subject(account.getId().toString()) .issuedAt(issuedAt).expiresAt(expiresAt).subject(account.getId().toString())
.claim("user_id", account.getId()).claim("username", account.getUsername()) .claim("user_id", account.getId()).claim("username", account.getUsername()).claim("display_name", account.getDisplayName() == null ? "" : account.getDisplayName())
.claim("tenant_id", account.getDefaultTenantId()).claim("operator_tenant_id", account.getOperatorTenantId()) .claim("tenant_id", account.getDefaultTenantId()).claim("operator_tenant_id", account.getOperatorTenantId())
.claim("membership_id", account.getDefaultMembershipId()).claim("app_code", "MERCHANT") .claim("membership_id", account.getDefaultMembershipId()).claim("app_code", "MERCHANT")
.claim("client_type", authorization.getClientType()).claim("token_version", account.getTokenVersion()) .claim("client_type", authorization.getClientType()).claim("token_version", account.getTokenVersion())
@@ -188,18 +209,26 @@ public class AuthService {
.claim("password_change_required", Boolean.TRUE.equals(account.getPasswordChangeRequired())).build(); .claim("password_change_required", Boolean.TRUE.equals(account.getPasswordChangeRequired())).build();
String accessToken = jwtEncoder.encode(JwtEncoderParameters.from(signingHeader(), claims)).getTokenValue(); String accessToken = jwtEncoder.encode(JwtEncoderParameters.from(signingHeader(), claims)).getTokenValue();
tokenSessionStore.storeAccessOnly(jwtId, account.getId(), authorization.getRoles(), tokenSessionStore.storeAccessOnly(jwtId, account.getId(), authorization.getRoles(),
authorization.getPermissions(), Duration.ofMinutes(15)); authorization.getPermissions(), SessionStateService.fingerprint(account, authorization), Duration.ofMinutes(15));
return new ImpersonationTokenResponse("Bearer", accessToken, 900); return new ImpersonationTokenResponse("Bearer", accessToken, 900,
authorization.getRoles(), authorization.getPermissions());
} }
public record ImpersonationTokenResponse(String tokenType, String accessToken, long expiresIn) { } public record ImpersonationTokenResponse(String tokenType, String accessToken, long expiresIn,
java.util.Set<String> roles,
java.util.Set<String> authorities) {
public ImpersonationTokenResponse {
roles = roles == null ? java.util.Set.of() : java.util.Set.copyOf(roles);
authorities = authorities == null ? java.util.Set.of() : java.util.Set.copyOf(authorities);
}
}
private JwsHeader signingHeader() { private JwsHeader signingHeader() {
return JwsHeader.with(MacAlgorithm.HS256).build(); return JwsHeader.with(SignatureAlgorithm.RS256).keyId(signingProperties.currentKid()).build();
} }
private static BizException invalidCredentials() { private static BizException invalidCredentials() {
return new BizException(ResultCode.UNAUTHORIZED, "用户名或密码错误"); return new BizException(ResultCode.UNAUTHORIZED, "登录标识或密码错误");
} }
} }
@@ -35,8 +35,20 @@ public class MerchantRegistrationService {
@BusinessAudit(event = "MERCHANT_REGISTER", resourceType = "ACCOUNT", @BusinessAudit(event = "MERCHANT_REGISTER", resourceType = "ACCOUNT",
resourceId = "#request.username", description = "商户注册", captureAfter = false) resourceId = "#request.username", description = "商户注册", captureAfter = false)
public MerchantRegisterResponse register(MerchantRegisterRequest request) { public MerchantRegisterResponse register(MerchantRegisterRequest request) {
if (accountRepository.existsByUsername(request.username())) { AuthAccount usernameAccount = accountRepository.findByUsername(request.username());
throw new BizException(ResultCode.CONFLICT, "Username already exists"); AuthAccount phoneAccount = accountRepository.findByPhone(request.phone());
if (usernameAccount != null && phoneAccount != null
&& usernameAccount.getId().equals(phoneAccount.getId())
&& "MERCHANT".equals(usernameAccount.getDefaultAppCode())
&& "ACTIVE".equals(usernameAccount.getAccountStatus())
&& passwordEncoder.matches(request.password(), usernameAccount.getPasswordHash())) {
return existingRegistration(usernameAccount);
}
if (usernameAccount != null) {
throw new BizException(ResultCode.CONFLICT, "用户名已存在");
}
if (phoneAccount != null) {
throw new BizException(ResultCode.CONFLICT, "该手机号已注册,请直接登录后继续完善企业资质");
} }
long accountId = IdWorker.getId(); long accountId = IdWorker.getId();
String passwordHash = passwordEncoder.encode(request.password()); String passwordHash = passwordEncoder.encode(request.password());
@@ -46,7 +58,7 @@ public class MerchantRegistrationService {
try { try {
var response = onboardingClient.register(internalToken, var response = onboardingClient.register(internalToken,
new IamOnboardingClient.OnboardingRequest(accountId, request.displayName(), new IamOnboardingClient.OnboardingRequest(accountId, request.displayName(),
request.companyName(), request.legalPerson(), request.legalPhone(), request.companyName(), request.legalPerson(), request.phone(),
request.contactEmail(), request.countryCode(), request.address())); request.contactEmail(), request.countryCode(), request.address()));
if (response == null) { if (response == null) {
throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "IAM registration failed"); throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "IAM registration failed");
@@ -81,6 +93,7 @@ public class MerchantRegistrationService {
AuthAccount account = new AuthAccount(); AuthAccount account = new AuthAccount();
account.setId(accountId); account.setId(accountId);
account.setUsername(request.username()); account.setUsername(request.username());
account.setPhone(request.phone());
account.setPasswordHash(passwordHash); account.setPasswordHash(passwordHash);
account.setDisplayName(request.displayName()); account.setDisplayName(request.displayName());
account.setAccountStatus("ACTIVE"); account.setAccountStatus("ACTIVE");
@@ -94,7 +107,7 @@ public class MerchantRegistrationService {
try { try {
accountRepository.insert(account); accountRepository.insert(account);
} catch (DuplicateKeyException exception) { } catch (DuplicateKeyException exception) {
BizException failure = new BizException(ResultCode.CONFLICT, "Username already exists"); BizException failure = new BizException(ResultCode.CONFLICT, "用户名或手机号已存在");
failure.addSuppressed(exception); failure.addSuppressed(exception);
compensateOnboarding(accountId, failure, compensationCompleted); compensateOnboarding(accountId, failure, compensationCompleted);
throw failure; throw failure;
@@ -103,7 +116,26 @@ public class MerchantRegistrationService {
throw exception; throw exception;
} }
return new MerchantRegisterResponse(accountId, onboarding.tenantId(), onboarding.membershipId(), return new MerchantRegisterResponse(accountId, onboarding.tenantId(), onboarding.membershipId(),
onboarding.tenantStatus(), "LOGIN_AND_COMPLETE_QUALIFICATIONS"); onboarding.tenantStatus(), onboarding.dimensionInitializationStatus(),
"LOGIN_AND_COMPLETE_QUALIFICATIONS");
}
private MerchantRegisterResponse existingRegistration(AuthAccount account) {
try {
var response = onboardingClient.registration(internalToken, account.getId());
if (response == null || !response.successful() || response.data() == null) {
throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "注册状态暂时无法确认,请直接登录后继续完善企业资质");
}
var onboarding = response.data();
if (!java.util.Set.of("PROFILE_REQUIRED", "REJECTED").contains(onboarding.tenantStatus())) {
throw new BizException(ResultCode.CONFLICT, "该账号已注册,请直接登录");
}
return new MerchantRegisterResponse(account.getId(), onboarding.tenantId(), onboarding.membershipId(),
onboarding.tenantStatus(), onboarding.dimensionInitializationStatus(),
"LOGIN_AND_COMPLETE_QUALIFICATIONS");
} catch (FeignException exception) {
throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "注册状态暂时无法确认,请直接登录后继续完善企业资质");
}
} }
private void registerRollbackCompensation(long accountId, AtomicBoolean compensationCompleted) { private void registerRollbackCompensation(long accountId, AtomicBoolean compensationCompleted) {
@@ -1,56 +0,0 @@
package com.baiyue.auth.service;
import com.baiyue.auth.client.AuthorizationLoader;
import com.baiyue.auth.client.AuthorizationSnapshot;
import com.baiyue.auth.config.OverseasJwtProperties;
import com.baiyue.auth.entity.AuthAccount;
import com.baiyue.auth.repository.AuthAccountRepository;
import com.baiyue.common.core.api.ResultCode;
import com.baiyue.common.core.exception.BizException;
import jakarta.annotation.Resource;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
import org.springframework.security.oauth2.jwt.JwtEncoder;
import org.springframework.security.oauth2.jwt.JwtEncoderParameters;
import org.springframework.security.oauth2.jwt.JwsHeader;
import org.springframework.stereotype.Service;
import java.time.Instant;
import java.util.List;
import java.util.UUID;
/** 将已经通过国内 HMAC 与 Redis 校验的会话兑换为海外 RS256 短时令牌。 */
@Service
@ConditionalOnProperty(name = "baiyue.security.overseas-jwt.enabled", havingValue = "true")
public class OverseasTokenService {
@Resource private AuthAccountRepository accounts;
@Resource private AuthorizationLoader authorizationLoader;
@Resource private OverseasJwtProperties properties;
@Resource @Qualifier("overseasJwtEncoder") private JwtEncoder encoder;
public OverseasTokenResponse exchange(long authenticatedUserId) {
AuthAccount account = accounts.findById(authenticatedUserId);
if (account == null || !"ACTIVE".equals(account.getAccountStatus())) {
throw new BizException(ResultCode.UNAUTHORIZED, "账号不可用");
}
AuthorizationSnapshot authorization = authorizationLoader.load(account);
Instant issuedAt = Instant.now();
Instant expiresAt = issuedAt.plus(properties.accessTokenTtl());
JwtClaimsSet claims = JwtClaimsSet.builder()
.issuer(properties.issuer()).audience(List.of(properties.audience()))
.subject(account.getId().toString()).id(UUID.randomUUID().toString())
.issuedAt(issuedAt).expiresAt(expiresAt)
.claim("userId", account.getId()).claim("tenantId", account.getDefaultTenantId())
.claim("appCode", account.getDefaultAppCode()).claim("roles", authorization.getRoles())
.claim("authorities", authorization.getPermissions())
.claim("permissionVersion", authorization.getPermissionVersion() == null ? 0L : authorization.getPermissionVersion())
.build();
String token = encoder.encode(JwtEncoderParameters.from(
JwsHeader.with(SignatureAlgorithm.RS256).keyId(properties.currentKid()).build(), claims)).getTokenValue();
return new OverseasTokenResponse("Bearer", token, properties.accessTokenTtl().toSeconds());
}
public record OverseasTokenResponse(String tokenType, String accessToken, long expiresIn) { }
}
@@ -0,0 +1,63 @@
package com.baiyue.auth.service;
import com.baiyue.auth.client.AuthorizationLoader;
import com.baiyue.auth.client.AuthorizationSnapshot;
import com.baiyue.auth.entity.AuthAccount;
import com.baiyue.auth.repository.AuthAccountRepository;
import com.baiyue.common.core.api.ResultCode;
import com.baiyue.common.core.exception.BizException;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import java.security.MessageDigest;
import java.util.HexFormat;
import java.util.TreeSet;
/** No cache: a validation started after a security commit must observe that commit. */
@Service
public class SessionStateService {
private static final ObjectMapper FINGERPRINT_JSON = new ObjectMapper();
@Resource private AuthAccountRepository accounts;
@Resource private AuthorizationLoader authorizationLoader;
@Resource private TokenSessionStore sessions;
public static String fingerprint(AuthAccount account, AuthorizationSnapshot snapshot) {
try {
if (account.getTokenVersion() == null || snapshot.getPermissionVersion() == null
|| snapshot.getTenantVersion() == null) throw new IllegalArgumentException("Missing persisted security version");
// Ordered JSON array avoids set ordering and delimiter ambiguities.
Object[] state = {account.getId(), account.getTokenVersion(), account.getDefaultTenantId(),
account.getDefaultMembershipId(), account.getDefaultAppCode(), account.getOperatorTenantId(),
snapshot.getPermissionVersion(), snapshot.getTenantVersion(), snapshot.getClientType(),
snapshot.getOverseasWarehouseId(), snapshot.getDomesticWarehouseId(),
new TreeSet<>(snapshot.getRoles()), new TreeSet<>(snapshot.getPermissions())};
return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256")
.digest(FINGERPRINT_JSON.writeValueAsBytes(state)));
} catch (Exception failure) {
throw new IllegalStateException("Cannot fingerprint authorization state", failure);
}
}
private void requireCurrent(Long userId, String expected) {
AuthAccount account = accounts.findCurrentById(userId);
if (account == null || !"ACTIVE".equals(account.getAccountStatus())
|| Integer.valueOf(1).equals(account.getDeleted()) || expected == null
|| !expected.equals(fingerprint(account, authorizationLoader.load(account)))) {
throw new BizException(ResultCode.UNAUTHORIZED, "登录授权已变更,请重新登录");
}
}
@Transactional(readOnly = true, propagation = Propagation.REQUIRES_NEW,
isolation = org.springframework.transaction.annotation.Isolation.READ_COMMITTED)
public boolean validate(String jwtId, Long userId) {
TokenSessionStore.SessionState state = sessions.loadState(jwtId);
if (state == null || !state.userId().equals(userId)) {
throw new BizException(ResultCode.UNAUTHORIZED, "登录会话已失效");
}
requireCurrent(userId, state.fingerprint());
return true;
}
}
@@ -12,8 +12,6 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException; import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.SetOperations; import org.springframework.data.redis.core.SetOperations;
import org.springframework.data.redis.core.Cursor;
import org.springframework.data.redis.core.ScanOptions;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
@@ -21,8 +19,6 @@ import java.security.MessageDigest;
import java.time.Duration; import java.time.Duration;
import java.util.HexFormat; import java.util.HexFormat;
import java.util.Set; import java.util.Set;
import java.util.ArrayList;
import java.util.List;
/** /**
* 认证会话存储服务,使用 Redis 保存和一次性消费刷新会话,支持刷新令牌轮换及主动撤销。 * 认证会话存储服务,使用 Redis 保存和一次性消费刷新会话,支持刷新令牌轮换及主动撤销。
@@ -42,7 +38,7 @@ public class TokenSessionStore {
} }
public void store(String jwtId, Long userId, Set<String> roles, Set<String> permissions, public void store(String jwtId, Long userId, Set<String> roles, Set<String> permissions,
String rawRefreshToken, Duration accessTtl, Duration refreshTtl) { String fingerprint, String rawRefreshToken, Duration accessTtl, Duration refreshTtl) {
String accessKey = RedisTokenKeys.access(keyPrefix, jwtId); String accessKey = RedisTokenKeys.access(keyPrefix, jwtId);
String refreshKey = RedisTokenKeys.refresh(keyPrefix, sha256(rawRefreshToken)); String refreshKey = RedisTokenKeys.refresh(keyPrefix, sha256(rawRefreshToken));
String userKey = userSessionsKey(userId); String userKey = userSessionsKey(userId);
@@ -50,25 +46,27 @@ public class TokenSessionStore {
String accessValue = objectMapper.writeValueAsString( String accessValue = objectMapper.writeValueAsString(
new AccessSession(1, userId, normalized(roles), normalized(permissions))); new AccessSession(1, userId, normalized(roles), normalized(permissions)));
redisTemplate.opsForValue().set(accessKey, accessValue, accessTtl); redisTemplate.opsForValue().set(accessKey, accessValue, accessTtl);
String refreshValue = objectMapper.writeValueAsString(new RefreshSession(userId, jwtId)); storeState(jwtId, userId, fingerprint, accessTtl);
String refreshValue = objectMapper.writeValueAsString(new RefreshSession(userId, jwtId, fingerprint));
redisTemplate.opsForValue().set(refreshKey, refreshValue, refreshTtl); redisTemplate.opsForValue().set(refreshKey, refreshValue, refreshTtl);
SetOperations<String, String> sets = redisTemplate.opsForSet(); SetOperations<String, String> sets = redisTemplate.opsForSet();
if (sets != null) sets.add(userKey, accessKey, refreshKey); if (sets != null) sets.add(userKey, accessKey, refreshKey, stateKey(jwtId));
redisTemplate.expire(userKey, refreshTtl); redisTemplate.expire(userKey, refreshTtl);
} catch (Exception exception) { } catch (Exception exception) {
try { redisTemplate.delete(Set.of(accessKey, refreshKey, userKey)); } catch (DataAccessException ignored) { } try { redisTemplate.delete(Set.of(accessKey, refreshKey, stateKey(jwtId))); } catch (DataAccessException ignored) { }
throw unavailable(exception); throw unavailable(exception);
} }
} }
public void storeAccessOnly(String jwtId, Long userId, Set<String> roles, public void storeAccessOnly(String jwtId, Long userId, Set<String> roles,
Set<String> permissions, Duration ttl) { Set<String> permissions, String fingerprint, Duration ttl) {
try { try {
String accessKey = RedisTokenKeys.access(keyPrefix, jwtId); String accessKey = RedisTokenKeys.access(keyPrefix, jwtId);
String accessValue = objectMapper.writeValueAsString( String accessValue = objectMapper.writeValueAsString(
new AccessSession(1, userId, normalized(roles), normalized(permissions))); new AccessSession(1, userId, normalized(roles), normalized(permissions)));
redisTemplate.opsForValue().set(accessKey, accessValue, ttl); redisTemplate.opsForValue().set(accessKey, accessValue, ttl);
redisTemplate.opsForSet().add(userSessionsKey(userId), accessKey); storeState(jwtId, userId, fingerprint, ttl);
redisTemplate.opsForSet().add(userSessionsKey(userId), accessKey, stateKey(jwtId));
redisTemplate.expire(userSessionsKey(userId), ttl); redisTemplate.expire(userSessionsKey(userId), ttl);
} catch (Exception exception) { } catch (Exception exception) {
throw unavailable(exception); throw unavailable(exception);
@@ -103,6 +101,7 @@ public class TokenSessionStore {
if (value == null || value.isBlank()) return null; if (value == null || value.isBlank()) return null;
RefreshSession session = objectMapper.readValue(value, RefreshSession.class); RefreshSession session = objectMapper.readValue(value, RefreshSession.class);
redisTemplate.delete(RedisTokenKeys.access(keyPrefix, session.accessJwtId())); redisTemplate.delete(RedisTokenKeys.access(keyPrefix, session.accessJwtId()));
redisTemplate.delete(stateKey(session.accessJwtId()));
return session; return session;
} catch (DataAccessException exception) { } catch (DataAccessException exception) {
throw unavailable(exception); throw unavailable(exception);
@@ -121,43 +120,26 @@ public class TokenSessionStore {
try { try {
Set<String> keys = redisTemplate.opsForSet().members(userKey); Set<String> keys = redisTemplate.opsForSet().members(userKey);
if (keys != null && !keys.isEmpty()) redisTemplate.delete(keys); if (keys != null && !keys.isEmpty()) redisTemplate.delete(keys);
// 兼容功能上线前尚未写入用户会话索引的存量令牌,SCAN 不阻塞 Redis 主线程。
List<String> legacyKeys = new ArrayList<>();
collectLegacyKeys(keyPrefix + ":access:*", userId, legacyKeys, false);
collectLegacyKeys(keyPrefix + ":refresh:*", userId, legacyKeys, true);
if (!legacyKeys.isEmpty()) redisTemplate.delete(legacyKeys);
redisTemplate.delete(userKey); redisTemplate.delete(userKey);
} catch (DataAccessException exception) { } catch (DataAccessException exception) {
throw unavailable(exception); throw unavailable(exception);
} }
} }
private void collectLegacyKeys(String pattern, Long userId, List<String> result, boolean refresh) { private String stateKey(String jwtId) { return keyPrefix + ":state:" + jwtId; }
Cursor<String> cursor = redisTemplate.scan(ScanOptions.scanOptions().match(pattern).count(500).build());
if (cursor == null) return; private void storeState(String jwtId, Long userId, String fingerprint, Duration ttl) throws Exception {
try (cursor) { if (fingerprint == null || fingerprint.isBlank()) throw new IllegalArgumentException("Missing security state");
while (cursor.hasNext()) { redisTemplate.opsForValue().set(stateKey(jwtId),
String key = cursor.next(); objectMapper.writeValueAsString(new SessionState(userId, fingerprint)), ttl);
String value = redisTemplate.opsForValue().get(key);
if ((!refresh && accessBelongsTo(value, userId)) || (refresh && belongsTo(value, userId))) {
result.add(key);
}
}
} catch (Exception exception) {
throw unavailable(exception);
}
} }
private boolean belongsTo(String value, Long userId) { public SessionState loadState(String jwtId) {
if (value == null || value.isBlank()) return false; try {
try { return userId.equals(objectMapper.readValue(value, RefreshSession.class).userId()); } if (!Boolean.TRUE.equals(redisTemplate.hasKey(RedisTokenKeys.access(keyPrefix, jwtId)))) return null;
catch (Exception ignored) { return false; } String value = redisTemplate.opsForValue().get(stateKey(jwtId));
} return value == null ? null : objectMapper.readValue(value, SessionState.class);
} catch (Exception failure) { throw unavailable(failure); }
private boolean accessBelongsTo(String value, Long userId) {
if (value == null || value.isBlank()) return false;
try { return userId.equals(objectMapper.readValue(value, AccessSession.class).userId()); }
catch (Exception ignored) { return false; }
} }
private static Set<String> normalized(Set<String> values) { private static Set<String> normalized(Set<String> values) {
@@ -182,7 +164,8 @@ public class TokenSessionStore {
} }
} }
public record RefreshSession(Long userId, String accessJwtId) { } public record SessionState(Long userId, String fingerprint) { }
public record RefreshSession(Long userId, String accessJwtId, String fingerprint) { }
public record AccessSession(int version, Long userId, Set<String> roles, Set<String> permissions) { public record AccessSession(int version, Long userId, Set<String> roles, Set<String> permissions) {
public AccessSession { public AccessSession {
roles = normalized(roles); roles = normalized(roles);
@@ -1,6 +0,0 @@
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: ${AUTH_DB_URL:jdbc:mysql://127.0.0.1:3306/baiyue_auth?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true}
username: ${AUTH_DB_USERNAME:root}
password: ${AUTH_DB_PASSWORD:123456}
@@ -1,15 +0,0 @@
spring:
cloud:
nacos:
config:
namespace: ${NACOS_NAMESPACE:test}
group: ${NACOS_GROUP:BAIYUE}
encode: UTF-8
discovery:
namespace: ${NACOS_NAMESPACE:test}
group: ${NACOS_GROUP:BAIYUE}
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: ${AUTH_TEST_DB_URL:jdbc:mysql://${TEST_DB_HOST:8.163.30.128}:${TEST_DB_PORT:3306}/baiyue_auth?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true&rewriteBatchedStatements=true}
username: ${AUTH_TEST_DB_USERNAME:${TEST_DB_USERNAME:root}}
password: ${AUTH_TEST_DB_PASSWORD:${TEST_DB_PASSWORD}}
+17 -28
View File
@@ -2,36 +2,35 @@ server:
port: ${AUTH_PORT:9100} port: ${AUTH_PORT:9100}
spring: spring:
profiles: profiles:
active: ${SPRING_PROFILES_ACTIVE:dev} active: ${SPRING_PROFILES_ACTIVE:test}
application: application:
name: auth-service name: auth-service
config: config:
import: import:
- optional:nacos:baiyue-common.yml?group=BAIYUE&refreshEnabled=true - nacos:baiyue-common.yml?group=${NACOS_GROUP:BAIYUE}&refreshEnabled=true
- optional:nacos:${spring.application.name}.yml?group=BAIYUE&refreshEnabled=true - nacos:${spring.application.name}.yml?group=${NACOS_GROUP:BAIYUE}&refreshEnabled=true
data: data:
redis: redis:
host: ${REDIS_HOST:127.0.0.1} host: ${REDIS_HOST}
port: ${REDIS_PORT:6379} port: ${REDIS_PORT:6379}
password: ${REDIS_PASSWORD:} password: ${REDIS_PASSWORD:}
database: ${REDIS_DATABASE:0} database: ${REDIS_DATABASE:0}
timeout: ${REDIS_TIMEOUT:3s} timeout: ${REDIS_TIMEOUT:3s}
cloud: cloud:
nacos: nacos:
server-addr: ${NACOS_SERVER_ADDR:127.0.0.1:8848} server-addr: ${NACOS_SERVER_ADDR}
username: ${NACOS_USERNAME:nacos} username: ${NACOS_USERNAME}
password: ${NACOS_PASSWORD:nacos} password: ${NACOS_PASSWORD}
discovery: discovery:
enabled: ${NACOS_DISCOVERY_ENABLED:true} enabled: ${NACOS_DISCOVERY_ENABLED:true}
register-enabled: ${NACOS_REGISTER_ENABLED:true} register-enabled: ${NACOS_REGISTER_ENABLED:true}
group: ${NACOS_GROUP:BAIYUE} group: ${NACOS_GROUP:BAIYUE}
namespace: ${NACOS_NAMESPACE:public} namespace: ${NACOS_NAMESPACE}
config: config:
namespace: ${NACOS_NAMESPACE:public} namespace: ${NACOS_NAMESPACE}
encode: UTF-8 encode: UTF-8
baiyue.security: baiyue.security:
jwt-secret: ${JWT_SECRET:change-me-to-at-least-32-bytes-long-before-start} internal-service-token: ${INTERNAL_SERVICE_TOKEN}
internal-service-token: ${INTERNAL_SERVICE_TOKEN:change-me-to-an-internal-token-at-least-32-bytes-long}
token-key-prefix: ${REDIS_TOKEN_KEY_PREFIX:baiyue:auth} token-key-prefix: ${REDIS_TOKEN_KEY_PREFIX:baiyue:auth}
issuer: ${JWT_ISSUER:auth-service} issuer: ${JWT_ISSUER:auth-service}
audience: ${JWT_AUDIENCE:lastleg-api} audience: ${JWT_AUDIENCE:lastleg-api}
@@ -39,13 +38,12 @@ baiyue.security:
refresh-token-ttl: ${REFRESH_TOKEN_TTL:7d} refresh-token-ttl: ${REFRESH_TOKEN_TTL:7d}
max-login-failures: ${MAX_LOGIN_FAILURES:5} max-login-failures: ${MAX_LOGIN_FAILURES:5}
lock-duration: ${LOGIN_LOCK_DURATION:15m} lock-duration: ${LOGIN_LOCK_DURATION:15m}
overseas-jwt: jwt-mode: jwks
enabled: ${OVERSEAS_JWT_ENABLED:false} session-mode: redis
issuer: ${OVERSEAS_JWT_ISSUER:auth-service} jwk-set-uri: ${JWT_JWKS_URI:http://auth:9100/api/auth/.well-known/jwks.json}
audience: ${OVERSEAS_JWT_AUDIENCE:lastleg-api} jwt:
rsa-jwk-set: ${OVERSEAS_JWT_RSA_JWK_SET:} rsa-jwk-set: ${JWT_RSA_JWK_SET:}
current-kid: ${OVERSEAS_JWT_CURRENT_KID:} current-kid: ${JWT_CURRENT_KID:}
access-token-ttl: ${OVERSEAS_ACCESS_TOKEN_TTL:15m}
baiyue.clients: baiyue.clients:
iam: iam:
@@ -53,13 +51,4 @@ baiyue.clients:
management.endpoints.web.exposure.include: health,info,prometheus management.endpoints.web.exposure.include: health,info,prometheus
logging: baiyue.security.session-validation-uri: ${AUTH_SESSION_VALIDATION_URL:http://auth:9100/internal/auth/accounts/sessions/validate}
file:
name: "${LOGGING_FILE_NAME:${LOG_PATH:./logs}/${spring.application.name}.log}"
logback:
rollingpolicy:
file-name-pattern: "${LOGGING_ROLLING_FILE_NAME_PATTERN:${LOG_PATH:./logs}/${spring.application.name}.%d{yyyy-MM-dd}.%i.log.gz}"
max-file-size: "${LOGGING_ROLLING_MAX_FILE_SIZE:100MB}"
max-history: "${LOGGING_ROLLING_MAX_HISTORY:7}"
total-size-cap: "${LOGGING_ROLLING_TOTAL_SIZE_CAP:5GB}"
clean-history-on-start: "${LOGGING_ROLLING_CLEAN_HISTORY_ON_START:false}"
@@ -0,0 +1,46 @@
-- Auth 单一登录标识增量:用户名或中国大陆手机号。
-- 可重复执行;手机号仅从 IAM 当前用户档案回填,无法确认的账号保持 NULL。
USE baiyue_auth;
DROP PROCEDURE IF EXISTS auth_add_login_phone;
DELIMITER $$
CREATE PROCEDURE auth_add_login_phone()
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'baiyue_auth' AND table_name = 'auth_account' AND column_name = 'phone') THEN
ALTER TABLE auth_account ADD COLUMN phone VARCHAR(32) NULL COMMENT '全局唯一中国大陆登录手机号' AFTER username;
END IF;
END$$
DELIMITER ;
CALL auth_add_login_phone();
DROP PROCEDURE auth_add_login_phone;
DROP PROCEDURE IF EXISTS auth_backfill_login_phone;
DELIMITER $$
CREATE PROCEDURE auth_backfill_login_phone()
BEGIN
UPDATE auth_account
SET phone = NULL
WHERE phone IS NOT NULL AND phone NOT REGEXP '^1[3-9][0-9]{9}$';
UPDATE auth_account account
JOIN baiyue_iam.iam_user user ON user.auth_user_id = account.id AND user.deleted = 0
JOIN (
SELECT phone
FROM baiyue_iam.iam_user
WHERE deleted = 0 AND phone REGEXP '^1[3-9][0-9]{9}$'
GROUP BY phone
HAVING COUNT(DISTINCT auth_user_id) = 1
) unique_phone ON unique_phone.phone = user.phone
SET account.phone = user.phone
WHERE account.phone IS NULL AND user.phone REGEXP '^1[3-9][0-9]{9}$';
IF NOT EXISTS (
SELECT 1 FROM information_schema.statistics
WHERE table_schema = 'baiyue_auth' AND table_name = 'auth_account'
AND index_name = 'uk_auth_account_phone') THEN
ALTER TABLE auth_account ADD UNIQUE KEY uk_auth_account_phone (phone);
END IF;
END$$
DELIMITER ;
CALL auth_backfill_login_phone();
DROP PROCEDURE auth_backfill_login_phone;
+2
View File
@@ -6,6 +6,7 @@ USE baiyue_auth;
CREATE TABLE IF NOT EXISTS auth_account ( CREATE TABLE IF NOT EXISTS auth_account (
id BIGINT NOT NULL COMMENT '统一登录账号ID(雪花ID)', id BIGINT NOT NULL COMMENT '统一登录账号ID(雪花ID)',
username VARCHAR(64) NOT NULL COMMENT '全局唯一登录名', username VARCHAR(64) NOT NULL COMMENT '全局唯一登录名',
phone VARCHAR(32) DEFAULT NULL COMMENT '全局唯一中国大陆登录手机号',
password_hash VARCHAR(255) NOT NULL COMMENT 'BCrypt密码哈希,禁止保存明文密码', password_hash VARCHAR(255) NOT NULL COMMENT 'BCrypt密码哈希,禁止保存明文密码',
display_name VARCHAR(64) NOT NULL COMMENT '登录后展示名称', display_name VARCHAR(64) NOT NULL COMMENT '登录后展示名称',
account_status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE' COMMENT '账号状态:ACTIVE正常、DISABLED停用、LOCKED锁定', account_status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE' COMMENT '账号状态:ACTIVE正常、DISABLED停用、LOCKED锁定',
@@ -23,6 +24,7 @@ CREATE TABLE IF NOT EXISTS auth_account (
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间', updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
PRIMARY KEY (id), PRIMARY KEY (id),
UNIQUE KEY uk_auth_account_username (username), UNIQUE KEY uk_auth_account_username (username),
UNIQUE KEY uk_auth_account_phone (phone),
KEY idx_auth_account_status (account_status, locked_until) KEY idx_auth_account_status (account_status, locked_until)
) ENGINE=InnoDB COMMENT='统一登录账号表:保存平台端和商户端共用的认证凭据及账号安全状态'; ) ENGINE=InnoDB COMMENT='统一登录账号表:保存平台端和商户端共用的认证凭据及账号安全状态';
@@ -1,4 +1,4 @@
-- Phase-one local administrator accounts. -- Phase-one local platform administrator account. Merchant accounts are created through onboarding.
-- Development-only initial password: ChangeMe123! -- Development-only initial password: ChangeMe123!
-- The BCrypt hash is intentionally stored instead of plaintext. -- The BCrypt hash is intentionally stored instead of plaintext.
USE baiyue_auth; USE baiyue_auth;
@@ -10,9 +10,7 @@ INSERT INTO auth_account (
operator_tenant_id, default_membership_id, default_app_code, deleted operator_tenant_id, default_membership_id, default_app_code, deleted
) VALUES ) VALUES
(1000001, 'platform_admin', '$2a$12$AMZJjd88leYPLI9uKvjZiew2CTQXI32BXoHFX.FrCi28/IeZ7mZ96', (1000001, 'platform_admin', '$2a$12$AMZJjd88leYPLI9uKvjZiew2CTQXI32BXoHFX.FrCi28/IeZ7mZ96',
'平台系统管理员', 'ACTIVE', 0, 1, 0, 0, 0, 3000001, 'PLATFORM', 0), '平台系统管理员', 'ACTIVE', 0, 1, 0, 0, 0, 3000001, 'PLATFORM', 0)
(1000002, 'merchant_admin', '$2a$12$AMZJjd88leYPLI9uKvjZiew2CTQXI32BXoHFX.FrCi28/IeZ7mZ96',
'IT 测试客户管理员', 'ACTIVE', 0, 1, 0, 10001, 10001, 3000002, 'MERCHANT', 0)
ON DUPLICATE KEY UPDATE ON DUPLICATE KEY UPDATE
password_hash = VALUES(password_hash), password_hash = VALUES(password_hash),
display_name = VALUES(display_name), display_name = VALUES(display_name),
@@ -0,0 +1,39 @@
openapi: 3.0.3
info:
title: Auth internal session validation
version: 1.0.0
paths:
/internal/auth/accounts/sessions/validate:
post:
summary: Validate persisted account and IAM authorization state
description: Internal network only. No Gateway public route. Verifies the Redis Access session and state fingerprint against current Auth account and a consistent IAM snapshot. No positive cache; missing state, stale state and dependency failure deny access. Does not accept or forward a user Bearer token.
security:
- internalServiceToken: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [jwtId, userId]
properties:
jwtId: {type: string, minLength: 1}
userId: {type: integer, format: int64}
responses:
'200':
description: Only string code 200 and boolean data true constitute success. Business errors deny access.
content:
application/json:
schema:
type: object
required: [code]
properties:
code: {type: string}
message: {type: string}
data: {type: boolean, nullable: true}
components:
securitySchemes:
internalServiceToken:
type: apiKey
in: header
name: X-Internal-Service-Token
@@ -0,0 +1,96 @@
package com.baiyue.auth.client;
import feign.Client;
import feign.FeignException;
import feign.Request;
import feign.Response;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration;
import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
import org.springframework.boot.env.YamlPropertySourceLoader;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.cloud.openfeign.FeignAutoConfiguration;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.cloud.openfeign.FeignClientProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.FileSystemResource;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import static org.assertj.core.api.Assertions.*;
class AuthorizationFeignConfigurationContractTest {
@Test
void actualNamedNacosConfigMatchesContextIdAndOverridesGlobalFeignBudget() {
new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(FeignAutoConfiguration.class,
JacksonAutoConfiguration.class, HttpMessageConvertersAutoConfiguration.class))
.withUserConfiguration(OfflineFeignConfiguration.class)
.withPropertyValues("baiyue.clients.iam.url=http://127.0.0.1:1")
.withInitializer(context -> {
for (String file : new String[]{"baiyue-common.yml", "auth-service.yml"}) {
Path path = Path.of("config", "nacos", "test", file);
if (!Files.isRegularFile(path)) path = Path.of("..").resolve(path);
try {
// Load real configuration as properties only, without config-data imports.
for (var source : new YamlPropertySourceLoader().load(file, new FileSystemResource(path))) {
context.getEnvironment().getPropertySources().addLast(source);
}
} catch (java.io.IOException failure) {
throw new IllegalStateException("Cannot read actual Auth Feign configuration", failure);
}
}
})
.run(context -> {
assertThat(context).hasNotFailed();
String contextId = IamAuthorizationClient.class.getAnnotation(FeignClient.class).contextId();
var properties = context.getBean(FeignClientProperties.class);
var named = properties.getConfig().get(contextId);
assertThat(named).as("Nacos key must match actual @FeignClient contextId").isNotNull();
assertThat(named.getConnectTimeout()).isEqualTo(1000);
assertThat(named.getReadTimeout()).isEqualTo(2000);
assertThat(named.isFollowRedirects()).isFalse();
assertThat(properties.getConfig().get("default").getConnectTimeout()).isEqualTo(3000);
assertThat(properties.getConfig().get("default").getReadTimeout()).isEqualTo(5000);
var transport = context.getBean(CapturingClient.class);
var client = context.getBean(IamAuthorizationClient.class);
var request = new AuthorizationSnapshotRequest(1L, 7L, 9L, "MERCHANT");
client.load("offline-service-token", request);
assertThat(transport.options.get().connectTimeoutMillis()).isEqualTo(1000);
assertThat(transport.options.get().readTimeoutMillis()).isEqualTo(2000);
assertThat(transport.options.get().isFollowRedirects()).isFalse();
transport.status.set(503);
assertThatThrownBy(() -> client.load("offline-service-token", request)).isInstanceOf(FeignException.class);
assertThat(transport.calls.get()).as("One success plus one failure, without automatic retry").isEqualTo(2);
});
}
@Configuration(proxyBeanMethods = false)
@EnableFeignClients(clients = IamAuthorizationClient.class)
static class OfflineFeignConfiguration {
@Bean CapturingClient capturingClient() { return new CapturingClient(); }
}
/** The real Feign factory builds the client; only network I/O is replaced. */
static class CapturingClient implements Client {
final AtomicReference<Request.Options> options = new AtomicReference<>();
final AtomicInteger calls = new AtomicInteger();
final AtomicInteger status = new AtomicInteger(204);
@Override public Response execute(Request request, Request.Options effectiveOptions) {
options.set(effectiveOptions);
calls.incrementAndGet();
return Response.builder().request(request).status(status.get()).reason("offline")
.headers(Map.of()).build();
}
}
}
@@ -10,14 +10,14 @@ import static org.assertj.core.api.Assertions.assertThat;
class AuthTokenTtlConfigurationContractTest { class AuthTokenTtlConfigurationContractTest {
@Test @Test
void cnNacosConfigurationKeepsShortAccessAndSevenDayRefreshTokens() throws Exception { void testNacosConfigurationKeepsUnifiedShortAccessAndSevenDayRefreshTokens() throws Exception {
String configuration = Files.readString( String configuration = Files.readString(
Path.of("..", "config", "nacos", "cn", "auth-service.yml"), StandardCharsets.UTF_8); Path.of("..", "config", "nacos", "test", "auth-service.yml"), StandardCharsets.UTF_8);
assertThat(configuration) assertThat(configuration)
.contains("access-token-ttl: ${ACCESS_TOKEN_TTL:24m}") .contains("access-token-ttl: ${ACCESS_TOKEN_TTL:24m}")
.contains("refresh-token-ttl: ${REFRESH_TOKEN_TTL:7d}") .contains("refresh-token-ttl: ${REFRESH_TOKEN_TTL:7d}")
.contains("access-token-ttl: ${OVERSEAS_ACCESS_TOKEN_TTL:15m}") .doesNotContain("OVERSEAS_ACCESS_TOKEN_TTL")
.doesNotContain("access-token-ttl: ${ACCESS_TOKEN_TTL:7d}"); .doesNotContain("access-token-ttl: ${ACCESS_TOKEN_TTL:7d}");
} }
} }
@@ -1,29 +0,0 @@
package com.baiyue.auth.config;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.security.oauth2.jwt.JwtEncoder;
import static org.assertj.core.api.Assertions.assertThat;
class DualTrackJwtConfigurationTest {
private final ApplicationContextRunner runner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(DomesticJwtConfiguration.class, OverseasJwtConfiguration.class))
.withBean(AuthSecurityProperties.class, () -> new AuthSecurityProperties(
"domestic-secret-at-least-32-bytes-long", "auth-service", "lastleg-api",
java.time.Duration.ofMinutes(24), java.time.Duration.ofDays(7), 5,
java.time.Duration.ofMinutes(15)))
.withPropertyValues(
"baiyue.security.overseas-jwt.enabled=false");
@Test
void startsDomesticEncoderWithoutRsaConfiguration() {
runner.run(context -> {
assertThat(context).hasNotFailed();
assertThat(context).hasBean("jwtEncoder");
assertThat(context).doesNotHaveBean("overseasJwtEncoder");
assertThat(context).hasSingleBean(JwtEncoder.class);
});
}
}
@@ -0,0 +1,43 @@
package com.baiyue.auth.config;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.gen.RSAKeyGenerator;
import org.junit.jupiter.api.Test;
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
import org.springframework.security.oauth2.jwt.*;
import java.time.Instant;
import java.util.List;
import static org.assertj.core.api.Assertions.*;
class UnifiedJwtConfigurationTest {
@Test
void signsWithCurrentKeyAndRetainsPreviousPublicKeyForRotation() throws Exception {
var current = new RSAKeyGenerator(2048).keyID("current").algorithm(JWSAlgorithm.RS256).generate();
var previous = new RSAKeyGenerator(2048).keyID("previous").algorithm(JWSAlgorithm.RS256).generate();
var properties = new UnifiedJwtProperties(new JWKSet(List.of(current, previous.toPublicJWK())).toString(false), "current");
var encoder = new UnifiedJwtConfiguration().jwtEncoder(properties);
var now = Instant.now();
var token = encoder.encode(JwtEncoderParameters.from(
JwsHeader.with(SignatureAlgorithm.RS256).keyId("current").build(),
JwtClaimsSet.builder().subject("42").issuedAt(now).expiresAt(now.plusSeconds(60)).build()));
var decoded = NimbusJwtDecoder.withPublicKey(current.toRSAPublicKey()).build().decode(token.getTokenValue());
assertThat(decoded.getSubject()).isEqualTo("42");
assertThat(decoded.getHeaders()).containsEntry("kid", "current").containsEntry("alg", "RS256");
assertThatThrownBy(() -> NimbusJwtDecoder.withPublicKey(previous.toRSAPublicKey()).build().decode(token.getTokenValue()))
.isInstanceOf(JwtException.class);
}
@Test
void rejectsMissingPrivateKeyAndPreviousPrivateKey() throws Exception {
var current = new RSAKeyGenerator(2048).keyID("current").generate();
var previous = new RSAKeyGenerator(2048).keyID("previous").generate();
var config = new UnifiedJwtConfiguration();
assertThatThrownBy(() -> config.jwtEncoder(new UnifiedJwtProperties(new JWKSet(current.toPublicJWK()).toString(), "current")))
.isInstanceOf(IllegalStateException.class);
assertThatThrownBy(() -> config.jwtEncoder(new UnifiedJwtProperties(new JWKSet(List.of(current, previous)).toString(false), "current")))
.isInstanceOf(IllegalStateException.class);
assertThatThrownBy(() -> config.jwtEncoder(new UnifiedJwtProperties("", "current")))
.isInstanceOf(IllegalStateException.class);
}
}
@@ -1,6 +1,6 @@
package com.baiyue.auth.controller; package com.baiyue.auth.controller;
import com.baiyue.auth.config.OverseasJwtProperties; import com.baiyue.auth.config.UnifiedJwtProperties;
import com.nimbusds.jose.JWSAlgorithm; import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.jwk.JWKSet; import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.KeyUse; import com.nimbusds.jose.jwk.KeyUse;
@@ -20,8 +20,7 @@ class JwksControllerTest {
RSAKey current = key("current", true); RSAKey current = key("current", true);
RSAKey previous = key("previous", false); RSAKey previous = key("previous", false);
String json = new JWKSet(List.of(current, previous)).toString(false); String json = new JWKSet(List.of(current, previous)).toString(false);
var properties = new OverseasJwtProperties(true, "auth-service", "lastleg-api", json, var properties = new UnifiedJwtProperties(json, "current");
"current", Duration.ofMinutes(15));
var body = new JwksController(properties).jwks().getBody(); var body = new JwksController(properties).jwks().getBody();
String published = new com.fasterxml.jackson.databind.ObjectMapper().writeValueAsString(body); String published = new com.fasterxml.jackson.databind.ObjectMapper().writeValueAsString(body);
@@ -0,0 +1,43 @@
package com.baiyue.auth.repository;
import org.junit.jupiter.api.Test;
import org.springframework.core.io.ClassPathResource;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.assertj.core.api.Assertions.assertThat;
class AuthLoginIdentifiersSqlContractTest {
@Test
void legacyMigrationBackfillsOnlyUniquelyOwnedPhonesAndKeepsDuplicatesUnbound() throws Exception {
assertSafeBackfill(new ClassPathResource("db/auth-login-identifiers.sql")
.getContentAsString(StandardCharsets.UTF_8));
}
@Test
void emptyDatabaseInitializationDeclaresPhoneUniquenessWithoutLegacyBackfill() throws Exception {
String sql = Files.readString(unifiedSql(), StandardCharsets.UTF_8);
assertThat(sql)
.contains("phone varchar(32) DEFAULT NULL")
.contains("UNIQUE KEY uk_auth_account_phone (phone)")
.doesNotContain("phone NOT REGEXP '^1[3-9][0-9]{9}$'")
.doesNotContain("HAVING COUNT(DISTINCT auth_user_id) = 1")
.doesNotContain("ALTER TABLE auth_account");
}
private static void assertSafeBackfill(String sql) {
assertThat(sql)
.contains("phone NOT REGEXP '^1[3-9][0-9]{9}$'")
.contains("HAVING COUNT(DISTINCT auth_user_id) = 1")
.contains("ADD UNIQUE KEY uk_auth_account_phone (phone)")
.doesNotContain("IAM 存在重复手机号,禁止建立 Auth 唯一登录索引");
}
private static Path unifiedSql() {
Path path = Path.of("deploy", "sql", "头程数据库统一增量-MySQL8.sql");
return Files.exists(path) ? path : Path.of("..", "deploy", "sql", "头程数据库统一增量-MySQL8.sql");
}
}
@@ -11,7 +11,7 @@ import static org.assertj.core.api.Assertions.assertThat;
class InitialAccountSeedSqlContractTest { class InitialAccountSeedSqlContractTest {
@Test @Test
void initializesOnlyThePlatformAndItTestAdministratorAccounts() throws Exception { void initializesOnlyThePlatformAdministratorAccount() throws Exception {
assertAccountSeed(new ClassPathResource("db/phase1-admin-accounts.sql") assertAccountSeed(new ClassPathResource("db/phase1-admin-accounts.sql")
.getContentAsString(StandardCharsets.UTF_8)); .getContentAsString(StandardCharsets.UTF_8));
assertAccountSeed(Files.readString(unifiedSql(), StandardCharsets.UTF_8)); assertAccountSeed(Files.readString(unifiedSql(), StandardCharsets.UTF_8));
@@ -19,11 +19,12 @@ class InitialAccountSeedSqlContractTest {
private static void assertAccountSeed(String sql) { private static void assertAccountSeed(String sql) {
assertThat(sql) assertThat(sql)
.contains("(1000001, 'platform_admin'") .containsPattern("\\(1000001\\s*,\\s*'platform_admin'")
.contains("'平台系统管理员', 'ACTIVE'") .containsPattern("'平台系统管理员'\\s*,\\s*'ACTIVE'")
.contains("(1000002, 'merchant_admin'")
.contains("'IT 测试客户管理员', 'ACTIVE'")
.contains("password_change_required") .contains("password_change_required")
.doesNotContain("merchant_admin")
.doesNotContain("IT_TEST")
.doesNotContain("(1000002, 'merchant_admin'")
.doesNotContain("一期演示企业管理员"); .doesNotContain("一期演示企业管理员");
} }
@@ -0,0 +1,48 @@
package com.baiyue.auth.repository;
import com.baiyue.auth.dto.AccountManagementDtos.ChangeLoginIdentifiersRequest;
import com.baiyue.auth.dto.MerchantRegisterRequest;
import jakarta.validation.Validation;
import org.junit.jupiter.api.Test;
import org.springframework.core.io.ClassPathResource;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import static org.assertj.core.api.Assertions.assertThat;
class LoginIdentifierContractTest {
@Test
void registrationExposesAnAccountPhoneInsteadOfALegalPersonPhoneAlias() {
var componentNames = Arrays.stream(MerchantRegisterRequest.class.getRecordComponents())
.map(component -> component.getName())
.toList();
assertThat(componentNames).contains("phone").doesNotContain("legalPhone");
}
@Test
void mainlandMobileRuleRejectsPlaceholderNumbersAcrossJavaAndSql() throws Exception {
try (var factory = Validation.buildDefaultValidatorFactory()) {
var validator = factory.getValidator();
var invalidRegistration = new MerchantRegisterRequest(
"merchant.user", "Password1", "测试用户", "测试企业", "张三",
"11111111111", null, "CN", null);
var validChange = new ChangeLoginIdentifiersRequest(
"merchant.user", "13800138000", "Password1");
var invalidChange = new ChangeLoginIdentifiersRequest(
"merchant.user", "11111111111", "Password1");
assertThat(validator.validate(invalidRegistration)).isNotEmpty();
assertThat(validator.validate(validChange)).isEmpty();
assertThat(validator.validate(invalidChange)).isNotEmpty();
}
String sql = new ClassPathResource("db/auth-login-identifiers.sql")
.getContentAsString(StandardCharsets.UTF_8);
assertThat(sql)
.contains("REGEXP '^1[3-9][0-9]{9}$'")
.doesNotContain("REGEXP '^1[0-9]{10}$'");
}
}
@@ -4,8 +4,10 @@ import com.baiyue.auth.dto.AccountManagementDtos.ChangePasswordRequest;
import com.baiyue.auth.dto.AccountManagementDtos.ForcedPasswordChangeRequest; import com.baiyue.auth.dto.AccountManagementDtos.ForcedPasswordChangeRequest;
import com.baiyue.auth.dto.AccountManagementDtos.CreateAccountRequest; import com.baiyue.auth.dto.AccountManagementDtos.CreateAccountRequest;
import com.baiyue.auth.dto.AccountManagementDtos.UpdateAccountProfileRequest; import com.baiyue.auth.dto.AccountManagementDtos.UpdateAccountProfileRequest;
import com.baiyue.auth.dto.AccountManagementDtos.UpdateAccountPhoneRequest;
import com.baiyue.auth.dto.AccountManagementDtos.UpdateAccountStatusRequest; import com.baiyue.auth.dto.AccountManagementDtos.UpdateAccountStatusRequest;
import com.baiyue.auth.dto.AccountManagementDtos.UpdateAccountScopeRequest; import com.baiyue.auth.dto.AccountManagementDtos.UpdateAccountScopeRequest;
import com.baiyue.auth.dto.AccountManagementDtos.ChangeLoginIdentifiersRequest;
import com.baiyue.auth.entity.AuthAccount; import com.baiyue.auth.entity.AuthAccount;
import com.baiyue.auth.repository.AuthAccountRepository; import com.baiyue.auth.repository.AuthAccountRepository;
import com.baiyue.common.core.exception.BizException; import com.baiyue.common.core.exception.BizException;
@@ -18,6 +20,7 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import org.springframework.dao.DuplicateKeyException;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
@@ -83,7 +86,7 @@ class AccountManagementServiceTest {
} }
@Test void createPlatformAccountPersistsPlatformAppCodeAndPasswordHashOnly() { @Test void createPlatformAccountPersistsPlatformAppCodeAndPasswordHashOnly() {
when(repository.findById(20L)).thenReturn(null); when(repository.findCurrentById(20L)).thenReturn(null);
when(repository.existsByUsername("platform.user")).thenReturn(false); when(repository.existsByUsername("platform.user")).thenReturn(false);
var result = service.create(new CreateAccountRequest(20L, "platform.user", "Password1", var result = service.create(new CreateAccountRequest(20L, "platform.user", "Password1",
@@ -95,6 +98,7 @@ class AccountManagementServiceTest {
assertEquals("PLATFORM", captor.getValue().getDefaultAppCode()); assertEquals("PLATFORM", captor.getValue().getDefaultAppCode());
assertTrue(encoder.matches("Password1", captor.getValue().getPasswordHash())); assertTrue(encoder.matches("Password1", captor.getValue().getPasswordHash()));
assertNotEquals("Password1", captor.getValue().getPasswordHash()); assertNotEquals("Password1", captor.getValue().getPasswordHash());
verify(repository, never()).findById(20L);
} }
@Test void usernameAvailabilityUsesTheAuthAccountRepositoryAsTheFactSource() { @Test void usernameAvailabilityUsesTheAuthAccountRepositoryAsTheFactSource() {
@@ -105,7 +109,7 @@ class AccountManagementServiceTest {
} }
@Test void createDisabledAccountAndSummaryExposeLastLoginAndScope() { @Test void createDisabledAccountAndSummaryExposeLastLoginAndScope() {
when(repository.findById(20L)).thenReturn(null); when(repository.findCurrentById(20L)).thenReturn(null);
when(repository.existsByUsername("merchant.user")).thenReturn(false); when(repository.existsByUsername("merchant.user")).thenReturn(false);
var created = service.create(new CreateAccountRequest(20L, "merchant.user", "Password1", var created = service.create(new CreateAccountRequest(20L, "merchant.user", "Password1",
@@ -139,6 +143,7 @@ class AccountManagementServiceTest {
account.setUsername("platform.user"); account.setDisplayName("平台用户"); account.setUsername("platform.user"); account.setDisplayName("平台用户");
account.setDefaultTenantId(0L); account.setOperatorTenantId(0L); account.setDefaultTenantId(0L); account.setOperatorTenantId(0L);
account.setDefaultMembershipId(30L); account.setDefaultAppCode("PLATFORM"); account.setDeleted(0); account.setDefaultMembershipId(30L); account.setDefaultAppCode("PLATFORM"); account.setDeleted(0);
when(repository.findCurrentById(10L)).thenReturn(account);
var result = service.create(new CreateAccountRequest(10L, "platform.user", "Password1", var result = service.create(new CreateAccountRequest(10L, "platform.user", "Password1",
"平台用户", 0L, 30L, "PLATFORM")); "平台用户", 0L, 30L, "PLATFORM"));
@@ -147,6 +152,44 @@ class AccountManagementServiceTest {
verify(repository, never()).insert(any()); verify(repository, never()).insert(any());
} }
@Test void concurrentInsertOfSameIdentityReturnsDurableWinner() {
account.setUsername("platform.user"); account.setDefaultTenantId(0L);
account.setOperatorTenantId(0L); account.setDefaultMembershipId(30L);
account.setDefaultAppCode("PLATFORM"); account.setDeleted(0);
when(repository.findCurrentById(10L)).thenReturn(null);
doThrow(new DuplicateKeyException("concurrent insert")).when(repository).insert(any());
var result = service.create(new CreateAccountRequest(10L, "platform.user", "Password1",
"平台用户", 0L, 30L, "PLATFORM"));
assertEquals(10L, result.accountId());
var order = inOrder(repository);
order.verify(repository).findCurrentById(10L);
order.verify(repository).existsByUsername("platform.user");
order.verify(repository).insert(any());
order.verify(repository).findById(10L);
verify(repository, never()).save(any());
}
@Test void concurrentInsertOfDifferentIdentityIsRejected() {
when(repository.findCurrentById(10L)).thenReturn(null);
doThrow(new DuplicateKeyException("concurrent insert")).when(repository).insert(any());
assertThrows(BizException.class, () -> service.create(new CreateAccountRequest(10L,
"platform.user", "Password1", "平台用户", 0L, 30L, "PLATFORM")));
verify(repository, never()).save(any());
}
@Test void concurrentUsernameConflictDoesNotCreateOrReturnAnotherAccount() {
when(repository.findCurrentById(20L)).thenReturn(null);
when(repository.findById(20L)).thenReturn(null);
doThrow(new DuplicateKeyException("username conflict")).when(repository).insert(any());
assertThrows(BizException.class, () -> service.create(new CreateAccountRequest(20L,
"platform.user", "Password1", "平台用户", 0L, 30L, "PLATFORM")));
verify(repository, never()).save(any());
}
@Test void synchronizeDisplayNameRevokesExistingSessions() { @Test void synchronizeDisplayNameRevokesExistingSessions() {
account.setDisplayName("旧名称"); account.setDisplayName("旧名称");
@@ -157,6 +200,44 @@ class AccountManagementServiceTest {
verify(sessions).revokeAll(10L); verify(sessions).revokeAll(10L);
} }
@Test void changeLoginIdentifiersRequiresCurrentPasswordAndRevokesSessions() {
account.setUsername("old.user");
account.setPhone("13800138000");
when(repository.existsByUsernameForOtherAccount("new.user", 10L)).thenReturn(false);
when(repository.existsByPhoneForOtherAccount("13900139000", 10L)).thenReturn(false);
var result = service.changeLoginIdentifiers(10L,
new ChangeLoginIdentifiersRequest("new.user", "13900139000", "OldPass1"));
assertEquals("new.user", result.username());
assertEquals("13900139000", result.phone());
assertEquals(3, account.getTokenVersion());
verify(sessions).revokeAll(10L);
}
@Test void managedPhoneChangeOrRemovalRevokesOnlyThatAccountsSessions() {
account.setPhone("13800138000");
service.updatePhone(10L, new UpdateAccountPhoneRequest(null));
assertEquals(null, account.getPhone());
assertEquals(3, account.getTokenVersion());
verify(repository).save(account);
verify(sessions).revokeAll(10L);
}
@Test void duplicatePhoneDoesNotChangeLoginIdentifiers() {
account.setUsername("old.user");
account.setPhone("13800138000");
when(repository.existsByUsernameForOtherAccount("new.user", 10L)).thenReturn(false);
when(repository.existsByPhoneForOtherAccount("13900139000", 10L)).thenReturn(true);
assertThrows(BizException.class, () -> service.changeLoginIdentifiers(10L,
new ChangeLoginIdentifiersRequest("new.user", "13900139000", "OldPass1")));
verify(repository, never()).save(any());
verify(sessions, never()).revokeAll(any());
}
@Test void rbacChangeRevokesAccessAndRefreshSessionsAndBumpsTokenVersions() { @Test void rbacChangeRevokesAccessAndRefreshSessionsAndBumpsTokenVersions() {
AuthAccount second = new AuthAccount(); AuthAccount second = new AuthAccount();
second.setId(11L); second.setTokenVersion(7); second.setAccountStatus("ACTIVE"); second.setId(11L); second.setTokenVersion(7); second.setAccountStatus("ACTIVE");
@@ -12,7 +12,7 @@ import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.jose.jws.MacAlgorithm; import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtEncoder; import org.springframework.security.oauth2.jwt.JwtEncoder;
import org.springframework.security.oauth2.jwt.JwtEncoderParameters; import org.springframework.security.oauth2.jwt.JwtEncoderParameters;
@@ -23,7 +23,10 @@ import java.time.Duration;
import java.util.Set; import java.util.Set;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
@@ -38,16 +41,20 @@ class AuthServiceSmallJwtTest {
@Configuration(proxyBeanMethods = false) @Configuration(proxyBeanMethods = false)
static class TestConfig { static class TestConfig {
@Bean
com.baiyue.auth.config.UnifiedJwtProperties signingProperties() {
return new com.baiyue.auth.config.UnifiedJwtProperties("unused", "current");
}
@Bean @Bean
AuthSecurityProperties properties() { AuthSecurityProperties properties() {
return new AuthSecurityProperties("unused", "auth-service", "lastleg-api", return new AuthSecurityProperties("auth-service", "lastleg-api",
Duration.ofMinutes(30), Duration.ofMinutes(30),
Duration.ofDays(7), 5, Duration.ofMinutes(10)); Duration.ofDays(7), 5, Duration.ofMinutes(10));
} }
} }
@Test @Test
void signsCrossRegionClaimsAndStoresSession() { void signsUnifiedClaimsAndStoresSession() {
AuthAccount account = new AuthAccount(); AuthAccount account = new AuthAccount();
account.setId(42L); account.setId(42L);
account.setUsername("operator"); account.setUsername("operator");
@@ -59,7 +66,7 @@ class AuthServiceSmallJwtTest {
account.setDefaultAppCode("PLATFORM"); account.setDefaultAppCode("PLATFORM");
account.setTokenVersion(1); account.setTokenVersion(1);
AuthorizationSnapshot snapshot = new AuthorizationSnapshot(42L, 0L, 84L, "PLATFORM", AuthorizationSnapshot snapshot = new AuthorizationSnapshot(42L, 0L, 84L, "PLATFORM",
"WEB", Set.of("PLATFORM_ADMIN"), Set.of("platform:order:list")); "WEB", Set.of("OVERSEAS_WAREHOUSE_PDA_OPERATOR"), Set.of("platform:order:list"), 1L, 900L);
Jwt encoded = org.mockito.Mockito.mock(Jwt.class); Jwt encoded = org.mockito.Mockito.mock(Jwt.class);
when(repository.findByUsername("operator")).thenReturn(account); when(repository.findByUsername("operator")).thenReturn(account);
@@ -68,7 +75,7 @@ class AuthServiceSmallJwtTest {
when(jwtEncoder.encode(any())).thenReturn(encoded); when(jwtEncoder.encode(any())).thenReturn(encoded);
when(encoded.getTokenValue()).thenReturn("small-token"); when(encoded.getTokenValue()).thenReturn("small-token");
service.login(new LoginRequest("operator", "secret")); var response = service.login(new LoginRequest("operator", "secret"));
ArgumentCaptor<JwtEncoderParameters> jwt = ArgumentCaptor.forClass(JwtEncoderParameters.class); ArgumentCaptor<JwtEncoderParameters> jwt = ArgumentCaptor.forClass(JwtEncoderParameters.class);
verify(jwtEncoder).encode(jwt.capture()); verify(jwtEncoder).encode(jwt.capture());
@@ -79,15 +86,111 @@ class AuthServiceSmallJwtTest {
.containsEntry("appCode", "PLATFORM") .containsEntry("appCode", "PLATFORM")
.containsEntry("permissionVersion", 1) .containsEntry("permissionVersion", 1)
.containsEntry("membership_id", 84L) .containsEntry("membership_id", 84L)
.containsEntry("overseas_warehouse_id", 900L)
.doesNotContainKeys("roles", "authorities", "permissions", "scope"); .doesNotContainKeys("roles", "authorities", "permissions", "scope");
assertThat(jwt.getValue().getClaims().getAudience()).containsExactly("lastleg-api"); assertThat(jwt.getValue().getClaims().getAudience()).containsExactly("lastleg-api");
assertThat(jwt.getValue().getJwsHeader().getAlgorithm()).isEqualTo(MacAlgorithm.HS256); assertThat(jwt.getValue().getJwsHeader().getAlgorithm()).isEqualTo(SignatureAlgorithm.RS256);
assertThat(jwt.getValue().getJwsHeader().getKeyId()).isNull(); assertThat(jwt.getValue().getJwsHeader().getKeyId()).isEqualTo("current");
verify(tokenSessionStore).store(any(), org.mockito.ArgumentMatchers.eq(42L), verify(tokenSessionStore).store(any(), org.mockito.ArgumentMatchers.eq(42L),
org.mockito.ArgumentMatchers.eq(Set.of("PLATFORM_ADMIN")), org.mockito.ArgumentMatchers.eq(Set.of("OVERSEAS_WAREHOUSE_PDA_OPERATOR")),
org.mockito.ArgumentMatchers.eq(Set.of("platform:order:list")), any(), org.mockito.ArgumentMatchers.eq(Set.of("platform:order:list")), anyString(), any(),
org.mockito.ArgumentMatchers.eq(Duration.ofMinutes(30)), org.mockito.ArgumentMatchers.eq(Duration.ofMinutes(30)),
org.mockito.ArgumentMatchers.eq(Duration.ofDays(7))); org.mockito.ArgumentMatchers.eq(Duration.ofDays(7)));
assertThat(response.roles()).containsExactly("OVERSEAS_WAREHOUSE_PDA_OPERATOR");
assertThat(response.authorities()).containsExactly("platform:order:list");
}
@Test
void mainlandPhoneIsResolvedAsTheSinglePhoneLoginIdentifier() {
AuthAccount account = new AuthAccount();
account.setId(42L);
account.setUsername("operator");
account.setPhone("13800138000");
account.setPasswordHash("hash");
account.setAccountStatus("ACTIVE");
account.setDefaultTenantId(0L);
account.setOperatorTenantId(0L);
account.setDefaultMembershipId(84L);
account.setDefaultAppCode("PLATFORM");
account.setTokenVersion(1);
AuthorizationSnapshot snapshot = new AuthorizationSnapshot(42L, 0L, 84L, "PLATFORM",
"WEB", Set.of(), Set.of(), 1L);
Jwt encoded = org.mockito.Mockito.mock(Jwt.class);
when(repository.findByPhone("13800138000")).thenReturn(account);
when(passwordEncoder.matches("secret", "hash")).thenReturn(true);
when(authorizationLoader.load(account)).thenReturn(snapshot);
when(jwtEncoder.encode(any())).thenReturn(encoded);
when(encoded.getTokenValue()).thenReturn("small-token");
service.login(new LoginRequest("13800138000", "secret"));
verify(repository).findByPhone("13800138000");
verify(repository, never()).findByUsername("13800138000");
}
@Test
void placeholderNumberIsNeverResolvedAsAMobileLoginIdentifier() {
when(repository.findByUsername("11111111111")).thenReturn(null);
assertThatThrownBy(() -> service.login(new LoginRequest("11111111111", "secret")))
.isInstanceOf(com.baiyue.common.core.exception.BizException.class)
.hasMessageContaining("登录标识或密码错误");
verify(repository).findByUsername("11111111111");
verify(repository, never()).findByPhone("11111111111");
}
@Test
void refreshRotatesUnifiedTokenFromCurrentAuthorizationSnapshot() {
AuthAccount account = new AuthAccount();
account.setId(52L);
account.setUsername("merchant");
account.setAccountStatus("ACTIVE");
account.setDefaultTenantId(10001L);
account.setOperatorTenantId(10001L);
account.setDefaultMembershipId(94L);
account.setDefaultAppCode("MERCHANT");
account.setTokenVersion(3);
AuthorizationSnapshot snapshot = new AuthorizationSnapshot(52L, 10001L, 94L, "MERCHANT",
"WEB", Set.of("MERCHANT_ADMIN"), Set.of("merchant:order:list"), 3L);
Jwt encoded = org.mockito.Mockito.mock(Jwt.class);
when(tokenSessionStore.consume("refresh-old")).thenReturn(
new TokenSessionStore.RefreshSession(52L, "refresh-session", SessionStateService.fingerprint(account, snapshot)));
when(repository.findById(52L)).thenReturn(account);
when(authorizationLoader.load(account)).thenReturn(snapshot);
when(jwtEncoder.encode(any())).thenReturn(encoded);
when(encoded.getTokenValue()).thenReturn("domestic-refreshed");
var response = service.refresh("refresh-old");
assertThat(response.accessToken()).isEqualTo("domestic-refreshed");
assertThat(response.tokenType()).isEqualTo("Bearer");
assertThat(response.expiresIn()).isEqualTo(1800);
assertThat(response.refreshToken()).isNotBlank().isNotEqualTo("refresh-old");
assertThat(response.refreshExpiresIn()).isEqualTo(604800);
assertThat(response.roles()).containsExactly("MERCHANT_ADMIN");
assertThat(response.authorities()).containsExactly("merchant:order:list");
verify(tokenSessionStore).consume("refresh-old");
verify(tokenSessionStore).store(anyString(), org.mockito.ArgumentMatchers.eq(52L),
org.mockito.ArgumentMatchers.eq(Set.of("MERCHANT_ADMIN")),
org.mockito.ArgumentMatchers.eq(Set.of("merchant:order:list")),
anyString(), org.mockito.ArgumentMatchers.eq(response.refreshToken()),
org.mockito.ArgumentMatchers.eq(Duration.ofMinutes(30)),
org.mockito.ArgumentMatchers.eq(Duration.ofDays(7)));
verify(authorizationLoader).load(account);
}
@Test
void consumedRefreshTokenCannotMintAnotherDomesticOrOverseasPair() {
when(tokenSessionStore.consume("refresh-consumed")).thenReturn(null);
assertThatThrownBy(() -> service.refresh("refresh-consumed"))
.isInstanceOf(com.baiyue.common.core.exception.BizException.class)
.hasMessageContaining("刷新令牌无效或已过期");
verify(repository, never()).findById(any());
verify(jwtEncoder, never()).encode(any());
} }
@Test @Test
@@ -102,7 +205,7 @@ class AuthServiceSmallJwtTest {
account.setDefaultAppCode("MERCHANT"); account.setDefaultAppCode("MERCHANT");
account.setTokenVersion(2); account.setTokenVersion(2);
AuthorizationSnapshot snapshot = new AuthorizationSnapshot(43L, 10001L, 85L, "MERCHANT", AuthorizationSnapshot snapshot = new AuthorizationSnapshot(43L, 10001L, 85L, "MERCHANT",
"WEB", Set.of("MERCHANT_OWNER"), Set.of("merchant:order:list")); "WEB", Set.of("MERCHANT_OWNER"), Set.of("merchant:order:list"), 2L);
Jwt encoded = org.mockito.Mockito.mock(Jwt.class); Jwt encoded = org.mockito.Mockito.mock(Jwt.class);
TokenSessionStore.ImpersonationTicket ticket = new TokenSessionStore.ImpersonationTicket( TokenSessionStore.ImpersonationTicket ticket = new TokenSessionStore.ImpersonationTicket(
"ticket-1", 43L, 10001L, 85L, 42L, 84L, "support"); "ticket-1", 43L, 10001L, 85L, 42L, 84L, "support");
@@ -113,7 +216,10 @@ class AuthServiceSmallJwtTest {
when(jwtEncoder.encode(any())).thenReturn(encoded); when(jwtEncoder.encode(any())).thenReturn(encoded);
when(encoded.getTokenValue()).thenReturn("small-impersonation-token"); when(encoded.getTokenValue()).thenReturn("small-impersonation-token");
service.exchangeImpersonationTicket("one-time-code"); AuthService.ImpersonationTokenResponse response = service.exchangeImpersonationTicket("one-time-code");
assertThat(response.roles()).containsExactly("MERCHANT_OWNER");
assertThat(response.authorities()).containsExactly("merchant:order:list");
ArgumentCaptor<JwtEncoderParameters> jwt = ArgumentCaptor.forClass(JwtEncoderParameters.class); ArgumentCaptor<JwtEncoderParameters> jwt = ArgumentCaptor.forClass(JwtEncoderParameters.class);
verify(jwtEncoder).encode(jwt.capture()); verify(jwtEncoder).encode(jwt.capture());
@@ -125,6 +231,6 @@ class AuthServiceSmallJwtTest {
verify(tokenSessionStore).storeAccessOnly(any(), org.mockito.ArgumentMatchers.eq(43L), verify(tokenSessionStore).storeAccessOnly(any(), org.mockito.ArgumentMatchers.eq(43L),
org.mockito.ArgumentMatchers.eq(Set.of("MERCHANT_OWNER")), org.mockito.ArgumentMatchers.eq(Set.of("MERCHANT_OWNER")),
org.mockito.ArgumentMatchers.eq(Set.of("merchant:order:list")), org.mockito.ArgumentMatchers.eq(Set.of("merchant:order:list")),
org.mockito.ArgumentMatchers.eq(Duration.ofMinutes(15))); anyString(), org.mockito.ArgumentMatchers.eq(Duration.ofMinutes(15)));
} }
} }
@@ -23,6 +23,7 @@ import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import static org.mockito.Mockito.never;
@SpringJUnitConfig(MerchantRegistrationService.class) @SpringJUnitConfig(MerchantRegistrationService.class)
@TestPropertySource(properties = "baiyue.security.internal-service-token=internal-token") @TestPropertySource(properties = "baiyue.security.internal-service-token=internal-token")
@@ -35,7 +36,7 @@ class MerchantRegistrationServiceTest {
void registeredAuthAccountUsesTheOwnerIdentityReturnedByIamOnboarding() { void registeredAuthAccountUsesTheOwnerIdentityReturnedByIamOnboarding() {
when(encoder.encode("Password123")).thenReturn("encoded-password"); when(encoder.encode("Password123")).thenReturn("encoded-password");
when(onboarding.register(eq("internal-token"), any())).thenReturn(ApiResponse.success( when(onboarding.register(eq("internal-token"), any())).thenReturn(ApiResponse.success(
new IamOnboardingClient.OnboardingResponse(10001L, 30001L, "PROFILE_REQUIRED"))); new IamOnboardingClient.OnboardingResponse(10001L, 30001L, "PROFILE_REQUIRED", "PENDING")));
var result = service.register(new MerchantRegisterRequest( var result = service.register(new MerchantRegisterRequest(
"registered_owner", "Password123", "注册人", "测试企业", "张三", "registered_owner", "Password123", "注册人", "测试企业", "张三",
@@ -54,15 +55,61 @@ class MerchantRegistrationServiceTest {
assertEquals(30001L, storedAccount.getValue().getDefaultMembershipId()); assertEquals(30001L, storedAccount.getValue().getDefaultMembershipId());
assertEquals("MERCHANT", storedAccount.getValue().getDefaultAppCode()); assertEquals("MERCHANT", storedAccount.getValue().getDefaultAppCode());
assertEquals("ACTIVE", storedAccount.getValue().getAccountStatus()); assertEquals("ACTIVE", storedAccount.getValue().getAccountStatus());
assertEquals("13800138000", storedAccount.getValue().getPhone());
assertEquals("13800138000", onboardingRequest.getValue().administratorPhone());
assertEquals("encoded-password", storedAccount.getValue().getPasswordHash()); assertEquals("encoded-password", storedAccount.getValue().getPasswordHash());
assertEquals("LOGIN_AND_COMPLETE_QUALIFICATIONS", result.nextStep()); assertEquals("LOGIN_AND_COMPLETE_QUALIFICATIONS", result.nextStep());
assertEquals("PENDING", result.dimensionInitializationStatus());
}
@Test
void exactIncompleteRegistrationRetryReturnsTheExistingRegistration() {
AuthAccount account = new AuthAccount();
account.setId(9001L);
account.setUsername("registered_owner");
account.setPhone("13800138000");
account.setPasswordHash("encoded-password");
account.setAccountStatus("ACTIVE");
account.setDefaultAppCode("MERCHANT");
when(accounts.findByUsername("registered_owner")).thenReturn(account);
when(accounts.findByPhone("13800138000")).thenReturn(account);
when(encoder.matches("Password123", "encoded-password")).thenReturn(true);
when(onboarding.registration("internal-token", 9001L)).thenReturn(ApiResponse.success(
new IamOnboardingClient.OnboardingResponse(10001L, 30001L, "PROFILE_REQUIRED", "READY")));
var result = service.register(new MerchantRegisterRequest(
"registered_owner", "Password123", "注册人", "测试企业", null,
"13800138000", "owner@example.com", "CN", "深圳市宝安区"));
assertEquals(9001L, result.accountId());
assertEquals(10001L, result.tenantId());
assertEquals("READY", result.dimensionInitializationStatus());
verify(onboarding, never()).register(eq("internal-token"), any());
verify(accounts, never()).insert(any());
}
@Test
void phoneOwnedByAnotherUsernameDirectsTheUserBackToLogin() {
AuthAccount account = new AuthAccount();
account.setId(9001L);
account.setUsername("registered_owner");
account.setPhone("13800138000");
when(accounts.findByPhone("13800138000")).thenReturn(account);
var failure = assertThrows(com.baiyue.common.core.exception.BizException.class,
() -> service.register(new MerchantRegisterRequest(
"another_owner", "Password123", "注册人", "测试企业", null,
"13800138000", "owner@example.com", "CN", "深圳市宝安区")));
assertEquals("该手机号已注册,请直接登录后继续完善企业资质", failure.getMessage());
verify(onboarding, never()).register(eq("internal-token"), any());
} }
@Test @Test
void failedAuthInsertCompensatesTheIamRegistration() { void failedAuthInsertCompensatesTheIamRegistration() {
when(encoder.encode("Password123")).thenReturn("encoded-password"); when(encoder.encode("Password123")).thenReturn("encoded-password");
when(onboarding.register(eq("internal-token"), any())).thenReturn(ApiResponse.success( when(onboarding.register(eq("internal-token"), any())).thenReturn(ApiResponse.success(
new IamOnboardingClient.OnboardingResponse(10001L, 30001L, "PROFILE_REQUIRED"))); new IamOnboardingClient.OnboardingResponse(10001L, 30001L, "PROFILE_REQUIRED", "PENDING")));
when(onboarding.compensate(eq("internal-token"), any())).thenReturn(ApiResponse.success(null)); when(onboarding.compensate(eq("internal-token"), any())).thenReturn(ApiResponse.success(null));
doThrow(new IllegalStateException("auth database unavailable")).when(accounts).insert(any()); doThrow(new IllegalStateException("auth database unavailable")).when(accounts).insert(any());
@@ -82,7 +129,7 @@ class MerchantRegistrationServiceTest {
void rolledBackAuthTransactionCompensatesAfterTheLocalTransactionCompletes() { void rolledBackAuthTransactionCompensatesAfterTheLocalTransactionCompletes() {
when(encoder.encode("Password123")).thenReturn("encoded-password"); when(encoder.encode("Password123")).thenReturn("encoded-password");
when(onboarding.register(eq("internal-token"), any())).thenReturn(ApiResponse.success( when(onboarding.register(eq("internal-token"), any())).thenReturn(ApiResponse.success(
new IamOnboardingClient.OnboardingResponse(10001L, 30001L, "PROFILE_REQUIRED"))); new IamOnboardingClient.OnboardingResponse(10001L, 30001L, "PROFILE_REQUIRED", "PENDING")));
when(onboarding.compensate(eq("internal-token"), any())).thenReturn(ApiResponse.success(null)); when(onboarding.compensate(eq("internal-token"), any())).thenReturn(ApiResponse.success(null));
TransactionSynchronizationManager.initSynchronization(); TransactionSynchronizationManager.initSynchronization();
@@ -1,74 +0,0 @@
package com.baiyue.auth.service;
import com.baiyue.auth.client.AuthorizationLoader;
import com.baiyue.auth.client.AuthorizationSnapshot;
import com.baiyue.auth.config.OverseasJwtProperties;
import com.baiyue.auth.entity.AuthAccount;
import com.baiyue.auth.repository.AuthAccountRepository;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtEncoder;
import org.springframework.security.oauth2.jwt.JwtEncoderParameters;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import org.springframework.test.context.TestPropertySource;
import java.time.Duration;
import java.util.Set;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@SpringJUnitConfig(classes = {OverseasTokenService.class, OverseasTokenServiceTest.Config.class})
@TestPropertySource(properties = "baiyue.security.overseas-jwt.enabled=true")
class OverseasTokenServiceTest {
@MockBean private AuthAccountRepository accounts;
@MockBean private AuthorizationLoader authorizationLoader;
@MockBean(name = "overseasJwtEncoder") private JwtEncoder encoder;
@Resource private OverseasTokenService service;
@Configuration(proxyBeanMethods = false)
static class Config {
@Bean OverseasJwtProperties properties() {
return new OverseasJwtProperties(true, "auth-service", "lastleg-api", "{}", "current-kid",
Duration.ofMinutes(15));
}
}
@Test
void signsShortRs256TokenWithCrossRegionClaims() {
AuthAccount account = new AuthAccount();
account.setId(42L); account.setAccountStatus("ACTIVE"); account.setDefaultTenantId(100L);
account.setDefaultMembershipId(84L); account.setDefaultAppCode("MERCHANT");
AuthorizationSnapshot snapshot = new AuthorizationSnapshot(42L, 100L, 84L, "MERCHANT", "WEB",
Set.of("MERCHANT_ADMIN"), Set.of("merchant:order:list"), 7L);
Jwt encoded = org.mockito.Mockito.mock(Jwt.class);
when(accounts.findById(42L)).thenReturn(account);
when(authorizationLoader.load(account)).thenReturn(snapshot);
when(encoder.encode(any())).thenReturn(encoded);
when(encoded.getTokenValue()).thenReturn("overseas-token");
var response = service.exchange(42L);
ArgumentCaptor<JwtEncoderParameters> captor = ArgumentCaptor.forClass(JwtEncoderParameters.class);
verify(encoder).encode(captor.capture());
var parameters = captor.getValue();
assertThat(parameters.getJwsHeader().getAlgorithm()).isEqualTo(SignatureAlgorithm.RS256);
assertThat(parameters.getJwsHeader().getKeyId()).isEqualTo("current-kid");
assertThat(parameters.getClaims().getAudience()).containsExactly("lastleg-api");
assertThat(parameters.getClaims().getClaims())
.containsEntry("userId", 42L).containsEntry("tenantId", 100L)
.containsEntry("appCode", "MERCHANT").containsEntry("permissionVersion", 7L)
.containsEntry("roles", Set.of("MERCHANT_ADMIN"))
.containsEntry("authorities", Set.of("merchant:order:list"))
.doesNotContainKeys("user_id", "tenant_id", "app_code");
assertThat(response.expiresIn()).isEqualTo(900);
}
}
@@ -0,0 +1,281 @@
package com.baiyue.auth.service;
import com.baiyue.auth.client.AuthorizationLoader;
import com.baiyue.auth.client.AuthorizationSnapshot;
import com.baiyue.auth.config.AuthSecurityProperties;
import com.baiyue.auth.config.UnifiedJwtProperties;
import com.baiyue.auth.dto.AccountManagementDtos.ResetPasswordRequest;
import com.baiyue.auth.dto.LoginRequest;
import com.baiyue.auth.entity.AuthAccount;
import com.baiyue.auth.repository.AuthAccountRepository;
import com.baiyue.common.core.exception.BizException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.h2.jdbcx.JdbcDataSource;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.data.redis.core.SetOperations;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtEncoder;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.transaction.annotation.AnnotationTransactionAttributeSource;
import org.springframework.transaction.interceptor.TransactionInterceptor;
import org.springframework.transaction.support.TransactionTemplate;
import java.time.Duration;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicBoolean;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
/** Real MyBatis repository, JDBC transactions/row locks and production login/refresh/revocation services.
* Redis is an atomic in-memory transport double; this is not a Redis/MySQL environment test. */
class SessionStateConcurrencyTest {
JdbcTemplate authDb, iamDb;
DataSourceTransactionManager authTx, iamTx;
AuthService auth;
AccountManagementService accounts;
SessionStateService states;
TokenSessionStore sessions;
JwtEncoder encoder;
AtomicBoolean redisFailure;
final ConcurrentMap<String, String> values = new ConcurrentHashMap<>();
final ConcurrentMap<String, Set<String>> sets = new ConcurrentHashMap<>();
@BeforeEach void setup() throws Exception {
authDb = database(); iamDb = database();
authTx = new DataSourceTransactionManager(authDb.getDataSource());
iamTx = new DataSourceTransactionManager(iamDb.getDataSource());
authDb.execute("CREATE TABLE auth_account(id BIGINT PRIMARY KEY, username VARCHAR(50),phone VARCHAR(50),"
+ "password_hash VARCHAR(100),display_name VARCHAR(50),account_status VARCHAR(20),failed_login_count INT,"
+ "locked_until TIMESTAMP,token_version INT,password_change_required BOOLEAN,default_tenant_id BIGINT,"
+ "operator_tenant_id BIGINT,default_membership_id BIGINT,default_app_code VARCHAR(20),deleted INT,"
+ "last_login_at TIMESTAMP,created_at TIMESTAMP,updated_at TIMESTAMP)");
authDb.update("INSERT INTO auth_account(id,username,password_hash,account_status,token_version,default_tenant_id,"
+ "operator_tenant_id,default_membership_id,default_app_code,deleted) VALUES(1,'operator','old-password','ACTIVE',1,7,7,9,'MERCHANT',0)");
iamDb.execute("CREATE TABLE authorization_state(id BIGINT PRIMARY KEY, version BIGINT, tenant_version BIGINT, permission VARCHAR(50), warehouse BIGINT, active BOOLEAN)");
iamDb.update("INSERT INTO authorization_state VALUES (1,1,1,'write',10,true)");
var factory = new com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean();
factory.setDataSource(authDb.getDataSource());
var config = new com.baomidou.mybatisplus.core.MybatisConfiguration();
config.setMapUnderscoreToCamelCase(true);
config.addMapper(com.baiyue.auth.mapper.AuthAccountMapper.class);
factory.setConfiguration(config);
var mapper = new org.mybatis.spring.SqlSessionTemplate(factory.getObject())
.getMapper(com.baiyue.auth.mapper.AuthAccountMapper.class);
var repository = new AuthAccountRepository();
ReflectionTestUtils.setField(repository, "mapper", mapper);
AuthorizationLoader loader = mock(AuthorizationLoader.class);
when(loader.load(any())).thenAnswer(call -> {
var tx = new TransactionTemplate(iamTx);
tx.setIsolationLevel(org.springframework.transaction.TransactionDefinition.ISOLATION_REPEATABLE_READ);
return tx.execute(status -> iamDb.queryForObject("SELECT * FROM authorization_state WHERE id=1", (rs, row) -> {
if (!rs.getBoolean("active")) throw new IllegalArgumentException("IAM identity disabled");
return new AuthorizationSnapshot(1L, 7L, 9L, "MERCHANT", "MERCHANT", Set.of("OPERATOR"),
Set.of(rs.getString("permission")), rs.getLong("version"), rs.getLong("warehouse"), null,
rs.getLong("tenant_version"));
}));
});
sessions = new TokenSessionStore();
ReflectionTestUtils.setField(sessions, "objectMapper", new ObjectMapper());
ReflectionTestUtils.setField(sessions, "keyPrefix", "test");
ReflectionTestUtils.setField(sessions, "redisTemplate", redis());
PasswordEncoder passwords = mock(PasswordEncoder.class);
when(passwords.matches(anyString(), anyString())).thenAnswer(c -> c.getArgument(0).equals(c.getArgument(1)));
when(passwords.encode(anyString())).thenAnswer(c -> c.getArgument(0));
encoder = mock(JwtEncoder.class);
when(encoder.encode(any())).thenAnswer(c -> {
var claims = ((org.springframework.security.oauth2.jwt.JwtEncoderParameters)c.getArgument(0)).getClaims();
return Jwt.withTokenValue(claims.getId()).header("alg", "RS256").claims(m -> m.putAll(claims.getClaims())).build();
});
var service = new AuthService();
ReflectionTestUtils.setField(service, "accountRepository", repository);
ReflectionTestUtils.setField(service, "authorizationLoader", loader);
ReflectionTestUtils.setField(service, "tokenSessionStore", sessions);
ReflectionTestUtils.setField(service, "passwordEncoder", passwords);
ReflectionTestUtils.setField(service, "jwtEncoder", encoder);
ReflectionTestUtils.setField(service, "properties", new AuthSecurityProperties("issuer", "aud", Duration.ofMinutes(5), Duration.ofHours(1), 5, Duration.ofMinutes(1)));
ReflectionTestUtils.setField(service, "signingProperties", new UnifiedJwtProperties("unused", "kid"));
auth = transactional(service);
var management = new AccountManagementService();
ReflectionTestUtils.setField(management, "repository", repository);
ReflectionTestUtils.setField(management, "tokenSessionStore", sessions);
ReflectionTestUtils.setField(management, "passwordEncoder", passwords);
accounts = transactional(management);
var state = new SessionStateService();
ReflectionTestUtils.setField(state, "accounts", repository);
ReflectionTestUtils.setField(state, "authorizationLoader", loader);
ReflectionTestUtils.setField(state, "sessions", sessions);
states = transactional(state);
}
@Test void lateLoginAfterRevokeBeforeIamCommitCannotUseOldPermissionsOrRefresh() throws Exception {
var revoked = new CountDownLatch(1);
var snapshotLoaded = new CountDownLatch(1);
var committed = new CountDownLatch(1);
doAnswer(c -> {
snapshotLoaded.countDown(); await(committed);
var claims = ((org.springframework.security.oauth2.jwt.JwtEncoderParameters)c.getArgument(0)).getClaims();
return Jwt.withTokenValue(claims.getId()).header("alg", "RS256").claims(m -> m.putAll(claims.getClaims())).build();
}).when(encoder).encode(any());
ExecutorService pool = Executors.newFixedThreadPool(2);
try {
Future<?> change = pool.submit(() -> {
new TransactionTemplate(iamTx).executeWithoutResult(tx -> {
iamDb.update("UPDATE authorization_state SET version=version+1, permission='read' WHERE id=1");
accounts.revokeSessions(Set.of(1L));
revoked.countDown(); await(snapshotLoaded);
});
committed.countDown();
});
await(revoked);
var login = pool.submit(() -> auth.login(new LoginRequest("operator", "old-password")));
var stale = login.get(5, TimeUnit.SECONDS); change.get(5, TimeUnit.SECONDS);
// Access and state were written AFTER revoke, yet the first post-commit request is denied.
assertThat(values).containsKey("test:access:" + stale.accessToken());
assertThatThrownBy(() -> states.validate(stale.accessToken(), 1L)).isInstanceOf(BizException.class);
assertThatThrownBy(() -> auth.refresh(stale.refreshToken())).isInstanceOf(BizException.class);
var fresh = auth.login(new LoginRequest("operator", "old-password"));
assertThat(states.validate(fresh.accessToken(), 1L)).isTrue();
assertThat(fresh.authorities()).containsExactly("read");
assertThatThrownBy(() -> auth.refresh(stale.refreshToken())).isInstanceOf(BizException.class);
} finally { committed.countDown(); pool.shutdownNow(); }
}
@Test void passwordResetWaitsForConcurrentLoginThenInvalidatesAccessAndRefresh() throws Exception {
var loaded = new CountDownLatch(1); var release = new CountDownLatch(1);
doAnswer(c -> {
loaded.countDown(); await(release);
var claims = ((org.springframework.security.oauth2.jwt.JwtEncoderParameters)c.getArgument(0)).getClaims();
return Jwt.withTokenValue(claims.getId()).header("alg", "RS256").claims(m -> m.putAll(claims.getClaims())).build();
}).when(encoder).encode(any());
ExecutorService pool = Executors.newFixedThreadPool(2);
try {
var login = pool.submit(() -> auth.login(new LoginRequest("operator", "old-password")));
await(loaded);
var resetting = new CountDownLatch(1);
var reset = pool.submit(() -> { resetting.countDown(); accounts.resetPassword(1L, new ResetPasswordRequest("new-password", false), false); });
await(resetting);
assertThatThrownBy(() -> reset.get(100, TimeUnit.MILLISECONDS)).isInstanceOf(TimeoutException.class);
release.countDown();
var old = login.get(5, TimeUnit.SECONDS); reset.get(5, TimeUnit.SECONDS);
assertThat(authDb.queryForObject("SELECT token_version FROM auth_account WHERE id=1", Integer.class)).isEqualTo(2);
assertThatThrownBy(() -> states.validate(old.accessToken(), 1L)).isInstanceOf(BizException.class);
assertThatThrownBy(() -> auth.refresh(old.refreshToken())).isInstanceOf(BizException.class);
} finally { release.countDown(); pool.shutdownNow(); }
}
@Test void failedRevocationRollsBackAuthAndIamAndDoesNotReportSuccess() {
var old = auth.login(new LoginRequest("operator", "old-password"));
redisFailure.set(true);
assertThatThrownBy(() -> new TransactionTemplate(iamTx).executeWithoutResult(tx -> {
iamDb.update("UPDATE authorization_state SET version=version+1, active=false WHERE id=1");
accounts.revokeSessions(Set.of(1L));
})).isInstanceOf(BizException.class);
redisFailure.set(false);
assertThat(authDb.queryForObject("SELECT token_version FROM auth_account WHERE id=1", Integer.class)).isEqualTo(1);
assertThat(iamDb.queryForObject("SELECT version FROM authorization_state WHERE id=1", Long.class)).isEqualTo(1);
assertThat(states.validate(old.accessToken(), 1L)).isTrue();
}
@Test void committedIamVersionRejectsResidualSessionWhenNoRedisDeletionHappens() {
var old = auth.login(new LoginRequest("operator", "old-password"));
new TransactionTemplate(iamTx).executeWithoutResult(tx ->
iamDb.update("UPDATE authorization_state SET tenant_version=tenant_version+1 WHERE id=1"));
assertThatThrownBy(() -> states.validate(old.accessToken(), 1L)).isInstanceOf(BizException.class);
assertThatThrownBy(() -> auth.refresh(old.refreshToken())).isInstanceOf(BizException.class);
var current = auth.login(new LoginRequest("operator", "old-password"));
assertThat(states.validate(current.accessToken(), 1L)).isTrue();
iamDb.update("UPDATE authorization_state SET warehouse=20 WHERE id=1");
assertThatThrownBy(() -> states.validate(current.accessToken(), 1L)).isInstanceOf(BizException.class);
}
@Test void iamRollbackPreservesFingerprintAndOtherUserCannotUseSession() {
var old = auth.login(new LoginRequest("operator", "old-password"));
new TransactionTemplate(iamTx).executeWithoutResult(tx -> {
iamDb.update("UPDATE authorization_state SET version=version+1, permission='read' WHERE id=1");
tx.setRollbackOnly();
});
assertThat(states.validate(old.accessToken(), 1L)).isTrue();
assertThatThrownBy(() -> states.validate(old.accessToken(), 2L)).isInstanceOf(BizException.class);
var refreshed = auth.refresh(old.refreshToken());
assertThat(states.validate(refreshed.accessToken(), 1L)).isTrue();
}
@Test void lostRevokeAcknowledgementRollsBackIamButCannotRestoreOldAuthSession() {
var old = auth.login(new LoginRequest("operator", "old-password"));
assertThatThrownBy(() -> new TransactionTemplate(iamTx).executeWithoutResult(tx -> {
iamDb.update("UPDATE authorization_state SET version=version+1, permission='read' WHERE id=1");
accounts.revokeSessions(Set.of(1L));
throw new IllegalStateException("Auth response timed out after its commit");
})).isInstanceOf(IllegalStateException.class);
assertThat(iamDb.queryForObject("SELECT permission FROM authorization_state WHERE id=1", String.class)).isEqualTo("write");
assertThat(authDb.queryForObject("SELECT token_version FROM auth_account WHERE id=1", Integer.class)).isEqualTo(2);
assertThatThrownBy(() -> states.validate(old.accessToken(), 1L)).isInstanceOf(BizException.class);
var fresh = auth.login(new LoginRequest("operator", "old-password"));
assertThat(states.validate(fresh.accessToken(), 1L)).isTrue();
assertThat(fresh.authorities()).containsExactly("write");
}
@Test void disabledCustomerOrMemberAndUnavailableIamRejectAccessAndRefresh() {
var old = auth.login(new LoginRequest("operator", "old-password"));
// No Redis deletion: the authoritative identity gate alone closes the session.
iamDb.update("UPDATE authorization_state SET active=false, tenant_version=tenant_version+1 WHERE id=1");
assertThatThrownBy(() -> states.validate(old.accessToken(), 1L)).isInstanceOf(IllegalArgumentException.class);
assertThatThrownBy(() -> auth.refresh(old.refreshToken())).isInstanceOf(IllegalArgumentException.class);
iamDb.update("UPDATE authorization_state SET active=true WHERE id=1");
var fresh = auth.login(new LoginRequest("operator", "old-password"));
iamDb.execute("DROP TABLE authorization_state");
assertThatThrownBy(() -> states.validate(fresh.accessToken(), 1L)).isInstanceOf(org.springframework.dao.DataAccessException.class);
assertThatThrownBy(() -> auth.refresh(fresh.refreshToken())).isInstanceOf(org.springframework.dao.DataAccessException.class);
}
@SuppressWarnings("unchecked") private StringRedisTemplate redis() {
redisFailure = new AtomicBoolean();
var redis = mock(StringRedisTemplate.class);
ValueOperations<String,String> ops = mock(ValueOperations.class);
SetOperations<String,String> index = mock(SetOperations.class);
when(redis.opsForValue()).thenReturn(ops); when(redis.opsForSet()).thenReturn(index);
doAnswer(c -> { values.put(c.getArgument(0), c.getArgument(1)); return null; }).when(ops).set(anyString(), anyString(), any(Duration.class));
when(ops.get(anyString())).thenAnswer(c -> values.get(c.getArgument(0)));
when(ops.getAndDelete(anyString())).thenAnswer(c -> values.remove(c.getArgument(0)));
when(redis.hasKey(anyString())).thenAnswer(c -> values.containsKey(c.getArgument(0)));
when(redis.delete(anyString())).thenAnswer(c -> values.remove(c.getArgument(0)) != null);
when(redis.delete(anyCollection())).thenAnswer(c -> {
if (redisFailure.get()) throw new org.springframework.data.redis.RedisConnectionFailureException("offline");
long count = 0; for (Object key : (java.util.Collection<?>) c.getArgument(0)) if (values.remove(key) != null) count++;
return count;
});
when(index.members(anyString())).thenAnswer(c -> sets.getOrDefault(c.getArgument(0), Set.of()));
doAnswer(c -> {
Set<String> members = sets.computeIfAbsent(c.getArgument(0), key -> ConcurrentHashMap.newKeySet());
for (int i=1; i<c.getArguments().length; i++) members.add(c.getArgument(i));
return 1L;
}).when(index).add(anyString(), any(String[].class));
return redis;
}
static JdbcTemplate database() {
var ds = new JdbcDataSource(); ds.setURL("jdbc:h2:mem:" + UUID.randomUUID() + ";MODE=MySQL;DB_CLOSE_DELAY=-1;LOCK_TIMEOUT=3000");
return new JdbcTemplate(ds);
}
@SuppressWarnings("unchecked") <T> T transactional(T target) {
var factory = new ProxyFactory(target); factory.setProxyTargetClass(true);
factory.addAdvice(new TransactionInterceptor(authTx, new AnnotationTransactionAttributeSource()));
return (T) factory.getProxy();
}
static void await(CountDownLatch latch) {
try { if (!latch.await(5, TimeUnit.SECONDS)) throw new AssertionError("Barrier timed out"); }
catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new AssertionError(e); }
}
}
@@ -47,7 +47,7 @@ class TokenSessionStoreTest {
when(redis.opsForValue()).thenReturn(values); when(redis.opsForValue()).thenReturn(values);
store.store("jti-1", 42L, Set.of("ADMIN"), Set.of("order:read"), store.store("jti-1", 42L, Set.of("ADMIN"), Set.of("order:read"),
"raw-refresh", Duration.ofMinutes(30), Duration.ofDays(7)); "state", "raw-refresh", Duration.ofMinutes(30), Duration.ofDays(7));
verify(values).set(eq("baiyue:auth:access:jti-1"), verify(values).set(eq("baiyue:auth:access:jti-1"),
org.mockito.ArgumentMatchers.argThat(value -> value.contains("\"version\":1") org.mockito.ArgumentMatchers.argThat(value -> value.contains("\"version\":1")
@@ -64,7 +64,7 @@ class TokenSessionStoreTest {
ValueOperations<String, String> values = mock(ValueOperations.class); ValueOperations<String, String> values = mock(ValueOperations.class);
when(redis.opsForValue()).thenReturn(values); when(redis.opsForValue()).thenReturn(values);
String json = new ObjectMapper().writeValueAsString( String json = new ObjectMapper().writeValueAsString(
new TokenSessionStore.RefreshSession(42L, "jti-1")); new TokenSessionStore.RefreshSession(42L, "jti-1", "state"));
when(values.getAndDelete(anyString())).thenReturn(json); when(values.getAndDelete(anyString())).thenReturn(json);
TokenSessionStore.RefreshSession session = store.consume("raw-refresh"); TokenSessionStore.RefreshSession session = store.consume("raw-refresh");
+4
View File
@@ -1,5 +1,9 @@
# Common Audit 模块指引 # Common Audit 模块指引
## 数据库访问与中文注释约束
- 强制继承 `../AGENTS.md` 的“数据库访问与中文代码注释”全部规则;该节是分层、SQL 执行和方法/核心逻辑中文注释的唯一维护处,本模块不另设例外。
## 职责边界 ## 职责边界
- 负责业务审计事件采集、注解切面、敏感字段脱敏以及日志/HTTP 输出。 - 负责业务审计事件采集、注解切面、敏感字段脱敏以及日志/HTTP 输出。
@@ -7,14 +7,12 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.scheduling.annotation.EnableScheduling;
/** /**
* 审计 Outbox 的 RocketMQ 发布配置。 * 审计 Outbox 的 RocketMQ 发布配置。
* 该配置与基础审计配置隔离:未引入 RocketMQ 的服务仍可使用 HTTP 审计投递。 * 该配置与基础审计配置隔离:未引入 RocketMQ 的服务仍可使用 HTTP 审计投递。
*/ */
@AutoConfiguration(after = BaiyueAuditAutoConfiguration.class) @AutoConfiguration(after = BaiyueAuditAutoConfiguration.class)
@EnableScheduling
@ConditionalOnClass(name = "org.apache.rocketmq.spring.core.RocketMQTemplate") @ConditionalOnClass(name = "org.apache.rocketmq.spring.core.RocketMQTemplate")
@ConditionalOnBean(type = { @ConditionalOnBean(type = {
"com.baiyue.common.audit.AuditOutboxRepository", "com.baiyue.common.audit.AuditOutboxRepository",
@@ -48,7 +48,26 @@ class BusinessAuditAspectTest {
assertThat(event.operatorTenantId()).isEqualTo(10001L); assertThat(event.operatorTenantId()).isEqualTo(10001L);
} }
@Test
void failedOperationStillPublishesSanitizedBeforeSnapshot() throws Throwable {
AuditEvent event = invokeFailure(new AuthenticatedUser(7L, 0L, 0L, 1L,
"platform.admin", "PLATFORM", "WEB", Set.of()), 10001L);
assertThat(event.success()).isFalse();
assertThat(event.errorCode()).isEqualTo("IllegalStateException");
assertThat(event.beforeData()).isEqualTo("[10001]");
assertThat(event.afterData()).isNull();
}
private static AuditEvent invoke(AuthenticatedUser user, long requestedTenantId) throws Throwable { private static AuditEvent invoke(AuthenticatedUser user, long requestedTenantId) throws Throwable {
return invoke(user, requestedTenantId, null);
}
private static AuditEvent invokeFailure(AuthenticatedUser user, long requestedTenantId) throws Throwable {
return invoke(user, requestedTenantId, new IllegalStateException("validation failed"));
}
private static AuditEvent invoke(AuthenticatedUser user, long requestedTenantId, Throwable failure) throws Throwable {
AuditSink sink = mock(AuditSink.class); AuditSink sink = mock(AuditSink.class);
BusinessAuditAspect aspect = new BusinessAuditAspect(); BusinessAuditAspect aspect = new BusinessAuditAspect();
ReflectionTestUtils.setField(aspect, "sink", sink); ReflectionTestUtils.setField(aspect, "sink", sink);
@@ -68,9 +87,14 @@ class BusinessAuditAspectTest {
when(joinPoint.getSignature()).thenReturn(signature); when(joinPoint.getSignature()).thenReturn(signature);
when(joinPoint.getTarget()).thenReturn(target); when(joinPoint.getTarget()).thenReturn(target);
when(joinPoint.getArgs()).thenReturn(new Object[]{user, requestedTenantId}); when(joinPoint.getArgs()).thenReturn(new Object[]{user, requestedTenantId});
when(joinPoint.proceed()).thenReturn("ok"); if (failure == null) when(joinPoint.proceed()).thenReturn("ok");
else when(joinPoint.proceed()).thenThrow(failure);
aspect.around(joinPoint, method.getAnnotation(BusinessAudit.class)); try {
aspect.around(joinPoint, method.getAnnotation(BusinessAudit.class));
} catch (Throwable error) {
if (failure == null) throw error;
}
ArgumentCaptor<AuditEvent> event = ArgumentCaptor.forClass(AuditEvent.class); ArgumentCaptor<AuditEvent> event = ArgumentCaptor.forClass(AuditEvent.class);
verify(sink).publish(event.capture()); verify(sink).publish(event.capture());
@@ -21,4 +21,18 @@ class SensitiveDataSanitizerTest {
assertThat(parsed.path("_truncated").asBoolean()).isTrue(); assertThat(parsed.path("_truncated").asBoolean()).isTrue();
assertThat(parsed.path("_originalLength").asInt()).isGreaterThan(40); assertThat(parsed.path("_originalLength").asInt()).isGreaterThan(40);
} }
@Test
void redactsIdentityAndContactAliasesInsideNestedObjects() throws Exception {
SensitiveDataSanitizer sanitizer = new SensitiveDataSanitizer(mapper, 1024);
String sanitized = sanitizer.sanitize(Map.of(
"profile", Map.of("idCard", "4401...", "contactPhone", "13800138000"),
"contacts", java.util.List.of(Map.of("emailAddress", "user@example.com"))));
JsonNode parsed = mapper.readTree(sanitized);
assertThat(parsed.at("/profile/idCard").asText()).isEqualTo("******");
assertThat(parsed.at("/profile/contactPhone").asText()).isEqualTo("******");
assertThat(parsed.at("/contacts/0/emailAddress").asText()).isEqualTo("******");
}
} }
+8 -1
View File
@@ -1,5 +1,9 @@
# Common Core 模块指引 # Common Core 模块指引
## 数据库访问与中文注释约束
- 强制继承 `../AGENTS.md` 的“数据库访问与中文代码注释”全部规则;该节是分层、SQL 执行和方法/核心逻辑中文注释的唯一维护处,本模块不另设例外。
## 职责边界 ## 职责边界
- 提供不依赖具体业务域的返回模型、异常、基础实体和 MyBatis-Plus 通用配置。 - 提供不依赖具体业务域的返回模型、异常、基础实体和 MyBatis-Plus 通用配置。
@@ -14,11 +18,14 @@
| 业务异常 | `src/main/java/com/baiyue/common/core/exception/BizException.java` | | 业务异常 | `src/main/java/com/baiyue/common/core/exception/BizException.java` |
| 通用审计字段 | `src/main/java/com/baiyue/common/core/model/BaseEntity.java` | | 通用审计字段 | `src/main/java/com/baiyue/common/core/model/BaseEntity.java` |
| MyBatis-Plus 与审计人 | `src/main/java/com/baiyue/common/core/mybatis/` | | MyBatis-Plus 与审计人 | `src/main/java/com/baiyue/common/core/mybatis/` |
| 有限重试与退避 | `src/main/java/com/baiyue/common/core/retry/BoundedRetryPolicy.java`;供多个服务的 Outbox 复用,不拥有持久化或投递逻辑 |
| 人工重试事务与凭据 | `src/main/java/com/baiyue/common/core/outbox/OutboxOperations.java`、`EventOperationsTokenGuard.java`;三个生产服务显式选择本库表及事件白名单,锁定 DEAD 原记录,状态恢复与审计同事务;不访问其他服务数据库或直接投递事件 |
| 逐次投递审计事务 | `src/main/java/com/baiyue/common/core/outbox/EventDeliveryJournal.java`;领取与STARTED、持有者CAS完成与审计各自同库REQUIRES_NEW事务;HTTP在两个事务之间执行,迟到结果不得写入;JDBC为optional依赖,Print等无数据库服务不被引入JDBC |
| 响应契约测试 | `src/test/java/com/baiyue/common/core/ApiResponseTest.java` | | 响应契约测试 | `src/test/java/com/baiyue/common/core/ApiResponseTest.java` |
## 修改与验证 ## 修改与验证
- 公共响应、错误码或基础实体变化会影响所有服务;先检索消费者并保持序列化和数据库兼容。 - 公共响应、错误码或基础实体变化会影响所有服务;先检索并同步修改全部消费者、测试和契约。当前正式模型不保留旧序列化或数据库兼容分支;若已有不可变外部契约,先单独定义升级规则。
- `ApiResponse.successful()` 只认字符串 `"200"`。不得恢复旧成功码 `"0"`,也不得把 `code` 改成数字;前端和 Feign 调用方都按字符串严格判断。 - `ApiResponse.successful()` 只认字符串 `"200"`。不得恢复旧成功码 `"0"`,也不得把 `code` 改成数字;前端和 Feign 调用方都按字符串严格判断。
- `ResultCode.httpStatus` 保留错误分类元数据,但普通业务异常的实际 HTTP 状态由 `common-web/GlobalExceptionHandler` 统一收敛为 200;真实 HTTP 状态例外见 `../docs/统一响应约定改造方案-HTTP200.md`。 - `ResultCode.httpStatus` 保留错误分类元数据,但普通业务异常的实际 HTTP 状态由 `common-web/GlobalExceptionHandler` 统一收敛为 200;真实 HTTP 状态例外见 `../docs/统一响应约定改造方案-HTTP200.md`。
- 新能力只有被多个模块稳定复用时才放入本模块。 - 新能力只有被多个模块稳定复用时才放入本模块。
+3
View File
@@ -14,6 +14,7 @@
<name>baiyue-common-core</name> <name>baiyue-common-core</name>
<dependencies> <dependencies>
<dependency><groupId>com.fasterxml.jackson.core</groupId><artifactId>jackson-annotations</artifactId></dependency>
<dependency><groupId>org.springframework</groupId><artifactId>spring-web</artifactId></dependency> <dependency><groupId>org.springframework</groupId><artifactId>spring-web</artifactId></dependency>
<dependency> <dependency>
<groupId>com.baomidou</groupId> <groupId>com.baomidou</groupId>
@@ -24,5 +25,7 @@
<artifactId>spring-boot-starter-test</artifactId> <artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency><groupId>org.springframework</groupId><artifactId>spring-jdbc</artifactId><optional>true</optional></dependency>
<dependency><groupId>com.h2database</groupId><artifactId>h2</artifactId><scope>test</scope></dependency>
</dependencies> </dependencies>
</project> </project>
@@ -1,5 +1,7 @@
package com.baiyue.common.core.api; package com.baiyue.common.core.api;
import com.fasterxml.jackson.annotation.JsonInclude;
import java.time.Instant; import java.time.Instant;
/** /**
@@ -8,7 +10,7 @@ import java.time.Instant;
public record ApiResponse<T>( public record ApiResponse<T>(
String code, String code,
String message, String message,
T data, @JsonInclude(JsonInclude.Include.ALWAYS) T data,
String requestId, String requestId,
String traceId, String traceId,
Instant timestamp) { Instant timestamp) {
@@ -0,0 +1,56 @@
package com.baiyue.common.core.container;
import com.baiyue.common.core.api.ResultCode;
import com.baiyue.common.core.exception.BizException;
import java.math.BigDecimal;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
/** Platform-wide container types and practical loading limits. */
public final class ContainerSpecificationCatalog {
public static final String DEFAULT_CONTAINER_TYPE = "40HQ";
private static final List<Specification> ORDERED_VALUES = List.of(
new Specification("40GP", new BigDecimal("56"), new BigDecimal("58"), new BigDecimal("19500"),
"美线电商较少使用,通常优先选择40HQ"),
new Specification("40HQ", new BigDecimal("65"), new BigDecimal("68"), new BigDecimal("19500"),
"轻泡货首选,适合家居、玩具、日用品"),
new Specification("45HQ", new BigDecimal("82"), new BigDecimal("86"), new BigDecimal("19500"),
"抛货量大时考虑,底盘和海外仓预约资源较少")
);
private static final Map<String, Specification> VALUES = ORDERED_VALUES.stream()
.collect(Collectors.toUnmodifiableMap(Specification::containerType, Function.identity()));
private ContainerSpecificationCatalog() {}
public static List<Specification> values() {
return ORDERED_VALUES;
}
public static Specification require(String containerType) {
String normalized = containerType == null ? "" : containerType.trim().toUpperCase(Locale.ROOT);
Specification specification = VALUES.get(normalized);
if (specification == null) {
throw new BizException(ResultCode.VALIDATION_FAILED, "柜型仅支持40GP、40HQ、45HQ");
}
return specification;
}
public static Specification requireMatch(String containerType, BigDecimal capacityCbm, BigDecimal maxWeightKg) {
Specification specification = require(containerType);
if (capacityCbm == null || maxWeightKg == null
|| specification.capacityCbm().compareTo(capacityCbm) != 0
|| specification.maxWeightKg().compareTo(maxWeightKg) != 0) {
throw new BizException(ResultCode.VALIDATION_FAILED,
"柜型容量和货重上限必须使用平台统一定义");
}
return specification;
}
public record Specification(String containerType, BigDecimal recommendedMinCbm, BigDecimal capacityCbm,
BigDecimal maxWeightKg, String description) {}
}
@@ -0,0 +1,44 @@
package com.baiyue.common.core.number;
import com.baiyue.common.core.api.ResultCode;
import com.baiyue.common.core.exception.BizException;
import java.time.Clock;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.Locale;
import java.util.function.ToLongFunction;
/** 统一业务单号格式;序列仍由各服务自己的 Repository/Mapper 分配。 */
public final class DailyDocumentNumber {
public static final ZoneId BUSINESS_ZONE = ZoneId.of("Asia/Shanghai");
/** 工具类不允许实例化。 */
private DailyDocumentNumber() {}
/** 先固定北京时间业务日,再使用前缀和日期分配跨客户、跨仓库日流水。 */
public static String next(String prefix, Clock clock, ToLongFunction<String> sequences) {
requirePrefix(prefix);
LocalDate date = LocalDate.now(clock.withZone(BUSINESS_ZONE));
String dateText = date.format(DateTimeFormatter.BASIC_ISO_DATE);
long value = sequences.applyAsLong(prefix + ":" + dateText);
return format(prefix, date, value);
}
/** 严格四位流水,不允许溢出后产生五位号码或退回随机编号。 */
public static String format(String prefix, LocalDate date, long value) {
requirePrefix(prefix);
if (value < 1 || value > 9999) {
throw new BizException(ResultCode.CONFLICT, "当日" + prefix + "单据四位流水号已用尽或分配失败");
}
return prefix + date.format(DateTimeFormatter.BASIC_ISO_DATE)
+ String.format(Locale.ROOT, "%04d", value);
}
/** 业务类型只能使用两至四位大写字母。 */
private static void requirePrefix(String prefix) {
if (prefix == null || !prefix.matches("[A-Z]{2,4}")) {
throw new IllegalArgumentException("业务单据前缀必须为2至4位大写字母");
}
}
}
@@ -0,0 +1,59 @@
package com.baiyue.common.core.outbox;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.support.TransactionTemplate;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.Objects;
import java.util.Set;
import java.util.function.BooleanSupplier;
import java.util.function.IntSupplier;
/** Same-database audit. HTTP must only run after begin returns; never hold a DB transaction over HTTP. */
public final class EventDeliveryJournal {
public enum Outcome { APPLIED, DUPLICATE, STALE, RETRY, DEAD, LEASE_EXPIRED }
private final JdbcTemplate jdbc;
private final TransactionTemplate transaction;
public EventDeliveryJournal(JdbcTemplate jdbc, PlatformTransactionManager manager) {
this.jdbc = Objects.requireNonNull(jdbc);
if (!(manager instanceof DataSourceTransactionManager local)
|| local.getDataSource() != jdbc.getDataSource()) {
throw new IllegalArgumentException("Delivery audit must share the local Outbox transaction manager");
}
transaction = new TransactionTemplate(manager);
transaction.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
}
public boolean begin(String attempt, String service, String eventType, String entryId,
String eventId, String target, LocalDateTime now, BooleanSupplier claim) {
return Boolean.TRUE.equals(transaction.execute(status -> {
if (!claim.getAsBoolean()) return false;
jdbc.update("INSERT INTO event_delivery_attempt(attempt_id,service_name,event_type,entry_id,event_id,target_name,started_at,outcome) VALUES(?,?,?,?,?,?,?,'STARTED')",
attempt, service, eventType, entryId, eventId, target, now);
return true;
}));
}
public boolean complete(String attempt, Outcome outcome, LocalDateTime now,
String errorCode, LocalDateTime nextRetry, IntSupplier updateOwnedOutbox) {
if (errorCode != null && !Set.of("RETRY_EXHAUSTED", "LEASE_EXPIRED", "INVALID_EVENT", "INVALID_RESPONSE", "PERMANENT_FAILURE", "DELIVERY_FAILURE").contains(errorCode)
&& !errorCode.matches("HTTP_[1-5][0-9]{2}")) throw new IllegalArgumentException("Unbounded audit error code");
return Boolean.TRUE.equals(transaction.execute(status -> {
int changed = updateOwnedOutbox.getAsInt();
if (changed == 0) return false; // A stale worker has no right to change either record.
if (changed != 1) throw new IllegalStateException("Unexpected Outbox update count");
LocalDateTime started = jdbc.queryForObject(
"SELECT started_at FROM event_delivery_attempt WHERE attempt_id=? AND outcome='STARTED' FOR UPDATE",
LocalDateTime.class, attempt);
long duration = Math.max(0, Duration.between(Objects.requireNonNull(started), now).toMillis());
int audited = jdbc.update("UPDATE event_delivery_attempt SET finished_at=?,duration_ms=?,outcome=?,error_code=?,next_retry_at=? WHERE attempt_id=? AND outcome='STARTED'",
now, duration, outcome.name(), errorCode, nextRetry, attempt);
if (audited != 1) throw new IllegalStateException("Delivery completion audit is missing");
return true;
}));
}
}
@@ -0,0 +1,4 @@
package com.baiyue.common.core.outbox;
/** Marks services that require durable attempt audit and cached delivery metrics. */
public interface EventDeliveryProducer { }
@@ -0,0 +1,20 @@
package com.baiyue.common.core.outbox;
import com.baiyue.common.core.api.ResultCode;
import com.baiyue.common.core.exception.BizException;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
/** Dedicated operations credential; a missing or weak configuration disables this capability. */
public final class EventOperationsTokenGuard {
private final byte[] expected;
public EventOperationsTokenGuard(String token) {
expected = token == null ? new byte[0] : token.getBytes(StandardCharsets.UTF_8);
}
public void require(String supplied) {
if (expected.length < 32 || supplied == null || supplied.length() > 4096
|| !MessageDigest.isEqual(expected, supplied.getBytes(StandardCharsets.UTF_8))) {
throw new BizException(ResultCode.FORBIDDEN, "事件运维凭据无效或入口未启用");
}
}
}
@@ -0,0 +1,131 @@
package com.baiyue.common.core.outbox;
import com.baiyue.common.core.api.ResultCode;
import com.baiyue.common.core.exception.BizException;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.support.TransactionTemplate;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.function.Function;
/** Transactional operator retry of an immutable, service-owned Outbox entry. No cross-schema access. */
public final class OutboxOperations {
public record Layout(String table, String idColumn, String statusColumn, Set<String> eventTypes) {
public Layout {
for (String name : List.of(table, idColumn, statusColumn)) {
if (!name.matches("[a-z][a-z0-9_]*")) throw new IllegalArgumentException("Invalid Outbox identifier");
}
eventTypes = Set.copyOf(eventTypes);
if (eventTypes.isEmpty()) throw new IllegalArgumentException("Event allowlist is required");
}
}
public record Identity(String eventId, String eventType, String payloadHash) { }
public record RetryRequest(String requestId, String operator, String reason, String expectedPayloadHash) { }
public record RetryResult(String entryId, String eventId, String payloadHash, String requestId, boolean duplicate) { }
public record Entry(String entryId, String eventId, String eventType, String payloadHash,
String status, int failures, String lastError, Object nextRetryAt) { }
private final JdbcTemplate jdbc;
private final Layout layout;
private final Function<String, Identity> identityReader;
private final TransactionTemplate transaction;
public OutboxOperations(JdbcTemplate jdbc, PlatformTransactionManager transactions, Layout layout,
Function<String, Identity> identityReader) {
this.jdbc = jdbc;
this.layout = layout;
this.identityReader = identityReader;
this.transaction = new TransactionTemplate(transactions);
}
public Entry inspect(String entryId) {
validateId(entryId);
Map<String, Object> row = find(entryId, false);
Identity identity = identity(row);
return new Entry(String.valueOf(row.get("entry_id")), identity.eventId(), identity.eventType(), identity.payloadHash(),
String.valueOf(row.get("entry_status")), ((Number) row.get("retry_count")).intValue(),
(String) row.get("last_error"), row.get("next_retry_at"));
}
public RetryResult retry(String entryId, RetryRequest request) {
validateId(entryId);
validateRequest(request);
try {
return transaction.execute(status -> {
Map<String, Object> row = find(entryId, true);
String canonicalId = String.valueOf(row.get("entry_id"));
Identity identity = identity(row);
if (!identity.payloadHash().equalsIgnoreCase(request.expectedPayloadHash())) {
throw conflict("事件摘要已变化,拒绝重试");
}
List<Map<String, Object>> prior = jdbc.queryForList(
"SELECT producer_table,entry_id,event_id,payload_hash,operator_name,reason FROM event_delivery_retry_audit WHERE request_id=?",
request.requestId());
if (!prior.isEmpty()) {
Map<String, Object> audit = prior.get(0);
if (!layout.table().equals(audit.get("producer_table")) || !canonicalId.equals(audit.get("entry_id"))
|| !identity.eventId().equals(audit.get("event_id"))
|| !identity.payloadHash().equals(audit.get("payload_hash"))
|| !request.operator().equals(audit.get("operator_name")) || !request.reason().equals(audit.get("reason"))) {
throw conflict("操作请求编号已用于不同重试参数");
}
return result(canonicalId, identity, request, true);
}
if (!"DEAD".equals(row.get("entry_status"))) throw conflict("仅允许重试 DEAD 事件");
// Relay due-time comparisons use UTC, independently of the JVM default zone.
LocalDateTime now = LocalDateTime.now(ZoneOffset.UTC);
int updated = jdbc.update("UPDATE " + layout.table() + " SET " + layout.statusColumn()
+ "='PENDING',retry_count=0,next_retry_at=?,locked_by=NULL,locked_at=NULL,last_error=NULL WHERE "
+ layout.idColumn() + "=? AND " + layout.statusColumn() + "='DEAD'", now, canonicalId);
if (updated != 1) throw conflict("事件状态已变化");
jdbc.update("INSERT INTO event_delivery_retry_audit(request_id,producer_table,entry_id,event_id,event_type,payload_hash,operator_name,reason,previous_failures,previous_error,requested_at) VALUES(?,?,?,?,?,?,?,?,?,?,?)",
request.requestId(), layout.table(), canonicalId, identity.eventId(), identity.eventType(), identity.payloadHash(),
request.operator(), request.reason(), row.get("retry_count"), row.get("last_error"), now);
return result(canonicalId, identity, request, false);
});
} catch (DuplicateKeyException duplicate) {
throw conflict("操作请求编号冲突,原事务已回滚");
}
}
private Map<String, Object> find(String id, boolean lock) {
List<Map<String, Object>> rows = jdbc.queryForList("SELECT " + layout.idColumn() + " AS entry_id,payload_json," + layout.statusColumn()
+ " AS entry_status,retry_count,last_error,next_retry_at FROM " + layout.table() + " WHERE "
+ layout.idColumn() + "=?" + (lock ? " FOR UPDATE" : ""), id);
if (rows.isEmpty()) throw new BizException(ResultCode.NOT_FOUND, "事件不存在");
return rows.get(0);
}
private Identity identity(Map<String, Object> row) {
Identity identity = identityReader.apply(String.valueOf(row.get("payload_json")));
if (!layout.eventTypes().contains(identity.eventType())) throw new BizException(ResultCode.FORBIDDEN, "事件不属于本运维入口");
return identity;
}
private static RetryResult result(String entryId, Identity identity, RetryRequest request, boolean duplicate) {
return new RetryResult(entryId, identity.eventId(), identity.payloadHash(), request.requestId(), duplicate);
}
private static void validateId(String entryId) {
if (entryId == null || !entryId.matches("[A-Za-z0-9-]{1,64}")) throw new BizException(ResultCode.BAD_REQUEST, "事件记录标识无效");
}
private static void validateRequest(RetryRequest request) {
if (request == null) throw new BizException(ResultCode.BAD_REQUEST, "重试参数不能为空");
try {
if (!UUID.fromString(request.requestId()).toString().equalsIgnoreCase(request.requestId())) throw new IllegalArgumentException();
} catch (RuntimeException failure) { throw new BizException(ResultCode.BAD_REQUEST, "requestId 必须为标准 UUID"); }
requireText(request.operator(), 128, "操作人");
requireText(request.reason(), 500, "原因");
if (request.expectedPayloadHash() == null || !request.expectedPayloadHash().matches("sha256:[0-9a-fA-F]{64}")) {
throw new BizException(ResultCode.BAD_REQUEST, "必须提供预期事件摘要");
}
}
private static void requireText(String value, int length, String label) {
if (value == null || value.isBlank() || value.length() > length || value.chars().anyMatch(Character::isISOControl)) {
throw new BizException(ResultCode.BAD_REQUEST, label + "无效");
}
}
private static BizException conflict(String message) { return new BizException(ResultCode.CONFLICT, message); }
}
@@ -0,0 +1,31 @@
package com.baiyue.common.core.retry;
import java.time.Duration;
import java.util.concurrent.ThreadLocalRandom;
/** Bounded exponential retry schedule; callers persist failed or expired attempts. */
public record BoundedRetryPolicy(int maxAttempts, long baseDelaySeconds, long maxDelaySeconds, double jitter) {
public BoundedRetryPolicy {
if (maxAttempts < 1 || baseDelaySeconds < 1 || maxDelaySeconds < baseDelaySeconds
|| maxDelaySeconds > 86400 || !Double.isFinite(jitter) || jitter < 0 || jitter > 0.5) {
throw new IllegalArgumentException("Invalid bounded retry policy");
}
}
public boolean exhausted(int failedAttempts) {
return failedAttempts >= maxAttempts;
}
public Duration delayAfterFailure(int failedAttempts) {
return delayAfterFailure(failedAttempts, ThreadLocalRandom.current().nextDouble());
}
public Duration delayAfterFailure(int failedAttempts, double random) {
if (failedAttempts < 1 || !Double.isFinite(random) || random < 0 || random > 1) {
throw new IllegalArgumentException("Invalid retry attempt or jitter sample");
}
long seconds = baseDelaySeconds;
for (int i = 1; i < failedAttempts && seconds < maxDelaySeconds; i++) {
seconds = Math.min(maxDelaySeconds, seconds * 2);
}
long millis = Math.round(seconds * 1000 * (1 - jitter + 2 * jitter * random));
return Duration.ofMillis(Math.min(maxDelaySeconds * 1000, Math.max(1, millis)));
}
}
@@ -0,0 +1,31 @@
package com.baiyue.common.core;
import com.baiyue.common.core.retry.BoundedRetryPolicy;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.*;
class BoundedRetryPolicyTest {
@Test void startsAtFiveSecondsDoublesAndCapsAtThirtyMinutes() {
var policy = new BoundedRetryPolicy(12, 5, 1800, 0);
assertThat(policy.delayAfterFailure(1).toSeconds()).isEqualTo(5);
assertThat(policy.delayAfterFailure(2).toSeconds()).isEqualTo(10);
assertThat(policy.delayAfterFailure(9).toSeconds()).isEqualTo(1280);
assertThat(policy.delayAfterFailure(10).toSeconds()).isEqualTo(1800);
assertThat(policy.delayAfterFailure(Integer.MAX_VALUE).toSeconds()).isEqualTo(1800);
assertThat(policy.exhausted(11)).isFalse();
assertThat(policy.exhausted(12)).isTrue();
}
@Test void jitterStaysBoundedEvenAtMaximumDelay() {
var policy = new BoundedRetryPolicy(12, 5, 1800, 0.1);
assertThat(policy.delayAfterFailure(1, 0).toMillis()).isEqualTo(4500);
assertThat(policy.delayAfterFailure(1, 1).toMillis()).isEqualTo(5500);
assertThat(policy.delayAfterFailure(20, 1).toSeconds()).isEqualTo(1800);
assertThat(policy.delayAfterFailure(20, 0).toSeconds()).isEqualTo(1620);
}
@Test void invalidConfigurationFailsClosed() {
assertThatThrownBy(() -> new BoundedRetryPolicy(0, 5, 1800, 0.1)).isInstanceOf(IllegalArgumentException.class);
assertThatThrownBy(() -> new BoundedRetryPolicy(12, 0, 1800, 0.1)).isInstanceOf(IllegalArgumentException.class);
assertThatThrownBy(() -> new BoundedRetryPolicy(12, 5, 3, 0.1)).isInstanceOf(IllegalArgumentException.class);
assertThatThrownBy(() -> new BoundedRetryPolicy(12, 5, 1800, Double.NaN)).isInstanceOf(IllegalArgumentException.class);
}
}
@@ -0,0 +1,83 @@
package com.baiyue.common.core;
import com.baiyue.common.core.outbox.EventDeliveryJournal;
import com.baiyue.common.core.outbox.EventDeliveryJournal.Outcome;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.support.TransactionTemplate;
import java.time.LocalDateTime;
import java.util.UUID;
import static org.assertj.core.api.Assertions.*;
class EventDeliveryJournalTest {
JdbcTemplate jdbc;
EventDeliveryJournal journal;
DataSourceTransactionManager transactions;
@BeforeEach void setup() {
var source = new DriverManagerDataSource("jdbc:h2:mem:" + UUID.randomUUID() + ";MODE=MySQL;DB_CLOSE_DELAY=-1", "sa", "");
jdbc = new JdbcTemplate(source);
transactions = new DataSourceTransactionManager(source);
journal = new EventDeliveryJournal(jdbc, transactions);
jdbc.execute("CREATE TABLE event_delivery_attempt(attempt_id VARCHAR(36) PRIMARY KEY,service_name VARCHAR(32) NOT NULL,event_type VARCHAR(96) NOT NULL,entry_id VARCHAR(128) NOT NULL,event_id VARCHAR(128),target_name VARCHAR(64) NOT NULL,started_at TIMESTAMP(3) NOT NULL,finished_at TIMESTAMP(3),duration_ms BIGINT,outcome VARCHAR(24) NOT NULL,error_code VARCHAR(64),next_retry_at TIMESTAMP(3))");
jdbc.execute("CREATE TABLE work(id INT PRIMARY KEY,state VARCHAR(20),owner VARCHAR(36))");
jdbc.update("INSERT INTO work VALUES(1,'PENDING',NULL)");
}
boolean begin(String owner) {
return journal.begin(owner, "firstleg", "FirstLegLoadingPlanCompleted", "1", UUID.randomUUID().toString(), "lastleg-events", LocalDateTime.now(),
() -> jdbc.update("UPDATE work SET state='PROCESSING',owner=? WHERE id=1 AND state='PENDING'", owner) == 1);
}
int finish(String owner) { return jdbc.update("UPDATE work SET state='PUBLISHED',owner=NULL WHERE id=1 AND owner=?", owner); }
String state() { return jdbc.queryForObject("SELECT state FROM work WHERE id=1", String.class); }
String outcome(String attempt) { return jdbc.queryForObject("SELECT outcome FROM event_delivery_attempt WHERE attempt_id=?", String.class, attempt); }
@Test void startedIsCommittedBeforeCallerContinuesEvenWithinAnOuterTransaction() {
new TransactionTemplate(transactions).executeWithoutResult(status -> {
assertThat(begin("a")).isTrue();
// Another connection sees STARTED before any HTTP would be invoked.
var other = new JdbcTemplate(jdbc.getDataSource());
assertThat(other.queryForObject("SELECT outcome FROM event_delivery_attempt", String.class)).isEqualTo("STARTED");
status.setRollbackOnly();
});
assertThat(state()).isEqualTo("PROCESSING");
assertThat(outcome("a")).isEqualTo("STARTED"); // process interruption leaves diagnostic evidence
assertThat(begin("b")).isFalse();
assertThat(jdbc.queryForObject("SELECT COUNT(*) FROM event_delivery_attempt", Integer.class)).isEqualTo(1);
}
@Test void startAuditFailureRollsBackTheClaim() {
assertThat(begin("a")).isTrue();
jdbc.update("UPDATE work SET state='PENDING',owner=NULL");
assertThatThrownBy(() -> begin("a")).isInstanceOf(RuntimeException.class);
assertThat(state()).isEqualTo("PENDING");
}
@Test void completionAuditFailureRollsBackOutboxAndPreservesStartedRecord() {
begin("a");
jdbc.execute("ALTER TABLE event_delivery_attempt ADD CONSTRAINT reject_applied CHECK(outcome <> 'APPLIED')");
assertThatThrownBy(() -> journal.complete("a", Outcome.APPLIED, LocalDateTime.now(), null, null, () -> finish("a"))).isInstanceOf(RuntimeException.class);
assertThat(state()).isEqualTo("PROCESSING");
assertThat(outcome("a")).isEqualTo("STARTED");
jdbc.execute("ALTER TABLE event_delivery_attempt DROP CONSTRAINT reject_applied");
assertThat(journal.complete("a", Outcome.APPLIED, LocalDateTime.now(), null, null, () -> finish("a"))).isTrue();
assertThat(state()).isEqualTo("PUBLISHED");
assertThat(outcome("a")).isEqualTo("APPLIED");
assertThat(jdbc.queryForObject("SELECT duration_ms FROM event_delivery_attempt", Long.class)).isNotNegative();
}
@Test void recoveryAndLateResultCannotOverwriteTheNewOwnerOrItsAudit() {
begin("a");
var retryAt = LocalDateTime.now().plusSeconds(5);
assertThat(journal.complete("a", Outcome.LEASE_EXPIRED, LocalDateTime.now(), "LEASE_EXPIRED", retryAt,
() -> jdbc.update("UPDATE work SET state='PENDING',owner=NULL WHERE owner='a'"))).isTrue();
begin("b");
assertThat(journal.complete("a", Outcome.APPLIED, LocalDateTime.now(), null, null, () -> finish("a"))).isFalse();
assertThat(outcome("a")).isEqualTo("LEASE_EXPIRED");
assertThat(outcome("b")).isEqualTo("STARTED");
assertThat(state()).isEqualTo("PROCESSING");
}
@Test void unexpectedCompletionWithoutStartedRecordRollsBackInsteadOfUsingAFallback() {
jdbc.update("UPDATE work SET state='PROCESSING',owner='missing'");
assertThatThrownBy(() -> journal.complete("missing", Outcome.APPLIED, LocalDateTime.now(), null, null, () -> finish("missing"))).isInstanceOf(RuntimeException.class);
assertThat(state()).isEqualTo("PROCESSING");
}
}
@@ -0,0 +1,23 @@
package com.baiyue.common.core;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.common.core.outbox.EventOperationsTokenGuard;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.*;
class EventOperationsTokenGuardTest {
@Test void missingOrShortConfigurationDisablesOperations() {
for (String configured : new String[] {null, "", "short"}) {
var guard = new EventOperationsTokenGuard(configured);
assertThatThrownBy(() -> guard.require(configured)).isInstanceOf(BizException.class);
}
}
@Test void onlyExactCredentialIsAccepted() {
String token = "operation-secret-".repeat(3);
var guard = new EventOperationsTokenGuard(token);
assertThatCode(() -> guard.require(token)).doesNotThrowAnyException();
for (String supplied : new String[] {null, "", token + " ", token.toUpperCase(), "a".repeat(4097)}) {
assertThatThrownBy(() -> guard.require(supplied)).isInstanceOf(BizException.class);
}
}
}

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