|
|
@@ -0,0 +1,853 @@
|
|
|
+# 审核中心统一详情页审核 + 服务角色申请分流合并 实施计划
|
|
|
+
|
|
|
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
|
+
|
|
|
+**Goal:** 让审核中心 12 个 Tab 统一"点开详情抽屉 → 通过/驳回(驳回必填意见)",并把小程序服务角色申请按角色分流合并进对应 Tab,删除独立菜单。
|
|
|
+
|
|
|
+**Architecture:** 前端新增通用 `AuditDetailDrawer` 组件承载详情与审核动作;每个 Tab 保留原列表数据源,涉及服务角色的 5 个 Tab 额外拉取 `/api/service-role/admin/list` 并在前端合并、去重、排序。后端只做增量:列表过滤/用户信息补充、审核通过逻辑修复、统计合并、幂等迁移。
|
|
|
+
|
|
|
+**Tech Stack:** Vue 2 + Element UI + Jest(vue-jest);Spring Boot 2.7.18 + MyBatis-Plus + JUnit 5 + Mockito;MySQL 8.0。
|
|
|
+
|
|
|
+**Spec:** `docs/superpowers/specs/2026-10-19-review-center-unified-detail-design.md`
|
|
|
+
|
|
|
+## Global Constraints
|
|
|
+
|
|
|
+- 生产库是 MySQL 8.0.34,DDL 必须幂等(`ADD COLUMN IF NOT EXISTS` 不支持下,使用 `information_schema` 判断或 try/catch 忽略重复错误)。
|
|
|
+- 所有驳回操作:原因 trim 后为空必须阻止提交(前端 + 后端双重校验)。
|
|
|
+- 待退款 Tab 只有「中止退款」,不得出现"通过"。
|
|
|
+- 不改审核权限模型(不新增后端角色校验)。
|
|
|
+- 代码在远程 `root@111.228.6.214:/app/cfc`,生产在 `/app/pro/cfc`;部署前必须备份。
|
|
|
+- 每个任务结束提交一次 git commit。
|
|
|
+
|
|
|
+## Review Focus
|
|
|
+
|
|
|
+1. **多角色 JSON 匹配**:`service_role_applications.roles` 是 JSON 数组字符串,过滤 `vendor` 时不能把 `activity_provider` 漏掉,也不能把 `planner` 误匹配进来。
|
|
|
+2. **重复行**:同一 userId 同时有 `users.*_status=pending` 和 service_role 待审记录时,合并列表只能出现一行(优先 service_role)。
|
|
|
+3. **空驳回意见**:抽屉里点"确认驳回"但意见为空/全空格时,必须停留在抽屉并提示,不能发请求。
|
|
|
+4. **资金动作语义**:待退款 Tab 的「中止退款」只能作用于 `pending`/`processing`,不能把 `success`/`failed` 记录变成可操作。
|
|
|
+5. **已审核记录**:`approved`/`rejected` 记录打开抽屉时不得显示通过/驳回按钮,只展示审核结果。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## File Structure
|
|
|
+
|
|
|
+**后端(修改)**
|
|
|
+- `cfc-backend/src/main/java/com/etotem/cfc/controller/ServiceRoleApplicationController.java` — 列表过滤参数与权限保留
|
|
|
+- `cfc-backend/src/main/java/com/etotem/cfc/service/ServiceRoleApplicationService.java` — 新增带用户信息的列表方法
|
|
|
+- `cfc-backend/src/main/java/com/etotem/cfc/service/impl/ServiceRoleApplicationServiceImpl.java` — 过滤/富化 + approve 修复
|
|
|
+- `cfc-backend/src/main/java/com/etotem/cfc/controller/admin/ReviewCenterController.java` — 统计合并 + knowledgePending
|
|
|
+- `cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminActivityRegistrationController.java` — 驳回原因参数
|
|
|
+- `cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminCommissionController.java` — 中止退款原因参数
|
|
|
+- `cfc-backend/src/main/java/com/etotem/cfc/service/PendingRefundService.java` — 中止退款写入原因
|
|
|
+- `cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminButlerController.java` — 管家用户申请列表接口
|
|
|
+- `cfc-backend/src/main/java/com/etotem/cfc/service/ActivityRegistrationService.java` — reject 增加 reason
|
|
|
+- `cfc-backend/src/main/java/com/etotem/cfc/entity/ActivityRegistration.java` — rejectReason 字段
|
|
|
+- `cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java` — 迁移 382-384
|
|
|
+
|
|
|
+**后端(新增测试)**
|
|
|
+- `cfc-backend/src/test/java/com/etotem/cfc/unit/ServiceRoleFilterTest.java`
|
|
|
+- `cfc-backend/src/test/java/com/etotem/cfc/unit/ServiceRoleApproveTest.java`
|
|
|
+- `cfc-backend/src/test/java/com/etotem/cfc/unit/ReviewCenterSummaryTest.java`
|
|
|
+- `cfc-backend/src/test/java/com/etotem/cfc/unit/ActivityRegistrationRejectTest.java`
|
|
|
+
|
|
|
+**前端(新增)**
|
|
|
+- `cfc-web/src/views/admin/review/components/AuditDetailDrawer.vue`
|
|
|
+- `cfc-web/src/views/admin/review/KnowledgeAdminAudit.vue`
|
|
|
+- `cfc-web/src/views/admin/review/__tests__/AuditDetailDrawer.spec.js`
|
|
|
+- `cfc-web/src/views/admin/review/__tests__/auditRowMapping.spec.js`
|
|
|
+- `cfc-web/src/utils/auditRowMapping.js`
|
|
|
+- `cfc-web/src/api/service-role.js`
|
|
|
+
|
|
|
+**前端(修改)**
|
|
|
+- 12 个 Tab:`cfc-web/src/views/admin/review/ArticleAudit.vue`、`ProductAudit.vue`、`GuideAudit.vue`、`NutritionistAudit.vue`、`VendorAudit.vue`、`ButlerAudit.vue`、`PackageAudit.vue`、`ActivityAudit.vue`、`ActivityRegistrationAudit.vue`、`WithdrawAudit.vue`、`PendingRefundAudit.vue`、`HierarchyAudit.vue`
|
|
|
+- `cfc-web/src/views/admin/ReviewCenter.vue`
|
|
|
+- `cfc-web/src/views/Layout.vue`
|
|
|
+- `cfc-web/src/router/index.js`
|
|
|
+- `cfc-web/src/api/activity.js`(活动报名驳回 reason)
|
|
|
+
|
|
|
+**前端(删除)**
|
|
|
+- `cfc-web/src/views/service/ServiceRoleApplications.vue`
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Task 1: 服务角色申请列表接口(role/status 过滤 + 用户信息)
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `cfc-backend/src/main/java/com/etotem/cfc/controller/ServiceRoleApplicationController.java:91-100`
|
|
|
+- Modify: `cfc-backend/src/main/java/com/etotem/cfc/service/ServiceRoleApplicationService.java`
|
|
|
+- Modify: `cfc-backend/src/main/java/com/etotem/cfc/service/impl/ServiceRoleApplicationServiceImpl.java`
|
|
|
+- Test: `cfc-backend/src/test/java/com/etotem/cfc/unit/ServiceRoleFilterTest.java`
|
|
|
+
|
|
|
+**Interfaces:**
|
|
|
+- Produces:
|
|
|
+ - `public static List<String> ServiceRoleApplicationServiceImpl.parseRoleFilter(String roleParam)` — `null`/空 → 空列表;逗号分隔、trim、去重。
|
|
|
+ - `public static boolean ServiceRoleApplicationServiceImpl.matchesAnyRole(String rolesJson, List<String> targetRoles)` — `rolesJson` 解析失败或无交集 → false。
|
|
|
+ - `List<Map<String,Object>> ServiceRoleApplicationService.listApplicationsWithUserInfo(Integer status, List<String> roleFilter)`
|
|
|
+ - 返回行字段:原实体全部字段 + `userName`、`realName`、`phone`。
|
|
|
+
|
|
|
+- [ ] **Step 1: 写失败测试**
|
|
|
+
|
|
|
+```java
|
|
|
+package com.etotem.cfc.unit;
|
|
|
+
|
|
|
+import com.etotem.cfc.service.impl.ServiceRoleApplicationServiceImpl;
|
|
|
+import org.junit.jupiter.api.Test;
|
|
|
+import java.util.Arrays;
|
|
|
+import java.util.Collections;
|
|
|
+import static org.junit.jupiter.api.Assertions.*;
|
|
|
+
|
|
|
+class ServiceRoleFilterTest {
|
|
|
+ @Test
|
|
|
+ void parseRoleFilter_空和null返回空列表() {
|
|
|
+ assertEquals(Collections.emptyList(), ServiceRoleApplicationServiceImpl.parseRoleFilter(null));
|
|
|
+ assertEquals(Collections.emptyList(), ServiceRoleApplicationServiceImpl.parseRoleFilter(" "));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void parseRoleFilter_逗号分隔去空格去重() {
|
|
|
+ assertEquals(Arrays.asList("vendor", "activity_provider"),
|
|
|
+ ServiceRoleApplicationServiceImpl.parseRoleFilter(" vendor, activity_provider ,vendor "));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void matchesAnyRole_按JSON数组精确匹配() {
|
|
|
+ String json = "[\"activity_provider\"]";
|
|
|
+ assertTrue(ServiceRoleApplicationServiceImpl.matchesAnyRole(json, Arrays.asList("vendor", "activity_provider")));
|
|
|
+ assertFalse(ServiceRoleApplicationServiceImpl.matchesAnyRole(json, Arrays.asList("vendor")));
|
|
|
+ assertFalse(ServiceRoleApplicationServiceImpl.matchesAnyRole(json, Arrays.asList("planner")));
|
|
|
+ assertFalse(ServiceRoleApplicationServiceImpl.matchesAnyRole("not-json", Arrays.asList("vendor")));
|
|
|
+ assertFalse(ServiceRoleApplicationServiceImpl.matchesAnyRole(null, Arrays.asList("vendor")));
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: 运行确认失败**
|
|
|
+
|
|
|
+Run: `cd /app/cfc/cfc-backend && mvn -q -Dtest=ServiceRoleFilterTest test`
|
|
|
+Expected: 编译失败 `cannot find symbol parseRoleFilter`.
|
|
|
+
|
|
|
+- [ ] **Step 3: 实现过滤与富化**
|
|
|
+
|
|
|
+在 `ServiceRoleApplicationServiceImpl` 加静态方法(解析用 `JSON.parseArray`,失败返回空列表)。新增 `listApplicationsWithUserInfo(Integer status, List<String> roleFilter)`:先 `this.list(status 条件)`,再 `matchesAnyRole` 过滤,最后批量 `userMapper.selectBatchIds` 填充 `userName/realName/phone`,按 `createdAt` 倒序。Controller `adminList` 增加 `@RequestParam(required=false) String role`,调用新方法。
|
|
|
+
|
|
|
+- [ ] **Step 4: 跑测试确认通过**
|
|
|
+
|
|
|
+Run: `cd /app/cfc/cfc-backend && mvn -q -Dtest=ServiceRoleFilterTest test`
|
|
|
+Expected: `BUILD SUCCESS`,3 个测试通过。
|
|
|
+
|
|
|
+- [ ] **Step 5: 提交**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd /app/cfc && git add cfc-backend/src/main/java/com/etotem/cfc/controller/ServiceRoleApplicationController.java cfc-backend/src/main/java/com/etotem/cfc/service/ServiceRoleApplicationService.java cfc-backend/src/main/java/com/etotem/cfc/service/impl/ServiceRoleApplicationServiceImpl.java cfc-backend/src/test/java/com/etotem/cfc/unit/ServiceRoleFilterTest.java
|
|
|
+git commit -m "feat(service-role): 申请列表支持角色/状态过滤并补充用户信息"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Task 2: 修复服务角色审核通过逻辑
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `cfc-backend/src/main/java/com/etotem/cfc/service/impl/ServiceRoleApplicationServiceImpl.java:83-148`
|
|
|
+- Test: `cfc-backend/src/test/java/com/etotem/cfc/unit/ServiceRoleApproveTest.java`
|
|
|
+
|
|
|
+**Interfaces:**
|
|
|
+- Consumes: `ServiceRoleApplicationServiceImpl` 的 `userMapper`、`guideMapper`、`butlerProfileMapper`,新增 `adminMapper`。
|
|
|
+- Produces: `public void applyApprovedRole(String role, ServiceRoleApplication app, User user, List<String> ownedRoles)`(`public`,测试在同模块不同包)。
|
|
|
+
|
|
|
+- [ ] **Step 1: 写失败测试**
|
|
|
+
|
|
|
+```java
|
|
|
+package com.etotem.cfc.unit;
|
|
|
+
|
|
|
+import com.etotem.cfc.entity.*;
|
|
|
+import com.etotem.cfc.mapper.*;
|
|
|
+import com.etotem.cfc.service.impl.ServiceRoleApplicationServiceImpl;
|
|
|
+import org.junit.jupiter.api.BeforeEach;
|
|
|
+import org.junit.jupiter.api.Test;
|
|
|
+import java.lang.reflect.Field;
|
|
|
+import java.util.ArrayList;
|
|
|
+import java.util.List;
|
|
|
+import static org.junit.jupiter.api.Assertions.*;
|
|
|
+import static org.mockito.Mockito.*;
|
|
|
+
|
|
|
+class ServiceRoleApproveTest {
|
|
|
+ private ServiceRoleApplicationServiceImpl service;
|
|
|
+ private GuideMapper guideMapper;
|
|
|
+ private ButlerProfileMapper butlerProfileMapper;
|
|
|
+ private AdminMapper adminMapper;
|
|
|
+
|
|
|
+ @BeforeEach
|
|
|
+ void setUp() throws Exception {
|
|
|
+ service = new ServiceRoleApplicationServiceImpl();
|
|
|
+ guideMapper = mock(GuideMapper.class);
|
|
|
+ butlerProfileMapper = mock(ButlerProfileMapper.class);
|
|
|
+ adminMapper = mock(AdminMapper.class);
|
|
|
+ set("guideMapper", guideMapper);
|
|
|
+ set("butlerProfileMapper", butlerProfileMapper);
|
|
|
+ set("adminMapper", adminMapper);
|
|
|
+ }
|
|
|
+
|
|
|
+ private void set(String name, Object v) throws Exception {
|
|
|
+ Field f = ServiceRoleApplicationServiceImpl.class.getDeclaredField(name);
|
|
|
+ f.setAccessible(true);
|
|
|
+ f.set(service, v);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void planner_通过后写teacher_status并创建admin记录() {
|
|
|
+ ServiceRoleApplication app = new ServiceRoleApplication();
|
|
|
+ app.setUserId(12L); app.setProvince("北京"); app.setCity("北京市");
|
|
|
+ User user = new User(); user.setId(12L); user.setPhone("13900000000"); user.setNickname("n");
|
|
|
+ List<String> roles = new ArrayList<>();
|
|
|
+ when(guideMapper.insert(any())).thenReturn(1);
|
|
|
+ when(adminMapper.selectOne(any())).thenReturn(null);
|
|
|
+ when(adminMapper.insert(any())).thenReturn(1);
|
|
|
+
|
|
|
+ service.applyApprovedRole("planner", app, user, roles);
|
|
|
+
|
|
|
+ assertEquals("approved", user.getTeacherStatus());
|
|
|
+ assertTrue(roles.contains("teacher"));
|
|
|
+ verify(guideMapper).insert(any(Guide.class));
|
|
|
+ verify(adminMapper).insert(any(Admin.class));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void butler_通过后写butler_status且profile状态为approved() {
|
|
|
+ ServiceRoleApplication app = new ServiceRoleApplication();
|
|
|
+ app.setUserId(12L);
|
|
|
+ User user = new User(); user.setId(12L);
|
|
|
+ List<String> roles = new ArrayList<>();
|
|
|
+ when(butlerProfileMapper.selectOne(any())).thenReturn(null);
|
|
|
+ when(butlerProfileMapper.insert(any())).thenReturn(1);
|
|
|
+
|
|
|
+ service.applyApprovedRole("butler", app, user, roles);
|
|
|
+
|
|
|
+ assertEquals("approved", user.getButlerStatus());
|
|
|
+ assertTrue(roles.contains("butler"));
|
|
|
+ verify(butlerProfileMapper).insert(argThat(p -> "approved".equals(p.getStatus())));
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: 运行确认失败**
|
|
|
+
|
|
|
+Run: `cd /app/cfc/cfc-backend && mvn -q -Dtest=ServiceRoleApproveTest test`
|
|
|
+Expected: 编译失败 `cannot find symbol applyApprovedRole`.
|
|
|
+
|
|
|
+- [ ] **Step 3: 实现**
|
|
|
+
|
|
|
+把 `approve` 里的 `switch` 抽成 `applyApprovedRole`,并补齐:
|
|
|
+- `planner`:`user.setTeacherStatus("approved")`;插入 Guide;roles 加 teacher;按手机号 upsert `admins`(复用 `AdminController.approveGuide` 的字段:phone/username/realName/nickname/role=teacher/status=1)。
|
|
|
+- `butler`:`user.setButlerStatus("approved")`;按 `user_id` upsert `butler_profiles`,状态写 `approved`(已存在则更新,不存在则插入)。
|
|
|
+- `approve` 开头判空 `user == null` 返回 `Result.error("用户不存在")`。
|
|
|
+- 其余角色逻辑保持不变。
|
|
|
+
|
|
|
+- [ ] **Step 4: 跑测试确认通过**
|
|
|
+
|
|
|
+Run: `cd /app/cfc/cfc-backend && mvn -q -Dtest=ServiceRoleApproveTest test`
|
|
|
+Expected: `BUILD SUCCESS`,2 个测试通过。
|
|
|
+
|
|
|
+- [ ] **Step 5: 提交**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd /app/cfc && git add cfc-backend/src/main/java/com/etotem/cfc/service/impl/ServiceRoleApplicationServiceImpl.java cfc-backend/src/test/java/com/etotem/cfc/unit/ServiceRoleApproveTest.java
|
|
|
+git commit -m "fix(service-role): 审核通过补齐规划师状态/admin记录与管家档案状态"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Task 3: 审核中心统计合并 + 知识管理员计数
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `cfc-backend/src/main/java/com/etotem/cfc/controller/admin/ReviewCenterController.java`
|
|
|
+- Test: `cfc-backend/src/test/java/com/etotem/cfc/unit/ReviewCenterSummaryTest.java`
|
|
|
+
|
|
|
+**Interfaces:**
|
|
|
+- Produces: `public static int countPendingByRoles(List<ServiceRoleApplication> apps, String... roles)`。
|
|
|
+- `summary()` 返回新增 `knowledgePending`;`guidePending/nutritionistPending/vendorPending/butlerPending` 改为"原计数 + 服务角色待审数"。
|
|
|
+
|
|
|
+- [ ] **Step 1: 写失败测试**
|
|
|
+
|
|
|
+```java
|
|
|
+package com.etotem.cfc.unit;
|
|
|
+
|
|
|
+import com.etotem.cfc.controller.admin.ReviewCenterController;
|
|
|
+import com.etotem.cfc.entity.ServiceRoleApplication;
|
|
|
+import org.junit.jupiter.api.Test;
|
|
|
+import java.util.Arrays;
|
|
|
+import java.util.Collections;
|
|
|
+import static org.junit.jupiter.api.Assertions.*;
|
|
|
+
|
|
|
+class ReviewCenterSummaryTest {
|
|
|
+ private ServiceRoleApplication app(String roles, int status) {
|
|
|
+ ServiceRoleApplication a = new ServiceRoleApplication();
|
|
|
+ a.setRoles(roles); a.setStatus(status); return a;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void countPendingByRoles_只数status0且角色命中() {
|
|
|
+ List<ServiceRoleApplication> list = Arrays.asList(
|
|
|
+ app("[\"planner\"]", 0),
|
|
|
+ app("[\"planner\"]", 1),
|
|
|
+ app("[\"activity_provider\"]", 0),
|
|
|
+ app("[\"vendor\"]", 0));
|
|
|
+ assertEquals(1, ReviewCenterController.countPendingByRoles(list, "planner"));
|
|
|
+ assertEquals(2, ReviewCenterController.countPendingByRoles(list, "vendor", "activity_provider"));
|
|
|
+ assertEquals(0, ReviewCenterController.countPendingByRoles(Collections.emptyList(), "planner"));
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: 运行确认失败**
|
|
|
+
|
|
|
+Run: `cd /app/cfc/cfc-backend && mvn -q -Dtest=ReviewCenterSummaryTest test`
|
|
|
+Expected: 编译失败 `cannot find symbol countPendingByRoles`.
|
|
|
+
|
|
|
+- [ ] **Step 3: 实现**
|
|
|
+
|
|
|
+注入 `ServiceRoleApplicationMapper`;`summary()` 一次性查 `status=0` 的申请列表,用 `countPendingByRoles` 计算各角色待审数并加到原计数;新增 `knowledgePending`。`guidePending` 原查询加 `.isNotNull("real_name")` 与列表口径一致。
|
|
|
+
|
|
|
+- [ ] **Step 4: 跑测试确认通过**
|
|
|
+
|
|
|
+Run: `cd /app/cfc/cfc-backend && mvn -q -Dtest=ReviewCenterSummaryTest test`
|
|
|
+Expected: `BUILD SUCCESS`.
|
|
|
+
|
|
|
+- [ ] **Step 5: 提交**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd /app/cfc && git add cfc-backend/src/main/java/com/etotem/cfc/controller/admin/ReviewCenterController.java cfc-backend/src/test/java/com/etotem/cfc/unit/ReviewCenterSummaryTest.java
|
|
|
+git commit -m "feat(review-center): 统计合并服务角色待审数并新增知识管理员计数"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Task 4: 数据库迁移(驳回原因列、规划师假数据、菜单隐藏)
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `cfc-backend/src/main/java/com/etotem/cfc/entity/ActivityRegistration.java`
|
|
|
+- Modify: `cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java`
|
|
|
+
|
|
|
+**Interfaces:**
|
|
|
+- Produces: `activity_registrations.reject_reason VARCHAR(500)`;`users.teacher_status` 默认值 NULL + 103 条假 pending 清空;`sys_menu` id=14 `visible=0`。
|
|
|
+
|
|
|
+- [ ] **Step 1: 写迁移(幂等)**
|
|
|
+
|
|
|
+在 `DatabaseInitializer.runMigrations()` 末尾追加迁移 382/383/384:
|
|
|
+- 382:`information_schema.columns` 判断后 `ALTER TABLE activity_registrations ADD COLUMN reject_reason VARCHAR(500) NULL COMMENT '驳回原因'`。
|
|
|
+- 383:`UPDATE users SET teacher_status = NULL WHERE teacher_status='pending' AND (role IS NULL OR role <> 'teacher')`;`ALTER TABLE users MODIFY teacher_status VARCHAR(20) DEFAULT NULL`;同步把建列语句(约 375 行)的 `DEFAULT 'pending'` 改为 `DEFAULT NULL`。
|
|
|
+- 384:`UPDATE sys_menu SET visible=0 WHERE path='/service-role-applications'`;`DELETE FROM sys_role_menu WHERE menu_id IN (SELECT id FROM sys_menu WHERE path='/service-role-applications')`;同步删除 `seedSysMenus()` 中 `serviceRoleApp` 条目。
|
|
|
+
|
|
|
+- [ ] **Step 2: 本地编译确认**
|
|
|
+
|
|
|
+Run: `cd /app/cfc/cfc-backend && mvn -q -DskipTests compile`
|
|
|
+Expected: `BUILD SUCCESS`.
|
|
|
+
|
|
|
+- [ ] **Step 3: 部署后 SQL 验证(Task 15 一起执行)**
|
|
|
+
|
|
|
+```sql
|
|
|
+SHOW COLUMNS FROM activity_registrations LIKE 'reject_reason';
|
|
|
+SHOW COLUMNS FROM users LIKE 'teacher_status'; -- Default 应为 NULL
|
|
|
+SELECT COUNT(*) FROM users WHERE teacher_status='pending'; -- 应为 1(id=2)
|
|
|
+SELECT visible FROM sys_menu WHERE path='/service-role-applications'; -- 0
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 4: 提交**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd /app/cfc && git add cfc-backend/src/main/java/com/etotem/cfc/entity/ActivityRegistration.java cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java
|
|
|
+git commit -m "feat(db): 活动报名驳回原因列、规划师假数据清理、服务角色菜单隐藏"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Task 5: 活动报名驳回原因落库
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `cfc-backend/src/main/java/com/etotem/cfc/service/ActivityRegistrationService.java:113-121`
|
|
|
+- Modify: `cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminActivityRegistrationController.java:38-48`
|
|
|
+- Modify: `cfc-web/src/api/activity.js:119-125`
|
|
|
+- Test: `cfc-backend/src/test/java/com/etotem/cfc/unit/ActivityRegistrationRejectTest.java`
|
|
|
+
|
|
|
+**Interfaces:**
|
|
|
+- Produces: `Result<Void> reject(Long registrationId, Long adminUserId, String reason)`;HTTP 参数 `{registrationId, reason}`。
|
|
|
+
|
|
|
+- [ ] **Step 1: 写失败测试**
|
|
|
+
|
|
|
+```java
|
|
|
+package com.etotem.cfc.unit;
|
|
|
+
|
|
|
+import com.etotem.cfc.entity.ActivityRegistration;
|
|
|
+import com.etotem.cfc.mapper.ActivityRegistrationMapper;
|
|
|
+import com.etotem.cfc.service.ActivityRegistrationService;
|
|
|
+import org.junit.jupiter.api.Test;
|
|
|
+import java.lang.reflect.Field;
|
|
|
+import static org.junit.jupiter.api.Assertions.*;
|
|
|
+import static org.mockito.Mockito.*;
|
|
|
+
|
|
|
+class ActivityRegistrationRejectTest {
|
|
|
+ @Test
|
|
|
+ void 驳回写入原因() throws Exception {
|
|
|
+ ActivityRegistrationService svc = new ActivityRegistrationService();
|
|
|
+ ActivityRegistrationMapper mapper = mock(ActivityRegistrationMapper.class);
|
|
|
+ Field f = ActivityRegistrationService.class.getDeclaredField("registrationMapper");
|
|
|
+ f.setAccessible(true); f.set(svc, mapper);
|
|
|
+ ActivityRegistration reg = new ActivityRegistration();
|
|
|
+ reg.setId(1L); reg.setStatus("pending");
|
|
|
+ when(mapper.selectById(1L)).thenReturn(reg);
|
|
|
+
|
|
|
+ svc.reject(1L, 9L, "信息不完整");
|
|
|
+
|
|
|
+ assertEquals("rejected", reg.getStatus());
|
|
|
+ assertEquals("信息不完整", reg.getRejectReason());
|
|
|
+ verify(mapper).updateById(reg);
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: 运行确认失败**
|
|
|
+
|
|
|
+Run: `cd /app/cfc/cfc-backend && mvn -q -Dtest=ActivityRegistrationRejectTest test`
|
|
|
+Expected: 编译失败(无 3 参 reject / 无 getRejectReason)。
|
|
|
+
|
|
|
+- [ ] **Step 3: 实现**
|
|
|
+
|
|
|
+`ActivityRegistration` 加 `private String rejectReason;`;`reject` 增第三参并 `reg.setRejectReason(reason)`;Controller 从 `params.get("reason")` 传入;前端 `rejectRegistration` 已透传 data,调用处补 `reason`。
|
|
|
+
|
|
|
+- [ ] **Step 4: 跑测试确认通过**
|
|
|
+
|
|
|
+Run: `cd /app/cfc/cfc-backend && mvn -q -Dtest=ActivityRegistrationRejectTest test`
|
|
|
+Expected: `BUILD SUCCESS`.
|
|
|
+
|
|
|
+- [ ] **Step 5: 提交**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd /app/cfc && git add cfc-backend/src/main/java/com/etotem/cfc/service/ActivityRegistrationService.java cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminActivityRegistrationController.java cfc-backend/src/main/java/com/etotem/cfc/entity/ActivityRegistration.java cfc-web/src/api/activity.js cfc-backend/src/test/java/com/etotem/cfc/unit/ActivityRegistrationRejectTest.java
|
|
|
+git commit -m "feat(activity): 报名驳回原因落库"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Task 6: 通用详情抽屉组件
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Create: `cfc-web/src/views/admin/review/components/AuditDetailDrawer.vue`
|
|
|
+- Create: `cfc-web/src/views/admin/review/__tests__/AuditDetailDrawer.spec.js`
|
|
|
+
|
|
|
+**Interfaces:**
|
|
|
+- Props:`visible:Boolean`、`title:String`、`status:[String,Number]`、`loading:Boolean`、`actionMode:String`(`approve-reject`|`cancel`)。
|
|
|
+- Events:`update:visible`、`approve`、`reject(reason)`、`cancel(reason)`。
|
|
|
+- 插槽:默认插槽放详情内容。
|
|
|
+- 待审核判定:`status === 'pending' || status === 0 || status === '0'`。
|
|
|
+
|
|
|
+- [ ] **Step 1: 写失败测试**
|
|
|
+
|
|
|
+```js
|
|
|
+import { shallowMount } from '@vue/test-utils'
|
|
|
+import AuditDetailDrawer from '../components/AuditDetailDrawer.vue'
|
|
|
+
|
|
|
+describe('AuditDetailDrawer', () => {
|
|
|
+ it('驳回意见为空时不发 reject', async () => {
|
|
|
+ const wrapper = shallowMount(AuditDetailDrawer, {
|
|
|
+ propsData: { visible: true, status: 'pending', actionMode: 'approve-reject' },
|
|
|
+ stubs: { 'el-drawer': true, 'el-button': true, 'el-input': true }
|
|
|
+ })
|
|
|
+ wrapper.vm.showRejectInput = true
|
|
|
+ wrapper.vm.rejectReason = ' '
|
|
|
+ wrapper.vm.confirmReject()
|
|
|
+ expect(wrapper.emitted().reject).toBeUndefined()
|
|
|
+ })
|
|
|
+
|
|
|
+ it('驳回意见非空时 emit reject(reason)', () => {
|
|
|
+ const wrapper = shallowMount(AuditDetailDrawer, {
|
|
|
+ propsData: { visible: true, status: 'pending', actionMode: 'approve-reject' },
|
|
|
+ stubs: { 'el-drawer': true, 'el-button': true, 'el-input': true }
|
|
|
+ })
|
|
|
+ wrapper.vm.showRejectInput = true
|
|
|
+ wrapper.vm.rejectReason = '材料不全'
|
|
|
+ wrapper.vm.confirmReject()
|
|
|
+ expect(wrapper.emitted().reject[0]).toEqual(['材料不全'])
|
|
|
+ })
|
|
|
+
|
|
|
+ it('cancel 模式不渲染通过按钮且原因必填', () => {
|
|
|
+ const wrapper = shallowMount(AuditDetailDrawer, {
|
|
|
+ propsData: { visible: true, status: 'processing', actionMode: 'cancel' },
|
|
|
+ stubs: { 'el-drawer': true, 'el-button': true, 'el-input': true }
|
|
|
+ })
|
|
|
+ expect(wrapper.vm.showApprove).toBe(false)
|
|
|
+ wrapper.vm.cancelReason = ''
|
|
|
+ wrapper.vm.confirmCancel()
|
|
|
+ expect(wrapper.emitted().cancel).toBeUndefined()
|
|
|
+ })
|
|
|
+})
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: 运行确认失败**
|
|
|
+
|
|
|
+Run: `cd /app/cfc/cfc-web && npx jest src/views/admin/review/__tests__/AuditDetailDrawer.spec.js`
|
|
|
+Expected: 找不到组件,测试失败。
|
|
|
+
|
|
|
+- [ ] **Step 3: 实现组件**
|
|
|
+
|
|
|
+`el-drawer` 右侧 720px;头部标题 + 状态标签;默认插槽;底部按钮区按 `actionMode`/`isPending` 渲染;`confirmApprove` 走 `$confirm` 后 emit;`confirmReject`/`confirmCancel` 校验 trim 非空,空则 `$message.warning`。提交中 `loading` 禁用按钮。
|
|
|
+
|
|
|
+- [ ] **Step 4: 跑测试确认通过**
|
|
|
+
|
|
|
+Run: `cd /app/cfc/cfc-web && npx jest src/views/admin/review/__tests__/AuditDetailDrawer.spec.js`
|
|
|
+Expected: 3 passed.
|
|
|
+
|
|
|
+- [ ] **Step 5: 提交**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd /app/cfc && git add cfc-web/src/views/admin/review/components/AuditDetailDrawer.vue cfc-web/src/views/admin/review/__tests__/AuditDetailDrawer.spec.js
|
|
|
+git commit -m "feat(review): 新增通用审核详情抽屉组件"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Task 7: 服务角色 API 与合并映射工具
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Create: `cfc-web/src/api/service-role.js`
|
|
|
+- Create: `cfc-web/src/utils/auditRowMapping.js`
|
|
|
+- Create: `cfc-web/src/views/admin/review/__tests__/auditRowMapping.spec.js`
|
|
|
+
|
|
|
+**Interfaces:**
|
|
|
+- `listServiceRoleApplications({ status, role })` → `POST /api/service-role/admin/list`。
|
|
|
+- `approveServiceRoleApplication(id)`、`rejectServiceRoleApplication(id, reason)`。
|
|
|
+- `mapServiceRoleRow(app, roleLabels)` → 统一行 `{source:'service_role', ...}`。
|
|
|
+- `mergeAuditRows(userRows, serviceRoleRows)` → 按 `submittedAt` 倒序、按 `userId` 去重(service_role 优先)。
|
|
|
+
|
|
|
+- [ ] **Step 1: 写失败测试**
|
|
|
+
|
|
|
+```js
|
|
|
+import { mergeAuditRows, mapServiceRoleRow } from '@/utils/auditRowMapping'
|
|
|
+
|
|
|
+describe('auditRowMapping', () => {
|
|
|
+ const sr = { id: 1, userId: 12, roles: '["activity_provider"]', status: 0, createdAt: '2026-10-19 21:29:28', userName: '他爹' }
|
|
|
+ const userRow = { id: 12, userId: 12, nickname: '他爹', status: 'pending', submittedAt: '2026-10-18 10:00:00' }
|
|
|
+
|
|
|
+ it('映射服务角色申请', () => {
|
|
|
+ const row = mapServiceRoleRow(sr, { activity_provider: '活动提供商' })
|
|
|
+ expect(row.source).toBe('service_role')
|
|
|
+ expect(row.applyId).toBe(1)
|
|
|
+ expect(row.roleLabel).toBe('活动提供商')
|
|
|
+ expect(row.status).toBe('pending')
|
|
|
+ })
|
|
|
+
|
|
|
+ it('同一 userId 优先保留 service_role 行', () => {
|
|
|
+ const merged = mergeAuditRows([userRow], [sr])
|
|
|
+ expect(merged).toHaveLength(1)
|
|
|
+ expect(merged[0].source).toBe('service_role')
|
|
|
+ })
|
|
|
+
|
|
|
+ it('按提交时间倒序', () => {
|
|
|
+ const older = { ...userRow, userId: 99, id: 99 }
|
|
|
+ const merged = mergeAuditRows([older], [sr])
|
|
|
+ expect(merged[0].source).toBe('service_role')
|
|
|
+ })
|
|
|
+})
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: 运行确认失败**
|
|
|
+
|
|
|
+Run: `cd /app/cfc/cfc-web && npx jest src/views/admin/review/__tests__/auditRowMapping.spec.js`
|
|
|
+Expected: 模块不存在。
|
|
|
+
|
|
|
+- [ ] **Step 3: 实现**
|
|
|
+
|
|
|
+`mapServiceRoleRow` 解析 `roles` JSON(容错),`status` 0/1/2 → pending/approved/rejected;`mergeAuditRows` 去重后按时间倒序;API 文件按现有 `request` 风格封装。
|
|
|
+
|
|
|
+- [ ] **Step 4: 跑测试确认通过**
|
|
|
+
|
|
|
+Run: `cd /app/cfc/cfc-web && npx jest src/views/admin/review/__tests__/auditRowMapping.spec.js`
|
|
|
+Expected: 3 passed.
|
|
|
+
|
|
|
+- [ ] **Step 5: 提交**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd /app/cfc && git add cfc-web/src/api/service-role.js cfc-web/src/utils/auditRowMapping.js cfc-web/src/views/admin/review/__tests__/auditRowMapping.spec.js
|
|
|
+git commit -m "feat(review): 服务角色 API 与合并映射工具"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Task 8: 文章审核、任务模板审核迁移到抽屉
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `cfc-web/src/views/admin/review/ArticleAudit.vue`
|
|
|
+- Modify: `cfc-web/src/views/admin/review/PackageAudit.vue`
|
|
|
+
|
|
|
+**Interfaces:**
|
|
|
+- Consumes: `AuditDetailDrawer`(Task 6)。
|
|
|
+
|
|
|
+- [ ] **Step 1: 改造 ArticleAudit**
|
|
|
+
|
|
|
+把现有 `el-dialog` 详情改为 `AuditDetailDrawer`,内容保留标题/分类/作者/时间/摘要/正文 HTML;操作列改为「审核/详情」按钮;通过 emit `approve` → 原 `handleAudit(row,'approved')`;驳回 emit `reject(reason)` → `adminArticleAudit({id, auditStatus:'rejected', auditReason: reason})`。保留批量通过/批量驳回。
|
|
|
+
|
|
|
+- [ ] **Step 2: 改造 PackageAudit**
|
|
|
+
|
|
|
+同 ArticleAudit:详情字段为名称/周期/价格/详细介绍/驳回原因;通过/驳回走原模板审核接口。
|
|
|
+
|
|
|
+- [ ] **Step 3: 构建验证**
|
|
|
+
|
|
|
+Run: `cd /app/cfc/cfc-web && npx vue-cli-service build --mode production 2>&1 | tail -5`
|
|
|
+Expected: `Build complete`。
|
|
|
+
|
|
|
+- [ ] **Step 4: 提交**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd /app/cfc && git add cfc-web/src/views/admin/review/ArticleAudit.vue cfc-web/src/views/admin/review/PackageAudit.vue
|
|
|
+git commit -m "refactor(review): 文章与任务模板审核改用统一详情抽屉"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Task 9: 其余 6 个审核 Tab 接入详情抽屉
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `ProductAudit.vue`、`ActivityAudit.vue`、`ActivityRegistrationAudit.vue`、`WithdrawAudit.vue`、`PendingRefundAudit.vue`、`HierarchyAudit.vue`(均在 `cfc-web/src/views/admin/review/`)
|
|
|
+- Modify: `cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminCommissionController.java`、`cfc-backend/src/main/java/com/etotem/cfc/service/PendingRefundService.java`(中止退款原因)
|
|
|
+
|
|
|
+- [ ] **Step 1: ProductAudit**
|
|
|
+
|
|
|
+详情:商品名称/图片/价格/库存/分类/供应商/描述/驳回原因;通过/驳回走 `reviewProduct`。
|
|
|
+
|
|
|
+- [ ] **Step 2: ActivityAudit**
|
|
|
+
|
|
|
+详情:活动名称/封面/时间/地点/主办方/内容/驳回原因;通过 → `auditActivity({id, action:'publish'})`;驳回带 reason。
|
|
|
+
|
|
|
+- [ ] **Step 3: ActivityRegistrationAudit**
|
|
|
+
|
|
|
+详情:报名用户/活动/报名时间/状态/驳回原因;通过 `approveRegistration({registrationId})`;驳回 `rejectRegistration({registrationId, reason})`。
|
|
|
+
|
|
|
+- [ ] **Step 4: WithdrawAudit**
|
|
|
+
|
|
|
+详情:用户/金额/账户信息/申请时间/状态/驳回原因(复用 `remark`);通过 `auditWithdrawal(id,'approved')`;驳回 `auditWithdrawal(id,'rejected',reason)`。
|
|
|
+
|
|
|
+- [ ] **Step 5: PendingRefundAudit**
|
|
|
+
|
|
|
+`actionMode='cancel'`:仅 `pending`/`processing` 可操作;详情展示类型/订单号/用户/金额/尝试次数/失败原因/创建时间;中止原因写入 `remark`。后端改动:`AdminCommissionController.cancelRefund` 读取 `params.get("reason")`,`PendingRefundService.cancelPendingRefund(Long refundId, String remark)` 改为两参(`remark` 为空时默认 `"管理员中止退款"`),前端 `cancelPendingRefund({refundId, reason})`。
|
|
|
+
|
|
|
+- [ ] **Step 6: HierarchyAudit**
|
|
|
+
|
|
|
+详情:申请人/当前上级/目标上级/申请原因/状态;通过/驳回走 `reviewSupplyHierarchyChange`(补 reason)。
|
|
|
+
|
|
|
+- [ ] **Step 7: 构建验证**
|
|
|
+
|
|
|
+Run: `cd /app/cfc/cfc-web && npx vue-cli-service build --mode production 2>&1 | tail -5`
|
|
|
+Expected: `Build complete`。
|
|
|
+
|
|
|
+- [ ] **Step 8: 提交**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd /app/cfc && git add cfc-web/src/views/admin/review/ProductAudit.vue cfc-web/src/views/admin/review/ActivityAudit.vue cfc-web/src/views/admin/review/ActivityRegistrationAudit.vue cfc-web/src/views/admin/review/WithdrawAudit.vue cfc-web/src/views/admin/review/PendingRefundAudit.vue cfc-web/src/views/admin/review/HierarchyAudit.vue
|
|
|
+git commit -m "refactor(review): 商品/活动/报名/提现/退款/上级审核接入统一详情抽屉"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Task 10: 规划师、营养师审核合并服务角色申请
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `GuideAudit.vue`、`NutritionistAudit.vue`
|
|
|
+
|
|
|
+**Interfaces:**
|
|
|
+- Consumes: `listServiceRoleApplications`、`mapServiceRoleRow`、`mergeAuditRows`(Task 7)。
|
|
|
+
|
|
|
+- [ ] **Step 1: GuideAudit**
|
|
|
+
|
|
|
+`loadData()` 并行拉取 `getGuideApplications({status, page:1, size:200})` 与 `listServiceRoleApplications({status:0, role:'planner'})`;合并后渲染统一列(申请人/类型/来源/状态/提交时间/操作);审核按 `source` 路由:service_role → `approveServiceRoleApplication(applyId)`/`rejectServiceRoleApplication(applyId, reason)`,否则原 `approveGuide/rejectGuide`;详情抽屉展示 §6.1 或 §6.2 字段。
|
|
|
+
|
|
|
+- [ ] **Step 2: NutritionistAudit**
|
|
|
+
|
|
|
+同 GuideAudit,角色 `nutritionist`,审核接口 `approveNutritionist/rejectNutritionist`。
|
|
|
+
|
|
|
+- [ ] **Step 3: 构建验证**
|
|
|
+
|
|
|
+Run: `cd /app/cfc/cfc-web && npx vue-cli-service build --mode production 2>&1 | tail -5`
|
|
|
+Expected: `Build complete`。
|
|
|
+
|
|
|
+- [ ] **Step 4: 提交**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd /app/cfc && git add cfc-web/src/views/admin/review/GuideAudit.vue cfc-web/src/views/admin/review/NutritionistAudit.vue
|
|
|
+git commit -m "feat(review): 规划师/营养师审核合并服务角色申请"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Task 11: 服务商审核合并服务角色申请
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `VendorAudit.vue`
|
|
|
+
|
|
|
+- [ ] **Step 1: 改造**
|
|
|
+
|
|
|
+`loadData()` 并行拉取 `getVendorList({status, page:1, size:200})` 与 `listServiceRoleApplications({status:0, role:'vendor,activity_provider'})`;合并去重;列表增加「类型」列(入驻/活动提供商/服务商);详情抽屉展示入驻申请人信息或服务角色材料;审核按 `source` 路由到 `reviewVendor` 或 `approveServiceRoleApplication/rejectServiceRoleApplication`。
|
|
|
+
|
|
|
+- [ ] **Step 2: 构建验证**
|
|
|
+
|
|
|
+Run: `cd /app/cfc/cfc-web && npx vue-cli-service build --mode production 2>&1 | tail -5`
|
|
|
+Expected: `Build complete`。
|
|
|
+
|
|
|
+- [ ] **Step 3: 提交**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd /app/cfc && git add cfc-web/src/views/admin/review/VendorAudit.vue
|
|
|
+git commit -m "feat(review): 服务商审核合并活动提供商/服务商服务角色申请"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Task 12: 管家审核数据源修复 + 合并
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `ButlerAudit.vue`
|
|
|
+
|
|
|
+- [ ] **Step 1: 改数据源**
|
|
|
+
|
|
|
+列表改为 `users.butler_status=pending` 的申请。后端新增接口 `POST /api/admin/butler/user-list`(`AdminButlerController`):入参 `{status, page, size}`,查 `users.butler_status=status`,返回 `{list, total}`,每行含 `userId/nickname/realName/phone/butlerStatus/butlerInfo/updatedAt`。详情解析 `butler_info`(tier/description);同时合并 `listServiceRoleApplications({status:0, role:'butler'})`。
|
|
|
+
|
|
|
+- [ ] **Step 2: 审核路由**
|
|
|
+
|
|
|
+`source='user'` → `reviewButler({userId, action})`;`service_role` → `approveServiceRoleApplication` / `rejectServiceRoleApplication`。保留接单状态开关(仅已通过记录显示)。
|
|
|
+
|
|
|
+- [ ] **Step 3: 构建验证**
|
|
|
+
|
|
|
+Run: `cd /app/cfc/cfc-web && npx vue-cli-service build --mode production 2>&1 | tail -5`
|
|
|
+Expected: `Build complete`。
|
|
|
+
|
|
|
+- [ ] **Step 4: 提交**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd /app/cfc && git add cfc-web/src/views/admin/review/ButlerAudit.vue cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminButlerController.java
|
|
|
+git commit -m "fix(review): 管家审核改查 users.butler_status 并合并服务角色申请"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Task 13: 新增知识管理员审核 Tab
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Create: `cfc-web/src/views/admin/review/KnowledgeAdminAudit.vue`
|
|
|
+- Modify: `cfc-web/src/views/admin/ReviewCenter.vue`
|
|
|
+
|
|
|
+- [ ] **Step 1: 新建组件**
|
|
|
+
|
|
|
+列表只拉 `listServiceRoleApplications({status, role:'article_admin'})`;列:申请人/申请角色/状态/提交时间/操作;详情抽屉展示 §6.1 全部字段;通过/驳回走服务角色接口。
|
|
|
+
|
|
|
+- [ ] **Step 2: 注册到审核中心**
|
|
|
+
|
|
|
+`ReviewCenter.vue` 增加 `el-tab-pane label="知识管理员审核" name="knowledge"`、组件 import/注册、`summaryCards` 加 `{key:'knowledge', label:'知识管理员审核'}`、`keyMap.knowledge='knowledgePending'`。
|
|
|
+
|
|
|
+- [ ] **Step 3: 构建验证**
|
|
|
+
|
|
|
+Run: `cd /app/cfc/cfc-web && npx vue-cli-service build --mode production 2>&1 | tail -5`
|
|
|
+Expected: `Build complete`。
|
|
|
+
|
|
|
+- [ ] **Step 4: 提交**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd /app/cfc && git add cfc-web/src/views/admin/review/KnowledgeAdminAudit.vue cfc-web/src/views/admin/ReviewCenter.vue
|
|
|
+git commit -m "feat(review): 新增知识管理员审核 Tab"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Task 14: 删除独立菜单与旧页面
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `cfc-web/src/views/Layout.vue`
|
|
|
+- Modify: `cfc-web/src/router/index.js`
|
|
|
+- Delete: `cfc-web/src/views/service/ServiceRoleApplications.vue`
|
|
|
+
|
|
|
+- [ ] **Step 1: Layout 删除菜单项**
|
|
|
+
|
|
|
+删除 `{ path: '/service-role-applications', label: '服务角色申请审核', ... }`。
|
|
|
+
|
|
|
+- [ ] **Step 2: 路由重定向**
|
|
|
+
|
|
|
+把 `path: 'service-role-applications'` 的组件改为重定向:保留路由名但 `redirect: '/review-center?tab=vendor'`(或直接删除该路由并加 `{path:'/service-role-applications', redirect:'/review-center?tab=vendor'}`)。
|
|
|
+
|
|
|
+- [ ] **Step 3: 删除旧页面**
|
|
|
+
|
|
|
+删除 `cfc-web/src/views/service/ServiceRoleApplications.vue`,全局搜索确认无引用(`grep -rn 'ServiceRoleApplications' cfc-web/src`)。
|
|
|
+
|
|
|
+- [ ] **Step 4: 构建验证**
|
|
|
+
|
|
|
+Run: `cd /app/cfc/cfc-web && npx vue-cli-service build --mode production 2>&1 | tail -5`
|
|
|
+Expected: `Build complete`。
|
|
|
+
|
|
|
+- [ ] **Step 5: 提交**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd /app/cfc && git add -A cfc-web/src/views/Layout.vue cfc-web/src/router/index.js cfc-web/src/views/service/ServiceRoleApplications.vue
|
|
|
+git commit -m "refactor(review): 删除服务角色申请独立菜单并重定向到审核中心"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Task 15: 部署与验证
|
|
|
+
|
|
|
+**Files:** 无代码改动。
|
|
|
+
|
|
|
+- [ ] **Step 1: 备份**
|
|
|
+
|
|
|
+```bash
|
|
|
+ssh root@111.228.6.214 "tar -czf /app/pro/cfc_backup_review_$(date +%Y%m%d_%H%M%S).tar.gz -C /app pro/cfc"
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: 后端打包部署**
|
|
|
+
|
|
|
+```bash
|
|
|
+ssh root@111.228.6.214 "cd /app/cfc/cfc-backend && mvn clean package -Dmaven.test.skip=true -Dmaven.compile.fork=true && cp target/cfc-backend-1.0.0.jar /app/pro/cfc/backend/cfc-backend-review-$(date +%Y%m%d).jar && cp target/cfc-backend-1.0.0.jar /app/pro/cfc/backend/cfc-backend-1.0.0.jar && systemctl restart cfc-backend"
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 3: 后端健康检查**
|
|
|
+
|
|
|
+```bash
|
|
|
+ssh root@111.228.6.214 "sleep 20; systemctl is-active cfc-backend; ss -tlnp | grep 9082; grep $(systemctl show -p MainPID --value cfc-backend) /app/pro/cfc/backend/cfc-backend.log | grep -c ERROR; curl -s -o /dev/null -w '%{http_code}' -X POST http://localhost:9082/api/vendor/status"
|
|
|
+```
|
|
|
+Expected: `active`、监听 9082、ERROR 数 0、HTTP 200/401。
|
|
|
+
|
|
|
+- [ ] **Step 4: 迁移验证**
|
|
|
+
|
|
|
+执行 Task 4 Step 3 的 SQL,确认列存在、默认值 NULL、pending 数量 1、菜单 visible=0。
|
|
|
+
|
|
|
+- [ ] **Step 5: 前端构建部署**
|
|
|
+
|
|
|
+```bash
|
|
|
+ssh root@111.228.6.214 "cd /app/cfc/cfc-web && npm run build && rsync -a --delete dist/ /app/pro/cfc/frontend/"
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 6: 管理端冒烟**
|
|
|
+
|
|
|
+```bash
|
|
|
+ssh root@111.228.6.214 "curl -s -o /dev/null -w '%{http_code}' -k https://cfc.etotem.com.cn/admin/"
|
|
|
+```
|
|
|
+Expected: 200。再用 admin 账号打开 `/review-center` 逐 Tab 检查:详情抽屉可打开、驳回空意见被拦截、知识管理员 Tab 存在、服务角色申请出现在服务商审核。
|
|
|
+
|
|
|
+- [ ] **Step 7: 服务角色端到端**
|
|
|
+
|
|
|
+用 user12 的 `activity_provider` 申请在「服务商审核」中通过,然后验证:
|
|
|
+
|
|
|
+```sql
|
|
|
+SELECT activity_provider_status, roles FROM users WHERE id=12;
|
|
|
+```
|
|
|
+Expected: `approved`,`roles` 含 `activity_manager`。
|
|
|
+
|
|
|
+- [ ] **Step 8: git 收尾**
|
|
|
+
|
|
|
+```bash
|
|
|
+ssh root@111.228.6.214 "cd /app/cfc && git add -A && git commit -m 'chore: build artifacts' 2>/dev/null; git push origin cfclub"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Self-Review
|
|
|
+
|
|
|
+**Spec coverage:** §2 目标 1(12 Tab 抽屉)→ Task 6/8/9;目标 2(分流合并)→ Task 1/10/11/12/13;目标 3(approve 修复)→ Task 2;目标 4(假数据)→ Task 4;§5.1 待退款例外 → Task 9 Step 5;§5.4 菜单 → Task 4/14;§7.1 → Task 2;§7.2 → Task 1;§7.3 → Task 3;§7.5 → Task 4/5;验证计划 → Task 15。
|
|
|
+
|
|
|
+**Type consistency:** `applyApprovedRole(String, ServiceRoleApplication, User, List<String>)` 在 Task 2 定义并在测试中使用;`listApplicationsWithUserInfo(Integer, List<String>)` 在 Task 1 定义,前端 Task 10/11/12/13 通过 HTTP 消费;`mergeAuditRows(userRows, serviceRoleRows)` 在 Task 7 定义并在 Task 10/11 使用;`AuditDetailDrawer` 的 `actionMode` 在 Task 6 定义,Task 9 Step 5 使用 `cancel`。
|
|
|
+
|
|
|
+**Review Focus 对应测试:** 多角色 JSON → Task 1 `matchesAnyRole` 测试;重复行 → Task 7 `mergeAuditRows` 测试;空驳回意见 → Task 6 测试;资金动作 → Task 9 Step 5 实现约束(构建验证);已审核只读 → Task 6 `showActions` 实现(构建验证)。
|