功能提交
This commit is contained in:
+16
-2
@@ -15,7 +15,7 @@
|
||||
| 内部资源锁定、释放、报废 | `controller/InternalWarehouseResourceLockController.java` |
|
||||
| 仓库与资源服务 | `service/WarehouseApplicationService.java`、`WarehouseResourceService.java`、`WarehouseResourceLockService.java` |
|
||||
| 内部服务令牌 | `security/InternalServiceTokenGuard.java` |
|
||||
| 数据库 | `src/main/resources/db/warehouse.sql` |
|
||||
| 数据库基线与中文注释契约 | `src/main/resources/db/warehouse.sql`、`src/test/java/com/baiyue/warehouse/schema/SchemaChineseCommentContractTest.java` |
|
||||
| 测试 | `src/test/java/com/baiyue/warehouse/` |
|
||||
|
||||
## 数据与安全规则
|
||||
@@ -24,8 +24,22 @@
|
||||
- 资源锁定以业务幂等键和 reservation 标识防重;释放/报废操作必须验证当前状态。
|
||||
- 内部资源接口必须校验服务令牌;普通用户不得直接调用。
|
||||
|
||||
## 数据库表索引与注释约束
|
||||
|
||||
| 业务域 | 表名 | 中文含义 |
|
||||
|---|---|---|
|
||||
| 库位 | `wh_location` | 海外仓库位资源 |
|
||||
| 托盘 | `wh_pallet` | 海外仓实物托盘资源 |
|
||||
| 资源锁 | `wh_resource_lock` | 仓储资源锁定记录 |
|
||||
| 仓库 | `wh_warehouse` | 海外仓档案 |
|
||||
| 仓库地址 | `wh_warehouse_address` | 海外仓地址 |
|
||||
| 服务能力 | `wh_service_capability` | 海外仓服务能力 |
|
||||
| 托盘绑定 | `wh_pallet_business_binding` | 托盘业务绑定关系 |
|
||||
|
||||
- 新增或修改表时,表 `COMMENT` 和每个普通字段的 `COMMENT` 必须填写准确的中文业务含义。
|
||||
- `SchemaChineseCommentContractTest` 自动扫描全部 `wh_` 表;新增字段必须同步更新基线 SQL、统一增量 SQL 和存量备注迁移。
|
||||
|
||||
## 修改与验证
|
||||
|
||||
- 字段、注释和索引同步维护基线 SQL 与统一增量 SQL。
|
||||
- 定向验证:`mvn -pl warehouse -am test`。
|
||||
|
||||
|
||||
@@ -48,25 +48,25 @@ CREATE TABLE IF NOT EXISTS wh_pallet (
|
||||
) ENGINE=InnoDB COMMENT='仓库托盘表:按租户和仓库维护托盘、容量、单价及状态';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS wh_resource_lock (
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT 'Snowflake resource lock ID',
|
||||
customer_id BIGINT UNSIGNED NOT NULL COMMENT 'Owning tenant ID',
|
||||
warehouse_id BIGINT UNSIGNED NOT NULL COMMENT 'Owning warehouse ID',
|
||||
pallet_id BIGINT UNSIGNED NOT NULL COMMENT 'Reserved pallet ID',
|
||||
location_id BIGINT UNSIGNED NOT NULL COMMENT 'Reserved location ID',
|
||||
business_type VARCHAR(50) NOT NULL COMMENT 'Calling business type',
|
||||
business_id VARCHAR(100) NOT NULL COMMENT 'Calling business identifier',
|
||||
reservation_key VARCHAR(160) NOT NULL COMMENT 'Caller supplied idempotency key',
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE' COMMENT 'ACTIVE, RELEASED or SCRAPPED',
|
||||
locked_at DATETIME(3) NOT NULL COMMENT 'Reservation time',
|
||||
released_at DATETIME(3) DEFAULT NULL COMMENT 'Release or scrap time',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '主键ID',
|
||||
customer_id BIGINT UNSIGNED NOT NULL COMMENT '客户租户ID',
|
||||
warehouse_id BIGINT UNSIGNED NOT NULL COMMENT '仓库ID',
|
||||
pallet_id BIGINT UNSIGNED NOT NULL COMMENT '托盘ID',
|
||||
location_id BIGINT UNSIGNED NOT NULL COMMENT '库位ID',
|
||||
business_type VARCHAR(50) NOT NULL COMMENT '业务类型',
|
||||
business_id VARCHAR(100) NOT NULL COMMENT '业务记录ID',
|
||||
reservation_key VARCHAR(160) NOT NULL COMMENT '资源预留幂等键',
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE' COMMENT '资源锁状态',
|
||||
locked_at DATETIME(3) NOT NULL COMMENT '锁定时间',
|
||||
released_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 (id),
|
||||
UNIQUE KEY uk_resource_lock_reservation (customer_id, reservation_key),
|
||||
KEY idx_resource_lock_pallet (customer_id, warehouse_id, pallet_id, status),
|
||||
KEY idx_resource_lock_location (customer_id, warehouse_id, location_id, status),
|
||||
KEY idx_resource_lock_business (customer_id, business_type, business_id)
|
||||
) ENGINE=InnoDB COMMENT='Atomic pallet and location reservation records';
|
||||
) ENGINE=InnoDB COMMENT='仓储资源锁定记录';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS wh_warehouse (
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '仓库ID(雪花ID)',
|
||||
@@ -160,12 +160,12 @@ ALTER TABLE wh_location
|
||||
MODIFY COLUMN version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS wh_pallet_business_binding (
|
||||
id BIGINT NOT NULL, customer_id BIGINT NOT NULL, warehouse_id BIGINT NOT NULL,
|
||||
business_type VARCHAR(32) NOT NULL, business_id VARCHAR(128) NOT NULL,
|
||||
pallet_id BIGINT NOT NULL, created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
id BIGINT NOT NULL COMMENT '主键ID', customer_id BIGINT NOT NULL COMMENT '客户租户ID', warehouse_id BIGINT NOT NULL COMMENT '仓库ID',
|
||||
business_type VARCHAR(32) NOT NULL COMMENT '业务类型', business_id VARCHAR(128) NOT NULL COMMENT '绑定业务记录ID',
|
||||
pallet_id BIGINT NOT NULL COMMENT '托盘ID', created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||
PRIMARY KEY (id), UNIQUE KEY uk_wh_pallet_binding (customer_id,warehouse_id,business_type,business_id),
|
||||
UNIQUE KEY uk_wh_pallet_binding_pallet (pallet_id)
|
||||
) ENGINE=InnoDB COMMENT='Customer business binding to real warehouse pallet';
|
||||
) ENGINE=InnoDB COMMENT='托盘业务绑定关系';
|
||||
|
||||
-- Every warehouse has one non-occupiable virtual location used for inventory history.
|
||||
INSERT INTO wh_location (id, customer_id, warehouse_id, zone_code, location_code, location_type,
|
||||
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
package com.baiyue.warehouse.schema;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class SchemaChineseCommentContractTest {
|
||||
|
||||
private static final Pattern TABLE = Pattern.compile(
|
||||
"(?is)CREATE\\s+TABLE\\s+IF\\s+NOT\\s+EXISTS\\s+(wh_[a-z0-9_]+)"
|
||||
+ "\\s*\\((.*?)\\)\\s*ENGINE\\s*=\\s*InnoDB\\s+COMMENT\\s*=\\s*'([^']*)'\\s*;");
|
||||
private static final Pattern CHINESE = Pattern.compile("[\\u4e00-\\u9fff]");
|
||||
|
||||
@Test
|
||||
void allWarehouseTablesAndColumnsHaveChineseCommentsAndMigrationCoverage() throws Exception {
|
||||
String baseline = Files.readString(
|
||||
Path.of("src", "main", "resources", "db", "warehouse.sql"), StandardCharsets.UTF_8);
|
||||
String incremental = Files.readString(
|
||||
Path.of("..", "scripts", "db", "incremental.sql"), StandardCharsets.UTF_8);
|
||||
|
||||
List<TableDefinition> tables = parseAndAssert(baseline, "仓储基线 SQL");
|
||||
assertEquals(7, tables.size(), "仓储表数量变化时必须同步表索引和备注迁移");
|
||||
parseAndAssert(incremental, "统一增量 SQL");
|
||||
|
||||
int marker = incremental.indexOf("2026-08-18 / 025 Chinese table and column comments");
|
||||
assertTrue(marker >= 0, "统一增量 SQL 缺少 025 中文备注迁移");
|
||||
String migration = incremental.substring(marker);
|
||||
for (TableDefinition table : tables) {
|
||||
assertTrue(migration.contains("table_name='" + table.name() + "'"),
|
||||
"025 备注迁移缺少表: " + table.name());
|
||||
for (String column : table.columns()) {
|
||||
assertTrue(migration.contains("table_name='" + table.name()
|
||||
+ "' AND column_name='" + column + "'"),
|
||||
"025 备注迁移缺少字段: " + table.name() + "." + column);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static List<TableDefinition> parseAndAssert(String sql, String source) {
|
||||
List<TableDefinition> tables = new ArrayList<>();
|
||||
Matcher matcher = TABLE.matcher(sql);
|
||||
while (matcher.find()) {
|
||||
String table = matcher.group(1);
|
||||
assertTrue(CHINESE.matcher(matcher.group(3)).find(), source + " 表备注必须包含中文: " + table);
|
||||
List<String> columns = new ArrayList<>();
|
||||
for (String definition : splitTopLevel(matcher.group(2))) {
|
||||
String normalized = definition.stripLeading().toUpperCase();
|
||||
if (normalized.matches("^(PRIMARY|UNIQUE|KEY|CONSTRAINT|FOREIGN|CHECK)\\b.*")) {
|
||||
continue;
|
||||
}
|
||||
String column = definition.stripLeading().split("\\s+", 2)[0].replace("`", "");
|
||||
columns.add(column);
|
||||
assertTrue(Pattern.compile("(?is)\\bCOMMENT\\s+'[^']*[\\u4e00-\\u9fff][^']*'")
|
||||
.matcher(definition).find(),
|
||||
source + " 字段备注必须包含中文: " + table + "." + column);
|
||||
}
|
||||
assertFalse(columns.isEmpty(), source + " 表定义没有字段: " + table);
|
||||
tables.add(new TableDefinition(table, columns));
|
||||
}
|
||||
assertFalse(tables.isEmpty(), source + " 未解析到仓储表");
|
||||
return tables;
|
||||
}
|
||||
|
||||
private static List<String> splitTopLevel(String body) {
|
||||
List<String> definitions = new ArrayList<>();
|
||||
int start = 0;
|
||||
int parentheses = 0;
|
||||
boolean quoted = false;
|
||||
for (int i = 0; i < body.length(); i++) {
|
||||
char current = body.charAt(i);
|
||||
if (current == '\'' && (i == 0 || body.charAt(i - 1) != '\\')) {
|
||||
quoted = !quoted;
|
||||
} else if (!quoted && current == '(') {
|
||||
parentheses++;
|
||||
} else if (!quoted && current == ')') {
|
||||
parentheses--;
|
||||
} else if (!quoted && parentheses == 0 && current == ',') {
|
||||
definitions.add(body.substring(start, i).trim());
|
||||
start = i + 1;
|
||||
}
|
||||
}
|
||||
definitions.add(body.substring(start).trim());
|
||||
return definitions.stream().filter(value -> !value.isEmpty()).toList();
|
||||
}
|
||||
|
||||
private record TableDefinition(String name, List<String> columns) {
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user