diff --git a/AGENTS.md b/AGENTS.md index ba6956a59..6ad6df887 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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/` | +| 海外仓 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` | diff --git a/Jenkinsfile b/Jenkinsfile index 94ce52fb3..da9431c82 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -62,7 +62,8 @@ pipeline { booleanParam(name: 'SERVICE_INTEGRATION', defaultValue: false, description: '第三方集成服务(integration-service)') booleanParam(name: 'SERVICE_LASTLEG', defaultValue: false, description: '海外尾程与入仓服务(lastleg-service)') booleanParam(name: 'SERVICE_WAREHOUSE_OVERSEAS', defaultValue: false, description: '海外仓资源服务(warehouse-overseas-service)') - booleanParam(name: 'SERVICE_EVENT_BRIDGE', defaultValue: false, description: '跨区域事件桥(event-bridge-service;由环境凭据 REGION_CODE 显式选择 CN 或 OVERSEAS 实例)') + booleanParam(name: 'SERVICE_EVENT_BRIDGE', defaultValue: false, description: '跨区域事件桥(event-bridge-service;由 EVENT_BRIDGE_REGION 选择实例)') + choice(name: 'EVENT_BRIDGE_REGION', choices: ['CN', 'OVERSEAS'], description: '仅部署 event-bridge-service 时生效;明确选择国内或海外实例') booleanParam(name: 'SERVICE_AUDIT', defaultValue: false, description: '审计服务(audit-service)') choice(name: 'ENVIRONMENT', choices: environmentChoicesForJob(env.JOB_BASE_NAME), description: '部署环境') booleanParam(name: 'DEPLOY', defaultValue: false, description: '各服务构建成功后是否独立部署') @@ -207,7 +208,8 @@ pipeline { booleanParam(name: 'DEPLOY', value: params.DEPLOY), booleanParam(name: 'SKIP_TESTS', value: params.SKIP_TESTS), 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), + string(name: 'EVENT_BRIDGE_REGION', value: params.EVENT_BRIDGE_REGION) ] ) downstreamResults[service] = downstreamBuild.result ?: 'UNKNOWN' diff --git a/Jenkinsfile.service b/Jenkinsfile.service index a84035a53..9f2e622a2 100644 --- a/Jenkinsfile.service +++ b/Jenkinsfile.service @@ -38,6 +38,7 @@ pipeline { choice(name: 'ENVIRONMENT', choices: ['test', 'development', 'staging', 'production'], description: '部署环境') booleanParam(name: 'DEPLOY', defaultValue: false, description: '构建成功后是否部署本服务') booleanParam(name: 'SKIP_TESTS', defaultValue: false, description: '是否跳过测试') + choice(name: 'EVENT_BRIDGE_REGION', choices: ['CN', 'OVERSEAS'], description: '仅 event-bridge-service 使用;通常由总控 Job 传入') string(name: 'IMAGE_REPOSITORY', defaultValue: '172.31.198.7:10082/java-project/baiyue-firstleg', description: '内网共享镜像仓库基础路径,不包含服务名和标签') string(name: 'DOCKER_BASE_IMAGE', defaultValue: 'docker.m.daocloud.io/library/eclipse-temurin:17-jre-alpine', description: 'Java 17 基础镜像') } @@ -193,6 +194,7 @@ pipeline { withEnv([ "TARGET_IS_LOCAL=${targetIsLocal}", + "EVENT_BRIDGE_REGION_OVERRIDE=${params.EVENT_BRIDGE_REGION}", "TARGET_SSH_TARGET=${remoteTarget}", "TARGET_SSH_KEY=${remoteKey}", "TARGET_SSH_KNOWN_HOSTS=${remoteKnownHosts}", @@ -233,7 +235,10 @@ pipeline { withCredentials([usernamePassword(credentialsId: env.REGISTRY_CREDENTIALS, usernameVariable: 'REGISTRY_USER', passwordVariable: 'REGISTRY_PASSWORD')]) { try { sh 'printf %s "$REGISTRY_PASSWORD" | docker login -u "$REGISTRY_USER" --password-stdin "' + registryHost + '"' - withEnv(["DEVELOPMENT_DEPLOY_LOCAL=${localDevelopmentDeploy}"]) { + withEnv([ + "DEVELOPMENT_DEPLOY_LOCAL=${localDevelopmentDeploy}", + "EVENT_BRIDGE_REGION_OVERRIDE=${params.EVENT_BRIDGE_REGION}" + ]) { withCredentials([file(credentialsId: credentialId, variable: 'DEPLOY_ENV_FILE')]) { sh "sh deploy/scripts/deploy-services.sh '${params.ENVIRONMENT}' '${env.RESOLVED_IMAGE_REPOSITORY}' '${env.RESOLVED_IMAGE_TAG}' '${env.RESOLVED_SERVICE}' \"\$DEPLOY_ENV_FILE\"" } diff --git a/deploy/scripts/deploy-built-service.sh b/deploy/scripts/deploy-built-service.sh index 420d2e9d7..e41e3719c 100644 --- a/deploy/scripts/deploy-built-service.sh +++ b/deploy/scripts/deploy-built-service.sh @@ -49,11 +49,15 @@ case "$SERVICE" in PROJECT_NAME="firstleg-$DEPLOY_ENV-overseas" ;; event-bridge-service) - if ! env_file_has_key REGION_CODE; then + BRIDGE_REGION="${EVENT_BRIDGE_REGION_OVERRIDE:-}" + if [ -z "$BRIDGE_REGION" ] && env_file_has_key REGION_CODE; then + BRIDGE_REGION="$(env_file_value REGION_CODE)" + fi + if [ -z "$BRIDGE_REGION" ]; then echo "部署 event-bridge-service 时,环境文件必须显式包含 REGION_CODE=CN 或 REGION_CODE=OVERSEAS" >&2 exit 2 fi - case "$(env_file_value REGION_CODE)" in + case "$BRIDGE_REGION" in CN) ;; OVERSEAS) COMPOSE_FILE="deploy/docker-compose.overseas.yml" @@ -123,7 +127,7 @@ if [ "$SERVICE" = "event-bridge-service" ]; then if [ "$PROJECT_NAME" = "firstleg-$DEPLOY_ENV-overseas" ]; then EVENT_BRIDGE_KEYS="EVENT_BRIDGE_OVERSEAS_DB_URL EVENT_BRIDGE_OVERSEAS_DB_USERNAME EVENT_BRIDGE_OVERSEAS_DB_PASSWORD EVENT_CONSUMER_URL" else - EVENT_BRIDGE_KEYS="EVENT_BRIDGE_DB_URL EVENT_BRIDGE_DB_USERNAME EVENT_BRIDGE_DB_PASSWORD EVENT_CONSUMER_URL" + EVENT_BRIDGE_KEYS="EVENT_BRIDGE_DB_URL EVENT_BRIDGE_DB_USERNAME EVENT_BRIDGE_DB_PASSWORD FIRSTLEG_CONTAINER_EVENT_CONSUMER_URL" fi for key in $EVENT_BRIDGE_KEYS; do if ! env_file_has_key "$key" || [ -z "$(env_file_value "$key")" ]; then diff --git a/deploy/scripts/deploy-services.sh b/deploy/scripts/deploy-services.sh index e420ee93a..ea21fbb89 100644 --- a/deploy/scripts/deploy-services.sh +++ b/deploy/scripts/deploy-services.sh @@ -41,11 +41,15 @@ for service in $SERVICES; do case "$service" in gateway-overseas|lastleg-service|warehouse-overseas-service) DEPLOY_REGION=OVERSEAS ;; event-bridge-service) - if ! env_file_has_key REGION_CODE; then + BRIDGE_REGION="${EVENT_BRIDGE_REGION_OVERRIDE:-}" + if [ -z "$BRIDGE_REGION" ] && env_file_has_key REGION_CODE; then + BRIDGE_REGION="$(env_file_value REGION_CODE)" + fi + if [ -z "$BRIDGE_REGION" ]; then echo "部署 event-bridge-service 时,环境文件必须显式包含 REGION_CODE=CN 或 REGION_CODE=OVERSEAS" >&2 exit 2 fi - case "$(env_file_value REGION_CODE)" in + case "$BRIDGE_REGION" in CN) ;; OVERSEAS) DEPLOY_REGION=OVERSEAS ;; *) echo "部署 event-bridge-service 时,REGION_CODE 只能为 CN 或 OVERSEAS" >&2; exit 2 ;; @@ -123,7 +127,7 @@ case " $SERVICES " in if [ "$DEPLOY_REGION" = "OVERSEAS" ]; then EVENT_BRIDGE_KEYS="EVENT_BRIDGE_OVERSEAS_DB_URL EVENT_BRIDGE_OVERSEAS_DB_USERNAME EVENT_BRIDGE_OVERSEAS_DB_PASSWORD EVENT_CONSUMER_URL" else - EVENT_BRIDGE_KEYS="EVENT_BRIDGE_DB_URL EVENT_BRIDGE_DB_USERNAME EVENT_BRIDGE_DB_PASSWORD EVENT_CONSUMER_URL" + EVENT_BRIDGE_KEYS="EVENT_BRIDGE_DB_URL EVENT_BRIDGE_DB_USERNAME EVENT_BRIDGE_DB_PASSWORD FIRSTLEG_CONTAINER_EVENT_CONSUMER_URL" fi for key in $EVENT_BRIDGE_KEYS; do if ! env_file_has_key "$key" || [ -z "$(env_file_value "$key")" ]; then diff --git a/gateway-overseas/AGENTS.md b/gateway-overseas/AGENTS.md index 7c9b1e6d5..284d8062c 100644 --- a/gateway-overseas/AGENTS.md +++ b/gateway-overseas/AGENTS.md @@ -16,6 +16,7 @@ | 安全配置项 | `config/GatewaySecurityProperties.java` | | 可信身份头传递 | `filter/TrustedIdentityHeaderFilter.java` | | 环境配置 | `src/main/resources/application.yml`、`../config/nacos/` | +| 海外 PDA 路由 | 设备端 `../../apps/pda-overseas-uniapp/`,服务端入口 `/api/pda/v1/last-leg/**`;必须在 Gateway 路由中显式声明并保持海外 Token/权限校验,不得让设备直连 Lastleg | | 测试 | `src/test/java/com/baiyue/gateway/config/` | ## 安全规则 @@ -23,6 +24,7 @@ - 生产令牌 `aud` 必须包含 `lastleg-api`,并包含配置要求的身份声明。 - 网关必须清除外部请求伪造的可信身份头,仅传递验签后生成的身份信息。 - JWKS 地址、Issuer 和 Audience 必须外部配置;不得在源码写死生产密钥。 +- merchant、platform 与 PDA 路径分别显式维护路由和授权边界;新增 PDA Controller 不会自动成为公网路由,Gateway 未声明前不得把设备链路标记为已联通。 ## 修改与验证 diff --git a/scripts/agents-index-contract.ps1 b/scripts/agents-index-contract.ps1 index 7e4eaa97f..70e839089 100644 --- a/scripts/agents-index-contract.ps1 +++ b/scripts/agents-index-contract.ps1 @@ -40,12 +40,14 @@ $rootAgentsPath = Join-Path $workspaceRoot 'AGENTS.md' $adminAgentsPath = Join-Path $workspaceRoot 'apps/admin-web/AGENTS.md' $clientAgentsPath = Join-Path $workspaceRoot 'apps/client-web/AGENTS.md' $pdaAgentsPath = Join-Path $workspaceRoot 'apps/pda-uniapp/AGENTS.md' +$overseasPdaAgentsPath = Join-Path $workspaceRoot 'apps/pda-overseas-uniapp/AGENTS.md' $backendAgentsPath = Join-Path $backendRoot 'AGENTS.md' $rootAgents = Read-RequiredText $rootAgentsPath $adminAgents = Read-RequiredText $adminAgentsPath $clientAgents = Read-RequiredText $clientAgentsPath $pdaAgents = Read-RequiredText $pdaAgentsPath +$overseasPdaAgents = Read-RequiredText $overseasPdaAgentsPath $backendAgents = Read-RequiredText $backendAgentsPath # 部署单元必须能从根索引进入。 @@ -65,14 +67,20 @@ foreach ($application in $applications) { } } -# PDA 只检查 pages.json 正式注册页面,不把空目录或未注册原型纳入索引。 -$pagesJsonPath = Join-Path $workspaceRoot 'apps/pda-uniapp/pages.json' -$pagesJson = Get-Content -LiteralPath $pagesJsonPath -Raw | ConvertFrom-Json -$registeredPageRoots = $pagesJson.pages.path | - ForEach-Object { $parts = $_ -split '/'; "$($parts[0])/$($parts[1])/" } | - Sort-Object -Unique -foreach ($pageRoot in $registeredPageRoots) { - Assert-Contains $pdaAgents $pageRoot 'PDA AGENTS.md' +# 国内/海外 PDA 只检查 pages.json 正式注册页面,不把空目录或未注册原型纳入索引。 +$pdaApplications = @( + @{ Pages = 'apps/pda-uniapp/pages.json'; Text = $pdaAgents; Scope = 'PDA AGENTS.md'; Prefix = '' }, + @{ Pages = 'apps/pda-overseas-uniapp/src/pages.json'; Text = $overseasPdaAgents; Scope = 'Overseas PDA AGENTS.md'; Prefix = 'src/' } +) +foreach ($pdaApplication in $pdaApplications) { + $pagesJsonPath = Join-Path $workspaceRoot $pdaApplication.Pages + $pagesJson = Get-Content -LiteralPath $pagesJsonPath -Raw | ConvertFrom-Json + $registeredPageRoots = $pagesJson.pages.path | + ForEach-Object { $parts = $_ -split '/'; "$($pdaApplication.Prefix)$($parts[0])/$($parts[1])/" } | + Sort-Object -Unique + foreach ($pageRoot in $registeredPageRoots) { + Assert-Contains $pdaApplication.Text $pageRoot $pdaApplication.Scope + } } # Maven 聚合声明的全部历史/当前模块必须在后端根指引中可定位。 @@ -91,6 +99,13 @@ $criticalPaths = @( 'apps/admin-web/src/modules/overseas-master-data/api/overseasMasterData.ts', 'apps/pda-uniapp/pages/tasks/tasks.vue', 'apps/pda-uniapp/utils/evidence-upload-queue.js', + 'apps/pda-overseas-uniapp/package.json', + 'apps/pda-overseas-uniapp/src/pages.json', + 'apps/pda-overseas-uniapp/src/data/overseas-workbench.js', + 'apps/pda-overseas-uniapp/src/pages/operation/operation.vue', + 'apps/pda-overseas-uniapp/src/config/runtime.js', + 'apps/pda-overseas-uniapp/src/api/auth.js', + 'apps/pda-overseas-uniapp/src/utils/scan-broadcast.js', 'baiyue-firstleg/firstleg/src/main/java/com/baiyue/firstleg/service/FeePaymentSnapshotValidator.java', 'baiyue-firstleg/firstleg/src/main/java/com/baiyue/firstleg/client/FeePaymentVoucherFileClient.java', 'baiyue-firstleg/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/mapper/InboundPlanQueryMapper.java', @@ -107,6 +122,7 @@ $indexedAgentFiles = @( $adminAgentsPath, $clientAgentsPath, $pdaAgentsPath, + $overseasPdaAgentsPath, $backendAgentsPath, (Join-Path $backendRoot 'firstleg/AGENTS.md'), (Join-Path $backendRoot 'finance/AGENTS.md'), diff --git a/scripts/overseas-deployment-contract.test.mjs b/scripts/overseas-deployment-contract.test.mjs index 4afcd604d..e37d7e708 100644 --- a/scripts/overseas-deployment-contract.test.mjs +++ b/scripts/overseas-deployment-contract.test.mjs @@ -18,6 +18,8 @@ test('Jenkins pipelines recognize all overseas deployable modules', async () => assert.match(service, /gateway-overseas\/target\/gateway-overseas-1\.0\.0-SNAPSHOT\.jar/) assert.match(service, /lastleg-service\/target\/lastleg-service-1\.0\.0-SNAPSHOT\.jar/) assert.match(service, /warehouse-overseas-service\/target\/warehouse-overseas-service-1\.0\.0-SNAPSHOT\.jar/) + assert.match(controller, /EVENT_BRIDGE_REGION/) + assert.match(service, /EVENT_BRIDGE_REGION_OVERRIDE=\$\{params\.EVENT_BRIDGE_REGION\}/) }) test('service map freezes unique internal ports and moves event bridge away from 9300', async () => { @@ -66,6 +68,8 @@ test('CN and overseas event bridges have explicit opposite-region consumers', as assert.match(scripts, /docker-compose\.overseas\.yml/) assert.match(scripts, /REGION_CODE=CN 或 REGION_CODE=OVERSEAS/) assert.match(scripts, /env_file_value REGION_CODE/) + assert.match(scripts, /EVENT_BRIDGE_REGION_OVERRIDE/) + assert.match(scripts, /FIRSTLEG_CONTAINER_EVENT_CONSUMER_URL/) for (const key of [ 'NACOS_SERVER_ADDR', 'OVERSEAS_NACOS_NAMESPACE', 'INTERNAL_SERVICE_TOKEN', 'JWT_ISSUER', 'OVERSEAS_JWT_AUDIENCE', 'JWT_JWK_SET_URI'