核心功能: - 实现 7 级店铺层级体系(Shop 模型 + 层级校验) - 实现企业管理模型(Enterprise 模型) - 实现个人客户管理模型(PersonalCustomer 模型) - 重构 Account 模型关联关系(基于 EnterpriseID 而非 ParentID) - 完整的 Store 层和 Service 层实现 - 递归查询下级店铺功能(含 Redis 缓存) - 全面的单元测试覆盖(Shop/Enterprise/PersonalCustomer Store + Shop Service) 技术要点: - 显式指定所有 GORM 模型的数据库字段名(column: 标签) - 统一的字段命名规范(数据库用 snake_case,Go 用 PascalCase) - 完整的中文字段注释和业务逻辑说明 - 100% 测试覆盖(20+ 测试用例全部通过) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
8.2 KiB
8.2 KiB
Design: 用户和组织模型架构设计
Context
背景
系统需要支持以下四种用户类型和对应的登录端口:
| 用户类型 | 登录端口 | 组织归属 | 角色数量 |
|---|---|---|---|
| 平台用户 | Web后台 | 无(平台级) | 可分配多个角色 |
| 代理账号 | Web后台 + H5 | 店铺 | 只能分配一种角色 |
| 企业账号 | H5 | 企业 | 只能分配一种角色 |
| 个人客户 | H5(个人端) | 无 | 无角色无权限 |
组织层级关系
平台(系统)
├── 店铺A(一级代理)
│ ├── 店铺B(二级代理,最多7级)
│ │ └── 企业X
│ └── 企业Y
├── 店铺C(一级代理)
│ └── ...
└── 企业Z(平台直属企业)
约束条件
- 代理层级最多 7 级
- 代理的上下级关系不可变更
- 一个店铺多个账号(账号权限相同)
- 一个企业目前只有一个账号
- 个人客户独立表,不参与 RBAC 体系
- 遵循项目的数据库设计原则:禁止外键、禁止 GORM 关联
Goals / Non-Goals
Goals
- 设计清晰的用户和组织模型,支持四种用户类型
- 建立店铺层级关系,支持 7 级代理
- 支持店铺、企业、个人客户的数据归属
- 为后续的角色权限体系打好基础
- 为后续的数据权限过滤打好基础
Non-Goals
- 本提案不实现角色权限体系(后续提案)
- 本提案不实现个人客户的微信登录(后续提案)
- 本提案不实现数据权限过滤逻辑(已在 004-rbac-data-permission 中定义)
- 本提案不处理资产绑定(未来功能)
Decisions
Decision 1: 账号统一存储 vs 分表存储
决策: 平台用户、代理账号、企业账号统一存储在 tb_account 表,个人客户独立存储在 tb_personal_customer 表。
理由:
- 平台/代理/企业账号都参与 RBAC 体系,有相似的字段结构
- 个人客户不参与 RBAC,有独特的微信绑定需求
- 统一存储便于账号管理和登录验证
- 通过
user_type字段区分账号类型
Decision 2: 代理层级关系的存储位置
决策: 代理层级关系存储在 tb_shop(店铺表)的 parent_id 字段,而非 tb_account 的 parent_id。
理由:
- 层级关系是店铺之间的关系,不是个人之间的关系
- 一个店铺有多个账号,账号之间不应该有上下级关系
- 现有
tb_account.parent_id字段将重新定义用途或移除
变更:
tb_account.parent_id字段移除或废弃- 新增
tb_shop.parent_id表示店铺的上级店铺 - 递归查询下级改为查询店铺的下级,而非账号的下级
Decision 3: 企业的归属关系
决策: 企业通过 owner_shop_id 字段表示归属于哪个店铺,NULL 表示平台直属。
理由:
- 企业可以归属于任意级别的代理(店铺)
- 企业也可以直接归属于平台
- 上级代理能看到下级代理的企业数据
Decision 4: 账号与组织的关联方式
决策: 账号通过 shop_id 或 enterprise_id 字段关联到组织。
实现:
- 平台用户:
shop_id = NULL,enterprise_id = NULL - 代理账号:
shop_id = 店铺ID,enterprise_id = NULL - 企业账号:
shop_id = NULL,enterprise_id = 企业ID
Decision 5: 数据权限过滤的调整
决策: 数据权限过滤基于 shop_id(店铺归属)而非 owner_id(账号归属)。
理由:
- 同一店铺的所有账号应该能看到店铺的所有数据
- 上级店铺应该能看到下级店铺的数据
owner_id字段保留用于记录数据的创建者(审计用途)
变更:
- 递归查询改为查询店铺的下级店铺 ID 列表
- 数据过滤条件改为
WHERE shop_id IN (当前店铺及下级店铺) - 平台用户(
user_type = 1或user_type = 2)跳过过滤
Data Models
Shop(店铺)
type Shop struct {
gorm.Model
BaseModel `gorm:"embedded"`
ShopName string `gorm:"not null;size:100"` // 店铺名称
ShopCode string `gorm:"uniqueIndex;size:50"` // 店铺编号
ParentID *uint `gorm:"index"` // 上级店铺ID(NULL表示一级代理)
Level int `gorm:"not null;default:1"` // 层级(1-7)
ContactName string `gorm:"size:50"` // 联系人姓名
ContactPhone string `gorm:"size:20"` // 联系人电话
Province string `gorm:"size:50"` // 省份
City string `gorm:"size:50"` // 城市
District string `gorm:"size:50"` // 区县
Address string `gorm:"size:255"` // 详细地址
Status int `gorm:"not null;default:1"` // 状态 0=禁用 1=启用
}
Enterprise(企业)
type Enterprise struct {
gorm.Model
BaseModel `gorm:"embedded"`
EnterpriseName string `gorm:"not null;size:100"` // 企业名称
EnterpriseCode string `gorm:"uniqueIndex;size:50"` // 企业编号
OwnerShopID *uint `gorm:"index"` // 归属店铺ID(NULL表示平台直属)
LegalPerson string `gorm:"size:50"` // 法人代表
ContactName string `gorm:"size:50"` // 联系人姓名
ContactPhone string `gorm:"size:20"` // 联系人电话
BusinessLicense string `gorm:"size:100"` // 营业执照号
Province string `gorm:"size:50"` // 省份
City string `gorm:"size:50"` // 城市
District string `gorm:"size:50"` // 区县
Address string `gorm:"size:255"` // 详细地址
Status int `gorm:"not null;default:1"` // 状态 0=禁用 1=启用
}
PersonalCustomer(个人客户)
type PersonalCustomer struct {
gorm.Model
Phone string `gorm:"uniqueIndex;size:20"` // 手机号(唯一标识)
Nickname string `gorm:"size:50"` // 昵称
AvatarURL string `gorm:"size:255"` // 头像URL
WxOpenID string `gorm:"index;size:100"` // 微信OpenID
WxUnionID string `gorm:"index;size:100"` // 微信UnionID
Status int `gorm:"not null;default:1"` // 状态 0=禁用 1=启用
}
Account(账号)- 修改
type Account struct {
gorm.Model
BaseModel `gorm:"embedded"`
Username string `gorm:"uniqueIndex;size:50"` // 用户名
Phone string `gorm:"uniqueIndex;size:20"` // 手机号
Password string `gorm:"not null;size:255" json:"-"` // 密码
UserType int `gorm:"not null;index"` // 用户类型 1=超级管理员 2=平台用户 3=代理账号 4=企业账号
ShopID *uint `gorm:"index"` // 店铺ID(代理账号必填)
EnterpriseID *uint `gorm:"index"` // 企业ID(企业账号必填)
Status int `gorm:"not null;default:1"` // 状态 0=禁用 1=启用
// 移除 ParentID 字段,层级关系由 Shop 表维护
}
Risks / Trade-offs
Risk 1: 现有数据迁移
- 风险: 现有
tb_account.parent_id字段被移除,可能影响现有数据 - 缓解: 当前系统是空架子,无实际数据需要迁移
Risk 2: 数据权限过滤逻辑变更
- 风险: 从
owner_id过滤改为shop_id过滤,需要调整现有代码 - 缓解: 现有的数据权限过滤尚未完全实现,可以直接按新设计实现
Risk 3: 店铺层级查询性能
- 风险: 7 级店铺层级的递归查询可能影响性能
- 缓解: 继续使用 Redis 缓存店铺的下级 ID 列表,30 分钟过期
Migration Plan
- 创建新表:
tb_shop、tb_enterprise、tb_personal_customer - 修改
tb_account表结构:- 添加
enterprise_id字段 - 移除
parent_id字段(如果有数据则先迁移)
- 添加
- 更新 GORM 模型定义
- 更新 Store 层实现
- 更新常量定义
Open Questions
店铺层级关系是否需要记录完整路径(如- 暂不需要,使用递归查询 + Redis 缓存/1/2/3/)以优化查询?企业账号未来扩展为多账号时,是否需要区分主账号和子账号?- 未来再设计