design.md 16 KB

设计文档

概述

本功能为财务申请页面(accountLogApply.vue)添加商家选择器,使总后台管理员能够代表任意商家提交结算申请,同时保持普通商家用户只能为自己提交申请的权限隔离。

核心设计原则:

  • 基于用户角色的条件渲染
  • 前端和后端双重权限验证
  • 数据联动和自动填充
  • 用户体验优化(搜索、分页、加载状态)

架构

系统层次

┌─────────────────────────────────────────┐
│         用户界面层 (Vue Component)        │
│  - 商家选择器组件                         │
│  - 表单验证和提交                         │
│  - 角色判断和条件渲染                     │
└──────────────┬──────────────────────────┘
               │
┌──────────────▼──────────────────────────┐
│         API 层 (HTTP Requests)          │
│  - 商家列表查询 (/store/index)           │
│  - 商家详情查询 (/store/read)            │
│  - 提现申请提交 (/account/apply)         │
└──────────────┬──────────────────────────┘
               │
┌──────────────▼──────────────────────────┐
│      服务层 (Laravel Services)           │
│  - StoreService: 商家数据查询            │
│  - AccountService: 提现申请处理          │
└──────────────┬──────────────────────────┘
               │
┌──────────────▼──────────────────────────┐
│       数据层 (Database Models)           │
│  - Store: 商家信息                       │
│  - Member: 用户余额                      │
│  - AccountLog: 提现记录                  │
└─────────────────────────────────────────┘

角色判断流程

用户登录
    │
    ▼
获取用户信息 (user.store_id)
    │
    ├─── store_id === 0 或 null ──→ 总后台管理员
    │                                │
    │                                ▼
    │                          显示商家选择器
    │                          要求选择商家
    │
    └─── store_id > 0 ──→ 商家用户
                          │
                          ▼
                    隐藏商家选择器
                    自动使用当前商家ID

组件和接口

前端组件结构

1. 商家选择器组件 (MerchantSelector)

位置: accountLogApply.vue 中的 el-select 组件

Props/Data:

{
  isSuperAdmin: Boolean,        // 是否为超级管理员
  applyForm: {
    store_id: Number,           // 选中的商家ID
    user_id: Number,            // 商家对应的用户ID
    name: String,               // 商家名称
    real_name: String,          // 真实姓名
    phone: String,              // 手机号
    balance: Number             // 可提现余额
  },
  storeOptions: Array,          // 商家选项列表
  storeSearchLoading: Boolean   // 搜索加载状态
}

Methods:

// 检查用户角色
checkUserRole(): void

// 搜索商家列表
searchStores(query: string): Promise<void>

// 处理商家选择变化
handleStoreChange(storeId: number): Promise<void>

// 加载商家信息(商家用户专用)
loadStoreInfoForMerchant(): Promise<void>

// 加载申请记录
getList(): Promise<void>

2. 角色检测实现

核心逻辑:

checkUserRole() {
  const user = this.user || {};
  // 关键判断:store_id === 0 或 null/undefined 表示总后台管理员
  // store_id > 0 表示商家用户
  this.isSuperAdmin = !user.store_id || user.store_id === 0;

  if (!this.isSuperAdmin) {
    // 商家用户:自动加载当前商家信息
    this.loadStoreInfoForMerchant();
  }
}

后端接口

1. 商家列表接口

端点: GET /admin/store/index

请求参数:

{
  page: number,      // 页码,默认 1
  limit: number,     // 每页数量,默认 15
  name: string,      // 商家名称(模糊搜索)
  status: number     // 商家状态,1-已审核
}

响应格式:

{
  code: 0,
  msg: "操作成功",
  data: [
    {
      id: number,           // 商家ID
      name: string,         // 商家名称
      phone: string,        // 手机号
      real_name: string,    // 真实姓名
      user_id: number,      // 关联的用户ID
      balance: number,      // 余额
      status: number        // 状态
    }
  ],
  count: number            // 总数
}

2. 商家详情接口

端点: GET /admin/store/read

请求参数:

{
  id: number  // 商家ID
}

响应格式:

{
  code: 0,
  msg: "操作成功",
  data: {
    id: number,
    name: string,
    phone: string,
    real_name: string,
    user_id: number,
    balance: number,
    // ... 其他商家信息
  }
}

3. 提现申请接口

端点: POST /admin/account/apply

请求参数:

{
  store_id: number,      // 商家ID(总后台管理员必填)
  user_id: number,       // 用户ID
  money: number,         // 提现金额
  real_name: string,     // 收款人姓名
  bank_name: string,     // 银行名称
  bank_account: string,  // 银行账号
  remark: string         // 备注
}

数据模型

Store (商家表)

{
  id: number,              // 主键
  user_id: number,         // 关联用户ID (外键 -> Member.id)
  name: string,            // 商家名称
  phone: string,           // 手机号
  real_name: string,       // 真实姓名
  status: number,          // 状态:0-待审核,1-已审核,2-已拒绝
  created_at: timestamp,   // 创建时间
  updated_at: timestamp    // 更新时间
}

