Jelajahi Sumber

feat: 添加数据库字段运行时自动同步处理器(SchemaSyncHandler)

User 6 bulan lalu
induk
melakukan
d056c1b07a
1 mengubah file dengan 92 tambahan dan 182 penghapusan
  1. 92 182
      zxyj-backend/src/main/java/com/zxyj/config/SchemaSyncHandler.java

+ 92 - 182
zxyj-backend/src/main/java/com/zxyj/config/SchemaSyncHandler.java

@@ -1,16 +1,12 @@
 package com.zxyj.config;
 
-import com.zxyj.mapper.*;
 import lombok.extern.slf4j.Slf4j;
-import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
-import org.springframework.dao.DataAccessException;
-import org.springframework.dao.InvalidDataAccessApiUsageException;
 import org.springframework.jdbc.core.JdbcTemplate;
 import org.springframework.stereotype.Component;
 import org.springframework.transaction.annotation.Transactional;
 
 import javax.annotation.Resource;
-import java.util.HashSet;
+import java.util.HashMap;
 import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
@@ -32,175 +28,113 @@ public class SchemaSyncHandler {
     private final Set<String> processedTables = ConcurrentHashMap.newKeySet();
 
     // 字段定义映射表:表名 -> (字段名 -> MySQL类型定义)
-    private static final Map<String, Map<String, String>> SCHEMA_DEFINITIONS = Map.of(
-            "rewards", Map.of(
-                    "title", "VARCHAR(100) NOT NULL DEFAULT ''",
-                    "points_required", "INT NOT NULL DEFAULT 0",
-                    "category", "VARCHAR(32)",
-                    "status", "ENUM('available','pending','exchanged','rejected') DEFAULT 'available'",
-                    "exchange_approved", "TINYINT",
-                    "exchanged_at", "DATETIME",
-                    "is_template", "TINYINT DEFAULT 0",
-                    "child_id", "BIGINT NULL",
-                    "family_id", "BIGINT NOT NULL"
-            ),
-            "tasks", Map.of(
-                    "title", "VARCHAR(200) NOT NULL",
-                    "description", "VARCHAR(500)",
-                    "category", "VARCHAR(32)",
-                    "points", "TINYINT DEFAULT 2",
-                    "deadline", "DATETIME",
-                    "child_id", "BIGINT NULL",
-                    "family_id", "BIGINT NOT NULL",
-                    "creator_id", "BIGINT NOT NULL",
-                    "status", "ENUM('pending','completed','overdue','cancelled') DEFAULT 'pending'",
-                    "executor_type", "ENUM('child','parent') DEFAULT 'child'",
-                    "executor_id", "BIGINT",
-                    "repeat_type", "ENUM('none','daily','weekly') DEFAULT 'none'",
-                    "need_review", "TINYINT DEFAULT 0",
-                    "review_by_category", "TINYINT DEFAULT 0",
-                    "bonus_rule", "VARCHAR(500)",
-                    "points_expire_days", "INT DEFAULT 0",
-                    "template_id", "BIGINT",
-                    "is_template", "TINYINT DEFAULT 0",
-                    "task_type", "VARCHAR(20)",
-                    "frequency", "VARCHAR(20)",
-                    "max_frequency", "INT",
-                    "complete_types", "VARCHAR(100)",
-                    "review_type", "VARCHAR(20)",
-                    "earliest_start", "DATETIME",
-                    "latest_end", "DATETIME"
-            ),
-            "users", Map.of(
-                    "openid", "VARCHAR(64) NOT NULL",
-                    "unionid", "VARCHAR(64)",
-                    "family_id", "BIGINT NOT NULL",
-                    "role", "ENUM('parent','child','teacher') NOT NULL",
-                    "nickname", "VARCHAR(50)",
-                    "avatar", "VARCHAR(255)",
-                    "password", "VARCHAR(128)",
-                    "roles", "VARCHAR(100)",
-                    "total_points", "INT DEFAULT 0",
-                    "id_card", "VARCHAR(18)",
-                    "real_name", "VARCHAR(50)",
-                    "phone", "VARCHAR(11)",
-                    "birthday", "DATE",
-                    "gender", "ENUM('male','female')",
-                    "teacher_no", "VARCHAR(50)",
-                    "teacher_photo", "VARCHAR(500)",
-                    "certificate_image", "VARCHAR(500)",
-                    "teacher_status", "VARCHAR(20) DEFAULT 'pending'",
-                    "teacher_family_ids", "VARCHAR(500)"
-            ),
-            "children", Map.of(
-                    "user_id", "BIGINT NOT NULL",
-                    "family_id", "BIGINT NOT NULL",
-                    "nickname", "VARCHAR(50) NOT NULL",
-                    "age", "TINYINT NOT NULL",
-                    "dan_level", "VARCHAR(2)",
-                    "penalty_enabled", "TINYINT DEFAULT 1",
-                    "theme", "VARCHAR(32) DEFAULT 'default'",
-                    "total_points", "INT DEFAULT 0",
-                    "streak_days", "INT DEFAULT 0",
-                    "birthday", "DATE",
-                    "gender", "ENUM('male','female')",
-                    "phone", "VARCHAR(11)",
-                    "id_card", "VARCHAR(18)",
-                    "focus_max_daily", "TINYINT DEFAULT 3",
-                    "focus_remaining", "TINYINT DEFAULT 3",
-                    "focus_reset_date", "DATE"
-            ),
-            "families", Map.of(
-                    "name", "VARCHAR(100)",
-                    "invite_code", "VARCHAR(8) UNIQUE",
-                    "street_id", "BIGINT",
-                    "province", "VARCHAR(50)",
-                    "city", "VARCHAR(50)",
-                    "district", "VARCHAR(50)",
-                    "street", "VARCHAR(100)",
-                    "teacher_id", "BIGINT"
-            ),
-            "task_templates", Map.of(
-                    "dan_level", "VARCHAR(2) NOT NULL",
-                    "title", "VARCHAR(200) NOT NULL",
-                    "points", "TINYINT DEFAULT 2",
-                    "duration_minutes", "TINYINT",
-                    "category", "VARCHAR(32)",
-                    "is_default", "TINYINT DEFAULT 1",
-                    "family_id", "BIGINT"
-            ),
-            "admin_task_templates", Map.of(
-                    "title", "VARCHAR(200) NOT NULL",
-                    "description", "TEXT",
-                    "points", "INT DEFAULT 10",
-                    "category", "VARCHAR(32)",
-                    "need_review", "TINYINT DEFAULT 0",
-                    "review_by_category", "TINYINT DEFAULT 0",
-                    "recommended_age", "INT",
-                    "difficulty", "ENUM('easy','medium','hard') DEFAULT 'medium'",
-                    "is_active", "TINYINT DEFAULT 1",
-                    "teacher_id", "BIGINT",
-                    "complete_types", "VARCHAR(100)",
-                    "duration", "INT",
-                    "review_type", "VARCHAR(20)",
-                    "task_type", "VARCHAR(20)",
-                    "frequency", "VARCHAR(20)",
-                    "max_frequency", "INT"
-            ),
-            "points_log", Map.of(
-                    "child_id", "BIGINT NOT NULL",
-                    "task_id", "BIGINT",
-                    "amount", "INT NOT NULL",
-                    "type", "ENUM('earn','spend','bonus','penalty','adjust','reset','refund') NOT NULL",
-                    "description", "VARCHAR(255)",
-                    "expire_at", "DATE",
-                    "expired", "TINYINT DEFAULT 0"
-            ),
-            "user_operation_logs", Map.of(
-                    "user_id", "BIGINT",
-                    "nickname", "VARCHAR(50)",
-                    "operation_type", "VARCHAR(20) NOT NULL",
-                    "description", "VARCHAR(500)",
-                    "request_path", "VARCHAR(200)",
-                    "request_method", "VARCHAR(10)",
-                    "request_params", "TEXT",
-                    "response_status", "INT",
-                    "client_ip", "VARCHAR(50)",
-                    "user_agent", "VARCHAR(500)"
-            )
-    );
+    private static final Map<String, Map<String, String>> SCHEMA_DEFINITIONS;
+    
+    static {
+        // rewards 表字段
+        Map<String, String> rewardsFields = new HashMap<>();
+        rewardsFields.put("title", "VARCHAR(100) NOT NULL DEFAULT ''");
+        rewardsFields.put("points_required", "INT NOT NULL DEFAULT 0");
+        rewardsFields.put("category", "VARCHAR(32)");
+        rewardsFields.put("status", "ENUM('available','pending','exchanged','rejected') DEFAULT 'available'");
+        rewardsFields.put("exchange_approved", "TINYINT");
+        rewardsFields.put("exchanged_at", "DATETIME");
+        rewardsFields.put("is_template", "TINYINT DEFAULT 0");
+        rewardsFields.put("child_id", "BIGINT NULL");
+        rewardsFields.put("family_id", "BIGINT NOT NULL");
+        
+        // tasks 表字段
+        Map<String, String> tasksFields = new HashMap<>();
+        tasksFields.put("title", "VARCHAR(200) NOT NULL");
+        tasksFields.put("description", "VARCHAR(500)");
+        tasksFields.put("category", "VARCHAR(32)");
+        tasksFields.put("points", "TINYINT DEFAULT 2");
+        tasksFields.put("deadline", "DATETIME");
+        tasksFields.put("child_id", "BIGINT NULL");
+        tasksFields.put("family_id", "BIGINT NOT NULL");
+        tasksFields.put("creator_id", "BIGINT NOT NULL");
+        tasksFields.put("status", "ENUM('pending','completed','overdue','cancelled') DEFAULT 'pending'");
+        tasksFields.put("executor_type", "ENUM('child','parent') DEFAULT 'child'");
+        tasksFields.put("executor_id", "BIGINT");
+        tasksFields.put("repeat_type", "ENUM('none','daily','weekly') DEFAULT 'none'");
+        tasksFields.put("need_review", "TINYINT DEFAULT 0");
+        tasksFields.put("bonus_rule", "VARCHAR(500)");
+        tasksFields.put("points_expire_days", "INT DEFAULT 0");
+        tasksFields.put("template_id", "BIGINT");
+        tasksFields.put("is_template", "TINYINT DEFAULT 0");
+        
+        // users 表字段
+        Map<String, String> usersFields = new HashMap<>();
+        usersFields.put("openid", "VARCHAR(64) NOT NULL");
+        usersFields.put("unionid", "VARCHAR(64)");
+        usersFields.put("family_id", "BIGINT NOT NULL");
+        usersFields.put("role", "ENUM('parent','child','teacher') NOT NULL");
+        usersFields.put("nickname", "VARCHAR(50)");
+        usersFields.put("avatar", "VARCHAR(255)");
+        usersFields.put("password", "VARCHAR(128)");
+        usersFields.put("roles", "VARCHAR(100)");
+        usersFields.put("total_points", "INT DEFAULT 0");
+        
+        // children 表字段
+        Map<String, String> childrenFields = new HashMap<>();
+        childrenFields.put("user_id", "BIGINT NOT NULL");
+        childrenFields.put("family_id", "BIGINT NOT NULL");
+        childrenFields.put("nickname", "VARCHAR(50) NOT NULL");
+        childrenFields.put("age", "TINYINT NOT NULL");
+        childrenFields.put("dan_level", "VARCHAR(2)");
+        childrenFields.put("penalty_enabled", "TINYINT DEFAULT 1");
+        childrenFields.put("theme", "VARCHAR(32) DEFAULT 'default'");
+        childrenFields.put("total_points", "INT DEFAULT 0");
+        childrenFields.put("streak_days", "INT DEFAULT 0");
+        childrenFields.put("birthday", "DATE");
+        childrenFields.put("gender", "ENUM('male','female')");
+        
+        // points_log 表字段
+        Map<String, String> pointsLogFields = new HashMap<>();
+        pointsLogFields.put("child_id", "BIGINT NOT NULL");
+        pointsLogFields.put("task_id", "BIGINT");
+        pointsLogFields.put("amount", "INT NOT NULL");
+        pointsLogFields.put("type", "ENUM('earn','spend','bonus','penalty','adjust','reset','refund') NOT NULL");
+        pointsLogFields.put("description", "VARCHAR(255)");
+        pointsLogFields.put("expire_at", "DATE");
+        pointsLogFields.put("expired", "TINYINT DEFAULT 0");
+        
+        SCHEMA_DEFINITIONS = new HashMap<>();
+        SCHEMA_DEFINITIONS.put("rewards", rewardsFields);
+        SCHEMA_DEFINITIONS.put("tasks", tasksFields);
+        SCHEMA_DEFINITIONS.put("users", usersFields);
+        SCHEMA_DEFINITIONS.put("children", childrenFields);
+        SCHEMA_DEFINITIONS.put("points_log", pointsLogFields);
+    }
 
     /**
      * 处理字段不存在的异常,自动添加字段后重试
-     * @param exception 原始SQL异常
+     * @param exception 原始SQL异常消息
      * @param operation 要重试的操作
      * @return 重试结果,如果成功返回null,失败返回异常消息
      */
     @Transactional
-    public String handleMissingColumn(DataAccessException exception, Runnable operation) {
-        String message = exception.getMessage();
-        
+    public String handleMissingColumn(String exception, Runnable operation) {
         // 匹配 "Unknown column 'xxx' in 'field list'" 或 "Field 'xxx' doesn't have a default value"
         Pattern pattern = Pattern.compile("Unknown column '(\\w+)' in '(?:field list|\\w+)'", Pattern.CASE_INSENSITIVE);
-        Matcher matcher = pattern.matcher(message);
+        Matcher matcher = pattern.matcher(exception);
         
-        // 尝试提取字段名
+        String fieldName = null;
         if (!matcher.find()) {
             // 也可能是 "Field 'xxx' doesn't have a default value" 情况
             pattern = Pattern.compile("Field '(\\w+)' doesn't have a default value", Pattern.CASE_INSENSITIVE);
-            matcher = pattern.matcher(message);
+            matcher = pattern.matcher(exception);
             if (!matcher.find()) {
-                log.warn("无法解析字段缺失错误: {}", message);
+                log.warn("无法解析字段缺失错误: {}", exception);
                 return "无法解析字段缺失错误";
             }
         }
+        fieldName = matcher.group(1);
         
-        String fieldName = matcher.group(1);
-        
-        // 尝试提取表名 - 从异常消息或SQL语句中提取
-        String tableName = extractTableName(message);
+        // 尝试提取表名
+        String tableName = extractTableName(exception);
         if (tableName == null) {
-            log.warn("无法从错误消息中提取表名: {}", message);
+            log.warn("无法从错误消息中提取表名: {}", exception);
             return "无法提取表名";
         }
         
@@ -220,7 +154,7 @@ public class SchemaSyncHandler {
             return null;
         } catch (Exception e) {
             log.error("重试操作仍然失败: {}", e.getMessage());
-            return "重��失败: " + e.getMessage();
+            return "重试失败: " + e.getMessage();
         }
     }
     
@@ -262,13 +196,13 @@ public class SchemaSyncHandler {
         Map<String, String> fields = SCHEMA_DEFINITIONS.get(tableName);
         if (fields == null) {
             log.warn("未找到表 {} 的字段定义", tableName);
-            return syncFromDatabase(tableName, fieldName);
+            return false;
         }
         
         String columnDef = fields.get(fieldName.toLowerCase());
         if (columnDef == null) {
             log.warn("表 {} 中未定义字段 {}", tableName, fieldName);
-            return syncFromDatabase(tableName, fieldName);
+            return false;
         }
         
         try {
@@ -289,30 +223,6 @@ public class SchemaSyncHandler {
         }
     }
     
-    /**
-     * 从数据库同步字段信息(当本地定义缺失时)
-     */
-    private boolean syncFromDatabase(String tableName, String fieldName) {
-        try {
-            // 查询 INFORMATION_SCHEMA 获取字段信息
-            String sql = "SELECT COLUMN_TYPE, IS_NULLABLE, COLUMN_DEFAULT " +
-                       "FROM INFORMATION_SCHEMA.COLUMNS " +
-                       "WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND COLUMN_NAME = ?";
-            
-            Map<String, Object> colInfo = jdbcTemplate.queryForMap(sql, tableName, fieldName + "_backup"); // 尝试查找相似字段
-            
-            log.info("从数据库同步字段信息: {}.{}", tableName, fieldName);
-            
-            // 简单地标记为已处理
-            processedTables.add(tableName + "." + fieldName);
-            return true;
-        } catch (Exception e) {
-            // 字段不在数据库中,尝试创建
-            log.warn("无法找到字段 {} 的定义,将尝试通用处理", fieldName);
-            return false;
-        }
-    }
-    
     /**
      * 检查字段是否已存在
      */