This commit is contained in:
2026-09-29 18:47:37 +08:00
parent 2c60a29e0f
commit c2927ab9a6
291 changed files with 4008 additions and 6298 deletions
-12
View File
@@ -1,12 +0,0 @@
# Auth signs short-lived overseas RS256 tokens with a private JWK Set.
OVERSEAS_JWT_ENABLED=true
OVERSEAS_JWT_ISSUER=auth-service
OVERSEAS_JWT_AUDIENCE=lastleg-api
OVERSEAS_JWT_CURRENT_KID=replace-with-current-signing-kid
OVERSEAS_ACCESS_TOKEN_TTL=15m
OVERSEAS_JWT_RSA_JWK_SET={"keys":[{"kty":"RSA","kid":"replace-with-current-signing-kid","use":"sig","alg":"RS256","n":"replace-with-modulus","e":"AQAB","d":"replace-with-private-exponent","p":"replace","q":"replace","dp":"replace","dq":"replace","qi":"replace"}]}
# Overseas services validate tokens against Auth's public JWKS endpoint.
JWT_JWK_SET_URI=https://replace-with-auth-host/api/auth/.well-known/jwks.json
JWT_ISSUER=auth-service
JWT_AUDIENCE=lastleg-api
+42
View File
@@ -0,0 +1,42 @@
# TEST 单机部署执行清单
本清单是本地可审核目标,尚未部署或完成真实环境验证。唯一入口:`docker-compose.yml`、`service-map.yml`、`env/.env.test.example` 与 `../config/nacos/test/`。正式规则见外层 `docs/测试环境单机部署与配置契约.md`、`docs/统一认证与服务通信契约.md`。
## 发布前清单
1. 记录13个服务镜像 tag/digest、后端和四端版本、SQL基线及14个Nacos配置版本。统一服务集合:Gateway/Auth/IAM/Order/Firstleg/Finance/Warehouse/Warehouse Overseas/Lastleg/File/Print/Audit/Integration。不存在单独海外Gateway或事件桥发布项。
2. 人工核对实际Nacos Namespace ID、Group、主机容量、文件卷UID 10001读写权限、MySQL各Schema、Redis DB/会话前缀、现有MQ topic/group。环境模板的Schema名只是待核基线,不能据此重命名运行库。
3. 所有容器共用 firstleg-test-network,并接外部 INFRA_NETWORK(默认 firstleg-test-infra-network)。中间件运行在宿主机时使用 host.docker.internal;运行在容器时用同网络服务名。禁止跨容器 localhost。Nacos应用名不等于Docker服务名,Auth HTTP地址使用 auth:9100。
4. Gateway仅向宿主机127.0.0.1发布9000(可配置GATEWAY_PUBLISHED_PORT)。同网络Nginx转发 gateway:9000;其余业务仅内部expose。不可把内部API端口直接发布给用户。
5. Auth私钥JWT_RSA_JWK_SET与JWT_CURRENT_KID由密钥凭据提供;所有服务共用JWT_JWKS_URI、Redis会话和内部服务令牌。EVENT_OPERATIONS_TOKEN与业务令牌分离,仅三家事件生产服务注入;为空禁用运维操作。禁止把真实env提交到仓库。
6. 本地Node合同、后端编译/测试及前端门禁完成后才能安排部署。Jenkins总控 backend-java,下游 baiyue-firstleg-<module>,构建节点java-builder,唯一部署节点java-test,凭据firstleg-test-env。旧dev/prod Job必须停用或改回其独立受维护版本,不得指向此TEST流水线;不对既有实际生产环境做改名、删除或部署。
## 停机切换
停止验收与业务写入并记录当前容器、网络、配置、卷、会话和消息对象。逐项取得部署、SQL和删除授权;本清单不授权执行。
先关闭旧写入、Relay和消费进程,再应用审核通过的服务SQL基线、统一Nacos和Compose配置。不执行整机清空Redis/MQ/文件,不删除业务Schema。业务MQ继续保留:文件导出、通知、审计启用参数按现有业务核实,test-前缀隔离;可靠审计可显式叠加docker-compose.audit.yml。文件卷持续挂载/data/files。
启动原则:中间件健康 → IAM/Auth和公共能力 → 业务消费者/生产者 → Gateway → 四端。Compose不创建循环depends_on;对每个服务以镜像HEALTHCHECK及真实消费者探针确认就绪后才开放写入。所有服务关闭自动SQL初始化,SQL必须单独审核执行。
执行入口deploy-built-service.sh仅接受test与13个保留服务,必须在目标服务器本机执行(TARGET_IS_LOCAL=true);缺失关键环境参数、没有HEALTHCHECK、异常退出、健康超时均失败。deploy-services.sh仅顺序调用此入口,任何失败停止后续服务。构建和部署仍由明确授权启动。
统一登录、撤销、客户隔离、仓绑定、文件和资源查询通过后,新造装柜→尾程、资金指令→结果回传数据。完成正式契约全链路与故障恢复矩阵前不得宣称环境跑通。
旧海外网关/两区桥退出目标清单不代表已停机。只有直接消费、可靠重试/DEAD治理、认证撤销及四端切换验证通过,且确认无旧调用,才能按明确对象清单退役旧容器、注册和专用配置。禁止提前停止现有线上桥。
## 失败恢复
架构/Schema不匹配时默认停写、保留证据,修复后发布完整匹配新版本。确需恢复旧测试工作时,由人工按记录整体恢复旧镜像、四端、Nacos/Compose和旧Schema基线;不混跑两套协议。
单服务自动回滚默认关闭。仅确认前后版本协议和Schema完全一致时,才能在单服务Jenkins勾选ALLOW_SAME_CONTRACT_ROLLBACK;失败只允许恢复同仓库同服务前一镜像,回滚成功仍将Job判为失败。首次部署、旧镜像不属于该服务或旧新镜像相同均拒绝回滚。
## 本地验证入口
`node --test scripts/java-topology-contract.test.mjs scripts/overseas-deployment-contract.test.mjs scripts/deploy-built-service-rollback.test.mjs scripts/finance-deployment-contract.test.mjs scripts/audit-deployment-contract.test.mjs`
PowerShell Nacos发布DryRun只列待发布项,不联网;Shell/Groovy离线合同不能替代真实Jenkins语法校验、Docker Compose渲染、镜像启动或HTTP验收。
四端协作确认:Admin/Client各自使用firstleg-test-admin-web与firstleg-test-client-web Compose项目,共接external firstleg-test-network;默认API_UPSTREAM=http://gateway:9000,页面仅绑定127.0.0.1:9001/9002。发布必须提供CLIENT_WEB_URL与VITE_ADMIN_WEB_ORIGIN,不能猜测运行域名。两个PDA保留独立产物,共用统一入口。
SQL初始化脚本只接受两份显式传入、已审核的空库baseline路径;未设置ALLOW_TEST_SCHEMA_INITIALIZATION=true与明确TEST_MYSQL_CONTAINER时拒绝。它不检查或创建授权,不允许用于已有数据环境,不自动清库,第一份失败立即停止。两份基线的当前维护入口为deploy/sql/README.md;实际SQL执行仍由独立授权覆盖。
+11 -11
View File
@@ -4,40 +4,40 @@ services:
AUDIT_OUTBOX_ENABLED: "true"
AUDIT_OUTBOX_PUBLISHER_ENABLED: "true"
ROCKETMQ_NAME_SERVER: ${ROCKETMQ_NAME_SERVER:?可靠审计启用时必须配置ROCKETMQ_NAME_SERVER}
ROCKETMQ_PRODUCER_GROUP: ${MQ_TOPIC_PREFIX-}auth-service-audit-publisher-v1
MQ_TOPIC_PREFIX: ${MQ_TOPIC_PREFIX-}
ROCKETMQ_PRODUCER_GROUP: ${MQ_TOPIC_PREFIX:-test-}auth-service-audit-publisher-v1
MQ_TOPIC_PREFIX: ${MQ_TOPIC_PREFIX:-test-}
iam:
environment:
AUDIT_OUTBOX_ENABLED: "true"
AUDIT_OUTBOX_PUBLISHER_ENABLED: "true"
ROCKETMQ_NAME_SERVER: ${ROCKETMQ_NAME_SERVER:?可靠审计启用时必须配置ROCKETMQ_NAME_SERVER}
ROCKETMQ_PRODUCER_GROUP: ${MQ_TOPIC_PREFIX-}iam-service-audit-publisher-v1
MQ_TOPIC_PREFIX: ${MQ_TOPIC_PREFIX-}
ROCKETMQ_PRODUCER_GROUP: ${MQ_TOPIC_PREFIX:-test-}iam-service-audit-publisher-v1
MQ_TOPIC_PREFIX: ${MQ_TOPIC_PREFIX:-test-}
order:
environment:
AUDIT_OUTBOX_ENABLED: "true"
AUDIT_OUTBOX_PUBLISHER_ENABLED: "true"
ROCKETMQ_NAME_SERVER: ${ROCKETMQ_NAME_SERVER:?可靠审计启用时必须配置ROCKETMQ_NAME_SERVER}
ROCKETMQ_PRODUCER_GROUP: ${MQ_TOPIC_PREFIX-}order-service-audit-publisher-v1
MQ_TOPIC_PREFIX: ${MQ_TOPIC_PREFIX-}
ROCKETMQ_PRODUCER_GROUP: ${MQ_TOPIC_PREFIX:-test-}order-service-audit-publisher-v1
MQ_TOPIC_PREFIX: ${MQ_TOPIC_PREFIX:-test-}
firstleg:
environment:
AUDIT_OUTBOX_ENABLED: "true"
AUDIT_OUTBOX_PUBLISHER_ENABLED: "true"
ROCKETMQ_NAME_SERVER: ${ROCKETMQ_NAME_SERVER:?可靠审计启用时必须配置ROCKETMQ_NAME_SERVER}
ROCKETMQ_PRODUCER_GROUP: ${MQ_TOPIC_PREFIX-}firstleg-service-audit-publisher-v1
MQ_TOPIC_PREFIX: ${MQ_TOPIC_PREFIX-}
ROCKETMQ_PRODUCER_GROUP: ${MQ_TOPIC_PREFIX:-test-}firstleg-service-audit-publisher-v1
MQ_TOPIC_PREFIX: ${MQ_TOPIC_PREFIX:-test-}
warehouse:
environment:
AUDIT_OUTBOX_ENABLED: "true"
AUDIT_OUTBOX_PUBLISHER_ENABLED: "true"
ROCKETMQ_NAME_SERVER: ${ROCKETMQ_NAME_SERVER:?可靠审计启用时必须配置ROCKETMQ_NAME_SERVER}
ROCKETMQ_PRODUCER_GROUP: ${MQ_TOPIC_PREFIX-}warehouse-service-audit-publisher-v1
MQ_TOPIC_PREFIX: ${MQ_TOPIC_PREFIX-}
ROCKETMQ_PRODUCER_GROUP: ${MQ_TOPIC_PREFIX:-test-}warehouse-service-audit-publisher-v1
MQ_TOPIC_PREFIX: ${MQ_TOPIC_PREFIX:-test-}
audit-service:
environment:
ROCKETMQ_NAME_SERVER: ${ROCKETMQ_NAME_SERVER:?可靠审计启用时必须配置ROCKETMQ_NAME_SERVER}
MQ_TOPIC_PREFIX: ${MQ_TOPIC_PREFIX-}
MQ_TOPIC_PREFIX: ${MQ_TOPIC_PREFIX:-test-}
AUDIT_MQ_CONSUMER_ENABLED: "true"
AUDIT_DLQ_CONSUMER_ENABLED: "true"
AUDIT_DLQ_COMPENSATION_ENABLED: "true"
-116
View File
@@ -1,116 +0,0 @@
name: firstleg-${DEPLOY_ENV:-test}-overseas
x-overseas-common: &overseas-common
restart: "on-failure:3"
stop_grace_period: ${STOP_GRACE_PERIOD:-45s}
networks: [firstleg-overseas-network, infra-network]
extra_hosts:
- "host.docker.internal:host-gateway"
environment: &overseas-environment
BAIYUE_DEPLOY_SLOT: ${DEPLOY_SLOT:-rolling}
BAIYUE_RELEASE_ID: ${RELEASE_ID:-local}
BAIYUE_RUNTIME_SCHEDULING_ENABLED: ${BAIYUE_RUNTIME_SCHEDULING_ENABLED:-true}
SERVER_SHUTDOWN: graceful
SPRING_LIFECYCLE_TIMEOUT_PER_SHUTDOWN_PHASE: ${SHUTDOWN_PHASE_TIMEOUT:-30s}
SPRING_PROFILES_ACTIVE: overseas
NACOS_SERVER_ADDR: ${NACOS_SERVER_ADDR}
NACOS_USERNAME: ${OVERSEAS_NACOS_USERNAME}
NACOS_PASSWORD: ${OVERSEAS_NACOS_PASSWORD}
NACOS_GROUP: ${NACOS_GROUP:-BAIYUE}
NACOS_NAMESPACE: ${OVERSEAS_NACOS_NAMESPACE:-prod-overseas}
REDIS_HOST: ${OVERSEAS_REDIS_HOST:-host.docker.internal}
REDIS_PORT: ${OVERSEAS_REDIS_PORT:-6379}
REDIS_PASSWORD: ${OVERSEAS_REDIS_PASSWORD:-}
REDIS_DATABASE: ${OVERSEAS_REDIS_DATABASE:-0}
INTERNAL_SERVICE_TOKEN: ${INTERNAL_SERVICE_TOKEN}
JWT_ISSUER: ${JWT_ISSUER}
JWT_AUDIENCE: ${OVERSEAS_JWT_AUDIENCE:-lastleg-api}
JWT_JWK_SET_URI: ${JWT_JWK_SET_URI}
REGION_CODE: OVERSEAS
DEPLOYMENT_CELL: ${OVERSEAS_DEPLOYMENT_CELL:-test-overseas}
DATA_RESIDENCY: OVERSEAS
SPRING_SQL_INIT_MODE: never
SERVER_MAX_HTTP_REQUEST_HEADER_SIZE: ${SERVER_MAX_HTTP_REQUEST_HEADER_SIZE:-32KB}
TZ: Asia/Shanghai
logging:
# 应用只写 stdout/stderr;json-file 兼容宝塔查看,大小与数量均受控。
driver: ${DOCKER_LOG_DRIVER:-json-file}
options:
max-size: ${DOCKER_LOG_MAX_SIZE:-50m}
max-file: "${DOCKER_LOG_MAX_FILE:-3}"
services:
gateway-overseas:
<<: *overseas-common
image: ${IMAGE_REPOSITORY}/gateway-overseas:${IMAGE_TAG}
container_name: firstleg-${DEPLOY_ENV:-test}-gateway-overseas${DEPLOY_SLOT_SUFFIX:-}
ports: ["${PUBLISHED_BIND_ADDRESS:-0.0.0.0}:${GATEWAY_OVERSEAS_PUBLISHED_PORT:-${GATEWAY_OVERSEAS_PORT:-9300}}:9300"]
environment:
<<: *overseas-environment
SERVER_PORT: 9300
SWAGGER_ENABLED: ${SWAGGER_ENABLED:-false}
CORS_ALLOWED_ORIGINS: ${OVERSEAS_CORS_ALLOWED_ORIGINS:-}
CORS_ALLOW_CREDENTIALS: ${CORS_ALLOW_CREDENTIALS:-false}
AUTH_SESSION_VALIDATION_URL: ${AUTH_SESSION_VALIDATION_URL}
PDA_SESSION_VALIDATION_TIMEOUT: ${PDA_SESSION_VALIDATION_TIMEOUT:-2s}
LASTLEG_SECURITY_SERVICE_URL: ${LASTLEG_SECURITY_SERVICE_URL:-http://lastleg-service:9303}
LASTLEG_SECURITY_VALIDATION_TIMEOUT: ${LASTLEG_SECURITY_VALIDATION_TIMEOUT:-2s}
lastleg-service:
<<: *overseas-common
image: ${IMAGE_REPOSITORY}/lastleg-service:${IMAGE_TAG}
container_name: firstleg-${DEPLOY_ENV:-test}-lastleg${DEPLOY_SLOT_SUFFIX:-}
ports: ["${PUBLISHED_BIND_ADDRESS:-0.0.0.0}:${LASTLEG_PUBLISHED_PORT:-${LASTLEG_PORT:-9303}}:9303"]
environment:
<<: *overseas-environment
SERVER_PORT: 9303
LASTLEG_DB_URL: ${LASTLEG_DB_URL}
LASTLEG_DB_USERNAME: ${LASTLEG_DB_USERNAME}
LASTLEG_DB_PASSWORD: ${LASTLEG_DB_PASSWORD}
CN_EVENT_BRIDGE_URL: ${CN_EVENT_BRIDGE_TARGET_URL:-http://event-bridge-cn:9312}
FIRSTLEG_EVENT_BRIDGE_URL: ${CN_EVENT_BRIDGE_TARGET_URL:-http://event-bridge-cn:9312}
DOMESTIC_WAREHOUSE_URL: ${DOMESTIC_WAREHOUSE_URL}
FILE_SERVICE_URL: ${FILE_SERVICE_URL}
# Print 注册在国内 Nacos namespace;海外服务通过共享 infra-network 的 Docker DNS 直连。
PRINT_SERVICE_URL: ${PRINT_SERVICE_URL:-http://print:9207}
WAREHOUSE_OVERSEAS_SERVICE_URL: ${WAREHOUSE_OVERSEAS_SERVICE_URL:-http://warehouse-overseas-service:9305}
warehouse-overseas-service:
<<: *overseas-common
image: ${IMAGE_REPOSITORY}/warehouse-overseas-service:${IMAGE_TAG}
container_name: firstleg-${DEPLOY_ENV:-test}-warehouse-overseas${DEPLOY_SLOT_SUFFIX:-}
ports: ["${PUBLISHED_BIND_ADDRESS:-0.0.0.0}:${WAREHOUSE_OVERSEAS_PUBLISHED_PORT:-${WAREHOUSE_OVERSEAS_PORT:-9305}}:9305"]
environment:
<<: *overseas-environment
SERVER_PORT: 9305
WAREHOUSE_DB_URL: ${WAREHOUSE_OVERSEAS_DB_URL}
WAREHOUSE_DB_USERNAME: ${WAREHOUSE_OVERSEAS_DB_USERNAME}
WAREHOUSE_DB_PASSWORD: ${WAREHOUSE_OVERSEAS_DB_PASSWORD}
DOMESTIC_WAREHOUSE_URL: ${DOMESTIC_WAREHOUSE_URL}
# 与海外 BOL 共用国内 Print;不能在海外 Nacos namespace 中发现该服务。
PRINT_SERVICE_URL: ${PRINT_SERVICE_URL:-http://print:9207}
event-bridge-service:
<<: *overseas-common
image: ${IMAGE_REPOSITORY}/event-bridge-service:${IMAGE_TAG}
container_name: firstleg-${DEPLOY_ENV:-test}-event-bridge-overseas${DEPLOY_SLOT_SUFFIX:-}
ports: ["${PUBLISHED_BIND_ADDRESS:-0.0.0.0}:${EVENT_BRIDGE_OVERSEAS_PUBLISHED_PORT:-${EVENT_BRIDGE_OVERSEAS_PORT:-9313}}:9312"]
environment:
<<: *overseas-environment
SERVER_PORT: 9312
EVENT_BRIDGE_DB_URL: ${EVENT_BRIDGE_OVERSEAS_DB_URL}
EVENT_BRIDGE_DB_USERNAME: ${EVENT_BRIDGE_OVERSEAS_DB_USERNAME}
EVENT_BRIDGE_DB_PASSWORD: ${EVENT_BRIDGE_OVERSEAS_DB_PASSWORD}
EVENT_CONSUMER_URL: ${EVENT_CONSUMER_URL:-http://lastleg-service:9303/internal/lastleg/events}
LASTLEG_SECURITY_EVENT_CONSUMER_URL: ${LASTLEG_SECURITY_EVENT_CONSUMER_URL:-http://lastleg-service:9303/internal/lastleg/security/v1/events}
networks:
firstleg-overseas-network:
infra-network:
aliases: [event-bridge-overseas]
networks:
firstleg-overseas-network:
name: firstleg-${DEPLOY_ENV:-test}-overseas-network
infra-network:
external: true
name: ${INFRA_NETWORK:-firstleg-test-infra-network}
+1 -1
View File
@@ -1,4 +1,4 @@
name: firstleg-${DEPLOY_ENV:-test}-rocketmq
name: firstleg-test-rocketmq
x-bounded-logging: &bounded-logging
driver: ${DOCKER_LOG_DRIVER:-json-file}
+104 -64
View File
@@ -1,4 +1,4 @@
name: firstleg-${DEPLOY_ENV:-test}
name: firstleg-test
x-common: &common
restart: "on-failure:3"
@@ -8,31 +8,36 @@ x-common: &common
extra_hosts:
- "host.docker.internal:host-gateway"
environment: &common-environment
DEPLOY_ENV: ${DEPLOY_ENV:-test}
BAIYUE_DEPLOY_SLOT: ${DEPLOY_SLOT:-rolling}
DEPLOY_ENV: test
BAIYUE_RELEASE_ID: ${RELEASE_ID:-local}
BAIYUE_RUNTIME_SCHEDULING_ENABLED: ${BAIYUE_RUNTIME_SCHEDULING_ENABLED:-true}
SERVER_SHUTDOWN: graceful
SPRING_LIFECYCLE_TIMEOUT_PER_SHUTDOWN_PHASE: ${SHUTDOWN_PHASE_TIMEOUT:-30s}
SPRING_PROFILES_ACTIVE: ${SPRING_PROFILES_ACTIVE:-cn}
SPRING_PROFILES_ACTIVE: test
NACOS_SERVER_ADDR: ${NACOS_SERVER_ADDR}
NACOS_USERNAME: ${NACOS_USERNAME}
NACOS_PASSWORD: ${NACOS_PASSWORD}
NACOS_GROUP: ${NACOS_GROUP:-BAIYUE}
NACOS_NAMESPACE: ${NACOS_NAMESPACE:-public}
NACOS_NAMESPACE: ${NACOS_NAMESPACE:?actual TEST namespace ID required}
REDIS_HOST: ${REDIS_HOST}
REDIS_PORT: ${REDIS_PORT:-6379}
REDIS_PASSWORD: ${REDIS_PASSWORD}
REDIS_USERNAME: ${REDIS_USERNAME:-default}
SPRING_DATA_REDIS_USERNAME: ${REDIS_USERNAME:-default}
REDIS_DATABASE: ${REDIS_DATABASE:-0}
JWT_SECRET: ${JWT_SECRET}
AUTH_SESSION_VALIDATION_URL: http://auth:9100/internal/auth/accounts/sessions/validate
JWT_ISSUER: ${JWT_ISSUER:-auth-service}
JWT_AUDIENCE: ${JWT_AUDIENCE:-lastleg-api}
JWT_JWKS_URI: http://auth:9100/api/auth/.well-known/jwks.json
REDIS_TOKEN_KEY_PREFIX: ${REDIS_TOKEN_KEY_PREFIX:-baiyue:auth}
SPRING_SQL_INIT_MODE: never
ROCKETMQ_NAME_SERVER: ${ROCKETMQ_NAME_SERVER}
MQ_TOPIC_PREFIX: test-
INTERNAL_SERVICE_TOKEN: ${INTERNAL_SERVICE_TOKEN}
BAIYUE_AUDIT_ENDPOINT: ${BAIYUE_AUDIT_ENDPOINT:-http://audit-service:9700/internal/audit/events}
BAIYUE_AUDIT_ENDPOINT: http://audit-service:9700/internal/audit/events
SERVER_MAX_HTTP_REQUEST_HEADER_SIZE: ${SERVER_MAX_HTTP_REQUEST_HEADER_SIZE:-32KB}
AUDIT_OUTBOX_ENABLED: ${AUDIT_OUTBOX_ENABLED:-false}
AUDIT_OUTBOX_PUBLISHER_ENABLED: ${AUDIT_OUTBOX_PUBLISHER_ENABLED:-false}
REGION_CODE: ${REGION_CODE:-CN}
DEPLOYMENT_CELL: ${DEPLOYMENT_CELL:-test-cn}
DATA_RESIDENCY: ${DATA_RESIDENCY:-CN}
TZ: Asia/Shanghai
logging:
# 应用只写 stdout/stderr;json-file 兼容宝塔查看,大小与数量均受控。
@@ -45,8 +50,8 @@ services:
gateway:
<<: *common
image: ${IMAGE_REPOSITORY}/gateway:${IMAGE_TAG}
container_name: firstleg-${DEPLOY_ENV:-test}-gateway${DEPLOY_SLOT_SUFFIX:-}
ports: ["${PUBLISHED_BIND_ADDRESS:-0.0.0.0}:${GATEWAY_PUBLISHED_PORT:-${GATEWAY_PORT:-9000}}:9000"]
container_name: firstleg-test-gateway
ports: ["${PUBLISHED_BIND_ADDRESS:-127.0.0.1}:${GATEWAY_PUBLISHED_PORT:-${GATEWAY_PORT:-9000}}:9000"]
environment:
<<: *common-environment
SERVER_PORT: 9000
@@ -57,46 +62,53 @@ services:
auth:
<<: *common
image: ${IMAGE_REPOSITORY}/auth:${IMAGE_TAG}
container_name: firstleg-${DEPLOY_ENV:-test}-auth${DEPLOY_SLOT_SUFFIX:-}
ports: ["${PUBLISHED_BIND_ADDRESS:-0.0.0.0}:${AUTH_PUBLISHED_PORT:-${AUTH_PORT:-9100}}:9100"]
container_name: firstleg-test-auth
expose: ["9100"]
environment:
<<: *common-environment
SERVER_PORT: 9100
JWT_RSA_JWK_SET: ${JWT_RSA_JWK_SET}
JWT_CURRENT_KID: ${JWT_CURRENT_KID}
AUTH_DB_URL: ${AUTH_DB_URL}
AUTH_DB_USERNAME: ${AUTH_DB_USERNAME}
AUTH_DB_PASSWORD: ${AUTH_DB_PASSWORD}
OVERSEAS_JWT_ENABLED: ${OVERSEAS_JWT_ENABLED:-false}
OVERSEAS_JWT_ISSUER: ${OVERSEAS_JWT_ISSUER:-auth-service}
OVERSEAS_JWT_AUDIENCE: ${OVERSEAS_JWT_AUDIENCE:-lastleg-api}
OVERSEAS_JWT_RSA_JWK_SET: ${OVERSEAS_JWT_RSA_JWK_SET:-}
OVERSEAS_JWT_CURRENT_KID: ${OVERSEAS_JWT_CURRENT_KID:-}
OVERSEAS_ACCESS_TOKEN_TTL: ${OVERSEAS_ACCESS_TOKEN_TTL:-15m}
iam:
<<: *common
image: ${IMAGE_REPOSITORY}/iam:${IMAGE_TAG}
container_name: firstleg-${DEPLOY_ENV:-test}-iam${DEPLOY_SLOT_SUFFIX:-}
ports: ["${PUBLISHED_BIND_ADDRESS:-0.0.0.0}:${IAM_PUBLISHED_PORT:-${IAM_PORT:-9201}}:9201"]
container_name: firstleg-test-iam
expose: ["9201"]
environment:
<<: *common-environment
SERVER_PORT: 9201
NOTIFICATION_SCHEDULER_ENABLED: ${NOTIFICATION_SCHEDULER_ENABLED:-false}
NOTIFICATION_MQ_CONSUMER_ENABLED: ${NOTIFICATION_MQ_CONSUMER_ENABLED:-false}
IAM_DB_URL: ${IAM_DB_URL}
IAM_DB_USERNAME: ${IAM_DB_USERNAME}
IAM_DB_PASSWORD: ${IAM_DB_PASSWORD}
SECURITY_EVENT_BRIDGE_URL: ${SECURITY_EVENT_BRIDGE_URL:-http://event-bridge-overseas:9312}
firstleg:
<<: *common
image: ${IMAGE_REPOSITORY}/firstleg:${IMAGE_TAG}
container_name: firstleg-${DEPLOY_ENV:-test}-business${DEPLOY_SLOT_SUFFIX:-}
ports: ["${PUBLISHED_BIND_ADDRESS:-0.0.0.0}:${FIRSTLEG_PUBLISHED_PORT:-${FIRSTLEG_PORT:-9202}}:9202"]
container_name: firstleg-test-business
expose: ["9202"]
environment:
<<: *common-environment
SERVER_PORT: 9202
EVENT_CONNECT_TIMEOUT_MS: ${EVENT_CONNECT_TIMEOUT_MS:-3000}
EVENT_READ_TIMEOUT_MS: ${EVENT_READ_TIMEOUT_MS:-15000}
EVENT_LEASE_SECONDS: ${EVENT_LEASE_SECONDS:-60}
EVENT_MAX_ATTEMPTS: ${EVENT_MAX_ATTEMPTS:-12}
EVENT_BASE_DELAY_SECONDS: ${EVENT_BASE_DELAY_SECONDS:-5}
EVENT_MAX_DELAY_SECONDS: ${EVENT_MAX_DELAY_SECONDS:-1800}
EVENT_JITTER: ${EVENT_JITTER:-0.1}
EVENT_OPERATIONS_TOKEN: ${EVENT_OPERATIONS_TOKEN:-}
LASTLEG_SERVICE_URL: http://lastleg-service:9303
FIRSTLEG_SERVICE_URL: http://firstleg:9202
FINANCE_SERVICE_URL: http://finance:9203
FIRSTLEG_DB_URL: ${FIRSTLEG_DB_URL}
FIRSTLEG_DB_USERNAME: ${FIRSTLEG_DB_USERNAME}
FIRSTLEG_DB_PASSWORD: ${FIRSTLEG_DB_PASSWORD}
LASTLEG_EVENT_BRIDGE_URL: ${LASTLEG_EVENT_BRIDGE_URL}
MATSON_TRACKING_ENABLED: ${MATSON_TRACKING_ENABLED:-false}
SML_TRACKING_ENABLED: ${SML_TRACKING_ENABLED:-false}
ZIM_TRACKING_ENABLED: ${ZIM_TRACKING_ENABLED:-false}
@@ -121,21 +133,31 @@ services:
finance:
<<: *common
image: ${IMAGE_REPOSITORY}/finance:${IMAGE_TAG}
container_name: firstleg-${DEPLOY_ENV:-test}-finance${DEPLOY_SLOT_SUFFIX:-}
ports: ["${PUBLISHED_BIND_ADDRESS:-0.0.0.0}:${FINANCE_PUBLISHED_PORT:-${FINANCE_PORT:-9203}}:9203"]
container_name: firstleg-test-finance
expose: ["9203"]
environment:
<<: *common-environment
SERVER_PORT: 9203
EVENT_CONNECT_TIMEOUT_MS: ${EVENT_CONNECT_TIMEOUT_MS:-3000}
EVENT_READ_TIMEOUT_MS: ${EVENT_READ_TIMEOUT_MS:-15000}
EVENT_LEASE_SECONDS: ${EVENT_LEASE_SECONDS:-60}
EVENT_MAX_ATTEMPTS: ${EVENT_MAX_ATTEMPTS:-12}
EVENT_BASE_DELAY_SECONDS: ${EVENT_BASE_DELAY_SECONDS:-5}
EVENT_MAX_DELAY_SECONDS: ${EVENT_MAX_DELAY_SECONDS:-1800}
EVENT_JITTER: ${EVENT_JITTER:-0.1}
EVENT_OPERATIONS_TOKEN: ${EVENT_OPERATIONS_TOKEN:-}
LASTLEG_SERVICE_URL: http://lastleg-service:9303
FIRSTLEG_SERVICE_URL: http://firstleg:9202
FINANCE_SERVICE_URL: http://finance:9203
FINANCE_DB_URL: ${FINANCE_DB_URL}
FINANCE_DB_USERNAME: ${FINANCE_DB_USERNAME}
FINANCE_DB_PASSWORD: ${FINANCE_DB_PASSWORD}
OVERSEAS_EVENT_BRIDGE_URL: ${OVERSEAS_EVENT_BRIDGE_URL:-http://event-bridge-overseas:9312}
order:
<<: *common
image: ${IMAGE_REPOSITORY}/order:${IMAGE_TAG}
container_name: firstleg-${DEPLOY_ENV:-test}-order${DEPLOY_SLOT_SUFFIX:-}
ports: ["${PUBLISHED_BIND_ADDRESS:-0.0.0.0}:${ORDER_PUBLISHED_PORT:-${ORDER_PORT:-9204}}:9204"]
container_name: firstleg-test-order
expose: ["9204"]
environment:
<<: *common-environment
SERVER_PORT: 9204
@@ -146,8 +168,8 @@ services:
warehouse:
<<: *common
image: ${IMAGE_REPOSITORY}/warehouse:${IMAGE_TAG}
container_name: firstleg-${DEPLOY_ENV:-test}-warehouse${DEPLOY_SLOT_SUFFIX:-}
ports: ["${PUBLISHED_BIND_ADDRESS:-0.0.0.0}:${WAREHOUSE_PUBLISHED_PORT:-${WAREHOUSE_PORT:-9205}}:9205"]
container_name: firstleg-test-warehouse
expose: ["9205"]
environment:
<<: *common-environment
SERVER_PORT: 9205
@@ -158,8 +180,8 @@ services:
file:
<<: *common
image: ${IMAGE_REPOSITORY}/file:${IMAGE_TAG}
container_name: firstleg-${DEPLOY_ENV:-test}-file${DEPLOY_SLOT_SUFFIX:-}
ports: ["${PUBLISHED_BIND_ADDRESS:-0.0.0.0}:${FILE_PUBLISHED_PORT:-${FILE_PORT:-9206}}:9206"]
container_name: firstleg-test-file
expose: ["9206"]
volumes:
- "${FILE_UPLOADS_HOST_DIR:-/data/firstleg/uploads}:/data/files"
environment:
@@ -172,8 +194,6 @@ services:
PDA_UPDATE_PUBLIC_BASE_URL: ${PDA_UPDATE_PUBLIC_BASE_URL:-}
JAVA_TOOL_OPTIONS: "-Xms1g -Xmx2g -XX:+ExitOnOutOfMemoryError -Dfile.encoding=UTF-8"
# Compose 会插值整份文件,即使只部署 auth 等其他服务;File 的必填校验由 deploy-services.sh 按服务执行。
ROCKETMQ_NAME_SERVER: ${ROCKETMQ_NAME_SERVER-}
MQ_TOPIC_PREFIX: ${MQ_TOPIC_PREFIX-}
FILE_EXPORT_CONSUMER_ENABLED: ${FILE_EXPORT_CONSUMER_ENABLED:-false}
FILE_EXPORT_CONSUMER_CONCURRENCY: ${FILE_EXPORT_CONSUMER_CONCURRENCY:-4}
FILE_EXPORT_PUBLISHER_ENABLED: ${FILE_EXPORT_PUBLISHER_ENABLED:-false}
@@ -182,8 +202,8 @@ services:
print:
<<: *common
image: ${IMAGE_REPOSITORY}/print:${IMAGE_TAG}
container_name: firstleg-${DEPLOY_ENV:-test}-print${DEPLOY_SLOT_SUFFIX:-}
ports: ["${PUBLISHED_BIND_ADDRESS:-0.0.0.0}:${PRINT_PUBLISHED_PORT:-${PRINT_PORT:-9207}}:9207"]
container_name: firstleg-test-print
expose: ["9207"]
environment:
<<: *common-environment
SERVER_PORT: 9207
@@ -192,8 +212,8 @@ services:
integration-service:
<<: *common
image: ${IMAGE_REPOSITORY}/integration-service:${IMAGE_TAG}
container_name: firstleg-${DEPLOY_ENV:-test}-integration${DEPLOY_SLOT_SUFFIX:-}
ports: ["${PUBLISHED_BIND_ADDRESS:-0.0.0.0}:${INTEGRATION_PUBLISHED_PORT:-${INTEGRATION_PORT:-9210}}:9210"]
container_name: firstleg-test-integration
expose: ["9210"]
environment:
<<: *common-environment
SERVER_PORT: 9210
@@ -238,31 +258,10 @@ services:
HEDE_TRACKING_REQUEST_TIMEOUT: ${HEDE_TRACKING_REQUEST_TIMEOUT:-30s}
HEDE_TRACKING_MIN_REQUEST_INTERVAL: ${HEDE_TRACKING_MIN_REQUEST_INTERVAL:-5s}
event-bridge-service:
<<: *common
image: ${IMAGE_REPOSITORY}/event-bridge-service:${IMAGE_TAG}
container_name: firstleg-${DEPLOY_ENV:-test}-event-bridge${DEPLOY_SLOT_SUFFIX:-}
ports: ["${PUBLISHED_BIND_ADDRESS:-0.0.0.0}:${EVENT_BRIDGE_PUBLISHED_PORT:-${EVENT_BRIDGE_PORT:-9312}}:9312"]
environment:
<<: *common-environment
SERVER_PORT: 9312
EVENT_BRIDGE_DB_URL: ${EVENT_BRIDGE_DB_URL}
EVENT_BRIDGE_DB_USERNAME: ${EVENT_BRIDGE_DB_USERNAME}
EVENT_BRIDGE_DB_PASSWORD: ${EVENT_BRIDGE_DB_PASSWORD}
EVENT_CONSUMER_URL: ${FIRSTLEG_CONTAINER_EVENT_CONSUMER_URL:-http://firstleg:9202/internal/firstleg/v1/container-events}
FIRSTLEG_CONTAINER_EVENT_CONSUMER_URL: ${FIRSTLEG_CONTAINER_EVENT_CONSUMER_URL:-http://firstleg:9202/internal/firstleg/v1/container-events}
FINANCE_FEE_EVENT_CONSUMER_URL: ${FINANCE_FEE_EVENT_CONSUMER_URL:-http://finance:9203/internal/finance/v1/business-fee-source-events}
FINANCE_WALLET_EVENT_CONSUMER_URL: ${FINANCE_WALLET_EVENT_CONSUMER_URL:-http://finance:9203/internal/finance/v1/wallet-command-events}
SPRING_SQL_INIT_MODE: never
networks:
firstleg-network:
infra-network:
aliases: [event-bridge-cn]
audit-service:
<<: *common
image: ${IMAGE_REPOSITORY}/audit-service:${IMAGE_TAG}
container_name: firstleg-${DEPLOY_ENV:-test}-audit${DEPLOY_SLOT_SUFFIX:-}
container_name: firstleg-test-audit
expose: ["9700"]
environment:
<<: *common-environment
@@ -274,9 +273,50 @@ services:
AUDIT_DLQ_CONSUMER_ENABLED: ${AUDIT_DLQ_CONSUMER_ENABLED:-false}
AUDIT_DLQ_COMPENSATION_ENABLED: ${AUDIT_DLQ_COMPENSATION_ENABLED:-false}
lastleg-service:
<<: *common
image: ${IMAGE_REPOSITORY}/lastleg-service:${IMAGE_TAG}
container_name: firstleg-test-lastleg
expose: ["9303"]
environment:
<<: *common-environment
SERVER_PORT: 9303
EVENT_CONNECT_TIMEOUT_MS: ${EVENT_CONNECT_TIMEOUT_MS:-3000}
EVENT_READ_TIMEOUT_MS: ${EVENT_READ_TIMEOUT_MS:-15000}
EVENT_LEASE_SECONDS: ${EVENT_LEASE_SECONDS:-60}
EVENT_MAX_ATTEMPTS: ${EVENT_MAX_ATTEMPTS:-12}
EVENT_BASE_DELAY_SECONDS: ${EVENT_BASE_DELAY_SECONDS:-5}
EVENT_MAX_DELAY_SECONDS: ${EVENT_MAX_DELAY_SECONDS:-1800}
EVENT_JITTER: ${EVENT_JITTER:-0.1}
EVENT_OPERATIONS_TOKEN: ${EVENT_OPERATIONS_TOKEN:-}
LASTLEG_SERVICE_URL: http://lastleg-service:9303
FIRSTLEG_SERVICE_URL: http://firstleg:9202
FINANCE_SERVICE_URL: http://finance:9203
LASTLEG_DB_URL: ${LASTLEG_DB_URL}
LASTLEG_DB_USERNAME: ${LASTLEG_DB_USERNAME}
LASTLEG_DB_PASSWORD: ${LASTLEG_DB_PASSWORD}
DOMESTIC_WAREHOUSE_URL: ${DOMESTIC_WAREHOUSE_URL:-http://warehouse:9205}
FILE_SERVICE_URL: ${FILE_SERVICE_URL:-http://file:9206}
PRINT_SERVICE_URL: ${PRINT_SERVICE_URL:-http://print:9207}
WAREHOUSE_OVERSEAS_SERVICE_URL: ${WAREHOUSE_OVERSEAS_SERVICE_URL:-http://warehouse-overseas-service:9305}
warehouse-overseas-service:
<<: *common
image: ${IMAGE_REPOSITORY}/warehouse-overseas-service:${IMAGE_TAG}
container_name: firstleg-test-warehouse-overseas
expose: ["9305"]
environment:
<<: *common-environment
SERVER_PORT: 9305
WAREHOUSE_DB_URL: ${WAREHOUSE_OVERSEAS_DB_URL}
WAREHOUSE_DB_USERNAME: ${WAREHOUSE_OVERSEAS_DB_USERNAME}
WAREHOUSE_DB_PASSWORD: ${WAREHOUSE_OVERSEAS_DB_PASSWORD}
DOMESTIC_WAREHOUSE_URL: ${DOMESTIC_WAREHOUSE_URL}
PRINT_SERVICE_URL: ${PRINT_SERVICE_URL:-http://print:9207}
networks:
firstleg-network:
name: firstleg-${DEPLOY_ENV:-test}-network
name: firstleg-test-network
infra-network:
external: true
name: ${INFRA_NETWORK:-firstleg-test-infra-network}
+1 -1
View File
@@ -1,4 +1,4 @@
ARG BASE_IMAGE=172.31.198.7:10082/java-project/eclipse-temurin:17-jre-alpine
ARG BASE_IMAGE=eclipse-temurin:17-jre-alpine
FROM ${BASE_IMAGE} AS extractor
WORKDIR /workspace
+1 -1
View File
@@ -7,7 +7,7 @@ MYSQL_PASSWORD=CHANGE_ME_TEST_MYSQL_PASSWORD
REDIS_PASSWORD=CHANGE_ME_TEST_REDIS_PASSWORD
# RocketMQ 与 File Service 位于同一 Docker 网络,Broker 向客户端公布服务名。
ROCKETMQ_BROKER_IP=rocketmq-broker
# 测试环境必须显式使用 test-;生产环境在对应凭据文件中显式设置为空字符串。
# 测试业务 MQ topic/group 使用 test- 前缀。
MQ_TOPIC_PREFIX=test-
AUDIT_OUTBOX_ENABLED=false
AUDIT_OUTBOX_PUBLISHER_ENABLED=false
+54
View File
@@ -0,0 +1,54 @@
# TEST only. Copy outside the repository and supply secrets securely.
NACOS_SERVER_ADDR=host.docker.internal:8848
NACOS_NAMESPACE=
NACOS_USERNAME=
NACOS_PASSWORD=
REDIS_HOST=host.docker.internal
REDIS_PORT=6379
REDIS_USERNAME=default
REDIS_DATABASE=0
REDIS_TOKEN_KEY_PREFIX=baiyue:auth
ROCKETMQ_NAME_SERVER=rocketmq-namesrv:9876
MQ_TOPIC_PREFIX=test-
JWT_ISSUER=auth-service
JWT_AUDIENCE=lastleg-api
JWT_RSA_JWK_SET=
JWT_CURRENT_KID=
EVENT_OPERATIONS_TOKEN=
INFRA_NETWORK=firstleg-test-infra-network
IMAGE_REPOSITORY=registry.example.com/team/baiyue-firstleg
IMAGE_TAG=
FILE_UPLOADS_HOST_DIR=/data/firstleg/uploads
CORS_ALLOWED_ORIGINS=
INTERNAL_SERVICE_TOKEN=
REDIS_PASSWORD=
AUTH_DB_URL=jdbc:mysql://host.docker.internal:3306/baiyue_auth?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
AUTH_DB_USERNAME=
AUTH_DB_PASSWORD=
IAM_DB_URL=jdbc:mysql://host.docker.internal:3306/baiyue_iam?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
IAM_DB_USERNAME=
IAM_DB_PASSWORD=
FIRSTLEG_DB_URL=jdbc:mysql://host.docker.internal:3306/baiyue_firstleg?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
FIRSTLEG_DB_USERNAME=
FIRSTLEG_DB_PASSWORD=
FINANCE_DB_URL=jdbc:mysql://host.docker.internal:3306/baiyue_finance?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
FINANCE_DB_USERNAME=
FINANCE_DB_PASSWORD=
ORDER_DB_URL=jdbc:mysql://host.docker.internal:3306/baiyue_order?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
ORDER_DB_USERNAME=
ORDER_DB_PASSWORD=
WAREHOUSE_DB_URL=jdbc:mysql://host.docker.internal:3306/baiyue_warehouse?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
WAREHOUSE_DB_USERNAME=
WAREHOUSE_DB_PASSWORD=
FILE_DB_URL=jdbc:mysql://host.docker.internal:3306/baiyue_file?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
FILE_DB_USERNAME=
FILE_DB_PASSWORD=
AUDIT_DB_URL=jdbc:mysql://host.docker.internal:3306/baiyue_audit?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
AUDIT_DB_USERNAME=
AUDIT_DB_PASSWORD=
LASTLEG_DB_URL=jdbc:mysql://host.docker.internal:3306/baiyue_lastleg?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
LASTLEG_DB_USERNAME=
LASTLEG_DB_PASSWORD=
WAREHOUSE_OVERSEAS_DB_URL=jdbc:mysql://host.docker.internal:3306/baiyue_lastlag_warehouse?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
WAREHOUSE_OVERSEAS_DB_USERNAME=
WAREHOUSE_OVERSEAS_DB_PASSWORD=
-411
View File
@@ -1,411 +0,0 @@
#!/usr/bin/env sh
set -eu
if [ "$#" -ne 7 ]; then
echo "用法: $0 <环境> <镜像仓库> <镜像标签> <服务> <环境文件> <Compose文件> <项目名>" >&2
exit 2
fi
DEPLOY_ENV="$1"
IMAGE_REPOSITORY="$2"
IMAGE_TAG="$3"
SERVICE="$4"
ENV_FILE="$5"
COMPOSE_FILE="$6"
BASE_PROJECT="$7"
case "$DEPLOY_ENV" in
test|production) ;;
*) echo "蓝绿执行器仅用于 test/production,当前: $DEPLOY_ENV" >&2; exit 2 ;;
esac
for file in "$ENV_FILE" "$COMPOSE_FILE"; do
[ -f "$file" ] || { echo "必需文件不存在: $file" >&2; exit 2; }
done
case "$SERVICE" in
*[!A-Za-z0-9_-]*|'') echo "非法服务名: $SERVICE" >&2; exit 2 ;;
esac
env_value() {
sed -n "s/^$1=//p" "$ENV_FILE" | tail -n 1 | tr -d '\r'
}
service_contract() {
case "$SERVICE:$COMPOSE_FILE" in
gateway:*) echo "gateway|9000|9000|gateway|gateway-active" ;;
auth:*) echo "auth|9100|9100|auth-service|auth" ;;
iam:*) echo "iam|9201|9201|iam-service|iam" ;;
firstleg:*) echo "business|9202|9202|firstleg-service|firstleg" ;;
finance:*) echo "finance|9203|9203|finance-service|finance" ;;
order:*) echo "order|9204|9204|order-service|order" ;;
warehouse:*) echo "warehouse|9205|9205|warehouse-service|warehouse" ;;
file:*) echo "file|9206|9206|file-service|file" ;;
print:*) echo "print|9207|9207|print-service|print" ;;
integration-service:*) echo "integration|9210|9210|integration-service|integration-service" ;;
audit-service:*) echo "audit|9700|9700|audit-service|audit-service" ;;
gateway-overseas:*) echo "gateway-overseas|9300|9300|gateway-overseas|gateway-overseas-active" ;;
lastleg-service:*) echo "lastleg|9303|9303|lastleg-service|lastleg-service" ;;
warehouse-overseas-service:*) echo "warehouse-overseas|9305|9305|warehouse-overseas-service|warehouse-overseas-service" ;;
event-bridge-service:*overseas*) echo "event-bridge-overseas|9312|9313|event-bridge-overseas|event-bridge-overseas" ;;
event-bridge-service:*) echo "event-bridge|9312|9312|event-bridge-cn|event-bridge-cn" ;;
*) echo "不支持蓝绿部署的服务: $SERVICE" >&2; exit 2 ;;
esac
}
IFS='|' read -r CONTAINER_STEM INTERNAL_PORT BASE_PORT NACOS_SERVICE ROUTING_ALIAS <<EOF
$(service_contract)
EOF
STATE_ROOT="${BLUE_GREEN_STATE_ROOT:-/data/firstleg/deploy/blue-green-state}"
STATE_KEY="${BASE_PROJECT}-${SERVICE}"
STATE_FILE="$STATE_ROOT/$STATE_KEY.active"
ROUTING_NETWORK="${BLUE_GREEN_ROUTING_NETWORK:-firstleg-${DEPLOY_ENV}-routing}"
HEALTH_TIMEOUT="${DEPLOY_HEALTH_TIMEOUT_SECONDS:-240}"
HEALTH_POLL="${DEPLOY_HEALTH_POLL_SECONDS:-5}"
STABILIZATION_SECONDS="${BLUE_GREEN_STABILIZATION_SECONDS:-10}"
DRAIN_SECONDS="${BLUE_GREEN_DRAIN_SECONDS:-30}"
STOP_TIMEOUT="${BLUE_GREEN_STOP_TIMEOUT_SECONDS:-45}"
IMAGE="$IMAGE_REPOSITORY/$SERVICE:$IMAGE_TAG"
for value in "$HEALTH_TIMEOUT" "$HEALTH_POLL" "$STABILIZATION_SECONDS" "$DRAIN_SECONDS" "$STOP_TIMEOUT"; do
case "$value" in ''|*[!0-9]*) echo "蓝绿时间参数必须为非负整数: $value" >&2; exit 2 ;; esac
done
mkdir -p "$STATE_ROOT"
chmod 750 "$STATE_ROOT"
docker network inspect "$ROUTING_NETWORK" >/dev/null 2>&1 || docker network create "$ROUTING_NETWORK" >/dev/null
legacy_container="firstleg-${DEPLOY_ENV}-${CONTAINER_STEM}"
active_slot=""
active_container=""
active_port=""
active_worker=""
if [ -s "$STATE_FILE" ]; then
IFS='|' read -r active_slot active_container active_port active_image active_worker < "$STATE_FILE"
elif docker inspect "$legacy_container" >/dev/null 2>&1; then
active_slot="legacy"
active_container="$legacy_container"
active_port="$BASE_PORT"
fi
if [ "$active_slot" = "blue" ]; then
target_slot="green"
else
target_slot="blue"
[ "$active_slot" = "legacy" ] && target_slot="green"
fi
if [ "$target_slot" = "blue" ]; then
target_port=$((BASE_PORT + 20000))
else
target_port=$((BASE_PORT + 10000))
fi
candidate="firstleg-${DEPLOY_ENV}-${CONTAINER_STEM}-${target_slot}"
worker_candidate="${candidate}-worker"
if docker inspect "$candidate" >/dev/null 2>&1; then
if [ "$candidate" = "$active_container" ]; then
echo "活动槽状态异常,候选容器与活动容器相同: $candidate" >&2
exit 1
fi
docker rm -f "$candidate" >/dev/null
fi
compose() {
docker compose --project-name "$BASE_PROJECT" --env-file "$ENV_FILE" -f "$COMPOSE_FILE" "$@"
}
container_health() {
container="$1"
docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' \
"$container" 2>/dev/null || echo missing
}
cleanup_candidate() {
docker rm -f "$candidate" >/dev/null 2>&1 || true
}
wait_for_container() {
container="$1"
label="$2"
elapsed=0
while [ "$elapsed" -lt "$HEALTH_TIMEOUT" ]; do
state="$(container_health "$container")"
echo "$label 健康状态: $state(已等待 ${elapsed}s)"
case "$state" in
healthy) return 0 ;;
unhealthy|exited|dead|missing) return 1 ;;
esac
sleep "$HEALTH_POLL"
elapsed=$((elapsed + HEALTH_POLL))
done
return 1
}
needs_worker() {
case "$SERVICE" in
iam|firstleg|finance|order|event-bridge-service|lastleg-service) return 0 ;;
file)
[ "$(env_value FILE_EXPORT_CONSUMER_ENABLED)" = true ] \
|| [ "$(env_value FILE_EXPORT_PUBLISHER_ENABLED)" = true ] \
|| [ "$(env_value FILE_EXPORT_RECONCILER_ENABLED)" = true ] \
|| [ "$(env_value AUDIT_OUTBOX_PUBLISHER_ENABLED)" = true ]
;;
audit-service)
[ "$(env_value AUDIT_MQ_CONSUMER_ENABLED)" = true ] \
|| [ "$(env_value AUDIT_DLQ_CONSUMER_ENABLED)" = true ] \
|| [ "$(env_value AUDIT_DLQ_COMPENSATION_ENABLED)" = true ]
;;
auth|warehouse|warehouse-overseas-service)
[ "$(env_value AUDIT_OUTBOX_PUBLISHER_ENABLED)" = true ]
;;
*) return 1 ;;
esac
}
start_worker_candidate() {
docker rm -f "$worker_candidate" >/dev/null 2>&1 || true
echo "启动单例 Worker: $worker_candidate"
if [ "$SERVICE" = file ]; then
worker_java_options="-Xms512m -Xmx2g -XX:+ExitOnOutOfMemoryError -Dfile.encoding=UTF-8"
else
worker_java_options="-Xms256m -Xmx1g -XX:+ExitOnOutOfMemoryError -Dfile.encoding=UTF-8"
fi
if ! compose run -d --no-deps --name "$worker_candidate" \
-e "NACOS_REGISTER_ENABLED=false" \
-e "BAIYUE_RUNTIME_SCHEDULING_ENABLED=true" \
-e "BAIYUE_DEPLOY_SLOT=${target_slot}-worker" \
-e "JAVA_TOOL_OPTIONS=$worker_java_options" \
-e "BAIYUE_RELEASE_ID=$IMAGE_TAG" "$SERVICE" >/dev/null; then
return 1
fi
docker network connect "$ROUTING_NETWORK" "$worker_candidate"
if ! wait_for_container "$worker_candidate" "候选 Worker"; then
docker logs --tail 200 "$worker_candidate" 2>&1 || true
docker rm -f "$worker_candidate" >/dev/null 2>&1 || true
return 1
fi
worker_image="$(docker inspect --format '{{.Config.Image}}' "$worker_candidate")"
[ "$worker_image" = "$IMAGE" ] || return 1
}
connect_routing_aliases() {
container="$1"
shift
docker network inspect "$ROUTING_NETWORK" --format '{{json .Containers}}' | grep -q "\"$container\"" \
&& return 0
set -- docker network connect $(for alias_name in "$@"; do printf '%s\n' --alias "$alias_name"; done)
"$@" "$ROUTING_NETWORK" "$container"
}
bootstrap_legacy_routes() {
for pair in \
gateway-active,gateway:gateway auth,auth-service:auth iam,iam-service:iam \
firstleg,firstleg-service:business finance,finance-service:finance order,order-service:order \
warehouse,warehouse-service:warehouse file,file-service:file print,print-service:print \
integration-service:integration audit-service:audit \
gateway-overseas-active,gateway-overseas:gateway-overseas \
lastleg-service:lastleg warehouse-overseas-service:warehouse-overseas \
event-bridge-cn:event-bridge event-bridge-overseas:event-bridge-overseas
do
aliases="${pair%%:*}"
stem="${pair#*:}"
container="firstleg-${DEPLOY_ENV}-${stem}"
if docker inspect "$container" >/dev/null 2>&1; then
connect_routing_aliases "$container" $(printf '%s' "$aliases" | tr ',' ' ') || true
fi
done
for container in firstleg-prod-admin-web firstleg-prod-client-web; do
if docker inspect "$container" >/dev/null 2>&1; then
connect_routing_aliases "$container" "${container##*-}" || true
fi
done
}
switch_gateway_proxy() {
[ "$SERVICE" = "gateway" ] || [ "$SERVICE" = "gateway-overseas" ] || return 0
if [ "$DEPLOY_ENV" = test ]; then
echo "测试 Gateway 仍由固定入口承载,不在后端蓝绿执行器中切换" >&2
return 1
fi
if [ "$SERVICE" = "gateway" ]; then required_alias=gateway-active; else required_alias=gateway-overseas-active; fi
for frontend_service in admin-web client-web; do
frontend_state="$STATE_ROOT/frontend-$frontend_service.active"
if [ ! -s "$frontend_state" ]; then
echo "$frontend_service 尚未完成首次蓝绿迁移,拒绝切换 Gateway" >&2
return 1
fi
IFS='|' read -r frontend_slot frontend frontend_port frontend_image < "$frontend_state"
if ! docker inspect "$frontend" >/dev/null 2>&1 \
|| ! docker exec "$frontend" nginx -T 2>&1 | grep -q "$required_alias"; then
echo "$frontend 尚未使用稳定上游 $required_alias,拒绝切换 Gateway" >&2
return 1
fi
done
route_root="${BLUE_GREEN_ROUTE_CONFIG_ROOT:-/www/server/panel/vhost/nginx}"
if [ "$SERVICE" = "gateway" ]; then
config="$route_root/00-blue-green-gateway.conf"
else
config="$route_root/00-blue-green-gateway-overseas.conf"
fi
if [ -z "$config" ] || [ ! -f "$config" ]; then
echo "$SERVICE 蓝绿切换要求环境文件配置有效的 Nginx 配置路径" >&2
return 1
fi
backup="$config.blue-green-backup"
cp -f "$config" "$backup"
if ! sed -i "s#127\.0\.0\.1:${active_port:-$BASE_PORT}#127.0.0.1:$target_port#g" "$config" \
|| cmp -s "$config" "$backup"; then
cp -f "$backup" "$config"
echo "未能在 Nginx 配置中切换 $SERVICE 上游端口" >&2
return 1
fi
if ! nginx -t || ! nginx -s reload; then
cp -f "$backup" "$config"
nginx -t && nginx -s reload || true
return 1
fi
rm -f "$backup"
}
reload_frontend_proxies() {
[ "$SERVICE" = "gateway" ] || [ "$SERVICE" = "gateway-overseas" ] || return 0
[ "$DEPLOY_ENV" = production ] || return 0
for frontend_service in admin-web client-web; do
frontend_state="$STATE_ROOT/frontend-$frontend_service.active"
[ -s "$frontend_state" ] || return 1
IFS='|' read -r frontend_slot frontend frontend_port frontend_image < "$frontend_state"
docker inspect "$frontend" >/dev/null 2>&1 || return 1
docker exec "$frontend" nginx -t
docker exec "$frontend" nginx -s reload
done
}
rollback_gateway_proxy() {
[ "$SERVICE" = "gateway" ] || [ "$SERVICE" = "gateway-overseas" ] || return 0
[ "$DEPLOY_ENV" = production ] || return 0
route_root="${BLUE_GREEN_ROUTE_CONFIG_ROOT:-/www/server/panel/vhost/nginx}"
if [ "$SERVICE" = "gateway" ]; then
config="$route_root/00-blue-green-gateway.conf"
else
config="$route_root/00-blue-green-gateway-overseas.conf"
fi
old_port="${active_port:-$BASE_PORT}"
sed -i "s#127\.0\.0\.1:$target_port#127.0.0.1:$old_port#g" "$config"
nginx -t && nginx -s reload
}
bootstrap_legacy_routes
export DEPLOY_ENV IMAGE_REPOSITORY IMAGE_TAG
echo "启动候选槽: service=$SERVICE slot=$target_slot port=$target_port image=$IMAGE"
if ! compose run -d --no-deps --name "$candidate" \
-e "BAIYUE_DEPLOY_SLOT=$target_slot" -e "BAIYUE_RELEASE_ID=$IMAGE_TAG" \
-e "BAIYUE_RUNTIME_SCHEDULING_ENABLED=false" \
-e "AUDIT_OUTBOX_PUBLISHER_ENABLED=false" \
-e "NOTIFICATION_SCHEDULER_ENABLED=false" \
-e "NOTIFICATION_MQ_CONSUMER_ENABLED=false" \
-e "FILE_EXPORT_CONSUMER_ENABLED=false" \
-e "FILE_EXPORT_PUBLISHER_ENABLED=false" \
-e "FILE_EXPORT_RECONCILER_ENABLED=false" \
-e "AUDIT_MQ_CONSUMER_ENABLED=false" \
-e "AUDIT_DLQ_CONSUMER_ENABLED=false" \
-e "AUDIT_DLQ_COMPENSATION_ENABLED=false" \
-p "${PUBLISHED_BIND_ADDRESS:-0.0.0.0}:$target_port:$INTERNAL_PORT" "$SERVICE" >/dev/null; then
cleanup_candidate
exit 1
fi
if ! wait_for_container "$candidate" "候选 Web 槽 $target_slot"; then
echo "候选槽未通过健康检查,保留旧实例" >&2
docker logs --tail 200 "$candidate" 2>&1 || true
cleanup_candidate
exit 1
fi
running_image="$(docker inspect --format '{{.Config.Image}}' "$candidate")"
if [ "$running_image" != "$IMAGE" ]; then
echo "候选镜像不一致: expected=$IMAGE actual=$running_image" >&2
cleanup_candidate
exit 1
fi
if [ "$NACOS_SERVICE" = "$ROUTING_ALIAS" ]; then
connect_routing_aliases "$candidate" "$ROUTING_ALIAS"
else
connect_routing_aliases "$candidate" "$ROUTING_ALIAS" "$NACOS_SERVICE"
fi
if [ "$STABILIZATION_SECONDS" -gt 0 ]; then
echo "候选槽稳定观察 ${STABILIZATION_SECONDS}s"
sleep "$STABILIZATION_SECONDS"
[ "$(container_health "$candidate")" = "healthy" ] || { cleanup_candidate; exit 1; }
fi
if ! switch_gateway_proxy; then
cleanup_candidate
exit 1
fi
worker_required=false
if needs_worker; then worker_required=true; fi
if [ "$worker_required" = true ] && [ "$active_slot" != legacy ]; then
if [ -n "$active_worker" ] && docker inspect "$active_worker" >/dev/null 2>&1; then
echo "暂停旧 Worker,避免新旧任务并行: $active_worker"
docker stop --time "$STOP_TIMEOUT" "$active_worker" >/dev/null
fi
if ! start_worker_candidate; then
[ -z "$active_worker" ] || docker start "$active_worker" >/dev/null 2>&1 || true
rollback_gateway_proxy || true
cleanup_candidate
exit 1
fi
fi
old_container_stopped=false
if [ -n "$active_container" ] && docker inspect "$active_container" >/dev/null 2>&1; then
if [ "$DRAIN_SECONDS" -gt 0 ]; then
echo "蓝绿切换后连接排空 ${DRAIN_SECONDS}s,旧 Web 实例保持运行: $active_container"
sleep "$DRAIN_SECONDS"
fi
echo "优雅停止旧 Web 实例: $active_container"
docker stop --time "$STOP_TIMEOUT" "$active_container" >/dev/null
old_container_stopped=true
fi
if [ "$worker_required" = true ] && [ "$active_slot" = legacy ]; then
if ! start_worker_candidate; then
[ "$old_container_stopped" = false ] || docker start "$active_container" >/dev/null 2>&1 || true
rollback_gateway_proxy || true
cleanup_candidate
exit 1
fi
fi
if ! reload_frontend_proxies; then
[ "$old_container_stopped" = false ] || docker start "$active_container" >/dev/null 2>&1 || true
if [ "$worker_required" = true ]; then
docker rm -f "$worker_candidate" >/dev/null 2>&1 || true
[ -z "$active_worker" ] || docker start "$active_worker" >/dev/null 2>&1 || true
fi
rollback_gateway_proxy || true
cleanup_candidate
reload_frontend_proxies || true
exit 1
fi
[ -z "$active_worker" ] || [ "$active_worker" = "$worker_candidate" ] \
|| docker rm "$active_worker" >/dev/null 2>&1 || true
[ "$old_container_stopped" = false ] || docker rm "$active_container" >/dev/null
if [ "$worker_required" = true ]; then
next_worker="$worker_candidate"
else
next_worker=""
fi
printf '%s|%s|%s|%s|%s\n' "$target_slot" "$candidate" "$target_port" "$IMAGE" "$next_worker" > "$STATE_FILE.tmp"
chmod 640 "$STATE_FILE.tmp"
mv -f "$STATE_FILE.tmp" "$STATE_FILE"
echo "蓝绿切换完成: service=$SERVICE active=$target_slot container=$candidate port=$target_port"
+38 -398
View File
@@ -28,177 +28,45 @@ for value_name in HEALTH_TIMEOUT_SECONDS HEALTH_POLL_SECONDS; do
esac
done
case "$DEPLOY_ENV" in
development|test|production) ;;
*)
echo "仅支持 development/test/production,当前: $DEPLOY_ENV" >&2
exit 2
;;
esac
if [ "$DEPLOY_ENV" = "development" ]; then
PUBLISHED_BIND_ADDRESS=127.0.0.1
else
PUBLISHED_BIND_ADDRESS="${PUBLISHED_BIND_ADDRESS:-0.0.0.0}"
fi
export PUBLISHED_BIND_ADDRESS
[ "$DEPLOY_ENV" = "test" ] || { echo "Only test is supported" >&2; exit 2; }
case "$SERVICE" in
*[!A-Za-z0-9_-]*|'')
echo "非法服务名: $SERVICE" >&2
exit 2
;;
gateway|auth|iam|firstleg|finance|order|warehouse|file|print|integration-service|lastleg-service|warehouse-overseas-service|audit-service) ;;
*) echo "Unknown or retired service: $SERVICE" >&2; exit 2 ;;
esac
if [ ! -f "$ENV_FILE" ]; then
echo "必需文件不存在: $ENV_FILE" >&2
exit 2
fi
env_file_has_key() {
grep -q "^$1=" "$ENV_FILE"
}
env_file_value() {
sed -n "s/^$1=//p" "$ENV_FILE" | tail -n 1 | tr -d '\r'
}
case "$SERVICE" in
gateway-overseas|lastleg-service|warehouse-overseas-service)
COMPOSE_FILE="deploy/docker-compose.overseas.yml"
PROJECT_NAME="firstleg-$DEPLOY_ENV-overseas"
;;
event-bridge-service)
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 "$BRIDGE_REGION" in
CN) ;;
OVERSEAS)
COMPOSE_FILE="deploy/docker-compose.overseas.yml"
PROJECT_NAME="firstleg-$DEPLOY_ENV-overseas"
;;
*)
echo "部署 event-bridge-service 时,REGION_CODE 只能为 CN 或 OVERSEAS" >&2
exit 2
;;
esac
;;
esac
if [ "$SERVICE" = "gateway-overseas" ]; then
if ! env_file_has_key AUTH_SESSION_VALIDATION_URL || [ -z "$(env_file_value AUTH_SESSION_VALIDATION_URL)" ]; then
echo "部署 gateway-overseas 时,环境文件必须包含非空 AUTH_SESSION_VALIDATION_URL" >&2
exit 2
fi
fi
for file in "$ENV_FILE" "$COMPOSE_FILE"; do
if [ ! -f "$file" ]; then
echo "必需文件不存在: $file" >&2
exit 2
fi
[ "${TARGET_IS_LOCAL:-false}" = "true" ] || { echo "TEST deploy must run on the target host" >&2; exit 2; }
[ -f "$ENV_FILE" ] || { echo "Environment file missing" >&2; exit 2; }
env_file_value() { sed -n "s/^$1=//p" "$ENV_FILE" | tail -n 1 | tr -d '\r'; }
for key in NACOS_SERVER_ADDR NACOS_NAMESPACE NACOS_USERNAME NACOS_PASSWORD REDIS_HOST REDIS_PORT REDIS_DATABASE REDIS_TOKEN_KEY_PREFIX INTERNAL_SERVICE_TOKEN JWT_RSA_JWK_SET JWT_CURRENT_KID ROCKETMQ_NAME_SERVER; do
[ -n "$(env_file_value "$key")" ] || { echo "Missing $key" >&2; exit 2; }
done
if [ "$SERVICE" = "finance" ]; then
for key in FINANCE_DB_URL FINANCE_DB_USERNAME FINANCE_DB_PASSWORD; do
if ! env_file_has_key "$key" || [ -z "$(env_file_value "$key")" ]; then
echo "部署 finance 服务时,环境文件必须包含非空 $key" >&2
exit 2
fi
[ "$(env_file_value MQ_TOPIC_PREFIX)" = "test-" ] || { echo "MQ_TOPIC_PREFIX must be test-" >&2; exit 2; }
case "$SERVICE" in
gateway|print|integration-service) db_prefix='' ;;
lastleg-service) db_prefix=LASTLEG ;;
warehouse-overseas-service) db_prefix=WAREHOUSE_OVERSEAS ;;
audit-service) db_prefix=AUDIT ;;
*) db_prefix=$(printf '%s' "$SERVICE" | tr '[:lower:]' '[:upper:]') ;;
esac
if [ -n "$db_prefix" ]; then
for suffix in URL USERNAME PASSWORD; do
key="${db_prefix}_DB_${suffix}"
[ -n "$(env_file_value "$key")" ] || { echo "Missing $key" >&2; exit 2; }
done
fi
if [ "$PROJECT_NAME" = "firstleg-$DEPLOY_ENV-overseas" ]; then
for key in NACOS_SERVER_ADDR OVERSEAS_NACOS_USERNAME OVERSEAS_NACOS_PASSWORD OVERSEAS_NACOS_NAMESPACE INTERNAL_SERVICE_TOKEN JWT_ISSUER OVERSEAS_JWT_AUDIENCE JWT_JWK_SET_URI; do
if ! env_file_has_key "$key" || [ -z "$(env_file_value "$key")" ]; then
echo "部署海外服务时,环境文件必须包含非空 $key" >&2
exit 2
fi
done
fi
if [ "$SERVICE" = "firstleg" ]; then
if ! env_file_has_key LASTLEG_EVENT_BRIDGE_URL || [ -z "$(env_file_value LASTLEG_EVENT_BRIDGE_URL)" ]; then
echo "部署 firstleg 时,环境文件必须包含非空 LASTLEG_EVENT_BRIDGE_URL" >&2
exit 2
fi
if [ "$(env_file_value TERMINAL_DRAYAGE_MAIL_ENABLED)" = "true" ]; then
for key in TERMINAL_DRAYAGE_MAIL_HOST TERMINAL_DRAYAGE_MAIL_PORT TERMINAL_DRAYAGE_MAIL_USERNAME TERMINAL_DRAYAGE_MAIL_PASSWORD; do
if ! env_file_has_key "$key" || [ -z "$(env_file_value "$key")" ]; then
echo "启用码头车队邮件时,环境文件必须包含非空 $key" >&2
exit 2
fi
done
fi
fi
if [ "$SERVICE" = "lastleg-service" ]; then
for key in LASTLEG_DB_URL LASTLEG_DB_USERNAME LASTLEG_DB_PASSWORD FIRSTLEG_EVENT_BRIDGE_URL DOMESTIC_WAREHOUSE_URL FILE_SERVICE_URL; do
if ! env_file_has_key "$key" || [ -z "$(env_file_value "$key")" ]; then
echo "部署 lastleg-service 时,环境文件必须包含非空 $key" >&2
exit 2
fi
if [ "$SERVICE" = "firstleg" ] && [ "$(env_file_value TERMINAL_DRAYAGE_MAIL_ENABLED)" = "true" ]; then
for key in TERMINAL_DRAYAGE_MAIL_HOST TERMINAL_DRAYAGE_MAIL_PORT TERMINAL_DRAYAGE_MAIL_USERNAME TERMINAL_DRAYAGE_MAIL_PASSWORD; do
[ -n "$(env_file_value "$key")" ] || { echo "Missing $key" >&2; exit 2; }
done
fi
if [ "$SERVICE" = "warehouse-overseas-service" ]; then
for key in WAREHOUSE_OVERSEAS_DB_URL WAREHOUSE_OVERSEAS_DB_USERNAME WAREHOUSE_OVERSEAS_DB_PASSWORD; do
if ! env_file_has_key "$key" || [ -z "$(env_file_value "$key")" ]; then
echo "部署 warehouse-overseas-service 时,环境文件必须包含非空 $key" >&2
exit 2
fi
done
fi
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 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
echo "部署 event-bridge-service 时,环境文件必须包含非空 $key" >&2
exit 2
fi
done
fi
if [ "$SERVICE" = "file" ]; then
if ! env_file_has_key ROCKETMQ_NAME_SERVER || [ -z "$(env_file_value ROCKETMQ_NAME_SERVER)" ]; then
echo "部署 file 服务时,环境文件必须包含非空 ROCKETMQ_NAME_SERVER" >&2
exit 2
fi
if ! env_file_has_key MQ_TOPIC_PREFIX; then
echo "部署 file 服务时,环境文件必须显式包含 MQ_TOPIC_PREFIX" >&2
exit 2
fi
MQ_PREFIX=$(env_file_value MQ_TOPIC_PREFIX)
if [ "$DEPLOY_ENV" = "test" ] && [ "$MQ_PREFIX" != "test-" ]; then
echo "测试环境部署 file 服务时 MQ_TOPIC_PREFIX 必须为 test-" >&2
exit 2
fi
if [ "$DEPLOY_ENV" = "development" ] && { [ -z "$MQ_PREFIX" ] || [ "$MQ_PREFIX" = "test-" ]; }; then
echo "开发环境部署 file 服务时 MQ_TOPIC_PREFIX 必须非空且不能使用 test-" >&2
exit 2
fi
if [ "$DEPLOY_ENV" = "production" ] && [ -n "$MQ_PREFIX" ]; then
echo "生产环境部署 file 服务时 MQ_TOPIC_PREFIX 必须为空,使用正式 Topic" >&2
exit 2
fi
fi
# Architecture/schema changes must use whole-release recovery, never an implicit old image.
export PUBLISHED_BIND_ADDRESS=127.0.0.1
: "${REGISTRY_USER:?REGISTRY_USER 未设置}"
: "${REGISTRY_PASSWORD:?REGISTRY_PASSWORD 未设置}"
local_registry_logout() {
docker logout "$REGISTRY_HOST" >/dev/null 2>&1 || true
if [ -n "${deploy_docker_config:-}" ]; then rm -rf -- "$deploy_docker_config"; fi
}
local_compose() {
@@ -255,8 +123,8 @@ wait_for_local_health() {
return 0
;;
no-health:running)
echo "容器未配置 HEALTHCHECK,以运行状态作为成功依据" >&2
return 0
echo "Missing HEALTHCHECK; deployment failed" >&2
return 1
;;
unhealthy|no-health:exited|no-health:dead)
return 1
@@ -270,6 +138,10 @@ wait_for_local_health() {
}
deploy_local() {
deploy_docker_config=$(mktemp -d)
DOCKER_CONFIG="$deploy_docker_config"
export DOCKER_CONFIG
trap local_registry_logout EXIT HUP INT TERM
printf '%s' "$REGISTRY_PASSWORD" | docker login -u "$REGISTRY_USER" --password-stdin "$REGISTRY_HOST"
trap local_registry_logout EXIT HUP INT TERM
previous_image="$(local_container_image || true)"
@@ -306,6 +178,11 @@ deploy_local() {
else
echo "新镜像容器不存在,无法读取启动日志" >&2
fi
if [ "${ALLOW_SAME_CONTRACT_ROLLBACK:-false}" != "true" ]; then
echo "Automatic rollback disabled: keep writes closed and recover the matched release" >&2
local_compose stop "$SERVICE" >/dev/null 2>&1 || true
return 1
fi
expected_prefix="$IMAGE_REPOSITORY/$SERVICE:"
case "$previous_image" in
"$expected_prefix"*)
@@ -346,241 +223,4 @@ deploy_local() {
return 1
}
if [ "${TARGET_IS_LOCAL:-false}" = "true" ]; then
echo "镜像已在目标服务器本机,直接部署: $IMAGE"
if [ "$DEPLOY_ENV" = "production" ]; then
printf '%s' "$REGISTRY_PASSWORD" | docker login -u "$REGISTRY_USER" --password-stdin "$REGISTRY_HOST"
trap local_registry_logout EXIT HUP INT TERM
docker pull "$IMAGE"
sh deploy/scripts/deploy-blue-green-service.sh \
"$DEPLOY_ENV" "$IMAGE_REPOSITORY" "$IMAGE_TAG" "$SERVICE" "$ENV_FILE" "$COMPOSE_FILE" "$PROJECT_NAME"
local_registry_logout
trap - EXIT HUP INT TERM
exit 0
fi
deploy_local
exit 0
fi
SSH_TARGET="${TARGET_SSH_TARGET:?TARGET_SSH_TARGET 未设置}"
SSH_KEY="${TARGET_SSH_KEY:?TARGET_SSH_KEY 未设置}"
SSH_KNOWN_HOSTS="${TARGET_SSH_KNOWN_HOSTS:?TARGET_SSH_KNOWN_HOSTS 未设置}"
REMOTE_DIR="${TARGET_DEPLOY_DIR:?TARGET_DEPLOY_DIR 未设置}"
for file in "$SSH_KEY" "$SSH_KNOWN_HOSTS"; do
if [ ! -f "$file" ]; then
echo "SSH 文件不存在: $file" >&2
exit 2
fi
done
run_ssh() {
ssh -i "$SSH_KEY" \
-o BatchMode=yes \
-o Compression=no \
-o StrictHostKeyChecking=yes \
-o UserKnownHostsFile="$SSH_KNOWN_HOSTS" \
-o ConnectTimeout=10 \
"$SSH_TARGET" "$@"
}
run_scp() {
scp -i "$SSH_KEY" \
-o BatchMode=yes \
-o Compression=no \
-o StrictHostKeyChecking=yes \
-o UserKnownHostsFile="$SSH_KNOWN_HOSTS" \
-o ConnectTimeout=10 \
"$@"
}
run_ssh install -d -m 750 "$REMOTE_DIR"
SUFFIX="${IMAGE_TAG}.$$.${SERVICE}"
REMOTE_ENV_NAME=".env.$PROJECT_NAME"
REMOTE_COMPOSE_NAME="docker-compose.$PROJECT_NAME.yml"
REMOTE_BLUE_GREEN_NAME="deploy-blue-green-service.sh"
REMOTE_ENV_TMP="$REMOTE_DIR/$REMOTE_ENV_NAME.$SUFFIX.tmp"
REMOTE_COMPOSE_TMP="$REMOTE_DIR/$REMOTE_COMPOSE_NAME.$SUFFIX.tmp"
REMOTE_BLUE_GREEN_TMP="$REMOTE_DIR/$REMOTE_BLUE_GREEN_NAME.$SUFFIX.tmp"
run_scp "$ENV_FILE" "$SSH_TARGET:$REMOTE_ENV_TMP"
run_scp "$COMPOSE_FILE" "$SSH_TARGET:$REMOTE_COMPOSE_TMP"
run_scp "deploy/scripts/deploy-blue-green-service.sh" "$SSH_TARGET:$REMOTE_BLUE_GREEN_TMP"
run_ssh sh -s -- "$REMOTE_ENV_TMP" "$REMOTE_COMPOSE_TMP" "$REMOTE_BLUE_GREEN_TMP" \
"$REMOTE_DIR/$REMOTE_ENV_NAME" "$REMOTE_DIR/$REMOTE_COMPOSE_NAME" "$REMOTE_DIR/$REMOTE_BLUE_GREEN_NAME" <<'REMOTE_PREPARE'
set -eu
env_tmp="$1"
compose_tmp="$2"
blue_green_tmp="$3"
env_target="$4"
compose_target="$5"
blue_green_target="$6"
chmod 600 "$env_tmp"
chmod 644 "$compose_tmp"
chmod 750 "$blue_green_tmp"
mv -f "$env_tmp" "$env_target"
mv -f "$compose_tmp" "$compose_target"
mv -f "$blue_green_tmp" "$blue_green_target"
REMOTE_PREPARE
remote_registry_logout() {
run_ssh docker logout "$REGISTRY_HOST" >/dev/null 2>&1 || true
}
echo "目标服务器从共享仓库拉取增量镜像: $IMAGE"
printf '%s' "$REGISTRY_PASSWORD" | run_ssh docker login -u "$REGISTRY_USER" --password-stdin "$REGISTRY_HOST"
trap remote_registry_logout EXIT HUP INT TERM
run_ssh docker pull "$IMAGE"
if [ "$DEPLOY_ENV" = "production" ]; then
echo "$DEPLOY_ENV 服务使用蓝绿执行器,旧实例在候选槽健康前保持运行"
run_ssh sh "$REMOTE_DIR/$REMOTE_BLUE_GREEN_NAME" \
"$DEPLOY_ENV" "$IMAGE_REPOSITORY" "$IMAGE_TAG" "$SERVICE" \
"$REMOTE_DIR/$REMOTE_ENV_NAME" "$REMOTE_DIR/$REMOTE_COMPOSE_NAME" "$PROJECT_NAME"
remote_registry_logout
trap - EXIT HUP INT TERM
exit 0
fi
run_ssh sh -s -- "$REMOTE_DIR" "$DEPLOY_ENV" "$IMAGE_REPOSITORY" "$IMAGE_TAG" "$SERVICE" "$PROJECT_NAME" "$REMOTE_ENV_NAME" "$REMOTE_COMPOSE_NAME" "$HEALTH_TIMEOUT_SECONDS" "$HEALTH_POLL_SECONDS" <<'REMOTE_DEPLOY'
set -eu
remote_dir="$1"
deploy_env="$2"
image_repository="$3"
image_tag="$4"
service="$5"
project_name="$6"
env_file="$7"
compose_file="$8"
health_timeout_seconds="$9"
health_poll_seconds="${10}"
cd "$remote_dir"
export DEPLOY_ENV="$deploy_env"
export IMAGE_REPOSITORY="$image_repository"
export IMAGE_TAG="$image_tag"
if [ "$deploy_env" = "development" ]; then
export PUBLISHED_BIND_ADDRESS=127.0.0.1
else
export PUBLISHED_BIND_ADDRESS="${PUBLISHED_BIND_ADDRESS:-0.0.0.0}"
fi
compose() {
docker compose \
--project-name "$project_name" \
--env-file "$env_file" \
-f "$compose_file" \
"$@"
}
container_id() {
compose ps --all --quiet "$service" | head -n 1
}
container_image() {
id="$(container_id)"
if [ -n "$id" ]; then
docker inspect --format '{{.Config.Image}}' "$id"
fi
}
health_state() {
id="$(container_id)"
if [ -z "$id" ]; then
echo missing
return
fi
docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}no-health:{{.State.Status}}{{end}}' "$id" 2>/dev/null || echo missing
}
wait_for_health() {
elapsed=0
while [ "$elapsed" -lt "$health_timeout_seconds" ]; do
state="$(health_state)"
echo "容器健康状态: $state(已等待 ${elapsed}s)"
case "$state" in
healthy) return 0 ;;
no-health:running)
echo "容器未配置 HEALTHCHECK,以运行状态作为成功依据" >&2
return 0
;;
unhealthy|no-health:exited|no-health:dead) return 1 ;;
esac
sleep "$health_poll_seconds"
elapsed=$((elapsed + health_poll_seconds))
done
echo "等待容器健康超时: ${health_timeout_seconds}s" >&2
return 1
}
new_image="$image_repository/$service:$image_tag"
previous_image="$(container_image || true)"
if [ -n "$previous_image" ]; then
echo "部署前运行镜像: $previous_image"
else
echo "首次部署,未发现可回滚的旧镜像"
fi
new_started=true
if ! compose up -d --no-deps --pull never "$service"; then
echo "新镜像启动命令失败: $new_image" >&2
new_started=false
fi
if [ "$new_started" = "true" ] && wait_for_health; then
running_image="$(container_image || true)"
if [ "$running_image" != "$new_image" ]; then
echo "运行容器镜像与请求镜像不一致: expected=$new_image actual=${running_image:-missing}" >&2
exit 1
fi
echo "新镜像健康检查通过: $new_image"
compose ps "$service"
exit 0
fi
echo "新镜像健康检查失败: $new_image" >&2
failed_container_id="$(container_id || true)"
if [ -n "$failed_container_id" ]; then
echo "新镜像容器最近日志(最多 200 行):" >&2
docker logs --tail 200 "$failed_container_id" 2>&1 || true
else
echo "新镜像容器不存在,无法读取启动日志" >&2
fi
expected_prefix="$image_repository/$service:"
case "$previous_image" in
"$expected_prefix"*) previous_tag="${previous_image#"$expected_prefix"}" ;;
'')
echo "首次部署没有可回滚镜像,部署失败" >&2
compose stop "$service" >/dev/null 2>&1 || true
exit 1
;;
*)
echo "旧镜像不属于当前服务,拒绝自动回滚: $previous_image" >&2
exit 1
;;
esac
if [ "$previous_image" = "$new_image" ]; then
echo "旧镜像与新镜像相同,无法自动回滚: $new_image" >&2
exit 1
fi
echo "开始自动回滚到: $previous_image" >&2
if ! docker pull "$previous_image"; then
echo "自动回滚失败,无法拉取旧镜像: $previous_image" >&2
exit 1
fi
IMAGE_TAG="$previous_tag"
export IMAGE_TAG
if ! compose up -d --no-deps --pull never "$service"; then
echo "自动回滚失败,旧镜像启动命令失败: $previous_image" >&2
exit 1
fi
if wait_for_health; then
echo "自动回滚成功: $previous_image" >&2
compose ps "$service" || true
else
echo "自动回滚失败,旧镜像未恢复健康: $previous_image" >&2
fi
exit 1
REMOTE_DEPLOY
remote_registry_logout
trap - EXIT HUP INT TERM
deploy_local
@@ -1,160 +0,0 @@
#!/usr/bin/env sh
set -eu
if [ "$#" -ne 4 ]; then
echo "用法: $0 <镜像仓库> <镜像标签> <逗号分隔服务> <环境文件>" >&2
exit 2
fi
IMAGE_REPOSITORY="$1"
IMAGE_TAG="$2"
SERVICE_CSV="$3"
ENV_FILE="$4"
SSH_TARGET="${DEVELOPMENT_SSH_TARGET:-deploy@172.31.198.8}"
SSH_KEY="${DEVELOPMENT_SSH_KEY:-/var/jenkins_home/.ssh/dev_deploy}"
SSH_KNOWN_HOSTS="${DEVELOPMENT_SSH_KNOWN_HOSTS:-/var/jenkins_home/.ssh/known_hosts}"
REMOTE_DIR="${DEVELOPMENT_DEPLOY_DIR:-/opt/firstleg/application}"
COMPOSE_FILE="${DEPLOY_COMPOSE_FILE:-deploy/docker-compose.yml}"
PROJECT_NAME="${DEPLOY_PROJECT_NAME:-firstleg-development}"
if [ ! -f "$SSH_KEY" ]; then
echo "开发环境 SSH 私钥不存在: $SSH_KEY" >&2
exit 2
fi
if [ ! -f "$SSH_KNOWN_HOSTS" ]; then
echo "SSH 主机指纹文件不存在: $SSH_KNOWN_HOSTS" >&2
exit 2
fi
if [ ! -f "$ENV_FILE" ]; then
echo "开发环境变量文件不存在: $ENV_FILE" >&2
exit 2
fi
if [ ! -f "$COMPOSE_FILE" ]; then
echo "Compose 文件不存在: $COMPOSE_FILE" >&2
exit 2
fi
SERVICES=$(printf '%s' "$SERVICE_CSV" | tr ',' ' ')
for service in $SERVICES; do
case "$service" in
*[!A-Za-z0-9_-]*|'')
echo "非法服务名: $service" >&2
exit 2
;;
esac
done
if [ "${DEVELOPMENT_DEPLOY_LOCAL:-false}" = "true" ]; then
echo "开发镜像已在开发节点本机构建,直接部署: $SERVICE_CSV"
install -d -m 750 "$REMOTE_DIR"
SUFFIX="${IMAGE_TAG}.$$.${SERVICE_CSV}"
LOCAL_ENV_TMP="$REMOTE_DIR/.env.$SUFFIX.tmp"
LOCAL_COMPOSE_TMP="$REMOTE_DIR/docker-compose.yml.$SUFFIX.tmp"
install -m 600 "$ENV_FILE" "$LOCAL_ENV_TMP"
install -m 644 "$COMPOSE_FILE" "$LOCAL_COMPOSE_TMP"
mv -f "$LOCAL_ENV_TMP" "$REMOTE_DIR/.env"
mv -f "$LOCAL_COMPOSE_TMP" "$REMOTE_DIR/docker-compose.yml"
for service in $SERVICES; do
docker image inspect "$IMAGE_REPOSITORY/$service:$IMAGE_TAG" >/dev/null
done
cd "$REMOTE_DIR"
export DEPLOY_ENV=development
export IMAGE_REPOSITORY IMAGE_TAG
export PUBLISHED_BIND_ADDRESS=127.0.0.1
docker compose \
--project-name "$PROJECT_NAME" \
--env-file .env \
-f docker-compose.yml \
pull $SERVICES
docker compose \
--project-name "$PROJECT_NAME" \
--env-file .env \
-f docker-compose.yml \
up -d --remove-orphans --pull never $SERVICES
docker compose \
--project-name "$PROJECT_NAME" \
--env-file .env \
-f docker-compose.yml \
ps $SERVICES
exit 0
fi
run_ssh() {
ssh -i "$SSH_KEY" \
-o BatchMode=yes \
-o StrictHostKeyChecking=yes \
-o UserKnownHostsFile="$SSH_KNOWN_HOSTS" \
-o ConnectTimeout=10 \
"$SSH_TARGET" "$@"
}
run_scp() {
scp -i "$SSH_KEY" \
-o BatchMode=yes \
-o StrictHostKeyChecking=yes \
-o UserKnownHostsFile="$SSH_KNOWN_HOSTS" \
-o ConnectTimeout=10 \
"$@"
}
run_ssh install -d -m 750 "$REMOTE_DIR"
SUFFIX="${IMAGE_TAG}.$$.${SERVICE_CSV}"
REMOTE_ENV_TMP="$REMOTE_DIR/.env.$SUFFIX.tmp"
REMOTE_COMPOSE_TMP="$REMOTE_DIR/docker-compose.yml.$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'
set -eu
remote_dir="$1"
env_tmp="$2"
compose_tmp="$3"
chmod 600 "$env_tmp"
chmod 644 "$compose_tmp"
mv -f "$env_tmp" "$remote_dir/.env"
mv -f "$compose_tmp" "$remote_dir/docker-compose.yml"
REMOTE_PREPARE
for service in $SERVICES; do
image="$IMAGE_REPOSITORY/$service:$IMAGE_TAG"
docker image inspect "$image" >/dev/null
echo "传输开发镜像: $image"
docker save "$image" | gzip -1 | run_ssh 'gzip -dc | docker load'
done
run_ssh sh -s -- "$REMOTE_DIR" "$IMAGE_REPOSITORY" "$IMAGE_TAG" "$SERVICE_CSV" "$PROJECT_NAME" <<'REMOTE_DEPLOY'
set -eu
remote_dir="$1"
image_repository="$2"
image_tag="$3"
service_csv="$4"
project_name="$5"
services=$(printf '%s' "$service_csv" | tr ',' ' ')
cd "$remote_dir"
export DEPLOY_ENV=development
export IMAGE_REPOSITORY="$image_repository"
export IMAGE_TAG="$image_tag"
export PUBLISHED_BIND_ADDRESS=127.0.0.1
docker compose \
--project-name "$project_name" \
--env-file .env \
-f docker-compose.yml \
up -d --remove-orphans --pull never $services
docker compose \
--project-name "$project_name" \
--env-file .env \
-f docker-compose.yml \
ps $services
REMOTE_DEPLOY
+4 -222
View File
@@ -1,225 +1,7 @@
#!/usr/bin/env sh
set -eu
if [ "$#" -ne 5 ]; then
echo "用法: $0 <环境> <镜像仓库> <镜像标签> <逗号分隔服务> <环境文件>" >&2
exit 2
fi
DEPLOY_ENV="$1"
IMAGE_REPOSITORY="$2"
IMAGE_TAG="$3"
SERVICE_CSV="$4"
ENV_FILE="$5"
if [ ! -f "$ENV_FILE" ]; then
echo "环境文件不存在: $ENV_FILE" >&2
exit 2
fi
SERVICES=$(printf '%s' "$SERVICE_CSV" | tr ',' ' ')
for service in $SERVICES; do
case "$service" in
*[!A-Za-z0-9_-]*|'')
echo "非法服务名: $service" >&2
exit 2
;;
esac
[ "$#" -eq 5 ] || { echo "Usage: $0 test repository tag service-csv env-file" >&2; exit 2; }
[ "$1" = "test" ] || exit 2
for service in $(printf '%s' "$4" | tr ',' ' '); do
sh deploy/scripts/deploy-built-service.sh "$1" "$2" "$3" "$service" "$5"
done
env_file_has_key() {
grep -q "^$1=" "$ENV_FILE"
}
env_file_value() {
sed -n "s/^$1=//p" "$ENV_FILE" | tail -n 1 | tr -d '\r'
}
DEPLOY_REGION=CN
for service in $SERVICES; do
case "$service" in
gateway-overseas|lastleg-service|warehouse-overseas-service) DEPLOY_REGION=OVERSEAS ;;
event-bridge-service)
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 "$BRIDGE_REGION" in
CN) ;;
OVERSEAS) DEPLOY_REGION=OVERSEAS ;;
*) echo "部署 event-bridge-service 时,REGION_CODE 只能为 CN 或 OVERSEAS" >&2; exit 2 ;;
esac
;;
esac
done
if [ "$DEPLOY_REGION" = "OVERSEAS" ]; then
for service in $SERVICES; do
case "$service" in
gateway-overseas|lastleg-service|warehouse-overseas-service|event-bridge-service) ;;
*) echo "一次部署不得混合国内与海外服务: $SERVICE_CSV" >&2; exit 2 ;;
esac
done
COMPOSE_FILE="deploy/docker-compose.overseas.yml"
PROJECT_NAME="firstleg-${DEPLOY_ENV}-overseas"
else
COMPOSE_FILE="deploy/docker-compose.yml"
PROJECT_NAME="firstleg-${DEPLOY_ENV}"
fi
if [ "$DEPLOY_REGION" = "OVERSEAS" ]; then
for key in NACOS_SERVER_ADDR OVERSEAS_NACOS_USERNAME OVERSEAS_NACOS_PASSWORD OVERSEAS_NACOS_NAMESPACE INTERNAL_SERVICE_TOKEN JWT_ISSUER OVERSEAS_JWT_AUDIENCE JWT_JWK_SET_URI; do
if ! env_file_has_key "$key" || [ -z "$(env_file_value "$key")" ]; then
echo "部署海外服务时,环境文件必须包含非空 $key" >&2
exit 2
fi
done
fi
case " $SERVICES " in
*" gateway-overseas "*)
if ! env_file_has_key AUTH_SESSION_VALIDATION_URL || [ -z "$(env_file_value AUTH_SESSION_VALIDATION_URL)" ]; then
echo "部署 gateway-overseas 时,环境文件必须包含非空 AUTH_SESSION_VALIDATION_URL" >&2
exit 2
fi
;;
esac
case " $SERVICES " in
*" finance "*)
for key in FINANCE_DB_URL FINANCE_DB_USERNAME FINANCE_DB_PASSWORD; do
if ! env_file_has_key "$key" || [ -z "$(env_file_value "$key")" ]; then
echo "部署 finance 服务时,环境文件必须包含非空 $key" >&2
exit 2
fi
done
;;
esac
case " $SERVICES " in
*" firstleg "*)
if ! env_file_has_key LASTLEG_EVENT_BRIDGE_URL || [ -z "$(env_file_value LASTLEG_EVENT_BRIDGE_URL)" ]; then
echo "部署 firstleg 时,环境文件必须包含非空 LASTLEG_EVENT_BRIDGE_URL" >&2
exit 2
fi
;;
esac
case " $SERVICES " in
*" lastleg-service "*)
for key in LASTLEG_DB_URL LASTLEG_DB_USERNAME LASTLEG_DB_PASSWORD FIRSTLEG_EVENT_BRIDGE_URL DOMESTIC_WAREHOUSE_URL FILE_SERVICE_URL; do
if ! env_file_has_key "$key" || [ -z "$(env_file_value "$key")" ]; then
echo "部署 lastleg-service 时,环境文件必须包含非空 $key" >&2
exit 2
fi
done
;;
esac
case " $SERVICES " in
*" warehouse-overseas-service "*)
for key in WAREHOUSE_OVERSEAS_DB_URL WAREHOUSE_OVERSEAS_DB_USERNAME WAREHOUSE_OVERSEAS_DB_PASSWORD; do
if ! env_file_has_key "$key" || [ -z "$(env_file_value "$key")" ]; then
echo "部署 warehouse-overseas-service 时,环境文件必须包含非空 $key" >&2
exit 2
fi
done
;;
esac
case " $SERVICES " in
*" event-bridge-service "*)
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 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
echo "部署 event-bridge-service 时,环境文件必须包含非空 $key" >&2
exit 2
fi
done
;;
esac
case " $SERVICES " in
*" file "*)
if ! env_file_has_key ROCKETMQ_NAME_SERVER || [ -z "$(env_file_value ROCKETMQ_NAME_SERVER)" ]; then
echo "部署 file 服务时,环境文件必须包含非空 ROCKETMQ_NAME_SERVER" >&2
exit 2
fi
if ! env_file_has_key MQ_TOPIC_PREFIX; then
echo "部署 file 服务时,环境文件必须显式包含 MQ_TOPIC_PREFIX:测试填 test-,生产填空字符串" >&2
exit 2
fi
MQ_PREFIX=$(env_file_value MQ_TOPIC_PREFIX)
case "$DEPLOY_ENV" in
test)
if [ "$MQ_PREFIX" != "test-" ]; then
echo "测试环境部署 file 服务时 MQ_TOPIC_PREFIX 必须为 test-" >&2
exit 2
fi
;;
production)
if [ -n "$MQ_PREFIX" ]; then
echo "生产环境部署 file 服务时 MQ_TOPIC_PREFIX 必须显式为空字符串" >&2
exit 2
fi
;;
*)
if [ -z "$MQ_PREFIX" ] || [ "$MQ_PREFIX" = "test-" ]; then
echo "预发布环境部署 file 服务时 MQ_TOPIC_PREFIX 必须为非空且不能使用 test-" >&2
exit 2
fi
;;
esac
;;
esac
export DEPLOY_ENV IMAGE_REPOSITORY IMAGE_TAG
if [ "$DEPLOY_ENV" = "development" ]; then
DEPLOY_COMPOSE_FILE="$COMPOSE_FILE" DEPLOY_PROJECT_NAME="$PROJECT_NAME" exec sh deploy/scripts/deploy-development-service.sh \
"$IMAGE_REPOSITORY" "$IMAGE_TAG" "$SERVICE_CSV" "$ENV_FILE"
fi
if [ "$DEPLOY_ENV" = "test" ] || [ "$DEPLOY_ENV" = "production" ]; then
docker compose \
--project-name "$PROJECT_NAME" \
--env-file "$ENV_FILE" \
-f "$COMPOSE_FILE" \
pull $SERVICES
for service in $SERVICES; do
if [ "$DEPLOY_ENV" = test ]; then
docker compose --project-name "$PROJECT_NAME" --env-file "$ENV_FILE" -f "$COMPOSE_FILE" \
up -d --no-deps --pull never "$service"
continue
fi
sh deploy/scripts/deploy-blue-green-service.sh \
"$DEPLOY_ENV" "$IMAGE_REPOSITORY" "$IMAGE_TAG" "$service" "$ENV_FILE" "$COMPOSE_FILE" "$PROJECT_NAME"
done
exit 0
fi
docker compose \
--project-name "$PROJECT_NAME" \
--env-file "$ENV_FILE" \
-f "$COMPOSE_FILE" \
pull $SERVICES
docker compose \
--project-name "$PROJECT_NAME" \
--env-file "$ENV_FILE" \
-f "$COMPOSE_FILE" \
up -d --remove-orphans --pull never $SERVICES
docker compose \
--project-name "$PROJECT_NAME" \
--env-file "$ENV_FILE" \
-f "$COMPOSE_FILE" \
ps $SERVICES
+12 -44
View File
@@ -1,47 +1,15 @@
#!/usr/bin/env sh
set -eu
# 仅用于测试基础设施容器。所有 SQL 均使用 CREATE ... IF NOT EXISTS 或幂等扩展语句,
# 不删除已有业务数据;仓库样例数据使用固定 ID 重复写入。
MYSQL_CONTAINER="${TEST_MYSQL_CONTAINER:-firstleg-test-mysql}"
if ! docker inspect --type container "$MYSQL_CONTAINER" >/dev/null 2>&1; then
echo "测试 MySQL 容器不存在:$MYSQL_CONTAINER" >&2
echo "请先执行 docker compose -f docker-compose-infra.yml up -d" >&2
exit 1
fi
if [ "$(docker inspect -f '{{.State.Running}}' "$MYSQL_CONTAINER")" != "true" ]; then
echo "测试 MySQL 容器未运行:$MYSQL_CONTAINER" >&2
exit 1
fi
for sql_file in \
auth/src/main/resources/db/auth.sql \
iam/src/main/resources/db/iam.sql \
auth/src/main/resources/db/phase1-admin-accounts.sql \
iam/src/main/resources/db/phase1-admin-rbac.sql \
iam/src/main/resources/db/phase1-onboarding.sql \
iam/src/main/resources/db/phase1-customer-profile.sql \
iam/src/main/resources/db/phase1-member-permissions.sql \
iam/src/main/resources/db/rbac-role-management-v2.sql \
iam/src/main/resources/db/phase1-platform-master-data-and-permission-management.sql \
order/src/main/resources/db/order.sql \
order/src/main/resources/db/phase1-order-extension.sql \
order/src/main/resources/db/phase1-masterdata-extension.sql \
warehouse/src/main/resources/db/warehouse.sql \
warehouse/src/main/resources/db/warehouse-partner.sql \
warehouse/src/main/resources/db/warehouse-express-company.sql \
warehouse/src/main/resources/db/phase1-warehouse-schema-migration.sql \
warehouse/src/main/resources/db/warehouse-seed.sql \
file/src/main/resources/db/file.sql
do
if [ ! -f "$sql_file" ]; then
echo "找不到初始化 SQL:$sql_file" >&2
exit 1
fi
echo "初始化测试数据库:$sql_file"
docker exec -i "$MYSQL_CONTAINER" sh -c 'exec mysql -uroot -p"$MYSQL_ROOT_PASSWORD" --default-character-set=utf8mb4' < "$sql_file"
# Empty-schema baseline only. Never invoked by Compose/Jenkins automatically.
[ "${ALLOW_TEST_SCHEMA_INITIALIZATION:-false}" = "true" ] || { echo 'Explicit TEST SQL authorization required' >&2; exit 2; }
: "${TEST_MYSQL_CONTAINER:?Specify the authorized TEST MySQL container}"
[ "$#" -eq 2 ] || { echo 'Usage: initialize-test-databases.sh <firstleg-baseline.sql> <overseas-baseline.sql>' >&2; exit 2; }
for sql_file in "$1" "$2"; do
[ -f "$sql_file" ] || { echo "Missing reviewed baseline: $sql_file" >&2; exit 2; }
done
[ "$1" != "$2" ] || { echo 'Two distinct reviewed baselines required' >&2; exit 2; }
[ "$(docker inspect -f '{{.State.Running}}' "$TEST_MYSQL_CONTAINER")" = 'true' ] || { echo 'TEST MySQL is not running' >&2; exit 1; }
for sql_file in "$1" "$2"; do
echo "Applying reviewed TEST baseline: $sql_file"
docker exec -i "$TEST_MYSQL_CONTAINER" sh -c 'exec mysql -uroot -p"$MYSQL_ROOT_PASSWORD" --default-character-set=utf8mb4' < "$sql_file"
done
echo "测试数据库初始化完成。"
-12
View File
@@ -3,11 +3,6 @@ services:
artifactId: gateway
jar: gateway/target/gateway-1.0.0-SNAPSHOT.jar
port: 9000
gateway-overseas:
artifactId: gateway-overseas
jar: gateway-overseas/target/gateway-overseas-1.0.0-SNAPSHOT.jar
port: 9300
regions: [OVERSEAS]
auth:
artifactId: auth-service
jar: auth/target/auth-service-1.0.0-SNAPSHOT.jar
@@ -44,17 +39,10 @@ services:
artifactId: lastleg-service
jar: lastleg-service/target/lastleg-service-1.0.0-SNAPSHOT.jar
port: 9303
regions: [OVERSEAS]
warehouse-overseas-service:
artifactId: warehouse-overseas-service
jar: warehouse-overseas-service/target/warehouse-overseas-service-1.0.0-SNAPSHOT.jar
port: 9305
regions: [OVERSEAS]
event-bridge-service:
artifactId: event-bridge-service
jar: event-bridge-service/target/event-bridge-service-1.0.0-SNAPSHOT.jar
port: 9312
regions: [CN, OVERSEAS]
audit-service:
artifactId: audit-service
jar: audit-service/target/audit-service-1.0.0-SNAPSHOT.jar
+8 -8
View File
@@ -1,4 +1,4 @@
# 区域数据库空库初始化
# 单测试环境数据库空库初始化
## 唯一入口与区域边界
@@ -6,11 +6,11 @@
| 区域 | 完整入口 | 服务 schema | 表数 |
|---|---|---|---:|
| 国内 | `头程数据库统一增量-MySQL8.sql` | `baiyue_auth`、`baiyue_iam`、`baiyue_order`、`baiyue_warehouse`、`baiyue_firstleg`、`baiyue_file`、`baiyue_audit`、`baiyue_event_bridge`、`baiyue_finance` | 223 |
| 海外 | `海外区域统一增量-MySQL8.sql` | `baiyue_lastleg`、`baiyue_lastlag_warehouse`、`baiyue_event_bridge_overseas` | 138 |
| 国内 | `头程数据库统一增量-MySQL8.sql` | `baiyue_auth`、`baiyue_iam`、`baiyue_order`、`baiyue_warehouse`、`baiyue_firstleg`、`baiyue_file`、`baiyue_audit`、`baiyue_finance` | 223 |
| 海外 | `海外区域统一增量-MySQL8.sql` | `baiyue_lastleg`、`baiyue_lastlag_warehouse` | 135 |
- 国内和海外是两个部署区域,不是把微服务合并为两个 schema。`baiyue_lastlag_warehouse` 保留当前正式的 `lastlag` 拼写。
- 两区域可分别在独立空实例执行,没有跨区域 SQL 依赖。海外不访问国内 IAM/Warehouse;国内不创建、查询或修改海外业务表。
- 所有服务位于同一测试环境;两份文件只分组组织服务 schema,不代表两个部署区域。`baiyue_lastlag_warehouse` 保留当前正式的 `lastlag` 拼写。
- 两份文件必须在同一测试数据库实例中完成初始化;业务服务仍只访问本服务 schema,经内部接口访问 IAM/Warehouse。已移除事件桥两个 schema 的创建,不对已有数据库执行 DROP。
- 国内 IAM 是全部平台与客户权限的唯一权威,包括海外仓、海外业务、海外主数据及财务权限;这些权限保留在国内种子中。
- 国内 Warehouse 管理包括海外仓在内的公共仓库主档;海外 Warehouse 只保存资源与主档 ID 引用。
- Gateway、Print、Integration 不新增数据库。Compose 继续使用 `SPRING_SQL_INIT_MODE=never`,服务启动不执行这些文件。
@@ -24,7 +24,7 @@
5. DDL 隐式提交,不可整体回滚。失败后停止并保留错误;经另行授权准备空库后从头执行,不针对半初始化库追加迁移或从失败行续跑。
6. Firstleg 测量项与拆分项互为外键:建空表期间暂设会话 `foreign_key_checks=0`,随后立即恢复 `1`,再插入种子。失败时关闭连接。两边外键完整保留在 CREATE TABLE 内。
成功时只在最末输出一行 `initialization_status=PASS`,并带 `region=CN/OVERSEAS`、`initialized_tables=223/138`。必须同时确认客户端退出码 0;忽略错误后出现的 PASS 不算成功。
成功时只在最末输出一行 `initialization_status=PASS`,并带 `region=CN/OVERSEAS`、`initialized_tables=223/135`。必须同时确认客户端退出码 0;忽略错误后出现的 PASS 不算成功。
## 平台管理员密码
@@ -44,9 +44,9 @@
| Warehouse | 既有公共仓目录 318 条及地址;义乌、东莞、洛杉矶、纽约 4 个作业仓收货能力 | 示例供应商、快递、往来单位、托盘、库位、预占、库存 |
| Firstleg | 112 个费用项目、15 个渠道目录、7 个始发地别名、4 个公共仓映射 | 客户报价、订单、入库、实测或履约记录 |
| File / Finance | 导出互斥键 `GLOBAL_CREATE`、默认平台财务主体 | 文件任务、资金、账单或交易流水 |
| Lastleg | 国内固定管理员对应的 USER/AUTHORIZATION version=0 安全投影,当前角色及完整 106 项海外权限 | 客户、订单、库存、发运门禁事实或新账号 |
| Lastleg | 无身份或授权种子;复用统一Auth/IAM会话 | 客户、订单、库存、发运门禁事实或独立账号 |
海外管理员投影只是国内已定义身份的初始化快照,不是独立账号或权限中心。后续变更仍由 IAM 事件同步,不提供历史投影回填。
IAM保留subject/tenant持久版本表;安全事件专用Outbox及Lastleg授权投影表已退出新空库基线。旧库不在本次脚本中执行删除或历史回填。
扣货写权限只默认授予平台管理员,与 IAM 扣货权限合同一致;旧脚本重放时通过范围授权额外授予订单运营的那一条关联不进入新基线。
+3 -7
View File
@@ -2,7 +2,6 @@
"schemaVersion": 1,
"services": [
"gateway",
"gateway-overseas",
"auth",
"iam",
"order",
@@ -10,7 +9,6 @@
"firstleg",
"file",
"audit-service",
"event-bridge-service",
"finance",
"integration-service",
"print",
@@ -19,7 +17,7 @@
],
"regions": {
"CN": {
"baseline": "baseline/cn/cn-baseline.sql",
"baseline": "头程数据库统一增量-MySQL8.sql",
"schemas": [
{ "name": "baiyue_auth", "services": ["auth"], "order": 10 },
{ "name": "baiyue_iam", "services": ["iam"], "order": 20 },
@@ -28,16 +26,14 @@
{ "name": "baiyue_firstleg", "services": ["firstleg"], "order": 50 },
{ "name": "baiyue_file", "services": ["file"], "order": 60 },
{ "name": "baiyue_audit", "services": ["audit-service"], "order": 70 },
{ "name": "baiyue_event_bridge", "services": ["event-bridge-service"], "order": 80 },
{ "name": "baiyue_finance", "services": ["finance"], "order": 90 }
]
},
"OVERSEAS": {
"baseline": "baseline/overseas/overseas-baseline.sql",
"baseline": "海外区域统一增量-MySQL8.sql",
"schemas": [
{ "name": "baiyue_lastleg", "services": ["lastleg-service"], "order": 10 },
{ "name": "baiyue_lastlag_warehouse", "services": ["warehouse-overseas-service"], "order": 20 },
{ "name": "baiyue_event_bridge_overseas", "services": ["event-bridge-service"], "order": 30 }
{ "name": "baiyue_lastlag_warehouse", "services": ["warehouse-overseas-service"], "order": 20 }
]
}
}
@@ -0,0 +1,22 @@
-- Region: CN
-- Schema: baiyue_finance
-- Purpose: transactional manual retry audit for immutable business Outbox entries.
-- Preconditions: service Outbox schema exists; authorized schema migration only.
-- Postconditions: retry state and operator audit can commit atomically.
SET NAMES utf8mb4;
CREATE TABLE event_delivery_retry_audit (
request_id VARCHAR(36) NOT NULL COMMENT '人工重试请求UUID和幂等键',
producer_table VARCHAR(64) NOT NULL COMMENT '本服务发件箱表名',
entry_id VARCHAR(64) NOT NULL COMMENT '发件箱记录主键',
event_id VARCHAR(128) NOT NULL COMMENT '原事件全局标识',
event_type VARCHAR(64) NOT NULL COMMENT '原事件类型',
payload_hash VARCHAR(71) NOT NULL COMMENT '原事件不可变负载摘要',
operator_name VARCHAR(128) NOT NULL COMMENT '持有运维凭据的操作人标识',
reason VARCHAR(500) NOT NULL COMMENT '人工重试原因',
previous_failures INT NOT NULL COMMENT '重试前累计失败次数',
previous_error VARCHAR(1000) NULL COMMENT '重试前最后一次脱敏错误',
requested_at DATETIME(3) NOT NULL COMMENT '重试申请提交时间',
PRIMARY KEY (request_id),
KEY idx_event_retry_entry (producer_table,entry_id,requested_at),
KEY idx_event_retry_event (event_id,requested_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='业务事件人工重试审计,与恢复待发送状态同事务提交';
@@ -0,0 +1,23 @@
-- Region: CN
-- Schema: baiyue_finance
-- Purpose: durable delivery attempts committed with Outbox lease transitions.
-- Preconditions: authorized schema migration before starting the new producer.
-- Postconditions: per-attempt audit exists without cross-schema access.
SET NAMES utf8mb4;
CREATE TABLE event_delivery_attempt (
attempt_id VARCHAR(36) NOT NULL COMMENT '本次投递租约UUID和审计主键',
service_name VARCHAR(32) NOT NULL COMMENT '本库所属生产服务',
event_type VARCHAR(96) NOT NULL COMMENT '白名单业务事件类型',
entry_id VARCHAR(128) NOT NULL COMMENT '生产者发件箱主键',
event_id VARCHAR(128) NULL COMMENT '合法原事件UUID或ULID;格式错误时为空',
target_name VARCHAR(64) NOT NULL COMMENT '固定目标业务服务',
started_at DATETIME(3) NOT NULL COMMENT '领取及审计事务提交前的开始时间',
finished_at DATETIME(3) NULL COMMENT '尝试完成或租约过期恢复时间',
duration_ms BIGINT NULL COMMENT '尝试耗时毫秒',
outcome VARCHAR(24) NOT NULL COMMENT 'STARTED/APPLIED/DUPLICATE/STALE/RETRY/DEAD/LEASE_EXPIRED',
error_code VARCHAR(64) NULL COMMENT '有限错误类别或HTTP状态;不含载荷与令牌',
next_retry_at DATETIME(3) NULL COMMENT '本次失败后下次计划投递时间',
PRIMARY KEY (attempt_id),
KEY idx_delivery_attempt_entry (service_name,entry_id,started_at),
KEY idx_delivery_attempt_outcome (outcome,started_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='本服务业务事件逐次投递审计';
@@ -0,0 +1,22 @@
-- Region: CN
-- Schema: baiyue_firstleg
-- Purpose: transactional manual retry audit for immutable business Outbox entries.
-- Preconditions: service Outbox schema exists; authorized schema migration only.
-- Postconditions: retry state and operator audit can commit atomically.
SET NAMES utf8mb4;
CREATE TABLE event_delivery_retry_audit (
request_id VARCHAR(36) NOT NULL COMMENT '人工重试请求UUID和幂等键',
producer_table VARCHAR(64) NOT NULL COMMENT '本服务发件箱表名',
entry_id VARCHAR(64) NOT NULL COMMENT '发件箱记录主键',
event_id VARCHAR(128) NOT NULL COMMENT '原事件全局标识',
event_type VARCHAR(64) NOT NULL COMMENT '原事件类型',
payload_hash VARCHAR(71) NOT NULL COMMENT '原事件不可变负载摘要',
operator_name VARCHAR(128) NOT NULL COMMENT '持有运维凭据的操作人标识',
reason VARCHAR(500) NOT NULL COMMENT '人工重试原因',
previous_failures INT NOT NULL COMMENT '重试前累计失败次数',
previous_error VARCHAR(1000) NULL COMMENT '重试前最后一次脱敏错误',
requested_at DATETIME(3) NOT NULL COMMENT '重试申请提交时间',
PRIMARY KEY (request_id),
KEY idx_event_retry_entry (producer_table,entry_id,requested_at),
KEY idx_event_retry_event (event_id,requested_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='业务事件人工重试审计,与恢复待发送状态同事务提交';
@@ -0,0 +1,23 @@
-- Region: CN
-- Schema: baiyue_firstleg
-- Purpose: durable delivery attempts committed with Outbox lease transitions.
-- Preconditions: authorized schema migration before starting the new producer.
-- Postconditions: per-attempt audit exists without cross-schema access.
SET NAMES utf8mb4;
CREATE TABLE event_delivery_attempt (
attempt_id VARCHAR(36) NOT NULL COMMENT '本次投递租约UUID和审计主键',
service_name VARCHAR(32) NOT NULL COMMENT '本库所属生产服务',
event_type VARCHAR(96) NOT NULL COMMENT '白名单业务事件类型',
entry_id VARCHAR(128) NOT NULL COMMENT '生产者发件箱主键',
event_id VARCHAR(128) NULL COMMENT '合法原事件UUID或ULID;格式错误时为空',
target_name VARCHAR(64) NOT NULL COMMENT '固定目标业务服务',
started_at DATETIME(3) NOT NULL COMMENT '领取及审计事务提交前的开始时间',
finished_at DATETIME(3) NULL COMMENT '尝试完成或租约过期恢复时间',
duration_ms BIGINT NULL COMMENT '尝试耗时毫秒',
outcome VARCHAR(24) NOT NULL COMMENT 'STARTED/APPLIED/DUPLICATE/STALE/RETRY/DEAD/LEASE_EXPIRED',
error_code VARCHAR(64) NULL COMMENT '有限错误类别或HTTP状态;不含载荷与令牌',
next_retry_at DATETIME(3) NULL COMMENT '本次失败后下次计划投递时间',
PRIMARY KEY (attempt_id),
KEY idx_delivery_attempt_entry (service_name,entry_id,started_at),
KEY idx_delivery_attempt_outcome (outcome,started_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='本服务业务事件逐次投递审计';
@@ -0,0 +1,22 @@
-- Region: OVERSEAS
-- Schema: baiyue_lastleg
-- Purpose: transactional manual retry audit for immutable business Outbox entries.
-- Preconditions: service Outbox schema exists; authorized schema migration only.
-- Postconditions: retry state and operator audit can commit atomically.
SET NAMES utf8mb4;
CREATE TABLE event_delivery_retry_audit (
request_id VARCHAR(36) NOT NULL COMMENT '人工重试请求UUID和幂等键',
producer_table VARCHAR(64) NOT NULL COMMENT '本服务发件箱表名',
entry_id VARCHAR(64) NOT NULL COMMENT '发件箱记录主键',
event_id VARCHAR(128) NOT NULL COMMENT '原事件全局标识',
event_type VARCHAR(64) NOT NULL COMMENT '原事件类型',
payload_hash VARCHAR(71) NOT NULL COMMENT '原事件不可变负载摘要',
operator_name VARCHAR(128) NOT NULL COMMENT '持有运维凭据的操作人标识',
reason VARCHAR(500) NOT NULL COMMENT '人工重试原因',
previous_failures INT NOT NULL COMMENT '重试前累计失败次数',
previous_error VARCHAR(1000) NULL COMMENT '重试前最后一次脱敏错误',
requested_at DATETIME(3) NOT NULL COMMENT '重试申请提交时间',
PRIMARY KEY (request_id),
KEY idx_event_retry_entry (producer_table,entry_id,requested_at),
KEY idx_event_retry_event (event_id,requested_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='业务事件人工重试审计,与恢复待发送状态同事务提交';
@@ -0,0 +1,23 @@
-- Region: OVERSEAS
-- Schema: baiyue_lastleg
-- Purpose: durable delivery attempts committed with Outbox lease transitions.
-- Preconditions: authorized schema migration before starting the new producer.
-- Postconditions: per-attempt audit exists without cross-schema access.
SET NAMES utf8mb4;
CREATE TABLE event_delivery_attempt (
attempt_id VARCHAR(36) NOT NULL COMMENT '本次投递租约UUID和审计主键',
service_name VARCHAR(32) NOT NULL COMMENT '本库所属生产服务',
event_type VARCHAR(96) NOT NULL COMMENT '白名单业务事件类型',
entry_id VARCHAR(128) NOT NULL COMMENT '生产者发件箱主键',
event_id VARCHAR(128) NULL COMMENT '合法原事件UUID或ULID;格式错误时为空',
target_name VARCHAR(64) NOT NULL COMMENT '固定目标业务服务',
started_at DATETIME(3) NOT NULL COMMENT '领取及审计事务提交前的开始时间',
finished_at DATETIME(3) NULL COMMENT '尝试完成或租约过期恢复时间',
duration_ms BIGINT NULL COMMENT '尝试耗时毫秒',
outcome VARCHAR(24) NOT NULL COMMENT 'STARTED/APPLIED/DUPLICATE/STALE/RETRY/DEAD/LEASE_EXPIRED',
error_code VARCHAR(64) NULL COMMENT '有限错误类别或HTTP状态;不含载荷与令牌',
next_retry_at DATETIME(3) NULL COMMENT '本次失败后下次计划投递时间',
PRIMARY KEY (attempt_id),
KEY idx_delivery_attempt_entry (service_name,entry_id,started_at),
KEY idx_delivery_attempt_outcome (outcome,started_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='本服务业务事件逐次投递审计';
+75 -102
View File
@@ -1,5 +1,5 @@
-- CN 当前纯空库初始化入口(文件名沿用原入口,不提供增量升级语义)。
-- MySQL 8.0+;只允许目标区域的服务 schema 为空,已有表立即报错;禁止 --force/遇错继续。
-- MySQL 8.0+;只允许目标测试环境的服务 schema 为空,已有表立即报错;禁止 --force/遇错继续。
-- 字段、索引、约束及注释直接定义在 CREATE TABLE 内;不回填、不清理、不改造存量。
-- 国内 IAM 管理两区域权限;海外不访问国内库。执行方法及系统种子清单见同目录 README.md。
-- DDL 隐式提交;失败后停止,不从中间续跑,也不把部分初始化当成成功。
@@ -319,25 +319,6 @@ CREATE TABLE iam_role_permission (
KEY idx_rp_tenant (tenant_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='角色权限关联表:定义角色可访问的菜单、页面、按钮和API资源';
CREATE TABLE iam_security_event_outbox (
event_id varchar(64) NOT NULL COMMENT '全局事件ID,消费者幂等键',
event_type varchar(64) NOT NULL COMMENT '安全事件类型',
aggregate_type varchar(32) NOT NULL COMMENT 'USER、TENANT或AUTHORIZATION',
aggregate_id varchar(64) NOT NULL COMMENT '聚合标识',
payload_json json NOT NULL COMMENT '标准跨区事件信封,不含密码、令牌和权限全集',
status varchar(16) NOT NULL DEFAULT 'PENDING' COMMENT '发送状态:PENDING、SENDING、SENT、DEAD',
attempt_count int NOT NULL DEFAULT '0' COMMENT '发送尝试次数',
next_retry_at datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '下次重试时间',
lease_owner varchar(128) DEFAULT NULL COMMENT '租约持有者',
lease_until datetime(3) DEFAULT NULL COMMENT '租约到期时间',
last_error varchar(1000) DEFAULT NULL COMMENT '最近一次错误',
sent_at datetime(3) DEFAULT NULL COMMENT '发送成功时间',
created_at datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
updated_at datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
PRIMARY KEY (event_id),
KEY idx_security_outbox_publish (status,next_retry_at,lease_until,created_at),
KEY idx_security_outbox_aggregate (aggregate_type,aggregate_id,created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='IAM可靠安全事件本地发送箱';
CREATE TABLE iam_security_subject_version (
auth_user_id bigint NOT NULL COMMENT '认证账号ID',
@@ -4661,88 +4642,6 @@ CREATE TABLE sys_audit_log (
KEY idx_audit_request (request_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='统一业务审计日志表:追加记录敏感操作的主体、租户、资源、结果及前后快照;不同于消息Outbox和消费幂等表';
-- 服务 schema:baiyue_event_bridge
CREATE DATABASE IF NOT EXISTS baiyue_event_bridge DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE baiyue_event_bridge;
CREATE TABLE bridge_delivery_attempt (
id bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
event_id varchar(128) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '事件全局标识',
source_region varchar(32) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '来源区域',
target_region varchar(32) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '目标区域',
attempt_no int NOT NULL COMMENT '投递序号',
status varchar(32) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '投递结果',
response_code varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '对端响应码',
error_message varchar(1000) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '错误信息',
started_at datetime(3) NOT NULL COMMENT '开始时间',
finished_at datetime(3) DEFAULT NULL COMMENT '完成时间',
PRIMARY KEY (id),
UNIQUE KEY uk_bridge_attempt (event_id,attempt_no),
KEY idx_bridge_attempt_event (event_id,started_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='跨区域事件投递尝试记录';
CREATE TABLE bridge_inbox (
event_id varchar(128) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '事件全局标识',
idempotency_key varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '接收幂等键',
event_type varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '事件类型',
event_version int NOT NULL COMMENT '事件契约版本',
source_region varchar(32) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '来源区域',
target_region varchar(32) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '目标区域',
customer_id varchar(128) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '客户或业务隔离标识;跨区域事件沿用来源系统口径',
aggregate_type varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '聚合类型',
aggregate_id varchar(128) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '聚合标识',
aggregate_version bigint NOT NULL COMMENT '聚合版本',
handover_version bigint DEFAULT NULL COMMENT '跨区交接版本',
trace_id varchar(128) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '链路标识',
correlation_id varchar(128) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '业务关联标识',
causation_id varchar(128) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '上游事件标识',
occurred_at datetime(3) NOT NULL COMMENT '业务事实发生时间',
payload_hash varchar(71) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '规范化负载摘要',
payload_json json NOT NULL COMMENT '事件负载',
status varchar(32) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'RECEIVED' COMMENT '消费状态',
lease_owner varchar(128) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '消费租约持有者',
lease_until datetime(3) DEFAULT NULL COMMENT '消费租约截止时间',
retry_count int NOT NULL DEFAULT '0' COMMENT '消费重试次数',
next_retry_at datetime(3) DEFAULT NULL COMMENT '下次消费时间',
last_error varchar(1000) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '最后错误',
received_at datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '接收时间',
processed_at datetime(3) DEFAULT NULL COMMENT '消费完成时间',
PRIMARY KEY (event_id),
UNIQUE KEY uk_bridge_inbox_idempotency (idempotency_key),
KEY idx_bridge_inbox_consume (status,next_retry_at,lease_until)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='跨区域事件已接收箱';
CREATE TABLE bridge_outbox (
event_id varchar(128) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '事件全局标识',
idempotency_key varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '发送幂等键',
event_type varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '事件类型',
event_version int NOT NULL COMMENT '事件契约版本',
source_region varchar(32) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '来源区域',
target_region varchar(32) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '目标区域',
customer_id varchar(128) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '客户或业务隔离标识;跨区域事件沿用来源系统口径',
aggregate_type varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '聚合类型',
aggregate_id varchar(128) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '聚合标识',
aggregate_version bigint NOT NULL COMMENT '聚合版本',
handover_version bigint DEFAULT NULL COMMENT '跨区交接版本',
trace_id varchar(128) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '链路标识',
correlation_id varchar(128) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '业务关联标识',
causation_id varchar(128) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '上游事件标识',
occurred_at datetime(3) NOT NULL COMMENT '业务事实发生时间',
payload_hash varchar(71) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '规范化负载摘要',
payload_json json NOT NULL COMMENT '事件负载',
status varchar(32) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'PENDING' COMMENT '投递状态',
retry_count int NOT NULL DEFAULT '0' COMMENT '已重试次数',
next_retry_at datetime(3) DEFAULT NULL COMMENT '下次重试时间',
lease_owner varchar(128) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '租约持有者',
lease_until datetime(3) DEFAULT NULL COMMENT '租约截止时间',
last_error varchar(1000) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '最后错误',
created_at datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
delivered_at datetime(3) DEFAULT NULL COMMENT '投递成功时间',
PRIMARY KEY (event_id),
UNIQUE KEY uk_bridge_outbox_idempotency (idempotency_key),
KEY idx_bridge_outbox_schedule (target_region,status,next_retry_at,lease_until)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='跨区域事件待投递箱';
-- 服务 schema:baiyue_finance
CREATE DATABASE IF NOT EXISTS baiyue_finance DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
USE baiyue_finance;
@@ -8142,4 +8041,78 @@ CREATE TABLE fl_sailing_booking_link (
CONSTRAINT chk_sailing_booking_released CHECK (booking_status_snapshot='RELEASED')
) ENGINE=InnoDB COMMENT='船期计划与已放舱订舱的最小关联及历史';
USE baiyue_firstleg;
CREATE TABLE event_delivery_retry_audit (
request_id VARCHAR(36) NOT NULL COMMENT '人工重试请求UUID和幂等键',
producer_table VARCHAR(64) NOT NULL COMMENT '本服务发件箱表名',
entry_id VARCHAR(64) NOT NULL COMMENT '发件箱记录主键',
event_id VARCHAR(128) NOT NULL COMMENT '原事件全局标识',
event_type VARCHAR(64) NOT NULL COMMENT '原事件类型',
payload_hash VARCHAR(71) NOT NULL COMMENT '原事件不可变负载摘要',
operator_name VARCHAR(128) NOT NULL COMMENT '持有运维凭据的操作人标识',
reason VARCHAR(500) NOT NULL COMMENT '人工重试原因',
previous_failures INT NOT NULL COMMENT '重试前累计失败次数',
previous_error VARCHAR(1000) NULL COMMENT '重试前最后一次脱敏错误',
requested_at DATETIME(3) NOT NULL COMMENT '重试申请提交时间',
PRIMARY KEY (request_id),
KEY idx_event_retry_entry (producer_table,entry_id,requested_at),
KEY idx_event_retry_event (event_id,requested_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='业务事件人工重试审计,与恢复待发送状态同事务提交';
USE baiyue_finance;
CREATE TABLE event_delivery_retry_audit (
request_id VARCHAR(36) NOT NULL COMMENT '人工重试请求UUID和幂等键',
producer_table VARCHAR(64) NOT NULL COMMENT '本服务发件箱表名',
entry_id VARCHAR(64) NOT NULL COMMENT '发件箱记录主键',
event_id VARCHAR(128) NOT NULL COMMENT '原事件全局标识',
event_type VARCHAR(64) NOT NULL COMMENT '原事件类型',
payload_hash VARCHAR(71) NOT NULL COMMENT '原事件不可变负载摘要',
operator_name VARCHAR(128) NOT NULL COMMENT '持有运维凭据的操作人标识',
reason VARCHAR(500) NOT NULL COMMENT '人工重试原因',
previous_failures INT NOT NULL COMMENT '重试前累计失败次数',
previous_error VARCHAR(1000) NULL COMMENT '重试前最后一次脱敏错误',
requested_at DATETIME(3) NOT NULL COMMENT '重试申请提交时间',
PRIMARY KEY (request_id),
KEY idx_event_retry_entry (producer_table,entry_id,requested_at),
KEY idx_event_retry_event (event_id,requested_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='业务事件人工重试审计,与恢复待发送状态同事务提交';
USE baiyue_firstleg;
CREATE TABLE event_delivery_attempt (
attempt_id VARCHAR(36) NOT NULL COMMENT '本次投递租约UUID和审计主键',
service_name VARCHAR(32) NOT NULL COMMENT '本库所属生产服务',
event_type VARCHAR(96) NOT NULL COMMENT '白名单业务事件类型',
entry_id VARCHAR(128) NOT NULL COMMENT '生产者发件箱主键',
event_id VARCHAR(128) NULL COMMENT '合法原事件UUID或ULID;格式错误时为空',
target_name VARCHAR(64) NOT NULL COMMENT '固定目标业务服务',
started_at DATETIME(3) NOT NULL COMMENT '领取及审计事务提交前的开始时间',
finished_at DATETIME(3) NULL COMMENT '尝试完成或租约过期恢复时间',
duration_ms BIGINT NULL COMMENT '尝试耗时毫秒',
outcome VARCHAR(24) NOT NULL COMMENT 'STARTED/APPLIED/DUPLICATE/STALE/RETRY/DEAD/LEASE_EXPIRED',
error_code VARCHAR(64) NULL COMMENT '有限错误类别或HTTP状态;不含载荷与令牌',
next_retry_at DATETIME(3) NULL COMMENT '本次失败后下次计划投递时间',
PRIMARY KEY (attempt_id),
KEY idx_delivery_attempt_entry (service_name,entry_id,started_at),
KEY idx_delivery_attempt_outcome (outcome,started_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='本服务业务事件逐次投递审计';
USE baiyue_finance;
CREATE TABLE event_delivery_attempt (
attempt_id VARCHAR(36) NOT NULL COMMENT '本次投递租约UUID和审计主键',
service_name VARCHAR(32) NOT NULL COMMENT '本库所属生产服务',
event_type VARCHAR(96) NOT NULL COMMENT '白名单业务事件类型',
entry_id VARCHAR(128) NOT NULL COMMENT '生产者发件箱主键',
event_id VARCHAR(128) NULL COMMENT '合法原事件UUID或ULID;格式错误时为空',
target_name VARCHAR(64) NOT NULL COMMENT '固定目标业务服务',
started_at DATETIME(3) NOT NULL COMMENT '领取及审计事务提交前的开始时间',
finished_at DATETIME(3) NULL COMMENT '尝试完成或租约过期恢复时间',
duration_ms BIGINT NULL COMMENT '尝试耗时毫秒',
outcome VARCHAR(24) NOT NULL COMMENT 'STARTED/APPLIED/DUPLICATE/STALE/RETRY/DEAD/LEASE_EXPIRED',
error_code VARCHAR(64) NULL COMMENT '有限错误类别或HTTP状态;不含载荷与令牌',
next_retry_at DATETIME(3) NULL COMMENT '本次失败后下次计划投递时间',
PRIMARY KEY (attempt_id),
KEY idx_delivery_attempt_entry (service_name,entry_id,started_at),
KEY idx_delivery_attempt_outcome (outcome,started_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='本服务业务事件逐次投递审计';
SELECT 'PASS' AS initialization_status, 'CN' AS region, 223 AS initialized_tables;
File diff suppressed because one or more lines are too long