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:
@@ -464,3 +464,161 @@ WHERE carrier_code LIKE '%CBN%' OR carrier_code LIKE '%广电%';
|
||||
3. 删除新增索引
|
||||
|
||||
**注意**:回滚会丢失所有新表的数据,请谨慎操作。
|
||||
|
||||
---
|
||||
|
||||
## 十三、变更历史
|
||||
|
||||
### 2026-01-13: 钱包和标签系统多租户改造(迁移 #000008)
|
||||
|
||||
**变更 ID**: `fix-wallet-tag-multi-tenant`
|
||||
|
||||
**变更原因**:
|
||||
|
||||
1. **钱包归属设计缺陷**:
|
||||
- 原设计:钱包绑定到 `user_id`(用户账号)
|
||||
- 问题:个人客户的卡/设备转手时,钱包无法随资源流转
|
||||
- 示例:个人客户 A 购买单卡充值 100 元,使用 50 元后转手给个人客户 B,B 登录后看不到剩余 50 元余额
|
||||
|
||||
2. **标签系统缺少多租户隔离**:
|
||||
- 原设计:标签表无 `enterprise_id` 和 `shop_id` 字段
|
||||
- 问题:企业 A 创建"测试标签"后,企业 B 无法创建同名标签(全局唯一冲突)
|
||||
- 问题:企业 A 可以看到企业 B 的所有标签(数据泄露)
|
||||
|
||||
**核心变更**:
|
||||
|
||||
#### 1. 钱包表(tb_wallet)结构变更
|
||||
|
||||
| 变更类型 | 字段 | 说明 |
|
||||
|---------|------|------|
|
||||
| ❌ 删除 | `user_id` | 不再绑定用户账号 |
|
||||
| ✅ 添加 | `resource_type` | 资源类型:`iot_card`(单卡)/ `device`(设备)/ `shop`(店铺) |
|
||||
| ✅ 添加 | `resource_id` | 资源 ID |
|
||||
|
||||
**钱包归属新设计**:
|
||||
|
||||
```
|
||||
个人客户单卡钱包:
|
||||
resource_type = 'iot_card'
|
||||
resource_id = 卡ID
|
||||
→ 卡转手时,新用户通过 ICCID 登录,钱包余额跟随卡流转
|
||||
|
||||
个人客户设备钱包(多卡共享):
|
||||
resource_type = 'device'
|
||||
resource_id = 设备ID
|
||||
→ 设备中的 3-4 张卡共享一个钱包
|
||||
|
||||
代理商店铺钱包:
|
||||
resource_type = 'shop'
|
||||
resource_id = 店铺ID
|
||||
→ 店铺内多个代理账号共享钱包,支持预存款采购
|
||||
```
|
||||
|
||||
**索引变更**:
|
||||
- 删除:`idx_wallet_user_type_currency`、`idx_wallet_user`
|
||||
- 添加:`idx_wallet_resource_type_currency`(唯一)、`idx_wallet_resource`
|
||||
|
||||
#### 2. 标签表(tb_tag)结构变更
|
||||
|
||||
| 变更类型 | 字段 | 说明 |
|
||||
|---------|------|------|
|
||||
| ✅ 添加 | `enterprise_id` | 企业 ID(企业标签) |
|
||||
| ✅ 添加 | `shop_id` | 店铺 ID(店铺标签) |
|
||||
|
||||
**标签三级隔离模型**:
|
||||
|
||||
| 标签类型 | enterprise_id | shop_id | 可见范围 | 唯一性 |
|
||||
|---------|--------------|---------|---------|--------|
|
||||
| 平台全局标签 | NULL | NULL | 所有用户 | 全局唯一 |
|
||||
| 企业标签 | 企业ID | NULL | 仅该企业 | 企业内唯一 |
|
||||
| 店铺标签 | NULL | 店铺ID | 该店铺及下级 | 店铺内唯一 |
|
||||
|
||||
**示例**:
|
||||
- 企业 A 创建"测试标签"(`enterprise_id=5, shop_id=NULL`)
|
||||
- 企业 B 也可以创建"测试标签"(`enterprise_id=8, shop_id=NULL`)
|
||||
- 两个标签相互隔离,互不可见
|
||||
|
||||
**索引变更**:
|
||||
- 删除:`idx_tag_name`(全局唯一)
|
||||
- 添加:`idx_tag_enterprise_name`(企业内唯一)
|
||||
- 添加:`idx_tag_shop_name`(店铺内唯一)
|
||||
- 添加:`idx_tag_global_name`(全局标签唯一)
|
||||
|
||||
#### 3. 资源标签表(tb_resource_tag)结构变更
|
||||
|
||||
| 变更类型 | 字段 | 说明 |
|
||||
|---------|------|------|
|
||||
| ✅ 添加 | `enterprise_id` | 企业 ID(从资源推断) |
|
||||
| ✅ 添加 | `shop_id` | 店铺 ID(从资源推断) |
|
||||
|
||||
**数据权限自动过滤**:
|
||||
|
||||
通过 GORM Callback 自动注入过滤条件:
|
||||
|
||||
```go
|
||||
// 代理用户查询标签
|
||||
WHERE shop_id IN (当前店铺及下级店铺)
|
||||
OR (enterprise_id IS NULL AND shop_id IS NULL)
|
||||
|
||||
// 企业用户查询标签
|
||||
WHERE enterprise_id = 当前企业ID
|
||||
OR (enterprise_id IS NULL AND shop_id IS NULL)
|
||||
|
||||
// 个人客户查询标签
|
||||
WHERE enterprise_id IS NULL AND shop_id IS NULL
|
||||
```
|
||||
|
||||
**数据迁移策略**:
|
||||
|
||||
1. **代理钱包迁移**:
|
||||
```sql
|
||||
UPDATE tb_wallet w
|
||||
SET resource_type = 'shop', resource_id = a.shop_id
|
||||
FROM tb_account a
|
||||
WHERE w.user_id = a.id AND w.wallet_type = 'agent';
|
||||
```
|
||||
|
||||
2. **用户钱包处理**:
|
||||
- 标记为 `PENDING_USER`,需要业务人员手动确认归属
|
||||
|
||||
3. **标签归属推断**:
|
||||
```sql
|
||||
-- 从 creator 推断企业标签
|
||||
UPDATE tb_tag t SET enterprise_id = a.enterprise_id
|
||||
FROM tb_account a WHERE a.id = t.creator;
|
||||
|
||||
-- 从 creator 推断店铺标签
|
||||
UPDATE tb_tag t SET shop_id = a.shop_id
|
||||
FROM tb_account a WHERE a.id = t.creator AND t.enterprise_id IS NULL;
|
||||
```
|
||||
|
||||
**迁移验证结果**:
|
||||
|
||||
- ✅ 备份表已创建:`tb_wallet_backup`、`tb_tag_backup`、`tb_resource_tag_backup`
|
||||
- ✅ 钱包表字段变更成功
|
||||
- ✅ 标签表字段变更成功
|
||||
- ✅ 资源标签表字段变更成功
|
||||
- ✅ 迁移耗时:300-960ms
|
||||
- ✅ 回滚耗时:500-960ms
|
||||
- ✅ 可重复执行(已处理备份表冲突)
|
||||
|
||||
**参考文档**:
|
||||
|
||||
- OpenSpec 变更提案:`openspec/changes/fix-wallet-tag-multi-tenant/proposal.md`
|
||||
- 技术设计文档:`openspec/changes/fix-wallet-tag-multi-tenant/design.md`
|
||||
- 实施清单:`openspec/changes/fix-wallet-tag-multi-tenant/tasks.md`
|
||||
|
||||
**测试覆盖**:
|
||||
|
||||
- ✅ 9 个单元测试验证标签多租户过滤(`pkg/gorm/callback_test.go`)
|
||||
- ✅ 迁移和回滚功能验证通过
|
||||
- ✅ OpenSpec 验证通过(`openspec validate --strict`)
|
||||
|
||||
**回滚方案**:
|
||||
|
||||
如需回滚,执行:
|
||||
```bash
|
||||
./scripts/migrate.sh down 1
|
||||
```
|
||||
|
||||
回滚会从备份表恢复数据,但会丢失备份后的新增数据。
|
||||
|
||||
Reference in New Issue
Block a user