Member (用户表)

{
  id: number,              // 主键
  store_id: number,        // 关联商家ID (0表示总后台管理员)
  username: string,        // 用户名
  phone: string,           // 手机号
  balance: decimal,        // 余额
  role_id: number,         // 角色ID
  created_at: timestamp,
  updated_at: timestamp
}

AccountLog (提现记录表)

{
  id: number,              // 主键
  user_id: number,         // 用户ID (外键 -> Member.id)
  store_id: number,        // 商家ID (外键 -> Store.id)
  money: decimal,          // 提现金额
  real_name: string,       // 收款人姓名
  bank_name: string,       // 银行名称
  bank_account: string,    // 银行账号
  status: number,          // 状态:0-待审核,1-已通过,2-已拒绝
  remark: string,          // 备注
  created_at: timestamp,
  updated_at: timestamp
}

数据关系

Member (用户)
  │
  ├─ store_id = 0 ──→ 总后台管理员
  │
  └─ store_id > 0 ──→ 商家用户
          │
          ▼
      Store (商家)
          │
          └─ user_id ──→ Member.id

正确性属性

属性是应该在系统所有有效执行中保持为真的特征或行为——本质上是关于系统应该做什么的正式声明。属性作为人类可读规范和机器可验证正确性保证之间的桥梁。

属性 1: 商家选择器角色可见性

对于任意 用户,当用户的 store_id 为 0 或 null 时,商家选择器组件应该可见;当 store_id 大于 0 时,商家选择器应该隐藏 验证需求: 1.1, 1.5, 3.2, 3.3

属性 2: 商家信息加载完整性

对于任意 选中的商家,系统加载的商家信息应该包含 user_id、name、real_name、phone 和 balance 字段,且这些字段的值应该与数据库中该商家的记录一致 验证需求: 1.2

属性 3: 商家切换信息更新

对于任意 两个不同的商家 A 和 B,当从商家 A 切换到商家 B 时,显示的商家信息(名称、手机号、余额)应该从 A 的信息变更为 B 的信息 验证需求: 1.3

属性 4: 提现记录 user_id 正确性

对于任意 商家和提现申请,创建的提现记录中的 user_id 字段应该等于选中商家的 user_id 验证需求: 1.4

属性 5: 搜索结果关键词匹配

对于任意 搜索关键词,返回的所有商家记录的名称或手机号字段应该包含该关键词(模糊匹配) 验证需求: 2.1, 5.2

属性 6: 商家选项显示完整性

对于任意 商家选项,渲染的选项文本应该包含商家名称、手机号和余额信息 验证需求: 2.3

属性 7: 申请记录商家关联性

对于任意 选中的商家,加载的申请记录列表中的所有记录的 user_id 应该等于该商家的 user_id 验证需求: 4.1, 4.2

属性 8: API 响应数据结构完整性

对于任意 商家列表 API 响应,返回的每个商家对象应该包含 id、name、phone、real_name、user_id 和 balance 字段 验证需求: 5.1

属性 9: 分页数据量正确性

对于任意 分页参数(page, limit),当总记录数大于等于 page * limit 时,返回的数据数量应该等于 limit;否则应该等于剩余记录数 验证需求: 5.3

属性 10: 商家详情数据完整性

对于任意 商家 ID,商家详情 API 返回的数据应该包含该商家的完整信息(id、name、phone、real_name、user_id、balance 等),且 balance 字段应该反映该商家关联用户的当前余额 验证需求: 5.4

错误处理

前端错误处理

1. 网络请求错误

  • 场景: API 请求失败(网络错误、超时、服务器错误)
  • 处理:
    • 显示友好的错误提示消息
    • 停止加载状态
    • 记录错误日志到控制台
    • 不影响其他功能的正常使用

2. 数据验证错误

  • 场景: 用户输入不符合要求
  • 处理:
    • 提现金额为空:显示"请输入提现金额"
    • 提现金额超过余额:显示"提现金额不能大于可用余额"
    • SuperAdmin 未选择商家:显示"请先选择商家",禁用提交按钮
    • 收款账号为空:显示"请输入收款账号"

3. 权限错误

  • 场景: 用户尝试访问无权限的功能
  • 处理:
    • 商家用户尝试查看其他商家数据:前端隐藏相关功能
    • 后端返回权限错误:显示"无权限访问"并跳转到首页

4. 数据加载错误

  • 场景: 商家信息或申请记录加载失败
  • 处理:
    • 显示错误提示
    • 保持表单可用状态
    • 允许用户重试

后端错误处理

1. 参数验证错误

  • 场景: 请求参数缺失或格式错误
  • 响应: javascript { code: 1, msg: "参数错误: [具体错误信息]", data: null }

2. 业务逻辑错误

  • 场景:
    • 商家不存在
    • 余额不足
    • 商家状态异常(未审核、已禁用)
  • 响应: javascript { code: 1, msg: "[具体业务错误信息]", data: null }

