Files
junhong_cmp_fiber/docs/add-user-organization-model/用户体系设计文档.md
huang a36e4a79c0 实现用户和组织模型(店铺、企业、个人客户)
核心功能:
- 实现 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>
2026-01-09 18:02:46 +08:00

832 lines
25 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.
# 用户体系设计文档
## 概述
本文档详细说明 junhong_cmp_fiber 系统的用户体系设计,包括四种用户类型、两种组织实体、数据关联关系和权限过滤机制。
**版本**: v1.0
**更新时间**: 2026-01-09
**相关提案**: openspec/changes/add-user-organization-model/
---
## 一、用户类型
系统支持四种用户类型,分别对应不同的登录端口和权限范围:
### 1.1 平台用户Platform User
**定义**: 系统平台的管理员账号,拥有全局管理权限。
**特征**:
- `user_type = 1`(超级管理员)或 `user_type = 2`(平台用户)
- `shop_id = NULL``enterprise_id = NULL`
- 可分配多个角色(通过 `tb_account_role` 表)
- 登录端口Web 后台管理系统
**权限范围**:
- 查看和管理所有店铺、企业、账号数据
- 配置系统级别设置
- 分配平台级别角色和权限
- 不受数据权限过滤限制(可看到全部数据)
**典型用例**:
```go
// 创建平台用户示例
account := &model.Account{
Username: "platform_admin",
Phone: "13800000001",
Password: hashedPassword,
UserType: constants.UserTypePlatform, // 2
ShopID: nil,
EnterpriseID: nil,
Status: constants.StatusEnabled,
}
```
---
### 1.2 代理账号Agent Account
**定义**: 归属于某个店铺(代理商)的员工账号。
**特征**:
- `user_type = 3`
- `shop_id = 店铺ID``enterprise_id = NULL`
- 一个店铺可以有多个代理账号
- 同一店铺的所有代理账号权限相同
- 只能分配一种角色
- 登录端口Web 后台管理系统 + H5 移动端
**权限范围**:
- 查看和管理本店铺及下级店铺的数据
- 查看和管理本店铺及下级店铺的企业客户数据
- 不能查看上级店铺的数据
- 不能查看其他平级店铺的数据
**数据权限过滤逻辑**:
```sql
-- 查询当前店铺及所有下级店铺的 ID 列表(递归查询 + Redis 缓存)
WITH RECURSIVE subordinate_shops AS (
SELECT id FROM tb_shop WHERE id = :current_shop_id
UNION
SELECT s.id FROM tb_shop s
INNER JOIN subordinate_shops ss ON s.parent_id = ss.id
WHERE s.deleted_at IS NULL
)
SELECT id FROM subordinate_shops;
-- 数据过滤条件
WHERE shop_id IN (:shop_ids_list)
```
**典型用例**:
```go
// 创建代理账号示例
shopID := uint(100)
account := &model.Account{
Username: "agent_001",
Phone: "13800000002",
Password: hashedPassword,
UserType: constants.UserTypeAgent, // 3
ShopID: &shopID,
EnterpriseID: nil,
Status: constants.StatusEnabled,
}
```
---
### 1.3 企业账号Enterprise Account
**定义**: 归属于某个企业客户的账号。
**特征**:
- `user_type = 4`
- `shop_id = NULL``enterprise_id = 企业ID`
- 一个企业目前只有一个账号(未来可能扩展为多账号)
- 只能分配一种角色
- 登录端口H5 移动端
**权限范围**:
- 查看和管理本企业的数据
- 查看本企业的物联网卡数据
- 不能查看其他企业的数据
**数据权限过滤逻辑**:
```sql
-- 企业账号只能看到自己企业的数据
WHERE enterprise_id = :current_enterprise_id
```
**典型用例**:
```go
// 创建企业账号示例
enterpriseID := uint(200)
account := &model.Account{
Username: "enterprise_001",
Phone: "13800000003",
Password: hashedPassword,
UserType: constants.UserTypeEnterprise, // 4
ShopID: nil,
EnterpriseID: &enterpriseID,
Status: constants.StatusEnabled,
}
```
---
### 1.4 个人客户Personal Customer
**定义**: 使用 H5 个人端的独立个人用户,不参与 RBAC 权限体系。
**特征**:
- 独立存储在 `tb_personal_customer` 表,不在 `tb_account`
- 不参与角色权限系统(无角色、无权限)
- 支持微信绑定OpenID、UnionID
- 登录端口H5 移动端(个人端)
**字段说明**:
- `phone`: 手机号(唯一标识,用于登录)
- `wx_open_id`: 微信 OpenID微信登录用
- `wx_union_id`: 微信 UnionID跨应用用户识别
- `nickname`: 用户昵称
- `avatar_url`: 头像 URL
**权限范围**:
- 查看和管理自己的个人资料
- 查看和管理自己的物联网卡数据
- 不能查看其他用户的数据
**典型用例**:
```go
// 创建个人客户示例
customer := &model.PersonalCustomer{
Phone: "13800000004",
Nickname: "张三",
AvatarURL: "https://example.com/avatar.jpg",
WxOpenID: "wx_openid_123456",
WxUnionID: "wx_unionid_abcdef",
Status: constants.StatusEnabled,
}
```
---
## 二、组织实体
### 2.1 店铺Shop
**定义**: 代理商组织实体,支持最多 7 级层级关系。
**表名**: `tb_shop`
**核心字段**:
```go
type Shop struct {
gorm.Model // ID, CreatedAt, UpdatedAt, DeletedAt
BaseModel `gorm:"embedded"` // Creator, Updater
ShopName string // 店铺名称
ShopCode string `gorm:"uniqueIndex"` // 店铺编号(唯一)
ParentID *uint `gorm:"index"` // 上级店铺IDNULL表示一级代理
Level int // 层级1-7
ContactName string // 联系人姓名
ContactPhone string // 联系人电话
Province string // 省份
City string // 城市
District string // 区县
Address string // 详细地址
Status int // 状态 0=禁用 1=启用
}
```
**层级关系说明**:
- 一级代理:`parent_id = NULL``level = 1`
- 二级代理:`parent_id = 一级店铺ID``level = 2`
- 最多支持 7 级层级
- 层级关系不可变更(业务约束)
**层级结构示例**:
```
平台
├── 店铺A一级代理level=1, parent_id=NULL
│ ├── 店铺B二级代理level=2, parent_id=店铺A.ID
│ │ └── 店铺C三级代理level=3, parent_id=店铺B.ID
│ └── 店铺D二级代理level=2, parent_id=店铺A.ID
└── 店铺E一级代理level=1, parent_id=NULL
```
**递归查询下级店铺**:
```sql
-- 查询店铺ID=100及其所有下级店铺PostgreSQL WITH RECURSIVE
WITH RECURSIVE subordinate_shops AS (
-- 基础查询:当前店铺自己
SELECT id, shop_name, parent_id, level
FROM tb_shop
WHERE id = 100 AND deleted_at IS NULL
UNION
-- 递归查询:当前店铺的所有下级
SELECT s.id, s.shop_name, s.parent_id, s.level
FROM tb_shop s
INNER JOIN subordinate_shops ss ON s.parent_id = ss.id
WHERE s.deleted_at IS NULL
)
SELECT id FROM subordinate_shops;
```
**Redis 缓存策略**:
- Key 格式:`shop:subordinate_ids:{shop_id}`
- 缓存内容店铺ID及其所有下级店铺的 ID 列表(包含自己)
- 过期时间30 分钟
- 缓存失效:店铺创建、更新、删除时清除相关缓存
**代码示例**:
```go
// Store 层方法
func (s *ShopStore) GetSubordinateShopIDs(ctx context.Context, shopID uint) ([]uint, error) {
// 1. 尝试从 Redis 读取缓存
cacheKey := constants.RedisShopSubordinateIDsKey(shopID)
cached, err := s.redis.Get(ctx, cacheKey).Result()
if err == nil {
var ids []uint
if err := json.Unmarshal([]byte(cached), &ids); err == nil {
return ids, nil
}
}
// 2. 缓存未命中,执行递归查询
query := `
WITH RECURSIVE subordinate_shops AS (
SELECT id FROM tb_shop WHERE id = ? AND deleted_at IS NULL
UNION
SELECT s.id FROM tb_shop s
INNER JOIN subordinate_shops ss ON s.parent_id = ss.id
WHERE s.deleted_at IS NULL
)
SELECT id FROM subordinate_shops
`
var ids []uint
if err := s.db.WithContext(ctx).Raw(query, shopID).Scan(&ids).Error; err != nil {
return nil, err
}
// 3. 写入 Redis 缓存
data, _ := json.Marshal(ids)
s.redis.Set(ctx, cacheKey, data, 30*time.Minute)
return ids, nil
}
```
---
### 2.2 企业Enterprise
**定义**: 企业客户组织实体,可归属于店铺或平台。
**表名**: `tb_enterprise`
**核心字段**:
```go
type Enterprise struct {
gorm.Model // ID, CreatedAt, UpdatedAt, DeletedAt
BaseModel `gorm:"embedded"` // Creator, Updater
EnterpriseName string // 企业名称
EnterpriseCode string `gorm:"uniqueIndex"` // 企业编号(唯一)
OwnerShopID *uint `gorm:"index"` // 归属店铺IDNULL表示平台直属
LegalPerson string // 法人代表
ContactName string // 联系人姓名
ContactPhone string // 联系人电话
BusinessLicense string // 营业执照号
Province string // 省份
City string // 城市
District string // 区县
Address string // 详细地址
Status int // 状态 0=禁用 1=启用
}
```
**归属关系说明**:
- 平台直属企业:`owner_shop_id = NULL`(由平台直接管理)
- 店铺归属企业:`owner_shop_id = 店铺ID`(由该店铺管理)
**数据权限规则**:
- 平台用户:可以查看所有企业(包括平台直属和所有店铺的企业)
- 代理账号:可以查看本店铺及下级店铺的企业
- 企业账号:只能查看自己的企业数据
**归属结构示例**:
```
平台
├── 企业Z平台直属owner_shop_id=NULL
├── 店铺A
│ ├── 企业X归属店铺Aowner_shop_id=店铺A.ID
│ └── 企业Y归属店铺Aowner_shop_id=店铺A.ID
└── 店铺B
└── 店铺C
└── 企业W归属店铺Cowner_shop_id=店铺C.ID
```
**代码示例**:
```go
// 创建平台直属企业
enterprise := &model.Enterprise{
EnterpriseName: "测试企业A",
EnterpriseCode: "ENT001",
OwnerShopID: nil, // NULL 表示平台直属
LegalPerson: "张三",
ContactName: "李四",
ContactPhone: "13800000001",
BusinessLicense: "91110000MA001234",
Status: constants.StatusEnabled,
}
// 创建归属店铺的企业
shopID := uint(100)
enterprise := &model.Enterprise{
EnterpriseName: "测试企业B",
EnterpriseCode: "ENT002",
OwnerShopID: &shopID, // 归属店铺ID
LegalPerson: "王五",
ContactName: "赵六",
ContactPhone: "13800000002",
BusinessLicense: "91110000MA005678",
Status: constants.StatusEnabled,
}
```
---
## 三、数据关联关系
### 3.1 关系图
```
┌─────────────────┐
│ tb_account │
│ (平台/代理/企业)│
└────────┬────────┘
├─────────────────┐
│ │
shop_id enterprise_id
│ │
▼ ▼
┌─────────────┐ ┌──────────────┐
│ tb_shop │ │ tb_enterprise│
│ (店铺) │ │ (企业) │
└──────┬──────┘ └──────┬───────┘
│ │
parent_id owner_shop_id
│ │
└──────────────────┘
┌──────────────────────┐
│ tb_personal_customer │
│ (个人客户) │
│ (独立表,不关联账号) │
└──────────────────────┘
```
### 3.2 关联规则
**重要设计原则**:
- 禁止使用数据库外键约束Foreign Key Constraints
- 禁止使用 GORM 的 ORM 关联关系(`foreignKey``references``hasMany``belongsTo` 等标签)
- 表之间的关联通过存储关联 ID 字段手动维护
- 关联数据查询必须在代码层面显式执行
**tb_account 关联规则**:
| user_type | shop_id | enterprise_id | 说明 |
|-----------|---------|---------------|------|
| 1 或 2 (平台用户) | NULL | NULL | 平台级账号 |
| 3 (代理账号) | 必填 | NULL | 归属店铺 |
| 4 (企业账号) | NULL | 必填 | 归属企业 |
**tb_enterprise 关联规则**:
- `owner_shop_id = NULL`:平台直属企业
- `owner_shop_id = 店铺ID`:归属该店铺的企业
**tb_shop 关联规则**:
- `parent_id = NULL`:一级代理
- `parent_id = 上级店铺ID`:下级代理
---
## 四、数据权限过滤
### 4.1 核心设计
数据权限过滤基于 **shop_id**(店铺归属)而非 `owner_id`(账号归属)。
**设计理由**:
1. 同一店铺的所有账号应该能看到店铺的所有数据
2. 上级店铺应该能看到下级店铺的数据
3. `owner_id` 字段保留用于记录数据的创建者(审计用途)
### 4.2 过滤逻辑
**平台用户user_type = 1 或 2**:
- 不受数据权限过滤限制
- 可以查看所有数据
**代理账号user_type = 3**:
1. 查询当前店铺及所有下级店铺的 ID 列表(递归查询 + Redis 缓存)
2. 数据查询条件:`WHERE shop_id IN (:shop_ids_list)`
**企业账号user_type = 4**:
- 数据查询条件:`WHERE enterprise_id = :current_enterprise_id`
### 4.3 代码实现示例
```go
// Service 层数据权限过滤
func (s *SomeService) applyDataPermissionFilter(ctx context.Context, query *gorm.DB) (*gorm.DB, error) {
// 从上下文获取当前用户信息
currentUser := GetCurrentUserFromContext(ctx)
// 平台用户跳过过滤
if currentUser.UserType == constants.UserTypeSuperAdmin ||
currentUser.UserType == constants.UserTypePlatform {
return query, nil
}
// 代理账号:基于 shop_id 过滤
if currentUser.UserType == constants.UserTypeAgent {
if currentUser.ShopID == nil {
return nil, errors.New("代理账号缺少店铺ID")
}
// 查询当前店铺及下级店铺 ID 列表(带 Redis 缓存)
shopIDs, err := s.shopStore.GetSubordinateShopIDs(ctx, *currentUser.ShopID)
if err != nil {
return nil, err
}
return query.Where("shop_id IN ?", shopIDs), nil
}
// 企业账号:基于 enterprise_id 过滤
if currentUser.UserType == constants.UserTypeEnterprise {
if currentUser.EnterpriseID == nil {
return nil, errors.New("企业账号缺少企业ID")
}
return query.Where("enterprise_id = ?", *currentUser.EnterpriseID), nil
}
return query, nil
}
```
---
## 五、数据库表结构
### 5.1 tb_shop店铺表
```sql
CREATE TABLE tb_shop (
id SERIAL PRIMARY KEY,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP,
creator INTEGER,
updater INTEGER,
shop_name VARCHAR(100) NOT NULL COMMENT '店铺名称',
shop_code VARCHAR(50) COMMENT '店铺编号',
parent_id INTEGER COMMENT '上级店铺ID',
level INTEGER NOT NULL DEFAULT 1 COMMENT '层级1-7',
contact_name VARCHAR(50) COMMENT '联系人姓名',
contact_phone VARCHAR(20) COMMENT '联系人电话',
province VARCHAR(50) COMMENT '省份',
city VARCHAR(50) COMMENT '城市',
district VARCHAR(50) COMMENT '区县',
address VARCHAR(255) COMMENT '详细地址',
status INTEGER NOT NULL DEFAULT 1 COMMENT '状态 0=禁用 1=启用'
);
CREATE INDEX idx_shop_parent_id ON tb_shop(parent_id);
CREATE UNIQUE INDEX idx_shop_code ON tb_shop(shop_code) WHERE deleted_at IS NULL;
CREATE INDEX idx_shop_deleted_at ON tb_shop(deleted_at);
```
### 5.2 tb_enterprise企业表
```sql
CREATE TABLE tb_enterprise (
id SERIAL PRIMARY KEY,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP,
creator INTEGER,
updater INTEGER,
enterprise_name VARCHAR(100) NOT NULL COMMENT '企业名称',
enterprise_code VARCHAR(50) COMMENT '企业编号',
owner_shop_id INTEGER COMMENT '归属店铺ID',
legal_person VARCHAR(50) COMMENT '法人代表',
contact_name VARCHAR(50) COMMENT '联系人姓名',
contact_phone VARCHAR(20) COMMENT '联系人电话',
business_license VARCHAR(100) COMMENT '营业执照号',
province VARCHAR(50) COMMENT '省份',
city VARCHAR(50) COMMENT '城市',
district VARCHAR(50) COMMENT '区县',
address VARCHAR(255) COMMENT '详细地址',
status INTEGER NOT NULL DEFAULT 1 COMMENT '状态 0=禁用 1=启用'
);
CREATE INDEX idx_enterprise_owner_shop_id ON tb_enterprise(owner_shop_id);
CREATE UNIQUE INDEX idx_enterprise_code ON tb_enterprise(enterprise_code) WHERE deleted_at IS NULL;
CREATE INDEX idx_enterprise_deleted_at ON tb_enterprise(deleted_at);
```
### 5.3 tb_personal_customer个人客户表
```sql
CREATE TABLE tb_personal_customer (
id SERIAL PRIMARY KEY,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP,
phone VARCHAR(20) COMMENT '手机号',
nickname VARCHAR(50) COMMENT '昵称',
avatar_url VARCHAR(255) COMMENT '头像URL',
wx_open_id VARCHAR(100) COMMENT '微信OpenID',
wx_union_id VARCHAR(100) COMMENT '微信UnionID',
status INTEGER NOT NULL DEFAULT 1 COMMENT '状态 0=禁用 1=启用'
);
CREATE UNIQUE INDEX idx_personal_customer_phone ON tb_personal_customer(phone) WHERE deleted_at IS NULL;
CREATE INDEX idx_personal_customer_wx_open_id ON tb_personal_customer(wx_open_id);
CREATE INDEX idx_personal_customer_wx_union_id ON tb_personal_customer(wx_union_id);
CREATE INDEX idx_personal_customer_deleted_at ON tb_personal_customer(deleted_at);
```
### 5.4 tb_account账号表 - 修改)
```sql
-- 添加字段
ALTER TABLE tb_account ADD COLUMN enterprise_id INTEGER;
CREATE INDEX idx_account_enterprise_id ON tb_account(enterprise_id);
-- 移除字段(如果存在)
ALTER TABLE tb_account DROP COLUMN IF EXISTS parent_id;
```
---
## 六、API 使用示例
### 6.1 创建店铺
**接口**: `POST /api/v1/shops`
**权限**: 平台用户
**请求示例**:
```json
{
"shop_name": "北京一级代理",
"shop_code": "BJ001",
"parent_id": null,
"level": 1,
"contact_name": "张三",
"contact_phone": "13800000001",
"province": "北京市",
"city": "北京市",
"district": "朝阳区",
"address": "朝阳路100号"
}
```
**响应示例**:
```json
{
"code": 0,
"message": "success",
"data": {
"id": 1,
"shop_name": "北京一级代理",
"shop_code": "BJ001",
"parent_id": null,
"level": 1,
"status": 1,
"created_at": "2026-01-09T10:00:00Z"
}
}
```
### 6.2 创建企业
**接口**: `POST /api/v1/enterprises`
**权限**: 平台用户或代理账号
**请求示例(平台直属)**:
```json
{
"enterprise_name": "测试科技有限公司",
"enterprise_code": "ENT001",
"owner_shop_id": null,
"legal_person": "李四",
"contact_name": "王五",
"contact_phone": "13800000002",
"business_license": "91110000MA001234",
"province": "北京市",
"city": "北京市",
"district": "海淀区",
"address": "中关村大街1号"
}
```
**请求示例(归属店铺)**:
```json
{
"enterprise_name": "测试科技有限公司",
"enterprise_code": "ENT002",
"owner_shop_id": 1,
"legal_person": "赵六",
"contact_name": "孙七",
"contact_phone": "13800000003",
"business_license": "91110000MA005678"
}
```
### 6.3 创建代理账号
**接口**: `POST /api/v1/accounts`
**权限**: 平台用户
**请求示例**:
```json
{
"username": "agent001",
"phone": "13800000004",
"password": "password123",
"user_type": 3,
"shop_id": 1,
"enterprise_id": null
}
```
### 6.4 查询下级店铺
**接口**: `GET /api/v1/shops/{shop_id}/subordinates`
**权限**: 平台用户或对应店铺的代理账号
**响应示例**:
```json
{
"code": 0,
"message": "success",
"data": {
"shop_ids": [1, 2, 3, 4],
"details": [
{
"id": 1,
"shop_name": "一级店铺",
"level": 1,
"parent_id": null
},
{
"id": 2,
"shop_name": "二级店铺1",
"level": 2,
"parent_id": 1
},
{
"id": 3,
"shop_name": "二级店铺2",
"level": 2,
"parent_id": 1
},
{
"id": 4,
"shop_name": "三级店铺",
"level": 3,
"parent_id": 2
}
]
}
}
```
---
## 七、常见问题FAQ
### Q1: 为什么不使用外键约束?
**A**: 这是项目的核心设计原则之一。原因包括:
1. **灵活性**: 业务逻辑完全在代码中控制,不受数据库约束限制
2. **性能**: 无外键约束意味着无数据库层面的引用完整性检查开销
3. **可控性**: 开发者完全掌控何时查询关联数据、查询哪些关联数据
4. **分布式友好**: 在微服务和分布式数据库场景下更容易扩展
### Q2: 为什么不使用 GORM 的关联关系(如 hasMany、belongsTo
**A**: 与不使用外键的理由类似:
1. **显式关联**: 代码中显式执行关联查询,数据流向清晰可见
2. **性能优化**: 避免 GORM 的 N+1 查询问题和自动 JOIN 开销
3. **简单直接**: 手动查询关联数据更容易理解和调试
### Q3: 代理账号的权限是如何继承的?
**A**: 代理账号的权限不是通过"继承"实现,而是通过**数据过滤范围**实现:
- 上级店铺可以查看下级店铺的数据(通过递归查询 shop_id 列表)
- 同一店铺的所有账号看到的数据范围相同
- 下级店铺不能查看上级店铺的数据
### Q4: 如何查询某个店铺的所有下级店铺?
**A**: 使用 `ShopStore.GetSubordinateShopIDs()` 方法:
```go
shopIDs, err := shopStore.GetSubordinateShopIDs(ctx, shopID)
// 返回的 shopIDs 包含当前店铺自己 + 所有下级店铺
```
该方法使用 PostgreSQL 的 `WITH RECURSIVE` 递归查询,并通过 Redis 缓存 30 分钟。
### Q5: 个人客户为什么单独存储,不放在 tb_account 表?
**A**: 因为个人客户与其他用户类型有本质区别:
1. **不参与 RBAC**: 无角色、无权限,不需要 account_role、role_permission 等关联
2. **字段差异**: 需要微信绑定字段wx_open_id、wx_union_id不需要 username
3. **数据量大**: 个人客户数量可能远超账号数量,分表便于扩展和优化
### Q6: 企业账号未来如何支持多账号?
**A**: 当前一个企业只有一个账号(`enterprise_id``tb_account` 中是唯一的)。
未来支持多账号时可以:
1. 取消 `enterprise_id` 的唯一约束
2.`tb_account` 添加 `is_primary` 字段区分主账号和子账号
3. 或者添加 `tb_enterprise_account` 中间表管理企业的多个账号
4. 在 Service 层实现企业内部的权限分配逻辑
### Q7: 店铺层级为什么限制为 7 级?
**A**: 这是业务约束。技术上可以支持更多层级,但考虑到:
1. 代理商管理的复杂度
2. 递归查询的性能影响
3. 实际业务场景中很少需要超过 7 级
### Q8: Redis 缓存失效策略是什么?
**A**: 当前使用简单的 TTL 过期策略30 分钟)。
更完善的缓存失效策略可以是:
- 店铺创建时:清除父店铺及所有祖先店铺的缓存
- 店铺更新时:如果 `parent_id` 变更,清除新旧父店铺的缓存
- 店铺删除时:清除父店铺及所有祖先店铺的缓存
代码示例:
```go
func (s *ShopStore) InvalidateSubordinateCache(ctx context.Context, shopID uint) error {
// 向上递归清除所有祖先店铺的缓存
// ...
}
```
---
## 八、后续优化建议
1. **性能优化**:
- 考虑在 `tb_shop` 添加 `path` 字段存储完整路径(如 `/1/2/3/`),减少递归查询
- 使用物化视图Materialized View缓存店铺层级关系
- 对高频查询的店铺 ID 列表使用 Redis Set 数据结构
2. **功能扩展**:
- 实现企业多账号支持
- 添加店铺层级变更功能(需要复杂的业务审批流程)
- 添加组织架构树的可视化展示
3. **监控和审计**:
- 记录所有组织关系变更的审计日志
- 监控递归查询的性能指标
- 监控 Redis 缓存命中率
4. **安全加固**:
- 限制店铺层级修改的权限(只有超级管理员)
- 防止循环引用(`parent_id` 指向自己或形成环)
- 数据权限过滤的单元测试和集成测试覆盖
---
## 九、相关文档
- **设计文档**: `openspec/changes/add-user-organization-model/design.md`
- **提案文档**: `openspec/changes/add-user-organization-model/proposal.md`
- **任务清单**: `openspec/changes/add-user-organization-model/tasks.md`
- **数据库迁移**: `migrations/000002_create_shop_enterprise_personal_customer.up.sql`
- **单元测试**: `tests/unit/shop_store_test.go`, `tests/unit/enterprise_store_test.go`, `tests/unit/personal_customer_store_test.go`
---
**文档结束**