Browse Source

feat: 能量行为配置系统 — 可配置每种行为的能量发放规则

E2E Test Bot 1 month ago
parent
commit
90a32d4634

+ 61 - 0
cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java

@@ -7026,5 +7026,66 @@ private void runMigration100() {
 		} catch (Exception e) {
 			log.warn("添加tasks表维度字段失败: {}", e.getMessage());
 		}
+
+		// 迁移108: 创建 energy_behavior_config 表(能量行为配置)
+		try {
+			jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS energy_behavior_config (" +
+					"id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+					"behavior_code VARCHAR(32) NOT NULL COMMENT '行为编码', " +
+					"behavior_name VARCHAR(50) NOT NULL COMMENT '行为名称', " +
+					"description VARCHAR(200) COMMENT '行为描述', " +
+					"default_amount INT DEFAULT 5 COMMENT '默认能量值', " +
+					"amount_source VARCHAR(32) DEFAULT 'fixed' COMMENT '能量值来源', " +
+					"dimension_assign JSON COMMENT '维度分配JSON', " +
+					"daily_limit INT DEFAULT 0 COMMENT '每日上限', " +
+					"cooldown_seconds INT DEFAULT 0 COMMENT '冷却时间(秒)', " +
+					"expire_days INT DEFAULT 0 COMMENT '过期天数', " +
+					"enabled TINYINT DEFAULT 1 COMMENT '是否启用', " +
+					"created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+					"updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
+					"UNIQUE KEY uk_behavior_code (behavior_code)" +
+					") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='能量行为配置表'");
+			log.info("已创建 energy_behavior_config 表");
+
+			// 插入预置数据(仅当表为空时插入)
+			Integer count = jdbcTemplate.queryForObject("SELECT COUNT(*) FROM energy_behavior_config", Integer.class);
+			if (count == null || count == 0) {
+				String[][] seedData = {
+					{"task_complete", "完成任务", "完成任务获得的能量", "5", "points", "[{\"dim\":\"action\",\"ratio\":100}]", "200", "0", "0"},
+					{"game_finish", "完成游戏", "完成小游戏获得的能量", "5", "points", "[{\"dim\":\"mind\",\"ratio\":30},{\"dim\":\"wisdom\",\"ratio\":50},{\"dim\":\"action\",\"ratio\":20}]", "200", "0", "0"},
+					{"activity_checkin", "活动签到", "活动签到获得的能量", "5", "fixed", "[{\"dim\":\"body\",\"ratio\":30},{\"dim\":\"action\",\"ratio\":70}]", "100", "0", "0"},
+					{"article_read", "阅读文章", "阅读文章获得的能量", "5", "points", "[{\"dim\":\"mind\",\"ratio\":30},{\"dim\":\"wisdom\",\"ratio\":50},{\"dim\":\"action\",\"ratio\":20}]", "100", "0", "0"},
+					{"product_purchase", "商品购买", "购买商品获得的能量", "10", "order_amount", "[{\"dim\":\"wealth\",\"ratio\":100}]", "500", "0", "30"},
+					{"health_checkin", "健康打卡", "健康打卡获得的能量", "5", "fixed", "[{\"dim\":\"body\",\"ratio\":100}]", "50", "0", "30"},
+					{"finance_checkin", "理财打卡", "理财打卡获得的能量", "5", "fixed", "[{\"dim\":\"wealth\",\"ratio\":100}]", "50", "0", "0"},
+					{"emotion_checkin", "情绪打卡", "情绪打卡获得的能量", "5", "fixed", "[{\"dim\":\"mind\",\"ratio\":100}]", "40", "0", "0"},
+					{"appointment_submit", "提交测评预约", "提交测评预约获得的能量", "10", "fixed", "[{\"dim\":\"action\",\"ratio\":100}]", "50", "0", "0"},
+					{"appointment_complete", "完成测评", "完成测评获得的能量", "30", "fixed", "[{\"dim\":\"mind\",\"ratio\":50},{\"dim\":\"wisdom\",\"ratio\":50}]", "100", "0", "0"},
+					{"streak_daily", "连续打卡每日", "每日连续打卡获得的能量", "5", "fixed", "[{\"dim\":\"body\",\"ratio\":30},{\"dim\":\"mind\",\"ratio\":20},{\"dim\":\"action\",\"ratio\":50}]", "50", "86400", "0"},
+					{"streak_milestone", "打卡里程碑", "达到打卡里程碑获得的能量", "20", "config", "[{\"dim\":\"body\",\"ratio\":30},{\"dim\":\"mind\",\"ratio\":20},{\"dim\":\"action\",\"ratio\":50}]", "100", "0", "0"},
+					{"growth_task", "成长任务", "完成每日成长任务获得的能量", "10", "config", "[{\"dim\":\"body\",\"ratio\":20},{\"dim\":\"mind\",\"ratio\":20},{\"dim\":\"wisdom\",\"ratio\":20},{\"dim\":\"action\",\"ratio\":20},{\"dim\":\"wealth\",\"ratio\":20}]", "100", "0", "1"},
+					{"micro_action", "微行动", "完成微行动获得的能量", "1", "fixed", "[{\"dim\":\"action\",\"ratio\":100}]", "20", "60", "30"},
+					{"onboarding", "新手引导", "完成新手引导任务获得的能量", "10", "config", "[{\"dim\":\"body\",\"ratio\":20},{\"dim\":\"mind\",\"ratio\":20},{\"dim\":\"wisdom\",\"ratio\":20},{\"dim\":\"action\",\"ratio\":20},{\"dim\":\"wealth\",\"ratio\":20}]", "50", "0", "30"},
+					{"invite_milestone", "邀请里程碑", "达到邀请人数里程碑获得的能量", "30", "config", "[{\"dim\":\"action\",\"ratio\":40},{\"dim\":\"wealth\",\"ratio\":60}]", "200", "0", "30"},
+					{"dimension_sync", "维度同步", "健康维度数据上传同步获得的能量", "10", "config", "[{\"dim\":\"body\",\"ratio\":100}]", "100", "0", "30"},
+					{"invite_friend", "邀请好友加入", "邀请好友注册加入获得的能量", "20", "fixed", "[{\"dim\":\"action\",\"ratio\":50},{\"dim\":\"wealth\",\"ratio\":50}]", "200", "0", "0"},
+					{"invite_family", "邀请家庭成员", "邀请家庭成员加入家庭获得的能量", "10", "fixed", "[{\"dim\":\"mind\",\"ratio\":30},{\"dim\":\"action\",\"ratio\":70}]", "100", "0", "0"},
+					{"report_upload", "上传体检报告", "上传体检报告获得的能量", "30", "fixed", "[{\"dim\":\"body\",\"ratio\":100}]", "100", "0", "30"},
+					{"plan_complete", "完成成长计划", "完成成长计划复盘获得的能量", "20", "fixed", "[{\"dim\":\"body\",\"ratio\":20},{\"dim\":\"mind\",\"ratio\":20},{\"dim\":\"wisdom\",\"ratio\":20},{\"dim\":\"action\",\"ratio\":20},{\"dim\":\"wealth\",\"ratio\":20}]", "100", "0", "0"},
+					{"wisdom_report", "上传智测评报告", "上传智慧测评报告获得的能量", "30", "fixed", "[{\"dim\":\"wisdom\",\"ratio\":100}]", "100", "0", "30"},
+				};
+				String sql = "INSERT INTO energy_behavior_config (behavior_code, behavior_name, description, default_amount, amount_source, dimension_assign, daily_limit, cooldown_seconds, expire_days, enabled) VALUES (?,?,?,?,?,?,?,?,?,1)";
+				for (String[] row : seedData) {
+					try {
+						jdbcTemplate.update(sql, row[0], row[1], row[2], Integer.parseInt(row[3]), row[4], row[5], Integer.parseInt(row[6]), Integer.parseInt(row[7]), Integer.parseInt(row[8]));
+					} catch (Exception e) {
+						log.warn("插入能量行为配置失败: behavior_code={}, error={}", row[0], e.getMessage());
+					}
+				}
+				log.info("已插入{}条能量行为配置", seedData.length);
+			}
+		} catch (Exception e) {
+			log.warn("创建 energy_behavior_config 表失败: {}", e.getMessage());
+		}
 	}
 }

