|
@@ -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. 查看各行为每日发放统计
|