This commit is contained in:
2026-08-26 19:46:14 +08:00
parent 63182a2b88
commit 3975759458
22 changed files with 446 additions and 75 deletions
+11
View File
@@ -10,8 +10,19 @@
powershell -ExecutionPolicy Bypass -File D:\baiyue-firstleg\scripts\publish-local-nacos-config.ps1
```
也可以使用统一入口;`local` 会被强制限制在 `public` namespace:
```powershell
powershell -ExecutionPolicy Bypass -File D:\baiyue-firstleg\scripts\publish-nacos-config.ps1 -Environment local
```
配置发布成功后,各服务仍由自身的 Nacos Discovery 配置在启动时自动注册,不需要手工创建服务实例。
本地海外链路也发布在同一 `public / BAIYUE` 作用域,包括
`gateway-overseas.yml`、`lastleg-service.yml` 和
`warehouse-overseas-service.yml`。这些配置默认通过 `127.0.0.1` 连接本机
Auth、国内仓库、文件和海外服务;需要跨主机运行时使用对应环境变量覆盖。
## MyBatis SQL 日志
本地公共配置使用 MyBatis `Slf4jImpl`,数据库服务的 Mapper 日志级别默认为 `DEBUG`。执行数据库操作时,控制台会打印 SQL 模板、参数和影响行数。
@@ -5,5 +5,7 @@ spring.datasource:
password: ${EVENT_BRIDGE_DB_PASSWORD:123456}
baiyue:
region: ${REGION_CODE:CN}
event-bridge:
accept-any-region: ${EVENT_BRIDGE_ACCEPT_ANY_REGION:true}
deployment-cell: ${DEPLOYMENT_CELL:local-cn}
data-residency: ${DATA_RESIDENCY:CN}
+16
View File
@@ -0,0 +1,16 @@
springdoc:
api-docs.enabled: ${SWAGGER_ENABLED:true}
swagger-ui.enabled: ${SWAGGER_ENABLED:true}
baiyue:
security:
issuer: ${JWT_ISSUER:auth-service}
audience: ${JWT_AUDIENCE:lastleg-api}
jwk-set-uri: ${JWT_JWK_SET_URI:http://127.0.0.1:9100/api/auth/.well-known/jwks.json}
pda-session-validation:
auth-url: ${AUTH_SESSION_VALIDATION_URL:http://127.0.0.1:9100}
internal-service-token: ${INTERNAL_SERVICE_TOKEN:phase1-local-internal-token-at-least-32-bytes}
timeout: ${PDA_SESSION_VALIDATION_TIMEOUT:2s}
overseas-projection:
service-url: ${LASTLEG_SECURITY_SERVICE_URL:http://127.0.0.1:9303}
internal-service-token: ${INTERNAL_SERVICE_TOKEN:phase1-local-internal-token-at-least-32-bytes}
timeout: ${LASTLEG_SECURITY_VALIDATION_TIMEOUT:2s}
+30
View File
@@ -0,0 +1,30 @@
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: ${LASTLEG_DB_URL:jdbc:mysql://127.0.0.1:3306/baiyue_lastleg?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true}
username: ${LASTLEG_DB_USERNAME:root}
password: ${LASTLEG_DB_PASSWORD:123456}
lastleg:
dispatch-query-api.enabled: ${LASTLEG_DISPATCH_QUERY_API_ENABLED:true}
smart-dispatch-api.enabled: ${LASTLEG_SMART_DISPATCH_API_ENABLED:true}
future-outbound-api.enabled: ${LASTLEG_FUTURE_OUTBOUND_API_ENABLED:true}
baiyue:
clients:
domestic-warehouse:
url: ${DOMESTIC_WAREHOUSE_URL:http://127.0.0.1:9205}
cache-ttl: ${DOMESTIC_WAREHOUSE_CACHE_TTL:30m}
file:
url: ${FILE_SERVICE_URL:http://127.0.0.1:9206}
event-bridge:
url: ${CN_EVENT_BRIDGE_URL:${FIRSTLEG_EVENT_BRIDGE_URL:http://127.0.0.1:9312}}
warehouse:
base-url: ${WAREHOUSE_OVERSEAS_SERVICE_URL:http://127.0.0.1:9305}
firstleg:
event-bridge-url: ${CN_EVENT_BRIDGE_URL:${FIRSTLEG_EVENT_BRIDGE_URL:http://127.0.0.1:9312}}
security:
internal-service-token: ${INTERNAL_SERVICE_TOKEN:phase1-local-internal-token-at-least-32-bytes}
jwt-mode: jwks
session-mode: claims
issuer: ${JWT_ISSUER:auth-service}
audience: ${JWT_AUDIENCE:lastleg-api}
jwk-set-uri: ${JWT_JWK_SET_URI:http://127.0.0.1:9100/api/auth/.well-known/jwks.json}
@@ -0,0 +1,19 @@
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: ${WAREHOUSE_DB_URL:jdbc:mysql://127.0.0.1:3306/baiyue_lastlag_warehouse?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true}
username: ${WAREHOUSE_DB_USERNAME:root}
password: ${WAREHOUSE_DB_PASSWORD:123456}
baiyue:
clients:
domestic-warehouse:
url: ${DOMESTIC_WAREHOUSE_URL:http://127.0.0.1:9205}
cache-ttl: ${DOMESTIC_WAREHOUSE_CACHE_TTL:30m}
internal-service:
token: ${INTERNAL_SERVICE_TOKEN:phase1-local-internal-token-at-least-32-bytes}
security:
jwt-mode: jwks
session-mode: claims
issuer: ${JWT_ISSUER:auth-service}
audience: ${JWT_AUDIENCE:lastleg-api}
jwk-set-uri: ${JWT_JWK_SET_URI:http://127.0.0.1:9100/api/auth/.well-known/jwks.json}
+15 -1
View File
@@ -1,5 +1,19 @@
# 海外 Nacos 配置
将本目录文件按原文件名作为 Data ID 发布到 `prod-overseas` 命名空间和 `BAIYUE` 分组。配置只引用环境变量,不保存密码或访问密钥。Gateway 必须同时启用 `overseas` Profile。
本目录是海外远程部署模板,由 dev/development、test 和 production 共用。配置只引用环境变量,不保存密码或访问密钥;三个环境必须发布到各自独立的 Nacos namespace,Gateway 必须同时启用 `overseas` Profile。
海外入口与仓储 Data ID 分别为 `gateway-overseas.yml`、`warehouse-overseas-service.yml`。`print-service.yml` 对应头程仓库保留的共用打印服务,不包含尾程旧 Print 模块。
发布示例:
```powershell
# 开发环境
powershell -ExecutionPolicy Bypass -File D:\baiyue-firstleg\scripts\publish-nacos-config.ps1 `
-Environment development -Region overseas -ServerAddr <dev-nacos> -Namespace <dev-overseas-namespace>
# 测试环境
powershell -ExecutionPolicy Bypass -File D:\baiyue-firstleg\scripts\publish-nacos-config.ps1 `
-Environment test -Region overseas -ServerAddr <test-nacos> -Namespace <test-overseas-namespace>
```
远程环境禁止使用 `public` namespace。数据库、Redis、JWKS、内部服务令牌和跨区服务地址继续由对应 Jenkins Secret file 提供;不要把真实值写入本目录。
@@ -16459,6 +16459,10 @@ WHERE role.tenant_id=0 AND role.app_code='PLATFORM'
CREATE DATABASE IF NOT EXISTS baiyue_lastleg
DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
USE baiyue_lastleg;
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_order' AND column_name='source_warehouse_id'), 'SELECT 1', 'ALTER TABLE ll_order ADD COLUMN source_warehouse_id VARCHAR(128) NULL COMMENT ''来源国内装柜仓 ID 快照'' AFTER customer_name'));
PREPARE lastleg_order_source_warehouse_stmt FROM @ddl; EXECUTE lastleg_order_source_warehouse_stmt; DEALLOCATE PREPARE lastleg_order_source_warehouse_stmt;
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_order' AND column_name='source_warehouse_name'), 'SELECT 1', 'ALTER TABLE ll_order ADD COLUMN source_warehouse_name VARCHAR(255) NULL COMMENT ''来源国内装柜仓名称快照'' AFTER source_warehouse_id'));
PREPARE lastleg_order_source_warehouse_stmt FROM @ddl; EXECUTE lastleg_order_source_warehouse_stmt; DEALLOCATE PREPARE lastleg_order_source_warehouse_stmt;
CREATE TABLE IF NOT EXISTS ll_dispatch_pallet_lock (
id BIGINT NOT NULL COMMENT '主键ID',
customer_id VARCHAR(128) NOT NULL COMMENT '客户租户ID',
+49 -53
View File
@@ -1,66 +1,62 @@
# 数据库环境切换说明
# Nacos 环境切换说明
## 环境定义
数据库、Redis 和服务地址均由所选环境的 Nacos 配置决定。本地启动时只切换
Nacos 地址与 namespace,不再通过 `application-test.yml` 单独切换数据库。
- `dev`:本地开发环境,连接 `127.0.0.1:3306`,默认账号为 `root/123456`。
- `test`:联调测试环境,连接 `8.163.30.128:3306`,各微服务仍使用独立数据库。
## 环境映射
所有服务默认使用 `dev`:
| Maven Profile | Nacos 地址 | Namespace |
| --- | --- | --- |
| `local` | `127.0.0.1:8848` | `public` |
| `dev` | `dev-nacos.buggoglobal.com:8848` | `DEV_NACOS_NAMESPACE` |
| `test` | `8.163.30.128:8848` | `d99d7998-5bb0-4ffe-a2a0-1a564ce14463` |
```yaml
spring:
profiles:
active: ${SPRING_PROFILES_ACTIVE:dev}
本地 Nacos 使用 `nacos/nacos`。开发和测试密码不写入 Git,需在启动 IDEA
之前设置一次用户环境变量:
```powershell
[Environment]::SetEnvironmentVariable('DEV_NACOS_PASSWORD', '<开发 Nacos 密码>', 'User')
[Environment]::SetEnvironmentVariable('TEST_NACOS_PASSWORD', '<测试 Nacos 密码>', 'User')
[Environment]::SetEnvironmentVariable('DEV_NACOS_NAMESPACE', '<开发海外 namespace>', 'User')
```
建议不要直接修改配置文件。通过 IDEA 的启动配置设置 `Active profiles` 即可切换:
设置后完全退出并重新打开 IDEA,使 Maven 能读取新环境变量。
- 本地数据库:`dev`
- 测试数据库:`test`
## IDEA 中切换
也可以使用启动参数:
1. 在 Maven 面板的 `Profiles` 中,`local`、`dev`、`test` 只勾选一个。
2. 重新加载 Maven 项目。
3. 在目标服务的 Maven `Plugins > spring-boot > spring-boot:run` 启动。
```text
--spring.profiles.active=test
例如海外查询链路需要分别启动 `gateway-overseas`、`lastleg-service` 和
`warehouse-overseas-service`。已经运行的进程不会自动切换环境,切换 Profile
后必须停止并重新启动。
IDEA 普通绿色 Run 按钮不执行 Maven,因此不会读取 Maven Profile。需要继续使用
绿色 Run 时,请在 Run Configuration 的 Environment variables 中设置
`NACOS_SERVER_ADDR`、`NACOS_NAMESPACE`、`NACOS_USERNAME` 和 `NACOS_PASSWORD`。
`gateway-overseas` 已默认启用 `overseas` Spring Profile。
## PowerShell 中切换
也可以在当前终端加载同一套变量,然后从同一终端启动服务:
```powershell
. .\scripts\use-nacos-environment.ps1 -Environment local -Region overseas
. .\scripts\use-nacos-environment.ps1 -Environment dev -Region overseas
. .\scripts\use-nacos-environment.ps1 -Environment test -Region overseas
```
## 测试数据库密码
脚本读取 `DEV_NACOS_PASSWORD`、`TEST_NACOS_PASSWORD`,namespace 优先读取
`*_OVERSEAS_NACOS_NAMESPACE`,未设置时读取 `*_NACOS_NAMESPACE`。命令前面的点和
空格不能省略,否则变量只存在于脚本子进程中。
测试数据库密码不写入 Git。启动 `test` 环境前,在 IDEA 启动配置的环境变量中设置:
## 当前限制
```text
TEST_DB_PASSWORD=测试数据库密码
```
公共测试连接参数支持覆盖:
```text
TEST_DB_HOST=8.163.30.128
TEST_DB_PORT=3306
TEST_DB_USERNAME=root
TEST_DB_PASSWORD=测试数据库密码
```
每个服务还支持使用自己的 `*_TEST_DB_URL`、`*_TEST_DB_USERNAME` 和 `*_TEST_DB_PASSWORD` 覆盖公共参数。
## 分库映射
| 服务 | 测试数据库 |
| --- | --- |
| 认证服务 | `baiyue_auth` |
| 权限服务 | `baiyue_iam` |
| 订单服务 | `baiyue_order` |
| 仓库服务 | `baiyue_warehouse` |
| 头程履约服务 | `baiyue_firstleg` |
| 文件服务 | `baiyue_file` |
| 审计服务 | `baiyue_audit` |
| 事件桥接服务 | `baiyue_event_bridge` |
网关、打印和集成服务没有业务数据库,但同样支持 `dev/test` Profile。网关在两种环境下都会自动加载 `cn` 路由配置。
## 注意事项
- `test` 只切换数据库,Nacos 和 Redis 默认仍连接本机。
- 环境变量优先级高于配置文件,可用于临时覆盖单个服务。
- 修改 IDEA 环境变量后需要重启对应服务。
- 不要把测试数据库密码提交到配置文件、启动脚本或 Git。
- 测试 Nacos 的 `8848` 客户端端口已确认可连接。
- 开发 Nacos 网页控制台可访问,但 `dev-nacos.buggoglobal.com:8848` 当前从本机
无法连接。需要开放 Nacos 客户端端口以及 Nacos 2.x/3.x 所需的 gRPC 端口,
或提供一个客户端可访问的内部地址。
- 国内和海外存在同名 Data ID,远程环境必须使用各自独立 namespace。开发海外
namespace 尚未确认,因此通过 `DEV_NACOS_NAMESPACE` 在本机配置。
- 不要同时勾选多个环境 Profile;Maven 对同名属性的最终取值不直观,容易连错环境。
@@ -31,10 +31,14 @@ public class EventInboxService {
@Value("${baiyue.region:CN}")
private String localRegion;
/** Local single-bridge mode may carry both regional envelopes; production keeps strict routing. */
@Value("${baiyue.event-bridge.accept-any-region:false}")
private boolean acceptAnyRegion;
@Transactional
public EventReceiveResult receive(EventEnvelope envelope) {
envelope.requireValid();
if (!envelope.targetRegion().name().equalsIgnoreCase(localRegion)) {
if (!acceptAnyRegion && !envelope.targetRegion().name().equalsIgnoreCase(localRegion)) {
throw new IllegalArgumentException("事件目标区域与当前事件桥区域不一致");
}
if (!EventPayloadHash.matches(
@@ -29,7 +29,7 @@ spring:
namespace: ${NACOS_NAMESPACE:}
baiyue:
region: ${REGION_CODE:OVERSEAS}
region: ${REGION_CODE:CN}
event-dispatcher:
consumer-url: ${EVENT_CONSUMER_URL:${LASTLEG_CONSUMER_URL:http://127.0.0.1:9303/internal/lastleg/events}}
firstleg-consumer-url: ${FIRSTLEG_CONTAINER_EVENT_CONSUMER_URL:http://127.0.0.1:9202/internal/firstleg/v1/container-events}
@@ -125,6 +125,15 @@ class EventInboxServiceTest {
.hasMessageContaining("目标区域");
}
@Test
void 本地单实例模式允许接收不同目标区域事件() {
EventInboxService localService = service("CN", true);
EventEnvelope envelope = withHash(envelope("sha256:placeholder"));
assertThat(localService.receive(envelope).result()).isEqualTo("已接收");
verify(store).insert(any(BridgeInbox.class));
}
private EventEnvelope envelope(String hash) {
return new EventEnvelope("550e8400-e29b-41d4-a716-446655440000", "OrderApproved", 1,
Instant.parse("2026-07-29T08:30:00Z"), RegionCode.CN, RegionCode.OVERSEAS,
@@ -133,11 +142,16 @@ class EventInboxServiceTest {
}
private EventInboxService service(String region) {
return service(region, false);
}
private EventInboxService service(String region, boolean acceptAnyRegion) {
EventInboxService service = new EventInboxService();
ReflectionTestUtils.setField(service, "inboxStore", store);
ReflectionTestUtils.setField(service, "objectMapper", objectMapper);
ReflectionTestUtils.setField(service, "metrics", metrics);
ReflectionTestUtils.setField(service, "localRegion", region);
ReflectionTestUtils.setField(service, "acceptAnyRegion", acceptAnyRegion);
return service;
}
@@ -39,9 +39,9 @@ spring:
baiyue:
security:
issuer: ${JWT_ISSUER:https://auth.example.com}
issuer: ${JWT_ISSUER:auth-service}
audience: ${JWT_AUDIENCE:lastleg-api}
jwk-set-uri: ${JWT_JWK_SET_URI:https://auth.example.com/.well-known/jwks.json}
jwk-set-uri: ${JWT_JWK_SET_URI:http://127.0.0.1:9100/api/auth/.well-known/jwks.json}
pda-session-validation:
auth-url: ${AUTH_SESSION_VALIDATION_URL}
internal-service-token: ${INTERNAL_SERVICE_TOKEN}
@@ -9,6 +9,36 @@ import java.nio.file.Path;
import static org.assertj.core.api.Assertions.assertThat;
class OverseasSecurityValidationDeploymentContractTest {
@Test
void gatewayLocalJwtDefaultsMatchAuthAndLastleg() throws Exception {
String gatewayApplication = read("gateway-overseas/src/main/resources/application.yml");
String lastlegApplication = read("lastleg-service/src/main/resources/application.yml");
String localGateway = read("config/nacos/local/gateway-overseas.yml");
String localLastleg = read("config/nacos/local/lastleg-service.yml");
String localWarehouse = read("config/nacos/local/warehouse-overseas-service.yml");
String remoteGateway = read("config/nacos/overseas/gateway-overseas.yml");
String remoteLastleg = read("config/nacos/overseas/lastleg-service.yml");
String remoteWarehouse = read("config/nacos/overseas/warehouse-overseas-service.yml");
for (String source : new String[]{gatewayApplication, lastlegApplication, localGateway, localLastleg, localWarehouse}) {
assertThat(source)
.contains("${JWT_ISSUER:auth-service}")
.contains("${JWT_AUDIENCE:lastleg-api}")
.contains("${JWT_JWK_SET_URI:http://127.0.0.1:9100/api/auth/.well-known/jwks.json}");
}
assertThat(gatewayApplication).doesNotContain("auth.example.com");
assertThat(localGateway)
.contains("${AUTH_SESSION_VALIDATION_URL:http://127.0.0.1:9100}")
.contains("${LASTLEG_SECURITY_SERVICE_URL:http://127.0.0.1:9303}");
assertThat(localLastleg).contains("${WAREHOUSE_OVERSEAS_SERVICE_URL:http://127.0.0.1:9305}");
assertThat(localWarehouse).contains("${DOMESTIC_WAREHOUSE_URL:http://127.0.0.1:9205}");
assertThat(remoteGateway + remoteLastleg + remoteWarehouse)
.doesNotContain("127.0.0.1", "root", "123456");
assertThat(remoteGateway).contains("${LASTLEG_SECURITY_SERVICE_URL:http://lastleg-service:9303}");
assertThat(remoteLastleg).contains("${LASTLEG_DB_URL}", "${LASTLEG_DB_USERNAME}", "${LASTLEG_DB_PASSWORD}");
assertThat(remoteWarehouse).contains("${WAREHOUSE_DB_URL}", "${WAREHOUSE_DB_USERNAME}", "${WAREHOUSE_DB_PASSWORD}");
}
@Test
void gatewayAndEventBridgeUseLastlegSecurityEndpointsWithoutTheRetiredService() throws Exception {
String client = read("gateway-overseas/src/main/java/com/baiyue/gateway/filter/HttpOverseasSecurityValidationClient.java");
+1 -1
View File
@@ -24,7 +24,7 @@ spring:
encode: UTF-8
baiyue:
security-event:
event-bridge-url: ${SECURITY_EVENT_BRIDGE_URL:http://event-bridge-overseas:9312}
event-bridge-url: ${SECURITY_EVENT_BRIDGE_URL:http://127.0.0.1:9312}
customer-dimension-initialization:
fixed-delay-ms: ${CUSTOMER_DIMENSION_INITIALIZATION_FIXED_DELAY_MS:5000}
max-attempts: ${CUSTOMER_DIMENSION_INITIALIZATION_MAX_ATTEMPTS:12}
@@ -0,0 +1,9 @@
package com.baiyue.lastleg.customervisit;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@MapperScan("com.baiyue.lastleg.customervisit.mapper")
public class CustomerVisitMapperConfiguration {
}
@@ -0,0 +1,9 @@
package com.baiyue.lastleg.exceptionorder;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@MapperScan("com.baiyue.lastleg.exceptionorder.mapper")
public class ExceptionOrderMapperConfiguration {
}
@@ -244,6 +244,10 @@ SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE tab
PREPARE lastleg_order_container_booking_stmt FROM @ddl; EXECUTE lastleg_order_container_booking_stmt; DEALLOCATE PREPARE lastleg_order_container_booking_stmt;
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_order' AND column_name='destination_type'), 'SELECT 1', 'ALTER TABLE ll_order ADD COLUMN destination_type VARCHAR(32) NULL COMMENT ''目的类型:FBA按箱数接收、OVERSEAS逐箱扫码'' AFTER order_type'));
PREPARE lastleg_order_destination_type_stmt FROM @ddl; EXECUTE lastleg_order_destination_type_stmt; DEALLOCATE PREPARE lastleg_order_destination_type_stmt;
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_order' AND column_name='source_warehouse_id'), 'SELECT 1', 'ALTER TABLE ll_order ADD COLUMN source_warehouse_id VARCHAR(128) NULL COMMENT ''来源国内装柜仓 ID 快照'' AFTER customer_name'));
PREPARE lastleg_order_source_warehouse_stmt FROM @ddl; EXECUTE lastleg_order_source_warehouse_stmt; DEALLOCATE PREPARE lastleg_order_source_warehouse_stmt;
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_order' AND column_name='source_warehouse_name'), 'SELECT 1', 'ALTER TABLE ll_order ADD COLUMN source_warehouse_name VARCHAR(255) NULL COMMENT ''来源国内装柜仓名称快照'' AFTER source_warehouse_id'));
PREPARE lastleg_order_source_warehouse_stmt FROM @ddl; EXECUTE lastleg_order_source_warehouse_stmt; DEALLOCATE PREPARE lastleg_order_source_warehouse_stmt;
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_inbound_order' AND column_name='destination_type'), 'SELECT 1', 'ALTER TABLE ll_inbound_order ADD COLUMN destination_type VARCHAR(32) NULL COMMENT ''目的类型:FBA按箱数接收、OVERSEAS逐箱扫码'' AFTER destination_warehouse_name'));
PREPARE lastleg_inbound_destination_type_stmt FROM @ddl; EXECUTE lastleg_inbound_destination_type_stmt; DEALLOCATE PREPARE lastleg_inbound_destination_type_stmt;
ALTER TABLE ll_order_detail COMMENT='尾程订单实装商品冻结明细';
@@ -524,7 +528,6 @@ ALTER TABLE ll_inbound_plan_line
MODIFY COLUMN plan_id BIGINT NOT NULL COMMENT '入库计划ID',
MODIFY COLUMN order_id BIGINT NOT NULL COMMENT '尾程订单ID',
MODIFY COLUMN inbound_code VARCHAR(128) NOT NULL COMMENT '头程入仓码快照',
MODIFY COLUMN destination_type VARCHAR(32) NOT NULL DEFAULT 'OVERSEAS' COMMENT '目的类型:FBA按箱数接收、OVERSEAS逐箱扫码',
MODIFY COLUMN destination_warehouse_id VARCHAR(128) NULL COMMENT '目的仓ID快照',
MODIFY COLUMN destination_warehouse_code VARCHAR(128) NULL COMMENT '目的仓编码快照',
MODIFY COLUMN destination_warehouse_name VARCHAR(255) NULL COMMENT '目的仓名称快照',
@@ -726,19 +729,17 @@ UPDATE ll_inbound_plan_pallet_allocation a JOIN ll_inbound_plan p ON p.id=a.plan
ALTER TABLE ll_inbound_plan_pallet_allocation MODIFY COLUMN customer_id VARCHAR(128) NOT NULL COMMENT '客户账号标识';
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.statistics WHERE table_schema=DATABASE() AND table_name='ll_inbound_plan_pallet_allocation' AND index_name='idx_ll_inbound_allocation_customer_plan'), 'SELECT 1', 'ALTER TABLE ll_inbound_plan_pallet_allocation ADD KEY idx_ll_inbound_allocation_customer_plan(customer_id,plan_id)'));
PREPARE inbound_customer_scope_stmt FROM @ddl; EXECUTE inbound_customer_scope_stmt; DEALLOCATE PREPARE inbound_customer_scope_stmt;
DROP PROCEDURE IF EXISTS assert_inbound_plan_customer_scope_ready;
DELIMITER $$
CREATE PROCEDURE assert_inbound_plan_customer_scope_ready()
BEGIN
IF EXISTS (SELECT 1 FROM ll_inbound_plan_line l LEFT JOIN ll_inbound_plan p ON p.id=l.plan_id WHERE l.customer_id IS NULL OR l.customer_id='' OR p.id IS NULL OR l.customer_id<>p.customer_id)
OR EXISTS (SELECT 1 FROM ll_inbound_plan_pallet v LEFT JOIN ll_inbound_plan p ON p.id=v.plan_id WHERE v.customer_id IS NULL OR v.customer_id='' OR p.id IS NULL OR v.customer_id<>p.customer_id)
OR EXISTS (SELECT 1 FROM ll_inbound_plan_pallet_allocation a LEFT JOIN ll_inbound_plan p ON p.id=a.plan_id WHERE a.customer_id IS NULL OR a.customer_id='' OR p.id IS NULL OR a.customer_id<>p.customer_id) THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='Inbound plan child customer scope migration is incomplete';
END IF;
END$$
DELIMITER ;
CALL assert_inbound_plan_customer_scope_ready();
DROP PROCEDURE assert_inbound_plan_customer_scope_ready;
DROP TEMPORARY TABLE IF EXISTS inbound_plan_customer_scope_assertion;
CREATE TEMPORARY TABLE inbound_plan_customer_scope_assertion (
is_valid TINYINT NOT NULL,
CONSTRAINT chk_inbound_plan_customer_scope_ready CHECK (is_valid=1)
);
INSERT INTO inbound_plan_customer_scope_assertion(is_valid)
SELECT 0
WHERE EXISTS (SELECT 1 FROM ll_inbound_plan_line l LEFT JOIN ll_inbound_plan p ON p.id=l.plan_id WHERE l.customer_id IS NULL OR l.customer_id='' OR p.id IS NULL OR l.customer_id<>p.customer_id)
OR EXISTS (SELECT 1 FROM ll_inbound_plan_pallet v LEFT JOIN ll_inbound_plan p ON p.id=v.plan_id WHERE v.customer_id IS NULL OR v.customer_id='' OR p.id IS NULL OR v.customer_id<>p.customer_id)
OR EXISTS (SELECT 1 FROM ll_inbound_plan_pallet_allocation a LEFT JOIN ll_inbound_plan p ON p.id=a.plan_id WHERE a.customer_id IS NULL OR a.customer_id='' OR p.id IS NULL OR a.customer_id<>p.customer_id);
DROP TEMPORARY TABLE inbound_plan_customer_scope_assertion;
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_inbound_plan_line' AND column_name='destination_type'), 'ALTER TABLE ll_inbound_plan_line MODIFY COLUMN destination_type VARCHAR(32) NOT NULL DEFAULT ''OVERSEAS'' COMMENT ''目的类型:FBA按箱数接收、OVERSEAS逐箱扫码''', 'ALTER TABLE ll_inbound_plan_line ADD COLUMN destination_type VARCHAR(32) NOT NULL DEFAULT ''OVERSEAS'' COMMENT ''目的类型:FBA按箱数接收、OVERSEAS逐箱扫码'' AFTER inbound_code'));
PREPARE receiving_container_line_stmt FROM @ddl; EXECUTE receiving_container_line_stmt; DEALLOCATE PREPARE receiving_container_line_stmt;
UPDATE ll_inbound_plan_line l JOIN ll_order o ON o.id=l.order_id
@@ -1191,7 +1192,7 @@ CREATE TABLE IF NOT EXISTS ll_outbound_pda_pick (
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_outbound_order' AND column_name='outbound_type'), 'SELECT 1', 'ALTER TABLE ll_outbound_order ADD COLUMN outbound_type VARCHAR(32) NOT NULL DEFAULT ''DISPATCH_DELIVERY'' COMMENT ''出库类型'' AFTER status'));
PREPARE outbound_upgrade_stmt FROM @ddl; EXECUTE outbound_upgrade_stmt; DEALLOCATE PREPARE outbound_upgrade_stmt;
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_outbound_pallet_item' AND column_name='sku_code'), 'SELECT 1', 'ALTER TABLE ll_outbound_pallet_item ADD COLUMN sku_code VARCHAR(128) NULL COMMENT ''一件代发SKU'' AFTER status'));
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_outbound_pallet_item' AND column_name='sku_code'), 'SELECT 1', 'ALTER TABLE ll_outbound_pallet_item ADD COLUMN sku_code VARCHAR(128) NULL COMMENT ''一件代发SKU'''));
PREPARE outbound_upgrade_stmt FROM @ddl; EXECUTE outbound_upgrade_stmt; DEALLOCATE PREPARE outbound_upgrade_stmt;
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_outbound_order' AND column_name='recipient_info'), 'SELECT 1', 'ALTER TABLE ll_outbound_order ADD COLUMN recipient_info VARCHAR(1000) NULL COMMENT ''一件代发收件信息'' AFTER outbound_type'));
PREPARE outbound_upgrade_stmt FROM @ddl; EXECUTE outbound_upgrade_stmt; DEALLOCATE PREPARE outbound_upgrade_stmt;
+44 -1
View File
@@ -69,6 +69,10 @@
<spotbugs-maven-plugin.version>4.9.8.5</spotbugs-maven-plugin.version>
<archunit.version>1.5.0</archunit.version>
<rocketmq.spring.version>2.3.3</rocketmq.spring.version>
<nacos.server-addr>127.0.0.1:8848</nacos.server-addr>
<nacos.namespace>public</nacos.namespace>
<nacos.username>nacos</nacos.username>
<nacos.password>nacos</nacos.password>
</properties>
<dependencies>
@@ -128,7 +132,19 @@
<build>
<pluginManagement>
<plugins>
<plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<systemPropertyVariables>
<NACOS_SERVER_ADDR>${nacos.server-addr}</NACOS_SERVER_ADDR>
<NACOS_NAMESPACE>${nacos.namespace}</NACOS_NAMESPACE>
<NACOS_GROUP>BAIYUE</NACOS_GROUP>
<NACOS_USERNAME>${nacos.username}</NACOS_USERNAME>
<NACOS_PASSWORD>${nacos.password}</NACOS_PASSWORD>
</systemPropertyVariables>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId>
<version>${maven.compiler.plugin.version}</version>
@@ -170,6 +186,33 @@
</plugins>
</build>
<profiles>
<profile>
<id>local</id>
<properties>
<nacos.server-addr>127.0.0.1:8848</nacos.server-addr>
<nacos.namespace>public</nacos.namespace>
<nacos.username>nacos</nacos.username>
<nacos.password>nacos</nacos.password>
</properties>
</profile>
<profile>
<id>dev</id>
<properties>
<nacos.server-addr>dev-nacos.buggoglobal.com:8848</nacos.server-addr>
<nacos.namespace>${env.DEV_NACOS_NAMESPACE}</nacos.namespace>
<nacos.username>nacos</nacos.username>
<nacos.password>${env.DEV_NACOS_PASSWORD}</nacos.password>
</properties>
</profile>
<profile>
<id>test</id>
<properties>
<nacos.server-addr>8.163.30.128:8848</nacos.server-addr>
<nacos.namespace>d99d7998-5bb0-4ffe-a2a0-1a564ce14463</nacos.namespace>
<nacos.username>nacos</nacos.username>
<nacos.password>${env.TEST_NACOS_PASSWORD}</nacos.password>
</properties>
</profile>
<profile>
<id>static-analysis-report</id>
<build>
@@ -5,6 +5,29 @@ import test from 'node:test'
const read = async (path) => (await readFile(new URL(`../${path}`, import.meta.url), 'utf8'))
.replaceAll('\r\n', '\n')
test('local, development and test Nacos publishing remain environment-isolated', async () => {
const publisher = await read('scripts/publish-nacos-config.ps1')
const local = [
await read('config/nacos/local/gateway-overseas.yml'),
await read('config/nacos/local/lastleg-service.yml'),
await read('config/nacos/local/warehouse-overseas-service.yml'),
].join('\n')
const remote = [
await read('config/nacos/overseas/gateway-overseas.yml'),
await read('config/nacos/overseas/lastleg-service.yml'),
await read('config/nacos/overseas/warehouse-overseas-service.yml'),
].join('\n')
assert.match(publisher, /ValidateSet\('local', 'dev', 'development', 'test', 'production'\)/)
assert.match(publisher, /必须显式指定非 public 的 Nacos namespace/)
assert.match(local, /127\.0\.0\.1:9100/)
assert.match(local, /127\.0\.0\.1:9303/)
assert.doesNotMatch(remote, /127\.0\.0\.1|password:\s*123456/)
assert.match(remote, /http:\/\/lastleg-service:9303/)
assert.match(remote, /\$\{LASTLEG_DB_URL\}/)
assert.match(remote, /\$\{WAREHOUSE_DB_URL\}/)
})
test('Jenkins pipelines recognize all overseas deployable modules', async () => {
const controller = await read('Jenkinsfile')
const service = await read('Jenkinsfile.service')
+68
View File
@@ -0,0 +1,68 @@
param(
[ValidateSet('local', 'dev', 'development', 'test', 'production')]
[string]$Environment,
[ValidateSet('cn', 'overseas')]
[string]$Region = 'cn',
[string]$ServerAddr = '127.0.0.1:8848',
[string]$Group = 'BAIYUE',
[string]$Namespace
)
$ErrorActionPreference = 'Stop'
$projectRoot = Split-Path -Parent (Split-Path -Parent $MyInvocation.MyCommand.Path)
if ($Environment -eq 'local') {
if ($Namespace -and $Namespace -ne 'public') {
throw 'local 环境只能发布到 public namespace'
}
$Namespace = 'public'
$configProfile = 'local'
} else {
if (-not $Namespace -or $Namespace -eq 'public') {
throw "$Environment/$Region 必须显式指定非 public 的 Nacos namespace"
}
$configProfile = $Region
}
$configRoot = (Resolve-Path (Join-Path $projectRoot "config\nacos\$configProfile")).Path
$healthUrl = "http://$ServerAddr/nacos/v1/console/health/readiness"
$publishUrl = "http://$ServerAddr/nacos/v1/cs/configs"
$health = Invoke-WebRequest -UseBasicParsing -Uri $healthUrl -TimeoutSec 5
if ($health.StatusCode -ne 200 -or $health.Content.Trim() -ne 'OK') {
throw "Nacos not ready: $healthUrl"
}
$files = Get-ChildItem -LiteralPath $configRoot -File -Filter '*.yml' | Sort-Object Name
if ($files.Count -eq 0) {
throw "No Nacos configuration found: $configRoot"
}
$published = 0
foreach ($file in $files) {
$resolvedFile = (Resolve-Path -LiteralPath $file.FullName).Path
if (-not $resolvedFile.StartsWith($configRoot, [System.StringComparison]::OrdinalIgnoreCase)) {
throw "Unexpected config path: $resolvedFile"
}
$body = @{
dataId = $file.Name
group = $Group
content = Get-Content -LiteralPath $resolvedFile -Raw -Encoding UTF8
type = 'yaml'
}
if ($Namespace -ne 'public') {
$body.tenant = $Namespace
}
$pairs = foreach ($entry in $body.GetEnumerator()) {
[uri]::EscapeDataString([string]$entry.Key) + '=' + [uri]::EscapeDataString([string]$entry.Value)
}
$result = Invoke-RestMethod -Method Post -Uri $publishUrl `
-ContentType 'application/x-www-form-urlencoded; charset=UTF-8' `
-Body ($pairs -join '&') -TimeoutSec 10
if ([string]$result -ne 'true') {
throw "Publish failed: $($file.Name), Nacos response: $result"
}
$published++
Write-Host "Published $($file.Name) -> $Namespace / $Group"
}
Write-Host "Published $Environment/$Region Nacos configuration: $published items."
+74
View File
@@ -0,0 +1,74 @@
param(
[ValidateSet('local', 'dev', 'development', 'test')]
[string]$Environment,
[ValidateSet('cn', 'overseas')]
[string]$Region = 'cn',
[string]$NacosServerAddr,
[string]$NacosNamespace,
[string]$NacosUsername,
[string]$NacosPassword
)
$ErrorActionPreference = 'Stop'
$target = if ($Environment -eq 'development') { 'dev' } else { $Environment }
$defaults = @{
local = @{ Server = '127.0.0.1:8848'; CnNamespace = 'public' }
dev = @{ Server = '8.163.49.32:8848'; CnNamespace = $env:DEV_NACOS_NAMESPACE }
test = @{ Server = '8.163.30.128:8848'; CnNamespace = 'd99d7998-5bb0-4ffe-a2a0-1a564ce14463' }
}
$selected = $defaults[$target]
$serverAddr = if ($NacosServerAddr) { $NacosServerAddr } else { $selected.Server }
$namespace = if ($NacosNamespace) {
$NacosNamespace
} elseif ($target -eq 'local') {
'public'
} elseif ($Region -eq 'overseas') {
$regionNamespace = [Environment]::GetEnvironmentVariable("$($target.ToUpperInvariant())_OVERSEAS_NACOS_NAMESPACE")
if ($regionNamespace) {
$regionNamespace
} else {
[Environment]::GetEnvironmentVariable("$($target.ToUpperInvariant())_NACOS_NAMESPACE")
}
} else {
$selected.CnNamespace
}
$username = if ($NacosUsername) {
$NacosUsername
} elseif ([Environment]::GetEnvironmentVariable("$($target.ToUpperInvariant())_NACOS_USERNAME")) {
[Environment]::GetEnvironmentVariable("$($target.ToUpperInvariant())_NACOS_USERNAME")
} else {
'nacos'
}
$password = if ($NacosPassword) {
$NacosPassword
} elseif ([Environment]::GetEnvironmentVariable("$($target.ToUpperInvariant())_NACOS_PASSWORD")) {
[Environment]::GetEnvironmentVariable("$($target.ToUpperInvariant())_NACOS_PASSWORD")
} elseif ($target -eq 'local') {
'nacos'
} else {
$null
}
if (-not $namespace) {
throw "$target/$Region 必须通过 -NacosNamespace 或对应的 *_NACOS_NAMESPACE 环境变量指定 namespace"
}
if (-not $password) {
throw "$target 必须通过 -NacosPassword 或 $($target.ToUpperInvariant())_NACOS_PASSWORD 提供 Nacos 密码"
}
$env:NACOS_SERVER_ADDR = $serverAddr
$env:NACOS_NAMESPACE = $namespace
$env:NACOS_GROUP = 'BAIYUE'
$env:NACOS_USERNAME = $username
$env:NACOS_PASSWORD = $password
$env:SPRING_CLOUD_NACOS_CONFIG_NAMESPACE = $namespace
$env:SPRING_CLOUD_NACOS_DISCOVERY_NAMESPACE = $namespace
$env:SPRING_PROFILES_ACTIVE = if ($Region -eq 'overseas') { 'overseas' } elseif ($target -eq 'local') { 'dev' } else { 'cn' }
$env:BAIYUE_RUNTIME_ENVIRONMENT = $target
$env:BAIYUE_RUNTIME_REGION = $Region
Write-Host "Runtime environment: $target / $Region"
Write-Host "Nacos: $serverAddr; namespace: $namespace; group: BAIYUE"
Write-Host "Spring profiles: $env:SPRING_PROFILES_ACTIVE"
Write-Host 'Nacos password is loaded into the current process and is not printed.'
+4
View File
@@ -44,6 +44,10 @@
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>