energy-behavior-integration.md 9.6 KB

能量行为配置与系统功能的关联机制

当前实现:硬编码调用

调用链路

用户完成动作
    ↓
业务 Service (TaskService / GameRecordService / ...)
    ↓
调用 energyService.awardEnergy(childId, sourceType, sourceId, amount, desc, expireDays)
    ↓
awardEnergy() 内部:
    1. 查 energy_source_config → sourceType + sourceId 精确匹配
    2. 有配置 → 按配置维度比例分配
    3. 无配置 → 全部归入 action(行) 维度
    ↓
    4. 日上限检查
    5. 按比例分配到各维度
    6. 更新余额 + 写流水

具体例子:完成任务

// TaskService.java 第414-424行 — 完成任务时触发
try {
    int energyAmount = Math.abs(pointsEarned);  // 能量值 = 任务积分
    if (energyAmount > 0) {
        energyService.awardEnergy(
            childId,           // 孩子ID
            "task",            // sourceType = "task"
            taskId,            // sourceId = 具体任务ID
            energyAmount,      // 能量值 = 积分
            "完成任务: " + title,  // 描述
            null               // 不过期
        );
    }
} catch (Exception e) {
    log.warn("能量发放失败(不影响积分)");
}

问题:维度分配全部走 Fallback

awardEnergy() 内部查 energy_source_config

SELECT * FROM energy_source_config
WHERE source_type = 'task' AND source_id = 123;

energy_source_config 表中只有 source_type='product' 的5条记录,没有 task 的配置。 所以 resolveDimensionRatios() 返回空 → 触发 Fallback:

// 默认全部归入 action(行) 维度,100%
return Collections.singletonMap(action.getId(), 10000);

结果:所有来源的能量都进入了 action 维度。

改造方案:配置驱动

新表:energy_behavior_config

每条记录定义一个"行为"的完整能量规则:

energy_behavior_config (
    behavior_code   VARCHAR(32)  -- 行为编码: task_complete / game_finish / ...
    behavior_name   VARCHAR(50)  -- 行为名称: 完成任务 / 完成游戏 / ...
    default_amount  INT          -- 默认能量值
    amount_source   VARCHAR(32)  -- 能量值来源: fixed / points / order_amount / config
    dimension_assign JSON        -- 维度分配: [{"dim":"action","ratio":100}]
    daily_limit     INT          -- 每日上限
    cooldown_seconds INT         -- 冷却时间
    expire_days     INT          -- 过期天数
    enabled         TINYINT      -- 是否启用
)

改造后的调用链路

用户完成动作
    ↓
业务 Service (TaskService)
    ↓
调用 energyService.awardByBehavior(childId, "task_complete", context)
    ↓
awardByBehavior() 内部:
    1. 查 energy_behavior_config → behavior_code = "task_complete"
    2. 配置不存在或不启用 → 返回,不给能量
    3. 配置存在 → 读取 amount_source 和 dimension_assign
         │
         ├─ amount_source = "points"    → 从 context 取积分值
         ├─ amount_source = "fixed"     → 用 default_amount
         ├─ amount_source = "order_amount" → 从 context 取订单金额
         └─ amount_source = "config"    → 从关联配置表读取
         │
         └─ dimension_assign = [{"dim":"action","ratio":50},{"dim":"wisdom","ratio":50}]
             → 按比例分配到各维度
    ↓
    4. 日上限检查
    5. 分配到各维度 → 更新余额 + 写流水

具体例子:改造后

// TaskService.java — 改造后
// 不再直接调用 awardEnergy,而是调用 awardByBehavior

// 构造上下文:包含所有可能需要的参数
Map<String, Object> context = new HashMap<>();
context.put("childId", childId);
context.put("sourceId", taskId);
context.put("points", pointsEarned);          // 积分值
context.put("taskTitle", task.getTitle());

