feat(wallet,tag): 钱包和标签系统多租户改造
核心变更: - 钱包表:删除 user_id,添加 resource_type/resource_id(绑定资源而非用户) - 标签表:添加 enterprise_id/shop_id(实现三级隔离:全局/企业/店铺) - GORM Callback:自动数据权限过滤 - 迁移脚本:可重复执行,已验证回滚功能 钱包归属重构原因: - 旧设计:钱包绑定用户账号,个人客户卡/设备转手后新用户无法使用余额 - 新设计:钱包绑定资源(卡/设备/店铺),余额随资源流转 标签三级隔离: - 平台全局标签:所有用户可见 - 企业标签:仅该企业可见(企业内唯一) - 店铺标签:该店铺及下级可见(店铺内唯一) 测试覆盖: - 9 个单元测试验证标签多租户过滤(全部通过) - 迁移和回滚功能测试通过(测试环境) - OpenSpec 验证通过 变更 ID: fix-wallet-tag-multi-tenant 迁移版本: 000008 参考: openspec/changes/archive/2026-01-13-fix-wallet-tag-multi-tenant/
This commit is contained in:
173
README.md
173
README.md
@@ -8,6 +8,179 @@
|
||||
|
||||
**技术栈**:Fiber + GORM + Viper + Zap + Lumberjack.v2 + Validator + sonic JSON + Asynq + PostgreSQL
|
||||
|
||||
---
|
||||
|
||||
## 核心业务说明
|
||||
|
||||
### 业务模式概览
|
||||
|
||||
君鸿卡管系统是一个物联网卡和号卡的全生命周期管理平台,支持三种客户类型和两种组织实体的多租户管理。
|
||||
|
||||
### 三种客户类型
|
||||
|
||||
| 客户类型 | 业务特点 | 典型场景 | 钱包归属 |
|
||||
|---------|---------|---------|---------|
|
||||
| **企业客户** | B端大客户,公对公支付 | 企业购买大量卡/设备用于业务运营 | ❌ 无钱包(后台直接分配套餐) |
|
||||
| **个人客户** | C端用户,微信登录 | 个人购买单卡或设备(含1-4张卡) | ✅ 钱包归属**卡/设备**(支持转手) |
|
||||
| **代理商** | 渠道分销商,层级管理 | 预存款采购套餐,按成本价+加价销售 | ✅ 钱包归属**店铺**(多账号共享) |
|
||||
|
||||
### 个人客户业务流程
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 个人客户使用流程 │
|
||||
└──────────────────────────┬──────────────────────────────────────┘
|
||||
│
|
||||
┌──────────▼──────────┐
|
||||
│ 1. 获得卡/设备 │
|
||||
│ - 单卡:ICCID │
|
||||
│ - 设备:设备号/IMEI │
|
||||
└──────────┬──────────┘
|
||||
│
|
||||
┌──────────▼──────────┐
|
||||
│ 2. 微信扫码登录 │
|
||||
│ - 输入ICCID/IMEI │
|
||||
│ - 首次需绑定手机号 │
|
||||
└──────────┬──────────┘
|
||||
│
|
||||
┌──────────▼──────────┐
|
||||
│ 3. 查看卡/设备信息 │
|
||||
│ - 流量使用情况 │
|
||||
│ - 套餐有效期 │
|
||||
│ - 钱包余额 │
|
||||
└──────────┬──────────┘
|
||||
│
|
||||
┌──────────▼──────────┐
|
||||
│ 4. 钱包充值 │
|
||||
│ - 微信支付 │
|
||||
│ - 支付宝支付 │
|
||||
└──────────┬──────────┘
|
||||
│
|
||||
┌──────────▼──────────┐
|
||||
│ 5. 购买套餐 │
|
||||
│ - 单卡套餐 │
|
||||
│ - 设备套餐(共享) │
|
||||
└──────────┬──────────┘
|
||||
│
|
||||
┌──────────▼──────────┐
|
||||
│ 6. 卡/设备转手 │
|
||||
│ - 新用户扫码登录 │
|
||||
│ - 钱包余额跟着走 │
|
||||
└─────────────────────┘
|
||||
```
|
||||
|
||||
### 钱包归属设计
|
||||
|
||||
#### 为什么钱包绑定资源(卡/设备)而非用户?
|
||||
|
||||
**问题场景**:
|
||||
```
|
||||
个人客户 A 购买单卡 → 充值 100 元 → 使用 50 元 → 转手给个人客户 B
|
||||
```
|
||||
|
||||
**如果钱包绑定用户**:
|
||||
- ❌ 个人客户 B 登录后看不到余额(钱包还在 A 账号下)
|
||||
- ❌ 需要手动转账或退款,体验极差
|
||||
|
||||
**钱包绑定资源(当前设计)**:
|
||||
- ✅ 个人客户 B 登录后看到剩余 50 元(钱包跟着卡走)
|
||||
- ✅ 无需任何额外操作,自然流转
|
||||
|
||||
#### 钱包归属规则
|
||||
|
||||
```go
|
||||
// 钱包模型
|
||||
type Wallet struct {
|
||||
ResourceType string // iot_card | device | shop
|
||||
ResourceID uint // 资源ID
|
||||
Balance int64 // 余额(分)
|
||||
// ...
|
||||
}
|
||||
|
||||
// 场景1:个人客户的单卡钱包
|
||||
resource_type = "iot_card"
|
||||
resource_id = 101 // 卡ID
|
||||
|
||||
// 场景2:个人客户的设备钱包(3张卡共享)
|
||||
resource_type = "device"
|
||||
resource_id = 1001 // 设备ID
|
||||
|
||||
// 场景3:代理商店铺钱包(多账号共享)
|
||||
resource_type = "shop"
|
||||
resource_id = 10 // 店铺ID
|
||||
```
|
||||
|
||||
### 设备套餐业务规则
|
||||
|
||||
#### 设备级套餐购买
|
||||
|
||||
```
|
||||
设备绑定 3 张 IoT 卡
|
||||
├── 卡1:ICCID-001
|
||||
├── 卡2:ICCID-002
|
||||
└── 卡3:ICCID-003
|
||||
|
||||
用户购买套餐:399 元/年,每月 3000G 流量
|
||||
├── 套餐分配:3 张卡都获得该套餐
|
||||
├── 流量共享:3000G/月 在 3 张卡之间共享(总共 3000G)
|
||||
├── 用户支付:399 元(一次性)
|
||||
└── 代理分佣:100 元(只分一次,不按卡数倍增)
|
||||
```
|
||||
|
||||
**关键点**:
|
||||
- ✅ 套餐自动分配到设备的所有卡
|
||||
- ✅ 流量是**设备级别共享**(非每卡独立)
|
||||
- ✅ 分佣**只计算一次**(防止重复分佣)
|
||||
|
||||
### 标签系统多租户隔离
|
||||
|
||||
#### 三级隔离模型
|
||||
|
||||
| 标签类型 | 创建者 | 可见范围 | 名称唯一性 | 示例 |
|
||||
|---------|-------|---------|-----------|------|
|
||||
| 平台全局标签 | 平台管理员 | 所有用户 | 全局唯一 | "VIP"、"重要客户" |
|
||||
| 企业标签 | 企业用户 | 仅该企业 | 企业内唯一 | 企业A的"测试标签" |
|
||||
| 店铺标签 | 代理商 | 该店铺及下级 | 店铺内唯一 | 店铺10的"华东区" |
|
||||
|
||||
#### 隔离规则
|
||||
|
||||
```
|
||||
企业 A 创建标签 "测试标签"
|
||||
├── enterprise_id = 5, shop_id = NULL
|
||||
├── 企业 A 的用户可见
|
||||
└── 企业 B 的用户不可见
|
||||
|
||||
企业 B 创建标签 "测试标签"(允许)
|
||||
├── enterprise_id = 8, shop_id = NULL
|
||||
├── 企业 B 的用户可见
|
||||
└── 与企业 A 的 "测试标签" 相互隔离
|
||||
|
||||
平台创建标签 "VIP"
|
||||
├── enterprise_id = NULL, shop_id = NULL
|
||||
└── 所有用户可见
|
||||
```
|
||||
|
||||
#### 数据权限自动过滤
|
||||
|
||||
```go
|
||||
// GORM Callback 自动注入过滤条件
|
||||
switch userType {
|
||||
case UserTypeAgent:
|
||||
// 代理用户:只看到自己店铺及下级店铺的标签
|
||||
db.Where("shop_id IN (?) OR (enterprise_id IS NULL AND shop_id IS NULL)", subordinateShopIDs)
|
||||
|
||||
case UserTypeEnterprise:
|
||||
// 企业用户:只看到自己企业的标签
|
||||
db.Where("enterprise_id = ? OR (enterprise_id IS NULL AND shop_id IS NULL)", enterpriseID)
|
||||
|
||||
default:
|
||||
// 个人客户:只看到全局标签
|
||||
db.Where("enterprise_id IS NULL AND shop_id IS NULL")
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 核心功能
|
||||
|
||||
- **认证中间件**:基于 Redis 的 Token 认证
|
||||
|
||||
Reference in New Issue
Block a user