3. 权限验证错误

  • 场景:
    • 商家用户尝试访问其他商家数据
    • 未登录用户访问
  • 响应: javascript { code: 401, msg: "无权限访问", data: null }

4. 数据库错误

  • 场景: 数据库连接失败、查询错误
  • 处理:
    • 记录详细错误日志
    • 返回通用错误信息(不暴露内部细节)
    • 响应: javascript { code: 500, msg: "系统错误,请稍后重试", data: null }

边界情况处理

1. 空数据处理

  • 商家列表为空:显示"暂无商家数据"
  • 申请记录为空:显示"暂无申请记录"
  • 搜索无结果:显示"未找到匹配的商家"

2. 并发操作处理

  • 防止重复提交:提交按钮添加 loading 状态
  • 搜索防抖:输入关键词后延迟 300ms 再发起请求

3. 数据一致性

  • 商家信息变更后自动刷新余额
  • 提交申请成功后刷新申请记录列表
  • 切换商家时清空之前的数据

测试策略

单元测试

前端单元测试

测试框架: Jest + Vue Test Utils

测试范围:

  1. 角色判断逻辑测试

    • 测试 checkUserRole() 方法
    • 验证 store_id = 0 时 isSuperAdmin = true
    • 验证 store_id > 0 时 isSuperAdmin = false
    • 验证 store_id = null 时 isSuperAdmin = true
  2. 商家选择器显示/隐藏测试

    • 测试 SuperAdmin 时选择器可见
    • 测试 MerchantUser 时选择器隐藏
  3. 表单验证测试

    • 测试提现金额验证规则
    • 测试收款账号验证规则
    • 测试 SuperAdmin 商家选择验证
  4. 数据处理方法测试

    • 测试 handleStoreChange() 方法
    • 测试 searchStores() 方法
    • 测试数据清空逻辑

后端单元测试

测试框架: PHPUnit

测试范围:

  1. StoreService 测试

    • 测试商家列表查询
    • 测试商家搜索功能
    • 测试商家详情查询
    • 测试余额字段返回
  2. 权限验证测试

    • 测试 SuperAdmin 权限
    • 测试 MerchantUser 权限
    • 测试数据隔离逻辑
  3. 提现申请测试

    • 测试申请创建
    • 测试 user_id 关联
    • 测试余额验证

属性测试

测试框架:

  • 前端: fast-check (JavaScript property-based testing)
  • 后端: PHPUnit with random data generators

配置: 每个属性测试运行至少 100 次迭代

测试标注格式: **Feature: admin-merchant-selector, Property {number}: {property_text}**

属性测试列表:

  1. 属性 1: 商家选择器角色可见性

    • 生成随机用户对象(store_id 为 0、null 或正整数)
    • 验证选择器可见性与 store_id 的关系
  2. 属性 2: 商家信息加载完整性

    • 生成随机商家数据
    • 验证加载的信息包含所有必需字段
  3. 属性 3: 商家切换信息更新

    • 生成两个不同的随机商家
    • 验证切换后信息正确更新
  4. 属性 4: 提现记录 user_id 正确性

    • 生成随机商家和提现金额
    • 验证创建的记录 user_id 正确
  5. 属性 5: 搜索结果关键词匹配

    • 生成随机关键词和商家列表
    • 验证所有结果都包含关键词
  6. 属性 6: 商家选项显示完整性

    • 生成随机商家数据
    • 验证渲染的选项包含所有必需信息
  7. 属性 7: 申请记录商家关联性

    • 生成随机商家和申请记录
    • 验证记录列表只包含该商家的记录
  8. 属性 8: API 响应数据结构完整性

    • 生成随机商家列表
    • 验证每个对象包含所有必需字段
  9. 属性 9: 分页数据量正确性

    • 生成随机分页参数和数据集
    • 验证返回的数据量符合分页规则
  10. 属性 10: 商家详情数据完整性

    • 生成随机商家 ID
    • 验证返回的详情数据完整且余额正确

集成测试

测试范围:

  1. 端到端用户流程测试

    • SuperAdmin 登录 → 选择商家 → 提交申请 → 验证记录
    • MerchantUser 登录 → 自动加载信息 → 提交申请 → 验证记录
  2. API 集成测试

    • 测试前端与后端 API 的完整交互
    • 测试数据流转的正确性
  3. 权限隔离测试

    • 测试不同角色的数据访问权限
    • 测试跨商家数据访问被正确阻止

测试数据准备

测试数据库:

  • 创建测试用的 SuperAdmin 账号(store_id = 0)
  • 创建多个测试商家账号(store_id > 0)
  • 创建测试商家数据(不同状态、不同余额)
  • 创建测试申请记录

Mock 数据:

  • 商家列表 mock 数据
  • 申请记录 mock 数据
  • API 响应 mock 数据

测试覆盖率目标

  • 代码覆盖率: ≥ 80%
  • 分支覆盖率: ≥ 75%
  • 核心业务逻辑覆盖率: 100%