Просмотр исходного кода

docs: 能量行为配置与系统功能关联机制设计

E2E Test Bot 1 месяц назад
Родитель
Сommit
69315a762c
1 измененных файлов с 245 добавлено и 0 удалено
  1. 245 0
      docs/analysis/energy-behavior-integration.md

+ 245 - 0
docs/analysis/energy-behavior-integration.md

@@ -0,0 +1,245 @@
+# 能量行为配置与系统功能的关联机制
+
+## 当前实现:硬编码调用
+
+### 调用链路
+
+```
+用户完成动作
+    ↓
+业务 Service (TaskService / GameRecordService / ...)
+    ↓
+调用 energyService.awardEnergy(childId, sourceType, sourceId, amount, desc, expireDays)
+    ↓
+awardEnergy() 内部:
+    1. 查 energy_source_config → sourceType + sourceId 精确匹配
+    2. 有配置 → 按配置维度比例分配
+    3. 无配置 → 全部归入 action(行) 维度
+    ↓
+    4. 日上限检查
+    5. 按比例分配到各维度
+    6. 更新余额 + 写流水
+```
+
+### 具体例子:完成任务
+
+```java
+// 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`:
+```sql
+SELECT * FROM energy_source_config
+WHERE source_type = 'task' AND source_id = 123;
+```
+
+`energy_source_config` 表中只有 `source_type='product'` 的5条记录,没有 `task` 的配置。
+所以 `resolveDimensionRatios()` 返回空 → 触发 Fallback:
+```java
+// 默认全部归入 action(行) 维度,100%
+return Collections.singletonMap(action.getId(), 10000);
+```
+
+**结果:所有来源的能量都进入了 action 维度。**
+
+## 改造方案:配置驱动
+
+### 新表:energy_behavior_config
+
+每条记录定义一个"行为"的完整能量规则:
+
+```sql
+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. 分配到各维度 → 更新余额 + 写流水
+```
+
+### 具体例子:改造后
+
+```java
+// 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);
+```
+
+```java
+// 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()` 方法内部增加配置读取逻辑:
+
+```java
+// 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. 查看各行为每日发放统计