From 3961d6324ac9b7757c6bfb8ecea6cb1b3caca6ee Mon Sep 17 00:00:00 2001
From: zjk <852185829@qq.com>
Date: 星期三, 14 十月 2026 11:44:21 +0800
Subject: [PATCH] feat(member-center): 新增会员中心全业务功能模块
---
hugray-server/ruoyi-modules/ruoyi-user/src/main/resources/mapper/user/AccountWarehouseMapper.xml | 13
hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/mapper/AccountWarehouseMapper.java | 22
hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/domain/vo/PurchaseOrderVo.java | 130 +
hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/domain/AccountSalesRecord.java | 186 ++
hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/domain/AccountPurchaseOrder.java | 186 ++
hugray-server/ruoyi-modules/ruoyi-user/src/main/resources/mapper/user/AccountWarehouseRecordMapper.xml | 63
hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/domain/vo/SalesMonthSummaryVo.java | 63
hugray-user-miniapp/pages.json | 7
hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/domain/vo/SalesTrendItemVo.java | 58
hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/domain/vo/MemberWarehouseVo.java | 89 +
hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/domain/vo/WarehouseRecordVo.java | 117 +
hugray-server/sql/hugray_account_member_center_20261014.sql | 96 +
doc/design/用户端_会员中心_20261014.md | 475 ++++++
hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/domain/vo/ProductSalesRatioVo.java | 35
hugray-server/ruoyi-modules/ruoyi-user/src/main/resources/mapper/user/AccountSalesRecordMapper.xml | 77 +
hugray-user-miniapp/pages/profile/index.vue | 5
hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/domain/vo/MemberSalesStatisticsVo.java | 144 +
hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/mapper/AccountSalesRecordMapper.java | 72
hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/domain/dto/MemberPurchaseDto.java | 54
hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/mapper/AccountWarehouseRecordMapper.java | 29
hugray-server/ruoyi-modules/ruoyi-user/src/main/resources/mapper/user/AccountPurchaseOrderMapper.xml | 76 +
hugray-user-miniapp/api/index.js | 43
hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/enums/WarehouseChangeTypeEnum.java | 70
hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/controller/AccountMemberController.java | 110 +
hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/domain/AccountWarehouseRecord.java | 163 ++
hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/enums/PurchaseOrderStatusEnum.java | 66
hugray-user-miniapp/pages/member/index.vue | 1394 ++++++++++++++++++
hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/mapper/AccountPurchaseOrderMapper.java | 42
hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/service/IAccountMemberCenterService.java | 59
hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/domain/vo/SalesUserRankingVo.java | 63
hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/service/impl/AccountMemberCenterServiceImpl.java | 387 +++++
31 files changed, 4,394 insertions(+), 0 deletions(-)
diff --git "a/doc/design/\347\224\250\346\210\267\347\253\257_\344\274\232\345\221\230\344\270\255\345\277\203_20261014.md" "b/doc/design/\347\224\250\346\210\267\347\253\257_\344\274\232\345\221\230\344\270\255\345\277\203_20261014.md"
new file mode 100644
index 0000000..3424287
--- /dev/null
+++ "b/doc/design/\347\224\250\346\210\267\347\253\257_\344\274\232\345\221\230\344\270\255\345\277\203_20261014.md"
@@ -0,0 +1,475 @@
+# 用户端小程序 个人中心-会员中心 接口 开发设计文档
+
+- 模块:ruoyi-user(会员业务)
+- 日期:2026-10-14
+- 需求来源:“这是个人中心-会员中心的原型,根据 pen 源文件的设计风格设计 UI 然后实现页面和后端的业务功能”
+- 关联前端:`hugray-user-miniapp`(新增 `pages/member/index.vue`)
+- 关联文档:`doc/design/用户端_个人中心_20261013.md`(会员等级 / 收益 / 登录取数范式)、`doc/design/用户端_收货地址_20261014.md`(C 端接口范式)
+- 数据权威源:`192.168.110.111:3306/hugray-account`(实际表结构优先于本文档)
+- 设计风格源:`doc/琥歌瑞0928.pen`(深蓝渐变卡 `#003E7E→#1E5EBC`、白玻璃卡、草绿主按钮 `#82C236→#639C1F`)
+
+---
+
+## 一、功能概述
+
+会员中心页(原型图一「会员中心」/ 图二「我的会员」)为 L1~L4 会员专属页面,包含两个 Tab:
+
+| Tab | 原型位置 | 展示内容 |
+| --- | --- | --- |
+| 销售统计 | 图一 | 深蓝卡(脱敏手机号 + 当前等级 + 等级详情);当月销售额 / 当月进货额 / 当月利润;当月利润额大数 + 环比;销售额折线图(按日 1~20 号);商品销售额比例饼图;销售用户排行列表 |
+| 我的仓库 | 图二 | 深蓝卡(同上);商品列表(商品图占位 + 名称 + 当前库存 + 最低需求库存 + 进货 / 仓库记录按钮);低库存警示(`当前库存` 红字 + 蓝色提示条) |
+
+展示规则(沿用已确认业务约定):`isRestrictedView = !isLoggedIn || memberLevel === 'C'`。**未登录或 C 普通用户**不展示会员中心入口;接口虽不校验等级,但前端受限态不进入该页。会员身份取自网关注入的登录态,天然防水平越权。
+
+**本期新增 5 个 C 端接口**(对应前端新增的会员中心接口):
+
+| # | 接口 | 用途 |
+| --- | --- | --- |
+| 1 | `GET /miniapp/account/user/member/sales-statistics` | 销售统计(三栏 + 趋势 + 比例 + 排行) |
+| 2 | `GET /miniapp/account/user/member/warehouses` | 我的仓库列表(含低库存标记) |
+| 3 | `POST /miniapp/account/user/member/purchase` | 进货(生成进货单 + 增库存 + 记流水) |
+| 4 | `GET /miniapp/account/user/member/warehouse-records` | 仓库变动记录(进货/库存流水) |
+| 5 | `GET /miniapp/account/user/member/purchase-orders` | 进货订单列表 |
+
+---
+
+## 二、现状与关键约束
+
+### 2.1 表结构现状
+
+| 表 | 现状 | 本次是否新增 |
+| --- | --- | --- |
+| `account_user` | 已有 `system_level`(会员等级);9 个业务统计列(`sell_amount_total` 等)为“待回填”占位,无写入方 | 否(只读) |
+| `account_warehouse` | 已有:`user_id`/`product_name`/`stock_qty`/`min_stock_qty`/`del_flag` 等,配套实体 + Mapper + XML 齐备 | 否(只读 + 更新库存) |
+| 会员销售流水 | **无** | 是 → `account_sales_record` |
+| 会员进货单 | **无** | 是 → `account_purchase_order` |
+| 云仓变动流水 | **无** | 是 → `account_warehouse_record` |
+
+### 2.2 已有接口缺口
+
+`ruoyi-user` 现有 C 端控制器 `AccountProfileController`(`/user`)仅覆盖个人中心 / 认证 / 邀请 / 收获地址;**无仓库、进货、销售统计相关接口**。`AccountWarehouseMapper` 已有 `selectWarehouseByUserId` / `insertWarehouse` / `updateWarehouse`,但**缺 Service 层与 C 端查询接口**。
+
+### 2.3 销售数据来源(已确认决策)
+
+后端无任何订单 / 交易模块(`ruoyi-product` 仅启动类,`hugray-order` 零表),故:
+
+- **销售统计采用“新建简化销售流水模型”**:`account_sales_record` 一条流水 = 一次销售事件(销售额 + 利润),本月三栏与趋势 / 比例 / 排行均由流水实时聚合。
+- **进货采用“真实进货”**:`POST .../purchase` 生成一条 `account_purchase_order`,同时增加 `account_warehouse.stock_qty` 并追加一条 `account_warehouse_record`;当月进货额由进货单实时聚合。
+- `account_user` 的 9 个“待回填”统计列**本期不回填**(避免双写口径不一致),页面数据一律由流水表实时聚合。
+
+### 2.4 金额单位约定
+
+沿用用户规范与既有约定:接口金额一律 **分(long)** 返回,前端 `formatYuan(fen, withThousands)` 格式化为元(两位小数,可带千分位)。
+
+### 2.5 当月利润口径
+
+`利润 = 销售额 − 进货成本`(按销售流水 `profit_amount` 字段逐条落库,聚合时 `SUM(profit_amount)`)。**说明**:销售流水写入方(订单模块)尚未就绪,本期利润随销售流水数据而来;无流水时为 0。
+
+---
+
+## 三、接口设计
+
+统一响应体:`R<T>`(`code=200` 成功)。前端 `utils/request.js` 按 `successCode=200` / `unauthorizedCode=401` 解包,调用方取 `res.data`。
+
+> 规范说明:用户规则中的“`Result<T>`、`code=0 成功`”为通用模板;本工程实际框架为 RuoYi `R`(200 成功)+ `@RestControllerAdvice` 全局异常。按“与现有工程保持一致”优先,采用框架既有约定。
+
+### 3.1 查询销售统计
+
+- **外部路径**:`GET /miniapp/account/user/member/sales-statistics`(网关 `StripPrefix=2` → 服务内 `/user/member/sales-statistics`)
+- **鉴权**:需登录(不在白名单,未登录由 `AuthFilter` 返回 401)
+- **入参**:无(会员身份取自网关注入的 `user_id` 头)
+- **出参**:`R<MemberSalesStatisticsVo>`
+
+```json
+{
+ "code": 200,
+ "msg": "操作成功",
+ "data": {
+ "phone": "136****6666",
+ "memberLevel": "L2",
+ "monthSalesAmount": 19210000,
+ "monthPurchaseAmount": 19210000,
+ "monthProfitAmount": 192100,
+ "monthSalesCount": 86,
+ "profitGrowthRate": 14,
+ "salesTrend": [
+ { "day": "1号", "amount": 300000 },
+ { "day": "5号", "amount": 420000 }
+ ],
+ "productSalesRatio": [
+ { "productName": "G&METS-V", "amount": 1245100 },
+ { "productName": "G&METS-II", "amount": 1245100 }
+ ],
+ "salesUserRanking": [
+ { "rank": 1, "nickname": "张三", "phone": "138****4561", "amount": 1645200 }
+ ]
+ }
+}
+```
+
+| 字段 | 类型 | 说明 |
+| --- | --- | --- |
+| phone | String | 当前会员手机号(**脱敏**) |
+| memberLevel | String | 会员等级 `C`/`L1~L4`(兜底 `C`) |
+| monthSalesAmount | Long | 当月销售额(分)= `SUM(account_sales_record.amount)` |
+| monthPurchaseAmount | Long | 当月进货额(分)= `SUM(account_purchase_order.total_amount)` |
+| monthProfitAmount | Long | 当月利润额(分)= `SUM(account_sales_record.profit_amount)`,可为负 |
+| monthSalesCount | Integer | 当月销售单数 = `COUNT(account_sales_record)` |
+| profitGrowthRate | Integer | 当月利润环比上月提升百分比(`Math.round`,无上月数据时返回 0) |
+| salesTrend[].day | String | 日标签(`1号`~`31号`),当月无销售当日补 0 |
+| salesTrend[].amount | Long | 当日销售额(分) |
+| productSalesRatio[].productName | String | 商品名称 |
+| productSalesRatio[].amount | Long | 该商品当月销售额(分) |
+| salesUserRanking[].rank | Integer | 名次(从 1 开始) |
+| salesUserRanking[].nickname | String | 购买人昵称(可空,前端回退脱敏手机号) |
+| salesUserRanking[].phone | String | 购买人手机号(**脱敏**) |
+| salesUserRanking[].amount | Long | 该用户当月购买额(分) |
+
+- **错误提示**:`获取销售统计失败`(会员不存在或查询异常)
+
+### 3.2 查询我的仓库列表
+
+- **外部路径**:`GET /miniapp/account/user/member/warehouses`
+- **鉴权**:需登录
+- **入参**:无
+- **出参**:`R<List<MemberWarehouseVo>>`
+
+```json
+{
+ "code": 200,
+ "msg": "操作成功",
+ "data": [
+ {
+ "warehouseId": 100,
+ "productName": "G&METS-II 超分子基团复合肽",
+ "productImage": null,
+ "stockQty": 100,
+ "minStockQty": 100,
+ "lowStock": false
+ }
+ ]
+}
+```
+
+| 字段 | 类型 | 说明 |
+| --- | --- | --- |
+| warehouseId | Long | 云仓行主键(进货时回传) |
+| productName | String | 商品名称 |
+| productImage | String | 商品图片(`account_warehouse` 无此列,本期恒为 null,前端用占位图) |
+| stockQty | Long | 当前库存 |
+| minStockQty | Long | 最低需求库存 |
+| lowStock | Boolean | 低库存标记:`stockQty < minStockQty` 时为 true(前端标红 + 蓝色提示条) |
+
+### 3.3 进货(生成进货单 + 增加库存)
+
+- **外部路径**:`POST /miniapp/account/user/member/purchase`
+- **鉴权**:需登录
+- **入参**(`MemberPurchaseDto`):
+
+```json
+{ "warehouseId": 100, "qty": 5 }
+```
+
+| 字段 | 类型 | 必填 | 说明 |
+| --- | --- | --- | --- |
+| warehouseId | Long | 是 | 云仓行主键(须属于当前会员) |
+| qty | Long | 是 | 进货数量(正整数) |
+
+- **出参**:`R<Void>`
+- **事务**:同一事务内 ① 校验归属 → ② 落进货单 → ③ `stock_qty = stock_qty + qty` → ④ 追加仓库变动流水。
+- **幂等**:进货单 `order_no` 由服务端生成(`PG + yyyyMMddHHmmss + 4 位随机`),`UNIQUE KEY` 兜底。
+- **错误提示**:`商品不存在或不属于当前会员`、`进货数量必须大于 0`、`进货失败,请稍后重试`。
+
+### 3.4 查询仓库变动记录
+
+- **外部路径**:`GET /miniapp/account/user/member/warehouse-records`
+- **鉴权**:需登录
+- **入参**:`pageNum`(默认 1)、`pageSize`(默认 10)
+- **出参**:`TableDataInfo`(顶层 `rows` / `total`)
+
+```json
+{
+ "code": 200,
+ "msg": "查询成功",
+ "rows": [
+ {
+ "recordId": 1,
+ "productName": "G&METS-II 超分子基团复合肽",
+ "changeType": "1",
+ "changeTypeName": "进货入库",
+ "changeQty": 5,
+ "afterQty": 105,
+ "remark": "进货单 PG20261014xxxx",
+ "createTime": "2026-10-14 10:00:00"
+ }
+ ],
+ "total": 1
+}
+```
+
+| 字段 | 类型 | 说明 |
+| --- | --- | --- |
+| recordId | Long | 流水主键 |
+| productName | String | 商品名称(变动时快照) |
+| changeType | String | 变动类型 `1` 进货入库 / `2` 销售出库 / `3` 库存调整 |
+| changeTypeName | String | 变动类型中文名(由枚举下发) |
+| changeQty | Long | 变动数量(入库为正,出库为负) |
+| afterQty | Long | 变动后库存 |
+| remark | String | 备注(进货单号等) |
+| createTime | String | 变动时间(`yyyy-MM-dd HH:mm:ss`) |
+
+### 3.5 查询进货订单列表
+
+- **外部路径**:`GET /miniapp/account/user/member/purchase-orders`
+- **鉴权**:需登录
+- **入参**:`pageNum`(默认 1)、`pageSize`(默认 10)
+- **出参**:`TableDataInfo`(顶层 `rows` / `total`)
+
+```json
+{
+ "code": 200,
+ "msg": "查询成功",
+ "rows": [
+ {
+ "orderId": 1,
+ "orderNo": "PG2026101410301234",
+ "productName": "G&METS-II 超分子基团复合肽",
+ "qty": 5,
+ "unitPrice": 20000,
+ "totalAmount": 100000,
+ "status": "1",
+ "statusName": "已入库",
+ "createTime": "2026-10-14 10:30:12"
+ }
+ ],
+ "total": 1
+}
+```
+
+| 字段 | 类型 | 说明 |
+| --- | --- | --- |
+| orderId | Long | 进货单主键 |
+| orderNo | String | 进货单号 |
+| productName | String | 商品名称 |
+| qty | Long | 进货数量 |
+| unitPrice | Long | 进货单价(分);`account_warehouse` 无单价列,本期由前端进货弹窗按商品默认单价传入或取 0 |
+| totalAmount | Long | 进货金额(分)= `qty × unitPrice` |
+| status | String | 状态 `1` 已入库 |
+| statusName | String | 状态中文名 |
+| createTime | String | 下单时间 |
+
+---
+
+## 四、数据库设计
+
+库:`hugray-account`(权威库 `192.168.110.111:3306`)。脚本:`sql/hugray_account_member_center_20261014.sql`(与代码同一次提交,含变更 + 回滚)。
+
+### 4.1 新增会员销售流水表 `account_sales_record`
+
+```sql
+CREATE TABLE IF NOT EXISTS `account_sales_record` (
+ `record_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '销售流水ID',
+ `seller_id` bigint(20) NOT NULL COMMENT '销售会员ID(account_user.user_id)',
+ `buyer_id` bigint(20) DEFAULT NULL COMMENT '购买会员ID(account_user.user_id,散客为空)',
+ `buyer_name` varchar(64) DEFAULT NULL COMMENT '购买人昵称(销售排行展示用)',
+ `buyer_phone` varchar(20) DEFAULT NULL COMMENT '购买人手机号(下发脱敏)',
+ `product_name` varchar(128) DEFAULT NULL COMMENT '商品名称(比例图/排行展示用)',
+ `qty` bigint(20) NOT NULL DEFAULT 0 COMMENT '销售数量',
+ `amount` bigint(20) NOT NULL DEFAULT 0 COMMENT '销售额(单位:分)',
+ `profit_amount` bigint(20) NOT NULL DEFAULT 0 COMMENT '利润额(单位:分,可为负)',
+ `sale_time` datetime DEFAULT NULL COMMENT '销售时间(聚合口径)',
+ `del_flag` char(1) DEFAULT '0' COMMENT '删除标志(0存在 2删除)',
+ `create_by` varchar(64) DEFAULT '' COMMENT '创建者',
+ `create_time` datetime DEFAULT NULL COMMENT '创建时间',
+ `update_by` varchar(64) DEFAULT '' COMMENT '更新者',
+ `update_time` datetime DEFAULT NULL COMMENT '更新时间',
+ `remark` varchar(500) DEFAULT NULL COMMENT '备注',
+ PRIMARY KEY (`record_id`),
+ KEY `idx_sales_seller_time` (`seller_id`, `sale_time`)
+) ENGINE=InnoDB AUTO_INCREMENT=100 DEFAULT CHARSET=utf8mb4 COMMENT='会员销售流水表';
+```
+
+### 4.2 新增会员进货单表 `account_purchase_order`
+
+```sql
+CREATE TABLE IF NOT EXISTS `account_purchase_order` (
+ `order_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '进货单ID',
+ `order_no` varchar(64) NOT NULL COMMENT '进货单号(幂等唯一)',
+ `user_id` bigint(20) NOT NULL COMMENT '进货会员ID(account_user.user_id)',
+ `warehouse_id` bigint(20) DEFAULT NULL COMMENT '云仓行ID(account_warehouse.id,快照)',
+ `product_name` varchar(128) DEFAULT NULL COMMENT '商品名称(快照)',
+ `qty` bigint(20) NOT NULL DEFAULT 0 COMMENT '进货数量',
+ `unit_price` bigint(20) NOT NULL DEFAULT 0 COMMENT '进货单价(单位:分)',
+ `total_amount` bigint(20) NOT NULL DEFAULT 0 COMMENT '进货金额(单位:分)= qty × unit_price',
+ `status` char(1) DEFAULT '1' COMMENT '状态(1已入库)',
+ `purchase_time` datetime DEFAULT NULL COMMENT '进货时间(聚合口径)',
+ `del_flag` char(1) DEFAULT '0' COMMENT '删除标志(0存在 2删除)',
+ `create_by` varchar(64) DEFAULT '' COMMENT '创建者',
+ `create_time` datetime DEFAULT NULL COMMENT '创建时间',
+ `update_by` varchar(64) DEFAULT '' COMMENT '更新者',
+ `update_time` datetime DEFAULT NULL COMMENT '更新时间',
+ `remark` varchar(500) DEFAULT NULL COMMENT '备注',
+ PRIMARY KEY (`order_id`),
+ UNIQUE KEY `uk_purchase_order_no` (`order_no`),
+ KEY `idx_purchase_user_time` (`user_id`, `purchase_time`)
+) ENGINE=InnoDB AUTO_INCREMENT=100 DEFAULT CHARSET=utf8mb4 COMMENT='会员进货单表';
+```
+
+### 4.3 新增云仓变动流水表 `account_warehouse_record`
+
+```sql
+CREATE TABLE IF NOT EXISTS `account_warehouse_record` (
+ `record_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '流水ID',
+ `user_id` bigint(20) NOT NULL COMMENT '会员ID(account_user.user_id)',
+ `warehouse_id` bigint(20) DEFAULT NULL COMMENT '云仓行ID(account_warehouse.id)',
+ `product_name` varchar(128) DEFAULT NULL COMMENT '商品名称(快照)',
+ `change_type` char(1) NOT NULL COMMENT '变动类型(1进货入库 2销售出库 3库存调整)',
+ `change_qty` bigint(20) NOT NULL DEFAULT 0 COMMENT '变动数量(入库为正,出库为负)',
+ `after_qty` bigint(20) NOT NULL DEFAULT 0 COMMENT '变动后库存',
+ `biz_no` varchar(64) DEFAULT NULL COMMENT '业务单号(关联进货单等)',
+ `del_flag` char(1) DEFAULT '0' COMMENT '删除标志(0存在 2删除)',
+ `create_by` varchar(64) DEFAULT '' COMMENT '创建者',
+ `create_time` datetime DEFAULT NULL COMMENT '变动时间',
+ `update_by` varchar(64) DEFAULT '' COMMENT '更新者',
+ `update_time` datetime DEFAULT NULL COMMENT '更新时间',
+ `remark` varchar(500) DEFAULT NULL COMMENT '备注',
+ PRIMARY KEY (`record_id`),
+ KEY `idx_whrecord_user_time` (`user_id`, `create_time`),
+ KEY `idx_whrecord_warehouse` (`warehouse_id`)
+) ENGINE=InnoDB AUTO_INCREMENT=100 DEFAULT CHARSET=utf8mb4 COMMENT='会员云仓变动流水表';
+```
+
+**回滚**:
+
+```sql
+DROP TABLE IF EXISTS `account_warehouse_record`;
+DROP TABLE IF EXISTS `account_purchase_order`;
+DROP TABLE IF EXISTS `account_sales_record`;
+```
+
+---
+
+## 五、流程设计
+
+### 5.1 查询销售统计(`MemberCenterServiceImpl.getSalesStatistics`)
+
+```
+STEP 1: 校验 userId 非空,为空抛「获取销售统计失败」
+STEP 2: 查 account_user,取 phone(脱敏)与 system_level(normalize 兜底 C);不存在抛异常
+STEP 3: 聚合三栏(单条 SQL,本月口径):销售额 / 利润额 / 销售单数
+STEP 4: 聚合并月进货额(account_purchase_order 本月 SUM(total_amount))
+STEP 5: 计算利润环比:上月利润额为 0 时环比返回 0,否则 round((本月-上月)/上月 × 100)
+STEP 6: 查当月销售额趋势(按 DAY(sale_time) 分组)→ 补全当月 1~N 号缺省日(amount=0)
+STEP 7: 查当月商品销售额比例(按 product_name 分组 SUM(amount),降序)
+STEP 8: 查当月销售用户排行(按 buyer 分组 SUM(amount),降序取前 10,手机号脱敏)
+STEP 9: 组装并返回 MemberSalesStatisticsVo
+```
+
+### 5.2 进货(`MemberCenterServiceImpl.purchase`,`@Transactional`)
+
+```
+STEP 1: 校验 userId / warehouseId / qty(qty 必须 > 0)
+STEP 2: 按 warehouseId 查 account_warehouse(del_flag='0');为空或 user_id != 当前会员 → 抛「商品不存在或不属于当前会员」(防水平越权)
+STEP 3: 生成唯一进货单号 order_no(PG + 时间戳 + 随机)
+STEP 4: 落进货单(status='1' 已入库,purchase_time=sysdate)
+STEP 5: 更新库存 stock_qty = stock_qty + qty(SQL 内自增,避免读改写竞态)
+STEP 6: 追加云仓变动流水(change_type='1',change_qty=+qty,after_qty=自增后库存,biz_no=order_no)
+STEP 7: 提交事务
+```
+
+### 5.3 仓库列表 / 变动记录 / 进货订单
+
+均为只读查询,按当前会员过滤(`user_id = 当前会员`),记录 / 订单按时间倒序分页;列表接口低库存标记在 Service 组装(`stockQty < minStockQty`)。
+
+---
+
+## 六、枚举与状态
+
+| 名称 | 取值 | 说明 |
+| --- | --- | --- |
+| 会员等级 `memberLevel`(源 `system_level`) | `C` / `L1` / `L2` / `L3` / `L4` | 复用 `MemberLevelEnum` |
+| 云仓变动类型 `changeType` | `1` 进货入库 / `2` 销售出库 / `3` 库存调整 | 新增 `WarehouseChangeTypeEnum`(`code` + `desc` + `getByCode`) |
+| 进货单状态 `status` | `1` 已入库 | 新增 `PurchaseOrderStatusEnum`(`code` + `desc` + `getByCode`) |
+| 删除标志 `delFlag` | `0` 存在 / `2` 删除 | 复用现有语义 |
+
+---
+
+## 七、影响范围与文件清单
+
+### 新增(ruoyi-user)
+
+| 文件 | 说明 |
+| --- | --- |
+| `domain/AccountSalesRecord.java` | 销售流水实体 |
+| `domain/AccountPurchaseOrder.java` | 进货单实体 |
+| `domain/AccountWarehouseRecord.java` | 云仓变动流水实体 |
+| `domain/dto/MemberPurchaseDto.java` | 进货入参 |
+| `domain/vo/MemberSalesStatisticsVo.java` | 销售统计聚合出参 |
+| `domain/vo/SalesTrendItemVo.java` | 销售额趋势项 |
+| `domain/vo/ProductSalesRatioVo.java` | 商品销售额比例项 |
+| `domain/vo/SalesUserRankingVo.java` | 销售用户排行项 |
+| `domain/vo/MemberWarehouseVo.java` | 云仓列表项(含低库存标记) |
+| `domain/vo/WarehouseRecordVo.java` | 云仓变动记录项 |
+| `domain/vo/PurchaseOrderVo.java` | 进货订单项 |
+| `enums/WarehouseChangeTypeEnum.java` | 云仓变动类型枚举 |
+| `enums/PurchaseOrderStatusEnum.java` | 进货单状态枚举 |
+| `mapper/AccountSalesRecordMapper.java` + XML | 销售流水聚合查询 |
+| `mapper/AccountPurchaseOrderMapper.java` + XML | 进货单落库 + 聚合 + 分页 |
+| `mapper/AccountWarehouseRecordMapper.java` + XML | 变动流水落库 + 分页 |
+| `service/IAccountMemberCenterService.java` + `service/impl/AccountMemberCenterServiceImpl.java` | 会员中心业务 |
+| `controller/AccountMemberCenterController.java` | C 端 5 个接口 |
+
+### 修改
+
+| 文件 | 改动 |
+| --- | --- |
+| `mapper/AccountWarehouseMapper.java` + XML | 新增 `selectWarehouseById`(按主键查,含 `del_flag='0'`)与 `increaseStockQty`(`stock_qty = stock_qty + #{qty}` 原子自增) |
+| `sql/hugray_account_member_center_20261014.sql` | 新增三张表 DDL(变更 + 回滚) |
+| 前端 `hugray-user-miniapp/api/index.js` | 新增 5 个会员中心接口方法 |
+| 前端 `hugray-user-miniapp/pages.json` | 注册 `pages/member/index` 路由(custom 导航) |
+| 前端 `hugray-user-miniapp/pages/profile/index.vue` | 菜单 `member` 由「敬请期待」改为跳转会员中心页 |
+| 前端 `hugray-user-miniapp/pages/member/index.vue` | 新增会员中心页(销售统计 / 我的仓库) |
+
+> 网关无需改动:`/miniapp/account/**` → `ruoyi-user` 路由已存在。
+
+---
+
+## 八、边界与异常清单
+
+| # | 场景 | 处理 |
+| --- | --- | --- |
+| 1 | 未登录访问 | 网关 `AuthFilter` 返回 401,前端跳登录页;后端不做分支 |
+| 2 | `user_id` 头缺失/为空 | 服务内兜底判空,抛业务异常(不 NPE) |
+| 3 | 会员不存在 / 已逻辑删除 | 抛业务异常 |
+| 4 | 进货 `warehouseId` 不属于当前会员 | 抛「商品不存在或不属于当前会员」(防水平越权) |
+| 5 | 进货 `qty` ≤ 0 | 抛「进货数量必须大于 0」 |
+| 6 | 当月无销售流水 | 三栏为 0,趋势全为 0,比例/排行为空数组 |
+| 7 | 上月利润为 0 | 环比返回 0(不抛除零) |
+| 8 | 排行购买人手机号 | 下发**脱敏**值(`138****4561`) |
+| 9 | 进货并发 | 库存用 SQL 原子自增 `stock_qty = stock_qty + #{qty}`,避免读改写竞态 |
+| 10 | 进货单号重复 | `uk_purchase_order_no` 唯一索引兜底,生成时已带随机段 |
+
+---
+
+## 九、规范符合性说明
+
+- 分层:Controller 只取身份 + 转发;业务在 Service;SQL 在 Mapper;Controller 不直接调 Mapper。
+- 命名:`getSalesStatistics` / `purchase` / `increaseStockQty` 等见名知意;Entity / DTO / VO 分离。
+- 注释:实体类字段注释(取值与单位)、Javadoc、核心流程 `// STEP x:` 注释。
+- 魔法值:变动类型、进货单状态收口枚举;云仓变动 `change_qty` 正负语义在 Service 注释说明。
+- 异常:统一 `ServiceException` + 全局处理器;不散落 try-catch。
+- 数据库:字段显式长度 + COMMENT;聚合列有索引;无 `SELECT *`;无 `${}` 拼接;无循环查库。
+- 幂等:进货单 `order_no` 唯一索引;库存原子自增。
+- 单测:`getSalesStatistics`(无流水 / 有流水 / 上月为 0)、`purchase`(正常 / 非本人 / qty≤0)补单测,Mockito Mock Mapper。
+- 说明(与现有工程一致优先):`account_*` 系列主键沿用自增(与同库既有表一致),未采用雪花 ID,作为已知项记录。
+
+---
+
+## 十、已确认决策
+
+1. **销售统计数据来源**:**已定** —— 新建简化销售流水模型 `account_sales_record`,本月三栏 / 趋势 / 比例 / 排行实时聚合。
+2. **进货业务口径**:**已定** —— 真实进货:生成进货单 `account_purchase_order` + 增加 `account_warehouse.stock_qty` + 追加变动流水 `account_warehouse_record`。
+3. **金额单位**:**已定** —— 接口返回分(`long`),前端格式化两位小数。
+4. **`account_user` 统计列**:**已定** —— 本期不回填,页面数据一律由流水表实时聚合(避免双写口径不一致)。
+5. **当月利润口径**:**已定** —— `SUM(account_sales_record.profit_amount)`;写入方(订单模块)就绪前无流水时为 0。
+6. **等级详情**:本期为前端本地展示(等级说明),不新增后端接口。
diff --git a/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/controller/AccountMemberController.java b/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/controller/AccountMemberController.java
new file mode 100644
index 0000000..4ef0cc9
--- /dev/null
+++ b/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/controller/AccountMemberController.java
@@ -0,0 +1,110 @@
+package com.ruoyi.user.controller;
+
+import java.util.List;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+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.core.web.controller.BaseController;
+import com.ruoyi.common.core.web.page.TableDataInfo;
+import com.ruoyi.common.security.utils.SecurityUtils;
+import com.ruoyi.user.domain.dto.MemberPurchaseDto;
+import com.ruoyi.user.domain.vo.MemberSalesStatisticsVo;
+import com.ruoyi.user.domain.vo.MemberWarehouseVo;
+import com.ruoyi.user.domain.vo.PurchaseOrderVo;
+import com.ruoyi.user.domain.vo.WarehouseRecordVo;
+import com.ruoyi.user.service.IAccountMemberCenterService;
+
+/**
+ * C端会员 个人中心-会员中心接口
+ *
+ * 网关前缀 /miniapp/account 由 StripPrefix=2 剥掉,故此处映射为 /user/member,
+ * 外部实际路径为 /miniapp/account/user/member/**。
+ *
+ * 会员身份取自网关注入的登录态(SecurityUtils.getUserId),不接受前端传 userId,
+ * 天然防止水平越权;未登录请求由网关直接以 401 拦截,故此处不处理未登录分支。
+ *
+ * @author hugray
+ */
+@RestController
+@RequestMapping("/user/member")
+public class AccountMemberController extends BaseController
+{
+ @Autowired
+ private IAccountMemberCenterService accountMemberCenterService;
+
+ /**
+ * 查询当前登录会员的销售统计
+ *
+ * 返回当月销售额 / 进货额 / 利润(单位分)、利润环比、销售额趋势、商品销售额比例与销售用户排行。
+ *
+ * @return 销售统计聚合对象
+ */
+ @GetMapping("/sales-statistics")
+ public R<MemberSalesStatisticsVo> salesStatistics()
+ {
+ return R.ok(accountMemberCenterService.getSalesStatistics(SecurityUtils.getUserId()));
+ }
+
+ /**
+ * 查询当前登录会员的云仓列表
+ *
+ * 含当前库存、最低需求库存与低库存标记;商品图片本期为空,由前端使用占位图。
+ *
+ * @return 云仓列表
+ */
+ @GetMapping("/warehouses")
+ public R<List<MemberWarehouseVo>> warehouses()
+ {
+ return R.ok(accountMemberCenterService.getWarehouseList(SecurityUtils.getUserId()));
+ }
+
+ /**
+ * 进货:生成进货单并增加对应云仓库存
+ *
+ * 云仓归属由服务端校验(非本会员商品拒绝),库存原子自增,同一事务内追加变动流水。
+ *
+ * @param dto 进货入参(warehouseId、qty 必填)
+ * @return 操作结果
+ */
+ @PostMapping("/purchase")
+ public R<Void> purchase(@Validated @RequestBody MemberPurchaseDto dto)
+ {
+ accountMemberCenterService.purchase(SecurityUtils.getUserId(), dto);
+ return R.ok();
+ }
+
+ /**
+ * 分页查询当前登录会员的云仓变动记录
+ *
+ * 分页参数 pageNum 默认 1、pageSize 默认 10,按变动时间倒序。
+ *
+ * @return 分页数据(rows 为变动记录,total 为总数)
+ */
+ @GetMapping("/warehouse-records")
+ public TableDataInfo warehouseRecords()
+ {
+ startPage();
+ List<WarehouseRecordVo> list = accountMemberCenterService.getWarehouseRecordList(SecurityUtils.getUserId());
+ return getDataTable(list);
+ }
+
+ /**
+ * 分页查询当前登录会员的进货订单
+ *
+ * 分页参数 pageNum 默认 1、pageSize 默认 10,按下单时间倒序。
+ *
+ * @return 分页数据(rows 为进货订单,total 为总数)
+ */
+ @GetMapping("/purchase-orders")
+ public TableDataInfo purchaseOrders()
+ {
+ startPage();
+ List<PurchaseOrderVo> list = accountMemberCenterService.getPurchaseOrderList(SecurityUtils.getUserId());
+ return getDataTable(list);
+ }
+}
diff --git a/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/domain/AccountPurchaseOrder.java b/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/domain/AccountPurchaseOrder.java
new file mode 100644
index 0000000..13f39df
--- /dev/null
+++ b/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/domain/AccountPurchaseOrder.java
@@ -0,0 +1,186 @@
+package com.ruoyi.user.domain;
+
+import java.util.Date;
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.ruoyi.common.core.web.domain.BaseEntity;
+
+/**
+ * 会员进货单 account_purchase_order
+ *
+ * 一条进货单 = 一次进货入库;落单同时增加 account_warehouse.stock_qty 并追加云仓变动流水。
+ * 当月进货额由本表按 purchase_time 实时聚合。
+ *
+ * @author hugray
+ */
+public class AccountPurchaseOrder extends BaseEntity
+{
+ private static final long serialVersionUID = 1L;
+
+ /** 进货单ID */
+ private Long orderId;
+
+ /** 进货单号(幂等唯一,服务端生成) */
+ private String orderNo;
+
+ /** 进货会员ID(account_user.user_id) */
+ private Long userId;
+
+ /** 云仓行ID(account_warehouse.id,快照) */
+ private Long warehouseId;
+
+ /** 商品名称(快照) */
+ private String productName;
+
+ /** 进货数量 */
+ private Long qty;
+
+ /** 进货单价(单位:分) */
+ private Long unitPrice;
+
+ /** 进货金额(单位:分)= qty × unitPrice */
+ private Long totalAmount;
+
+ /** 状态(1已入库,见 PurchaseOrderStatusEnum) */
+ private String status;
+
+ /** 进货时间(聚合口径) */
+ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+ private Date purchaseTime;
+
+ /** 删除标志(0存在 2删除) */
+ private String delFlag;
+
+ public Long getOrderId()
+ {
+ return orderId;
+ }
+
+ public void setOrderId(Long orderId)
+ {
+ this.orderId = orderId;
+ }
+
+ public String getOrderNo()
+ {
+ return orderNo;
+ }
+
+ public void setOrderNo(String orderNo)
+ {
+ this.orderNo = orderNo;
+ }
+
+ public Long getUserId()
+ {
+ return userId;
+ }
+
+ public void setUserId(Long userId)
+ {
+ this.userId = userId;
+ }
+
+ public Long getWarehouseId()
+ {
+ return warehouseId;
+ }
+
+ public void setWarehouseId(Long warehouseId)
+ {
+ this.warehouseId = warehouseId;
+ }
+
+ public String getProductName()
+ {
+ return productName;
+ }
+
+ public void setProductName(String productName)
+ {
+ this.productName = productName;
+ }
+
+ public Long getQty()
+ {
+ return qty;
+ }
+
+ public void setQty(Long qty)
+ {
+ this.qty = qty;
+ }
+
+ public Long getUnitPrice()
+ {
+ return unitPrice;
+ }
+
+ public void setUnitPrice(Long unitPrice)
+ {
+ this.unitPrice = unitPrice;
+ }
+
+ public Long getTotalAmount()
+ {
+ return totalAmount;
+ }
+
+ public void setTotalAmount(Long totalAmount)
+ {
+ this.totalAmount = totalAmount;
+ }
+
+ public String getStatus()
+ {
+ return status;
+ }
+
+ public void setStatus(String status)
+ {
+ this.status = status;
+ }
+
+ public Date getPurchaseTime()
+ {
+ return purchaseTime;
+ }
+
+ public void setPurchaseTime(Date purchaseTime)
+ {
+ this.purchaseTime = purchaseTime;
+ }
+
+ public String getDelFlag()
+ {
+ return delFlag;
+ }
+
+ public void setDelFlag(String delFlag)
+ {
+ this.delFlag = delFlag;
+ }
+
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE)
+ .append("orderId", getOrderId())
+ .append("orderNo", getOrderNo())
+ .append("userId", getUserId())
+ .append("warehouseId", getWarehouseId())
+ .append("productName", getProductName())
+ .append("qty", getQty())
+ .append("unitPrice", getUnitPrice())
+ .append("totalAmount", getTotalAmount())
+ .append("status", getStatus())
+ .append("purchaseTime", getPurchaseTime())
+ .append("delFlag", getDelFlag())
+ .append("createBy", getCreateBy())
+ .append("createTime", getCreateTime())
+ .append("updateBy", getUpdateBy())
+ .append("updateTime", getUpdateTime())
+ .append("remark", getRemark())
+ .toString();
+ }
+}
diff --git a/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/domain/AccountSalesRecord.java b/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/domain/AccountSalesRecord.java
new file mode 100644
index 0000000..80850d7
--- /dev/null
+++ b/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/domain/AccountSalesRecord.java
@@ -0,0 +1,186 @@
+package com.ruoyi.user.domain;
+
+import java.util.Date;
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.ruoyi.common.core.web.domain.BaseEntity;
+
+/**
+ * 会员销售流水 account_sales_record
+ *
+ * 一条流水 = 一次销售事件。当月销售额 / 利润额 / 销售单数、销售额趋势、商品销售额比例、
+ * 销售用户排行均由本表按 sale_time 实时聚合。
+ *
+ * @author hugray
+ */
+public class AccountSalesRecord extends BaseEntity
+{
+ private static final long serialVersionUID = 1L;
+
+ /** 销售流水ID */
+ private Long recordId;
+
+ /** 销售会员ID(account_user.user_id) */
+ private Long sellerId;
+
+ /** 购买会员ID(account_user.user_id,散客为空) */
+ private Long buyerId;
+
+ /** 购买人昵称(销售排行展示用) */
+ private String buyerName;
+
+ /** 购买人手机号(下发前脱敏) */
+ private String buyerPhone;
+
+ /** 商品名称(比例图 / 排行展示用) */
+ private String productName;
+
+ /** 销售数量 */
+ private Long qty;
+
+ /** 销售额(单位:分) */
+ private Long amount;
+
+ /** 利润额(单位:分,可为负) */
+ private Long profitAmount;
+
+ /** 销售时间(聚合口径) */
+ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+ private Date saleTime;
+
+ /** 删除标志(0存在 2删除) */
+ private String delFlag;
+
+ public Long getRecordId()
+ {
+ return recordId;
+ }
+
+ public void setRecordId(Long recordId)
+ {
+ this.recordId = recordId;
+ }
+
+ public Long getSellerId()
+ {
+ return sellerId;
+ }
+
+ public void setSellerId(Long sellerId)
+ {
+ this.sellerId = sellerId;
+ }
+
+ public Long getBuyerId()
+ {
+ return buyerId;
+ }
+
+ public void setBuyerId(Long buyerId)
+ {
+ this.buyerId = buyerId;
+ }
+
+ public String getBuyerName()
+ {
+ return buyerName;
+ }
+
+ public void setBuyerName(String buyerName)
+ {
+ this.buyerName = buyerName;
+ }
+
+ public String getBuyerPhone()
+ {
+ return buyerPhone;
+ }
+
+ public void setBuyerPhone(String buyerPhone)
+ {
+ this.buyerPhone = buyerPhone;
+ }
+
+ public String getProductName()
+ {
+ return productName;
+ }
+
+ public void setProductName(String productName)
+ {
+ this.productName = productName;
+ }
+
+ public Long getQty()
+ {
+ return qty;
+ }
+
+ public void setQty(Long qty)
+ {
+ this.qty = qty;
+ }
+
+ public Long getAmount()
+ {
+ return amount;
+ }
+
+ public void setAmount(Long amount)
+ {
+ this.amount = amount;
+ }
+
+ public Long getProfitAmount()
+ {
+ return profitAmount;
+ }
+
+ public void setProfitAmount(Long profitAmount)
+ {
+ this.profitAmount = profitAmount;
+ }
+
+ public Date getSaleTime()
+ {
+ return saleTime;
+ }
+
+ public void setSaleTime(Date saleTime)
+ {
+ this.saleTime = saleTime;
+ }
+
+ public String getDelFlag()
+ {
+ return delFlag;
+ }
+
+ public void setDelFlag(String delFlag)
+ {
+ this.delFlag = delFlag;
+ }
+
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE)
+ .append("recordId", getRecordId())
+ .append("sellerId", getSellerId())
+ .append("buyerId", getBuyerId())
+ .append("buyerName", getBuyerName())
+ .append("buyerPhone", getBuyerPhone())
+ .append("productName", getProductName())
+ .append("qty", getQty())
+ .append("amount", getAmount())
+ .append("profitAmount", getProfitAmount())
+ .append("saleTime", getSaleTime())
+ .append("delFlag", getDelFlag())
+ .append("createBy", getCreateBy())
+ .append("createTime", getCreateTime())
+ .append("updateBy", getUpdateBy())
+ .append("updateTime", getUpdateTime())
+ .append("remark", getRemark())
+ .toString();
+ }
+}
diff --git a/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/domain/AccountWarehouseRecord.java b/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/domain/AccountWarehouseRecord.java
new file mode 100644
index 0000000..8a54102
--- /dev/null
+++ b/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/domain/AccountWarehouseRecord.java
@@ -0,0 +1,163 @@
+package com.ruoyi.user.domain;
+
+import java.util.Date;
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+import com.ruoyi.common.core.web.domain.BaseEntity;
+
+/**
+ * 会员云仓变动流水 account_warehouse_record
+ *
+ * 每次库存变动(进货入库 / 销售出库 / 库存调整)追加一条流水,供「仓库记录」倒序展示。
+ *
+ * @author hugray
+ */
+public class AccountWarehouseRecord extends BaseEntity
+{
+ private static final long serialVersionUID = 1L;
+
+ /** 流水ID */
+ private Long recordId;
+
+ /** 会员ID(account_user.user_id) */
+ private Long userId;
+
+ /** 云仓行ID(account_warehouse.id) */
+ private Long warehouseId;
+
+ /** 商品名称(快照) */
+ private String productName;
+
+ /** 变动类型(1进货入库 2销售出库 3库存调整,见 WarehouseChangeTypeEnum) */
+ private String changeType;
+
+ /** 变动数量(入库为正,出库为负) */
+ private Long changeQty;
+
+ /** 变动后库存 */
+ private Long afterQty;
+
+ /** 业务单号(关联进货单等,便于溯源) */
+ private String bizNo;
+
+ /** 删除标志(0存在 2删除) */
+ private String delFlag;
+
+ 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 getWarehouseId()
+ {
+ return warehouseId;
+ }
+
+ public void setWarehouseId(Long warehouseId)
+ {
+ this.warehouseId = warehouseId;
+ }
+
+ public String getProductName()
+ {
+ return productName;
+ }
+
+ public void setProductName(String productName)
+ {
+ this.productName = productName;
+ }
+
+ public String getChangeType()
+ {
+ return changeType;
+ }
+
+ public void setChangeType(String changeType)
+ {
+ this.changeType = changeType;
+ }
+
+ public Long getChangeQty()
+ {
+ return changeQty;
+ }
+
+ public void setChangeQty(Long changeQty)
+ {
+ this.changeQty = changeQty;
+ }
+
+ public Long getAfterQty()
+ {
+ return afterQty;
+ }
+
+ public void setAfterQty(Long afterQty)
+ {
+ this.afterQty = afterQty;
+ }
+
+ public String getBizNo()
+ {
+ return bizNo;
+ }
+
+ public void setBizNo(String bizNo)
+ {
+ this.bizNo = bizNo;
+ }
+
+ public String getDelFlag()
+ {
+ return delFlag;
+ }
+
+ public void setDelFlag(String delFlag)
+ {
+ this.delFlag = delFlag;
+ }
+
+ /**
+ * 变动时间:直接复用父类 createTime(流水创建即变动发生),此处仅做展示别名。
+ */
+ public Date getChangeTime()
+ {
+ return getCreateTime();
+ }
+
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE)
+ .append("recordId", getRecordId())
+ .append("userId", getUserId())
+ .append("warehouseId", getWarehouseId())
+ .append("productName", getProductName())
+ .append("changeType", getChangeType())
+ .append("changeQty", getChangeQty())
+ .append("afterQty", getAfterQty())
+ .append("bizNo", getBizNo())
+ .append("delFlag", getDelFlag())
+ .append("createBy", getCreateBy())
+ .append("createTime", getCreateTime())
+ .append("updateBy", getUpdateBy())
+ .append("updateTime", getUpdateTime())
+ .append("remark", getRemark())
+ .toString();
+ }
+}
diff --git a/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/domain/dto/MemberPurchaseDto.java b/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/domain/dto/MemberPurchaseDto.java
new file mode 100644
index 0000000..896a8e8
--- /dev/null
+++ b/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/domain/dto/MemberPurchaseDto.java
@@ -0,0 +1,54 @@
+package com.ruoyi.user.domain.dto;
+
+import jakarta.validation.constraints.NotNull;
+
+/**
+ * 会员进货入参
+ *
+ * 会员身份取自登录态,不接受前端传入 userId;warehouseId 须属于当前会员(服务端校验归属)。
+ *
+ * @author hugray
+ */
+public class MemberPurchaseDto
+{
+ /** 云仓行ID(account_warehouse.id,须属于当前会员) */
+ @NotNull(message = "进货商品不能为空")
+ private Long warehouseId;
+
+ /** 进货数量(正整数) */
+ @NotNull(message = "进货数量不能为空")
+ private Long qty;
+
+ /** 进货单价(单位:分;可选,缺省按 0 计) */
+ private Long unitPrice;
+
+ public Long getWarehouseId()
+ {
+ return warehouseId;
+ }
+
+ public void setWarehouseId(Long warehouseId)
+ {
+ this.warehouseId = warehouseId;
+ }
+
+ public Long getQty()
+ {
+ return qty;
+ }
+
+ public void setQty(Long qty)
+ {
+ this.qty = qty;
+ }
+
+ public Long getUnitPrice()
+ {
+ return unitPrice;
+ }
+
+ public void setUnitPrice(Long unitPrice)
+ {
+ this.unitPrice = unitPrice;
+ }
+}
diff --git a/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/domain/vo/MemberSalesStatisticsVo.java b/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/domain/vo/MemberSalesStatisticsVo.java
new file mode 100644
index 0000000..e74a6c5
--- /dev/null
+++ b/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/domain/vo/MemberSalesStatisticsVo.java
@@ -0,0 +1,144 @@
+package com.ruoyi.user.domain.vo;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 会员销售统计 VO(会员中心「销售统计」Tab 聚合出参)
+ *
+ * 金额单位一律「分」;无销售流水时三栏为 0、趋势全为 0、比例与排行为空数组。
+ *
+ * @author hugray
+ */
+public class MemberSalesStatisticsVo
+{
+ /** 当前会员手机号(脱敏,形如 136****6666) */
+ private String phone;
+
+ /** 会员等级(C / L1~L4) */
+ private String memberLevel;
+
+ /** 当月销售额(分)= SUM(account_sales_record.amount) */
+ private Long monthSalesAmount;
+
+ /** 当月进货额(分)= SUM(account_purchase_order.total_amount) */
+ private Long monthPurchaseAmount;
+
+ /** 当月利润额(分)= SUM(account_sales_record.profit_amount),可为负 */
+ private Long monthProfitAmount;
+
+ /** 当月销售单数 = COUNT(account_sales_record) */
+ private Integer monthSalesCount;
+
+ /** 当月利润环比上月提升百分比(无上月数据时为 0) */
+ private Integer profitGrowthRate;
+
+ /** 当月销售额趋势(按日补零,1 号到当日) */
+ private List<SalesTrendItemVo> salesTrend = new ArrayList<>();
+
+ /** 当月商品销售额比例 */
+ private List<ProductSalesRatioVo> productSalesRatio = new ArrayList<>();
+
+ /** 当月销售用户排行(Top 10) */
+ private List<SalesUserRankingVo> salesUserRanking = new ArrayList<>();
+
+ public String getPhone()
+ {
+ return phone;
+ }
+
+ public void setPhone(String phone)
+ {
+ this.phone = phone;
+ }
+
+ public String getMemberLevel()
+ {
+ return memberLevel;
+ }
+
+ public void setMemberLevel(String memberLevel)
+ {
+ this.memberLevel = memberLevel;
+ }
+
+ public Long getMonthSalesAmount()
+ {
+ return monthSalesAmount;
+ }
+
+ public void setMonthSalesAmount(Long monthSalesAmount)
+ {
+ this.monthSalesAmount = monthSalesAmount;
+ }
+
+ public Long getMonthPurchaseAmount()
+ {
+ return monthPurchaseAmount;
+ }
+
+ public void setMonthPurchaseAmount(Long monthPurchaseAmount)
+ {
+ this.monthPurchaseAmount = monthPurchaseAmount;
+ }
+
+ public Long getMonthProfitAmount()
+ {
+ return monthProfitAmount;
+ }
+
+ public void setMonthProfitAmount(Long monthProfitAmount)
+ {
+ this.monthProfitAmount = monthProfitAmount;
+ }
+
+ public Integer getMonthSalesCount()
+ {
+ return monthSalesCount;
+ }
+
+ public void setMonthSalesCount(Integer monthSalesCount)
+ {
+ this.monthSalesCount = monthSalesCount;
+ }
+
+ public Integer getProfitGrowthRate()
+ {
+ return profitGrowthRate;
+ }
+
+ public void setProfitGrowthRate(Integer profitGrowthRate)
+ {
+ this.profitGrowthRate = profitGrowthRate;
+ }
+
+ public List<SalesTrendItemVo> getSalesTrend()
+ {
+ return salesTrend;
+ }
+
+ public void setSalesTrend(List<SalesTrendItemVo> salesTrend)
+ {
+ this.salesTrend = salesTrend;
+ }
+
+ public List<ProductSalesRatioVo> getProductSalesRatio()
+ {
+ return productSalesRatio;
+ }
+
+ public void setProductSalesRatio(List<ProductSalesRatioVo> productSalesRatio)
+ {
+ this.productSalesRatio = productSalesRatio;
+ }
+
+ public List<SalesUserRankingVo> getSalesUserRanking()
+ {
+ return salesUserRanking;
+ }
+
+ public void setSalesUserRanking(List<SalesUserRankingVo> salesUserRanking)
+ {
+ this.salesUserRanking = salesUserRanking;
+ }
+}
diff --git a/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/domain/vo/MemberWarehouseVo.java b/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/domain/vo/MemberWarehouseVo.java
new file mode 100644
index 0000000..23df1ce
--- /dev/null
+++ b/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/domain/vo/MemberWarehouseVo.java
@@ -0,0 +1,89 @@
+package com.ruoyi.user.domain.vo;
+
+/**
+ * 我的仓库列表项 VO(会员中心「我的仓库」Tab)
+ *
+ * 商品图片列在 account_warehouse 不存在,本期恒为 null,由前端使用占位图。
+ *
+ * @author hugray
+ */
+public class MemberWarehouseVo
+{
+ /** 云仓行主键(进货时回传) */
+ private Long warehouseId;
+
+ /** 商品名称 */
+ private String productName;
+
+ /** 商品图片(本期恒为 null,前端用占位图) */
+ private String productImage;
+
+ /** 当前库存 */
+ private Long stockQty;
+
+ /** 最低需求库存 */
+ private Long minStockQty;
+
+ /** 低库存标记:stockQty < minStockQty 时为 true(前端标红 + 提示条) */
+ private Boolean lowStock;
+
+ public Long getWarehouseId()
+ {
+ return warehouseId;
+ }
+
+ public void setWarehouseId(Long warehouseId)
+ {
+ this.warehouseId = warehouseId;
+ }
+
+ public String getProductName()
+ {
+ return productName;
+ }
+
+ public void setProductName(String productName)
+ {
+ this.productName = productName;
+ }
+
+ public String getProductImage()
+ {
+ return productImage;
+ }
+
+ public void setProductImage(String productImage)
+ {
+ this.productImage = productImage;
+ }
+
+ public Long getStockQty()
+ {
+ return stockQty;
+ }
+
+ public void setStockQty(Long stockQty)
+ {
+ this.stockQty = stockQty;
+ }
+
+ public Long getMinStockQty()
+ {
+ return minStockQty;
+ }
+
+ public void setMinStockQty(Long minStockQty)
+ {
+ this.minStockQty = minStockQty;
+ }
+
+ public Boolean getLowStock()
+ {
+ return lowStock;
+ }
+
+ public void setLowStock(Boolean lowStock)
+ {
+ this.lowStock = lowStock;
+ }
+}
diff --git a/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/domain/vo/ProductSalesRatioVo.java b/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/domain/vo/ProductSalesRatioVo.java
new file mode 100644
index 0000000..8ab2a64
--- /dev/null
+++ b/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/domain/vo/ProductSalesRatioVo.java
@@ -0,0 +1,35 @@
+package com.ruoyi.user.domain.vo;
+
+/**
+ * 商品销售额比例项 VO(销售统计饼图数据点)
+ *
+ * @author hugray
+ */
+public class ProductSalesRatioVo
+{
+ /** 商品名称 */
+ private String productName;
+
+ /** 该商品当月销售额(分) */
+ private Long amount;
+
+ public String getProductName()
+ {
+ return productName;
+ }
+
+ public void setProductName(String productName)
+ {
+ this.productName = productName;
+ }
+
+ public Long getAmount()
+ {
+ return amount;
+ }
+
+ public void setAmount(Long amount)
+ {
+ this.amount = amount;
+ }
+}
diff --git a/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/domain/vo/PurchaseOrderVo.java b/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/domain/vo/PurchaseOrderVo.java
new file mode 100644
index 0000000..33c6c16
--- /dev/null
+++ b/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/domain/vo/PurchaseOrderVo.java
@@ -0,0 +1,130 @@
+package com.ruoyi.user.domain.vo;
+
+import java.util.Date;
+import com.fasterxml.jackson.annotation.JsonFormat;
+
+/**
+ * 进货订单项 VO(「进货订单」列表)
+ *
+ * @author hugray
+ */
+public class PurchaseOrderVo
+{
+ /** 进货单主键 */
+ private Long orderId;
+
+ /** 进货单号 */
+ private String orderNo;
+
+ /** 商品名称 */
+ private String productName;
+
+ /** 进货数量 */
+ private Long qty;
+
+ /** 进货单价(分) */
+ private Long unitPrice;
+
+ /** 进货金额(分)= qty × unitPrice */
+ private Long totalAmount;
+
+ /** 状态(1已入库) */
+ private String status;
+
+ /** 状态中文名 */
+ private String statusName;
+
+ /** 下单时间 */
+ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+ private Date createTime;
+
+ public Long getOrderId()
+ {
+ return orderId;
+ }
+
+ public void setOrderId(Long orderId)
+ {
+ this.orderId = orderId;
+ }
+
+ public String getOrderNo()
+ {
+ return orderNo;
+ }
+
+ public void setOrderNo(String orderNo)
+ {
+ this.orderNo = orderNo;
+ }
+
+ public String getProductName()
+ {
+ return productName;
+ }
+
+ public void setProductName(String productName)
+ {
+ this.productName = productName;
+ }
+
+ public Long getQty()
+ {
+ return qty;
+ }
+
+ public void setQty(Long qty)
+ {
+ this.qty = qty;
+ }
+
+ public Long getUnitPrice()
+ {
+ return unitPrice;
+ }
+
+ public void setUnitPrice(Long unitPrice)
+ {
+ this.unitPrice = unitPrice;
+ }
+
+ public Long getTotalAmount()
+ {
+ return totalAmount;
+ }
+
+ public void setTotalAmount(Long totalAmount)
+ {
+ this.totalAmount = totalAmount;
+ }
+
+ public String getStatus()
+ {
+ return status;
+ }
+
+ public void setStatus(String status)
+ {
+ this.status = status;
+ }
+
+ public String getStatusName()
+ {
+ return statusName;
+ }
+
+ public void setStatusName(String statusName)
+ {
+ this.statusName = statusName;
+ }
+
+ public Date getCreateTime()
+ {
+ return createTime;
+ }
+
+ public void setCreateTime(Date createTime)
+ {
+ this.createTime = createTime;
+ }
+}
diff --git a/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/domain/vo/SalesMonthSummaryVo.java b/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/domain/vo/SalesMonthSummaryVo.java
new file mode 100644
index 0000000..fb6ada8
--- /dev/null
+++ b/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/domain/vo/SalesMonthSummaryVo.java
@@ -0,0 +1,63 @@
+package com.ruoyi.user.domain.vo;
+
+/**
+ * 销售月汇总 VO(服务端内部聚合载体,不直接对外返回)
+ *
+ * 单条 SQL 同时聚合本月销售额 / 利润额 / 单数与上月利润额,避免多次数据库往返。
+ *
+ * @author hugray
+ */
+public class SalesMonthSummaryVo
+{
+ /** 本月销售额(分) */
+ private Long salesAmount;
+
+ /** 本月利润额(分,可为负) */
+ private Long profitAmount;
+
+ /** 本月销售单数 */
+ private Integer salesCount;
+
+ /** 上月利润额(分,用于环比计算) */
+ private Long prevMonthProfit;
+
+ public Long getSalesAmount()
+ {
+ return salesAmount;
+ }
+
+ public void setSalesAmount(Long salesAmount)
+ {
+ this.salesAmount = salesAmount;
+ }
+
+ public Long getProfitAmount()
+ {
+ return profitAmount;
+ }
+
+ public void setProfitAmount(Long profitAmount)
+ {
+ this.profitAmount = profitAmount;
+ }
+
+ public Integer getSalesCount()
+ {
+ return salesCount;
+ }
+
+ public void setSalesCount(Integer salesCount)
+ {
+ this.salesCount = salesCount;
+ }
+
+ public Long getPrevMonthProfit()
+ {
+ return prevMonthProfit;
+ }
+
+ public void setPrevMonthProfit(Long prevMonthProfit)
+ {
+ this.prevMonthProfit = prevMonthProfit;
+ }
+}
diff --git a/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/domain/vo/SalesTrendItemVo.java b/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/domain/vo/SalesTrendItemVo.java
new file mode 100644
index 0000000..26a7bcd
--- /dev/null
+++ b/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/domain/vo/SalesTrendItemVo.java
@@ -0,0 +1,58 @@
+package com.ruoyi.user.domain.vo;
+
+/**
+ * 销售额趋势项 VO(销售统计折线图数据点)
+ *
+ * @author hugray
+ */
+public class SalesTrendItemVo
+{
+ /** 日标签(对外展示,形如 1号 / 5号) */
+ private String day;
+
+ /** 月内第几天(1~31,仅用于服务端补零对齐,不对外使用) */
+ private Integer dayOfMonth;
+
+ /** 当日销售额(分) */
+ private Long amount;
+
+ public SalesTrendItemVo()
+ {
+ }
+
+ public SalesTrendItemVo(String day, Long amount)
+ {
+ this.day = day;
+ this.amount = amount;
+ }
+
+ public String getDay()
+ {
+ return day;
+ }
+
+ public void setDay(String day)
+ {
+ this.day = day;
+ }
+
+ public Integer getDayOfMonth()
+ {
+ return dayOfMonth;
+ }
+
+ public void setDayOfMonth(Integer dayOfMonth)
+ {
+ this.dayOfMonth = dayOfMonth;
+ }
+
+ public Long getAmount()
+ {
+ return amount;
+ }
+
+ public void setAmount(Long amount)
+ {
+ this.amount = amount;
+ }
+}
diff --git a/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/domain/vo/SalesUserRankingVo.java b/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/domain/vo/SalesUserRankingVo.java
new file mode 100644
index 0000000..8499d36
--- /dev/null
+++ b/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/domain/vo/SalesUserRankingVo.java
@@ -0,0 +1,63 @@
+package com.ruoyi.user.domain.vo;
+
+/**
+ * 销售用户排行项 VO(销售统计排行列表)
+ *
+ * 手机号下发前已脱敏;昵称可为空,前端回退展示脱敏手机号。
+ *
+ * @author hugray
+ */
+public class SalesUserRankingVo
+{
+ /** 名次(从 1 开始) */
+ private Integer rank;
+
+ /** 购买人昵称(可为空) */
+ private String nickname;
+
+ /** 购买人手机号(脱敏,形如 138****4561) */
+ private String phone;
+
+ /** 该用户当月购买额(分) */
+ private Long amount;
+
+ public Integer getRank()
+ {
+ return rank;
+ }
+
+ public void setRank(Integer rank)
+ {
+ this.rank = rank;
+ }
+
+ 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 Long getAmount()
+ {
+ return amount;
+ }
+
+ public void setAmount(Long amount)
+ {
+ this.amount = amount;
+ }
+}
diff --git a/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/domain/vo/WarehouseRecordVo.java b/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/domain/vo/WarehouseRecordVo.java
new file mode 100644
index 0000000..ab26840
--- /dev/null
+++ b/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/domain/vo/WarehouseRecordVo.java
@@ -0,0 +1,117 @@
+package com.ruoyi.user.domain.vo;
+
+import java.util.Date;
+import com.fasterxml.jackson.annotation.JsonFormat;
+
+/**
+ * 云仓变动记录项 VO(「仓库记录」弹层列表)
+ *
+ * @author hugray
+ */
+public class WarehouseRecordVo
+{
+ /** 流水主键 */
+ private Long recordId;
+
+ /** 商品名称(变动时快照) */
+ private String productName;
+
+ /** 变动类型(1进货入库 2销售出库 3库存调整) */
+ private String changeType;
+
+ /** 变动类型中文名(由枚举下发) */
+ private String changeTypeName;
+
+ /** 变动数量(入库为正,出库为负) */
+ private Long changeQty;
+
+ /** 变动后库存 */
+ private Long afterQty;
+
+ /** 备注(进货单号等) */
+ private String remark;
+
+ /** 变动时间 */
+ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+ private Date createTime;
+
+ public Long getRecordId()
+ {
+ return recordId;
+ }
+
+ public void setRecordId(Long recordId)
+ {
+ this.recordId = recordId;
+ }
+
+ public String getProductName()
+ {
+ return productName;
+ }
+
+ public void setProductName(String productName)
+ {
+ this.productName = productName;
+ }
+
+ public String getChangeType()
+ {
+ return changeType;
+ }
+
+ public void setChangeType(String changeType)
+ {
+ this.changeType = changeType;
+ }
+
+ public String getChangeTypeName()
+ {
+ return changeTypeName;
+ }
+
+ public void setChangeTypeName(String changeTypeName)
+ {
+ this.changeTypeName = changeTypeName;
+ }
+
+ public Long getChangeQty()
+ {
+ return changeQty;
+ }
+
+ public void setChangeQty(Long changeQty)
+ {
+ this.changeQty = changeQty;
+ }
+
+ public Long getAfterQty()
+ {
+ return afterQty;
+ }
+
+ public void setAfterQty(Long afterQty)
+ {
+ this.afterQty = afterQty;
+ }
+
+ 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;
+ }
+}
diff --git a/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/enums/PurchaseOrderStatusEnum.java b/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/enums/PurchaseOrderStatusEnum.java
new file mode 100644
index 0000000..340bd35
--- /dev/null
+++ b/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/enums/PurchaseOrderStatusEnum.java
@@ -0,0 +1,66 @@
+package com.ruoyi.user.enums;
+
+/**
+ * 进货单状态枚举
+ *
+ * 与 account_purchase_order.status 字段一一对应。本期进货即入库,仅一种终态。
+ *
+ * @author hugray
+ */
+public enum PurchaseOrderStatusEnum
+{
+ /** 已入库 */
+ STOCKED("1", "已入库");
+
+ /** 状态编码(落库值) */
+ private final String code;
+
+ /** 状态描述 */
+ private final String desc;
+
+ PurchaseOrderStatusEnum(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 PurchaseOrderStatusEnum getByCode(String code)
+ {
+ for (PurchaseOrderStatusEnum status : values())
+ {
+ if (status.code.equals(code))
+ {
+ return status;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * 按编码取描述,未命中返回空字符串,避免前端展示 null
+ *
+ * @param code 状态编码
+ * @return 状态描述
+ */
+ public static String descOf(String code)
+ {
+ PurchaseOrderStatusEnum status = getByCode(code);
+ return status == null ? "" : status.getDesc();
+ }
+}
diff --git a/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/enums/WarehouseChangeTypeEnum.java b/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/enums/WarehouseChangeTypeEnum.java
new file mode 100644
index 0000000..740af6c
--- /dev/null
+++ b/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/enums/WarehouseChangeTypeEnum.java
@@ -0,0 +1,70 @@
+package com.ruoyi.user.enums;
+
+/**
+ * 云仓变动类型枚举
+ *
+ * 与 account_warehouse_record.change_type 字段一一对应。
+ *
+ * @author hugray
+ */
+public enum WarehouseChangeTypeEnum
+{
+ /** 进货入库(数量为正) */
+ PURCHASE_IN("1", "进货入库"),
+ /** 销售出库(数量为负) */
+ SALE_OUT("2", "销售出库"),
+ /** 库存调整(可正可负) */
+ ADJUST("3", "库存调整");
+
+ /** 类型编码(落库值) */
+ private final String code;
+
+ /** 类型描述 */
+ private final String desc;
+
+ WarehouseChangeTypeEnum(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 WarehouseChangeTypeEnum getByCode(String code)
+ {
+ for (WarehouseChangeTypeEnum type : values())
+ {
+ if (type.code.equals(code))
+ {
+ return type;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * 按编码取描述,未命中返回空字符串,避免前端展示 null
+ *
+ * @param code 类型编码
+ * @return 类型描述
+ */
+ public static String descOf(String code)
+ {
+ WarehouseChangeTypeEnum type = getByCode(code);
+ return type == null ? "" : type.getDesc();
+ }
+}
diff --git a/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/mapper/AccountPurchaseOrderMapper.java b/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/mapper/AccountPurchaseOrderMapper.java
new file mode 100644
index 0000000..0d658bf
--- /dev/null
+++ b/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/mapper/AccountPurchaseOrderMapper.java
@@ -0,0 +1,42 @@
+package com.ruoyi.user.mapper;
+
+import java.util.Date;
+import java.util.List;
+import org.apache.ibatis.annotations.Param;
+import com.ruoyi.user.domain.AccountPurchaseOrder;
+
+/**
+ * 会员进货单 数据层
+ *
+ * @author hugray
+ */
+public interface AccountPurchaseOrderMapper
+{
+ /**
+ * 新增进货单(落单即入库,order_no 唯一索引兜底幂等)
+ *
+ * @param accountPurchaseOrder 进货单
+ * @return 影响行数
+ */
+ public int insertPurchaseOrder(AccountPurchaseOrder accountPurchaseOrder);
+
+ /**
+ * 聚合本月进货额
+ *
+ * @param userId 会员ID
+ * @param monthStart 本月起始(含)
+ * @param nextMonthStart 次月起始(不含)
+ * @return 本月进货额(单位:分;无进货时为 0)
+ */
+ public Long selectMonthPurchaseAmount(@Param("userId") Long userId,
+ @Param("monthStart") Date monthStart,
+ @Param("nextMonthStart") Date nextMonthStart);
+
+ /**
+ * 查询指定会员的进货单列表(按进货时间倒序,配合 startPage 分页)
+ *
+ * @param userId 会员ID
+ * @return 进货单列表
+ */
+ public List<AccountPurchaseOrder> selectPurchaseOrderList(@Param("userId") Long userId);
+}
diff --git a/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/mapper/AccountSalesRecordMapper.java b/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/mapper/AccountSalesRecordMapper.java
new file mode 100644
index 0000000..e6fd7c2
--- /dev/null
+++ b/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/mapper/AccountSalesRecordMapper.java
@@ -0,0 +1,72 @@
+package com.ruoyi.user.mapper;
+
+import java.util.Date;
+import java.util.List;
+import org.apache.ibatis.annotations.Param;
+import com.ruoyi.user.domain.vo.ProductSalesRatioVo;
+import com.ruoyi.user.domain.vo.SalesMonthSummaryVo;
+import com.ruoyi.user.domain.vo.SalesTrendItemVo;
+import com.ruoyi.user.domain.vo.SalesUserRankingVo;
+
+/**
+ * 会员销售流水 数据层
+ *
+ * 全部为聚合查询,命中 idx_sales_seller_time(seller_id, sale_time)联合索引;
+ * 时间区间由 Service 传入月初 / 次月初 / 上月初,避免 SQL 内做日期方言运算。
+ *
+ * @author hugray
+ */
+public interface AccountSalesRecordMapper
+{
+ /**
+ * 聚合本月销售额 / 利润额 / 单数与上月利润额(单条 SQL 取回,避免多次数据库往返)
+ *
+ * @param sellerId 销售会员ID
+ * @param monthStart 本月起始(含)
+ * @param nextMonthStart 次月起始(不含,用于划定本月上界)
+ * @param prevMonthStart 上月起始(含,用于上月利润环比口径)
+ * @return 月汇总(单位:分;无流水时各栏为 0)
+ */
+ public SalesMonthSummaryVo selectMonthSummary(@Param("sellerId") Long sellerId,
+ @Param("monthStart") Date monthStart,
+ @Param("nextMonthStart") Date nextMonthStart,
+ @Param("prevMonthStart") Date prevMonthStart);
+
+ /**
+ * 按日聚合本月销售额(趋势折线图原始数据,缺省日由 Service 补零)
+ *
+ * @param sellerId 销售会员ID
+ * @param monthStart 本月起始(含)
+ * @param nextMonthStart 次月起始(不含)
+ * @return 按日升序的趋势行(仅含当月有销售的日期)
+ */
+ public List<SalesTrendItemVo> selectDailyTrend(@Param("sellerId") Long sellerId,
+ @Param("monthStart") Date monthStart,
+ @Param("nextMonthStart") Date nextMonthStart);
+
+ /**
+ * 按商品聚合本月销售额(比例饼图数据,降序)
+ *
+ * @param sellerId 销售会员ID
+ * @param monthStart 本月起始(含)
+ * @param nextMonthStart 次月起始(不含)
+ * @return 商品销售额列表(降序)
+ */
+ public List<ProductSalesRatioVo> selectProductSalesRatio(@Param("sellerId") Long sellerId,
+ @Param("monthStart") Date monthStart,
+ @Param("nextMonthStart") Date nextMonthStart);
+
+ /**
+ * 按购买人聚合本月购买额(销售排行,降序取前 10)
+ *
+ * 手机号在 Service 下发前脱敏;由 SQL 侧 LIMIT 控制取数规模。
+ *
+ * @param sellerId 销售会员ID
+ * @param monthStart 本月起始(含)
+ * @param nextMonthStart 次月起始(不含)
+ * @return 排行列表(降序,最多 10 条;rank 由 Service 填充)
+ */
+ public List<SalesUserRankingVo> selectSalesUserRanking(@Param("sellerId") Long sellerId,
+ @Param("monthStart") Date monthStart,
+ @Param("nextMonthStart") Date nextMonthStart);
+}
diff --git a/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/mapper/AccountWarehouseMapper.java b/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/mapper/AccountWarehouseMapper.java
index ccbf199..d1369c4 100644
--- a/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/mapper/AccountWarehouseMapper.java
+++ b/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/mapper/AccountWarehouseMapper.java
@@ -1,6 +1,7 @@
package com.ruoyi.user.mapper;
import java.util.List;
+import org.apache.ibatis.annotations.Param;
import com.ruoyi.user.domain.AccountWarehouse;
/**
@@ -16,6 +17,27 @@
public List<AccountWarehouse> selectWarehouseByUserId(Long userId);
/**
+ * 按主键查询云仓库存(仅未删除行)
+ *
+ * 供进货前校验归属使用:取回行的 userId 与当前会员比对,防范水平越权。
+ *
+ * @param id 云仓行主键
+ * @return 云仓库存(不存在或已删除返回 null)
+ */
+ public AccountWarehouse selectWarehouseById(@Param("id") Long id);
+
+ /**
+ * 库存原子自增(进货入库)
+ *
+ * 用 SQL 内 `stock_qty = stock_qty + #{qty}` 自增,避免"读-改-写"竞态导致并发丢更新。
+ *
+ * @param id 云仓行主键
+ * @param qty 增加数量(正数)
+ * @return 影响行数
+ */
+ public int increaseStockQty(@Param("id") Long id, @Param("qty") Long qty);
+
+ /**
* 新增云仓库存
*/
public int insertWarehouse(AccountWarehouse accountWarehouse);
diff --git a/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/mapper/AccountWarehouseRecordMapper.java b/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/mapper/AccountWarehouseRecordMapper.java
new file mode 100644
index 0000000..d89198c
--- /dev/null
+++ b/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/mapper/AccountWarehouseRecordMapper.java
@@ -0,0 +1,29 @@
+package com.ruoyi.user.mapper;
+
+import java.util.List;
+import org.apache.ibatis.annotations.Param;
+import com.ruoyi.user.domain.AccountWarehouseRecord;
+
+/**
+ * 会员云仓变动流水 数据层
+ *
+ * @author hugray
+ */
+public interface AccountWarehouseRecordMapper
+{
+ /**
+ * 新增云仓变动流水(进货入库 / 销售出库 / 库存调整)
+ *
+ * @param accountWarehouseRecord 变动流水
+ * @return 影响行数
+ */
+ public int insertWarehouseRecord(AccountWarehouseRecord accountWarehouseRecord);
+
+ /**
+ * 查询指定会员的云仓变动流水(按变动时间倒序,配合 startPage 分页)
+ *
+ * @param userId 会员ID
+ * @return 变动流水列表
+ */
+ public List<AccountWarehouseRecord> selectWarehouseRecordList(@Param("userId") Long userId);
+}
diff --git a/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/service/IAccountMemberCenterService.java b/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/service/IAccountMemberCenterService.java
new file mode 100644
index 0000000..cb9c5b4
--- /dev/null
+++ b/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/service/IAccountMemberCenterService.java
@@ -0,0 +1,59 @@
+package com.ruoyi.user.service;
+
+import java.util.List;
+import com.ruoyi.user.domain.dto.MemberPurchaseDto;
+import com.ruoyi.user.domain.vo.MemberSalesStatisticsVo;
+import com.ruoyi.user.domain.vo.MemberWarehouseVo;
+import com.ruoyi.user.domain.vo.PurchaseOrderVo;
+import com.ruoyi.user.domain.vo.WarehouseRecordVo;
+
+/**
+ * 会员中心 业务层
+ *
+ * 覆盖「销售统计」与「我的仓库」两个 Tab:统计聚合、仓库列表、进货入库、变动记录、进货订单。
+ * 会员身份一律由调用方(Controller)从登录态解析后传入 userId,本层不接受前端直传。
+ *
+ * @author hugray
+ */
+public interface IAccountMemberCenterService
+{
+ /**
+ * 查询当前会员的销售统计(当月三栏 + 环比 + 趋势 + 比例 + 排行)
+ *
+ * @param userId 会员ID
+ * @return 销售统计聚合出参
+ */
+ public MemberSalesStatisticsVo getSalesStatistics(Long userId);
+
+ /**
+ * 查询当前会员的云仓列表(含低库存标记)
+ *
+ * @param userId 会员ID
+ * @return 云仓列表
+ */
+ public List<MemberWarehouseVo> getWarehouseList(Long userId);
+
+ /**
+ * 进货:生成进货单 + 增加库存 + 追加变动流水(同一事务)
+ *
+ * @param userId 会员ID
+ * @param dto 进货入参(warehouseId 须属于当前会员)
+ */
+ public void purchase(Long userId, MemberPurchaseDto dto);
+
+ /**
+ * 查询当前会员的云仓变动记录(按变动时间倒序,配合 startPage 分页)
+ *
+ * @param userId 会员ID
+ * @return 变动记录列表
+ */
+ public List<WarehouseRecordVo> getWarehouseRecordList(Long userId);
+
+ /**
+ * 查询当前会员的进货订单(按下单时间倒序,配合 startPage 分页)
+ *
+ * @param userId 会员ID
+ * @return 进货订单列表
+ */
+ public List<PurchaseOrderVo> getPurchaseOrderList(Long userId);
+}
diff --git a/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/service/impl/AccountMemberCenterServiceImpl.java b/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/service/impl/AccountMemberCenterServiceImpl.java
new file mode 100644
index 0000000..ca25661
--- /dev/null
+++ b/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/service/impl/AccountMemberCenterServiceImpl.java
@@ -0,0 +1,387 @@
+package com.ruoyi.user.service.impl;
+
+import java.time.LocalDate;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ThreadLocalRandom;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import com.ruoyi.common.core.exception.ServiceException;
+import com.ruoyi.user.domain.AccountPurchaseOrder;
+import com.ruoyi.user.domain.AccountUser;
+import com.ruoyi.user.domain.AccountWarehouse;
+import com.ruoyi.user.domain.AccountWarehouseRecord;
+import com.ruoyi.user.domain.dto.MemberPurchaseDto;
+import com.ruoyi.user.domain.vo.MemberSalesStatisticsVo;
+import com.ruoyi.user.domain.vo.MemberWarehouseVo;
+import com.ruoyi.user.domain.vo.ProductSalesRatioVo;
+import com.ruoyi.user.domain.vo.PurchaseOrderVo;
+import com.ruoyi.user.domain.vo.SalesMonthSummaryVo;
+import com.ruoyi.user.domain.vo.SalesTrendItemVo;
+import com.ruoyi.user.domain.vo.SalesUserRankingVo;
+import com.ruoyi.user.domain.vo.WarehouseRecordVo;
+import com.ruoyi.user.enums.MemberLevelEnum;
+import com.ruoyi.user.enums.PurchaseOrderStatusEnum;
+import com.ruoyi.user.enums.WarehouseChangeTypeEnum;
+import com.ruoyi.user.mapper.AccountPurchaseOrderMapper;
+import com.ruoyi.user.mapper.AccountSalesRecordMapper;
+import com.ruoyi.user.mapper.AccountUserMapper;
+import com.ruoyi.user.mapper.AccountWarehouseMapper;
+import com.ruoyi.user.mapper.AccountWarehouseRecordMapper;
+import com.ruoyi.user.service.IAccountMemberCenterService;
+
+/**
+ * 会员中心 业务实现
+ *
+ * @author hugray
+ */
+@Service
+public class AccountMemberCenterServiceImpl implements IAccountMemberCenterService
+{
+ /** 进货单号前缀 */
+ private static final String PURCHASE_ORDER_PREFIX = "PG";
+
+ /** 进货单号随机段上界(4 位) */
+ private static final int ORDER_NO_RANDOM_BOUND = 10000;
+
+ /** 销售排行取前 N 名 */
+ private static final int RANKING_LIMIT = 10;
+
+ @Autowired
+ private AccountUserMapper accountUserMapper;
+
+ @Autowired
+ private AccountSalesRecordMapper accountSalesRecordMapper;
+
+ @Autowired
+ private AccountPurchaseOrderMapper accountPurchaseOrderMapper;
+
+ @Autowired
+ private AccountWarehouseMapper accountWarehouseMapper;
+
+ @Autowired
+ private AccountWarehouseRecordMapper accountWarehouseRecordMapper;
+
+ @Override
+ public MemberSalesStatisticsVo getSalesStatistics(Long userId)
+ {
+ // STEP 1: 校验会员身份(网关注入的登录态)
+ if (userId == null)
+ {
+ throw new ServiceException("获取销售统计失败");
+ }
+
+ // STEP 2: 取会员资料(手机号脱敏 + 等级归一化兜底)
+ AccountUser user = accountUserMapper.selectAccountUserById(userId);
+ if (user == null)
+ {
+ throw new ServiceException("获取销售统计失败");
+ }
+
+ // STEP 3: 计算本月 / 上月时间边界(在 Java 侧算好,避免 SQL 日期方言差异)
+ LocalDate today = LocalDate.now();
+ LocalDate monthStartDate = today.withDayOfMonth(1);
+ Date monthStart = toDate(monthStartDate);
+ Date nextMonthStart = toDate(monthStartDate.plusMonths(1));
+ Date prevMonthStart = toDate(monthStartDate.minusMonths(1));
+
+ // STEP 4: 聚合本月销售额 / 利润额 / 单数与上月利润
+ SalesMonthSummaryVo summary = accountSalesRecordMapper.selectMonthSummary(
+ userId, monthStart, nextMonthStart, prevMonthStart);
+ if (summary == null)
+ {
+ summary = new SalesMonthSummaryVo();
+ }
+
+ // STEP 5: 聚合本月进货额(来自进货单)
+ Long monthPurchaseAmount = accountPurchaseOrderMapper.selectMonthPurchaseAmount(
+ userId, monthStart, nextMonthStart);
+
+ // STEP 6: 组装出参
+ MemberSalesStatisticsVo vo = new MemberSalesStatisticsVo();
+ vo.setPhone(maskPhone(user.getPhone()));
+ vo.setMemberLevel(MemberLevelEnum.normalize(user.getSystemLevel()));
+ vo.setMonthSalesAmount(nullToZero(summary.getSalesAmount()));
+ vo.setMonthPurchaseAmount(nullToZero(monthPurchaseAmount));
+ vo.setMonthProfitAmount(nullToZero(summary.getProfitAmount()));
+ vo.setMonthSalesCount(summary.getSalesCount() == null ? 0 : summary.getSalesCount());
+ vo.setProfitGrowthRate(calcGrowthRate(summary.getProfitAmount(), summary.getPrevMonthProfit()));
+
+ // STEP 7: 趋势折线(当月 1 号到当日,缺省日补 0)
+ vo.setSalesTrend(buildSalesTrend(userId, monthStart, nextMonthStart, today));
+
+ // STEP 8: 商品销售额比例
+ List<ProductSalesRatioVo> ratioList = accountSalesRecordMapper.selectProductSalesRatio(
+ userId, monthStart, nextMonthStart);
+ vo.setProductSalesRatio(ratioList == null ? new ArrayList<>() : ratioList);
+
+ // STEP 9: 销售用户排行(脱敏 + 填名次)
+ vo.setSalesUserRanking(buildRanking(userId, monthStart, nextMonthStart));
+ return vo;
+ }
+
+ @Override
+ public List<MemberWarehouseVo> getWarehouseList(Long userId)
+ {
+ if (userId == null)
+ {
+ throw new ServiceException("获取仓库列表失败");
+ }
+ List<AccountWarehouse> warehouses = accountWarehouseMapper.selectWarehouseByUserId(userId);
+ List<MemberWarehouseVo> voList = new ArrayList<>();
+ if (warehouses == null)
+ {
+ return voList;
+ }
+ for (AccountWarehouse warehouse : warehouses)
+ {
+ MemberWarehouseVo vo = new MemberWarehouseVo();
+ vo.setWarehouseId(warehouse.getId());
+ vo.setProductName(warehouse.getProductName());
+ // 商品图片列在 account_warehouse 不存在,恒为 null,由前端使用占位图
+ vo.setProductImage(null);
+ vo.setStockQty(nullToZero(warehouse.getStockQty()));
+ vo.setMinStockQty(nullToZero(warehouse.getMinStockQty()));
+ vo.setLowStock(isLowStock(warehouse.getStockQty(), warehouse.getMinStockQty()));
+ voList.add(vo);
+ }
+ return voList;
+ }
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public void purchase(Long userId, MemberPurchaseDto dto)
+ {
+ // STEP 1: 基础校验
+ if (userId == null || dto == null || dto.getWarehouseId() == null)
+ {
+ throw new ServiceException("进货失败,请稍后重试");
+ }
+ if (dto.getQty() == null || dto.getQty() <= 0)
+ {
+ throw new ServiceException("进货数量必须大于 0");
+ }
+
+ // STEP 2: 校验云仓归属(防水平越权:非本会员商品直接拒绝)
+ AccountWarehouse warehouse = accountWarehouseMapper.selectWarehouseById(dto.getWarehouseId());
+ if (warehouse == null || !userId.equals(warehouse.getUserId()))
+ {
+ throw new ServiceException("商品不存在或不属于当前会员");
+ }
+
+ // STEP 3: 生成唯一进货单号(时间戳 + 随机段,UNIQUE KEY 兜底幂等)
+ String orderNo = generatePurchaseOrderNo();
+ Long qty = dto.getQty();
+ long unitPrice = dto.getUnitPrice() == null || dto.getUnitPrice() < 0 ? 0L : dto.getUnitPrice();
+
+ // STEP 4: 落进货单(status=已入库)
+ AccountPurchaseOrder order = new AccountPurchaseOrder();
+ order.setOrderNo(orderNo);
+ order.setUserId(userId);
+ order.setWarehouseId(warehouse.getId());
+ order.setProductName(warehouse.getProductName());
+ order.setQty(qty);
+ order.setUnitPrice(unitPrice);
+ order.setTotalAmount(qty * unitPrice);
+ order.setStatus(PurchaseOrderStatusEnum.STOCKED.getCode());
+ accountPurchaseOrderMapper.insertPurchaseOrder(order);
+
+ // STEP 5: 库存原子自增(SQL 内自增,避免并发读改写丢更新)
+ accountWarehouseMapper.increaseStockQty(warehouse.getId(), qty);
+
+ // STEP 6: 追加云仓变动流水(进货入库,数量为正)
+ long afterQty = nullToZero(warehouse.getStockQty()) + qty;
+ AccountWarehouseRecord record = new AccountWarehouseRecord();
+ record.setUserId(userId);
+ record.setWarehouseId(warehouse.getId());
+ record.setProductName(warehouse.getProductName());
+ record.setChangeType(WarehouseChangeTypeEnum.PURCHASE_IN.getCode());
+ record.setChangeQty(qty);
+ record.setAfterQty(afterQty);
+ record.setBizNo(orderNo);
+ record.setRemark("进货单 " + orderNo);
+ accountWarehouseRecordMapper.insertWarehouseRecord(record);
+ // STEP 7: 事务提交(由 Spring 统一提交)
+ }
+
+ @Override
+ public List<WarehouseRecordVo> getWarehouseRecordList(Long userId)
+ {
+ if (userId == null)
+ {
+ throw new ServiceException("获取仓库记录失败");
+ }
+ List<AccountWarehouseRecord> records = accountWarehouseRecordMapper.selectWarehouseRecordList(userId);
+ List<WarehouseRecordVo> voList = new ArrayList<>();
+ if (records == null)
+ {
+ return voList;
+ }
+ for (AccountWarehouseRecord record : records)
+ {
+ WarehouseRecordVo vo = new WarehouseRecordVo();
+ vo.setRecordId(record.getRecordId());
+ vo.setProductName(record.getProductName());
+ vo.setChangeType(record.getChangeType());
+ vo.setChangeTypeName(WarehouseChangeTypeEnum.descOf(record.getChangeType()));
+ vo.setChangeQty(nullToZero(record.getChangeQty()));
+ vo.setAfterQty(nullToZero(record.getAfterQty()));
+ vo.setRemark(record.getRemark());
+ vo.setCreateTime(record.getCreateTime());
+ voList.add(vo);
+ }
+ return voList;
+ }
+
+ @Override
+ public List<PurchaseOrderVo> getPurchaseOrderList(Long userId)
+ {
+ if (userId == null)
+ {
+ throw new ServiceException("获取进货订单失败");
+ }
+ List<AccountPurchaseOrder> orders = accountPurchaseOrderMapper.selectPurchaseOrderList(userId);
+ List<PurchaseOrderVo> voList = new ArrayList<>();
+ if (orders == null)
+ {
+ return voList;
+ }
+ for (AccountPurchaseOrder order : orders)
+ {
+ PurchaseOrderVo vo = new PurchaseOrderVo();
+ vo.setOrderId(order.getOrderId());
+ vo.setOrderNo(order.getOrderNo());
+ vo.setProductName(order.getProductName());
+ vo.setQty(nullToZero(order.getQty()));
+ vo.setUnitPrice(nullToZero(order.getUnitPrice()));
+ vo.setTotalAmount(nullToZero(order.getTotalAmount()));
+ vo.setStatus(order.getStatus());
+ vo.setStatusName(PurchaseOrderStatusEnum.descOf(order.getStatus()));
+ vo.setCreateTime(order.getCreateTime());
+ voList.add(vo);
+ }
+ return voList;
+ }
+
+ /**
+ * 组装销售额趋势:把「有销售的日期」铺满当月 1 号至当日,缺省日补 0
+ */
+ private List<SalesTrendItemVo> buildSalesTrend(Long userId, Date monthStart, Date nextMonthStart, LocalDate today)
+ {
+ List<SalesTrendItemVo> rawList = accountSalesRecordMapper.selectDailyTrend(userId, monthStart, nextMonthStart);
+ Map<Integer, Long> amountByDay = new HashMap<>();
+ if (rawList != null)
+ {
+ for (SalesTrendItemVo item : rawList)
+ {
+ amountByDay.put(item.getDayOfMonth(), nullToZero(item.getAmount()));
+ }
+ }
+ List<SalesTrendItemVo> result = new ArrayList<>();
+ int maxDay = today.getDayOfMonth();
+ for (int day = 1; day <= maxDay; day++)
+ {
+ Long amount = amountByDay.get(day);
+ result.add(new SalesTrendItemVo(day + "号", amount == null ? 0L : amount));
+ }
+ return result;
+ }
+
+ /**
+ * 组装销售用户排行:手机号脱敏 + 依次填名次
+ */
+ private List<SalesUserRankingVo> buildRanking(Long userId, Date monthStart, Date nextMonthStart)
+ {
+ List<SalesUserRankingVo> rankingList = accountSalesRecordMapper.selectSalesUserRanking(
+ userId, monthStart, nextMonthStart);
+ List<SalesUserRankingVo> result = new ArrayList<>();
+ if (rankingList == null)
+ {
+ return result;
+ }
+ int rank = 1;
+ for (SalesUserRankingVo item : rankingList)
+ {
+ if (rank > RANKING_LIMIT)
+ {
+ break;
+ }
+ item.setRank(rank);
+ item.setPhone(maskPhone(item.getPhone()));
+ item.setAmount(nullToZero(item.getAmount()));
+ result.add(item);
+ rank++;
+ }
+ return result;
+ }
+
+ /**
+ * 计算利润环比:上月利润为 0 时返回 0(不抛除零),否则四舍五入为整数百分比
+ */
+ private Integer calcGrowthRate(Long current, Long previous)
+ {
+ long prev = nullToZero(previous);
+ if (prev == 0L)
+ {
+ return 0;
+ }
+ long cur = nullToZero(current);
+ return (int) Math.round((cur - prev) * 100.0 / prev);
+ }
+
+ /**
+ * 低库存判定:库存存在且低于最低需求库存
+ */
+ private boolean isLowStock(Long stockQty, Long minStockQty)
+ {
+ if (stockQty == null || minStockQty == null)
+ {
+ return false;
+ }
+ return stockQty < minStockQty;
+ }
+
+ /**
+ * 手机号脱敏:长度不足 7 位原样返回,否则保留前 3 位与后 4 位
+ */
+ private String maskPhone(String phone)
+ {
+ if (phone == null || phone.length() < 7)
+ {
+ return phone;
+ }
+ return phone.substring(0, 3) + "****" + phone.substring(phone.length() - 4);
+ }
+
+ /**
+ * 生成进货单号:PG + yyyyMMddHHmmss + 4 位随机,唯一索引兜底
+ */
+ private String generatePurchaseOrderNo()
+ {
+ String time = DateTimeFormatter.ofPattern("yyyyMMddHHmmss").format(java.time.LocalDateTime.now());
+ String random = String.format("%04d", ThreadLocalRandom.current().nextInt(ORDER_NO_RANDOM_BOUND));
+ return PURCHASE_ORDER_PREFIX + time + random;
+ }
+
+ /**
+ * LocalDate 转 java.util.Date(按系统时区当天零点)
+ */
+ private Date toDate(LocalDate date)
+ {
+ return Date.from(date.atStartOfDay(ZoneId.systemDefault()).toInstant());
+ }
+
+ /**
+ * Long 空值归零
+ */
+ private long nullToZero(Long value)
+ {
+ return value == null ? 0L : value;
+ }
+}
diff --git a/hugray-server/ruoyi-modules/ruoyi-user/src/main/resources/mapper/user/AccountPurchaseOrderMapper.xml b/hugray-server/ruoyi-modules/ruoyi-user/src/main/resources/mapper/user/AccountPurchaseOrderMapper.xml
new file mode 100644
index 0000000..50b5155
--- /dev/null
+++ b/hugray-server/ruoyi-modules/ruoyi-user/src/main/resources/mapper/user/AccountPurchaseOrderMapper.xml
@@ -0,0 +1,76 @@
+<?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.AccountPurchaseOrderMapper">
+
+ <resultMap type="com.ruoyi.user.domain.AccountPurchaseOrder" id="AccountPurchaseOrderResult">
+ <id property="orderId" column="order_id" />
+ <result property="orderNo" column="order_no" />
+ <result property="userId" column="user_id" />
+ <result property="warehouseId" column="warehouse_id" />
+ <result property="productName" column="product_name" />
+ <result property="qty" column="qty" />
+ <result property="unitPrice" column="unit_price" />
+ <result property="totalAmount" column="total_amount" />
+ <result property="status" column="status" />
+ <result property="purchaseTime" column="purchase_time" />
+ <result property="delFlag" column="del_flag" />
+ <result property="createBy" column="create_by" />
+ <result property="createTime" column="create_time" />
+ <result property="updateBy" column="update_by" />
+ <result property="updateTime" column="update_time" />
+ <result property="remark" column="remark" />
+ </resultMap>
+
+ <sql id="selectPurchaseOrderVo">
+ select order_id, order_no, user_id, warehouse_id, product_name, qty, unit_price,
+ total_amount, status, purchase_time, del_flag,
+ create_by, create_time, update_by, update_time, remark
+ from account_purchase_order
+ </sql>
+
+ <!-- 落单即入库:purchase_time 即聚合口径时间,create_time 记录写入时间 -->
+ <insert id="insertPurchaseOrder" parameterType="com.ruoyi.user.domain.AccountPurchaseOrder" useGeneratedKeys="true" keyProperty="orderId">
+ insert into account_purchase_order(
+ order_no,
+ user_id,
+ <if test="warehouseId != null">warehouse_id,</if>
+ <if test="productName != null">product_name,</if>
+ qty,
+ unit_price,
+ total_amount,
+ status,
+ purchase_time,
+ del_flag,
+ create_time
+ )values(
+ #{orderNo},
+ #{userId},
+ <if test="warehouseId != null">#{warehouseId},</if>
+ <if test="productName != null">#{productName},</if>
+ #{qty},
+ #{unitPrice},
+ #{totalAmount},
+ #{status},
+ sysdate(),
+ '0',
+ sysdate()
+ )
+ </insert>
+
+ <!-- 本月进货额区间 [monthStart, nextMonthStart),命中 idx_purchase_user_time -->
+ <select id="selectMonthPurchaseAmount" resultType="java.lang.Long">
+ select ifnull(sum(total_amount), 0)
+ from account_purchase_order
+ where del_flag = '0' and user_id = #{userId}
+ and purchase_time >= #{monthStart} and purchase_time < #{nextMonthStart}
+ </select>
+
+ <select id="selectPurchaseOrderList" parameterType="Long" resultMap="AccountPurchaseOrderResult">
+ <include refid="selectPurchaseOrderVo"/>
+ where del_flag = '0' and user_id = #{userId}
+ order by order_id desc
+ </select>
+
+</mapper>
diff --git a/hugray-server/ruoyi-modules/ruoyi-user/src/main/resources/mapper/user/AccountSalesRecordMapper.xml b/hugray-server/ruoyi-modules/ruoyi-user/src/main/resources/mapper/user/AccountSalesRecordMapper.xml
new file mode 100644
index 0000000..eda2339
--- /dev/null
+++ b/hugray-server/ruoyi-modules/ruoyi-user/src/main/resources/mapper/user/AccountSalesRecordMapper.xml
@@ -0,0 +1,77 @@
+<?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.AccountSalesRecordMapper">
+
+ <resultMap type="com.ruoyi.user.domain.vo.SalesMonthSummaryVo" id="SalesMonthSummaryResult">
+ <result property="salesAmount" column="sales_amount" />
+ <result property="profitAmount" column="profit_amount" />
+ <result property="salesCount" column="sales_count" />
+ <result property="prevMonthProfit" column="prev_month_profit" />
+ </resultMap>
+
+ <resultMap type="com.ruoyi.user.domain.vo.SalesTrendItemVo" id="SalesTrendResult">
+ <result property="dayOfMonth" column="day_of_month" />
+ <result property="amount" column="amount" />
+ </resultMap>
+
+ <resultMap type="com.ruoyi.user.domain.vo.ProductSalesRatioVo" id="ProductSalesRatioResult">
+ <result property="productName" column="product_name" />
+ <result property="amount" column="amount" />
+ </resultMap>
+
+ <resultMap type="com.ruoyi.user.domain.vo.SalesUserRankingVo" id="SalesUserRankingResult">
+ <result property="nickname" column="nickname" />
+ <result property="phone" column="phone" />
+ <result property="amount" column="amount" />
+ </resultMap>
+
+ <!-- 单条 SQL 聚合三栏 + 上月利润:本月区间 [monthStart, nextMonthStart),上月利润区间 [prevMonthStart, monthStart) -->
+ <select id="selectMonthSummary" resultMap="SalesMonthSummaryResult">
+ select
+ ifnull(sum(case when sale_time >= #{monthStart} and sale_time < #{nextMonthStart} then amount end), 0) as sales_amount,
+ ifnull(sum(case when sale_time >= #{monthStart} and sale_time < #{nextMonthStart} then profit_amount end), 0) as profit_amount,
+ ifnull(sum(case when sale_time >= #{monthStart} and sale_time < #{nextMonthStart} then 1 else 0 end), 0) as sales_count,
+ ifnull(sum(case when sale_time >= #{prevMonthStart} and sale_time < #{monthStart} then profit_amount end), 0) as prev_month_profit
+ from account_sales_record
+ where del_flag = '0' and seller_id = #{sellerId}
+ </select>
+
+ <select id="selectDailyTrend" resultMap="SalesTrendResult">
+ select
+ day(sale_time) as day_of_month,
+ ifnull(sum(amount), 0) as amount
+ from account_sales_record
+ where del_flag = '0' and seller_id = #{sellerId}
+ and sale_time >= #{monthStart} and sale_time < #{nextMonthStart}
+ group by day(sale_time)
+ order by day_of_month asc
+ </select>
+
+ <select id="selectProductSalesRatio" resultMap="ProductSalesRatioResult">
+ select
+ ifnull(product_name, '') as product_name,
+ ifnull(sum(amount), 0) as amount
+ from account_sales_record
+ where del_flag = '0' and seller_id = #{sellerId}
+ and sale_time >= #{monthStart} and sale_time < #{nextMonthStart}
+ group by product_name
+ order by amount desc
+ </select>
+
+ <!-- 排行取前 10,控制单次返回规模 -->
+ <select id="selectSalesUserRanking" resultMap="SalesUserRankingResult">
+ select
+ buyer_name as nickname,
+ buyer_phone as phone,
+ ifnull(sum(amount), 0) as amount
+ from account_sales_record
+ where del_flag = '0' and seller_id = #{sellerId}
+ and sale_time >= #{monthStart} and sale_time < #{nextMonthStart}
+ group by buyer_id, buyer_name, buyer_phone
+ order by amount desc
+ limit 10
+ </select>
+
+</mapper>
diff --git a/hugray-server/ruoyi-modules/ruoyi-user/src/main/resources/mapper/user/AccountWarehouseMapper.xml b/hugray-server/ruoyi-modules/ruoyi-user/src/main/resources/mapper/user/AccountWarehouseMapper.xml
index a95689d..fa6cb16 100644
--- a/hugray-server/ruoyi-modules/ruoyi-user/src/main/resources/mapper/user/AccountWarehouseMapper.xml
+++ b/hugray-server/ruoyi-modules/ruoyi-user/src/main/resources/mapper/user/AccountWarehouseMapper.xml
@@ -30,6 +30,19 @@
order by id asc
</select>
+ <select id="selectWarehouseById" parameterType="Long" resultMap="AccountWarehouseResult">
+ <include refid="selectWarehouseVo"/>
+ where del_flag = '0' and id = #{id}
+ </select>
+
+ <!-- 库存原子自增:非空校验兜底,避免库存为 NULL 时自增结果为 NULL -->
+ <update id="increaseStockQty">
+ update account_warehouse
+ set stock_qty = ifnull(stock_qty, 0) + #{qty},
+ update_time = sysdate()
+ where id = #{id} and del_flag = '0'
+ </update>
+
<insert id="insertWarehouse" parameterType="AccountWarehouse" useGeneratedKeys="true" keyProperty="id">
insert into account_warehouse(
<if test="userId != null">user_id,</if>
diff --git a/hugray-server/ruoyi-modules/ruoyi-user/src/main/resources/mapper/user/AccountWarehouseRecordMapper.xml b/hugray-server/ruoyi-modules/ruoyi-user/src/main/resources/mapper/user/AccountWarehouseRecordMapper.xml
new file mode 100644
index 0000000..8ebbc91
--- /dev/null
+++ b/hugray-server/ruoyi-modules/ruoyi-user/src/main/resources/mapper/user/AccountWarehouseRecordMapper.xml
@@ -0,0 +1,63 @@
+<?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.AccountWarehouseRecordMapper">
+
+ <resultMap type="com.ruoyi.user.domain.AccountWarehouseRecord" id="AccountWarehouseRecordResult">
+ <id property="recordId" column="record_id" />
+ <result property="userId" column="user_id" />
+ <result property="warehouseId" column="warehouse_id" />
+ <result property="productName" column="product_name" />
+ <result property="changeType" column="change_type" />
+ <result property="changeQty" column="change_qty" />
+ <result property="afterQty" column="after_qty" />
+ <result property="bizNo" column="biz_no" />
+ <result property="delFlag" column="del_flag" />
+ <result property="createBy" column="create_by" />
+ <result property="createTime" column="create_time" />
+ <result property="updateBy" column="update_by" />
+ <result property="updateTime" column="update_time" />
+ <result property="remark" column="remark" />
+ </resultMap>
+
+ <sql id="selectWarehouseRecordVo">
+ select record_id, user_id, warehouse_id, product_name, change_type, change_qty,
+ after_qty, biz_no, del_flag, create_by, create_time, update_by, update_time, remark
+ from account_warehouse_record
+ </sql>
+
+ <!-- 变动时间即 create_time,入库时统一落 sysdate(),避免与业务时间口径漂移 -->
+ <insert id="insertWarehouseRecord" parameterType="com.ruoyi.user.domain.AccountWarehouseRecord" useGeneratedKeys="true" keyProperty="recordId">
+ insert into account_warehouse_record(
+ user_id,
+ <if test="warehouseId != null">warehouse_id,</if>
+ <if test="productName != null">product_name,</if>
+ change_type,
+ change_qty,
+ after_qty,
+ <if test="bizNo != null">biz_no,</if>
+ <if test="remark != null">remark,</if>
+ del_flag,
+ create_time
+ )values(
+ #{userId},
+ <if test="warehouseId != null">#{warehouseId},</if>
+ <if test="productName != null">#{productName},</if>
+ #{changeType},
+ #{changeQty},
+ #{afterQty},
+ <if test="bizNo != null">#{bizNo},</if>
+ <if test="remark != null">#{remark},</if>
+ '0',
+ sysdate()
+ )
+ </insert>
+
+ <select id="selectWarehouseRecordList" parameterType="Long" resultMap="AccountWarehouseRecordResult">
+ <include refid="selectWarehouseRecordVo"/>
+ where del_flag = '0' and user_id = #{userId}
+ order by record_id desc
+ </select>
+
+</mapper>
diff --git a/hugray-server/sql/hugray_account_member_center_20261014.sql b/hugray-server/sql/hugray_account_member_center_20261014.sql
new file mode 100644
index 0000000..65e614d
--- /dev/null
+++ b/hugray-server/sql/hugray_account_member_center_20261014.sql
@@ -0,0 +1,96 @@
+-- ----------------------------
+-- C 端会员 个人中心-会员中心 相关变更(ruoyi-user 用户服务)
+--
+-- 需求:个人中心「会员中心」页需展示「销售统计」(当月销售额/进货额/利润、销售额趋势、
+-- 商品销售额比例、销售用户排行)与「我的仓库」(库存列表、低库存警示、进货、仓库记录)。
+-- 变更内容:
+-- 1. 新增「会员销售流水表」account_sales_record,销售三栏/趋势/比例/排行由其按时间实时聚合;
+-- 2. 新增「会员进货单表」account_purchase_order,进货入库留痕,当月进货额由其聚合;
+-- 3. 新增「会员云仓变动流水表」account_warehouse_record,记录每次库存变动(进货入库等)。
+--
+-- 说明(按权威库 192.168.110.111 的既有结构对齐):
+-- account_warehouse 已有库存列,本脚本不改动该表;进货仅在其上做 stock_qty 原子自增。
+-- account_user 的 9 个业务统计列本期不回填,页面数据一律由流水表实时聚合,避免双写口径不一致。
+--
+-- 目标库 hugray-account 由使用方预先创建(utf8mb4)。
+-- 注意:库名含连字符,任何引用都必须用反引号。
+-- ----------------------------
+
+USE `hugray-account`;
+
+-- 变更 1:新增会员销售流水表(一条流水 = 一次销售事件,含销售额与利润额)
+CREATE TABLE IF NOT EXISTS `account_sales_record` (
+ `record_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '销售流水ID',
+ `seller_id` bigint(20) NOT NULL COMMENT '销售会员ID(account_user.user_id)',
+ `buyer_id` bigint(20) DEFAULT NULL COMMENT '购买会员ID(account_user.user_id,散客为空)',
+ `buyer_name` varchar(64) DEFAULT NULL COMMENT '购买人昵称(销售排行展示用)',
+ `buyer_phone` varchar(20) DEFAULT NULL COMMENT '购买人手机号(下发脱敏)',
+ `product_name` varchar(128) DEFAULT NULL COMMENT '商品名称(比例图/排行展示用)',
+ `qty` bigint(20) NOT NULL DEFAULT 0 COMMENT '销售数量',
+ `amount` bigint(20) NOT NULL DEFAULT 0 COMMENT '销售额(单位:分)',
+ `profit_amount` bigint(20) NOT NULL DEFAULT 0 COMMENT '利润额(单位:分,可为负)',
+ `sale_time` datetime DEFAULT NULL COMMENT '销售时间(聚合口径)',
+ `del_flag` char(1) DEFAULT '0' COMMENT '删除标志(0存在 2删除)',
+ `create_by` varchar(64) DEFAULT '' COMMENT '创建者',
+ `create_time` datetime DEFAULT NULL COMMENT '创建时间',
+ `update_by` varchar(64) DEFAULT '' COMMENT '更新者',
+ `update_time` datetime DEFAULT NULL COMMENT '更新时间',
+ `remark` varchar(500) DEFAULT NULL COMMENT '备注',
+ PRIMARY KEY (`record_id`),
+ -- 覆盖「按销售会员 + 时间区间」的聚合,避免全表扫描
+ KEY `idx_sales_seller_time` (`seller_id`, `sale_time`)
+) ENGINE=InnoDB AUTO_INCREMENT=100 DEFAULT CHARSET=utf8mb4 COMMENT='会员销售流水表';
+
+-- 变更 2:新增会员进货单表(生成进货单即增加云仓库存,下单时间即聚合口径)
+CREATE TABLE IF NOT EXISTS `account_purchase_order` (
+ `order_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '进货单ID',
+ `order_no` varchar(64) NOT NULL COMMENT '进货单号(幂等唯一)',
+ `user_id` bigint(20) NOT NULL COMMENT '进货会员ID(account_user.user_id)',
+ `warehouse_id` bigint(20) DEFAULT NULL COMMENT '云仓行ID(account_warehouse.id,快照)',
+ `product_name` varchar(128) DEFAULT NULL COMMENT '商品名称(快照)',
+ `qty` bigint(20) NOT NULL DEFAULT 0 COMMENT '进货数量',
+ `unit_price` bigint(20) NOT NULL DEFAULT 0 COMMENT '进货单价(单位:分)',
+ `total_amount` bigint(20) NOT NULL DEFAULT 0 COMMENT '进货金额(单位:分)= qty × unit_price',
+ `status` char(1) DEFAULT '1' COMMENT '状态(1已入库)',
+ `purchase_time` datetime DEFAULT NULL COMMENT '进货时间(聚合口径)',
+ `del_flag` char(1) DEFAULT '0' COMMENT '删除标志(0存在 2删除)',
+ `create_by` varchar(64) DEFAULT '' COMMENT '创建者',
+ `create_time` datetime DEFAULT NULL COMMENT '创建时间',
+ `update_by` varchar(64) DEFAULT '' COMMENT '更新者',
+ `update_time` datetime DEFAULT NULL COMMENT '更新时间',
+ `remark` varchar(500) DEFAULT NULL COMMENT '备注',
+ PRIMARY KEY (`order_id`),
+ -- 业务幂等:进货单号全局唯一,重复入库被数据库兜底拦截
+ UNIQUE KEY `uk_purchase_order_no` (`order_no`),
+ -- 覆盖「按会员 + 时间区间」的进货额聚合
+ KEY `idx_purchase_user_time` (`user_id`, `purchase_time`)
+) ENGINE=InnoDB AUTO_INCREMENT=100 DEFAULT CHARSET=utf8mb4 COMMENT='会员进货单表';
+
+-- 变更 3:新增会员云仓变动流水表(进货入库/销售出库等库存变动留痕)
+CREATE TABLE IF NOT EXISTS `account_warehouse_record` (
+ `record_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '流水ID',
+ `user_id` bigint(20) NOT NULL COMMENT '会员ID(account_user.user_id)',
+ `warehouse_id` bigint(20) DEFAULT NULL COMMENT '云仓行ID(account_warehouse.id)',
+ `product_name` varchar(128) DEFAULT NULL COMMENT '商品名称(快照)',
+ `change_type` char(1) NOT NULL COMMENT '变动类型(1进货入库 2销售出库 3库存调整)',
+ `change_qty` bigint(20) NOT NULL DEFAULT 0 COMMENT '变动数量(入库为正,出库为负)',
+ `after_qty` bigint(20) NOT NULL DEFAULT 0 COMMENT '变动后库存',
+ `biz_no` varchar(64) DEFAULT NULL COMMENT '业务单号(关联进货单等)',
+ `del_flag` char(1) DEFAULT '0' COMMENT '删除标志(0存在 2删除)',
+ `create_by` varchar(64) DEFAULT '' COMMENT '创建者',
+ `create_time` datetime DEFAULT NULL COMMENT '变动时间',
+ `update_by` varchar(64) DEFAULT '' COMMENT '更新者',
+ `update_time` datetime DEFAULT NULL COMMENT '更新时间',
+ `remark` varchar(500) DEFAULT NULL COMMENT '备注',
+ PRIMARY KEY (`record_id`),
+ -- 覆盖「按会员 + 时间倒序」的流水分页
+ KEY `idx_whrecord_user_time` (`user_id`, `create_time`),
+ KEY `idx_whrecord_warehouse` (`warehouse_id`)
+) ENGINE=InnoDB AUTO_INCREMENT=100 DEFAULT CHARSET=utf8mb4 COMMENT='会员云仓变动流水表';
+
+-- ----------------------------
+-- 回滚语句(如需回滚,按依赖倒序执行)
+-- ----------------------------
+-- DROP TABLE IF EXISTS `account_warehouse_record`;
+-- DROP TABLE IF EXISTS `account_purchase_order`;
+-- DROP TABLE IF EXISTS `account_sales_record`;
diff --git a/hugray-user-miniapp/api/index.js b/hugray-user-miniapp/api/index.js
index 8683ba6..45baca9 100644
--- a/hugray-user-miniapp/api/index.js
+++ b/hugray-user-miniapp/api/index.js
@@ -110,6 +110,49 @@
}
/**
+ * 查询当前会员的销售统计(会员中心「销售统计」Tab)
+ * 返回 data 含 phone / memberLevel、三栏金额(单位:分)、利润环比 profitGrowthRate(%)、
+ * 销售额趋势 salesTrend、商品销售额比例 productSalesRatio、销售用户排行 salesUserRanking
+ */
+export function getMemberSalesStatistics() {
+ return get('/miniapp/account/user/member/sales-statistics')
+}
+
+/**
+ * 查询当前会员的云仓列表(「我的仓库」Tab)
+ * 返回 data 为数组,元素含 warehouseId / productName / stockQty / minStockQty / lowStock
+ */
+export function getMemberWarehouses() {
+ return get('/miniapp/account/user/member/warehouses')
+}
+
+/**
+ * 会员进货(生成进货单并增加云仓库存,会员身份由网关注入)
+ * @param {Object} data { warehouseId, qty, unitPrice } unitPrice 单位为分,选填
+ */
+export function memberPurchase(data) {
+ return post('/miniapp/account/user/member/purchase', data)
+}
+
+/**
+ * 查询当前会员的云仓变动记录(「仓库记录」弹层)
+ * @param {Object} params { pageNum, pageSize }
+ * 说明:后端返回 TableDataInfo,数据在顶层 rows(非 data 包裹),总数为 total
+ */
+export function getMemberWarehouseRecords(params) {
+ return get('/miniapp/account/user/member/warehouse-records', params)
+}
+
+/**
+ * 查询当前会员的进货订单(「进货订单」Tab)
+ * @param {Object} params { pageNum, pageSize }
+ * 说明:后端返回 TableDataInfo,数据在顶层 rows(非 data 包裹),总数为 total
+ */
+export function getMemberPurchaseOrders(params) {
+ return get('/miniapp/account/user/member/purchase-orders', params)
+}
+
+/**
* 上传头像文件到文件服务,返回 { data: { name, url } }
* 说明:走网关 /file/** -> ruoyi-file,需登录态(网关白名单不含 /file/upload)
* @param {String} filePath 本地临时文件路径(uni.chooseImage 返回)
diff --git a/hugray-user-miniapp/pages.json b/hugray-user-miniapp/pages.json
index a117e14..1d95863 100644
--- a/hugray-user-miniapp/pages.json
+++ b/hugray-user-miniapp/pages.json
@@ -58,6 +58,13 @@
}
},
{
+ "path": "pages/member/index",
+ // 顶部为深蓝会员卡 + 三段式 Tab,需自定义导航栏承载
+ "style": {
+ "navigationStyle": "custom"
+ }
+ },
+ {
"path": "pages/contact/index",
// 顶部为深蓝底品牌区,与白底原生导航冲突,故使用自定义导航栏
"style": {
diff --git a/hugray-user-miniapp/pages/member/index.vue b/hugray-user-miniapp/pages/member/index.vue
new file mode 100644
index 0000000..94b3b7d
--- /dev/null
+++ b/hugray-user-miniapp/pages/member/index.vue
@@ -0,0 +1,1394 @@
+<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>
+ </view>
+
+ <scroll-view class="scroll" scroll-y>
+ <!-- 会员信息深蓝卡:脱敏手机号 + 当前等级 + 等级详情入口 -->
+ <view class="member-card">
+ <view class="member-left">
+ <text class="member-phone">{{ memberPhone || '—' }}</text>
+ <view class="member-level-line">
+ <text class="member-level-label">当前等级</text>
+ <text class="member-level-value">{{ memberLevel }}</text>
+ </view>
+ </view>
+ <view class="level-detail-btn" @tap="handleLevelDetail">
+ <text class="level-detail-text">等级详情</text>
+ </view>
+ </view>
+
+ <!-- 三段式 Tab:销售统计 / 我的仓库 / 进货订单 -->
+ <view class="tabs">
+ <view
+ v-for="tab in TAB_LIST"
+ :key="tab.key"
+ class="tab-item"
+ @tap="handleTabChange(tab.key)"
+ >
+ <text class="tab-text" :class="{ 'tab-text--active': activeTab === tab.key }">
+ {{ tab.label }}
+ </text>
+ <view v-if="activeTab === tab.key" class="tab-indicator"></view>
+ </view>
+ </view>
+
+ <!-- ==================== Tab1:销售统计 ==================== -->
+ <view v-if="activeTab === 'sales'" class="panel">
+ <!-- 当月数据汇总:三栏 + 利润环比 -->
+ <view class="stat-card">
+ <view class="stat-head">
+ <text class="stat-head-label">当月销售</text>
+ <text class="stat-head-growth">{{ growthRateText }}</text>
+ </view>
+ <text class="stat-amount">¥{{ formatYuan(stats.monthSalesAmount, true) }}</text>
+ <text class="stat-sub">当月利润 ¥{{ formatYuan(stats.monthProfitAmount, true) }}</text>
+ <view class="stat-cols">
+ <view class="stat-col">
+ <text class="stat-col-label">当月销售额</text>
+ <text class="stat-col-value">¥{{ formatYuan(stats.monthSalesAmount, true) }}</text>
+ </view>
+ <view class="stat-col">
+ <text class="stat-col-label">当月进货额</text>
+ <text class="stat-col-value">¥{{ formatYuan(stats.monthPurchaseAmount, true) }}</text>
+ </view>
+ <view class="stat-col">
+ <text class="stat-col-label">销售单数</text>
+ <text class="stat-col-value">{{ stats.monthSalesCount || 0 }}</text>
+ </view>
+ </view>
+ </view>
+
+ <!-- 销售额折线图(当月 1 号至当日) -->
+ <view class="chart-card">
+ <text class="chart-title">销售额</text>
+ <view class="chart-box">
+ <qiun-data-charts
+ v-if="hasTrend"
+ type="line"
+ :chartData="trendChartData"
+ :opts="TREND_OPTS"
+ />
+ <view v-else class="chart-empty">
+ <text class="chart-empty-text">暂无销售数据</text>
+ </view>
+ </view>
+ </view>
+
+ <!-- 商品销售额比例:环形图 + 图例 -->
+ <view class="chart-card">
+ <text class="chart-title">商品销售额比例</text>
+ <view v-if="ratioList.length > 0" class="ratio-wrap">
+ <view class="ratio-chart">
+ <qiun-data-charts type="ring" :chartData="ratioChartData" :opts="RATIO_OPTS" />
+ </view>
+ <view class="ratio-legend">
+ <view
+ v-for="(item, index) in ratioList"
+ :key="item.productName"
+ class="legend-row"
+ >
+ <view class="legend-dot" :style="{ backgroundColor: legendColor(index) }"></view>
+ <text class="legend-name">{{ item.productName }}</text>
+ <text class="legend-value">¥{{ formatYuan(item.amount, true) }}</text>
+ </view>
+ </view>
+ </view>
+ <view v-else class="chart-empty">
+ <text class="chart-empty-text">暂无销售数据</text>
+ </view>
+ </view>
+
+ <!-- 销售用户排行(Top 10) -->
+ <view class="chart-card">
+ <text class="chart-title">销售用户排行</text>
+ <view v-if="rankingList.length > 0" class="rank-list">
+ <view v-for="item in rankingList" :key="item.rank" class="rank-row">
+ <view class="rank-index">
+ <text class="rank-index-text">{{ item.rank }}</text>
+ </view>
+ <text class="rank-name">{{ item.nickname || '会员' }}</text>
+ <text class="rank-phone">{{ item.phone || '—' }}</text>
+ <text class="rank-amount">¥{{ formatYuan(item.amount, true) }}</text>
+ </view>
+ </view>
+ <view v-else class="chart-empty">
+ <text class="chart-empty-text">暂无排行数据</text>
+ </view>
+ </view>
+ </view>
+
+ <!-- ==================== Tab2:我的仓库 ==================== -->
+ <view v-else-if="activeTab === 'warehouse'" class="panel">
+ <view v-if="warehouseList.length > 0" class="warehouse-list">
+ <view
+ v-for="item in warehouseList"
+ :key="item.warehouseId"
+ class="warehouse-card"
+ >
+ <!-- 低库存警示条:低于最低需求库存时置顶展示 -->
+ <view v-if="item.lowStock" class="low-stock-bar">
+ <view class="low-stock-icon"></view>
+ <text class="low-stock-bar-text">已低于最低需求库存,无法销售该商品,请及时补充</text>
+ </view>
+ <view class="warehouse-row">
+ <!-- 商品图:后端暂未下发图片字段,回退占位图 -->
+ <view class="product-thumb">
+ <image
+ v-if="item.productImage"
+ class="product-thumb-img"
+ :src="item.productImage"
+ mode="aspectFill"
+ />
+ <view v-else class="icon icon-image"></view>
+ </view>
+ <view class="warehouse-main">
+ <text class="product-name">{{ item.productName }}</text>
+ <view class="warehouse-stock">
+ <text class="stock-label">当前库存:</text>
+ <text class="stock-value" :class="{ 'stock-value--low': item.lowStock }">
+ {{ item.stockQty }}
+ </text>
+ <text class="stock-label stock-label--gap">最低需求库存:</text>
+ <text class="stock-value">{{ item.minStockQty }}</text>
+ </view>
+ <view class="warehouse-actions">
+ <view class="btn-green" @tap="handleOpenPurchase(item)">
+ <text class="btn-green-text">进货</text>
+ </view>
+ <view class="btn-green" @tap="handleOpenRecords(item)">
+ <text class="btn-green-text">仓库记录</text>
+ </view>
+ </view>
+ </view>
+ </view>
+ </view>
+ </view>
+ <view v-else-if="!warehouseLoading" class="empty">
+ <text class="empty-title">暂无云仓商品</text>
+ <text class="empty-desc">开通云仓后可在此查看库存与进货</text>
+ </view>
+ </view>
+
+ <!-- ==================== Tab3:进货订单 ==================== -->
+ <view v-else class="panel">
+ <view v-if="orderList.length > 0" class="order-list">
+ <view v-for="item in orderList" :key="item.orderId" class="order-card">
+ <view class="order-head">
+ <text class="order-no">单号:{{ item.orderNo }}</text>
+ <text class="order-status">{{ item.statusName }}</text>
+ </view>
+ <text class="order-product">{{ item.productName }}</text>
+ <view class="order-foot">
+ <text class="order-qty">数量 ×{{ item.qty }}</text>
+ <text class="order-amount">¥{{ formatYuan(item.totalAmount, true) }}</text>
+ </view>
+ <text class="order-time">{{ item.createTime }}</text>
+ </view>
+ </view>
+ <view v-else-if="!orderLoading" class="empty">
+ <text class="empty-title">暂无进货订单</text>
+ <text class="empty-desc">在「我的仓库」进货后此处可查看</text>
+ </view>
+ </view>
+ </scroll-view>
+
+ <!-- ==================== 进货弹窗 ==================== -->
+ <view v-if="purchaseVisible" class="mask" @tap="closePurchase">
+ <view class="dialog" @tap.stop>
+ <text class="dialog-title">进货</text>
+ <text class="dialog-product">{{ purchaseTarget.productName }}</text>
+ <view class="form-row">
+ <text class="form-label">进货数量</text>
+ <input
+ class="form-input"
+ type="number"
+ placeholder="请输入进货数量"
+ placeholder-class="form-placeholder"
+ v-model="purchaseQty"
+ />
+ </view>
+ <view class="form-row">
+ <text class="form-label">进货单价(元)</text>
+ <input
+ class="form-input"
+ type="digit"
+ placeholder="选填,默认 0"
+ placeholder-class="form-placeholder"
+ v-model="purchaseUnitPrice"
+ />
+ </view>
+ <view class="dialog-actions">
+ <view class="btn-plain" @tap="closePurchase">
+ <text class="btn-plain-text">取消</text>
+ </view>
+ <view class="btn-green btn-green--fill" @tap="handleSubmitPurchase">
+ <text class="btn-green-text">确认进货</text>
+ </view>
+ </view>
+ </view>
+ </view>
+
+ <!-- ==================== 仓库记录弹窗 ==================== -->
+ <view v-if="recordsVisible" class="mask" @tap="closeRecords">
+ <view class="dialog dialog--records" @tap.stop>
+ <text class="dialog-title">仓库记录</text>
+ <text class="dialog-sub">{{ recordsProductName }}</text>
+ <scroll-view class="record-scroll" scroll-y>
+ <view v-for="item in warehouseRecords" :key="item.recordId" class="record-row">
+ <view class="record-line1">
+ <text class="record-product">{{ item.productName }}</text>
+ <text
+ class="record-change"
+ :class="item.changeQty < 0 ? 'record-change--out' : 'record-change--in'"
+ >
+ {{ item.changeQty > 0 ? '+' : '' }}{{ item.changeQty }}
+ </text>
+ </view>
+ <view class="record-line2">
+ <text class="record-type">{{ item.changeTypeName }}</text>
+ <text class="record-after">结存 {{ item.afterQty }}</text>
+ <text class="record-time">{{ item.createTime }}</text>
+ </view>
+ </view>
+ <view v-if="warehouseRecords.length === 0" class="record-empty">
+ <text class="record-empty-text">暂无记录</text>
+ </view>
+ </scroll-view>
+ <view class="btn-green btn-green--fill" @tap="closeRecords">
+ <text class="btn-green-text">关闭</text>
+ </view>
+ </view>
+ </view>
+ </view>
+</template>
+
+<script setup>
+import { computed, ref } from 'vue'
+import { onLoad } from '@dcloudio/uni-app'
+import { ensureLogin } from '@/utils/request.js'
+import { formatYuan } from '@/utils/format.js'
+import {
+ getMemberSalesStatistics,
+ getMemberWarehouses,
+ memberPurchase,
+ getMemberWarehouseRecords,
+ getMemberPurchaseOrders
+} from '@/api/index.js'
+import qiunDataCharts from '@/uni_modules/qiun-data-charts/components/qiun-data-charts/qiun-data-charts.vue'
+
+/** 状态栏高度:自定义导航栏下用于顶部留白 */
+const statusBarHeight = uni.getSystemInfoSync().statusBarHeight || 0
+
+/** 三段式 Tab 配置:key 仅前端切换用 */
+const TAB_LIST = [
+ { key: 'sales', label: '销售统计' },
+ { key: 'warehouse', label: '我的仓库' },
+ { key: 'orders', label: '进货订单' }
+]
+
+/** 图表/图例配色(与设计稿一致:蓝 / 青 / 草绿),按序循环使用 */
+const LEGEND_COLORS = ['#2F86E8', '#2FB5B8', '#7CC142']
+
+/** 折线图样式:隐藏图例、虚线网格、直线折线 */
+const TREND_OPTS = {
+ color: ['#1E5EBC'],
+ padding: [15, 10, 0, 5],
+ legend: { show: false },
+ // X 轴标签由 trendChartData 自行等间距抽稀,此处不再用 labelCount(避免 uCharts 强制补末尾标签造成相邻)
+ xAxis: { disableGrid: true, fontSize: 10, fontColor: '#94A3B8' },
+ yAxis: {
+ gridType: 'dash',
+ dashLength: 4,
+ splitNumber: 4,
+ gridColor: '#E2E8F0',
+ fontColor: '#94A3B8',
+ // axisLine:false 去掉左侧 Y 轴竖线,仅保留刻度文字
+ data: [{ min: 0, axisLine: false }]
+ },
+ extra: { line: { type: 'straight', width: 2, activeType: 'hollow' } }
+}
+
+/** 环形图样式:内部留空、不显示数据标签(图例由页面自绘) */
+const RATIO_OPTS = {
+ color: LEGEND_COLORS,
+ padding: [5, 5, 5, 5],
+ legend: { show: false },
+ dataLabel: false,
+ extra: { ring: { ringWidth: 36, activeOpacity: 0.5, activeRadius: 10 } }
+}
+
+/** 销售统计出参默认值:接口未返回时页面展示全零,避免 NaN */
+const DEFAULT_STATS = {
+ phone: '',
+ memberLevel: 'C',
+ monthSalesAmount: 0,
+ monthPurchaseAmount: 0,
+ monthProfitAmount: 0,
+ monthSalesCount: 0,
+ profitGrowthRate: 0,
+ salesTrend: [],
+ productSalesRatio: [],
+ salesUserRanking: []
+}
+
+/** 当前 Tab,默认「销售统计」 */
+const activeTab = ref('sales')
+
+/** 销售统计聚合数据 */
+const stats = ref({ ...DEFAULT_STATS })
+
+/** 会员脱敏手机号 */
+const memberPhone = computed(() => stats.value.phone)
+
+/** 会员等级:缺省兜底 C,与后端 MemberLevelEnum.normalize 保持一致 */
+const memberLevel = computed(() => stats.value.memberLevel || 'C')
+
+/** 商品销售额比例列表 */
+const ratioList = computed(() => stats.value.productSalesRatio || [])
+
+/** 销售用户排行列表 */
+const rankingList = computed(() => stats.value.salesUserRanking || [])
+
+/** 是否存在销售趋势数据(无数据时不渲染图表,改展示空态) */
+const hasTrend = computed(() => (stats.value.salesTrend || []).length > 0)
+
+/** 利润环比文案:正数为提升、负数为下降 */
+const growthRateText = computed(() => {
+ const rate = stats.value.profitGrowthRate || 0
+ return rate >= 0 ? `上月提升 ${rate}%` : `上月下降 ${Math.abs(rate)}%`
+})
+
+/** 折线图数据:分类为「N号」,金额由分转为元 */
+/** 折线图 X 轴最多显示的标签数 */
+const TREND_MAX_LABELS = 6
+
+const trendChartData = computed(() => {
+ const trend = stats.value.salesTrend || []
+ const total = trend.length
+ // 等间距抽稀:以最后一天为锚点向前按固定 step 取标签,保证间距一致且始终包含最新日期
+ const step = Math.max(1, Math.ceil((total - 1) / (TREND_MAX_LABELS - 1)))
+ return {
+ categories: trend.map((item, index) => ((total - 1 - index) % step === 0 ? item.day : '')),
+ series: [{ name: '销售额', data: trend.map((item) => Number(formatYuan(item.amount))) }]
+ }
+})
+
+/** 环形图数据:uCharts 单系列 data 数组承载各商品金额(元) */
+const ratioChartData = computed(() => ({
+ series: [
+ {
+ data: ratioList.value.map((item) => ({
+ name: item.productName,
+ value: Number(formatYuan(item.amount))
+ }))
+ }
+ ]
+}))
+
+/** 我的仓库列表 */
+const warehouseList = ref([])
+/** 我的仓库加载中标志 */
+const warehouseLoading = ref(false)
+
+/** 进货订单列表 */
+const orderList = ref([])
+/** 进货订单加载中标志 */
+const orderLoading = ref(false)
+
+/** 进货弹窗显隐 */
+const purchaseVisible = ref(false)
+/** 进货目标商品(含 warehouseId / productName) */
+const purchaseTarget = ref({})
+/** 进货数量输入值 */
+const purchaseQty = ref('')
+/** 进货单价输入值(元) */
+const purchaseUnitPrice = ref('')
+
+/** 仓库记录弹窗显隐 */
+const recordsVisible = ref(false)
+/** 仓库记录列表 */
+const warehouseRecords = ref([])
+/** 当前查看记录的商品名(弹窗副标题) */
+const recordsProductName = ref('')
+
+/** 图例色:按序循环取色,与环形图分段色保持一致 */
+const legendColor = (index) => LEGEND_COLORS[index % LEGEND_COLORS.length]
+
+/** 拉取销售统计(含手机号 / 等级 / 三栏金额 / 趋势 / 比例 / 排行) */
+const loadStatistics = async () => {
+ try {
+ const res = await getMemberSalesStatistics()
+ stats.value = { ...DEFAULT_STATS, ...(res.data || {}) }
+ } catch (error) {
+ console.error('[MEMBER] 获取销售统计失败', error)
+ }
+}
+
+/** 拉取云仓列表 */
+const loadWarehouses = async () => {
+ warehouseLoading.value = true
+ try {
+ const res = await getMemberWarehouses()
+ warehouseList.value = res.data || []
+ } catch (error) {
+ console.error('[MEMBER] 获取云仓列表失败', error)
+ warehouseList.value = []
+ } finally {
+ warehouseLoading.value = false
+ }
+}
+
+/** 拉取进货订单(分页接口,取前 20 条) */
+const loadOrders = async () => {
+ orderLoading.value = true
+ try {
+ const res = await getMemberPurchaseOrders({ pageNum: 1, pageSize: 20 })
+ // 分页接口返回 TableDataInfo:数据在顶层 rows(非 data 包裹)
+ orderList.value = res.rows || []
+ } catch (error) {
+ console.error('[MEMBER] 获取进货订单失败', error)
+ orderList.value = []
+ } finally {
+ orderLoading.value = false
+ }
+}
+
+/** Tab 切换:进入「我的仓库」「进货订单」时按需懒加载 */
+const handleTabChange = (key) => {
+ if (activeTab.value === key) return
+ activeTab.value = key
+ if (key === 'warehouse' && warehouseList.value.length === 0) {
+ loadWarehouses()
+ }
+ if (key === 'orders' && orderList.value.length === 0) {
+ loadOrders()
+ }
+}
+
+/** 打开进货弹窗:清空上一次输入 */
+const handleOpenPurchase = (item) => {
+ purchaseTarget.value = item
+ purchaseQty.value = ''
+ purchaseUnitPrice.value = ''
+ purchaseVisible.value = true
+}
+
+/** 关闭进货弹窗 */
+const closePurchase = () => {
+ purchaseVisible.value = false
+}
+
+/** 提交进货:校验数量后调用接口,成功后刷新云仓列表 */
+const handleSubmitPurchase = async () => {
+ const qty = Number(purchaseQty.value)
+ if (!Number.isInteger(qty) || qty <= 0) {
+ uni.showToast({ title: '请输入正确的进货数量', icon: 'none' })
+ return
+ }
+ // 单价选填:元 → 分,未填或非法则不传(后端按 0 计)
+ const unitPriceYuan = Number(purchaseUnitPrice.value)
+ const unitPrice =
+ Number.isFinite(unitPriceYuan) && unitPriceYuan > 0 ? Math.round(unitPriceYuan * 100) : undefined
+ try {
+ await memberPurchase({ warehouseId: purchaseTarget.value.warehouseId, qty, unitPrice })
+ uni.showToast({ title: '进货成功', icon: 'success' })
+ purchaseVisible.value = false
+ loadWarehouses()
+ } catch (error) {
+ console.error('[MEMBER] 进货失败', error)
+ uni.showToast({ title: error.message || '进货失败', icon: 'none' })
+ }
+}
+
+/** 打开仓库记录弹窗:拉取流水并按当前商品名筛选 */
+const handleOpenRecords = async (item) => {
+ recordsProductName.value = item.productName
+ warehouseRecords.value = []
+ recordsVisible.value = true
+ try {
+ const res = await getMemberWarehouseRecords({ pageNum: 1, pageSize: 50 })
+ const rows = res.rows || []
+ // 记录接口按会员维度下发,这里按商品名筛选,展示该商品的变动明细
+ warehouseRecords.value = rows.filter((record) => record.productName === item.productName)
+ } catch (error) {
+ console.error('[MEMBER] 获取仓库记录失败', error)
+ }
+}
+
+/** 关闭仓库记录弹窗 */
+const closeRecords = () => {
+ recordsVisible.value = false
+}
+
+/** 等级详情:后端暂未提供等级规则接口,先给出占位反馈 */
+const handleLevelDetail = () => {
+ uni.showToast({ title: '等级详情敬请期待', icon: 'none' })
+}
+
+/** 返回:优先返回上一页,无上一页时回个人中心(Tab 页) */
+const handleBack = () => {
+ const pages = getCurrentPages()
+ if (pages.length > 1) {
+ uni.navigateBack()
+ return
+ }
+ uni.switchTab({ url: '/pages/profile/index' })
+}
+
+/** 页面加载:校验登录态后拉取销售统计(仓库与订单按 Tab 懒加载) */
+onLoad(() => {
+ if (!ensureLogin()) return
+ loadStatistics()
+})
+</script>
+
+<style scoped lang="scss">
+.page {
+ height: 100vh;
+ display: flex;
+ flex-direction: column;
+ box-sizing: border-box;
+ background-color: #f8fafc;
+}
+
+/* ==================== 自定义导航栏 ==================== */
+
+.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 #0f172a;
+ border-bottom: 4rpx solid #0f172a;
+ transform: rotate(45deg);
+}
+
+.nav-title {
+ font-size: 34rpx;
+ font-weight: 700;
+ line-height: 1;
+ letter-spacing: 1rpx;
+ color: #0f172a;
+}
+
+.scroll {
+ flex: 1;
+ overflow: hidden;
+}
+
+/* ==================== 会员信息深蓝卡 ==================== */
+
+.member-card {
+ margin: 24rpx 32rpx 0;
+ padding: 40rpx 32rpx;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ box-sizing: border-box;
+ border-radius: 24rpx;
+ /* 深蓝渐变:与设计稿 #003E7E → #1E5EBC 一致 */
+ background: linear-gradient(135deg, #003e7e 0%, #1e5ebc 100%);
+ box-shadow: 0 12rpx 32rpx rgba(0, 62, 126, 0.24);
+}
+
+.member-left {
+ display: flex;
+ flex-direction: column;
+ gap: 20rpx;
+}
+
+.member-phone {
+ font-size: 28rpx;
+ line-height: 1;
+ letter-spacing: 1rpx;
+ color: rgba(255, 255, 255, 0.88);
+}
+
+.member-level-line {
+ display: flex;
+ align-items: baseline;
+ gap: 16rpx;
+}
+
+.member-level-label {
+ font-size: 28rpx;
+ font-weight: 500;
+ line-height: 1;
+ color: #ffffff;
+}
+
+.member-level-value {
+ font-size: 56rpx;
+ font-weight: 800;
+ line-height: 1;
+ letter-spacing: 2rpx;
+ color: #ffffff;
+}
+
+/* 草绿主按钮:等级详情 */
+.level-detail-btn {
+ flex-shrink: 0;
+ height: 64rpx;
+ display: flex;
+ align-items: center;
+ padding: 0 30rpx;
+ box-sizing: border-box;
+ border-radius: 12rpx;
+ background: linear-gradient(135deg, #82c236 0%, #639c1f 100%);
+ box-shadow: 0 6rpx 16rpx rgba(99, 156, 31, 0.32);
+}
+
+.level-detail-text {
+ font-size: 26rpx;
+ font-weight: 600;
+ line-height: 1;
+ color: #ffffff;
+}
+
+/* ==================== 三段式 Tab ==================== */
+
+.tabs {
+ margin-top: 24rpx;
+ display: flex;
+ align-items: center;
+ background-color: #ffffff;
+ border-bottom: 2rpx solid #e2e8f0;
+}
+
+.tab-item {
+ flex: 1;
+ height: 88rpx;
+ position: relative;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.tab-text {
+ font-size: 30rpx;
+ line-height: 1;
+ color: #64748b;
+}
+
+.tab-text--active {
+ font-weight: 600;
+ color: #639c1f;
+}
+
+/* 选中态底部指示条(草绿) */
+.tab-indicator {
+ position: absolute;
+ bottom: 0;
+ left: 50%;
+ transform: translateX(-50%);
+ width: 96rpx;
+ height: 6rpx;
+ border-radius: 6rpx;
+ background-color: #82c236;
+}
+
+.panel {
+ padding: 24rpx 32rpx 60rpx;
+ display: flex;
+ flex-direction: column;
+ gap: 24rpx;
+}
+
+/* ==================== 通用卡片 ==================== */
+
+.stat-card,
+.chart-card {
+ padding: 28rpx;
+ box-sizing: border-box;
+ border-radius: 20rpx;
+ background-color: #ffffff;
+ border: 2rpx solid #e2e8f0;
+ box-shadow: 0 6rpx 16rpx rgba(15, 23, 42, 0.05);
+}
+
+/* ==================== Tab1:销售统计 ==================== */
+
+.stat-head {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+}
+
+.stat-head-label {
+ font-size: 26rpx;
+ line-height: 1;
+ color: #64748b;
+}
+
+.stat-head-growth {
+ font-size: 24rpx;
+ line-height: 1;
+ color: #e11d48;
+}
+
+.stat-amount {
+ display: block;
+ margin-top: 20rpx;
+ font-size: 52rpx;
+ font-weight: 800;
+ line-height: 1;
+ color: #0f172a;
+}
+
+.stat-sub {
+ display: block;
+ margin-top: 14rpx;
+ font-size: 24rpx;
+ line-height: 1;
+ color: #94a3b8;
+}
+
+.stat-cols {
+ margin-top: 28rpx;
+ padding-top: 28rpx;
+ display: flex;
+ align-items: center;
+ border-top: 2rpx solid #f1f5f9;
+}
+
+.stat-col {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 12rpx;
+}
+
+.stat-col-label {
+ font-size: 24rpx;
+ line-height: 1;
+ color: #64748b;
+}
+
+.stat-col-value {
+ font-size: 30rpx;
+ font-weight: 700;
+ line-height: 1;
+ color: #0f172a;
+}
+
+.chart-title {
+ display: block;
+ font-size: 28rpx;
+ font-weight: 600;
+ line-height: 1;
+ color: #0f172a;
+ text-align: center;
+}
+
+.chart-box {
+ margin-top: 20rpx;
+ height: 400rpx;
+}
+
+/* 图表空态 */
+.chart-empty {
+ margin-top: 20rpx;
+ height: 200rpx;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.chart-empty-text {
+ font-size: 26rpx;
+ line-height: 1;
+ color: #94a3b8;
+}
+
+/* 环形图 + 图例:左图右文 */
+.ratio-wrap {
+ margin-top: 20rpx;
+ display: flex;
+ align-items: center;
+}
+
+.ratio-chart {
+ width: 260rpx;
+ height: 260rpx;
+ flex-shrink: 0;
+}
+
+.ratio-legend {
+ flex: 1;
+ min-width: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 20rpx;
+}
+
+.legend-row {
+ display: flex;
+ align-items: center;
+ gap: 12rpx;
+}
+
+.legend-dot {
+ flex-shrink: 0;
+ width: 16rpx;
+ height: 16rpx;
+ border-radius: 4rpx;
+}
+
+.legend-name {
+ flex: 1;
+ min-width: 0;
+ font-size: 24rpx;
+ line-height: 1;
+ color: #475569;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+}
+
+.legend-value {
+ flex-shrink: 0;
+ font-size: 24rpx;
+ font-weight: 600;
+ line-height: 1;
+ color: #0f172a;
+}
+
+/* 销售用户排行 */
+.rank-list {
+ margin-top: 12rpx;
+ display: flex;
+ flex-direction: column;
+}
+
+.rank-row {
+ height: 92rpx;
+ display: flex;
+ align-items: center;
+ gap: 20rpx;
+ border-bottom: 2rpx solid #f1f5f9;
+}
+
+.rank-row:last-child {
+ border-bottom: none;
+}
+
+.rank-index {
+ flex-shrink: 0;
+ width: 44rpx;
+ height: 44rpx;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 50%;
+ background-color: #e8f1fc;
+}
+
+.rank-index-text {
+ font-size: 24rpx;
+ font-weight: 700;
+ line-height: 1;
+ color: #1e5ebc;
+}
+
+.rank-name {
+ flex-shrink: 0;
+ max-width: 160rpx;
+ font-size: 28rpx;
+ font-weight: 500;
+ line-height: 1;
+ color: #0f172a;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+}
+
+.rank-phone {
+ flex: 1;
+ min-width: 0;
+ font-size: 26rpx;
+ line-height: 1;
+ color: #94a3b8;
+}
+
+.rank-amount {
+ flex-shrink: 0;
+ font-size: 28rpx;
+ font-weight: 600;
+ line-height: 1;
+ color: #0f172a;
+}
+
+/* ==================== Tab2:我的仓库 ==================== */
+
+.warehouse-list {
+ display: flex;
+ flex-direction: column;
+ gap: 24rpx;
+}
+
+.warehouse-card {
+ border-radius: 20rpx;
+ overflow: hidden;
+ background-color: #ffffff;
+ border: 2rpx solid #e2e8f0;
+ box-shadow: 0 6rpx 16rpx rgba(15, 23, 42, 0.05);
+}
+
+/* 低库存警示条:蓝底白字 */
+.low-stock-bar {
+ display: flex;
+ align-items: center;
+ gap: 12rpx;
+ padding: 18rpx 24rpx;
+ background-color: #2f86e8;
+}
+
+.low-stock-icon {
+ flex-shrink: 0;
+ width: 32rpx;
+ height: 32rpx;
+ border-radius: 50%;
+ border: 3rpx solid #ffffff;
+ box-sizing: border-box;
+}
+
+.low-stock-bar-text {
+ flex: 1;
+ font-size: 24rpx;
+ line-height: 1.3;
+ color: #ffffff;
+}
+
+.warehouse-row {
+ padding: 24rpx;
+ display: flex;
+ align-items: flex-start;
+ gap: 24rpx;
+}
+
+/* 商品图占位(#F1F5F9 底 + 图片图标) */
+.product-thumb {
+ flex-shrink: 0;
+ width: 160rpx;
+ height: 160rpx;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 12rpx;
+ overflow: hidden;
+ background-color: #f1f5f9;
+}
+
+.product-thumb-img {
+ width: 100%;
+ height: 100%;
+}
+
+.warehouse-main {
+ flex: 1;
+ min-width: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 16rpx;
+}
+
+.product-name {
+ font-size: 28rpx;
+ font-weight: 600;
+ line-height: 1.35;
+ color: #0f172a;
+}
+
+.warehouse-stock {
+ display: flex;
+ align-items: center;
+ flex-wrap: wrap;
+}
+
+.stock-label {
+ font-size: 24rpx;
+ line-height: 1;
+ color: #64748b;
+}
+
+.stock-label--gap {
+ margin-left: 24rpx;
+}
+
+.stock-value {
+ margin-left: 6rpx;
+ font-size: 26rpx;
+ font-weight: 600;
+ line-height: 1;
+ color: #0f172a;
+}
+
+/* 低库存数值标红 */
+.stock-value--low {
+ color: #ef4444;
+}
+
+.warehouse-actions {
+ display: flex;
+ align-items: center;
+ gap: 20rpx;
+}
+
+/* 草绿按钮:进货 / 仓库记录 */
+.btn-green {
+ height: 64rpx;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 0 36rpx;
+ box-sizing: border-box;
+ border-radius: 10rpx;
+ background: linear-gradient(135deg, #82c236 0%, #639c1f 100%);
+ box-shadow: 0 6rpx 16rpx rgba(99, 156, 31, 0.26);
+}
+
+.btn-green--fill {
+ flex: 1;
+}
+
+.btn-green-text {
+ font-size: 26rpx;
+ font-weight: 600;
+ line-height: 1;
+ color: #ffffff;
+}
+
+/* ==================== Tab3:进货订单 ==================== */
+
+.order-list {
+ display: flex;
+ flex-direction: column;
+ gap: 24rpx;
+}
+
+.order-card {
+ padding: 28rpx;
+ display: flex;
+ flex-direction: column;
+ gap: 16rpx;
+ border-radius: 20rpx;
+ background-color: #ffffff;
+ border: 2rpx solid #e2e8f0;
+ box-shadow: 0 6rpx 16rpx rgba(15, 23, 42, 0.05);
+}
+
+.order-head {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+}
+
+.order-no {
+ font-size: 24rpx;
+ line-height: 1;
+ color: #94a3b8;
+}
+
+.order-status {
+ font-size: 24rpx;
+ font-weight: 600;
+ line-height: 1;
+ color: #639c1f;
+}
+
+.order-product {
+ font-size: 28rpx;
+ font-weight: 600;
+ line-height: 1.35;
+ color: #0f172a;
+}
+
+.order-foot {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+}
+
+.order-qty {
+ font-size: 26rpx;
+ line-height: 1;
+ color: #64748b;
+}
+
+.order-amount {
+ font-size: 32rpx;
+ font-weight: 700;
+ line-height: 1;
+ color: #0f172a;
+}
+
+.order-time {
+ font-size: 22rpx;
+ line-height: 1;
+ color: #cbd5e1;
+}
+
+/* ==================== 空态 ==================== */
+
+.empty {
+ padding: 140rpx 0 0;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 16rpx;
+}
+
+.empty-title {
+ font-size: 30rpx;
+ font-weight: 600;
+ line-height: 1;
+ color: #475569;
+}
+
+.empty-desc {
+ font-size: 26rpx;
+ line-height: 1;
+ color: #94a3b8;
+}
+
+/* ==================== 弹窗(进货 / 仓库记录) ==================== */
+
+.mask {
+ position: fixed;
+ left: 0;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ z-index: 100;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 0 60rpx;
+ box-sizing: border-box;
+ background-color: rgba(15, 23, 42, 0.45);
+}
+
+.dialog {
+ width: 100%;
+ padding: 40rpx 32rpx;
+ box-sizing: border-box;
+ border-radius: 24rpx;
+ background-color: #ffffff;
+ display: flex;
+ flex-direction: column;
+ gap: 24rpx;
+}
+
+.dialog-title {
+ font-size: 34rpx;
+ font-weight: 700;
+ line-height: 1;
+ color: #0f172a;
+ text-align: center;
+}
+
+.dialog-product {
+ font-size: 26rpx;
+ line-height: 1.3;
+ color: #64748b;
+ text-align: center;
+}
+
+.dialog-sub {
+ font-size: 24rpx;
+ line-height: 1;
+ color: #94a3b8;
+ text-align: center;
+}
+
+.form-row {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 24rpx;
+ height: 88rpx;
+ padding: 0 24rpx;
+ box-sizing: border-box;
+ border-radius: 12rpx;
+ background-color: #f8fafc;
+}
+
+.form-label {
+ flex-shrink: 0;
+ font-size: 28rpx;
+ line-height: 1;
+ color: #334155;
+}
+
+.form-input {
+ flex: 1;
+ height: 88rpx;
+ font-size: 28rpx;
+ line-height: 88rpx;
+ text-align: right;
+ color: #0f172a;
+}
+
+.form-placeholder {
+ color: #cbd5e1;
+}
+
+.dialog-actions {
+ margin-top: 8rpx;
+ display: flex;
+ align-items: center;
+ gap: 24rpx;
+}
+
+.btn-plain {
+ flex: 1;
+ height: 88rpx;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ box-sizing: border-box;
+ border-radius: 44rpx;
+ background-color: #f1f5f9;
+}
+
+.btn-plain-text {
+ font-size: 28rpx;
+ font-weight: 600;
+ line-height: 1;
+ color: #475569;
+}
+
+/* 弹窗内草绿按钮:撑满、圆角胶囊 */
+.dialog-actions .btn-green,
+.dialog .btn-green--fill {
+ height: 88rpx;
+ border-radius: 44rpx;
+}
+
+/* 仓库记录弹窗:列表可滚动 */
+.dialog--records {
+ max-height: 76vh;
+}
+
+.record-scroll {
+ max-height: 640rpx;
+}
+
+.record-row {
+ padding: 20rpx 0;
+ display: flex;
+ flex-direction: column;
+ gap: 12rpx;
+ border-bottom: 2rpx solid #f1f5f9;
+}
+
+.record-line1,
+.record-line2 {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 16rpx;
+}
+
+.record-product {
+ flex: 1;
+ min-width: 0;
+ font-size: 26rpx;
+ font-weight: 600;
+ line-height: 1;
+ color: #0f172a;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+}
+
+.record-change {
+ flex-shrink: 0;
+ font-size: 28rpx;
+ font-weight: 700;
+ line-height: 1;
+}
+
+.record-change--in {
+ color: #639c1f;
+}
+
+.record-change--out {
+ color: #ef4444;
+}
+
+.record-type {
+ font-size: 24rpx;
+ line-height: 1;
+ color: #64748b;
+}
+
+.record-after,
+.record-time {
+ font-size: 22rpx;
+ line-height: 1;
+ color: #94a3b8;
+}
+
+.record-empty {
+ padding: 60rpx 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.record-empty-text {
+ font-size: 26rpx;
+ line-height: 1;
+ color: #94a3b8;
+}
+
+/* ==================== 图标(内联 base64 SVG) ==================== */
+
+.icon {
+ flex-shrink: 0;
+ background-repeat: no-repeat;
+ background-position: center center;
+ background-size: 100% 100%;
+}
+
+/* 商品图占位图标(#CBD5E1) */
+.icon-image {
+ width: 64rpx;
+ height: 64rpx;
+ background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjQ0JENUUxIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCI+PHJlY3Qgd2lkdGg9IjE4IiBoZWlnaHQ9IjE4IiB4PSIzIiB5PSIzIiByeD0iMiIgcnk9IjIiLz48Y2lyY2xlIGN4PSI5IiBjeT0iOSIgcj0iMiIvPjxwYXRoIGQ9Im0yMSAxNS0zLjA4Ni0zLjA4NmEyIDIgMCAwIDAtMi44MjggMEw2IDIxIi8+PC9zdmc+');
+}
+</style>
diff --git a/hugray-user-miniapp/pages/profile/index.vue b/hugray-user-miniapp/pages/profile/index.vue
index d41373d..0e26faf 100644
--- a/hugray-user-miniapp/pages/profile/index.vue
+++ b/hugray-user-miniapp/pages/profile/index.vue
@@ -169,6 +169,11 @@
uni.navigateTo({ url: '/pages/profile/invite' })
return
}
+ // 会员中心:跳转会员中心页(销售统计 / 我的仓库 / 进货订单)
+ if (menu.key === 'member') {
+ uni.navigateTo({ url: '/pages/member/index' })
+ return
+ }
uni.showToast({ title: '敬请期待', icon: 'none' })
}
--
Gitblit v1.7.1