This commit is contained in:
2026-08-21 17:19:49 +08:00
parent 8c280e19c4
commit af97217184
10 changed files with 104 additions and 20 deletions
+2 -1
View File
@@ -37,6 +37,7 @@
| 海外 API 路由、海外 Token、Audience/JWKS | `gateway-overseas/` | `gateway-overseas/AGENTS.md` |
| 尾程订单、海外入仓计划、换标、库存、采购、本土卡车 | `lastleg-service/` | `lastleg-service/AGENTS.md`,再按 `inboundplan/`、`relabel/`、`inventory/`、`procurement/`、`trucking/` 定位 |
| 海外入仓分页、FBA 预约、PDA 卸柜照片 | `lastleg-service/` | `inboundplan/controller/InboundPlanController.java`、`InboundPlanQueryMapper.java`、`InboundFbaAppointmentService.java`、`inboundplan/pda/` |
| 海外仓排车查询、候选与智能预排 | `lastleg-service/` | `dispatch/controller/DispatchQueryController.java`、`SmartDispatchController.java`;只读排车和预排创建独立开关,不自动开放编辑、取消、发车、PDA 出库或派送 |
| 海外仓 PDA 到仓、卸柜、实收、上架、合托 | `lastleg-service/`、`warehouse-overseas-service/` | 业务流程 `lastleg-service/inboundplan/pda/`;库位、托盘和资源锁归 `warehouse-overseas-service/`;设备端 `../apps/pda-overseas-uniapp/`,公网入口 `gateway-overseas/` |
| 海外仓库、海外库位、海外托盘、资源锁 | `warehouse-overseas-service/` | `warehouse-overseas-service/AGENTS.md` |
| PDF 标签、审计、跨区双向事件 | `print/`、`audit-service/`、`event-bridge-service/` | 事件模型先读 `common-event-contract/AGENTS.md`;可靠投递与 CN/OVERSEAS 目标路由读 `event-bridge-service/AGENTS.md` |
@@ -75,7 +76,7 @@
- 数据库只访问所属服务库。模块表结构位于 `<module>/src/main/resources/db/`;当前开发阶段直接更新最新模块基线和 `deploy/sql/头程数据库统一增量-MySQL8.sql`,不为历史开发数据新增兼容迁移。已有名称含 `migration` 的脚本仅作现有结构来源和开发追溯,部署说明见 `deploy/sql/README.md`。
- 通用配置在 `config/nacos/{local,cn}/`;服务日志、上传文件、`target/`、`reports/` 不属于源码。
- Java 签名变更的最低编译门禁是 `mvn -pl <module> -am test-compile`,它必须在部署前完成并覆盖测试源码;有行为变化时继续运行 `mvn -pl <module> -am test`。`-DskipTests` 可跳过测试执行但仍须保留测试编译,`-Dmaven.test.skip=true` 会跳过测试编译,不得作为签名变更的交付验证。全量验证仍使用 `mvn -DskipTests validate`、`mvn test`;不要在本地启动 Java/Nacos/数据库,除非用户明确要求。
- `AGENTS.md` 索引维护完成后运行 `./scripts/agents-index-contract.ps1`;脚本只读源码目录和指定指引,验证部署单元、模块覆盖、关键入口与排除目录,不访问 HTTP、数据库或部署环境。
- `AGENTS.md` 索引维护完成后运行 `./scripts/agents-index-contract.ps1`;脚本只读源码目录和指定指引,验证根快速表唯一性、部署单元、模块覆盖、关键入口与排除目录,不访问 HTTP、数据库或部署环境。
## 全量 HTTP 验收脚本路由
+18 -13
View File
@@ -214,25 +214,28 @@ run_scp() {
run_ssh install -d -m 750 "$REMOTE_DIR"
SUFFIX="${IMAGE_TAG}.$$.${SERVICE}"
REMOTE_ENV_TMP="$REMOTE_DIR/.env.$SUFFIX.tmp"
REMOTE_COMPOSE_TMP="$REMOTE_DIR/docker-compose.yml.$SUFFIX.tmp"
REMOTE_ENV_NAME=".env.$PROJECT_NAME"
REMOTE_COMPOSE_NAME="docker-compose.$PROJECT_NAME.yml"
REMOTE_ENV_TMP="$REMOTE_DIR/$REMOTE_ENV_NAME.$SUFFIX.tmp"
REMOTE_COMPOSE_TMP="$REMOTE_DIR/$REMOTE_COMPOSE_NAME.$SUFFIX.tmp"
run_scp "$ENV_FILE" "$SSH_TARGET:$REMOTE_ENV_TMP"
run_scp "$COMPOSE_FILE" "$SSH_TARGET:$REMOTE_COMPOSE_TMP"
run_ssh sh -s -- "$REMOTE_DIR" "$REMOTE_ENV_TMP" "$REMOTE_COMPOSE_TMP" <<'REMOTE_PREPARE'
run_ssh sh -s -- "$REMOTE_ENV_TMP" "$REMOTE_COMPOSE_TMP" "$REMOTE_DIR/$REMOTE_ENV_NAME" "$REMOTE_DIR/$REMOTE_COMPOSE_NAME" <<'REMOTE_PREPARE'
set -eu
remote_dir="$1"
env_tmp="$2"
compose_tmp="$3"
env_tmp="$1"
compose_tmp="$2"
env_target="$3"
compose_target="$4"
chmod 600 "$env_tmp"
chmod 644 "$compose_tmp"
mv -f "$env_tmp" "$remote_dir/.env"
mv -f "$compose_tmp" "$remote_dir/docker-compose.yml"
mv -f "$env_tmp" "$env_target"
mv -f "$compose_tmp" "$compose_target"
REMOTE_PREPARE
echo "传输镜像到 $SSH_TARGET: $IMAGE"
docker save "$IMAGE" | gzip -1 | run_ssh 'gzip -dc | docker load'
run_ssh sh -s -- "$REMOTE_DIR" "$DEPLOY_ENV" "$IMAGE_REPOSITORY" "$IMAGE_TAG" "$SERVICE" "$PROJECT_NAME" <<'REMOTE_DEPLOY'
run_ssh sh -s -- "$REMOTE_DIR" "$DEPLOY_ENV" "$IMAGE_REPOSITORY" "$IMAGE_TAG" "$SERVICE" "$PROJECT_NAME" "$REMOTE_ENV_NAME" "$REMOTE_COMPOSE_NAME" <<'REMOTE_DEPLOY'
set -eu
remote_dir="$1"
deploy_env="$2"
@@ -240,18 +243,20 @@ image_repository="$3"
image_tag="$4"
service="$5"
project_name="$6"
env_file="$7"
compose_file="$8"
cd "$remote_dir"
export DEPLOY_ENV="$deploy_env"
export IMAGE_REPOSITORY="$image_repository"
export IMAGE_TAG="$image_tag"
docker compose \
--project-name "$project_name" \
--env-file .env \
-f docker-compose.yml \
--env-file "$env_file" \
-f "$compose_file" \
up -d --remove-orphans --pull never "$service"
docker compose \
--project-name "$project_name" \
--env-file .env \
-f docker-compose.yml \
--env-file "$env_file" \
-f "$compose_file" \
ps "$service"
REMOTE_DEPLOY
+3 -1
View File
@@ -10,7 +10,8 @@
| 功能 | 入口 |
|---|---|
| 事件接收、订单列表/详情/导出、目的仓调整 | `src/main/java/com/baiyue/lastleg/controller/LastlegOrderController.java` |
| 事件接收、平台订单列表/详情/导出、目的仓调整 | `src/main/java/com/baiyue/lastleg/controller/LastlegOrderController.java` |
| 客户尾程订单列表与详情 | `controller/MerchantLastlegOrderController.java`;客户范围只取认证身份,稳定类型/状态见 `model/LastlegOrderType.java`、`LastlegOrderStatus.java` |
| 装柜完成事件消费 | `service/LoadingCompletedConsumer.java`;当前生产信封为 `FIRST_LEG_LOADING_PLAN_COMPLETED` V2,消费端按冻结字段校验后建单 |
| 尾程单号生成 | `service/LastlegOrderNoGenerator.java` |
| 订单查询与更新 | `service/LastlegOrderService.java` |
@@ -24,6 +25,7 @@
| PDA 卸柜照片文件上传与归属校验 | `InboundPdaController.java` 的 `/unload-photos/files`、`inboundplan/pda/gateway/UnloadPhotoFileGateway.java`、`HttpUnloadPhotoFileGateway.java`;File 类型固定 `OVERSEAS_UNLOAD_PHOTO` |
| 卸柜照片与海外卸柜完成 Outbox | `inboundplan/pda/controller/InboundPdaController.java`、`inboundplan/pda/service/InboundUnloadCompletedOutboxService.java`、`entity/LastlegEventOutbox.java`、`mapper/LastlegEventOutboxMapper.java`;事件模型见 `../common-event-contract/AGENTS.md` |
| 入仓后 SKU 库存余额、库位和流水 | `inventory/`;平台只读查询与非出库库存命令分别从 `inventory/query/controller/InventoryQueryController.java`、`inventory/command/controller/InventoryCommandController.java` 进入 |
| 平台排车列表/详情与智能预排 | `dispatch/controller/DispatchQueryController.java`、`SmartDispatchController.java`、`dispatch/mapper/DispatchCandidateMapper.java`、`dispatch/service/DispatchService.java`;入口 `/api/platform/v1/last-leg/dispatches`,范围开关与禁用写操作见“四期开发范围路由” |
| 仓储资源调用 | `inboundplan/pda/gateway/WarehouseResourceGateway.java`、`HttpWarehouseResourceGateway.java` |
| 仓储转运、历史订单/入仓绑定、可选换标联动 | `transfer/controller/WarehouseTransferController.java`、`transfer/service/WarehouseTransferService.java` |
| 一件代发接收、SKU/箱唛库存 | `dropship/service/DropshipOrderService.java`、`dropship/entity/`、`dropship/mapper/` |
@@ -33,7 +33,7 @@ public class InboundPlanController {
/** 按入仓计划明细分页查询可预约的入库订单行。 */
@GetMapping
@PreAuthorize("hasAuthority('" + OverseasPermissions.INBOUND_ORDER_CREATE + "')")
@PreAuthorize("hasAuthority('" + OverseasPermissions.INBOUND_ORDER_LIST + "')")
@Operation(summary = "分页查询入库订单", description = "Required authority: " + OverseasPermissions.INBOUND_ORDER_LIST)
public ApiResponse<PageResult<InboundPlanListResponse>> page(
@AuthenticationPrincipal AuthenticatedUser user,
@@ -43,7 +43,7 @@ public class InboundPlanController {
}
@PostMapping("/convert")
@PreAuthorize("hasAuthority('" + OverseasPermissions.INBOUND_ORDER_LIST + "')")
@PreAuthorize("hasAuthority('" + OverseasPermissions.INBOUND_ORDER_CREATE + "')")
/** 将入仓单转换为入仓计划。 */
@Operation(summary = "入仓单转入仓计划", description = "Required authority: " + OverseasPermissions.INBOUND_ORDER_CREATE)
public ApiResponse<InboundPlanResponse> convert(@AuthenticationPrincipal AuthenticatedUser user,
@@ -13,6 +13,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
class InboundPlanPermissionContractTest {
private static final String LIST = "platform:overseas-warehouse:inbound-order:list";
private static final String CREATE = "platform:overseas-warehouse:inbound-order:create";
private static final String FBA_APPOINTMENT =
"platform:overseas-warehouse:inbound-order:fba-appointment";
@@ -28,6 +29,17 @@ class InboundPlanPermissionContractTest {
}
}
@Test
void conversionUsesCanonicalInboundOrderCreateAuthority() {
Method method = Arrays.stream(InboundPlanController.class.getDeclaredMethods())
.filter(candidate -> candidate.getName().equals("convert"))
.findFirst()
.orElseThrow();
assertEquals("hasAuthority('" + CREATE + "')", method.getAnnotation(PreAuthorize.class).value());
assertEquals("Required authority: " + CREATE, method.getAnnotation(Operation.class).description());
}
@Test
void fbaAppointmentWritesUseDedicatedAuthority() {
Map<String, Method> methods = Arrays.stream(InboundPlanController.class.getDeclaredMethods())
+1 -1
View File
@@ -31,7 +31,7 @@ node scripts/create-pending-review-orders.mjs
| Admin/Client 本地测试与类型检查 | `scripts/frontend-full-regression.ps1` |
| PDA 本地页面和状态测试 | `../apps/pda-uniapp/tests/*.test.mjs` |
| 本地数据库 SQL 静态/受控验收 | `scripts/local-db-full-acceptance.ps1` |
| AGENTS 快速索引一致性 | `scripts/agents-index-contract.ps1`;只读核对部署单元、业务模块、PDA 注册页面、Maven 模块、关键入口与排除目录 |
| AGENTS 快速索引一致性 | `scripts/agents-index-contract.ps1`;只读核对根快速表唯一性、部署单元、业务模块、PDA 注册页面、Maven 模块、关键入口与排除目录 |
Admin/Client HTTP 脚本的离线合同只验证脚本结构、参数和模块边界,不访问测试环境:
+41 -2
View File
@@ -36,6 +36,27 @@ function Assert-Path([string]$relativePath) {
}
}
function Assert-MarkdownTableUnique([string]$path, [string]$heading, [int]$keyColumn, [string]$scope) {
$script:checked++
$lines = Get-Content -LiteralPath $path
$inside = $false
$keys = [System.Collections.Generic.List[string]]::new()
foreach ($line in $lines) {
if ($line -eq $heading) {
$inside = $true
continue
}
if ($inside -and $line.StartsWith('## ')) { break }
if (-not $inside -or -not $line.StartsWith('|')) { continue }
$cells = @($line.Trim('|').Split('|') | ForEach-Object { $_.Trim() })
if ($keyColumn -ge $cells.Count -or $cells[$keyColumn] -match '^:?-{3,}:?$') { continue }
$keys.Add($cells[$keyColumn])
}
$keys | Group-Object | Where-Object Count -gt 1 | ForEach-Object {
Add-Failure "$scope 存在重复索引键: $($_.Name)($($_.Count) 次)"
}
}
$rootAgentsPath = Join-Path $workspaceRoot 'AGENTS.md'
$adminAgentsPath = Join-Path $workspaceRoot 'apps/admin-web/AGENTS.md'
$clientAgentsPath = Join-Path $workspaceRoot 'apps/client-web/AGENTS.md'
@@ -55,6 +76,10 @@ Get-ChildItem -LiteralPath (Join-Path $workspaceRoot 'apps') -Directory |
Sort-Object Name |
ForEach-Object { Assert-Contains $rootAgents "apps/$($_.Name)/" '根 AGENTS.md' }
Assert-Contains $rootAgents 'baiyue-firstleg/' '根 AGENTS.md'
Assert-MarkdownTableUnique $rootAgentsPath '## 项目快速入口' 0 '根项目表'
Assert-MarkdownTableUnique $rootAgentsPath '## 项目快速入口' 1 '根项目目录'
Assert-MarkdownTableUnique $rootAgentsPath '## 业务快速路由表' 0 '根业务路由表'
Assert-MarkdownTableUnique $rootAgentsPath '## 原型入口索引' 0 '根原型表'
# 前端当前全部业务模块必须能从应用级索引命中。
$applications = @(
@@ -95,6 +120,9 @@ $criticalPaths = @(
'apps/admin-web/src/modules/overseas-warehouse/api/inboundPlans.ts',
'apps/admin-web/src/modules/overseas-warehouse/api/inventory.ts',
'apps/admin-web/src/modules/overseas-warehouse/api/pallets.ts',
'apps/admin-web/src/modules/overseas-warehouse/api/dispatches.ts',
'apps/admin-web/src/modules/overseas-warehouse/pages/DispatchesView.vue',
'apps/admin-web/src/modules/overseas-warehouse/components/SmartDispatchDialog.vue',
'apps/admin-web/src/modules/overseas-business/api/platformOverseasBusiness.ts',
'apps/admin-web/src/modules/overseas-master-data/api/overseasMasterData.ts',
'apps/pda-uniapp/pages/tasks/tasks.vue',
@@ -111,8 +139,19 @@ $criticalPaths = @(
'baiyue-firstleg/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/mapper/InboundPlanQueryMapper.java',
'baiyue-firstleg/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/service/InboundFbaAppointmentService.java',
'baiyue-firstleg/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/gateway/UnloadPhotoFileGateway.java',
'baiyue-firstleg/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/controller/DispatchQueryController.java',
'baiyue-firstleg/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/controller/SmartDispatchController.java',
'baiyue-firstleg/lastleg-service/src/main/java/com/baiyue/lastleg/controller/MerchantLastlegOrderController.java',
'baiyue-firstleg/lastleg-service/src/main/java/com/baiyue/lastleg/model/LastlegOrderType.java',
'baiyue-firstleg/lastleg-service/src/main/java/com/baiyue/lastleg/model/LastlegOrderStatus.java',
'baiyue-firstleg/firstleg/src/main/java/com/baiyue/firstleg/portschedule/service/CarrierScheduleQuerySlots.java',
'baiyue-firstleg/firstleg/src/main/java/com/baiyue/firstleg/portschedule/entity/CarrierScheduleExceptionFactEntity.java',
'baiyue-firstleg/integration-service/src/main/java/com/baiyue/integration/domain/CarrierScheduleStandardNode.java',
'baiyue-firstleg/integration-service/src/main/java/com/baiyue/integration/domain/CarrierScheduleExceptionCode.java',
'baiyue-firstleg/gateway-overseas/src/test/java/com/baiyue/gateway/config/OverseasLastlegRouteContractTest.java',
'baiyue-firstleg/event-bridge-service/src/main/java/com/baiyue/eventbridge/service/FirstlegEventClient.java',
'baiyue-firstleg/common-event-contract/src/main/java/com/baiyue/common/event/FirstLegLoadingPlanCompletedV2.java'
'baiyue-firstleg/common-event-contract/src/main/java/com/baiyue/common/event/FirstLegLoadingPlanCompletedV2.java',
'docs/四期/船节点标准化契约.md'
)
$criticalPaths | ForEach-Object { Assert-Path $_ }
@@ -164,5 +203,5 @@ if ($failures.Count -gt 0) {
exit 1
}
Write-Host "AGENTS 索引合同通过:$checked 项检查,覆盖部署单元、前端模块、PDA 注册页面、Maven 模块、关键入口和排除目录。" -ForegroundColor Green
Write-Host "AGENTS 索引合同通过:$checked 项检查,覆盖快速表唯一性、部署单元、前端模块、PDA 注册页面、Maven 模块、关键入口和排除目录。" -ForegroundColor Green
exit 0
+2
View File
@@ -3442,6 +3442,8 @@ SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE tab
PREPARE schema_comments_025 FROM @ddl; EXECUTE schema_comments_025; DEALLOCATE PREPARE schema_comments_025;
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='wh_pallet' AND column_name='remark' AND COALESCE(column_comment,'') <> '备注'), 'ALTER TABLE wh_pallet MODIFY COLUMN remark VARCHAR(500) DEFAULT NULL COMMENT ''备注''', 'SELECT 1'));
PREPARE schema_comments_025 FROM @ddl; EXECUTE schema_comments_025; DEALLOCATE PREPARE schema_comments_025;
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='wh_pallet' AND column_name='fba_warehouse_code' AND COALESCE(column_comment,'') <> '所属FBA仓编码'), 'ALTER TABLE wh_pallet MODIFY COLUMN fba_warehouse_code VARCHAR(128) DEFAULT NULL COMMENT ''所属FBA仓编码''', 'SELECT 1'));
PREPARE schema_comments_025 FROM @ddl; EXECUTE schema_comments_025; DEALLOCATE PREPARE schema_comments_025;
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='wh_pallet' AND column_name='created_by' AND COALESCE(column_comment,'') <> '创建人ID'), 'ALTER TABLE wh_pallet MODIFY COLUMN created_by BIGINT UNSIGNED DEFAULT NULL COMMENT ''创建人ID''', 'SELECT 1'));
PREPARE schema_comments_025 FROM @ddl; EXECUTE schema_comments_025; DEALLOCATE PREPARE schema_comments_025;
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='wh_pallet' AND column_name='created_at' AND COALESCE(column_comment,'') <> '创建时间'), 'ALTER TABLE wh_pallet MODIFY COLUMN created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT ''创建时间''', 'SELECT 1'));
@@ -70,6 +70,10 @@ test('CN and overseas event bridges have explicit opposite-region consumers', as
assert.match(scripts, /env_file_value REGION_CODE/)
assert.match(scripts, /EVENT_BRIDGE_REGION_OVERRIDE/)
assert.match(scripts, /FIRSTLEG_CONTAINER_EVENT_CONSUMER_URL/)
assert.match(scripts, /COMPOSE_FILE="deploy\/docker-compose\.overseas\.yml"/)
assert.match(scripts, /PROJECT_NAME="firstleg-\$DEPLOY_ENV-overseas"/)
assert.match(scripts, /REMOTE_ENV_NAME="\.env\.\$PROJECT_NAME"/)
assert.match(scripts, /REMOTE_COMPOSE_NAME="docker-compose\.\$PROJECT_NAME\.yml"/)
for (const key of [
'NACOS_SERVER_ADDR', 'OVERSEAS_NACOS_NAMESPACE', 'INTERNAL_SERVICE_TOKEN',
'JWT_ISSUER', 'OVERSEAS_JWT_AUDIENCE', 'JWT_JWK_SET_URI'
@@ -39,6 +39,25 @@ test('Java builds use the shared builder pool and deploy on the target environme
assert.match(directDeployScript, /--pull never/)
})
test('parallel remote deployments keep env and compose assets isolated by project', () => {
assert.match(directDeployScript, /REMOTE_ENV_NAME="\.env\.\$PROJECT_NAME"/)
assert.match(directDeployScript, /REMOTE_COMPOSE_NAME="docker-compose\.\$PROJECT_NAME\.yml"/)
assert.match(directDeployScript, /REMOTE_ENV_TMP="\$REMOTE_DIR\/\$REMOTE_ENV_NAME\.\$SUFFIX\.tmp"/)
assert.match(directDeployScript, /REMOTE_COMPOSE_TMP="\$REMOTE_DIR\/\$REMOTE_COMPOSE_NAME\.\$SUFFIX\.tmp"/)
assert.match(directDeployScript, /env_target="\$3"/)
assert.match(directDeployScript, /compose_target="\$4"/)
assert.match(directDeployScript, /mv -f "\$env_tmp" "\$env_target"/)
assert.match(directDeployScript, /mv -f "\$compose_tmp" "\$compose_target"/)
assert.match(directDeployScript, /env_file="\$7"/)
assert.match(directDeployScript, /compose_file="\$8"/)
assert.match(directDeployScript, /--env-file "\$env_file"/)
assert.match(directDeployScript, /-f "\$compose_file"/)
assert.doesNotMatch(directDeployScript, /mv -f "\$env_tmp" "\$remote_dir\/\.env"/)
assert.doesNotMatch(directDeployScript, /mv -f "\$compose_tmp" "\$remote_dir\/docker-compose\.yml"/)
assert.doesNotMatch(directDeployScript, /--env-file \.env/)
assert.doesNotMatch(directDeployScript, /-f docker-compose\.yml/)
})
test('shared builders use a reachable public Maven mirror instead of the retired private proxy', () => {
assert.match(pipeline, /MAVEN_SETTINGS = 'deploy\/maven\/settings-ci\.xml'/)
assert.match(ciMavenSettings, /https:\/\/maven\.aliyun\.com\/repository\/public/)