diff --git a/common-agent-boot/src/test/java/com/bruce/integration/schema/SqlEntityMappingContractTests.java b/common-agent-boot/src/test/java/com/bruce/integration/schema/SqlEntityMappingContractTests.java
new file mode 100644
index 0000000..2eb4ba9
--- /dev/null
+++ b/common-agent-boot/src/test/java/com/bruce/integration/schema/SqlEntityMappingContractTests.java
@@ -0,0 +1,313 @@
+package com.bruce.integration.schema;
+
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.bruce.agent.entity.AgentCapabilityBinding;
+import com.bruce.agent.entity.AgentDefinition;
+import com.bruce.agent.entity.AgentMessage;
+import com.bruce.agent.entity.AgentSession;
+import com.bruce.common.domain.entity.SysAttachment;
+import com.bruce.common.domain.entity.SysEnum;
+import com.bruce.common.domain.model.BaseEntity;
+import com.bruce.mcp.entity.McpCapability;
+import com.bruce.mcp.entity.McpServer;
+import com.bruce.modelprovider.entity.ModelCallLog;
+import com.bruce.modelprovider.entity.ModelConfig;
+import com.bruce.modelprovider.entity.ModelProvider;
+import com.bruce.modelprovider.entity.ModelRouteRule;
+import com.bruce.modelprovider.entity.RagStoreModelConfig;
+import com.bruce.rag.entity.RagChunk;
+import com.bruce.rag.entity.RagChunkEmbedding;
+import com.bruce.rag.entity.RagDocument;
+import com.bruce.rag.entity.RagDocumentParseResult;
+import com.bruce.rag.entity.RagStore;
+import com.bruce.skill.entity.SkillDefinition;
+import com.bruce.skill.entity.SkillVersion;
+import com.bruce.workflow.entity.StudioProject;
+import com.bruce.workflow.entity.WorkflowDefinition;
+import com.bruce.workflow.entity.WorkflowRun;
+import com.bruce.workflow.entity.WorkflowRunStep;
+import com.bruce.workflow.entity.WorkflowVersion;
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+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.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * 校验 SQL 建表脚本与实体字段映射保持一致。
+ *
+ * 这组测试直接读取 script/sql 下的建表脚本,把数据库字段与 Java Entity 的映射关系做逐表比对,
+ * 用来补强此前偏结构性的 mapper/repository 验证。
+ */
+class SqlEntityMappingContractTests {
+
+ private static final Path SQL_DIR = Path.of("..", "script", "sql");
+
+ /**
+ * BaseEntity 约定的公共审计字段,需要在所有业务表脚本中保留。
+ */
+ private static final Set BASE_COLUMNS = Set.of(
+ "id", "create_by", "create_time", "update_by", "update_time", "version"
+ );
+
+ @Test
+ void entityMappedColumnsShouldExistInSqlScripts() throws IOException {
+ Map> tableColumns = loadTableColumns();
+ for (Class> entityClass : entityClasses()) {
+ TableName tableName = entityClass.getAnnotation(TableName.class);
+ assertNotNull(tableName, "实体缺少 @TableName: " + entityClass.getName());
+
+ String table = tableName.value();
+ Set sqlColumns = tableColumns.get(table);
+ assertNotNull(sqlColumns, "SQL 脚本未找到表定义: " + table);
+
+ Set entityColumns = collectEntityColumns(entityClass);
+ for (String column : entityColumns) {
+ assertTrue(sqlColumns.contains(column),
+ () -> "表 " + table + " 缺少实体映射字段 " + column + ",实体: " + entityClass.getSimpleName());
+ }
+ }
+ }
+
+ @Test
+ void sqlTablesShouldHaveExpectedEntityCoverage() throws IOException {
+ Map> tableColumns = loadTableColumns();
+ Set entityTables = new LinkedHashSet<>();
+ for (Class> entityClass : entityClasses()) {
+ entityTables.add(entityClass.getAnnotation(TableName.class).value());
+ }
+
+ Set expectedTables = Set.of(
+ "sys_enum",
+ "sys_attachment",
+ "rag_store",
+ "rag_document",
+ "rag_document_parse_result",
+ "rag_chunk",
+ "rag_chunk_embedding",
+ "agent_definition",
+ "agent_session",
+ "agent_message",
+ "agent_capability_binding",
+ "model_provider",
+ "model_config",
+ "model_route_rule",
+ "rag_store_model_config",
+ "model_call_log",
+ "studio_project",
+ "workflow_definition",
+ "workflow_version",
+ "workflow_run",
+ "workflow_run_step",
+ "mcp_server",
+ "mcp_capability",
+ "skill_definition",
+ "skill_version"
+ );
+
+ assertEquals(expectedTables, entityTables, "实体覆盖的表清单应与当前模块表一致");
+ assertTrue(tableColumns.keySet().containsAll(expectedTables), "SQL 脚本应覆盖全部模块表");
+ }
+
+ @Test
+ void entityTablesShouldRetainBaseAuditColumns() throws IOException {
+ Map> tableColumns = loadTableColumns();
+ for (Class> entityClass : entityClasses()) {
+ String table = entityClass.getAnnotation(TableName.class).value();
+ Set sqlColumns = tableColumns.get(table);
+ assertNotNull(sqlColumns, "SQL 脚本未找到表定义: " + table);
+ assertTrue(sqlColumns.containsAll(BASE_COLUMNS),
+ () -> "表 " + table + " 缺少 BaseEntity 审计字段,实际字段: " + sqlColumns);
+ }
+ }
+
+ @Test
+ void sqlColumnParserShouldIgnoreConstraintsAndIndexes() throws IOException {
+ Map> tableColumns = loadTableColumns();
+ Collection> allColumns = tableColumns.values();
+ assertFalse(allColumns.stream().flatMap(Set::stream).anyMatch(column -> column.startsWith("constraint")),
+ "列解析不应把约束名当成字段");
+ assertFalse(allColumns.stream().flatMap(Set::stream).anyMatch(column -> column.startsWith("foreign")),
+ "列解析不应把外键定义当成字段");
+ }
+
+ private Map> loadTableColumns() throws IOException {
+ Map> tableColumns = new LinkedHashMap<>();
+ Pattern createTablePattern = Pattern.compile("CREATE TABLE(?: IF NOT EXISTS)?\\s+([a-zA-Z0-9_]+)\\s*\\(",
+ Pattern.CASE_INSENSITIVE);
+ for (Path sqlFile : sqlFiles()) {
+ List lines = Files.readAllLines(sqlFile, StandardCharsets.UTF_8);
+ String currentTable = null;
+ Set currentColumns = null;
+ int nesting = 0;
+ for (String rawLine : lines) {
+ String line = rawLine.trim();
+ if (line.isEmpty() || line.startsWith("--")) {
+ continue;
+ }
+
+ if (currentTable == null) {
+ Matcher matcher = createTablePattern.matcher(line);
+ if (matcher.find()) {
+ currentTable = matcher.group(1).toLowerCase(Locale.ROOT);
+ currentColumns = new LinkedHashSet<>();
+ tableColumns.put(currentTable, currentColumns);
+ nesting = 1;
+ }
+ continue;
+ }
+
+ nesting += count(line, '(');
+ nesting -= count(line, ')');
+
+ if (!line.startsWith("CONSTRAINT")
+ && !line.startsWith("PRIMARY KEY")
+ && !line.startsWith("FOREIGN KEY")
+ && !line.startsWith("UNIQUE")
+ && !line.startsWith("CHECK")) {
+ String column = extractColumnName(line);
+ if (column != null) {
+ currentColumns.add(column);
+ }
+ }
+
+ if (nesting <= 0) {
+ currentTable = null;
+ currentColumns = null;
+ nesting = 0;
+ }
+ }
+ }
+ return tableColumns;
+ }
+
+ private List sqlFiles() throws IOException {
+ List sqlFiles = new ArrayList<>();
+ try (var paths = Files.list(SQL_DIR)) {
+ paths.filter(path -> path.getFileName().toString().endsWith(".sql"))
+ .sorted()
+ .forEach(sqlFiles::add);
+ }
+ return sqlFiles;
+ }
+
+ private String extractColumnName(String line) {
+ String sanitized = line.replace(",", "").trim();
+ if (sanitized.isEmpty()) {
+ return null;
+ }
+ int firstSpace = sanitized.indexOf(' ');
+ if (firstSpace <= 0) {
+ return null;
+ }
+ String column = sanitized.substring(0, firstSpace).trim();
+ if (!column.matches("[a-zA-Z_][a-zA-Z0-9_]*")) {
+ return null;
+ }
+ return column.toLowerCase(Locale.ROOT);
+ }
+
+ private int count(String text, char target) {
+ int result = 0;
+ for (int index = 0; index < text.length(); index++) {
+ if (text.charAt(index) == target) {
+ result++;
+ }
+ }
+ return result;
+ }
+
+ private Set collectEntityColumns(Class> entityClass) {
+ Set columns = new LinkedHashSet<>();
+ for (Field field : allFields(entityClass)) {
+ if (Modifier.isStatic(field.getModifiers()) || Modifier.isTransient(field.getModifiers())) {
+ continue;
+ }
+ TableField tableField = field.getAnnotation(TableField.class);
+ String column = tableField == null || tableField.value().isBlank()
+ ? camelToSnake(field.getName())
+ : tableField.value();
+ columns.add(column.toLowerCase(Locale.ROOT));
+ }
+ return columns;
+ }
+
+ private List allFields(Class> entityClass) {
+ List fields = new ArrayList<>();
+ Class> current = entityClass;
+ while (current != null && current != Object.class) {
+ fields.addAll(Arrays.asList(current.getDeclaredFields()));
+ if (current == BaseEntity.class) {
+ break;
+ }
+ current = current.getSuperclass();
+ }
+ return fields;
+ }
+
+ private String camelToSnake(String value) {
+ StringBuilder builder = new StringBuilder();
+ for (int index = 0; index < value.length(); index++) {
+ char current = value.charAt(index);
+ if (Character.isUpperCase(current)) {
+ if (index > 0) {
+ builder.append('_');
+ }
+ builder.append(Character.toLowerCase(current));
+ } else {
+ builder.append(current);
+ }
+ }
+ return builder.toString();
+ }
+
+ private List> entityClasses() {
+ return List.of(
+ SysEnum.class,
+ SysAttachment.class,
+ RagStore.class,
+ RagDocument.class,
+ RagDocumentParseResult.class,
+ RagChunk.class,
+ RagChunkEmbedding.class,
+ AgentDefinition.class,
+ AgentSession.class,
+ AgentMessage.class,
+ AgentCapabilityBinding.class,
+ ModelProvider.class,
+ ModelConfig.class,
+ ModelRouteRule.class,
+ RagStoreModelConfig.class,
+ ModelCallLog.class,
+ StudioProject.class,
+ WorkflowDefinition.class,
+ WorkflowVersion.class,
+ WorkflowRun.class,
+ WorkflowRunStep.class,
+ McpServer.class,
+ McpCapability.class,
+ SkillDefinition.class,
+ SkillVersion.class
+ );
+ }
+}