Files
hfb_sys/docs/order-testing-messaging-analysis.md
T
2026-05-24 11:17:10 +08:00

509 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# HFB Sys 订单购买测试 & 消息系统增强 —— 综合分析报告
> 生成日期:2026-05-24
> 状态:待实施
---
## 第一部分:如何进行实际的订单购买测试
### 1. 当前完整订单购买链路
从前端到后端,当前链路如下:
```
租客浏览商品列表 → 点击商品详情 → 点击"立即租赁"
MobileListingDetailView.vue 调用 createOrder(listingId)
POST /api/orders {listing_id}
handler.CreateOrder() → 验证 listing 状态(published & approved)
service.CreateOrder(ctx, userID, listingID)
repository.Create(tx) → 一条大事务:
├── SELECT listing + account FOR UPDATE (悲观锁防并发)
├── 校验: listing.status=published, account.status=published
├── INSERT rental_orders (status=pending_handoff, rent_hours=24 硬编码)
├── UPDATE listing.status → rented, account.status → rented
├── INSERT account_snapshot (JSON 字段, 记录下单时资产快照)
├── INSERT wallet_ledger (order_lock, 模拟冻结 rent_amount + deposit_amount)
├── UPDATE wallet_accounts (frozen_balance ↑)
├── notification.Append() → 通知号主+租客
└── COMMIT
```
**关键发现——无支付环节**`POST /api/orders` 调用后直接进入 `pending_handoff`,完全没有支付确认步骤。这是因为项目计划明确说"一期先做账务模型,不直接接真实支付和提现"。
### 2. 当前存在的 9 个关键缺失
| # | 缺失项 | 影响 | 涉及文件 |
|---|--------|------|----------|
| 1 | **无支付确认步骤** | 订单创建即生效,无资金冻结验证 | `order/repository.go` Create() |
| 2 | **租期硬编码 24h** | `internalOrderHours = 24`,用户无法选择 | `order/repository.go:21` |
| 3 | **无余额校验** | 下单时未检查租客 `available_balance >= rent_amount + deposit_amount` | `order/repository.go` Create() |
| 4 | **前端余额硬编码** | `MobileProfileView.vue` 写死 `¥0.00`,未调 API | `MobileProfileView.vue` |
| 5 | **钱包首次查询才创建** | `GET /api/wallet/balance` 首次访问才初始化账户 | `wallet/repository.go` |
| 6 | **无充值入口** | 没有任何充值接口或页面 | 全项目 |
| 7 | **站内信前端空壳** | `MobileMessagesView.vue` 只显示 `van-empty` | `MobileMessagesView.vue` |
| 8 | **无租期即将结束提醒** | 项目计划中规划了但未实现 | 超时扫描任务 |
| 9 | **短信完全未接** | 只有登录验证码的 mock,业务通知短信完全缺失 | `integrations/sms/` |
### 3. 要完成端到端测试必须补充的最小功能集
```
必须在测试前补充的功能(按依赖顺序):
第1步:余额充值(测试前置条件)
└── POST /api/wallet/recharge {amount} → 给测试用户加余额
第2步:租期选择(核心流程)
└── 前端:下单时增加租期选择组件
└── 后端:CreateOrderDTO 增加 rent_hours 字段
第3步:支付确认步骤(状态机改造)
└── 新增订单状态:pending_payment
└── POST /api/orders → pending_payment(而非直接 pending_handoff
└── POST /api/orders/{id}/pay → 校验余额 → 扣款 → pending_handoff
第4步:前端站内信接入
└── MobileMessagesView.vue 接入 fetchNotifications API
└── 底部导航栏增加未读消息角标
```
### 4. 支付确认步骤的推荐设计
```
建议的状态机改造(最小侵入):
POST /api/orders
[新增] pending_payment(待支付,默认 15 分钟超时自动取消)
POST /api/orders/{id}/pay
├── 校验:订单归属租客、status=pending_payment、未超时
├── 校验:wallet_accounts.available_balance >= rent_amount + deposit_amount
├── UPDATE wallet_accounts: available_balance -= total, frozen_balance += total
├── INSERT wallet_ledger: order_lock (冻结流水)
├── UPDATE rental_orders: status → pending_handoff
├── UPDATE listing + account: status → rented
└── notification.Append() → 通知双方
```
对应的新 API
- `POST /api/orders/{id}/pay` — 确认支付
- `GET /api/orders/{id}/payment-status` — 查询支付状态
- 超时扫描任务新增:`pending_payment` 超过 15 分钟 → 自动取消
### 5. 完整测试场景清单
#### 5.1 正常流程测试
| 场景 | 步骤 | 验证点 |
|------|------|--------|
| 完整租赁闭环 | 浏览→下单→支付→号主交接→收号确认→租赁→结账→号主确认→完成 | 所有状态流转正确,资金流水完整 |
| 租期选择 | 选择不同租期(1h/3h/12h/24h) | rent_end_at 正确计算,租金按比例 |
| 多商品下单 | 不同商品分别下单 | 互不影响,各自锁定 |
| 结账修改流程 | 号主修改结账金额→租客同意修正 | 状态流转:pending_checkout_confirm → pending_checkout_accept → completed |
#### 5.2 边界与异常测试
| 场景 | 验证点 |
|------|--------|
| 余额不足下单 | 返回明确错误,不创建订单 |
| 已租出商品下单 | `SELECT FOR UPDATE` 后检测到 status≠published,拒绝 |
| 审核未通过商品下单 | 拒绝(review_status≠approved |
| 自己租自己 | 拒绝(owner_id = renter_id |
| 未实名租客下单 | 根据 system_config 的 `realname_required_for_order` 配置 |
| 非法状态转换 | 已取消订单不能再次取消,已完成订单不能结账等 |
| 结账争议流程 | 任一方发起争议→客服仲裁→完成/关闭 |
#### 5.3 并发测试
| 场景 | 验证点 |
|------|--------|
| 2 人同时下单同一商品 | `SELECT FOR UPDATE` 保证只有一人成功 |
| 同时支付+超时取消 | 互斥,不会出现既支付又取消 |
| 并发结算 | wallet_ledger 不出现负数或重复入账 |
| 并发创建订单 | 库存锁定正确 |
#### 5.4 超时测试(依赖超时扫描任务)
| 超时类型 | 默认阈值 | 期望行为 |
|----------|----------|----------|
| 待支付超时 | 15 min | 订单自动取消 |
| 号主待交接超时 | 30 min | handoff_status → owner_timeout,通知双方 |
| 租客确认收号超时 | 30 min | status → abnormal,客服介入 |
| 租客逾期未结账 | 10 min 宽限 | status → overdue |
| 号主确认结账超时 | 120 min | status → abnormal |
#### 5.5 安全测试
| 场景 | 验证点 |
|------|--------|
| 未登录访问 | 所有写接口返回 401 |
| 跨用户操作 | A 不能取消 B 的订单,不能确认 B 的收号 |
| 后台接口保护 | 普通用户 token 不能访问 /api/admin/* |
| 重复提交 | 防重机制(前端 loading + 后端幂等) |
| SQL 注入 | 所有查询通过 GORM 参数化 |
| 越权查看 | 用户不能查看他人订单、钱包、交接记录 |
### 6. 具体测试实施方案
#### 6.1 测试环境搭建
```bash
# 1. 启动基础设施
docker compose -f deploy/docker-compose.dev.yml up -d
# 2. 初始化数据库
docker exec -i hfb-mysql mysql -uhfb -psecret hfb_sys < backend/migrations/000001_init.sql
# 3. 启动后端
cd backend && cp .env.example .env && go run ./cmd/api
# 4. 启动前端
cd frontend && npm install && npm run dev
```
#### 6.2 手动端到端测试步骤(核心路径)
```
前置条件:准备两个测试手机号(号主A + 租客B)
Step 1: 号主A 注册/登录
POST /api/auth/sms/send {phone: "138xxxx0001"}
POST /api/auth/sms/login {phone: "138xxxx0001", code: "从日志获取"}
Step 2: 号主A 实名认证(mock
POST /api/realname/start {name: "张三", id_no: "110101199001011234"}
Step 3: 号主A 发布商品
POST /api/listings {
game_name: "三角洲行动",
server_region: "微信区",
haf_coin_amount: 5000000,
price_hourly: 10,
deposit_amount: 100,
min_rent_hours: 1,
max_rent_hours: 24
}
POST /api/listings/{id}/submit-review
Step 4: 后台审核通过
POST /api/admin/auth/login {username: "admin", password: "admin123456", captcha: "..."}
POST /api/admin/listings/{id}/approve
Step 5: 租客B 注册/登录
(同 Step 1,使用 138xxxx0002)
Step 6: 租客B 充值(当前需手动)
直接改数据库或通过调试接口给租客B加余额
Step 7: 租客B 浏览并下单
GET /api/listings → 查看商品
POST /api/orders {listing_id, rent_hours: 3}
Step 8: 租客B 支付(当前直接跳到 pending_handoff,后续需补充)
验证订单状态
Step 9: 号主A 提交交接说明
POST /api/orders/{id}/handoff {content: "登录方式:..."}
Step 10: 租客B 确认收号
POST /api/orders/{id}/confirm-receive
Step 11: 租客B 发起结账
POST /api/orders/{id}/checkout {
content: "使用结束",
consumable_amount: 30,
coin_consumed_m: 0.5
}
Step 12: 号主A 确认结账
POST /api/orders/{id}/checkout/confirm
Step 13: 验证结果
GET /api/orders/{id} → status=completed
GET /api/wallet/ledger → 流水完整
```
#### 6.3 自动化测试建议
| 层级 | 工具 | 覆盖内容 |
|------|------|----------|
| 单元测试 | Go testing + testify | Repository 的事务方法,Service 参数校验 |
| 集成测试 | Go testing + testcontainers | 真实 MySQL/Redis 下的完整流程 |
| API 测试 | Postman/Newman 或 Go httptest | 所有 API 端点的正常+异常路径 |
| E2E 测试 | Playwright 或 Cypress | 前端页面操作完整流程 |
| 并发测试 | Go benchmark + goroutines | 并发下单、并发结算 |
---
## 第二部分:应增加什么消息系统
### 1. 现有消息系统现状评估
#### 1.1 站内信体系
**好消息**:后端站内信的写入非常完整——所有 18 个关键业务节点都在事务内通过 `notification.Append(tx, ...)` 写入,覆盖了订单全生命周期的每个状态变更。
| 文件 | notification.Append 调用次数 | 场景 |
|------|------------------------------|------|
| `order/repository.go` | 9 次 | 创建/取消/交接/收号/结账/修改结账/客服关闭/标记异常/完成结算 |
| `dispute/repository.go` | 4 次 | 发起申诉(2)/仲裁完成(2) |
| `ordertimeout/job.go` | 4 次 | 号主超时/收号超时/逾期/确认结账超时 |
**坏消息**:前端完全没接——`MobileMessagesView.vue` 只有一个 `van-empty` 组件,`Desktop NotificationsView.vue` 虽然接入了 API 但缺乏未读数角标和分类。
#### 1.2 表结构分析
```sql
notifications (
id, user_id, type, title, content,
biz_type, biz_id, read_at, created_at
)
```
**当前优点**
- 简洁高效
- `(user_id, read_at, created_at)` 联合索引支持快速分页查询
**当前不足**
- 缺少 `category` 字段(订单消息/系统通知/公告无法区分)
- 缺少软删除标记
- 缺少 `extra_data` (JSON) 用于存储跳转链接等扩展信息
#### 1.3 对照项目计划的缺口
项目计划第 8 章规划的但**尚未实现**的场景:
| 规划场景 | 当前状态 | 优先级 |
|----------|----------|--------|
| 商品审核通过/拒绝通知号主 | ✅ 已实现 | — |
| 租期即将结束提醒 | ❌ 未实现 | 🔴 高 |
| 账户冻结/解冻通知用户 | ❌ 未实现 | 🟡 中 |
| 短信登录验证码 | ✅ mock | — |
| 租期即将到期短信 | ❌ 未实现 | 🔴 高 |
| 申诉被受理短信 | ❌ 未实现 | 🟡 中 |
| 仲裁结果短信 | ❌ 未实现 | 🟡 中 |
| WebSocket/SSE 实时推送 | ❌ 未实现 | 🟢 低 |
### 2. 推荐的消息系统增强架构
#### 2.1 站内信增强(Phase 1 — 立即做)
```
最小可行方案:
┌─────────────────────────────────────────────────┐
│ notifications 表扩展 │
├─────────────────────────────────────────────────┤
│ + category VARCHAR(32) -- order|system|announce │
│ + link_type VARCHAR(32) -- order_detail|... │
│ + link_id BIGINT │
│ + extra_data JSON -- 扩展数据 │
├─────────────────────────────────────────────────┤
│ 新增 API: │
│ GET /api/notifications/unread-count │
│ POST /api/notifications/batch-read │
└─────────────────────────────────────────────────┘
前端改动:
├── MobileMessagesView.vue:接入 fetchNotifications,消息列表
├── 底部导航栏:未读消息红点角标
├── Desktop NotificationsView.vue:增强消息分类和已读标记
└── Router:增加消息未读数 store
```
#### 2.2 短信系统增强(Phase 2 — 短期)
```
设计短信 Provider 抽象层:
internal/integrations/sms/
├── provider.go ← Provider 接口
│ type Provider interface {
│ Send(ctx, phone, templateCode string, params map[string]string) error
│ SendBatch(ctx, phones []string, ...) error
│ }
├── mock.go ← MockProvider(开发环境)
├── aliyun.go ← AliyunProvider(生产环境)
├── limiter.go ← 限流器(基于 Redis)
│ - 手机号维度:60s/次,10次/天
│ - IP 维度:100次/小时
│ - 场景维度:可配置
└── logger.go ← 发送日志(sms_logs 表)
短信触发场景:
├── 登录验证码(已有 mock
├── 租期即将到期提醒(新增 🔴)
├── 订单申诉被受理(新增 🟡)
├── 仲裁结果通知(新增 🟡)
└── 高风险安全提醒(预留)
```
#### 2.3 SSE 实时推送架构(Phase 3 — 中期)
```
推荐 SSEServer-Sent Events)而非 WebSocket
理由:本场景是单向推送(服务端→客户端),SSE 更轻量,
基于 HTTP,不需要额外协议升级,断线自动重连。
Go 侧架构:
┌──────────────────────────────────────┐
│ SSEBroker (单例) │
├──────────────────────────────────────┤
│ clients map[userID]*SSEClient │
│ Notify(userID, event) │
│ Broadcast(event) │
└──────────────────────────────────────┘
↓ 事件分发
┌──────────────────────────────────────┐
│ SSE Events: │
│ - order.status_changed │
│ - order.handoff_updated │
│ - order.checkout_updated │
│ - notification.new │
│ - dispute.updated │
└──────────────────────────────────────┘
Vue 侧:
// composables/useSSE.ts
const eventSource = new EventSource('/api/sse/stream?token=xxx')
eventSource.addEventListener('notification.new', (e) => {
const data = JSON.parse(e.data)
notificationStore.incrementUnread()
})
重连策略:
- EventSource 原生支持自动重连
- 可设置 retry interval: 3000ms
- 重连时重新验证 token
```
#### 2.4 订单全链路状态变更推送矩阵
```
订单状态变更 → 推送内容设计:
pending_payment → 租客: {type:"order.created", title:"订单已创建", orderId, amount, expireAt}
号主: {type:"order.new_rental", title:"有新的租用请求"}
pending_handoff → 租客: {type:"order.paid", title:"支付成功,等待号主交接"}
号主: {type:"order.pending_handoff", title:"请尽快完成账号交接"}
pending_renter_confirm → 租客: {type:"handoff.submitted", title:"号主已完成交接,请确认收号"}
renting → 号主: {type:"handoff.confirmed", title:"租客已确认收号,租赁开始"}
[租期结束前5分钟] → 租客: {type:"rental.expiring", title:"租期即将结束"}
pending_checkout_confirm → 号主: {type:"checkout.submitted", title:"租客已发起结账"}
completed → 双方: {type:"order.completed", title:"订单已完成"}
```
### 3. 数据库设计建议
#### 3.1 新增表
```sql
-- 短信发送日志
CREATE TABLE sms_logs (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
phone VARCHAR(20) NOT NULL,
template_code VARCHAR(64) NOT NULL,
template_params JSON,
status VARCHAR(16) NOT NULL DEFAULT 'pending', -- pending|sent|failed
provider VARCHAR(32) NOT NULL,
provider_msg_id VARCHAR(128),
error_msg VARCHAR(512),
biz_type VARCHAR(32),
biz_id BIGINT UNSIGNED,
user_id BIGINT UNSIGNED,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX idx_sms_logs_phone_created (phone, created_at),
INDEX idx_sms_logs_status (status)
);
-- SSE 推送记录(可选,用于对账)
CREATE TABLE push_records (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT UNSIGNED NOT NULL,
event_type VARCHAR(64) NOT NULL,
payload JSON,
delivered TINYINT DEFAULT 0,
delivered_at DATETIME,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX idx_push_records_user (user_id, created_at)
);
```
#### 3.2 notifications 表扩展
```sql
ALTER TABLE notifications
ADD COLUMN category VARCHAR(32) NOT NULL DEFAULT 'order'
COMMENT 'order|system|announce|security',
ADD COLUMN link_type VARCHAR(32) DEFAULT NULL
COMMENT 'order_detail|listing_detail|dispute_detail|wallet|url',
ADD COLUMN link_id BIGINT UNSIGNED DEFAULT NULL,
ADD COLUMN extra_data JSON DEFAULT NULL;
```
### 4. 实施优先级总览
```
Phase 1(立即 — 让消息系统可用)
├── ✅ 让 MobileMessagesView.vue 接入 fetchNotifications
├── ✅ 底部导航栏增加未读消息角标
├── ✅ 增加 GET /api/notifications/unread-count API
├── ✅ 增加 POST /api/notifications/batch-read API
└── ✅ 消息列表支持点击跳转到对应订单详情
Phase 2(短期 — 完善通知体系)
├── notifications 表扩展 category/link_type/extra_data
├── 实现租期即将结束通知(站内信 + 前端定时检查)
├── 实现用户冻结/解冻通知
├── 短信 Provider 抽象层 + 限流器
├── sms_logs 表 + 短信日志后台查看
└── 接入一个真实短信服务商
Phase 3(中期 — 实时体验)
├── SSE Broker 实现
├── 订单状态实时推送接入前端
├── 租期倒计时实时更新
├── 客服消息实时通知
└── push_records 表(可选)
```
---
## 附录:关键文件索引
### 后端核心文件
| 文件 | 说明 |
|------|------|
| `backend/internal/modules/order/repository.go` | 订单 Repository,核心业务逻辑(1089行) |
| `backend/internal/modules/order/handler.go` | 订单 HTTP Handler376行) |
| `backend/internal/modules/order/service.go` | 订单 Service 层(160行) |
| `backend/internal/modules/order/dto.go` | 订单 DTO 定义(126行) |
| `backend/internal/model/order.go` | RentalOrder 模型定义 |
| `backend/internal/modules/wallet/repository.go` | 钱包 Repository + AppendEntries |
| `backend/internal/modules/notification/repository.go` | 通知 Repository + Append |
| `backend/internal/modules/dispute/repository.go` | 争议仲裁 Repository |
| `backend/internal/jobs/ordertimeout/job.go` | 超时扫描定时任务 |
### 前端核心文件
| 文件 | 说明 |
|------|------|
| `frontend/src/views/mobile/MobileListingDetailView.vue` | 商品详情/下单页 |
| `frontend/src/views/mobile/MobileOrdersView.vue` | 订单列表页 |
| `frontend/src/views/mobile/MobileMessagesView.vue` | 消息页(空壳) |
| `frontend/src/views/mobile/MobileProfileView.vue` | 个人中心(余额硬编码) |
| `frontend/src/api/orders.ts` | 订单 API 定义 |
| `frontend/src/api/notifications.ts` | 通知 API 定义 |
| `frontend/src/api/wallet.ts` | 钱包 API 定义 |