Преглед изворни кода

feat(unlock-gates): 通关奖励真实发放(CF值+能量)+管理端关卡列表分页

- UnlockGateService.grantGateRewards 真实发放: PlatformPointsService.earn(CF值) + EnergyService.awardEnergy(能量), 失败回滚
- refreshFamilyGates 增加 triggerUserId 重载, 4处钩子(FiveDimensionSelfCheck/FamilyInvite/FamilyMembers/ProductOrder)传入触发用户
- 前端文案统一: 小程序 gate-lock/gates + 管理端 gates 的'积分'改为'CF值'
- AdminUnlockGateController.list 分页(selectPage, page/size 参数)
- cfc-web: getUnlockGateList 透传分页参数 + gates.vue 加 el-pagination
E2E Test Bot пре 2 месеци
родитељ
комит
36f3cd9a28

+ 12 - 3
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminUnlockGateController.java

@@ -1,6 +1,8 @@
 package com.etotem.cfc.controller.admin;
 
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.etotem.cfc.common.Result;
 import com.etotem.cfc.entity.FamilyGateProgress;
 import com.etotem.cfc.entity.UnlockGate;
@@ -28,10 +30,17 @@ public class AdminUnlockGateController {
     @Resource
     private FamilyGateProgressMapper familyGateProgressMapper;
 
-    @Operation(summary = "关卡列表")
+    @Operation(summary = "关卡列表(分页)")
     @PostMapping("/list")
-    public Result<List<UnlockGate>> list() {
-        List<UnlockGate> gates = unlockGateMapper.selectList(
+    public Result<IPage<UnlockGate>> list(@RequestBody(required = false) Map<String, Object> body) {
+        long page = 1;
+        long size = 10;
+        if (body != null) {
+            if (body.get("page") != null) page = Long.parseLong(body.get("page").toString());
+            if (body.get("size") != null) size = Long.parseLong(body.get("size").toString());
+        }
+        Page<UnlockGate> pager = new Page<>(page, size);
+        IPage<UnlockGate> gates = unlockGateMapper.selectPage(pager,
                 new LambdaQueryWrapper<UnlockGate>().orderByAsc(UnlockGate::getSortOrder));
         return Result.success(gates);
     }

+ 1 - 1
cfc-backend/src/main/java/com/etotem/cfc/controller/family/FamilyInviteController.java

@@ -321,7 +321,7 @@ public class FamilyInviteController {
             // 触发通关解锁刷新:邀请家人成功
             User user = userMapper.selectById(userId);
             if (user != null && user.getFamilyId() != null) {
-                unlockGateService.refreshFamilyGates(user.getFamilyId());
+                unlockGateService.refreshFamilyGates(user.getFamilyId(), userId);
             }
             return Result.success("加入家庭成功");
         } catch (Exception e) {

+ 4 - 4
cfc-backend/src/main/java/com/etotem/cfc/controller/family/FamilyMembersController.java

@@ -41,10 +41,10 @@ public Result<FamilyMemberVO> addMember(@RequestBody AddFamilyMemberDTO dto,
  try {
    FamilyMemberVO vo = familyMemberService.addMember(userId, dto);
    // 触发通关解锁刷新:创建家人成功
-   User user = userMapper.selectById(userId);
-   if (user != null && user.getFamilyId() != null) {
-       unlockGateService.refreshFamilyGates(user.getFamilyId());
-   }
+    User user = userMapper.selectById(userId);
+    if (user != null && user.getFamilyId() != null) {
+        unlockGateService.refreshFamilyGates(user.getFamilyId(), userId);
+    }
    return Result.success(vo);
  } catch (IllegalArgumentException e) {
    return Result.error(e.getMessage());

+ 1 - 1
cfc-backend/src/main/java/com/etotem/cfc/controller/family/FiveDimensionSelfCheckController.java

@@ -60,7 +60,7 @@ public class FiveDimensionSelfCheckController {
             if (user != null) {
                 unlockGateService.refreshPersonalGates(userId, user.getFamilyId());
                 if (user.getFamilyId() != null) {
-                    unlockGateService.refreshFamilyGates(user.getFamilyId());
+                    unlockGateService.refreshFamilyGates(user.getFamilyId(), userId);
                 }
             }
             return Result.success(vo);

+ 1 - 1
cfc-backend/src/main/java/com/etotem/cfc/service/ProductOrderService.java

@@ -710,7 +710,7 @@ public class ProductOrderService {
         // 触发通关解锁刷新:购物成功(PURCHASE 关卡)
         try {
             if (order.getFamilyId() != null) {
-                unlockGateService.refreshFamilyGates(order.getFamilyId());
+                unlockGateService.refreshFamilyGates(order.getFamilyId(), order.getBuyerId());
             }
         } catch (Exception e) {
             log.error("刷新通关解锁进度失败: orderNo={}", orderNo, e);

+ 51 - 10
cfc-backend/src/main/java/com/etotem/cfc/service/UnlockGateService.java

@@ -51,6 +51,9 @@ public class UnlockGateService {
     @Resource
     private EnergyService energyService;
 
+    @Resource
+    private PlatformPointsService platformPointsService;
+
     // ========== 核心判定 ==========
 
     /**
@@ -168,6 +171,15 @@ public class UnlockGateService {
      * 刷新家庭全部关卡进度(幂等,写 family_gate_progress)
      */
     public void refreshFamilyGates(Long familyId) {
+        refreshFamilyGates(familyId, null);
+    }
+
+    /**
+     * 刷新家庭全部关卡进度(幂等,写 family_gate_progress)
+     *
+     * @param triggerUserId 触发本次刷新的用户(用于发放通关奖励;可为 null)
+     */
+    public void refreshFamilyGates(Long familyId, Long triggerUserId) {
         if (familyId == null) return;
         List<UnlockGate> gates = unlockGateMapper.selectList(
                 new LambdaQueryWrapper<UnlockGate>().eq(UnlockGate::getEnabled, 1));
@@ -193,7 +205,7 @@ public class UnlockGateService {
                         familyGateProgressMapper.updateById(progress);
                     }
                     // 发放奖励
-                    grantGateRewards(familyId, gate);
+                    grantGateRewards(familyId, triggerUserId, gate);
                 }
             } else {
                 if (progress == null) {
@@ -244,7 +256,7 @@ public class UnlockGateService {
                         progress.setUnlockedAt(new Date());
                         familyGateProgressMapper.updateById(progress);
                     }
-                    grantGateRewards(familyId, gate);
+                    grantGateRewards(familyId, userId, gate);
                 }
             }
         }
@@ -253,19 +265,48 @@ public class UnlockGateService {
     // ========== 奖励发放 ==========
 
     /**
-     * 发放关卡通关奖励(积分+能量),幂等
+     * 发放关卡通关奖励(CF值 + 能量),幂等:
+     * 仅在进度 LOCKED → UNLOCKED 转换时由调用方触发,不会重复发放。
+     *
+     * @param triggerUserId 触发解锁的用户(家长=userId,孩子=对应 FamilyMember.id 由其映射)
      */
-    private void grantGateRewards(Long familyId, UnlockGate gate) {
+    private void grantGateRewards(Long familyId, Long triggerUserId, UnlockGate gate) {
         if (gate.getRewardPoints() == null && gate.getRewardEnergy() == null) return;
-        // 积分发放:通过积分服务
+        if (triggerUserId == null) {
+            log.info("关卡[{}]通关无触发用户,跳过奖励发放: familyId={}", gate.getName(), familyId);
+            return;
+        }
+        // CF值发放(平台积分,走 PlatformPointsService)
         if (gate.getRewardPoints() != null && gate.getRewardPoints() > 0) {
             try {
-                // 积分发放逻辑(调用现有积分服务)
-                // 目前先通过 energyService 的能量发放,积分部分后续接入
-                log.info("关卡[{}]通关奖励: 积分+{}, 能量+{}, familyId={}",
-                        gate.getName(), gate.getRewardPoints(), gate.getRewardEnergy(), familyId);
+                platformPointsService.earn(triggerUserId, gate.getRewardPoints(),
+                        "unlock_gate", gate.getId(), "通关奖励: " + gate.getName());
+                log.info("关卡[{}]通关奖励CF值+{}: userId={}", gate.getName(), gate.getRewardPoints(), triggerUserId);
+            } catch (Exception e) {
+                log.warn("关卡CF值奖励发放失败: userId={}, gateId={}, error={}",
+                        triggerUserId, gate.getId(), e.getMessage());
+            }
+        }
+        // 能量发放:家长直接用 userId,孩子映射到 FamilyMember.id(EnergyService 的 memberId 语义)
+        if (gate.getRewardEnergy() != null && gate.getRewardEnergy() > 0) {
+            Long energyMemberId = triggerUserId;
+            try {
+                if (familyId != null) {
+                    FamilyMember fm = familyMemberMapper.selectOne(
+                            new LambdaQueryWrapper<FamilyMember>()
+                                    .eq(FamilyMember::getFamilyId, familyId)
+                                    .eq(FamilyMember::getUserId, triggerUserId)
+                                    .last("LIMIT 1"));
+                    if (fm != null) {
+                        energyMemberId = fm.getId();
+                    }
+                }
+                energyService.awardEnergy(energyMemberId, "unlock_gate", gate.getId(),
+                        gate.getRewardEnergy(), "通关奖励: " + gate.getName(), null);
+                log.info("关卡[{}]通关奖励能量+{}: memberId={}", gate.getName(), gate.getRewardEnergy(), energyMemberId);
             } catch (Exception e) {
-                log.warn("关卡奖励发放失败: {}", e.getMessage());
+                log.warn("关卡能量奖励发放失败: memberId={}, gateId={}, error={}",
+                        energyMemberId, gate.getId(), e.getMessage());
             }
         }
     }

+ 2 - 2
cfc-frontend/components/gate-lock.vue

@@ -6,7 +6,7 @@
     <text class="gate-desc">{{ gateData.description }}</text>
     <view class="gate-reward" v-if="totalReward > 0">
       <text class="reward-text">通关奖励</text>
-      <text class="reward-points" v-if="gateData.rewardPoints > 0">+{{ gateData.rewardPoints }}积分</text>
+      <text class="reward-points" v-if="gateData.rewardPoints > 0">+{{ gateData.rewardPoints }} CF值</text>
       <text class="reward-energy" v-if="gateData.rewardEnergy > 0">+{{ gateData.rewardEnergy }}能量</text>
     </view>
     <button class="gate-btn" @click="goAction">{{ btnText }}</button>
@@ -62,7 +62,7 @@ export default {
           url = '/pages/family/self-check'
           break
         case 'INVITE_FAMILY':
-          url = '/pages/family/address-edit'
+          url = '/pages/profile-extra/family-members'
           break
         case 'CREATE_FAMILY_MEMBER':
           url = '/pages/profile-extra/family-members'

+ 4 - 4
cfc-frontend/pages/profile-extra/gates.vue

@@ -15,7 +15,7 @@
           <text class="gate-desc">{{ g.description }}</text>
           <view class="gate-reward" v-if="g.rewardPoints > 0 || g.rewardEnergy > 0">
             <text class="reward-label">奖励:</text>
-            <text class="reward-points" v-if="g.rewardPoints > 0">+{{ g.rewardPoints }}积分</text>
+            <text class="reward-points" v-if="g.rewardPoints > 0">+{{ g.rewardPoints }} CF值</text>
             <text class="reward-energy" v-if="g.rewardEnergy > 0">+{{ g.rewardEnergy }}能量</text>
           </view>
         </view>
@@ -42,7 +42,7 @@
           <text class="gate-desc">{{ g.description }}</text>
           <view class="gate-reward" v-if="g.rewardPoints > 0 || g.rewardEnergy > 0">
             <text class="reward-label">奖励:</text>
-            <text class="reward-points" v-if="g.rewardPoints > 0">+{{ g.rewardPoints }}积分</text>
+            <text class="reward-points" v-if="g.rewardPoints > 0">+{{ g.rewardPoints }} CF值</text>
             <text class="reward-energy" v-if="g.rewardEnergy > 0">+{{ g.rewardEnergy }}能量</text>
           </view>
         </view>
@@ -67,7 +67,7 @@
           <text class="gate-desc">{{ g.description }}</text>
           <view class="gate-reward" v-if="g.rewardPoints > 0 || g.rewardEnergy > 0">
             <text class="reward-label">奖励:</text>
-            <text class="reward-points" v-if="g.rewardPoints > 0">+{{ g.rewardPoints }}积分</text>
+            <text class="reward-points" v-if="g.rewardPoints > 0">+{{ g.rewardPoints }} CF值</text>
             <text class="reward-energy" v-if="g.rewardEnergy > 0">+{{ g.rewardEnergy }}能量</text>
           </view>
         </view>
@@ -130,7 +130,7 @@ export default {
           url = '/pages/family/self-check'
           break
         case 'INVITE_FAMILY':
-          url = '/pages/family/address-edit'
+          url = '/pages/profile-extra/family-members'
           break
         case 'CREATE_FAMILY_MEMBER':
           url = '/pages/profile-extra/family-members'

+ 3 - 2
cfc-web/src/api/admin.js

@@ -819,10 +819,11 @@ export function deleteSysConfig(id) {
 
 // ========== 通关解锁关卡配置 API ==========
 
-export function getUnlockGateList() {
+export function getUnlockGateList(params) {
   return request({
     url: '/api/admin/unlock-gates/list',
-    method: 'post'
+    method: 'post',
+    data: params || {}
   })
 }
 

+ 42 - 4
cfc-web/src/views/admin/gates.vue

@@ -42,7 +42,7 @@
         </el-table-column>
         <el-table-column label="奖励" width="140">
           <template slot-scope="scope">
-            <span v-if="scope.row.rewardPoints > 0" style="color:#B8860B">+{{ scope.row.rewardPoints }}积分</span>
+            <span v-if="scope.row.rewardPoints > 0" style="color:#B8860B">+{{ scope.row.rewardPoints }} CF值</span>
             <span v-if="scope.row.rewardEnergy > 0" style="color:#3A7CC2;margin-left:6px">+{{ scope.row.rewardEnergy }}能量</span>
             <span v-if="!scope.row.rewardPoints && !scope.row.rewardEnergy">—</span>
           </template>
@@ -59,6 +59,18 @@
           </template>
         </el-table-column>
       </el-table>
+      <div style="margin-top:15px;text-align:right">
+        <el-pagination
+          background
+          layout="total, prev, pager, next, sizes"
+          :current-page="page.current"
+          :page-size="page.size"
+          :page-sizes="[10, 20, 50]"
+          :total="page.total"
+          @current-change="handlePageChange"
+          @size-change="handleSizeChange">
+        </el-pagination>
+      </div>
     </el-card>
 
     <!-- 家庭进度管理 -->
@@ -146,7 +158,7 @@
             SELF_CHECK: {"minCount":1} | MICRO_ACTION: {"count":1} | STREAK_DAYS: {"days":3}
           </div>
         </el-form-item>
-        <el-form-item label="积分奖励">
+          <el-form-item label="CF值奖励">
           <el-input-number v-model="form.rewardPoints" :min="0" :max="10000"></el-input-number>
         </el-form-item>
         <el-form-item label="能量奖励">
@@ -195,6 +207,11 @@ export default {
       loading: false,
       submitting: false,
       gateList: [],
+      page: {
+        current: 1,
+        size: 10,
+        total: 0
+      },
       dialogVisible: false,
       dialogTitle: '新增关卡',
       form: this.getEmptyForm(),
@@ -239,17 +256,38 @@ export default {
     async loadGates() {
       this.loading = true
       try {
-        const res = await getUnlockGateList()
-        this.gateList = (res.data || []).map(function(g) {
+        const res = await getUnlockGateList({
+          page: this.page.current,
+          size: this.page.size
+        })
+        var data = res.data || {}
+        var records = data.records || []
+        // 删除当前页最后一条后回退页码,避免空页
+        if (records.length === 0 && this.page.current > 1) {
+          this.page.current = this.page.current - 1
+          this.loadGates()
+          return
+        }
+        this.gateList = records.map(function(g) {
           g._enabled = g.enabled === 1
           return g
         })
+        this.page.total = data.total || 0
       } catch (e) {
         this.$message.error('加载关卡列表失败')
       } finally {
         this.loading = false
       }
     },
+    handlePageChange(page) {
+      this.page.current = page
+      this.loadGates()
+    },
+    handleSizeChange(size) {
+      this.page.size = size
+      this.page.current = 1
+      this.loadGates()
+    },
     handleAdd() {
       this.dialogTitle = '新增关卡'
       this.form = this.getEmptyForm()