# 用户端小程序 实名认证 开发设计文档 - 模块:ruoyi-user(会员业务)+ ruoyi-gateway(路由/鉴权头注入)+ hugray-admin(后台主体认证审核) - 日期:2026-10-13 - 需求来源:"点击实名认证跳转到实名认证页面,只做基础的身份证格式校验和姓名必填校验不做三方真实验证,提交后的数据需要到管理后台的主体认证审核列表" - 关联前端:`hugray-user-miniapp`(`pages/profile/edit.vue` 入口、新增 `pages/profile/certification.vue`) - 关联后台:`hugray-admin/src/views/member/certification/index.vue`(主体认证审核列表,菜单 2202) - 关联文档:`doc/design/用户端_我的资料_头像上传_20261013.md`(同类 C 端写接口范式)、`doc/design/用户端_我的资料_20261013.html`(本页 UI 参考) - 数据权威源:`192.168.110.111:3306/hugray-account`(实际表结构优先于本文档) --- ## 一、功能概述 「我的资料」页(`pages/profile/edit.vue`)「实名认证」行当前点击仅弹占位 toast,本次改为**跳转到实名认证页**: 1. 新增 C 端实名认证页 `pages/profile/certification.vue`(自定义导航栏 + 姓名 / 身份证号两个输入 + 底部「提交」按钮,见原型图); 2. 页面**只做基础校验**:姓名必填、身份证号为 18 位标准格式;**不接入任何三方实名核验**; 3. 提交后落 `account_certification_apply`(`subject_type='1'` 个人、`audit_status='0'` 待审核),并联动 `account_user.auth_status='1'`(审核中),使其出现在后台**主体认证审核列表**; 4. 「我的资料」页实名认证徽标改为按后端下发的 `authStatus` 显示真实状态(未认证 / 认证中 / 已认证 / 认证失败),形成闭环。 后台「审」链路(列表 / 详情 / 通过 / 驳回)**已存在且无需改动**:`AccountCertificationApplyController`(`/account/user/certification/**`,权限 `member:certification:*`)、`AccountCertificationApplyServiceImpl`(审核 + `auth_status` 联动 + 通过后落 `account_certification`)。本次只补 C 端"提交"这一缺口。 --- ## 二、现状与关键约束 ### 2.1 缺口:只有"审"没有"增" `AccountCertificationApplyController` 类注释明确:*"本模块只有"审"没有"增删":申请由 C 端会员提交(不在本仓库范围)"*。C 端提交接口与页面均需**新建**。 ### 2.2 数据模型已就绪(无 DDL 变更) | 表 | 用途 | 关键列 | | --- | --- | --- | | `account_certification_apply` | 每次提交的申请(驳回重提多一条),**本次落这张表** | `id`(自增主键兼申请号)、`user_id`、`subject_type`、`real_name`、`id_card`、`audit_status`、`del_flag`、`create_by`、`create_time` | | `account_certification` | 当前生效认证资料,审核**通过**时由服务层落一份 | 审核通过后写入,本次不直接写 | | `account_user.auth_status` | 会员整体认证状态 | 0未认证 1审核中 2已认证 3认证失败 | > 状态联动口径(见 DDL 注释):**提交申请 → `auth_status='1'`;通过 → `'2'`;驳回 → `'3'`**。通过/驳回联动已由既有审核服务实现,本次只补"提交 → '1'"。 ### 2.3 主键策略 `account_certification_apply.id` 为 `bigint AUTO_INCREMENT`(既有表,非雪花),故 insert 走 `useGeneratedKeys="true" keyProperty="id"`,与既有 `insertCertification` 一致。 ### 2.4 认证状态取值必须收口 状态/类型取值语义已在多处存在,本次沿用既有字符串约定(与 `AccountCertificationApplyServiceImpl` 中的 `AUDIT_PASS="1"` 等常量同风格),在服务层抽为常量,不散落魔法值。 ### 2.5 路径规划 - 网关注入 `user_id`;`SecurityUtils.getUserId()` 取会员身份,**C 端接口不接受前端传 userId**(防水平越权)。 - C 端个人中心控制器 `AccountProfileController`(`@RequestMapping("/user")`,外部 `/miniapp/account/user/**`)已承载 `/info`、`/avatar`,本次**在其上新增** `POST /user/certification`(外部 `POST /miniapp/account/user/certification`)。 - 与后台 `AccountCertificationApplyController`(外部 `/account/user/certification/**`)**前缀不同域**(`/miniapp/account` vs `/account`),无冲突。 --- ## 三、接口设计 统一响应体:C 端 `R`(成功 `code=200`);后台 `AjaxResult` / `TableDataInfo`。 ### 3.1 提交实名认证(本次新增) - **外部路径**:`POST /miniapp/account/user/certification`(网关 `StripPrefix=2` → 服务内 `POST /user/certification`) - **鉴权**:需登录。路径不在网关白名单,未登录由 `AuthFilter` 直接返回 401,前端 `request.js` 自动跳登录页;**后端不做未登录分支**。 - **入参**:JSON,会员身份取自网关注入的 `user_id` 头,**不接受前端传 userId** | 字段 | 类型 | 必填 | 校验 | 说明 | | --- | --- | --- | --- | --- | | realName | String | 是 | `@NotBlank` + `@Size(max=64)` | 姓名(对应 `real_name varchar(64)`) | | idCard | String | 是 | `@NotBlank` + 18 位格式 `@Pattern` + `@Size(max=32)` | 身份证号(对应 `id_card varchar(32)`) | - **出参**:`R`(成功即 `code=200`) - **错误提示**:`姓名不能为空` / `姓名过长` / `身份证号不能为空` / `身份证号格式不正确` / `已有实名认证申请正在审核中,请勿重复提交` 身份证号基础格式正则(18 位:地区码 6 位 + 出生日期 + 顺序码 + 校验位;**只校验结构与日期范围,不校验校验码、不核验真实性**): ``` ^[1-9]\d{5}(18|19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dXx]$ ``` ### 3.2 查询会员信息(本次扩展) - **外部路径**:`GET /miniapp/account/user/info`(既有) - **改动**:出参 `AccountUserInfoVo` 新增 `authStatus` 字段(`account_user.auth_status` 原值:0/1/2/3),供「我的资料」页显示实名认证真实状态。手机号仍只下发脱敏值。 ### 3.3 后台审核链路(既有,不改) `GET /account/user/certification/list`(`member:certification:list`)、`GET /account/user/certification/detail/{id}`、`PUT /account/user/certification/audit`(`member:certification:audit`)。C 端提交的申请将直接出现在该列表(`subject_type='1'` 个人、`audit_status='0'` 待审核,列表按待审核在前排序)。 --- ## 四、数据库设计 库:`hugray-account`。 **无 DDL 变更**:`account_certification_apply` 与 `account_user.auth_status` 均已存在,本次仅插入 / 更新。 --- ## 五、流程设计 ### 5.1 前端提交链路(`pages/profile/certification.vue`) ``` STEP 1: onShow 校验登录态(ensureLogin),未登录跳登录页 STEP 2: 用户填写姓名 / 身份证号 STEP 3: 点「提交」→ 本地校验:姓名非空(trim)且 ≤64;身份证号匹配 18 位正则 STEP 4: 校验不通过则 toast 提示并中止,不发请求 STEP 5: 调 submitCertification(realName, idCard) → POST /miniapp/account/user/certification STEP 6: 成功后 toast「提交成功,等待审核」并 navigateBack 回我的资料页;失败提示后端错误信息 ``` ### 5.2 后端提交(`AccountCertificationApplyServiceImpl.submitPersonalCertification`) ``` STEP 1: 校验 userId 非空(未登录由网关拦截,此处兜底),为空抛业务异常 STEP 2: 防重复提交:该会员若存在 audit_status='0'(待审核)且未删除的申请, 抛「已有实名认证申请正在审核中,请勿重复提交」 STEP 3: 组装申请快照:subject_type='1'(个人)、real_name、id_card、 audit_status='0'、create_by=登录用户名 STEP 4: insertCertificationApply 落 account_certification_apply(自增回填 id) STEP 5: 联动 account_user.auth_status='1'(审核中,调既有 updateUserAuthStatus 单列语句) STEP 6: 以上 STEP 4/5 同事务(@Transactional),任一失败整体回滚 ``` ### 5.3 状态流转(端到端) ``` C 端提交 ──► account_certification_apply.audit_status='0' + account_user.auth_status='1' │ 后台「通过」──► audit_status='1' + auth_status='2' + 落 account_certification(既有逻辑) 后台「驳回」──► audit_status='2' + auth_status='3' + 记审核意见(既有逻辑) ``` --- ## 六、枚举与状态 | 名称 | 取值 | 说明 | | --- | --- | --- | | `subject_type` 主体类型 | `1` 个人 / `2` 个体工商户 / `3` 企业 | 本次固定提交 `1`(个人实名);企业认证入口本期仍为占位 | | `audit_status` 审核状态 | `0` 待审核 / `1` 已通过 / `2` 已驳回 | 提交置 `0`,审核改 `1`/`2` | | `auth_status` 会员认证状态 | `0` 未认证 / `1` 审核中 / `2` 已认证 / `3` 认证失败 | 提交置 `1`;通过置 `2`;驳回置 `3` | 「我的资料」页徽标与 `auth_status` 映射:`0`→未认证、`1`→认证中、`2`→已认证、`3`→认证失败。 --- ## 七、影响范围与文件清单 ### 新增(ruoyi-user) | 文件 | 说明 | | --- | --- | | `domain/dto/AccountCertificationApplyDto.java` | C 端实名认证入参(`realName` / `idCard`,含校验注解与字段注释) | | `src/test/.../AccountCertificationApplyServiceImplTest.java` | 提交服务单测(正常 / 待审核重复提交拒绝 / 状态联动) | ### 修改(ruoyi-user) | 文件 | 改动 | | --- | --- | | `controller/AccountProfileController.java` | 注入 `IAccountCertificationApplyService`;新增 `POST /user/certification` | | `service/IAccountCertificationApplyService.java` | 新增 `submitPersonalCertification(Long userId, String realName, String idCard)` | | `service/impl/AccountCertificationApplyServiceImpl.java` | 实现提交(防重校验 + 落申请 + 联动 `auth_status='1'`,`@Transactional`) | | `mapper/AccountCertificationApplyMapper.java` | 新增 `insertCertificationApply(AccountCertificationApply)`、`countPendingApplyByUserId(Long userId)` | | `resources/mapper/user/AccountCertificationApplyMapper.xml` | 新增上述两条 SQL | | `domain/vo/AccountUserInfoVo.java` | 新增 `authStatus` 字段 | | `service/impl/AccountUserServiceImpl.java` | `getAccountUserInfo` 填充 `authStatus` | | `src/test/.../AccountUserServiceImplTest.java` | 补充 `authStatus` 下发断言 | ### 新增(前端 hugray-user-miniapp) | 文件 | 说明 | | --- | --- | | `pages/profile/certification.vue` | 实名认证页(自定义导航栏 + 姓名/身份证号输入 + 底部提交;沿用 `edit.vue` 的导航与按钮风格) | ### 修改(前端 hugray-user-miniapp) | 文件 | 改动 | | --- | --- | | `pages.json` | 注册 `pages/profile/certification`(`navigationStyle: custom`) | | `api/index.js` | 新增 `submitCertification(realName, idCard)`(→ `/miniapp/account/user/certification`) | | `pages/profile/edit.vue` | 「实名认证」行由 `handlePlaceholder` 改为 `uni.navigateTo` 跳认证页;`realNameStatus` 改为按 `/info` 返回的 `authStatus` 映射 | > 网关无需改动:`/miniapp/account/**` 路由已存在。 --- ## 八、边界与异常清单 | # | 场景 | 处理 | | --- | --- | --- | | 1 | 未登录访问 | 网关 `AuthFilter` 返回 401,前端跳登录页;后端不做分支 | | 2 | `user_id` 头缺失/为空 | 服务内兜底判空,抛业务异常(不 NPE) | | 3 | 姓名为空 / 纯空格 | DTO `@NotBlank` 拦截,提示「姓名不能为空」 | | 4 | 姓名超长(>64) | DTO `@Size` 拦截,提示「姓名过长」 | | 5 | 身份证号为空 | DTO `@NotBlank` 拦截,提示「身份证号不能为空」 | | 6 | 身份证号格式非法(非 18 位 / 含非法字符 / 非 X 结尾) | DTO `@Pattern` 拦截,提示「身份证号格式不正确」 | | 7 | 已有待审核申请再次提交 | 服务层 `countPendingApplyByUserId>0` 时抛「已有实名认证申请正在审核中,请勿重复提交」 | | 8 | 落库或状态联动失败 | 同一事务回滚,返回统一错误,不产生半更新 | | 9 | 越权 | 会员身份取自令牌注入头,不接受前端传 `userId`,天然防水平越权 | | 10 | 三方实名核验 | **本期明确不做**,仅基础格式校验 | --- ## 九、规范符合性说明 - 分层:Controller 只取身份 + 转发,业务在 Service,SQL 在 Mapper;Controller 不直接调 Mapper。 - 命名:`submitPersonalCertification` / `insertCertificationApply` / `countPendingApplyByUserId` 见名知意;DTO 与 Entity 分离。 - 注释:DTO / Mapper 方法 / Service 方法具备 Javadoc;核心流程按 `// STEP x:` 分步注释;实体状态字段注明取值。 - 魔法值:`subject_type`、`audit_status`、`auth_status` 取值抽为常量;错误提示集中。 - 事务:提交接口 `insert + 联动更新` 同事务,保证一致性。 - 幂等/防重:以待审核申请为闸门,拒绝重复提交。 - 数据库:无 DDL;无 `SELECT *`;无 `${}` 拼接;无循环查库。 - 单测:覆盖提交正常、重复提交拒绝、状态联动;Mockito Mock Mapper,不依赖真实库。 --- ## 十、已确认决策 1. **状态回显**:**已定** —— 扩展 `AccountUserInfoVo.authStatus`,「我的资料」页徽标显示真实状态(未认证/认证中/已认证/认证失败)。 2. **重复提交**:**已定** —— 存在「待审核」申请时**拒绝并提示**,不新增重复单。 3. **核验深度**:**已定** —— 仅姓名必填 + 身份证号 18 位格式校验,**不做三方实名核验**。 4. **入口范围**:**已定** —— 本期只做「实名认证」(个人 `subject_type='1'`);「企业/个体户认证」入口维持占位。 --- ## 十一、增量需求:认证状态回填与只读(2026-10-13 补充) ### 11.1 需求描述 进入实名认证页时,按当前认证状态**回填**该会员已提交的资料,并区分交互: | 当前状态 | 姓名 / 身份证号 | 底部按钮(文案 / 可点性) | 驳回原因 | | --- | --- | --- | --- | | 未认证(无申请) | 空,可编辑 | 「提交」/ 可点 | 不展示 | | 认证中(待审核) | 回填,**只读** | 「审核中」/ **置灰不可点** | 不展示 | | 认证成功(已通过) | 回填,**只读** | 「已认证」/ **置灰不可点** | 不展示 | | 认证失败(已驳回) | 回填,**可编辑** | 「重新提交」/ 可点 | **在身份证号下方展示** | > 已确认决策:认证成功同样置灰不可点(已认证无需重复提交);认证中 / 认证成功输入框只读不可编辑。 ### 11.2 接口设计(新增:查询当前认证状态) - **外部路径**:`GET /miniapp/account/user/certification`(网关 `StripPrefix=2` → 服务内 `GET /user/certification`) - 与既有 `POST /miniapp/account/user/certification`(提交)**同路径不同方法**,无需新增路由。 - 鉴权:需登录;会员身份取自网关注入的登录态,**不接受前端传 userId**。 取该会员**最近一次**申请(`account_certification_apply` 按 `id desc limit 1`,只取 `del_flag='0'`),出参 `AccountCertificationStatusVo`: | 字段 | 类型 | 说明 | | --- | --- | --- | | realName | String | 姓名(回填用) | | idCard | String | 身份证号(回填用) | | auditStatus | String | 0待审核 1已通过 2已驳回;**为空表示从未提交** | | auditRemark | String | 驳回原因(audit_status='2' 时有值) | 无申请时返回**空字段对象**(各字段为 null),前端按「未认证」处理,不返回 404。 ### 11.3 前端状态映射 `auditStatus` → 页面状态:`'0'`→认证中、`'1'`→认证成功、`'2'`→认证失败、空/缺省→未认证。 - 只读 & 置灰条件:页面状态 ∈ {认证中, 认证成功}; - 驳回原因展示条件:页面状态 = 认证失败 且 `auditRemark` 非空; - 按钮文案:未认证→「提交」、认证中→「审核中」、认证成功→「已认证」、认证失败→「重新提交」(未匹配兜底「提交」)。 ### 11.4 影响范围(增量文件清单) **新增(ruoyi-user)** | 文件 | 说明 | | --- | --- | | `domain/vo/AccountCertificationStatusVo.java` | C 端认证状态回填出参(realName / idCard / auditStatus / auditRemark) | **修改(ruoyi-user)** | 文件 | 改动 | | --- | --- | | `mapper/AccountCertificationApplyMapper.java` | 新增 `selectLatestApplyByUserId(Long userId)` | | `resources/mapper/user/AccountCertificationApplyMapper.xml` | 新增上述查询 SQL(取最近一条) | | `service/IAccountCertificationApplyService.java` | 新增 `getCertificationStatus(Long userId)` | | `service/impl/AccountCertificationApplyServiceImpl.java` | 实现查询:取最近一条申请映射为 VO,无则返回空对象 | | `controller/AccountProfileController.java` | 新增 `GET /user/certification` | | `src/test/.../AccountCertificationApplyServiceImplTest.java` | 补充查询用例(有申请回填 / 无申请空对象) | **修改(前端 hugray-user-miniapp)** | 文件 | 改动 | | --- | --- | | `api/index.js` | 新增 `getCertificationStatus()`(→ `GET /miniapp/account/user/certification`) | | `pages/profile/certification.vue` | 进入页面拉取状态并回填;认证中/成功输入框只读、按钮置灰且文案随状态变化(提交/审核中/已认证/重新提交);认证失败展示驳回原因 | > 无需 DDL、无需网关改动(`/miniapp/account/**` 路由已存在)。 ### 11.5 边界与异常(增量) | # | 场景 | 处理 | | --- | --- | --- | | 1 | 从未提交(查询返回空对象) | 页面按未认证处理,空表单可编辑可提交 | | 2 | 查询接口异常 | 静默降级为未认证(空表单),不阻塞页面 | | 3 | 认证中 / 认证成功 | 输入框 `disabled` 只读,按钮置灰且点击直接 return | | 4 | 认证失败但驳回原因为空 | 不展示驳回原因区域(仅可编辑重提) | | 5 | 认证失败后重新提交成功 | 回退上一页;再次进入时按新的「待审核」状态回填只读 |