+ 62 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminEnergyBehaviorConfigController.java

@@ -0,0 +1,62 @@
+package com.etotem.cfc.controller.admin;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.EnergyBehaviorConfig;
+import com.etotem.cfc.mapper.EnergyBehaviorConfigMapper;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+@Tag(name = "管理端-能量行为配置")
+@RestController
+@RequestMapping("/api/admin/energy-behavior")
+public class AdminEnergyBehaviorConfigController {
+
+    @Resource
+    private EnergyBehaviorConfigMapper behaviorConfigMapper;
+
+    @Operation(summary = "获取所有能量行为配置")
+    @PostMapping("/list")
+    public Result<List<EnergyBehaviorConfig>> list() {
+        List<EnergyBehaviorConfig> list = behaviorConfigMapper.selectList(
+                new LambdaQueryWrapper<EnergyBehaviorConfig>().orderByAsc(EnergyBehaviorConfig::getBehaviorCode));
+        return Result.success(list);
+    }
+
+    @Operation(summary = "获取单个能量行为配置")
+    @PostMapping("/detail")
+    public Result<EnergyBehaviorConfig> detail(@RequestBody Map<String, Object> params) {
+        Long id = Long.valueOf(params.get("id").toString());
+        EnergyBehaviorConfig config = behaviorConfigMapper.selectById(id);
+        if (config == null) return Result.error("配置不存在");
+        return Result.success(config);
+    }
+
+    @Operation(summary = "更新能量行为配置")
+    @PostMapping("/update")
+    public Result<String> update(@RequestBody EnergyBehaviorConfig config) {
+        EnergyBehaviorConfig existing = behaviorConfigMapper.selectById(config.getId());
+        if (existing == null) return Result.error("配置不存在");
+        config.setUpdatedAt(new Date());
+        behaviorConfigMapper.updateById(config);
+        return Result.success("更新成功");
+    }
+
+    @Operation(summary = "切换启用/禁用状态")
+    @PostMapping("/toggle")
+    public Result<String> toggle(@RequestBody Map<String, Object> params) {
+        Long id = Long.valueOf(params.get("id").toString());
+        EnergyBehaviorConfig config = behaviorConfigMapper.selectById(id);
+        if (config == null) return Result.error("配置不存在");
+        config.setEnabled(config.getEnabled() != null && config.getEnabled() == 1 ? 0 : 1);
+        config.setUpdatedAt(new Date());
+        behaviorConfigMapper.updateById(config);
+        return Result.success(config.getEnabled() == 1 ? "已启用" : "已禁用");
+    }
+}

