From f8c73a38709f7120d11b04bd70c6af3ccc53014d Mon Sep 17 00:00:00 2001
From: zjk <852185829@qq.com>
Date: 星期二, 13 十月 2026 14:51:37 +0800
Subject: [PATCH] feat: 新增用户小程序会员个人中心及资料编辑页面
---
hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/domain/AccountEarningRecord.java | 121 +++
hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/service/impl/AccountUserServiceImpl.java | 75 +
hugray-server/ruoyi-modules/ruoyi-user/src/test/java/com/ruoyi/user/service/impl/AccountUserServiceImplTest.java | 120 +++
hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/controller/AccountProfileController.java | 42 +
hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/enums/EarningSourceEnum.java | 61 +
hugray-server/sql/hugray_account_20261013.sql | 37 +
doc/design/用户端_我的资料_20261013.html | 377 ++++++++++
hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/mapper/AccountEarningRecordMapper.java | 22
hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/enums/MemberLevelEnum.java | 78 ++
doc/nacos/dev/ruoyi-user-dev.yml | 2
hugray-user-miniapp/pages.json | 7
hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/domain/vo/AccountUserInfoVo.java | 90 ++
hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/controller/AccountMemberInnerController.java | 2
hugray-server/ruoyi-modules/ruoyi-user/src/main/resources/mapper/user/AccountUserMapper.xml | 14
hugray-server/sql/hugray_account_20261012.sql | 14
doc/design/用户端_首页与导航_20261012.md | 14
hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/service/IAccountUserService.java | 9
hugray-user-miniapp/pages/profile/edit.vue | 447 ++++++++++++
doc/design/用户端_登录注册_20261012.md | 23
hugray-user-miniapp/pages/profile/index.vue | 87 +
doc/design/用户端_个人中心_20261013.md | 263 +++++++
hugray-user-miniapp/api/index.js | 7
hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/domain/vo/EarningSummaryVo.java | 72 ++
hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/domain/AccountUser.java | 27
hugray-server/ruoyi-api/ruoyi-api-system/src/main/java/com/ruoyi/system/api/domain/AccountMember.java | 12
hugray-server/ruoyi-modules/ruoyi-user/src/main/resources/mapper/user/AccountEarningRecordMapper.xml | 23
hugray-user-miniapp/utils/format.js | 20
27 files changed, 1,986 insertions(+), 80 deletions(-)
diff --git "a/doc/design/\347\224\250\346\210\267\347\253\257_\344\270\252\344\272\272\344\270\255\345\277\203_20261013.md" "b/doc/design/\347\224\250\346\210\267\347\253\257_\344\270\252\344\272\272\344\270\255\345\277\203_20261013.md"
new file mode 100644
index 0000000..cf76667
--- /dev/null
+++ "b/doc/design/\347\224\250\346\210\267\347\253\257_\344\270\252\344\272\272\344\270\255\345\277\203_20261013.md"
@@ -0,0 +1,263 @@
+# 用户端小程序 个人中心 接口 开发设计文档
+
+- 模块:ruoyi-user(会员业务)+ ruoyi-gateway(路由/鉴权头注入)
+- 日期:2026-10-13
+- 需求来源:“个人中心页的接口功能实现,根据现在展示的字段实现对应的 java 后端业务”
+- 关联前端:`hugray-user-miniapp`(`pages/profile/index.vue`)
+- 关联文档:`doc/design/用户端_登录注册_20261012.md`(C 端会员落库与令牌载体方案)、`doc/design/用户端_首页与导航_20261012.md`(9.3 个人中心页规格)
+- 数据权威源:`192.168.110.111:3306/hugray-account`(实际表结构优先于本文档)
+
+---
+
+## 一、功能概述
+
+个人中心页(`pages/profile/index.vue`)当前为本地 mock,需由后端下发以下字段,页面据此渲染:
+
+| 展示字段 | 页面位置 | 现状 | 本次是否提供 |
+| --- | --- | --- | --- |
+| 姓名 `nickname` | 头部文本 | mock `辉煌G891` | 是(`account_user.nickname`;注册未采集姓名字段,落库为空,前端回退展示脱敏手机号) |
+| 手机号 `phone`(脱敏) | 头部名称回退 | 无 | 是(`account_user.phone`,脱敏为 `133****5555`,不下发明文) |
+| 会员等级 `memberLevel` | 等级胶囊 `C`/`L1~L4` | mock `L4` | 是(复用既有 `account_user.system_level`) |
+| 头像 `avatar` | 头部头像 | 相机图标占位 | 是(已存在字段,有值前端渲染图片,无值回退图标) |
+| 今日 / 本月 / 累计收益 | 收益玻璃卡三栏 | mock `10.00 / 400.00 / 128000.00` | 是(新增收益流水表,实时聚合) |
+| 仓库订单角标数量 | 菜单角标 | mock `1` | 否(见 2.5,无数据来源,本期不入接口) |
+
+展示规则(沿用已确认业务约定):`isRestrictedView = !isLoggedIn || memberLevel === 'C'`;**未登录或 C 普通用户**仅展示「收货地址」「联系我们」两行菜单并隐藏收益卡;**L1~L4** 展示全部内容。该判定由前端完成,后端只负责下发 `memberLevel` 真实值。
+
+本期只新增 **一个 C 端查询接口**:`GET /miniapp/account/user/info`(对应前端已预留的 `getMemberInfo()`)。
+
+---
+
+## 二、现状与关键约束
+
+### 2.1 表结构缺口
+
+`account_user`(`hugray-account` 库)现有字段:`user_id, username, phone, password, nickname, avatar, gender, birthday, status, del_flag, my_referral_code, system_level, referrer_id, referrer_bind_time, create_*, update_*, remark`。
+
+**已有「会员等级」字段 `system_level`(`varchar(8)`,默认 `C`),无任何「收益」相关表/字段** → 收益部分本次新增;等级复用既有列,不再新增重复列。
+
+### 2.2 已有接口不足以支撑
+
+`ruoyi-user` 现有控制器:
+
+- `AccountUserController`(`/user`,后台 CRUD,全部挂 `@RequiresPermissions`)——面向后端管理,不适合 C 端调用。
+- `AccountMemberInnerController`(`/member`,`@InnerAuth`)——仅服务间调用,按手机号查会员 / 注册。
+
+**无按当前登录会员返回个人中心数据的接口** → 本次新增。
+
+### 2.3 登录态与取数来源(已定方案)
+
+- C 端令牌沿用 RuoYi `TokenService`,网关 `AuthFilter` 校验通过后向目标服务注入请求头 `user_id`、`username`(`SecurityConstants.DETAILS_USER_ID/DETAILS_USERNAME`)。
+- `ruoyi-user` 侧通过 `SecurityUtils.getUserId()` 读取 `user_id`,即当前登录会员 ID。
+- 该路径**不经过** `/miniapp/system/user/getInfo`(后者面向后台 `sys_user`,见 2.4)。
+
+### 2.4 前端预留入口的取舍
+
+`hugray-user-miniapp/api/index.js` 现有两个未接线函数:
+
+| 函数 | 路径 | 处理 |
+| --- | --- | --- |
+| `getMemberInfo()` | `/miniapp/account/user/info` | **本期采用**(指向 ruoyi-user) |
+| `getUserInfo()` | `/miniapp/system/user/getInfo` | **误导**:指向后台 `sys_user`,与 C 端会员不同源;全仓无调用点,建议删除 |
+
+结论:个人中心统一走 `getMemberInfo()`;`getUserInfo()` 予以移除(避免后续误用)。
+
+### 2.5 仓库订单角标数量:本期不提供
+
+角标数量应来源于「仓库订单」,但 `ruoyi-product` 模块**当前仅有一个启动类**(无订单/仓库任何表与接口),无数据来源。本期接口不返回该字段,前端保持现状(`withBadge` 已登录展示,值仍为本地 mock `1`),待订单模块就绪后由订单服务提供独立接口,不在本次范围。
+
+### 2.6 金额单位约定
+
+用户规范要求“金额用分(long)”。故接口 `earnings` 三值均以 **分** 返回(如 `1000` = 10.00 元),前端新增 `分→元` 格式化后再展示为两位小数。
+
+---
+
+## 三、接口设计
+
+统一响应体:`R<T>`(`code=200` 成功),与 C 端 `/miniapp/**` 既有约定一致;前端 `utils/request.js` 按 `successCode=200` / `unauthorizedCode=401` 解包,调用方取 `res.data`。
+
+> 规范说明:用户规则中的“`Result<T>`、`code=0 成功`、错误码分段”为通用模板;本工程实际框架为 RuoYi `R`/`AjaxResult`(200 成功)+ `@RestControllerAdvice` 全局异常。按“与现有工程保持一致”优先,采用框架既有约定。
+
+### 3.1 查询当前会员个人中心信息
+
+- **外部路径**:`GET /miniapp/account/user/info`(网关 `StripPrefix=2` → 服务内 `/user/info`)
+- **鉴权**:需登录。路径不在网关白名单内,未登录由 `AuthFilter` 直接返回 401,前端 `request.js` 自动跳登录页;**后端不做未登录分支**。
+- **入参**:无(会员身份取自网关注入的 `user_id` 头)
+- **出参**:`R<AccountUserInfoVo>`
+
+```json
+{
+ "code": 200,
+ "msg": "操作成功",
+ "data": {
+ "userId": 100,
+ "nickname": null,
+ "phone": "133****5555",
+ "avatar": null,
+ "memberLevel": "L4",
+ "earnings": { "today": 1000, "month": 40000, "total": 12800000 }
+ }
+}
+```
+
+| 字段 | 类型 | 说明 |
+| --- | --- | --- |
+| userId | Long | 会员ID |
+| nickname | String | 姓名/昵称(注册未采集,可为空;为空时前端回退展示脱敏手机号) |
+| phone | String | 手机号(**脱敏**,形如 `133****5555`,不下发明文) |
+| avatar | String | 头像地址,可为空 |
+| memberLevel | String | 会员等级 `C`/`L1~L4`(空或非法兜底为 `C`;对外字段名沿用 `memberLevel`,数据源为 `account_user.system_level`) |
+| earnings.today | Long | 今日收益(单位:分) |
+| earnings.month | Long | 本月收益(单位:分) |
+| earnings.total | Long | 累计收益(单位:分) |
+
+- **错误提示**:`获取会员信息失败`(会员不存在或查询异常)
+- **说明**:手机号仅下发**脱敏**值(`133****5555`),不下发明文;`userName`(C 端即手机号)不下发,避免敏感信息外泄。
+
+---
+
+## 四、数据库设计
+
+库:`hugray-account`(权威库 `192.168.110.111:3306`)。脚本:`sql/hugray_account_20261013.sql`(与代码同一次提交,含变更 + 回滚)。
+
+### 4.1 会员等级:复用既有 `account_user.system_level`(无 DDL)
+
+权威库 `account_user` 已存在 `system_level varchar(8) DEFAULT 'C'`(取值 `C<L1<L2<L3<L4`,从低到高),故**不再新增 `member_level` 重复列**。代码统一读写既有 `system_level`,对外出参仍为 `memberLevel`。
+
+### 4.2 变更:新增收益流水表 `account_earning_record`
+
+```sql
+CREATE TABLE `account_earning_record` (
+ `record_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '收益记录ID',
+ `user_id` bigint(20) NOT NULL COMMENT '会员ID(关联 account_user.user_id)',
+ `amount` bigint(20) NOT NULL DEFAULT 0 COMMENT '收益金额(单位:分,可正可负)',
+ `source` char(1) NOT NULL COMMENT '收益来源(1订单收益 2邀请奖励 3等级差价 9其他)',
+ `biz_no` varchar(64) DEFAULT NULL COMMENT '业务单号(同来源幂等去重)',
+ `remark` varchar(255) DEFAULT NULL COMMENT '备注',
+ `create_time` datetime DEFAULT NULL COMMENT '发生时间',
+ `update_time` datetime DEFAULT NULL COMMENT '更新时间',
+ PRIMARY KEY (`record_id`),
+ KEY `idx_earning_user_time` (`user_id`, `create_time`),
+ UNIQUE KEY `uk_earning_source_biz` (`source`, `biz_no`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='会员收益流水表';
+```
+
+- 采用**流水 + 实时聚合**而非三个汇总结算字段:今日 / 本月随日期自然滚动,无需定时归零;同时天然支撑菜单「收入明细」页(后续)。
+- `idx_earning_user_time` 覆盖 `WHERE user_id=? AND create_time>=?` 聚合,避免全表扫描。
+- `uk_earning_source_biz` 由 `(source, biz_no)` 保证业务幂等;`biz_no` 为 `NULL` 时 MySQL 唯一索引不冲突(手工补录场景允许)。
+
+**回滚**:
+
+```sql
+DROP TABLE IF EXISTS `account_earning_record`;
+```
+
+---
+
+## 五、流程设计
+
+### 5.1 查询个人中心信息(`AccountUserServiceImpl.getAccountUserInfo`)
+
+```
+STEP 1: 校验 userId 非空(未登录由网关拦截,此处兜底),为空抛「获取会员信息失败」
+STEP 2: 按 userId 查 account_user(del_flag='0');为空抛「获取会员信息失败」
+STEP 3: 查收益汇总(单条 SQL,单位分),无流水返回 0
+STEP 4: 组装 AccountUserInfoVo:nickname/avatar 直取;phone 脱敏(`133****5555`);memberLevel 取 system_level,空或非法兜底 MemberLevelEnum.C
+STEP 5: 返回 VO
+```
+
+收益汇总 SQL(一次查询出三值):
+
+```sql
+SELECT
+ IFNULL(SUM(CASE WHEN create_time >= CURDATE() THEN amount END), 0) AS today,
+ IFNULL(SUM(CASE WHEN create_time >= DATE_FORMAT(CURDATE(), '%Y-%m-01') THEN amount END), 0) AS month,
+ IFNULL(SUM(amount), 0) AS total
+FROM account_earning_record
+WHERE user_id = #{userId}
+```
+
+---
+
+## 六、枚举与状态
+
+| 名称 | 取值 | 说明 |
+| --- | --- | --- |
+| 会员等级 `memberLevel`(源 `system_level`) | `C` / `L1` / `L2` / `L3` / `L4` | `MemberLevelEnum`:`C`=普通用户(受限展示);`L1~L4`=会员;含 `code` + `desc` + `getByCode` + `normalize` |
+| 会员状态 `status` | `0` 正常 / `1` 停用 | 复用现有语义 |
+| 删除标志 `delFlag` | `0` 存在 / `2` 删除 | 复用现有语义 |
+| 收益来源 `source` | `1` 订单收益 / `2` 邀请奖励 / `3` 等级差价 / `9` 其他 | `EarningSourceEnum`(本期仅建枚举,写入方待订单/邀请模块接入) |
+
+> 等级升降级规则(何种行为升到 L1~L4)属于会员体系策略,尚未定义。本期 `system_level` 由后台维护(`AccountUserController` 编辑),接口只负责读取下发;后续等级规则确定后新增独立升级流程。
+
+---
+
+## 七、影响范围与文件清单
+
+### 新增(ruoyi-user)
+
+| 文件 | 说明 |
+| --- | --- |
+| `enums/MemberLevelEnum.java` | 会员等级枚举(`C`/`L1~L4`,含 `getByCode` + `normalize`) |
+| `enums/EarningSourceEnum.java` | 收益来源枚举(`1/2/3/9`) |
+| `domain/AccountEarningRecord.java` | 收益流水实体(含字段注释) |
+| `domain/vo/AccountUserInfoVo.java` | 个人中心信息出参 VO(含脱敏手机号 `phone`) |
+| `domain/vo/EarningSummaryVo.java` | 收益汇总出参(today/month/total,单位分) |
+| `mapper/AccountEarningRecordMapper.java` + `resources/mapper/user/AccountEarningRecordMapper.xml` | 收益汇总查询 |
+| `controller/AccountProfileController.java` | C 端接口 `GET /user/info` |
+
+### 修改
+
+| 文件 | 改动 |
+| --- | --- |
+| ruoyi-user `domain/AccountUser.java` | 复用既有 `systemLevel`(源 `system_level`)字段;推荐关系字段为 `referrerId`(源 `referrer_id`)+ getter/setter + toString |
+| ruoyi-user `mapper/AccountUserMapper.xml` | `system_level` / `referrer_id` 纳入 resultMap / 列 / insert / update |
+| ruoyi-user `service/IAccountUserService.java` + `service/impl/AccountUserServiceImpl.java` | 新增 `getAccountUserInfo(Long userId)`;注册不再以手机号填充姓名(`nickname` 留空);出参手机号脱敏 |
+| `sql/hugray_account_20261013.sql` | 新增 DDL(变更 + 回滚) |
+| 前端 `hugray-user-miniapp/api/index.js` | 删除误导且无调用的 `getUserInfo()` |
+| 前端 `hugray-user-miniapp/utils/`(新增格式化方法) | `分→元` 两位小数格式化 |
+| 前端 `hugray-user-miniapp/pages/profile/index.vue` | `onShow` 调 `getMemberInfo()` 替换 mock;头部名称优先姓名、为空回退脱敏手机号 |
+| `doc/design/用户端_首页与导航_20261012.md` | 9.3 / 9.5 标注 mock 已由接口替换 |
+
+> 网关无需改动:`/miniapp/account/**` → `ruoyi-user` 路由已存在。
+
+---
+
+## 八、边界与异常清单
+
+| # | 场景 | 处理 |
+| --- | --- | --- |
+| 1 | 未登录访问 | 网关 `AuthFilter` 返回 401,前端跳登录页;后端不做分支 |
+| 2 | `user_id` 头缺失/为空 | 服务内兜底判空,抛「获取会员信息失败」(不 NPE) |
+| 3 | 会员不存在 / 已逻辑删除 | 抛「获取会员信息失败」 |
+| 4 | `system_level` 为空或非法(历史脏数据) | 兜底为 `C`,不影响页面渲染 |
+| 5 | 无任何收益流水 | 三值均返回 `0`(分),前端展示 `0.00` |
+| 6 | 收益为负(退款冲正) | `amount` 支持负数,聚合自然抵扣 |
+| 7 | 金额单位一致性 | 接口统一返回分;前端统一格式化,禁止后端直接返回字符串金额 |
+| 8 | 敏感信息 | 接口下发**脱敏**手机号(`133****5555`),不下发明文与 `userName`;日志手机号脱敏 |
+| 9 | 越权 | 会员身份取自令牌注入头,不接受前端传入 `userId`,天然防水平越权 |
+| 10 | 姓名未采集(注册未填) | `nickname` 为空,前端头部回退展示脱敏手机号,不报错 |
+
+---
+
+## 九、规范符合性说明
+
+- 分层:Controller 只做取身份 + 转发,业务在 Service,SQL 在 Mapper;Controller 不直接调 Mapper。
+- 命名:`getAccountUserInfo` / `selectEarningSummary` 等见名知意;Entity / VO 分离。
+- 注释:类注释、字段注释(含取值与单位说明)、Javadoc、核心流程 `// STEP x:` 注释。
+- 魔法值:等级、收益来源收口枚举;CUCRATE 等 SQL 表达式内聚在 Mapper。
+- 异常:统一 `ServiceException` + 全局处理器,不散落 try-catch 拼返回值。
+- 日志:`@Slf4j` 占位符 + `[业务标记]` 前缀,敏感字段脱敏。
+- 数据库:字段显式长度 + COMMENT;聚合列有索引;无 `SELECT *`;无 `${}` 拼接;无循环查库。
+- 单测:`getAccountUserInfo`(正常 / 会员不存在 / 无流水 / 等级空兜底 / 等级非法兜底)补充单测,Mockito Mock Mapper。
+- 说明(与现有工程一致优先):`account_earning_record` 主键沿用自增(与同库 `account_user` 及全工程 RuoYi 风格一致),未采用雪花 ID,作为已知项记录。
+
+---
+
+## 十、已确认决策
+
+1. **等级存储方式**:**已定** —— 复用既有 `account_user.system_level`(`C`/`L1~L4`,默认 `C`),不新增 `member_level` 重复列。
+2. **收益数据模型**:**已定** —— 新建流水表 `account_earning_record` + 实时聚合。
+3. **金额单位**:**已定** —— 接口返回分(`long`),前端格式化为两位小数。
+4. **仓库订单角标**:**已定** —— 本期不提供(`ruoyi-product` 尚无订单实现)。
+5. **数据库连接**:**已定** —— `ruoyi-user-dev.yml` master 已由 `localhost` 改为权威库 `192.168.110.111:3306/hugray-account`(与 `ruoyi-system`/`ruoyi-product` 一致)。
diff --git "a/doc/design/\347\224\250\346\210\267\347\253\257_\346\210\221\347\232\204\350\265\204\346\226\231_20261013.html" "b/doc/design/\347\224\250\346\210\267\347\253\257_\346\210\221\347\232\204\350\265\204\346\226\231_20261013.html"
new file mode 100644
index 0000000..fee42d4
--- /dev/null
+++ "b/doc/design/\347\224\250\346\210\267\347\253\257_\346\210\221\347\232\204\350\265\204\346\226\231_20261013.html"
@@ -0,0 +1,377 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+<meta charset="UTF-8" />
+<meta name="viewport" content="width=device-width, initial-scale=1.0" />
+<title>我的资料 · UI 高保真原型</title>
+<style>
+ :root {
+ --brand-blue: #003e7e;
+ --brand-blue-2: #1e5ebc;
+ --green: #639c1f;
+ --green-2: #82c236;
+ --ink: #0f172a;
+ --label: #1f2937;
+ --sub: #64748b;
+ --muted: #94a3b8;
+ --line: #f1f5f9;
+ --line-2: #e2e8f0;
+ }
+
+ * { margin: 0; padding: 0; box-sizing: border-box; }
+
+ body {
+ font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei", sans-serif;
+ background: #eef2f7;
+ color: var(--ink);
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ padding: 40px 16px 64px;
+ min-height: 100vh;
+ }
+
+ /* ==================== 文档头 ==================== */
+ .doc-head { text-align: center; margin-bottom: 28px; }
+ .doc-title { font-size: 20px; font-weight: 700; }
+ .doc-sub { margin-top: 6px; font-size: 13px; color: var(--sub); }
+
+ /* ==================== 手机框 ==================== */
+ .phone {
+ width: 375px;
+ height: 812px;
+ background: #fff;
+ border-radius: 44px;
+ box-shadow: 0 24px 60px rgba(15, 23, 42, .18), 0 2px 8px rgba(15, 23, 42, .06);
+ overflow: hidden;
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ }
+
+ /* 状态栏(模拟) */
+ .status-bar {
+ height: 44px;
+ flex-shrink: 0;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 0 22px 0 28px;
+ font-size: 14px;
+ font-weight: 600;
+ letter-spacing: .2px;
+ }
+ .status-icons { display: flex; align-items: center; gap: 5px; }
+
+ /* 自定义导航栏 */
+ .nav {
+ height: 44px;
+ flex-shrink: 0;
+ position: relative;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+ .nav-back {
+ position: absolute;
+ left: 10px;
+ width: 40px;
+ height: 40px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+ .nav-title { font-size: 17px; font-weight: 700; letter-spacing: .5px; }
+ .nav-actions {
+ position: absolute;
+ right: 14px;
+ display: flex;
+ align-items: center;
+ gap: 16px;
+ }
+
+ /* ==================== 列表 ==================== */
+ .body { flex: 1; display: flex; flex-direction: column; }
+ .list { padding: 4px 16px 0; }
+
+ .row {
+ min-height: 58px;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ border-bottom: 1px solid var(--line);
+ }
+ .row:last-child { border-bottom: none; }
+ .row-label { font-size: 15px; color: var(--label); }
+ .row-right { display: flex; align-items: center; gap: 8px; }
+ .row-value { font-size: 15px; color: var(--muted); letter-spacing: .3px; }
+
+ /* 头像 */
+ .avatar {
+ width: 46px;
+ height: 46px;
+ border-radius: 50%;
+ background: #f1f5f9;
+ border: 1px solid var(--line-2);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ overflow: hidden;
+ }
+ .avatar img { width: 100%; height: 100%; object-fit: cover; }
+
+ /* 认证状态徽标 */
+ .badge {
+ font-size: 13px;
+ font-weight: 500;
+ line-height: 1;
+ padding: 5px 10px;
+ border-radius: 6px;
+ border: 1px solid;
+ }
+ .badge--none { color: #ea580c; background: #fff7ed; border-color: #fdba74; }
+ .badge--ing { color: #1e5ebc; background: #eff6ff; border-color: #93c5fd; }
+ .badge--fail { color: #dc2626; background: #fef2f2; border-color: #fca5a5; }
+ .badge--done { color: #4d7c0f; background: #f7fee7; border-color: #bef264; }
+
+ /* ==================== 底部操作区 ==================== */
+ .footer {
+ margin-top: auto;
+ padding: 16px 24px 22px;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 18px;
+ }
+ .btn-logout {
+ width: 100%;
+ height: 48px;
+ border-radius: 24px;
+ background: linear-gradient(90deg, #82c236 0%, #639c1f 100%);
+ color: #fff;
+ font-size: 15px;
+ font-weight: 600;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ box-shadow: 0 8px 20px rgba(99, 156, 31, .28);
+ }
+ .link-cancel {
+ font-size: 14px;
+ color: var(--brand-blue-2);
+ text-decoration: underline;
+ text-underline-offset: 3px;
+ }
+
+ /* Home 指示条 */
+ .home-bar {
+ height: 26px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+ }
+ .home-bar span {
+ width: 134px;
+ height: 5px;
+ border-radius: 3px;
+ background: #0f172a;
+ opacity: .85;
+ }
+
+ /* ==================== 图例 / 设计说明 ==================== */
+ .legend {
+ width: 375px;
+ margin-top: 32px;
+ display: flex;
+ flex-direction: column;
+ gap: 18px;
+ }
+ .card {
+ background: #fff;
+ border-radius: 16px;
+ padding: 18px 20px;
+ box-shadow: 0 6px 18px rgba(15, 23, 42, .05);
+ }
+ .card h3 {
+ font-size: 14px;
+ font-weight: 700;
+ margin-bottom: 14px;
+ color: var(--ink);
+ }
+ .badge-row { display: flex; flex-wrap: wrap; gap: 10px; }
+ .legend-note {
+ font-size: 13px;
+ line-height: 1.7;
+ color: var(--sub);
+ }
+ .legend-note b { color: var(--label); font-weight: 600; }
+ .legend-note .no { color: var(--brand-blue-2); font-weight: 700; margin-right: 4px; }
+ .legend-note li { list-style: none; margin-bottom: 6px; }
+ .avatar-row { display: flex; gap: 28px; align-items: center; }
+ .avatar-item { display: flex; flex-direction: column; align-items: center; gap: 8px; }
+ .avatar-item small { font-size: 12px; color: var(--muted); }
+</style>
+</head>
+<body>
+ <div class="doc-head">
+ <div class="doc-title">「我的资料」页面 · UI 高保真原型</div>
+ <div class="doc-sub">用户端小程序 · 375×812 · 本期交互为占位</div>
+ </div>
+
+ <!-- ==================== 手机原型 ==================== -->
+ <div class="phone">
+ <!-- 状态栏 -->
+ <div class="status-bar">
+ <span>12:00</span>
+ <span class="status-icons">
+ <!-- 信号 -->
+ <svg width="17" height="11" viewBox="0 0 17 11" fill="#0f172a">
+ <rect x="0" y="7" width="3" height="4" rx="1"></rect>
+ <rect x="4.5" y="5" width="3" height="6" rx="1"></rect>
+ <rect x="9" y="2.5" width="3" height="8.5" rx="1"></rect>
+ <rect x="13.5" y="0" width="3" height="11" rx="1"></rect>
+ </svg>
+ <!-- WiFi -->
+ <svg width="16" height="11" viewBox="0 0 16 11" fill="#0f172a">
+ <path d="M8 10.5 5.4 7.6a4 4 0 0 1 5.2 0L8 10.5Z"></path>
+ <path d="M8 5.6c-1.9 0-3.6.7-4.9 1.9L1.4 5.6A9.6 9.6 0 0 1 8 3.1c2.6 0 4.9 1 6.6 2.5l-1.7 1.9A7.1 7.1 0 0 0 8 5.6Z" opacity=".95"></path>
+ </svg>
+ <!-- 电池 -->
+ <svg width="25" height="12" viewBox="0 0 25 12" fill="none">
+ <rect x=".5" y=".5" width="21" height="11" rx="3" stroke="#0f172a" opacity=".4"></rect>
+ <rect x="2" y="2" width="18" height="8" rx="1.6" fill="#0f172a"></rect>
+ <rect x="22.5" y="4" width="2" height="4" rx="1" fill="#0f172a" opacity=".5"></rect>
+ </svg>
+ </span>
+ </div>
+
+ <!-- 导航栏 -->
+ <div class="nav">
+ <div class="nav-back">
+ <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="#1f2937" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
+ <path d="m15 18-6-6 6-6" />
+ </svg>
+ </div>
+ <div class="nav-title">我的资料</div>
+ <div class="nav-actions">
+ <!-- 更多 -->
+ <svg width="22" height="22" viewBox="0 0 24 24" fill="#334155">
+ <circle cx="5" cy="12" r="1.7"></circle>
+ <circle cx="12" cy="12" r="1.7"></circle>
+ <circle cx="19" cy="12" r="1.7"></circle>
+ </svg>
+ <!-- 帮助 -->
+ <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="#334155" stroke-width="2">
+ <circle cx="12" cy="12" r="9"></circle>
+ <path d="M9.4 9.2a2.6 2.6 0 1 1 3.3 2.5c-.8.3-1.3 1-1.3 1.9v.3" stroke-linecap="round"></path>
+ <circle cx="11.4" cy="17" r="1" fill="#334155" stroke="none"></circle>
+ </svg>
+ </div>
+ </div>
+
+ <!-- 内容 -->
+ <div class="body">
+ <div class="list">
+ <!-- 头像 -->
+ <div class="row">
+ <span class="row-label">头像</span>
+ <div class="row-right">
+ <div class="avatar">
+ <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="#94a3b8" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+ <path d="M14.5 4h-5L7 7H4a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2h-3l-2.5-3z"></path>
+ <circle cx="12" cy="13" r="3"></circle>
+ </svg>
+ </div>
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#cbd5e1" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+ <path d="m9 18 6-6-6-6" />
+ </svg>
+ </div>
+ </div>
+
+ <!-- 手机号(不可更换) -->
+ <div class="row">
+ <span class="row-label">手机号</span>
+ <div class="row-right">
+ <span class="row-value">136****6542</span>
+ </div>
+ </div>
+
+ <!-- 实名认证 -->
+ <div class="row">
+ <span class="row-label">实名认证</span>
+ <div class="row-right">
+ <span class="badge badge--none">未认证</span>
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#cbd5e1" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+ <path d="m9 18 6-6-6-6" />
+ </svg>
+ </div>
+ </div>
+
+ <!-- 企业 / 个体户认证 -->
+ <div class="row">
+ <span class="row-label">企业/个体户认证</span>
+ <div class="row-right">
+ <span class="badge badge--none">未认证</span>
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#cbd5e1" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+ <path d="m9 18 6-6-6-6" />
+ </svg>
+ </div>
+ </div>
+ </div>
+
+ <!-- 底部操作 -->
+ <div class="footer">
+ <div class="btn-logout">退出登录</div>
+ <span class="link-cancel">注销账号</span>
+ </div>
+ </div>
+
+ <!-- Home 指示条 -->
+ <div class="home-bar"><span></span></div>
+ </div>
+
+ <!-- ==================== 图例与设计说明 ==================== -->
+ <div class="legend">
+ <div class="card">
+ <h3>认证状态徽标(4 态)</h3>
+ <div class="badge-row">
+ <span class="badge badge--none">未认证</span>
+ <span class="badge badge--ing">认证中</span>
+ <span class="badge badge--fail">认证失败</span>
+ <span class="badge badge--done">已认证</span>
+ </div>
+ </div>
+
+ <div class="card">
+ <h3>头像两种状态</h3>
+ <div class="avatar-row">
+ <div class="avatar-item">
+ <div class="avatar">
+ <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="#94a3b8" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+ <path d="M14.5 4h-5L7 7H4a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2h-3l-2.5-3z"></path>
+ <circle cx="12" cy="13" r="3"></circle>
+ </svg>
+ </div>
+ <small>未设置(相机占位)</small>
+ </div>
+ <div class="avatar-item">
+ <div class="avatar">
+ <img src="https://coresg-normal.trae.ai/api/ide/v1/text_to_image?prompt=portrait%20photo%20of%20a%20smiling%20chinese%20young%20man%2C%20studio%20headshot%2C%20clean%20light%20background%2C%20high%20quality&image_size=square" alt="avatar" />
+ </div>
+ <small>已设置(圆形裁切)</small>
+ </div>
+ </div>
+ </div>
+
+ <div class="card">
+ <h3>原型标注</h3>
+ <ul class="legend-note">
+ <li><span class="no">1</span>认证状态共 <b>4</b> 种:未认证 / 认证中 / 认证失败 / 已认证,由后端返回状态枚举驱动徽标样式。</li>
+ <li><span class="no">2</span>后台按会员等级设置所需认证方式;完成<b>企业/个体户认证</b>后,页面将有限展示企业/个体户信息。</li>
+ <li><span class="no">3</span>手机号在小程序<b>暂不支持更换</b>,仅只读展示(已脱敏),故该行无箭头。</li>
+ </ul>
+ </div>
+ </div>
+</body>
+</html>
diff --git "a/doc/design/\347\224\250\346\210\267\347\253\257_\347\231\273\345\275\225\346\263\250\345\206\214_20261012.md" "b/doc/design/\347\224\250\346\210\267\347\253\257_\347\231\273\345\275\225\346\263\250\345\206\214_20261012.md"
index 872635e..0f68308 100644
--- "a/doc/design/\347\224\250\346\210\267\347\253\257_\347\231\273\345\275\225\346\263\250\345\206\214_20261012.md"
+++ "b/doc/design/\347\224\250\346\210\267\347\253\257_\347\231\273\345\275\225\346\263\250\345\206\214_20261012.md"
@@ -78,7 +78,7 @@
### 2.7 表结构缺口
-`account_user` 无推荐码、无推荐人字段 → 新增 `my_referral_code`、`referrer_user_id`。
+`account_user` 无推荐码字段 → 新增 `my_referral_code`;推荐关系复用既有 `referrer_id`(权威库已存在),不新增重复列。
### 2.8 既有代码缺陷
@@ -212,18 +212,18 @@
```sql
ALTER TABLE `account_user`
- ADD COLUMN `my_referral_code` varchar(20) DEFAULT NULL COMMENT '本人推荐码(注册时生成,全局唯一)' AFTER `remark`,
- ADD COLUMN `referrer_user_id` bigint(20) DEFAULT NULL COMMENT '推荐人会员ID(关联 account_user.user_id)' AFTER `my_referral_code`;
+ ADD COLUMN `my_referral_code` varchar(20) DEFAULT NULL COMMENT '本人推荐码(注册时生成,全局唯一)' AFTER `remark`;
-- 推荐码全局唯一:随机生成且不重复使用,逻辑删除行占用不影响业务
CREATE UNIQUE INDEX `uk_account_user_referral_code` ON `account_user` (`my_referral_code`);
+-- 推荐关系复用既有 referrer_id(权威库已存在),不新增 referrer_user_id
```
回滚:
```sql
DROP INDEX `uk_account_user_referral_code` ON `account_user`;
-ALTER TABLE `account_user` DROP COLUMN `referrer_user_id`, DROP COLUMN `my_referral_code`;
+ALTER TABLE `account_user` DROP COLUMN `my_referral_code`;
```
说明:
@@ -266,7 +266,7 @@
STEP 5: Feign 调 ruoyi-user 注册:
5.1 按推荐码查推荐人,不存在抛「推荐人码无效」;推荐人 status=1 抛「推荐人账号异常」
5.2 生成唯一 my_referral_code(随机 8 位大写字母数字,库内查重,冲突重试 ≤5 次)
- 5.3 组装 AccountUser:userName=phone、phone、nickname=phone、myReferralCode、referrerUserId、status=0、delFlag=0
+ 5.3 组装 AccountUser:userName=phone、phone、nickname=phone、myReferralCode、referrerId、status=0、delFlag=0
5.4 插入(无密码,password 为空)
STEP 6: 删除验证码 key;finally 释放锁
```
@@ -311,8 +311,9 @@
通用推荐码(ruoyi-user)
AccountUserServiceImpl.registerMember 定位推荐人前先判定:
- 命中「配置的通用推荐码」→ 跳过推荐人查询与校验,referrerUserId 留空(不绑定推荐人)
- 否则走原有「按码查推荐人 → 不存在/停用则拒绝」逻辑
+ 命中「配置的通用推荐码」→ 跳过推荐人查询与校验,referrerId 留空(不绑定推荐人);
+ 同时直接发放 L4 会员等级(system_level = L4)
+ 否则走原有「按码查推荐人 → 不存在/停用则拒绝」逻辑,且不设等级(走库默认值 C)
```
命中通用码时打印 WARN 日志留痕(手机号脱敏)。
@@ -320,7 +321,7 @@
| 服务 | 配置项 | 示例值 | 说明 |
| --- | --- | --- | --- |
| ruoyi-auth | `hugray.auth.universal-sms-code` | `999999` | 空 = 关闭;仅 `login` / `register` 场景生效 |
-| ruoyi-user | `hugray.user.universal-referral-code` | `HUGRAY88` | 空 = 关闭;命中则不绑定推荐人 |
+| ruoyi-user | `hugray.user.universal-referral-code` | `HUGRAY88` | 空 = 关闭;命中则不绑定推荐人,且注册用户直接为 L4 等级 |
---
@@ -365,7 +366,7 @@
| 文件 | 改动 |
| --- | --- |
-| ruoyi-user `domain/AccountUser.java` | 新增 `myReferralCode`、`referrerUserId` 字段与注释、toString |
+| ruoyi-user `domain/AccountUser.java` | 新增 `myReferralCode` 字段与注释、toString;推荐关系复用既有 `referrerId`(源 `referrer_id`) |
| ruoyi-user `mapper/AccountUserMapper.java` + `resources/mapper/user/AccountUserMapper.xml` | resultMap/列/insert/update 增列;新增 `selectAccountUserByReferralCode` |
| ruoyi-user `service/impl/AccountUserServiceImpl.insertAccountUser` | 密码为空时跳过加密(当前无条件 `encryptPassword`,C 端无密码会 NPE) |
| `sql/hugray_account_20261012.sql` | 新增 DDL 脚本 |
@@ -497,7 +498,7 @@
| 20 | 验证码登录:手机号未注册 | 下发(scene=login)即拦「该手机号未注册,请先注册」;登录接口再兜底,均**不消费验证码** |
| 21 | 验证码登录:账号停用 / 删除 | 「账号已停用,请联系管理员」/「账号不存在」,校验在消费验证码之前 |
| 22 | 通用验证码命中(`login` / `register` 且 `hugray.auth.universal-sms-code` 已配置) | 直接放行:不读缓存、不计数、不消费;`reset` / `common` 场景不生效;配置为空即关闭 |
-| 23 | 通用推荐码命中(`hugray.user.universal-referral-code` 已配置且有值) | 跳过推荐人查询与校验,`referrerUserId` 留空(不绑定推荐人);命中通用码时不再校验「推荐人码无效 / 推荐人账号异常」 |
+| 23 | 通用推荐码命中(`hugray.user.universal-referral-code` 已配置且有值) | 跳过推荐人查询与校验,`referrerId` 留空(不绑定推荐人),且注册用户等级直接为 `L4`;命中通用码时不再校验「推荐人码无效 / 推荐人账号异常」 |
---
@@ -526,7 +527,7 @@
6. 登录页双按钮(微信一键登录 / 手机验证登录),「手机验证登录」为页内嵌面板;后端 `POST /miniapp/auth/phone/code-login` 与 `SmsSceneEnum.LOGIN` 同步落地。✔
7. 登录页按 `.pen` 设计稿 + `static/` 素材做视觉 1:1 还原(背景装饰、Logo、背书图标、标语卡、按钮尺寸与渐变、协议行、Home 条),规格见 3.5 节。✔
8. 登录页 UI 缺陷修复:微信一键登录恢复通栏;协议行字号 23rpx → 26rpx;展开验证码面板后隐藏「手机验证登录」按钮并在面板内提供「收起」入口。✔
-9. 测试便利:通用验证码 `999999`(`hugray.auth.universal-sms-code`,仅 `login` / `register` 生效)+ 通用推荐码 `HUGRAY88`(`hugray.user.universal-referral-code`,命中不绑定推荐人);均通过 nacos 配置开启,配置为空即关闭,命中打印 WARN 留痕。✔
+9. 测试便利:通用验证码 `999999`(`hugray.auth.universal-sms-code`,仅 `login` / `register` 生效)+ 通用推荐码 `HUGRAY88`(`hugray.user.universal-referral-code`,命中不绑定推荐人且注册用户直接发放 `L4` 等级);均通过 nacos 配置开启,配置为空即关闭,命中打印 WARN 留痕。✔
10. 注册流程:注册页**无需阅读/勾选协议**(提交仅校验手机号 / 验证码 / 推荐码);注册成功页与登录/注册页视觉统一(渐变底 + 装饰层 + 居中结果区 + 渐变胶囊「返回登录」按钮 + Home 条),规格见 3.7 节。✔
> 编码顺序:常量 → DDL → Feign/内部接口 → auth 服务与控制器 → 网关配置 → 前端前缀对齐 → `GetDiagnostics` 校验 → 单测。
diff --git "a/doc/design/\347\224\250\346\210\267\347\253\257_\351\246\226\351\241\265\344\270\216\345\257\274\350\210\252_20261012.md" "b/doc/design/\347\224\250\346\210\267\347\253\257_\351\246\226\351\241\265\344\270\216\345\257\274\350\210\252_20261012.md"
index 5e095d0..9827380 100644
--- "a/doc/design/\347\224\250\346\210\267\347\253\257_\351\246\226\351\241\265\344\270\216\345\257\274\350\210\252_20261012.md"
+++ "b/doc/design/\347\224\250\346\210\267\347\253\257_\351\246\226\351\241\265\344\270\216\345\257\274\350\210\252_20261012.md"
@@ -231,7 +231,7 @@
| 3 | 个人中心角标数量(未读)由后端下发 | 待接口(当前默认 1) |
| 4 | 缺货订单列表页真实内容 | 占位 |
| 5 | 「探索更多」产品详情页 | 占位(toast 提示) |
-| 6 | 三页真实数据接口(订单列表 / 收益数值 / 会员等级与昵称) | 待接口(当前 mock) |
+| 6 | 真实数据接口:个人中心「昵称 / 头像 / 等级 / 收益」已由 `getMemberInfo` 下发;订单列表仍待 `getOrderList` | 部分完成 |
| 7 | 三页图片素材(方案卡、订单缩略图、用户头像)入仓后替换渐变占位 | 待素材 |
@@ -273,16 +273,16 @@
| 页面底色 | 175° 渐变 `#D6E6F8` 0 → `#C4DCF6` .18 → `#E8F1FC` .38 → `#F8FAFC` .58 → `#F8FAFC` 1 | `.page` |
| 头部信息区 | x20 335×72,居中,头像与文本间隙 14 | `.header` |
| 头像 | 70×70 R35 fill `#F1F5F9` + 2.5 白描边 | 已登录内嵌 `camera` 32×32 `#94A3B8`;未登录内嵌 `user` 34×34 `#CBD5E1` |
-| 文本信息 | 昵称 18pt 700 `#0F172A` | 已登录「辉煌G891」+ 等级胶囊(`userLevel` 动态展示 L0~L4);未登录「点击登录 / 注册」+ 副标 12pt `#64748B` |
+| 文本信息 | 昵称 18pt 700 `#0F172A` | 已登录「辉煌G891」+ 等级胶囊(`userLevel` 动态展示 C/L1~L4);未登录「点击登录 / 注册」+ 副标 12pt `#64748B` |
| 会员等级胶囊 | h22 渐变 90° `#FDE68A→#FCD34D→#F59E0B`,V 勋章 18×18 渐变 145° | `.level-pill` / `.level-medal`;等级文字 12.5pt 800 `#78350F` |
| 主按钮 | h32 渐变 90° `#1E5EBC→#003E7E` R16 | 已登录「编辑资料」/ 未登录「立即登录」 |
-| 收益玻璃卡 | x16 343×74,180° 半透明白渐变 + blur20,R16 | 三栏等分:**今日 10.00 / 本月 400.00 / 累计 128000.00**;未登录统一 `0.00`;**未登录或 L0 普通用户不展示该卡** |
-| 功能菜单卡 | x16 343×408,同款玻璃态,6 行 × 68(未登录 / L0 仅 2 行) | 行图标 20×20 渐变描边 `#3B82F6→#1D4ED8`,右箭头 16×16 `#94A3B8` |
-| 菜单项与等级 | — | L1~L4 且已登录时展示 6 项:`share-2` 我的邀请码 / `crown` 会员中心 / `file-text` 仓库订单 / `badge-cent` 收入明细 / `map-pin` 收货地址 / `contact` 联系我们;**未登录或 L0 普通用户仅展示「收货地址」「联系我们」** |
+| 收益玻璃卡 | x16 343×74,180° 半透明白渐变 + blur20,R16 | 三栏等分:**今日 10.00 / 本月 400.00 / 累计 128000.00**;未登录统一 `0.00`;**未登录或 C 普通用户不展示该卡** |
+| 功能菜单卡 | x16 343×408,同款玻璃态,6 行 × 68(未登录 / C 仅 2 行) | 行图标 20×20 渐变描边 `#3B82F6→#1D4ED8`,右箭头 16×16 `#94A3B8` |
+| 菜单项与等级 | — | L1~L4 且已登录时展示 6 项:`share-2` 我的邀请码 / `crown` 会员中心 / `file-text` 仓库订单 / `badge-cent` 收入明细 / `map-pin` 收货地址 / `contact` 联系我们;**未登录或 C 普通用户仅展示「收货地址」「联系我们」** |
| 仓库订单角标 | 18×18 `#EF4444` R9 文字 1 | **仅已登录态**展示 |
| ICP 区 | x16 343×40 居中 | `京ICP备2026092800号-1X` + 版权行;**内容不足一屏时贴页面底部**(`.page` 为纵向 flex,`.icp` 用 `margin-top: auto` 吸收剩余空间) |
-> **用户等级与展示规则(L0~L4)**:由 `userLevel` 控制,`isRestrictedView = !isLoggedIn || userLevel === 'L0'`。**未登录或 L0 普通用户**在个人中心仅保留「收货地址」「联系我们」两行菜单,并隐藏收益数据卡;**已登录且 L1~L4** 展示全部内容。等级接口未联调(当前 mock `L4`),联调后由 `getUserInfo` 下发等级替换。
+> **用户等级与展示规则(C/L1~L4)**:由 `userLevel` 控制,`isRestrictedView = !isLoggedIn || userLevel === 'C'`。**未登录或 C 普通用户**在个人中心仅保留「收货地址」「联系我们」两行菜单,并隐藏收益数据卡;**已登录且 L1~L4** 展示全部内容。昵称、头像、等级、收益均已由 `getMemberInfo`(`GET /miniapp/account/user/info`)下发;会员等级空或非法时后端兜底 `C`。
### 9.4 新增内联 base64 图标
@@ -300,7 +300,7 @@
- 三页模板均引入 `<stock-order-toast />`,保证缺货提示全局可见。
- 首页 / 健康方案 / 我的订单 / 个人中心:**均不校验登录态**,未登录可正常浏览(原 `ensureLogin()` 守卫已移除,仅缺货订单列表页保留)。
- 个人中心:`onShow` 读 `getToken()` 仅用于切换「已登录 / 未登录」展示态,不做跳转;未登录时点击头像 / 名称 / 主按钮直接 `uni.reLaunch` 至登录页,**点击菜单项先 `uni.showModal` 提示登录、确认后跳转**。
-- 订单列表、收益数值、用户昵称、用户等级均为本地 mock,待 `getOrderList` / `getMemberInfo` / `getUserInfo` 就绪后替换。
+- 订单列表、仓库订单角标仍为本地 mock,待 `getOrderList` 就绪后替换;收益数值、用户昵称、头像、用户等级已由 `getMemberInfo`(`GET /miniapp/account/user/info`,金额单位分,前端 `formatYuan` 格式化)替换。
- 「探索更多 / 二级筛选 / 去支付 / 查看订单 / 编辑资料」本轮统一为 toast 占位(菜单项仅在已登录态为占位)。
### 9.6 联系我们页(`pages/contact/index.vue`)
diff --git a/doc/nacos/dev/ruoyi-user-dev.yml b/doc/nacos/dev/ruoyi-user-dev.yml
index 5bc5a9a..9771117 100644
--- a/doc/nacos/dev/ruoyi-user-dev.yml
+++ b/doc/nacos/dev/ruoyi-user-dev.yml
@@ -33,7 +33,7 @@
# 主库数据源
master:
driver-class-name: com.mysql.cj.jdbc.Driver
- url: jdbc:mysql://localhost:3306/hugray-account?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
+ url: jdbc:mysql://192.168.110.111:3306/hugray-account?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
username: root
password: <脱敏归档,真实值见配置中心>
# 从库数据源
diff --git a/hugray-server/ruoyi-api/ruoyi-api-system/src/main/java/com/ruoyi/system/api/domain/AccountMember.java b/hugray-server/ruoyi-api/ruoyi-api-system/src/main/java/com/ruoyi/system/api/domain/AccountMember.java
index 5b94d84..75cfd80 100644
--- a/hugray-server/ruoyi-api/ruoyi-api-system/src/main/java/com/ruoyi/system/api/domain/AccountMember.java
+++ b/hugray-server/ruoyi-api/ruoyi-api-system/src/main/java/com/ruoyi/system/api/domain/AccountMember.java
@@ -35,7 +35,7 @@
private String myReferralCode;
/** 推荐人会员ID(关联 account_user.user_id) */
- private Long referrerUserId;
+ private Long referrerId;
/** 状态(0正常 1停用) */
private String status;
@@ -100,14 +100,14 @@
this.myReferralCode = myReferralCode;
}
- public Long getReferrerUserId()
+ public Long getReferrerId()
{
- return referrerUserId;
+ return referrerId;
}
- public void setReferrerUserId(Long referrerUserId)
+ public void setReferrerId(Long referrerId)
{
- this.referrerUserId = referrerUserId;
+ this.referrerId = referrerId;
}
public String getStatus()
@@ -130,7 +130,7 @@
.append("nickname", getNickname())
.append("avatar", getAvatar())
.append("myReferralCode", getMyReferralCode())
- .append("referrerUserId", getReferrerUserId())
+ .append("referrerId", getReferrerId())
.append("status", getStatus())
.toString();
}
diff --git a/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/controller/AccountMemberInnerController.java b/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/controller/AccountMemberInnerController.java
index 5917d58..080e3be 100644
--- a/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/controller/AccountMemberInnerController.java
+++ b/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/controller/AccountMemberInnerController.java
@@ -75,7 +75,7 @@
member.setNickname(accountUser.getNickname());
member.setAvatar(accountUser.getAvatar());
member.setMyReferralCode(accountUser.getMyReferralCode());
- member.setReferrerUserId(accountUser.getReferrerUserId());
+ member.setReferrerId(accountUser.getReferrerId());
member.setStatus(accountUser.getStatus());
return member;
}
diff --git a/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/controller/AccountProfileController.java b/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/controller/AccountProfileController.java
new file mode 100644
index 0000000..c2f4ea8
--- /dev/null
+++ b/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/controller/AccountProfileController.java
@@ -0,0 +1,42 @@
+package com.ruoyi.user.controller;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import com.ruoyi.common.core.domain.R;
+import com.ruoyi.common.security.utils.SecurityUtils;
+import com.ruoyi.user.domain.vo.AccountUserInfoVo;
+import com.ruoyi.user.service.IAccountUserService;
+
+/**
+ * C端会员 个人中心接口
+ *
+ * 网关前缀 /miniapp/account 由 StripPrefix=2 剥掉,故此处映射为 /user,
+ * 外部实际路径为 /miniapp/account/user/**。
+ *
+ * 会员身份取自网关注入的登录态(SecurityUtils.getUserId),不接受前端传 userId,
+ * 天然防止水平越权;未登录请求由网关直接以 401 拦截,故此处不处理未登录分支。
+ *
+ * @author hugray
+ */
+@RestController
+@RequestMapping("/user")
+public class AccountProfileController
+{
+ @Autowired
+ private IAccountUserService accountUserService;
+
+ /**
+ * 查询当前登录会员的个人中心信息
+ *
+ * 返回昵称、头像、会员等级与收益汇总(今日 / 本月 / 累计,单位分)。
+ *
+ * @return 会员信息展示对象
+ */
+ @GetMapping("/info")
+ public R<AccountUserInfoVo> info()
+ {
+ return R.ok(accountUserService.getAccountUserInfo(SecurityUtils.getUserId()));
+ }
+}
diff --git a/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/domain/AccountEarningRecord.java b/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/domain/AccountEarningRecord.java
new file mode 100644
index 0000000..6dc1584
--- /dev/null
+++ b/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/domain/AccountEarningRecord.java
@@ -0,0 +1,121 @@
+package com.ruoyi.user.domain;
+
+import java.util.Date;
+import com.fasterxml.jackson.annotation.JsonFormat;
+
+/**
+ * 会员收益流水 account_earning_record
+ *
+ * 一条记录代表一次收益事件;个人中心「今日 / 本月 / 累计收益」由本表按时间实时聚合得出,
+ * 无需在 account_user 上维护需要定时归零的汇总字段。
+ *
+ * @author hugray
+ */
+public class AccountEarningRecord
+{
+ /** 收益记录ID */
+ private Long recordId;
+
+ /** 会员ID(关联 account_user.user_id) */
+ private Long userId;
+
+ /** 收益金额(单位:分,可正可负,负数表示扣减) */
+ private Long amount;
+
+ /** 收益来源(1订单收益 2邀请奖励 3等级差价 9其他,见 EarningSourceEnum) */
+ private String source;
+
+ /** 业务单号(同来源幂等去重,可为空) */
+ private String bizNo;
+
+ /** 备注 */
+ private String remark;
+
+ /** 发生时间 */
+ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+ private Date createTime;
+
+ /** 更新时间 */
+ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+ private Date updateTime;
+
+ public Long getRecordId()
+ {
+ return recordId;
+ }
+
+ public void setRecordId(Long recordId)
+ {
+ this.recordId = recordId;
+ }
+
+ public Long getUserId()
+ {
+ return userId;
+ }
+
+ public void setUserId(Long userId)
+ {
+ this.userId = userId;
+ }
+
+ public Long getAmount()
+ {
+ return amount;
+ }
+
+ public void setAmount(Long amount)
+ {
+ this.amount = amount;
+ }
+
+ public String getSource()
+ {
+ return source;
+ }
+
+ public void setSource(String source)
+ {
+ this.source = source;
+ }
+
+ public String getBizNo()
+ {
+ return bizNo;
+ }
+
+ public void setBizNo(String bizNo)
+ {
+ this.bizNo = bizNo;
+ }
+
+ public String getRemark()
+ {
+ return remark;
+ }
+
+ public void setRemark(String remark)
+ {
+ this.remark = remark;
+ }
+
+ public Date getCreateTime()
+ {
+ return createTime;
+ }
+
+ public void setCreateTime(Date createTime)
+ {
+ this.createTime = createTime;
+ }
+
+ public Date getUpdateTime()
+ {
+ return updateTime;
+ }
+
+ public void setUpdateTime(Date updateTime)
+ {
+ this.updateTime = updateTime;
+ }
+}
diff --git a/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/domain/AccountUser.java b/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/domain/AccountUser.java
index 4bff50b..f1d8edd 100644
--- a/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/domain/AccountUser.java
+++ b/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/domain/AccountUser.java
@@ -70,7 +70,11 @@
private String myReferralCode;
/** 推荐人会员ID(关联 account_user.user_id) */
- private Long referrerUserId;
+ private Long referrerId;
+
+ /** 系统等级(C普通用户 L1~L4会员,对应既有列 account_user.system_level,见 MemberLevelEnum) */
+ @Excel(name = "系统等级")
+ private String systemLevel;
public Long getUserId()
{
@@ -187,14 +191,24 @@
this.myReferralCode = myReferralCode;
}
- public Long getReferrerUserId()
+ public Long getReferrerId()
{
- return referrerUserId;
+ return referrerId;
}
- public void setReferrerUserId(Long referrerUserId)
+ public void setReferrerId(Long referrerId)
{
- this.referrerUserId = referrerUserId;
+ this.referrerId = referrerId;
+ }
+
+ public String getSystemLevel()
+ {
+ return systemLevel;
+ }
+
+ public void setSystemLevel(String systemLevel)
+ {
+ this.systemLevel = systemLevel;
}
@Override
@@ -210,7 +224,8 @@
.append("status", getStatus())
.append("delFlag", getDelFlag())
.append("myReferralCode", getMyReferralCode())
- .append("referrerUserId", getReferrerUserId())
+ .append("referrerId", getReferrerId())
+ .append("systemLevel", getSystemLevel())
.append("createBy", getCreateBy())
.append("createTime", getCreateTime())
.append("updateBy", getUpdateBy())
diff --git a/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/domain/vo/AccountUserInfoVo.java b/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/domain/vo/AccountUserInfoVo.java
new file mode 100644
index 0000000..25bd635
--- /dev/null
+++ b/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/domain/vo/AccountUserInfoVo.java
@@ -0,0 +1,90 @@
+package com.ruoyi.user.domain.vo;
+
+/**
+ * C端会员信息 VO(个人中心页展示用)
+ *
+ * 仅暴露页面所需字段;手机号仅下发脱敏值(如 133****5555),不下发明文;
+ * 会员身份由服务端从登录态解析,不接受前端传入。
+ *
+ * @author hugray
+ */
+public class AccountUserInfoVo
+{
+ /** 会员ID */
+ private Long userId;
+
+ /** 姓名/昵称(注册未采集,可为空;为空时前端回退展示脱敏手机号) */
+ private String nickname;
+
+ /** 手机号(脱敏,形如 133****5555;仅供展示,不下发明文) */
+ private String phone;
+
+ /** 头像地址(可为空,为空时前端回退默认图标) */
+ private String avatar;
+
+ /** 会员等级(对外展示字段,取值 C普通用户 / L1~L4会员,源自 account_user.system_level) */
+ private String memberLevel;
+
+ /** 收益汇总(今日 / 本月 / 累计,单位:分) */
+ private EarningSummaryVo earnings;
+
+ public Long getUserId()
+ {
+ return userId;
+ }
+
+ public void setUserId(Long userId)
+ {
+ this.userId = userId;
+ }
+
+ public String getNickname()
+ {
+ return nickname;
+ }
+
+ public void setNickname(String nickname)
+ {
+ this.nickname = nickname;
+ }
+
+ public String getPhone()
+ {
+ return phone;
+ }
+
+ public void setPhone(String phone)
+ {
+ this.phone = phone;
+ }
+
+ public String getAvatar()
+ {
+ return avatar;
+ }
+
+ public void setAvatar(String avatar)
+ {
+ this.avatar = avatar;
+ }
+
+ public String getMemberLevel()
+ {
+ return memberLevel;
+ }
+
+ public void setMemberLevel(String memberLevel)
+ {
+ this.memberLevel = memberLevel;
+ }
+
+ public EarningSummaryVo getEarnings()
+ {
+ return earnings;
+ }
+
+ public void setEarnings(EarningSummaryVo earnings)
+ {
+ this.earnings = earnings;
+ }
+}
diff --git a/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/domain/vo/EarningSummaryVo.java b/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/domain/vo/EarningSummaryVo.java
new file mode 100644
index 0000000..82b463d
--- /dev/null
+++ b/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/domain/vo/EarningSummaryVo.java
@@ -0,0 +1,72 @@
+package com.ruoyi.user.domain.vo;
+
+/**
+ * 会员收益汇总 VO
+ *
+ * 金额单位统一为「分」,由前端负责格式化为元展示;
+ * 无收益流水时三栏均为 0,避免前端出现 null 判断分支。
+ *
+ * @author hugray
+ */
+public class EarningSummaryVo
+{
+ /** 今日收益(单位:分) */
+ private Long today;
+
+ /** 本月收益(单位:分) */
+ private Long month;
+
+ /** 累计收益(单位:分) */
+ private Long total;
+
+ public EarningSummaryVo()
+ {
+ }
+
+ public EarningSummaryVo(Long today, Long month, Long total)
+ {
+ this.today = today;
+ this.month = month;
+ this.total = total;
+ }
+
+ /**
+ * 全零汇总:用于会员无任何收益流水的场景
+ *
+ * @return 三栏均为 0 的汇总对象
+ */
+ public static EarningSummaryVo zero()
+ {
+ return new EarningSummaryVo(0L, 0L, 0L);
+ }
+
+ public Long getToday()
+ {
+ return today;
+ }
+
+ public void setToday(Long today)
+ {
+ this.today = today;
+ }
+
+ public Long getMonth()
+ {
+ return month;
+ }
+
+ public void setMonth(Long month)
+ {
+ this.month = month;
+ }
+
+ public Long getTotal()
+ {
+ return total;
+ }
+
+ public void setTotal(Long total)
+ {
+ this.total = total;
+ }
+}
diff --git a/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/enums/EarningSourceEnum.java b/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/enums/EarningSourceEnum.java
new file mode 100644
index 0000000..0f425ec
--- /dev/null
+++ b/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/enums/EarningSourceEnum.java
@@ -0,0 +1,61 @@
+package com.ruoyi.user.enums;
+
+/**
+ * 收益来源枚举
+ *
+ * 与 account_earning_record.source 字段一一对应;本期仅定义取值,
+ * 实际写入方(订单收益 / 邀请奖励 / 等级差价)待对应业务模块接入后补齐。
+ *
+ * @author hugray
+ */
+public enum EarningSourceEnum
+{
+ /** 订单收益 */
+ ORDER("1", "订单收益"),
+ /** 邀请奖励 */
+ INVITE("2", "邀请奖励"),
+ /** 等级差价 */
+ LEVEL_DIFF("3", "等级差价"),
+ /** 其他 */
+ OTHER("9", "其他");
+
+ /** 来源编码(落库值) */
+ private final String code;
+
+ /** 来源描述 */
+ private final String desc;
+
+ EarningSourceEnum(String code, String desc)
+ {
+ this.code = code;
+ this.desc = desc;
+ }
+
+ public String getCode()
+ {
+ return code;
+ }
+
+ public String getDesc()
+ {
+ return desc;
+ }
+
+ /**
+ * 按编码获取枚举
+ *
+ * @param code 来源编码
+ * @return 命中返回枚举;未命中返回 null
+ */
+ public static EarningSourceEnum getByCode(String code)
+ {
+ for (EarningSourceEnum source : values())
+ {
+ if (source.code.equals(code))
+ {
+ return source;
+ }
+ }
+ return null;
+ }
+}
diff --git a/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/enums/MemberLevelEnum.java b/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/enums/MemberLevelEnum.java
new file mode 100644
index 0000000..092f3cd
--- /dev/null
+++ b/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/enums/MemberLevelEnum.java
@@ -0,0 +1,78 @@
+package com.ruoyi.user.enums;
+
+/**
+ * 会员等级枚举
+ *
+ * 与 account_user.system_level 字段一一对应(复用既有列,不再另建等级列):
+ * C —— 普通用户(个人中心受限展示:仅「收货地址」「联系我们」)
+ * L1~L4 —— 会员(个人中心展示全部内容,含收益卡与全部菜单)
+ *
+ * @author hugray
+ */
+public enum MemberLevelEnum
+{
+ /** 普通用户(受限展示) */
+ C("C", "普通用户"),
+ /** 会员 L1 */
+ L1("L1", "会员"),
+ /** 会员 L2 */
+ L2("L2", "会员"),
+ /** 会员 L3 */
+ L3("L3", "会员"),
+ /** 会员 L4 */
+ L4("L4", "会员");
+
+ /** 等级编码(落库值) */
+ private final String code;
+
+ /** 等级描述 */
+ private final String desc;
+
+ /** 默认等级:普通用户,用于等级为空或非法的兜底 */
+ public static final MemberLevelEnum DEFAULT = C;
+
+ MemberLevelEnum(String code, String desc)
+ {
+ this.code = code;
+ this.desc = desc;
+ }
+
+ public String getCode()
+ {
+ return code;
+ }
+
+ public String getDesc()
+ {
+ return desc;
+ }
+
+ /**
+ * 按编码获取枚举
+ *
+ * @param code 等级编码
+ * @return 命中返回枚举;未命中返回 null
+ */
+ public static MemberLevelEnum getByCode(String code)
+ {
+ for (MemberLevelEnum level : values())
+ {
+ if (level.code.equals(code))
+ {
+ return level;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * 归一化等级编码:非法或空值统一兜底为默认等级,避免脏数据影响前端展示
+ *
+ * @param code 原始等级编码
+ * @return 合法的等级编码
+ */
+ public static String normalize(String code)
+ {
+ return getByCode(code) == null ? DEFAULT.getCode() : code;
+ }
+}
diff --git a/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/mapper/AccountEarningRecordMapper.java b/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/mapper/AccountEarningRecordMapper.java
new file mode 100644
index 0000000..096793f
--- /dev/null
+++ b/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/mapper/AccountEarningRecordMapper.java
@@ -0,0 +1,22 @@
+package com.ruoyi.user.mapper;
+
+import org.apache.ibatis.annotations.Param;
+import com.ruoyi.user.domain.vo.EarningSummaryVo;
+
+/**
+ * 会员收益流水 数据层
+ *
+ * @author hugray
+ */
+public interface AccountEarningRecordMapper
+{
+ /**
+ * 汇总指定会员的收益(今日 / 本月 / 累计)
+ *
+ * 单条 SQL 聚合三栏,命中 idx_earning_user_time 索引;无流水时三栏均为 0。
+ *
+ * @param userId 会员ID
+ * @return 收益汇总(单位:分)
+ */
+ public EarningSummaryVo selectEarningSummary(@Param("userId") Long userId);
+}
diff --git a/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/service/IAccountUserService.java b/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/service/IAccountUserService.java
index faf33e1..a27cd26 100644
--- a/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/service/IAccountUserService.java
+++ b/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/service/IAccountUserService.java
@@ -2,6 +2,7 @@
import java.util.List;
import com.ruoyi.user.domain.AccountUser;
+import com.ruoyi.user.domain.vo.AccountUserInfoVo;
/**
* 会员用户 业务层
@@ -62,4 +63,12 @@
* @return 注册成功后的会员信息(含生成的 userId 与 myReferralCode)
*/
public AccountUser registerMember(String phone, String referralCode);
+
+ /**
+ * 查询C端会员个人中心信息(昵称 / 头像 / 会员等级 / 收益汇总)
+ *
+ * @param userId 会员ID(取自登录态,非前端传入)
+ * @return 会员信息展示对象;会员不存在时抛业务异常
+ */
+ public AccountUserInfoVo getAccountUserInfo(Long userId);
}
diff --git a/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/service/impl/AccountUserServiceImpl.java b/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/service/impl/AccountUserServiceImpl.java
index ebd2a43..98a8af8 100644
--- a/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/service/impl/AccountUserServiceImpl.java
+++ b/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/service/impl/AccountUserServiceImpl.java
@@ -13,6 +13,10 @@
import com.ruoyi.user.config.UniversalCodeProperties;
import com.ruoyi.user.constant.ReferralConstants;
import com.ruoyi.user.domain.AccountUser;
+import com.ruoyi.user.domain.vo.AccountUserInfoVo;
+import com.ruoyi.user.domain.vo.EarningSummaryVo;
+import com.ruoyi.user.enums.MemberLevelEnum;
+import com.ruoyi.user.mapper.AccountEarningRecordMapper;
import com.ruoyi.user.mapper.AccountUserMapper;
import com.ruoyi.user.service.IAccountUserService;
@@ -28,6 +32,9 @@
@Autowired
private AccountUserMapper accountUserMapper;
+
+ @Autowired
+ private AccountEarningRecordMapper accountEarningRecordMapper;
@Autowired
private UniversalCodeProperties universalCodeProperties;
@@ -118,24 +125,74 @@
{
throw new ServiceException("手机号已注册,请前往登录");
}
- // STEP 2: 解析推荐人——命中通用推荐码则不绑定推荐人,否则按码定位并校验
- Long referrerUserId = resolveReferrerUserId(phone, referralCode);
- // STEP 3: 生成全局唯一的本人推荐码(库内查重,冲突重试)
+ // STEP 2: 判定是否命中通用推荐码(命中则不绑定推荐人,且直接发放 L4 会员等级)
+ boolean universalReferral = isUniversalReferralCode(referralCode);
+ // STEP 3: 解析推荐人——命中通用推荐码则不绑定推荐人,否则按码定位并校验
+ Long referrerUserId = resolveReferrerUserId(phone, referralCode, universalReferral);
+ // STEP 4: 生成全局唯一的本人推荐码(库内查重,冲突重试)
String myReferralCode = generateUniqueReferralCode();
- // STEP 4: 组装会员信息(C端无密码,账号=手机号),落库(userId 回填)
+ // STEP 5: 组装会员信息(C端无密码,账号=手机号;姓名注册未采集,落库留空,由用户后续在资料页完善)
AccountUser accountUser = new AccountUser();
accountUser.setUserName(phone);
accountUser.setPhone(phone);
- accountUser.setNickname(phone);
accountUser.setStatus(UserConstants.NORMAL);
accountUser.setMyReferralCode(myReferralCode);
- accountUser.setReferrerUserId(referrerUserId);
+ accountUser.setReferrerId(referrerUserId);
+ // 通用推荐码注册的会员直接发放 L4 等级;其余不设等级,走库默认值 C(普通用户)
+ if (universalReferral)
+ {
+ accountUser.setSystemLevel(MemberLevelEnum.L4.getCode());
+ }
accountUser.setCreateBy(phone);
if (insertAccountUser(accountUser) <= 0)
{
throw new ServiceException("注册失败,请稍后重试");
}
return accountUser;
+ }
+
+ @Override
+ public AccountUserInfoVo getAccountUserInfo(Long userId)
+ {
+ // STEP 1: 校验会员ID(登录态缺失时直接失败,避免无归属查询)
+ if (StringUtils.isNull(userId))
+ {
+ throw new ServiceException("获取会员信息失败");
+ }
+ // STEP 2: 按会员ID查询会员(逻辑删除已在 SQL 中过滤)
+ AccountUser accountUser = accountUserMapper.selectAccountUserById(userId);
+ if (StringUtils.isNull(accountUser))
+ {
+ throw new ServiceException("获取会员信息失败");
+ }
+ // STEP 3: 查询收益汇总(单位分),无任何流水时直接返回全零,避免前端 null 分支
+ EarningSummaryVo earnings = accountEarningRecordMapper.selectEarningSummary(userId);
+ if (StringUtils.isNull(earnings))
+ {
+ earnings = EarningSummaryVo.zero();
+ }
+ // STEP 4: 组装展示对象;等级为空或非法值统一兜底为 C(普通用户);手机号脱敏后下发
+ AccountUserInfoVo vo = new AccountUserInfoVo();
+ vo.setUserId(accountUser.getUserId());
+ vo.setNickname(accountUser.getNickname());
+ vo.setPhone(maskPhone(accountUser.getPhone()));
+ vo.setAvatar(accountUser.getAvatar());
+ vo.setMemberLevel(MemberLevelEnum.normalize(accountUser.getSystemLevel()));
+ vo.setEarnings(earnings);
+ // STEP 5: 返回个人中心展示数据
+ return vo;
+ }
+
+ /**
+ * 判定是否为通用推荐码
+ *
+ * @param referralCode 注册提交的推荐码
+ * @return true 表示命中通用推荐码
+ */
+ private boolean isUniversalReferralCode(String referralCode)
+ {
+ String universalCode = universalCodeProperties.getUniversalReferralCode();
+ return StringUtils.isNotEmpty(universalCode) && universalCode.equals(referralCode);
}
/**
@@ -146,13 +203,13 @@
*
* @param phone 注册手机号(仅用于日志留痕,脱敏输出)
* @param referralCode 推荐码
+ * @param universalReferral 是否已命中通用推荐码
* @return 推荐人用户ID;命中通用码时为 null
*/
- private Long resolveReferrerUserId(String phone, String referralCode)
+ private Long resolveReferrerUserId(String phone, String referralCode, boolean universalReferral)
{
- String universalCode = universalCodeProperties.getUniversalReferralCode();
// 命中通用推荐码:不绑定推荐人
- if (StringUtils.isNotEmpty(universalCode) && universalCode.equals(referralCode))
+ if (universalReferral)
{
log.warn("[REFERRAL] universal code hit, not binding referrer, phone={}", maskPhone(phone));
return null;
diff --git a/hugray-server/ruoyi-modules/ruoyi-user/src/main/resources/mapper/user/AccountEarningRecordMapper.xml b/hugray-server/ruoyi-modules/ruoyi-user/src/main/resources/mapper/user/AccountEarningRecordMapper.xml
new file mode 100644
index 0000000..bdeafc9
--- /dev/null
+++ b/hugray-server/ruoyi-modules/ruoyi-user/src/main/resources/mapper/user/AccountEarningRecordMapper.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.ruoyi.user.mapper.AccountEarningRecordMapper">
+
+ <resultMap type="com.ruoyi.user.domain.vo.EarningSummaryVo" id="EarningSummaryResult">
+ <result property="today" column="today" />
+ <result property="month" column="month" />
+ <result property="total" column="total" />
+ </resultMap>
+
+ <!-- 单条 SQL 聚合今日 / 本月 / 累计,避免多次数据库往返;命中 (user_id, create_time) 联合索引 -->
+ <select id="selectEarningSummary" parameterType="Long" resultMap="EarningSummaryResult">
+ select
+ ifnull(sum(case when create_time >= curdate() then amount end), 0) as today,
+ ifnull(sum(case when create_time >= date_format(curdate(), '%Y-%m-01') then amount end), 0) as month,
+ ifnull(sum(amount), 0) as total
+ from account_earning_record
+ where user_id = #{userId}
+ </select>
+
+</mapper>
diff --git a/hugray-server/ruoyi-modules/ruoyi-user/src/main/resources/mapper/user/AccountUserMapper.xml b/hugray-server/ruoyi-modules/ruoyi-user/src/main/resources/mapper/user/AccountUserMapper.xml
index f2594b3..ce17e9d 100644
--- a/hugray-server/ruoyi-modules/ruoyi-user/src/main/resources/mapper/user/AccountUserMapper.xml
+++ b/hugray-server/ruoyi-modules/ruoyi-user/src/main/resources/mapper/user/AccountUserMapper.xml
@@ -16,7 +16,8 @@
<result property="status" column="status" />
<result property="delFlag" column="del_flag" />
<result property="myReferralCode" column="my_referral_code" />
- <result property="referrerUserId" column="referrer_user_id" />
+ <result property="referrerId" column="referrer_id" />
+ <result property="systemLevel" column="system_level" />
<result property="createBy" column="create_by" />
<result property="createTime" column="create_time" />
<result property="updateBy" column="update_by" />
@@ -25,7 +26,7 @@
</resultMap>
<sql id="selectAccountUserVo">
- select user_id, username, phone, password, nickname, avatar, gender, birthday, status, del_flag, my_referral_code, referrer_user_id, create_by, create_time, update_by, update_time, remark
+ select user_id, username, phone, password, nickname, avatar, gender, birthday, status, del_flag, my_referral_code, referrer_id, system_level, create_by, create_time, update_by, update_time, remark
from account_user
</sql>
@@ -79,7 +80,8 @@
<if test="birthday != null">birthday,</if>
<if test="status != null and status != ''">status,</if>
<if test="myReferralCode != null and myReferralCode != ''">my_referral_code,</if>
- <if test="referrerUserId != null">referrer_user_id,</if>
+ <if test="referrerId != null">referrer_id,</if>
+ <if test="systemLevel != null and systemLevel != ''">system_level,</if>
<if test="remark != null">remark,</if>
<if test="createBy != null and createBy != ''">create_by,</if>
create_time
@@ -93,7 +95,8 @@
<if test="birthday != null">#{birthday},</if>
<if test="status != null and status != ''">#{status},</if>
<if test="myReferralCode != null and myReferralCode != ''">#{myReferralCode},</if>
- <if test="referrerUserId != null">#{referrerUserId},</if>
+ <if test="referrerId != null">#{referrerId},</if>
+ <if test="systemLevel != null and systemLevel != ''">#{systemLevel},</if>
<if test="remark != null">#{remark},</if>
<if test="createBy != null and createBy != ''">#{createBy},</if>
sysdate()
@@ -112,7 +115,8 @@
<if test="birthday != null">birthday = #{birthday},</if>
<if test="status != null and status != ''">status = #{status},</if>
<if test="myReferralCode != null and myReferralCode != ''">my_referral_code = #{myReferralCode},</if>
- <if test="referrerUserId != null">referrer_user_id = #{referrerUserId},</if>
+ <if test="referrerId != null">referrer_id = #{referrerId},</if>
+ <if test="systemLevel != null and systemLevel != ''">system_level = #{systemLevel},</if>
<if test="remark != null">remark = #{remark},</if>
<if test="updateBy != null">update_by = #{updateBy},</if>
update_time = sysdate()
diff --git a/hugray-server/ruoyi-modules/ruoyi-user/src/test/java/com/ruoyi/user/service/impl/AccountUserServiceImplTest.java b/hugray-server/ruoyi-modules/ruoyi-user/src/test/java/com/ruoyi/user/service/impl/AccountUserServiceImplTest.java
index c5e7df2..e638e85 100644
--- a/hugray-server/ruoyi-modules/ruoyi-user/src/test/java/com/ruoyi/user/service/impl/AccountUserServiceImplTest.java
+++ b/hugray-server/ruoyi-modules/ruoyi-user/src/test/java/com/ruoyi/user/service/impl/AccountUserServiceImplTest.java
@@ -21,12 +21,17 @@
import com.ruoyi.common.core.exception.ServiceException;
import com.ruoyi.user.config.UniversalCodeProperties;
import com.ruoyi.user.domain.AccountUser;
+import com.ruoyi.user.domain.vo.AccountUserInfoVo;
+import com.ruoyi.user.domain.vo.EarningSummaryVo;
+import com.ruoyi.user.enums.MemberLevelEnum;
+import com.ruoyi.user.mapper.AccountEarningRecordMapper;
import com.ruoyi.user.mapper.AccountUserMapper;
/**
- * 会员注册服务单元测试
+ * 会员服务单元测试
*
- * 覆盖通用推荐码放行、无效/停用推荐码拒绝、正常绑定推荐人三分支。
+ * 覆盖会员注册(通用推荐码放行、无效/停用推荐码拒绝、正常绑定推荐人)
+ * 与个人中心会员信息查询(正常返回、入参/会员缺失校验、收益与等级兜底)。
*
* @author hugray
*/
@@ -39,6 +44,9 @@
@Mock
private AccountUserMapper accountUserMapper;
+
+ @Mock
+ private AccountEarningRecordMapper accountEarningRecordMapper;
@Mock
private UniversalCodeProperties universalCodeProperties;
@@ -60,8 +68,12 @@
ArgumentCaptor<AccountUser> captor = ArgumentCaptor.forClass(AccountUser.class);
verify(accountUserMapper).insertAccountUser(captor.capture());
AccountUser saved = captor.getValue();
- assertNull(saved.getReferrerUserId());
+ assertNull(saved.getReferrerId());
assertNotNull(saved.getMyReferralCode());
+ // 姓名注册未采集,落库留空(不再以手机号填充)
+ assertNull(saved.getNickname());
+ // 通用推荐码注册直接发放 L4 会员等级
+ assertEquals(MemberLevelEnum.L4.getCode(), saved.getSystemLevel());
// 通用码不参与推荐人定位
verify(accountUserMapper, never()).selectAccountUserByReferralCode(UNIVERSAL_CODE);
}
@@ -113,6 +125,106 @@
ArgumentCaptor<AccountUser> captor = ArgumentCaptor.forClass(AccountUser.class);
verify(accountUserMapper).insertAccountUser(captor.capture());
- assertEquals(100L, captor.getValue().getReferrerUserId());
+ assertEquals(100L, captor.getValue().getReferrerId());
+ // 非通用码注册不设等级,走库默认值 C(普通用户)
+ assertNull(captor.getValue().getSystemLevel());
+ }
+
+ @Test
+ @DisplayName("会员存在时应返回昵称/头像/等级与收益汇总")
+ void getAccountUserInfo_shouldReturnProfile_whenMemberExists()
+ {
+ AccountUser member = new AccountUser();
+ member.setUserId(66L);
+ member.setNickname("辉煌G891");
+ member.setPhone("13355555555");
+ member.setAvatar("https://cdn.example.com/a.png");
+ member.setSystemLevel(MemberLevelEnum.L3.getCode());
+ EarningSummaryVo summary = new EarningSummaryVo(1000L, 40000L, 12800000L);
+ when(accountUserMapper.selectAccountUserById(66L)).thenReturn(member);
+ when(accountEarningRecordMapper.selectEarningSummary(66L)).thenReturn(summary);
+
+ AccountUserInfoVo vo = accountUserService.getAccountUserInfo(66L);
+
+ assertEquals(66L, vo.getUserId());
+ assertEquals("辉煌G891", vo.getNickname());
+ assertEquals("133****5555", vo.getPhone());
+ assertEquals("https://cdn.example.com/a.png", vo.getAvatar());
+ assertEquals(MemberLevelEnum.L3.getCode(), vo.getMemberLevel());
+ assertEquals(1000L, vo.getEarnings().getToday());
+ assertEquals(40000L, vo.getEarnings().getMonth());
+ assertEquals(12800000L, vo.getEarnings().getTotal());
+ }
+
+ @Test
+ @DisplayName("会员ID为空时应抛出获取会员信息失败")
+ void getAccountUserInfo_shouldThrow_whenUserIdNull()
+ {
+ ServiceException ex = assertThrows(ServiceException.class,
+ () -> accountUserService.getAccountUserInfo(null));
+ assertEquals("获取会员信息失败", ex.getMessage());
+ }
+
+ @Test
+ @DisplayName("会员不存在时应抛出获取会员信息失败")
+ void getAccountUserInfo_shouldThrow_whenMemberNotFound()
+ {
+ when(accountUserMapper.selectAccountUserById(66L)).thenReturn(null);
+
+ ServiceException ex = assertThrows(ServiceException.class,
+ () -> accountUserService.getAccountUserInfo(66L));
+ assertEquals("获取会员信息失败", ex.getMessage());
+ }
+
+ @Test
+ @DisplayName("无收益流水时应兜底返回全零汇总")
+ void getAccountUserInfo_shouldFallbackZeroEarnings_whenNoRecord()
+ {
+ AccountUser member = new AccountUser();
+ member.setUserId(66L);
+ member.setSystemLevel(MemberLevelEnum.L1.getCode());
+ when(accountUserMapper.selectAccountUserById(66L)).thenReturn(member);
+ when(accountEarningRecordMapper.selectEarningSummary(66L)).thenReturn(null);
+
+ AccountUserInfoVo vo = accountUserService.getAccountUserInfo(66L);
+
+ assertNotNull(vo.getEarnings());
+ assertEquals(0L, vo.getEarnings().getToday());
+ assertEquals(0L, vo.getEarnings().getMonth());
+ assertEquals(0L, vo.getEarnings().getTotal());
+ }
+
+ @Test
+ @DisplayName("等级为空时应兜底为 C")
+ void getAccountUserInfo_shouldFallbackC_whenSystemLevelBlank()
+ {
+ assertFallbackLevel(null, MemberLevelEnum.C.getCode());
+ assertFallbackLevel("", MemberLevelEnum.C.getCode());
+ }
+
+ @Test
+ @DisplayName("等级为非法值时应兜底为 C")
+ void getAccountUserInfo_shouldFallbackC_whenSystemLevelIllegal()
+ {
+ assertFallbackLevel("L9", MemberLevelEnum.C.getCode());
+ }
+
+ /**
+ * 校验不同等级取值下的等级兜底结果
+ *
+ * @param rawLevel 库中原始等级值
+ * @param expectedLevel 期望下发的等级
+ */
+ private void assertFallbackLevel(String rawLevel, String expectedLevel)
+ {
+ AccountUser member = new AccountUser();
+ member.setUserId(66L);
+ member.setSystemLevel(rawLevel);
+ when(accountUserMapper.selectAccountUserById(66L)).thenReturn(member);
+ when(accountEarningRecordMapper.selectEarningSummary(66L)).thenReturn(EarningSummaryVo.zero());
+
+ AccountUserInfoVo vo = accountUserService.getAccountUserInfo(66L);
+
+ assertEquals(expectedLevel, vo.getMemberLevel());
}
}
diff --git a/hugray-server/sql/hugray_account_20261012.sql b/hugray-server/sql/hugray_account_20261012.sql
index ac23db4..2942c5f 100644
--- a/hugray-server/sql/hugray_account_20261012.sql
+++ b/hugray-server/sql/hugray_account_20261012.sql
@@ -2,7 +2,12 @@
-- C 端会员用户表变更(ruoyi-user 用户服务)
--
-- 需求:用户小程序注册需支持「推荐码(邀请码)」。
--- 变更内容:account_user 增加「本人推荐码」与「推荐人会员ID」两个字段,并为本人推荐码建唯一索引。
+-- 变更内容:account_user 增加「本人推荐码」列,并为其建唯一索引。
+--
+-- 说明(按权威库 192.168.110.111 的既有字段调整):
+-- 权威库已存在「推荐人」列 referrer_id(自连接 account_user.user_id)与
+-- system_level,故本脚本不再新增 referrer_user_id / member_level 等重复列,
+-- 推荐关系直接复用既有 referrer_id。
--
-- 目标库 hugray-account 由使用方预先创建(utf8mb4)。
-- 注意:库名含连字符,任何引用都必须用反引号。
@@ -10,10 +15,9 @@
USE `hugray-account`;
--- 变更语句:新增两个字段
+-- 变更语句:新增「本人推荐码」列(权威库无此列,推荐码机制所需)
ALTER TABLE `account_user`
- ADD COLUMN `my_referral_code` varchar(20) DEFAULT NULL COMMENT '本人推荐码(注册时生成,全局唯一)' AFTER `remark`,
- ADD COLUMN `referrer_user_id` bigint(20) DEFAULT NULL COMMENT '推荐人会员ID(关联 account_user.user_id)' AFTER `my_referral_code`;
+ ADD COLUMN `my_referral_code` varchar(20) DEFAULT NULL COMMENT '本人推荐码(注册时生成,全局唯一)' AFTER `remark`;
-- 本人推荐码全局唯一:随机生成且不重复使用,逻辑删除行占用不影响业务
CREATE UNIQUE INDEX `uk_account_user_referral_code` ON `account_user` (`my_referral_code`);
@@ -22,4 +26,4 @@
-- 回滚语句(如需回滚,执行以下语句)
-- ----------------------------
-- DROP INDEX `uk_account_user_referral_code` ON `account_user`;
--- ALTER TABLE `account_user` DROP COLUMN `referrer_user_id`, DROP COLUMN `my_referral_code`;
+-- ALTER TABLE `account_user` DROP COLUMN `my_referral_code`;
diff --git a/hugray-server/sql/hugray_account_20261013.sql b/hugray-server/sql/hugray_account_20261013.sql
new file mode 100644
index 0000000..1bfd89b
--- /dev/null
+++ b/hugray-server/sql/hugray_account_20261013.sql
@@ -0,0 +1,37 @@
+-- ----------------------------
+-- C 端会员 个人中心 相关变更(ruoyi-user 用户服务)
+--
+-- 需求:个人中心页需展示「会员等级」与「今日/本月/累计收益」。
+-- 变更内容:新增「会员收益流水表」account_earning_record,收益三栏由其按时间实时聚合。
+--
+-- 说明(按权威库 192.168.110.111 的既有字段调整):
+-- 会员等级复用权威库既有列 account_user.system_level(C<L1<L2<L3<L4,从低到高),
+-- 故本脚本不再新增 member_level 重复列。
+--
+-- 目标库 hugray-account 由使用方预先创建(utf8mb4)。
+-- 注意:库名含连字符,任何引用都必须用反引号。
+-- ----------------------------
+
+USE `hugray-account`;
+
+-- 变更:新增会员收益流水表(一条流水=一次收益事件,三栏由 SQL 按时间聚合)
+CREATE TABLE `account_earning_record` (
+ `record_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '收益记录ID',
+ `user_id` bigint(20) NOT NULL COMMENT '会员ID(关联 account_user.user_id)',
+ `amount` bigint(20) NOT NULL DEFAULT 0 COMMENT '收益金额(单位:分,可正可负)',
+ `source` char(1) NOT NULL COMMENT '收益来源(1订单收益 2邀请奖励 3等级差价 9其他)',
+ `biz_no` varchar(64) DEFAULT NULL COMMENT '业务单号(同来源幂等去重)',
+ `remark` varchar(255) DEFAULT NULL COMMENT '备注',
+ `create_time` datetime DEFAULT NULL COMMENT '发生时间',
+ `update_time` datetime DEFAULT NULL COMMENT '更新时间',
+ PRIMARY KEY (`record_id`),
+ -- 覆盖「按会员 + 时间区间」的聚合查询,避免全表扫描
+ KEY `idx_earning_user_time` (`user_id`, `create_time`),
+ -- 业务幂等:同来源同业务单号唯一(biz_no 为空时 MySQL 唯一索引不冲突,允许手工补录)
+ UNIQUE KEY `uk_earning_source_biz` (`source`, `biz_no`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='会员收益流水表';
+
+-- ----------------------------
+-- 回滚语句(如需回滚,执行以下语句)
+-- ----------------------------
+-- DROP TABLE IF EXISTS `account_earning_record`;
diff --git a/hugray-user-miniapp/api/index.js b/hugray-user-miniapp/api/index.js
index 1ed7643..cc7e5ec 100644
--- a/hugray-user-miniapp/api/index.js
+++ b/hugray-user-miniapp/api/index.js
@@ -20,13 +20,6 @@
}
/**
- * 获取当前登录人信息(含会员等级 L1~L4 / 普通用户)
- */
-export function getUserInfo() {
- return get('/miniapp/system/user/getInfo')
-}
-
-/**
* 手机号一键登录(微信手机号快速验证,凭证换取登录态)
* @param {Object} data { code } 手机号快速验证凭证
*/
diff --git a/hugray-user-miniapp/pages.json b/hugray-user-miniapp/pages.json
index 2cb82bc..fdb3bde 100644
--- a/hugray-user-miniapp/pages.json
+++ b/hugray-user-miniapp/pages.json
@@ -30,6 +30,13 @@
}
},
{
+ "path": "pages/profile/edit",
+ // 导航栏含更多 / 帮助图标,需自定义导航栏承载
+ "style": {
+ "navigationStyle": "custom"
+ }
+ },
+ {
"path": "pages/contact/index",
// 顶部为深蓝底品牌区,与白底原生导航冲突,故使用自定义导航栏
"style": {
diff --git a/hugray-user-miniapp/pages/profile/edit.vue b/hugray-user-miniapp/pages/profile/edit.vue
new file mode 100644
index 0000000..9cf013f
--- /dev/null
+++ b/hugray-user-miniapp/pages/profile/edit.vue
@@ -0,0 +1,447 @@
+<template>
+ <view class="page">
+ <!-- 自定义导航栏:状态栏留白由行内 paddingTop 撑开 -->
+ <view class="nav" :style="{ paddingTop: statusBarHeight + 'px' }">
+ <view class="nav-bar">
+ <!-- 返回 -->
+ <view class="nav-back" @tap="handleBack">
+ <view class="icon-back"></view>
+ </view>
+ <text class="nav-title">我的资料</text>
+ <!-- 更多 / 帮助(本期占位) -->
+ <view class="nav-actions">
+ <view class="nav-action" @tap="handlePlaceholder('更多')">
+ <view class="icon-more">
+ <view class="dot"></view>
+ <view class="dot"></view>
+ <view class="dot"></view>
+ </view>
+ </view>
+ <view class="nav-action" @tap="handlePlaceholder('帮助')">
+ <view class="icon-help"><text class="icon-help-text">?</text></view>
+ </view>
+ </view>
+ </view>
+ </view>
+
+ <view class="body">
+ <view class="list">
+ <!-- 头像:点击更换(本期占位) -->
+ <view class="row" @tap="handlePlaceholder('头像更换')">
+ <text class="row-label">头像</text>
+ <view class="row-right">
+ <view class="avatar">
+ <image v-if="avatar" class="avatar-img" :src="avatar" mode="aspectFill" />
+ <view v-else class="icon icon-camera"></view>
+ </view>
+ <view class="icon icon-chevron-right"></view>
+ </view>
+ </view>
+
+ <!-- 手机号:小程序内暂不支持更换,仅只读展示(已脱敏),故无箭头 -->
+ <view class="row">
+ <text class="row-label">手机号</text>
+ <text class="row-value">{{ phone || '—' }}</text>
+ </view>
+
+ <!-- 实名认证 -->
+ <view class="row" @tap="handlePlaceholder('实名认证')">
+ <text class="row-label">实名认证</text>
+ <view class="row-right">
+ <view class="badge" :class="realNameBadge.cls">
+ <text class="badge-text">{{ realNameBadge.text }}</text>
+ </view>
+ <view class="icon icon-chevron-right"></view>
+ </view>
+ </view>
+
+ <!-- 企业 / 个体户认证 -->
+ <view class="row" @tap="handlePlaceholder('企业/个体户认证')">
+ <text class="row-label">企业/个体户认证</text>
+ <view class="row-right">
+ <view class="badge" :class="companyBadge.cls">
+ <text class="badge-text">{{ companyBadge.text }}</text>
+ </view>
+ <view class="icon icon-chevron-right"></view>
+ </view>
+ </view>
+ </view>
+
+ <!-- 底部操作区 -->
+ <view class="footer">
+ <view class="btn-logout" @tap="handleLogout">
+ <text class="btn-logout-text">退出登录</text>
+ </view>
+ <text class="link-cancel" @tap="handlePlaceholder('注销账号')">注销账号</text>
+ </view>
+ </view>
+ </view>
+</template>
+
+<script setup>
+import { computed, ref } from 'vue'
+import { onShow } from '@dcloudio/uni-app'
+import { clearToken, ensureLogin } from '@/utils/request.js'
+import { getMemberInfo, logout } from '@/api/index.js'
+
+/** 状态栏高度:自定义导航栏下用于顶部留白 */
+const statusBarHeight = uni.getSystemInfoSync().statusBarHeight || 0
+
+/** 用户头像:登录后由 getMemberInfo 下发,为空时回退相机占位 */
+const avatar = ref('')
+
+/** 用户手机号:登录后由 getMemberInfo 下发(已脱敏,形如 133****5555) */
+const phone = ref('')
+
+/**
+ * 认证状态字典:key 为后端下发的状态枚举,值含徽标文案与样式类
+ * 本期为原型,两处认证均取 none(未认证)占位,后端接入后直接替换 ref 初值即可
+ */
+const AUTH_STATUS_MAP = {
+ none: { text: '未认证', cls: 'badge--none' },
+ ing: { text: '认证中', cls: 'badge--ing' },
+ fail: { text: '认证失败', cls: 'badge--fail' },
+ done: { text: '已认证', cls: 'badge--done' }
+}
+
+/** 实名认证状态(本期占位) */
+const realNameStatus = ref('none')
+
+/** 企业 / 个体户认证状态(本期占位) */
+const companyStatus = ref('none')
+
+/** 实名认证徽标:状态缺省兜底「未认证」 */
+const realNameBadge = computed(() => AUTH_STATUS_MAP[realNameStatus.value] || AUTH_STATUS_MAP.none)
+
+/** 企业 / 个体户认证徽标:状态缺省兜底「未认证」 */
+const companyBadge = computed(() => AUTH_STATUS_MAP[companyStatus.value] || AUTH_STATUS_MAP.none)
+
+/** 返回上一页:无上级页面(如直接进入)时回到个人中心 */
+const handleBack = () => {
+ const pages = getCurrentPages()
+ if (pages.length > 1) {
+ uni.navigateBack()
+ return
+ }
+ uni.switchTab({ url: '/pages/profile/index' })
+}
+
+/** 占位交互:头像更换 / 实名认证 / 企业认证 / 注销 / 更多 / 帮助本期均不接后端 */
+const handlePlaceholder = (name) => {
+ uni.showToast({ title: `${name}功能开发中`, icon: 'none' })
+}
+
+/** 退出登录:二次确认后调用退出接口并清理本地登录态,回到个人中心 */
+const handleLogout = () => {
+ uni.showModal({
+ title: '提示',
+ content: '确定要退出登录吗?',
+ confirmText: '退出',
+ cancelText: '取消',
+ success: async (res) => {
+ if (!res.confirm) return
+ try {
+ await logout()
+ } catch (error) {
+ // 退出接口失败不阻塞本地退出,仅记录上下文日志
+ console.error('[PROFILE_EDIT] 退出登录接口失败', error)
+ }
+ clearToken()
+ uni.reLaunch({ url: '/pages/profile/index' })
+ }
+ })
+}
+
+/** 拉取当前会员信息(头像 / 脱敏手机号),失败时静默降级为空 */
+const loadMemberInfo = async () => {
+ try {
+ const res = await getMemberInfo()
+ const data = res.data || {}
+ avatar.value = data.avatar || ''
+ phone.value = data.phone || ''
+ } catch (error) {
+ console.error('[PROFILE_EDIT] 拉取会员信息失败', error)
+ avatar.value = ''
+ phone.value = ''
+ }
+}
+
+// 进入页面校验登录态,未登录跳登录页;已登录则拉取会员信息
+onShow(() => {
+ if (!ensureLogin()) return
+ loadMemberInfo()
+})
+</script>
+
+<style scoped lang="scss">
+.page {
+ min-height: 100vh;
+ display: flex;
+ flex-direction: column;
+ box-sizing: border-box;
+ background-color: #ffffff;
+}
+
+/* ==================== 自定义导航栏 ==================== */
+
+.nav {
+ flex-shrink: 0;
+ background-color: #ffffff;
+}
+
+.nav-bar {
+ position: relative;
+ height: 88rpx;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.nav-back {
+ position: absolute;
+ left: 16rpx;
+ top: 50%;
+ transform: translateY(-50%);
+ width: 64rpx;
+ height: 64rpx;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+/* 纯 CSS 左向箭头:左 + 下边框旋转 45° */
+.icon-back {
+ width: 22rpx;
+ height: 22rpx;
+ border-left: 4rpx solid #1f2937;
+ border-bottom: 4rpx solid #1f2937;
+ transform: rotate(45deg);
+}
+
+.nav-title {
+ font-size: 34rpx;
+ font-weight: 700;
+ line-height: 1;
+ letter-spacing: 1rpx;
+ color: #0f172a;
+}
+
+.nav-actions {
+ position: absolute;
+ right: 28rpx;
+ top: 50%;
+ transform: translateY(-50%);
+ display: flex;
+ align-items: center;
+ gap: 32rpx;
+}
+
+.nav-action {
+ width: 56rpx;
+ height: 56rpx;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+/* 更多:三点 */
+.icon-more {
+ display: flex;
+ align-items: center;
+ gap: 6rpx;
+}
+
+.icon-more .dot {
+ width: 8rpx;
+ height: 8rpx;
+ border-radius: 50%;
+ background: #334155;
+}
+
+/* 帮助:圆圈问号 */
+.icon-help {
+ width: 36rpx;
+ height: 36rpx;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ box-sizing: border-box;
+ border: 3rpx solid #334155;
+ border-radius: 50%;
+}
+
+.icon-help-text {
+ font-size: 22rpx;
+ font-weight: 700;
+ line-height: 1;
+ color: #334155;
+}
+
+/* ==================== 资料列表 ==================== */
+
+.body {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+}
+
+.list {
+ padding: 8rpx 32rpx 0;
+}
+
+.row {
+ min-height: 116rpx;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ border-bottom: 2rpx solid #f1f5f9;
+}
+
+/* 末行无分隔线 */
+.row:last-child {
+ border-bottom: none;
+}
+
+.row-label {
+ font-size: 30rpx;
+ line-height: 1;
+ color: #1f2937;
+}
+
+.row-right {
+ display: flex;
+ align-items: center;
+ gap: 16rpx;
+}
+
+.row-value {
+ font-size: 30rpx;
+ line-height: 1;
+ letter-spacing: 1rpx;
+ color: #94a3b8;
+}
+
+/* 头像容器:46pt 圆形 */
+.avatar {
+ width: 92rpx;
+ height: 92rpx;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ box-sizing: border-box;
+ border-radius: 50%;
+ background: #f1f5f9;
+ border: 2rpx solid #e2e8f0;
+ overflow: hidden;
+}
+
+.avatar-img {
+ width: 100%;
+ height: 100%;
+ border-radius: 50%;
+}
+
+/* ==================== 认证状态徽标(4 态) ==================== */
+
+.badge {
+ display: flex;
+ align-items: center;
+ padding: 10rpx 20rpx;
+ border-radius: 12rpx;
+ border: 2rpx solid;
+}
+
+.badge-text {
+ font-size: 26rpx;
+ font-weight: 500;
+ line-height: 1;
+}
+
+/* 未认证 */
+.badge--none {
+ color: #ea580c;
+ background: #fff7ed;
+ border-color: #fdba74;
+}
+
+/* 认证中 */
+.badge--ing {
+ color: #1e5ebc;
+ background: #eff6ff;
+ border-color: #93c5fd;
+}
+
+/* 认证失败 */
+.badge--fail {
+ color: #dc2626;
+ background: #fef2f2;
+ border-color: #fca5a5;
+}
+
+/* 已认证 */
+.badge--done {
+ color: #4d7c0f;
+ background: #f7fee7;
+ border-color: #bef264;
+}
+
+/* ==================== 底部操作区 ==================== */
+
+.footer {
+ /* 内容不足一屏时按钮自动贴底 */
+ margin-top: auto;
+ padding: 32rpx 48rpx 44rpx;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 36rpx;
+}
+
+.btn-logout {
+ width: 100%;
+ height: 96rpx;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 48rpx;
+ background: linear-gradient(90deg, #82c236 0%, #639c1f 100%);
+ box-shadow: 0 16rpx 40rpx rgba(99, 156, 31, 0.28);
+}
+
+.btn-logout-text {
+ font-size: 30rpx;
+ font-weight: 600;
+ line-height: 1;
+ color: #ffffff;
+}
+
+.link-cancel {
+ font-size: 28rpx;
+ line-height: 1;
+ color: #1e5ebc;
+ text-decoration: underline;
+}
+
+/* ==================== 图标(内联 base64 SVG) ==================== */
+
+.icon {
+ flex-shrink: 0;
+ background-repeat: no-repeat;
+ background-position: center center;
+ background-size: 100% 100%;
+}
+
+/* 相机占位(#94A3B8) */
+.icon-camera {
+ width: 40rpx;
+ height: 40rpx;
+ background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjOTRBM0I4IiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCI+PHBhdGggZD0iTTE0LjUgNGgtNUw3IDdINGEyIDIgMCAwIDAtMiAydjlhMiAyIDAgMCAwIDIgMmgxNmEyIDIgMCAwIDAgMi0yVjlhMiAyIDAgMCAwLTItMmgtM2wtMi41LTN6Ii8+PGNpcmNsZSBjeD0iMTIiIGN5PSIxMyIgcj0iMyIvPjwvc3ZnPg==');
+}
+
+/* 右箭头(#94A3B8) */
+.icon-chevron-right {
+ width: 32rpx;
+ height: 32rpx;
+ background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjOTRBM0I4IiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCI+PHBhdGggZD0ibTkgMTggNi02LTYtNiIvPjwvc3ZnPg==');
+}
+</style>
diff --git a/hugray-user-miniapp/pages/profile/index.vue b/hugray-user-miniapp/pages/profile/index.vue
index 1da43be..38ce0af 100644
--- a/hugray-user-miniapp/pages/profile/index.vue
+++ b/hugray-user-miniapp/pages/profile/index.vue
@@ -7,11 +7,13 @@
<view class="header">
<view class="user-main" @tap="handleAccountTap">
<view class="avatar">
- <view class="icon" :class="isLoggedIn ? 'icon-camera' : 'icon-user'"></view>
+ <!-- 有头像则渲染图片,否则回退图标(已登录相机 / 未登录用户) -->
+ <image v-if="avatar" class="avatar-img" :src="avatar" mode="aspectFill" />
+ <view v-else class="icon" :class="isLoggedIn ? 'icon-camera' : 'icon-user'"></view>
</view>
<view class="user-text">
<!-- 已登录:昵称 + 会员等级;未登录:登录入口 + 引导文案 -->
- <text class="user-name">{{ isLoggedIn ? nickname : '点击登录 / 注册' }}</text>
+ <text class="user-name">{{ isLoggedIn ? displayName : '点击登录 / 注册' }}</text>
<view v-if="isLoggedIn" class="level-pill">
<view class="level-medal">
<text class="level-medal-text">V</text>
@@ -26,7 +28,7 @@
</view>
</view>
- <!-- 收益数据展示区(343×74pt):未登录或 L0 普通用户不展示 -->
+ <!-- 收益数据展示区(343×74pt):未登录或 C 普通用户不展示 -->
<view v-if="!isRestrictedView" class="earnings-card">
<view v-for="item in earnings" :key="item.key" class="earning-item">
<text class="earning-label">{{ item.label }}</text>
@@ -71,6 +73,8 @@
import { computed, ref } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import { getToken } from '@/utils/request.js'
+import { getMemberInfo } from '@/api/index.js'
+import { formatYuan } from '@/utils/format.js'
import config from '@/config/index.js'
import StockOrderToast from '@/components/stock-order-toast/stock-order-toast.vue'
@@ -80,22 +84,34 @@
/** 登录态:本页需同时兼容「已登录 / 未登录」两种展示,故以本地 token 判定 */
const isLoggedIn = ref(false)
-/** 用户昵称:用户信息接口未就绪,先本地 mock(联调后替换为 getUserInfo) */
-const nickname = ref('辉煌G891')
+/** 用户姓名/昵称:登录后由 getMemberInfo 下发;注册未采集,通常为空 */
+const nickname = ref('')
-/** 用户等级:取值 L0~L4,其中 L0 为普通用户;等级接口未联调,先本地 mock(联调后取 getUserInfo 下发等级) */
-const userLevel = ref('L4')
+/** 用户手机号:登录后由 getMemberInfo 下发(已脱敏,形如 133****5555) */
+const phone = ref('')
-/** 是否受限展示:未登录或 L0 普通用户,个人中心仅展示「收货地址」「联系我们」 */
-const isRestrictedView = computed(() => !isLoggedIn.value || userLevel.value === 'L0')
+/** 头部展示名:姓名优先,为空时回退展示脱敏手机号 */
+const displayName = computed(() => nickname.value || phone.value)
-/** 收益三栏:未登录态统一展示 0.00 */
+/** 用户头像:登录后由 getMemberInfo 下发,为空时回退默认图标 */
+const avatar = ref('')
+
+/** 用户等级:取值 C/L1~L4,登录后由 getMemberInfo 下发;C 为普通用户 */
+const userLevel = ref('C')
+
+/** 是否受限展示:未登录或 C 普通用户,个人中心仅展示「收货地址」「联系我们」 */
+const isRestrictedView = computed(() => !isLoggedIn.value || userLevel.value === 'C')
+
+/** 收益汇总原始值(单位:分),登录后由 getMemberInfo 下发,未登录保持全零 */
+const earningsRaw = ref({ today: 0, month: 0, total: 0 })
+
+/** 收益三栏:后端下发单位为分,统一格式化为元展示;未登录统一展示 0.00 */
const earnings = computed(() => {
- const zeroValue = '0.00'
+ const source = isLoggedIn.value ? earningsRaw.value : { today: 0, month: 0, total: 0 }
return [
- { key: 'today', label: '今日收益', value: isLoggedIn.value ? '10.00' : zeroValue },
- { key: 'month', label: '本月收益', value: isLoggedIn.value ? '400.00' : zeroValue },
- { key: 'total', label: '累计收益', value: isLoggedIn.value ? '128000.00' : zeroValue }
+ { key: 'today', label: '今日收益', value: formatYuan(source.today) },
+ { key: 'month', label: '本月收益', value: formatYuan(source.month) },
+ { key: 'total', label: '累计收益', value: formatYuan(source.total) }
]
})
@@ -109,18 +125,18 @@
{ key: 'contact', icon: 'contact', title: '联系我们' }
]
-/** 当前可见菜单:受限展示(未登录 / L0)时仅保留「收货地址」「联系我们」 */
+/** 当前可见菜单:受限展示(未登录 / C)时仅保留「收货地址」「联系我们」 */
const menuList = computed(() =>
isRestrictedView.value ? MENU_LIST.filter((menu) => !menu.restricted) : MENU_LIST
)
-/** 头像 / 名称 / 按钮点击:未登录跳登录页,已登录进入资料编辑 */
+/** 头像 / 名称 / 编辑资料按钮点击:未登录跳登录页,已登录进入「我的资料」页 */
const handleAccountTap = () => {
if (!isLoggedIn.value) {
uni.reLaunch({ url: config.loginPage })
return
}
- uni.showToast({ title: '敬请期待', icon: 'none' })
+ uni.navigateTo({ url: '/pages/profile/edit' })
}
/** 菜单项点击:未登录先提示登录;已登录进入二级功能页(未接入的暂为占位反馈) */
@@ -151,9 +167,37 @@
uni.showToast({ title: '敬请期待', icon: 'none' })
}
-// 每次页面显示均刷新登录态,保证登录 / 退出后展示态同步
+/**
+ * 拉取当前会员信息(昵称 / 头像 / 等级 / 收益汇总)
+ * 失败时静默降级为普通用户态,不阻塞页面展示
+ */
+const loadMemberInfo = async () => {
+ try {
+ const res = await getMemberInfo()
+ const data = res.data || {}
+ nickname.value = data.nickname || ''
+ // 手机号后端已脱敏(133****5555),可直接用于展示兜底
+ phone.value = data.phone || ''
+ avatar.value = data.avatar || ''
+ // 等级缺省兜底 C,与后端 MemberLevelEnum.normalize 保持一致
+ userLevel.value = data.memberLevel || 'C'
+ earningsRaw.value = data.earnings || { today: 0, month: 0, total: 0 }
+ } catch (error) {
+ console.error('[PROFILE] 拉取会员信息失败', error)
+ nickname.value = ''
+ phone.value = ''
+ avatar.value = ''
+ userLevel.value = 'C'
+ earningsRaw.value = { today: 0, month: 0, total: 0 }
+ }
+}
+
+// 每次页面显示均刷新登录态,保证登录 / 退出后展示态同步;已登录则拉取会员信息
onShow(() => {
isLoggedIn.value = Boolean(getToken())
+ if (isLoggedIn.value) {
+ loadMemberInfo()
+ }
})
</script>
@@ -208,6 +252,13 @@
border: 5rpx solid #ffffff;
}
+/* 用户头像图片:铺满头像容器并裁为圆形 */
+.avatar-img {
+ width: 100%;
+ height: 100%;
+ border-radius: 50%;
+}
+
/* 已登录:相机图标;未登录:用户图标 */
.icon-camera {
width: 64rpx;
diff --git a/hugray-user-miniapp/utils/format.js b/hugray-user-miniapp/utils/format.js
new file mode 100644
index 0000000..6fef783
--- /dev/null
+++ b/hugray-user-miniapp/utils/format.js
@@ -0,0 +1,20 @@
+/**
+ * 通用格式化工具(用户端)
+ * 约定:后端金额统一以「分」下发,前端展示统一转为「元」
+ */
+
+/**
+ * 金额分 → 元
+ * @param {Number|String} fen 金额(单位:分)
+ * @param {Boolean} [withThousands=false] 是否添加千分位分隔(如 128,000.00)
+ * @returns {String} 保留两位小数的金额字符串,如 12800000 → '128000.00'
+ */
+export function formatYuan(fen, withThousands = false) {
+ const num = Number(fen)
+ // 非法、空值统一按 0 处理,避免页面出现 NaN
+ const yuan = Number.isFinite(num) ? num / 100 : 0
+ const fixed = yuan.toFixed(2)
+ if (!withThousands) return fixed
+ const [intPart, decPart] = fixed.split('.')
+ return intPart.replace(/\B(?=(\d{3})+(?!\d))/g, ',') + '.' + decPart
+}
--
Gitblit v1.7.1