energyService.awardByBehavior(childId, "task_complete", context);
// EnergyService.java — 新增方法
public void awardByBehavior(Long childId, String behaviorCode, Map<String, Object> context) {
    // 1. 查配置
    EnergyBehaviorConfig config = behaviorConfigMapper.selectOne(
        new LambdaQueryWrapper<EnergyBehaviorConfig>()
            .eq(EnergyBehaviorConfig::getBehaviorCode, behaviorCode)
            .eq(EnergyBehaviorConfig::getEnabled, 1)
    );
    if (config == null) return;  // 无配置或未启用,不给能量

    // 2. 计算能量值
    int amount = 0;
    switch (config.getAmountSource()) {
        case "fixed":
            amount = config.getDefaultAmount();
            break;
        case "points":
            Integer points = (Integer) context.get("points");
            amount = points != null ? Math.abs(points) : config.getDefaultAmount();
            break;
        case "order_amount":
            Integer orderAmount = (Integer) context.get("orderAmount");
            amount = orderAmount != null ? orderAmount : config.getDefaultAmount();
            break;
    }
    if (amount <= 0) return;

    // 3. 解析维度分配配置
    // dimension_assign = [{"dim":"action","ratio":50},{"dim":"wisdom","ratio":50}]
    JSONArray dimAssign = JSON.parseArray(config.getDimensionAssign());
    Map<Long, Integer> dimRatios = new LinkedHashMap<>();
    for (int i = 0; i < dimAssign.size(); i++) {
        JSONObject item = dimAssign.getJSONObject(i);
        String dimCode = item.getString("dim");
        int ratio = item.getInt("ratio");
        EnergyDimension dim = getDimByCode(dimCode);
        if (dim != null) {
            dimRatios.put(dim.getId(), ratio * 100);  // 转为10000制
        }
    }
    if (dimRatios.isEmpty()) return;

    // 4. 日上限检查
    if (isDailyLimitExceeded(childId, dimRatios.keySet(), amount)) {
        log.warn("日上限已达,跳过: behaviorCode={}, childId={}", behaviorCode, childId);
        return;
    }

    // 5. 按比例分配 + 更新余额 + 写流水
    Map<Long, Integer> allocations = calculateAllocations(amount, dimRatios);
    // ... 更新各维度余额和流水 ...
}

所有行为如何关联到系统代码

现有行为(19个)

behavior_code 触发位置 当前代码
task_complete TaskService.completeTask() 硬编码 awardEnergy(childId, "task", ...)
game_finish GameRecordService.completeGame() 硬编码 awardEnergy(childId, "game", ...)
activity_checkin ActivityService.checkin() 硬编码 awardEnergy(childId, "activity", ...)
article_read ArticleService.completeArticleRead() 硬编码 awardEnergy(childId, "article", ...)
product_purchase ProductOrderService.handlePayment() 硬编码 awardEnergy(childId, "product", ...)
health_checkin HealthCheckinService.checkin() 硬编码 awardEnergy(childId, "health_checkin", ...)
finance_checkin FinanceCheckinService.checkin() 硬编码 awardEnergy(childId, "checkin", ...)
emotion_checkin EmotionCheckinService.checkin() 硬编码 awardEnergy(childId, "emotion_checkin", ...)
appointment_submit AssessmentAppointmentService.create() 硬编码 awardEnergy(childId, "appointment", ...)
appointment_complete AssessmentAppointmentService.complete() 硬编码 awardEnergy(childId, "appointment", ...)
streak_daily StreakService.checkStreak() 硬编码 awardEnergy(childId, "streak", ...)
streak_milestone StreakService.checkStreak() 硬编码同上
growth_task GrowthTaskService.completeTask() 硬编码 awardEnergy(childId, "growth_day", ...)
micro_action MicroActionService.complete() 硬编码 awardEnergy(childId, "micro_action", ...)
onboarding OnboardingService.claim() 硬编码 awardEnergy(childId, "onboarding", ...)
invite_milestone InviteMilestoneService.check() 硬编码 awardEnergy(childId, "invite_milestone", ...)
dimension_sync DimensionEnergySyncService.sync() 硬编码 awardEnergy(childId, "dimension", ...)

新增行为(5个,尚未实现)

behavior_code 触发位置 实现方式
invite_friend CommissionService.bindReferral() 绑定推荐人时触发
invite_family FamilyInviteService.acceptByCode() 家庭成员接受邀请时触发
report_upload HealthReportController.uploadAndParse() 上传体检报告时触发
plan_complete GrowthPlanService.reviewPlan() 完成成长计划时触发
wisdom_report DanAssessmentController.uploadReport() 上传智测评报告时触发

改造步骤

第一步:替换现有硬编码调用(最小改动)

不改业务逻辑,只在 awardEnergy() 方法内部增加配置读取逻辑:

// EnergyService.awardEnergy() — 改造
public Map<String, Integer> awardEnergy(Long childId, String sourceType, Long sourceId, ...) {
    // 新增:先查 behavior_config
    EnergyBehaviorConfig config = matchBehaviorConfig(sourceType);
    if (config != null && config.getEnabled()) {
        // 使用配置的维度分配
        return awardByConfig(childId, config, sourceType, sourceId, totalAmount, description, daysToExpire);
    }
    // 无配置 → 原有逻辑(查 energy_source_config + Fallback)
    ...
}

这样所有现有调用代码不用改,只需要在 awardEnergy() 内部增加一个配置查询点。

第二步:添加配置数据

energy_behavior_config 表中插入预置数据,每个 sourceType 对应一条配置。

第三步:管理后台

实现管理后台的 CRUD 页面,让管理员可以:

  1. 启用/禁用某个行为
  2. 修改能量值
  3. 修改维度分配比例
  4. 查看各行为每日发放统计