+ 51 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/EnergyBehaviorConfig.java

@@ -0,0 +1,51 @@
+package com.etotem.cfc.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+@Data
+@TableName("energy_behavior_config")
+public class EnergyBehaviorConfig implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    /** 行为编码: task_complete/game_finish/activity_checkin/... */
+    private String behaviorCode;
+
+    /** 行为名称: 完成任务/完成游戏/活动签到/... */
+    private String behaviorName;
+
+    /** 行为描述 */
+    private String description;
+
+    /** 默认能量值 */
+    private Integer defaultAmount;
+
+    /** 能量值来源: fixed(固定)/points(跟随积分)/order_amount(跟随金额)/config(从关联表读取) */
+    private String amountSource;
+
+    /** 维度分配JSON: [{"dim":"action","ratio":50},{"dim":"wisdom","ratio":50}] */
+    private String dimensionAssign;
+
+    /** 每日上限(0=不限制) */
+    private Integer dailyLimit;
+
+    /** 冷却时间(秒,0=无冷却) */
+    private Integer cooldownSeconds;
+
+    /** 过期天数(0=永久) */
+    private Integer expireDays;
+
+    /** 是否启用: 0=禁用 1=启用 */
+    private Integer enabled;
+
+    private Date createdAt;
+
+    private Date updatedAt;
+}

