2026-07-30-service-role-apply-design.md 7.1 KB

服务角色统一申请设计

日期: 2026-07-30 状态: ✅ 已实施

概述

"我的"页面增加统一的服务角色申请入口,用户可多选申请成为规划师、营养师、管家、服务商、文章管理员、活动提供商,上传身份证和资质证书,填写联系地址,提交后台审核。

现有分散的申请页面(butler/apply.vue、nutritionist/apply.vue、vendor/apply.vue、guide/register)将被此统一入口替换。

角色枚举

角色 Key 显示名称 现有后端 说明
planner 规划师 GuideApplication/Guide 成长规划师
nutritionist 营养师 User.nutritionistStatus
butler 管家 User.butlerStatus + ButlerProfile
vendor 服务商 User.vendorStatus + vendorInfo 供应商/服务商
article_admin 文章管理员 新增 User.article_admin_status 知识管理模块
activity_provider 活动提供商 新增 User.activity_provider_status

数据库设计

新建表 service_role_applications

CREATE TABLE `service_role_applications` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user_id` int(11) NOT NULL COMMENT '申请人ID',
  `roles` varchar(255) NOT NULL COMMENT '申请角色,JSON数组 e.g. ["planner","nutritionist"]',
  `id_card` varchar(18) DEFAULT '' COMMENT '身份证号',
  `id_card_images` text COMMENT '身份证照片,JSON数组[正面URL,反面URL]',
  `certificate_images` text COMMENT '资质证书照片,JSON数组',
  `province` varchar(50) DEFAULT '' COMMENT '省',
  `city` varchar(50) DEFAULT '' COMMENT '市',
  `district` varchar(50) DEFAULT '' COMMENT '区',
  `street` varchar(50) DEFAULT '' COMMENT '街道',
  `address_detail` varchar(200) DEFAULT '' COMMENT '详细地址',
  `status` tinyint(4) NOT NULL DEFAULT 0 COMMENT '0-pending, 1-approved, 2-rejected',
  `reject_reason` varchar(500) DEFAULT '' COMMENT '驳回原因',
  `reviewed_by` int(11) DEFAULT NULL COMMENT '审核人ID',
  `reviewed_at` datetime DEFAULT NULL COMMENT '审核时间',
  `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  KEY `idx_user_id` (`user_id`),
  KEY `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='服务角色申请表';

User 表新增字段

`article_admin_status` tinyint(4) DEFAULT NULL COMMENT '0无 1待审 2通过 3驳回',
`activity_provider_status` tinyint(4) DEFAULT NULL COMMENT '0无 1待审 2通过 3驳回',

后端设计

实体

@Data
@TableName("service_role_applications")
public class ServiceRoleApplication {
    @TableId(type = IdType.AUTO)
    private Long id;
    private Long userId;
    private String roles;             // JSON Array
    private String idCard;
    private String idCardImages;      // JSON Array
    private String certificateImages; // JSON Array
    private String province;
    private String city;
    private String district;
    private String street;
    private String addressDetail;
    private Integer status;           // 0=pending, 1=approved, 2=rejected
    private String rejectReason;
    private Long reviewedBy;
    private LocalDateTime reviewedAt;
    private LocalDateTime createdAt;
    private LocalDateTime updatedAt;
}

DTO

@Data
public class ServiceRoleApplyDTO {
    @NotEmpty(message = "至少选择一个角色")
    private List<String> roles;
    private String idCard;
    private List<String> idCardImages;
    private List<String> certificateImages;
    private String province;
    private String city;
    private String district;
    private String street;
    private String addressDetail;
}

Controller: ServiceRoleApplicationController

POST /api/service-role/apply          → 提交申请(含图片URL列表)
POST /api/service-role/my-apply       → 查询我的申请记录
POST /api/service-role/upload-image   → 上传单张图片(MultipartFile, 返回图片URL)

统一 @PostMapping,返回 Result<T>

Service: ServiceRoleApplicationService

public interface ServiceRoleApplicationService {
    Result submit(Long userId, ServiceRoleApplyDTO dto);
    Result getMyApplication(Long userId);
    Result approve(Long id, Long reviewerId);
    Result reject(Long id, String reason, Long reviewerId);
}

审批通过同步逻辑

planner           → 创建 Guide 记录
nutritionist      → User.nutritionistStatus = 2 (approved)
butler            → User.butlerStatus = 2 + 创建 ButlerProfile
vendor            → User.vendorStatus = 2
article_admin     → User.article_admin_status = 2
activity_provider → User.activity_provider_status = 2

前端设计

ProfileMenu 新增入口

pages/profile/components/ProfileMenu.vue 的「个人」分组新增菜单项(因 profile.vue 传 show-services=false 隐藏了服务分组):

<view class="menu-item" @click="goToServiceRoleApply">
  <text>📋 服务角色申请</text>
  <text class="arrow">›</text>
</view>
goToServiceRoleApply() {
  uni.navigateTo({ url: '/pages/profile/service-role-apply' })
}

新增页面 pages/profile/service-role-apply.vue

页面结构:

  1. 角色选择区 — 卡片式多选,6个角色网格布局,选中显示✓标记。参考 assessment/apply.vue:class="{selected:...}" 模式
  2. 身份证信息 — 身份证号输入框 + 正反面照片上传(两个独立上传位)
  3. 资质证书 — 可添加多张证书照片,每张独立上传,显示缩略图,可删除
  4. 联系地址 — 复用 <AddressPicker> 四级联动 + 详细地址输入框
  5. 提交按钮 — 渐变样式,表单验证通过后可点击

图片上传

使用 uni.chooseImage() + uni.uploadFile(),先上传到服务器获取 URL,再提交表单时传 URL 列表。

API

export const submitServiceRoleApply = (data) => post('/api/service-role/apply', data)
export const getMyServiceRoleApply = (data) => post('/api/service-role/my-apply', data)
export const uploadServiceRoleImage = (filePath) => ... // uni.uploadFile

页面注册

pages.json 中 profile 子包注册:

{
  "path": "pages/profile/service-role-apply",
  "style": { "navigationBarTitleText": "服务角色申请" }
}

Web 管理端审核

cfc-web 新增 ServiceRoleApplications.vue

  • 列表展示所有 pending 申请
  • 详情查看:申请角色、身份证照片、资质证书、地址
  • 操作:批准/驳回(驳回时填写原因)

替换现有申请页

统一入口上线后:

  • pages/butler/apply.vue — 标记废弃,可考虑跳转到统一入口
  • pages/nutritionist/apply.vue — 同上
  • pages/vendor/apply.vue — 同上
  • pages/guide/register/index.vue — 同上

实施顺序

  1. 数据库迁移(建表 + User 加字段)
  2. 后端 Entity + DTO + Mapper
  3. 后端 Service + Controller + 图片上传
  4. Web 管理端审核页面
  5. 前端统一申请页
  6. ProfileMenu 入口
  7. 替换/废弃旧申请页