优化
This commit is contained in:
@@ -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
|
||||
@@ -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执行仍由独立授权覆盖。
|
||||
@@ -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"
|
||||
|
||||
@@ -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,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
@@ -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,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
|
||||
|
||||
Vendored
+1
-1
@@ -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
|
||||
|
||||
Vendored
+54
@@ -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=
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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 "测试数据库初始化完成。"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 扣货权限合同一致;旧脚本重放时通过范围授权额外授予订单运营的那一条关联不进入新基线。
|
||||
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
+39
-116
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user