Sfoglia il codice sorgente

feat(points): 后台积分管理改用成员姓名+家庭列及筛选

- 后端 /api/admin/points/logs 返回 PointsLogVO,补充成员姓名与所属家庭名
- 新增 familyId 按家庭、keyword 按成员姓名模糊筛选(与 memberId 为交集)
- 前端 Points.vue:孩子ID列改为成员姓名,新增所属家庭列与家庭筛选下拉
- 同步 docs/flows/admin-overview-flow.md 接口说明
Sisyphus 6 giorni fa
parent
commit
f150dc8990

+ 107 - 5
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminController.java

@@ -7,6 +7,7 @@ import com.etotem.cfc.common.AdminRoles;
 import com.etotem.cfc.common.Result;
 import com.etotem.cfc.entity.*;
 import com.etotem.cfc.mapper.*;
+import com.etotem.cfc.dto.PointsLogVO;
 import com.etotem.cfc.dto.TaskReviewDTO;
 import com.etotem.cfc.service.TaskService;
 import com.etotem.cfc.service.UserService;
@@ -30,6 +31,8 @@ import java.util.List;
 import java.util.Map;
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
 import com.etotem.cfc.util.SortUtil;
 import com.etotem.cfc.util.ParamUtils;
 
@@ -598,10 +601,12 @@ public class AdminController {
 
     // 积分管理
     @PostMapping("/points/logs")
-    public Result<Page<PointsLog>> getPointsLogs(@RequestBody Map<String, Object> params) {
+    public Result<Page<PointsLogVO>> getPointsLogs(@RequestBody Map<String, Object> params) {
         Integer page = 1;
         Integer size = 10;
         Long memberId = null;
+        Long familyId = null;
+        String keyword = null;
         
         // 安全解析page
         if (params.get("page") != null) {
@@ -625,7 +630,7 @@ public class AdminController {
             }
         }
         
-        // 安全解析childId
+        // 安全解析childId(成员ID)
         if (params.get("memberId") != null) {
             if (params.get("memberId") instanceof Number) {
                 memberId = ((Number) params.get("memberId")).longValue();
@@ -633,13 +638,59 @@ public class AdminController {
                 try { memberId = Long.parseLong(params.get("memberId").toString()); } catch (Exception e) {}
             }
         }
+
+        // 安全解析familyId(按家庭筛选)
+        if (params.get("familyId") != null) {
+            if (params.get("familyId") instanceof Number) {
+                familyId = ((Number) params.get("familyId")).longValue();
+            } else {
+                try { familyId = Long.parseLong(params.get("familyId").toString()); } catch (Exception e) {}
+            }
+        }
+
+        // 成员姓名模糊查询
+        if (params.get("keyword") != null) {
+            keyword = params.get("keyword").toString().trim();
+            if (keyword.isEmpty()) keyword = null;
+        }
         
         String type = (String) params.get("type");
         
+        // points_log.child_id 即 family_members.id:把成员ID/家庭/姓名条件换算成成员ID集合(多条件为交集)
+        Set<Long> memberIds = null;
+        if (memberId != null) {
+            memberIds = new HashSet<>();
+            memberIds.add(memberId);
+        }
+        if (familyId != null || keyword != null) {
+            LambdaQueryWrapper<FamilyMember> memberWrapper = new LambdaQueryWrapper<>();
+            if (familyId != null) {
+                memberWrapper.eq(FamilyMember::getFamilyId, familyId);
+            }
+            if (keyword != null) {
+                memberWrapper.like(FamilyMember::getNickname, keyword);
+            }
+            memberWrapper.select(FamilyMember::getId);
+            Set<Long> filteredIds = new HashSet<>();
+            for (FamilyMember m : familyMemberMapper.selectList(memberWrapper)) {
+                filteredIds.add(m.getId());
+            }
+            if (memberIds == null) {
+                memberIds = filteredIds;
+            } else {
+                memberIds.retainAll(filteredIds);
+            }
+        }
+        // 筛选条件已生效但无匹配成员 → 直接返回空页
+        if (memberIds != null && memberIds.isEmpty()) {
+            Page<PointsLogVO> emptyPage = new Page<>(page, size);
+            return Result.success(emptyPage);
+        }
+        
         Page<PointsLog> pageParam = new Page<>(page, size);
         LambdaQueryWrapper<PointsLog> wrapper = new LambdaQueryWrapper<>();
-        if (memberId != null) {
-            wrapper.eq(PointsLog::getChildId, memberId);
+        if (memberIds != null) {
+            wrapper.in(PointsLog::getChildId, memberIds);
         }
         if (type != null && !type.isEmpty()) {
             wrapper.eq(PointsLog::getType, type);
@@ -647,7 +698,58 @@ public class AdminController {
         wrapper.orderByDesc(PointsLog::getCreatedAt);
         SortUtil.applySort(wrapper);
         Page<PointsLog> result = pointsLogMapper.selectPage(pageParam, wrapper);
-        return Result.success(result);
+
+        // 批量补充成员姓名与所属家庭名称
+        Set<Long> logMemberIds = new HashSet<>();
+        for (PointsLog log : result.getRecords()) {
+            Long logMemberId = log.getChildId() != null ? log.getChildId() : log.getFamilyMemberId();
+            if (logMemberId != null) logMemberIds.add(logMemberId);
+        }
+        Map<Long, FamilyMember> memberMap = new HashMap<>();
+        if (!logMemberIds.isEmpty()) {
+            for (FamilyMember m : familyMemberMapper.selectList(
+                    new LambdaQueryWrapper<FamilyMember>().in(FamilyMember::getId, logMemberIds))) {
+                memberMap.put(m.getId(), m);
+            }
+        }
+        Map<Long, Family> familyMap = new HashMap<>();
+        Set<Long> familyIds = new HashSet<>();
+        for (FamilyMember m : memberMap.values()) {
+            if (m.getFamilyId() != null) familyIds.add(m.getFamilyId());
+        }
+        if (!familyIds.isEmpty()) {
+            for (Family f : familyMapper.selectList(
+                    new LambdaQueryWrapper<Family>().in(Family::getId, familyIds))) {
+                familyMap.put(f.getId(), f);
+            }
+        }
+
+        Page<PointsLogVO> voPage = new Page<>(result.getCurrent(), result.getSize(), result.getTotal());
+        List<PointsLogVO> voRecords = new ArrayList<>();
+        for (PointsLog log : result.getRecords()) {
+            PointsLogVO vo = new PointsLogVO();
+            vo.setId(log.getId());
+            vo.setChildId(log.getChildId());
+            vo.setMemberId(log.getChildId() != null ? log.getChildId() : log.getFamilyMemberId());
+            vo.setAmount(log.getAmount());
+            vo.setType(log.getType());
+            vo.setCategory(log.getCategory());
+            vo.setDescription(log.getDescription());
+            vo.setTaskId(log.getTaskId());
+            vo.setCreatedAt(log.getCreatedAt());
+            FamilyMember member = memberMap.get(vo.getMemberId());
+            if (member != null) {
+                vo.setMemberName(member.getNickname());
+                vo.setFamilyId(member.getFamilyId());
+                Family family = familyMap.get(member.getFamilyId());
+                if (family != null) {
+                    vo.setFamilyName(family.getName());
+                }
+            }
+            voRecords.add(vo);
+        }
+        voPage.setRecords(voRecords);
+        return Result.success(voPage);
     }
 
     @PostMapping("/points/stats")

+ 45 - 0
cfc-backend/src/main/java/com/etotem/cfc/dto/PointsLogVO.java

@@ -0,0 +1,45 @@
+package com.etotem.cfc.dto;
+
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * 积分流水VO(后台积分管理列表)
+ * 在 points_log 实体基础上补充成员姓名与所属家庭信息,
+ * 将历史"孩子ID"(points_log.child_id,实际为 family_members.id)展示为成员姓名,并支持按家庭筛选。
+ */
+@Data
+public class PointsLogVO {
+
+    private Long id;
+
+    /** 成员ID(points_log.child_id,历史字段名,实际为 family_members.id) */
+    private Long childId;
+
+    /** 成员ID(兼容字段,与 childId 同值) */
+    private Long memberId;
+
+    /** 成员姓名 */
+    private String memberName;
+
+    /** 所属家庭ID */
+    private Long familyId;
+
+    /** 所属家庭名称 */
+    private String familyName;
+
+    private Integer amount;
+
+    /** 积分类型: earn/spend/bonus/penalty/adjust/reset/refund */
+    private String type;
+
+    /** 积分分类: task-任务积分, system-系统积分 */
+    private String category;
+
+    private String description;
+
+    private Long taskId;
+
+    private Date createdAt;
+}

+ 44 - 10
cfc-web/src/views/Points.vue

@@ -16,14 +16,31 @@
 
       <!-- 搜索筛选 -->
       <el-form :inline="true" :model="searchForm" style="margin-bottom: 20px">
-        <el-form-item label="孩子ID">
+        <el-form-item label="成员姓名">
           <el-input
-            v-model="searchForm.memberId"
-            placeholder="请输入孩子ID"
+            v-model="searchForm.keyword"
+            placeholder="请输入成员姓名"
             clearable
             @keyup.enter.native="handleSearch"
           ></el-input>
         </el-form-item>
+        <el-form-item label="家庭">
+          <el-select
+            v-model="searchForm.familyId"
+            placeholder="全部家庭"
+            clearable
+            filterable
+            style="width: 220px"
+            @change="handleSearch"
+          >
+            <el-option
+              v-for="f in familyOptions"
+              :key="f.id"
+              :label="f.name"
+              :value="f.id"
+            ></el-option>
+          </el-select>
+        </el-form-item>
         <el-form-item label="类型">
           <el-select v-model="searchForm.type" placeholder="全部类型" clearable @change="handleSearch">
             <el-option label="获得" value="earn"></el-option>
@@ -48,9 +65,14 @@
             <span class="sort-header" @click.stop="onSortClick('id', $event)">ID<span v-if="sortIndex('id') !== null" class="sort-index" :class="sortClass('id') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('id') }}</span>{{ sortIcon('id') }}</span>
           </template>
         </el-table-column>
-        <el-table-column prop="childId" label="孩子ID" width="100">
-          <template slot="header">
-            <span class="sort-header" @click.stop="onSortClick('childId', $event)">孩子ID<span v-if="sortIndex('childId') !== null" class="sort-index" :class="sortClass('childId') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('childId') }}</span>{{ sortIcon('childId') }}</span>
+        <el-table-column prop="memberName" label="成员姓名" width="140">
+          <template slot-scope="scope">
+            <span>{{ scope.row.memberName || '-' }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="familyName" label="所属家庭" min-width="180">
+          <template slot-scope="scope">
+            <span>{{ scope.row.familyName || '-' }}</span>
           </template>
         </el-table-column>
         <el-table-column prop="amount" label="积分变动" width="120">
@@ -114,7 +136,7 @@
 
 <script>
 import SortMixin from '@/mixins/SortMixin'
-import { getPointsLogs } from '@/api/admin'
+import { getPointsLogs, getFamilyList } from '@/api/admin'
 
 export default {
   name: 'Points',
@@ -128,9 +150,11 @@ export default {
     return {
       loading: false,
       tableData: [],
-      sortableColumns: { id: 'ID', childId: '孩子ID', amount: '积分变动', type: '类型', description: '描述', taskId: '任务ID', createdAt: '时间' },
+      familyOptions: [],
+      sortableColumns: { id: 'ID', amount: '积分变动', type: '类型', description: '描述', taskId: '任务ID', createdAt: '时间' },
       searchForm: {
-        memberId: '',
+        keyword: '',
+        familyId: '',
         type: ''
       },
       pagination: {
@@ -141,9 +165,18 @@ export default {
     }
   },
   mounted() {
+    this.loadFamilyOptions()
     this.loadPointsLogs()
   },
   methods: {
+    async loadFamilyOptions() {
+      try {
+        const res = await getFamilyList({ page: 1, size: 500 })
+        this.familyOptions = (res.data && res.data.records) ? res.data.records : (res.data || [])
+      } catch (error) {
+        this.familyOptions = []
+      }
+    },
     loadList() {
       this.loadPointsLogs()
     },
@@ -171,7 +204,8 @@ export default {
     },
     handleReset() {
       this.searchForm = {
-        memberId: '',
+        keyword: '',
+        familyId: '',
         type: ''
       }
       this.handleSearch()

+ 2 - 2
docs/flows/admin-overview-flow.md

@@ -118,7 +118,7 @@ flowchart TD
     %% ================================================================
     subgraph 阶段四[阶段四:积分与模板管理]
         P2 -->|"积分日志"| G1["🏠 管理后台:积分管理页"]:::page
-        G1 -->|"分页, 孩子ID, 类型"| A27["🔗 POST /api/admin/points/logs"]:::api
+        G1 -->|"分页, 成员姓名/家庭, 类型"| A27["🔗 POST /api/admin/points/logs"]:::api
         A27 -->|"childId, type, page"| S27["⚙️ PointsLogMapper.selectPage()"]:::service
         S27 -->|"分页积分流水"| D_POINTS
         G1 -->|"积分统计"| A28["🔗 POST /api/admin/points/stats"]:::api
@@ -276,7 +276,7 @@ flowchart TD
 
 | 端点 | 说明 | 端口 | 请求数据 | 响应数据 |
 |------|------|------|---------|---------|
-| `POST /api/admin/points/logs` | 积分日志分页 | 🖥管理后台 | `{page, size, childId, type}` | `Page<PointsLog>` |
+| `POST /api/admin/points/logs` | 积分日志分页(按成员/家庭筛选,返回成员姓名+家庭名) | 🖥管理后台 | `{page, size, memberId, familyId, keyword, type}` | `Page<PointsLogVO>`(含 childId/memberId/memberName/familyId/familyName) |
 | `POST /api/admin/points/stats` | 积分统计概览 | 🖥管理后台 | — | `{totalChildren}` |
 
 ### 任务模板管理