+ 9 - 0
cfc-backend/src/main/java/com/etotem/cfc/mapper/EnergyBehaviorConfigMapper.java

@@ -0,0 +1,9 @@
+package com.etotem.cfc.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.etotem.cfc.entity.EnergyBehaviorConfig;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface EnergyBehaviorConfigMapper extends BaseMapper<EnergyBehaviorConfig> {
+}

+ 151 - 7
cfc-backend/src/main/java/com/etotem/cfc/service/EnergyService.java

@@ -6,6 +6,9 @@ import com.etotem.cfc.dto.EnergySandboxDTO;
 import com.etotem.cfc.dto.MemberEnergyDTO;
 import com.etotem.cfc.entity.*;
 import com.etotem.cfc.mapper.*;
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
@@ -1096,6 +1099,9 @@ public class EnergyService {
     @Resource
     private EnergySourceConfigMapper energySourceConfigMapper;
 
+    @Resource
+    private EnergyBehaviorConfigMapper energyBehaviorConfigMapper;
+
     @Resource
     private EnergyLogMapper energyLogMapper;
 
@@ -1127,7 +1133,13 @@ public class EnergyService {
                                             Integer daysToExpire) {
         if (totalAmount == null || totalAmount <= 0) return new HashMap<>();
 
-        // 1. 查比例配置 → Fallback链
+        // 0. 查 behavior_config:sourceType 映射到 behavior_code
+        EnergyBehaviorConfig behaviorConfig = matchBehaviorConfig(sourceType);
+        if (behaviorConfig != null) {
+            return awardByConfig(childId, behaviorConfig, sourceType, sourceId, totalAmount, description, daysToExpire);
+        }
+
+        // 1. 无 behavior_config 时使用旧逻辑:查 energy_source_config → Fallback
         List<EnergySourceConfig> configs = energySourceConfigMapper.selectList(
                 new LambdaQueryWrapper<EnergySourceConfig>()
                         .eq(EnergySourceConfig::getSourceType, sourceType)
@@ -1143,14 +1155,64 @@ public class EnergyService {
             return new HashMap<>();
         }
 
-        // 3. 按比例分配(整数,余数加到最大比例维度)
+        // 3. 按比例分配
         Map<Long, Integer> allocations = calculateAllocations(totalAmount, dimRatios);
-
         // 4. 更新余额 + 写流水
+        return executeAward(childId, sourceType, sourceId, description, daysToExpire, allocations);
+    }
+
+    /**
+     * 根据 behavior_config 发放能量
+     */
+    @Transactional
+    public Map<String, Integer> awardByConfig(Long childId, EnergyBehaviorConfig config,
+                                              String sourceType, Long sourceId,
+                                              Integer totalAmount, String description,
+                                              Integer daysToExpire) {
+        if (!isEnabled(config)) return new HashMap<>();
+
+        // 解析维度分配JSON
+        Map<Long, Integer> dimRatios = parseDimensionAssign(config.getDimensionAssign());
+        if (dimRatios.isEmpty()) {
+            // 无维度配置时使用旧 Fallback
+            EnergyDimension action = getDimByCode("action");
+            if (action != null) dimRatios = Collections.singletonMap(action.getId(), 10000);
+            else return new HashMap<>();
+        }
+
+        // 日上限检查(使用配置的 daily_limit,大于0时覆盖全局限制)
+        Integer configDailyLimit = config.getDailyLimit();
+        if (configDailyLimit != null && configDailyLimit > 0) {
+            if (isDailyLimitExceeded(childId, dimRatios.keySet(), totalAmount, configDailyLimit)) {
+                log.warn("行为日上限已达,跳过: behaviorCode={}, childId={}", config.getBehaviorCode(), childId);
+                return new HashMap<>();
+            }
+        } else {
+            if (isDailyLimitExceeded(childId, dimRatios.keySet(), totalAmount)) {
+                log.warn("全局日上限已达,跳过: behaviorCode={}, childId={}", config.getBehaviorCode(), childId);
+                return new HashMap<>();
+            }
+        }
+
+        // 计算过期时间
+        Integer expireDays = config.getExpireDays() != null && config.getExpireDays() > 0
+                ? config.getExpireDays() : (daysToExpire != null ? daysToExpire : null);
+
+        // 按比例分配
+        Map<Long, Integer> allocations = calculateAllocations(totalAmount, dimRatios);
+        return executeAward(childId, sourceType, sourceId, totalAmount, description, expireDays, allocations);
+    }
+
+    /**
+     * 执行能量发放(更新余额+写流水)
+     */
+    private Map<String, Integer> executeAward(Long childId, String sourceType, Long sourceId,
+                                               Integer totalAmount, String description,
+                                               Integer expireDays, Map<Long, Integer> allocations) {
         Map<String, Integer> result = new LinkedHashMap<>();
         Date now = new Date();
-        Date expiresAt = daysToExpire != null
-                ? new Date(now.getTime() + (long) daysToExpire * 86400000L) : null;
+        Date expiresAt = expireDays != null
+                ? new Date(now.getTime() + (long) expireDays * 86400000L) : null;
 
         for (Map.Entry<Long, Integer> entry : allocations.entrySet()) {
             if (entry.getValue() <= 0) continue;
@@ -1179,6 +1241,80 @@ public class EnergyService {
         return result;
     }
 
+    /**
+     * 通过 sourceType 匹配 behavior_config
+     * 映射规则: sourceType → behavior_code
+     * 如 "task" → "task_complete", "game" → "game_finish"
+     */
+    private EnergyBehaviorConfig matchBehaviorConfig(String sourceType) {
+        if (sourceType == null || sourceType.isEmpty()) return null;
+        String behaviorCode = sourceTypeToBehaviorCode(sourceType);
+        if (behaviorCode == null) return null;
+        try {
+            return energyBehaviorConfigMapper.selectOne(
+                    new LambdaQueryWrapper<EnergyBehaviorConfig>()
+                            .eq(EnergyBehaviorConfig::getBehaviorCode, behaviorCode)
+                            .eq(EnergyBehaviorConfig::getEnabled, 1)
+            );
+        } catch (Exception e) {
+            log.warn("查询能量行为配置失败: behaviorCode={}, error={}", behaviorCode, e.getMessage());
+            return null;
+        }
+    }
+
+    /**
+     * sourceType → behavior_code 映射
+     */
+    private String sourceTypeToBehaviorCode(String sourceType) {
+        switch (sourceType) {
+            case "task": return "task_complete";
+            case "game": return "game_finish";
+            case "activity": return "activity_checkin";
+            case "article": return "article_read";
+            case "product": return "product_purchase";
+            case "health_checkin": return "health_checkin";
+            case "checkin": return "finance_checkin";  // 理财打卡
+            case "emotion_checkin": return "emotion_checkin";
+            case "appointment": return "appointment_submit";  // 预约/完成共用
+            case "streak": return "streak_daily";
+            case "growth_day": return "growth_task";
+            case "micro_action": return "micro_action";
+            case "onboarding": return "onboarding";
+            case "invite_milestone": return "invite_milestone";
+            case "dimension": return "dimension_sync";
+            default: return null;
+        }
+    }
+
+    /**
+     * 解析维度分配JSON → Map<dimensionId, ratio>
+     * 输入: [{"dim":"action","ratio":50},{"dim":"wisdom","ratio":50}]
+     * 输出: {4: 5000, 3: 5000}
+     */
+    private Map<Long, Integer> parseDimensionAssign(String dimensionAssign) {
+        Map<Long, Integer> result = new LinkedHashMap<>();
+        if (dimensionAssign == null || dimensionAssign.isEmpty()) return result;
+        try {
+            com.alibaba.fastjson.JSONArray arr = com.alibaba.fastjson.JSON.parseArray(dimensionAssign);
+            for (int i = 0; i < arr.size(); i++) {
+                com.alibaba.fastjson.JSONObject item = arr.getJSONObject(i);
+                String dimCode = item.getString("dim");
+                int ratio = item.getInt("ratio");
+                EnergyDimension dim = getDimByCode(dimCode);
+                if (dim != null) {
+                    result.put(dim.getId(), ratio * 100); // 转为10000制
+                }
+            }
+        } catch (Exception e) {
+            log.warn("解析维度分配JSON失败: {}", e.getMessage());
+        }
+        return result;
+    }
+
+    private boolean isEnabled(EnergyBehaviorConfig config) {
+        return config != null && config.getEnabled() != null && config.getEnabled() == 1;
+    }
+
     /**
      * 扣除能量
      *
@@ -1448,12 +1584,19 @@ public class EnergyService {
      * 检查日上限是否已达
      */
     private boolean isDailyLimitExceeded(Long childId, Set<Long> dimIds, Integer amount) {
+        return isDailyLimitExceeded(childId, dimIds, amount, GLOBAL_DAILY_LIMIT);
+    }
+
+    /**
+     * 检查日上限(支持自定义全局上限)
+     */
+    private boolean isDailyLimitExceeded(Long childId, Set<Long> dimIds, Integer amount, Integer customGlobalLimit) {
         if (amount == null || amount <= 0) return true;
 
         Date todayStart = getTodayStart();
         Date todayEnd = getTodayEnd();
 
-        // 全局日上限
+        // 全局日上限(使用自定义上限)
         LambdaQueryWrapper<EnergyLog> globalWrapper = new LambdaQueryWrapper<EnergyLog>()
                 .eq(EnergyLog::getChildId, childId)
                 .gt(EnergyLog::getAmount, 0)
@@ -1461,7 +1604,8 @@ public class EnergyService {
         Integer globalToday = energyLogMapper.selectList(globalWrapper).stream()
                 .mapToInt(e -> e.getAmount() != null ? e.getAmount() : 0)
                 .sum();
-        if (globalToday + amount > GLOBAL_DAILY_LIMIT) return true;
+        int globalLimit = customGlobalLimit != null ? customGlobalLimit : GLOBAL_DAILY_LIMIT;
+        if (globalToday + amount > globalLimit) return true;
 
         // 各维度日上限
         for (Long dimId : dimIds) {

+ 18 - 0
cfc-backend/src/main/resources/schema.sql

@@ -3571,3 +3571,21 @@ CREATE TABLE IF NOT EXISTS payment_orders (
     INDEX idx_family_id (family_id),
     INDEX idx_status (status)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='会员支付订单';
+
+-- 能量行为配置表(可配置每种行为的能量发放规则)
+CREATE TABLE IF NOT EXISTS energy_behavior_config (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    behavior_code VARCHAR(32) NOT NULL COMMENT '行为编码: task_complete/game_finish/...',
+    behavior_name VARCHAR(50) NOT NULL COMMENT '行为名称',
+    description VARCHAR(200) COMMENT '行为描述',
+    default_amount INT DEFAULT 5 COMMENT '默认能量值',
+    amount_source VARCHAR(32) DEFAULT 'fixed' COMMENT '能量值来源: fixed(固定)/points(跟随积分)/order_amount(跟随金额)/config(从配置表读取)',
+    dimension_assign JSON COMMENT '维度分配: [{"dim":"action","ratio":100}]',
+    daily_limit INT DEFAULT 0 COMMENT '每日上限(0=不限制)',
+    cooldown_seconds INT DEFAULT 0 COMMENT '冷却时间(秒,0=无冷却)',
+    expire_days INT DEFAULT 0 COMMENT '过期天数(0=永久)',
+    enabled TINYINT DEFAULT 1 COMMENT '是否启用: 0=禁用 1=启用',
+    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    UNIQUE KEY uk_behavior_code (behavior_code)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='能量行为配置表';