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

feat(review): 服务角色 API 与合并映射工具,列表接口改读 body 过滤参数

Xiaogang Liao пре 5 дана
родитељ
комит
44815d1b1d

+ 10 - 2
cfc-backend/src/main/java/com/etotem/cfc/controller/ServiceRoleApplicationController.java

@@ -90,12 +90,20 @@ public class ServiceRoleApplicationController {
 
 
     @PostMapping("/admin/list")
     @PostMapping("/admin/list")
     @Operation(summary = "管理端: 获取申请列表")
     @Operation(summary = "管理端: 获取申请列表")
-    public Result adminList(@RequestParam(required = false, defaultValue = "0") Integer status,
-                            @RequestParam(required = false) String role,
+    public Result adminList(@RequestBody(required = false) Map<String, Object> params,
                             @RequestAttribute("role") String operatorRole) {
                             @RequestAttribute("role") String operatorRole) {
         if (!"admin".equals(operatorRole) && !"operator".equals(operatorRole)) {
         if (!"admin".equals(operatorRole) && !"operator".equals(operatorRole)) {
             return Result.error("无权限:仅管理员/运营可查看服务角色申请列表");
             return Result.error("无权限:仅管理员/运营可查看服务角色申请列表");
         }
         }
+        Map<String, Object> body = params != null ? params : new HashMap<>();
+        // 未传 status 时默认只看待审(0);显式传空串表示全部
+        Integer status = 0;
+        if (body.containsKey("status")) {
+            Object statusObj = body.get("status");
+            status = (statusObj == null || "".equals(statusObj.toString()))
+                    ? null : Integer.valueOf(statusObj.toString());
+        }
+        String role = body.get("role") != null ? body.get("role").toString() : null;
         List<Map<String, Object>> list = serviceRoleApplicationService.listApplicationsWithUserInfo(
         List<Map<String, Object>> list = serviceRoleApplicationService.listApplicationsWithUserInfo(
                 status, com.etotem.cfc.service.impl.ServiceRoleApplicationServiceImpl.parseRoleFilter(role));
                 status, com.etotem.cfc.service.impl.ServiceRoleApplicationServiceImpl.parseRoleFilter(role));
         return Result.success(list);
         return Result.success(list);

+ 29 - 0
cfc-web/src/api/service-role.js

@@ -0,0 +1,29 @@
+import request from '@/utils/request'
+
+// 管理端:服务角色申请列表(支持 status 与 role 过滤)
+// status 省略=仅待审(0),传空串=全部;role 支持逗号分隔多角色
+export function listServiceRoleApplications(data = {}) {
+  return request({
+    url: '/api/service-role/admin/list',
+    method: 'post',
+    data
+  })
+}
+
+// 管理端:通过申请
+export function approveServiceRoleApplication(id) {
+  return request({
+    url: '/api/service-role/admin/approve',
+    method: 'post',
+    data: { id }
+  })
+}
+
+// 管理端:驳回申请(reason 必填)
+export function rejectServiceRoleApplication(id, reason) {
+  return request({
+    url: '/api/service-role/admin/reject',
+    method: 'post',
+    data: { id, reason }
+  })
+}

+ 86 - 0
cfc-web/src/utils/auditRowMapping.js

@@ -0,0 +1,86 @@
+// 审核中心:服务角色申请与各 Tab 原有列表行的统一映射/合并工具
+
+const ROLE_LABELS = {
+  planner: '规划师',
+  nutritionist: '营养师',
+  butler: '管家',
+  vendor: '服务商',
+  activity_provider: '活动提供商',
+  article_admin: '知识管理员'
+}
+
+const SERVICE_ROLE_STATUS = { 0: 'pending', 1: 'approved', 2: 'rejected' }
+
+// 解析后端 JSON 字符串字段(roles / images),容错返回数组
+export function parseJsonArray(value) {
+  if (Array.isArray(value)) return value
+  if (!value) return []
+  try {
+    const arr = JSON.parse(value)
+    return Array.isArray(arr) ? arr : []
+  } catch (e) {
+    return []
+  }
+}
+
+export function parseRoles(roles) {
+  return parseJsonArray(roles)
+}
+
+export function roleLabel(role, roleLabels = {}) {
+  const labels = { ...ROLE_LABELS, ...roleLabels }
+  return labels[role] || role
+}
+
+// 服务角色申请 → 统一审核行
+export function mapServiceRoleRow(app, roleLabels = {}) {
+  if (!app) return null
+  const roles = parseRoles(app.roles)
+  return {
+    source: 'service_role',
+    applyId: app.id,
+    id: app.id,
+    userId: app.userId,
+    nickname: app.userName,
+    realName: app.realName,
+    phone: app.phone,
+    roles,
+    roleLabel: roles.map(r => roleLabel(r, roleLabels)).join('、'),
+    status: SERVICE_ROLE_STATUS[app.status] || 'pending',
+    rejected: app.status === 2,
+    rejectReason: app.rejectReason,
+    submittedAt: app.createdAt,
+    reviewedAt: app.reviewedAt,
+    idCard: app.idCard,
+    idCardImages: parseJsonArray(app.idCardImages),
+    certificateImages: parseJsonArray(app.certificateImages),
+    province: app.province,
+    city: app.city,
+    district: app.district,
+    street: app.street,
+    addressDetail: app.addressDetail,
+    raw: app
+  }
+}
+
+function submittedTime(row) {
+  const t = row.submittedAt || row.createdAt || 0
+  const parsed = new Date(t).getTime()
+  return isNaN(parsed) ? 0 : parsed
+}
+
+// 合并原 Tab 行与服务角色申请行:同 userId 去重(服务角色优先),按提交时间倒序
+export function mergeAuditRows(userRows = [], serviceRoleRows = []) {
+  const map = new Map()
+  for (const row of userRows) {
+    if (!row) continue
+    const key = String(row.userId != null ? row.userId : row.id)
+    map.set(key, { source: 'user', ...row })
+  }
+  for (const row of serviceRoleRows) {
+    if (!row) continue
+    const key = String(row.userId != null ? row.userId : row.applyId)
+    map.set(key, { source: 'service_role', ...row })
+  }
+  return Array.from(map.values()).sort((a, b) => submittedTime(b) - submittedTime(a))
+}

+ 54 - 0
cfc-web/src/views/admin/review/__tests__/auditRowMapping.spec.js

@@ -0,0 +1,54 @@
+import { mergeAuditRows, mapServiceRoleRow, parseJsonArray } from '@/utils/auditRowMapping'
+
+describe('auditRowMapping', () => {
+  const sr = {
+    id: 1,
+    userId: 12,
+    roles: '["activity_provider"]',
+    status: 0,
+    createdAt: '2026-10-19 21:29:28',
+    userName: '他爹',
+    idCardImages: '["/uploads/a.jpg"]',
+    province: '北京'
+  }
+  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')
+    expect(row.idCardImages).toEqual(['/uploads/a.jpg'])
+    expect(row.province).toBe('北京')
+  })
+
+  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).toHaveLength(2)
+    expect(merged[0].source).toBe('service_role')
+    expect(merged[0].userId).toBe(12)
+  })
+
+  it('roles 非法 JSON 容错为空数组', () => {
+    expect(parseJsonArray('not-json')).toEqual([])
+    expect(parseJsonArray(null)).toEqual([])
+    const row = mapServiceRoleRow({ id: 2, userId: 3, roles: 'oops', status: 1 })
+    expect(row.roles).toEqual([])
+    expect(row.roleLabel).toBe('')
+    expect(row.status).toBe('approved')
+  })
+})