核心功能: - 实现 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>
25 KiB
用户体系设计文档
概述
本文档详细说明 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 后台管理系统
权限范围:
- 查看和管理所有店铺、企业、账号数据
- 配置系统级别设置
- 分配平台级别角色和权限
- 不受数据权限过滤限制(可看到全部数据)
典型用例:
// 创建平台用户示例
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 = 3shop_id = 店铺ID,enterprise_id = NULL- 一个店铺可以有多个代理账号
- 同一店铺的所有代理账号权限相同
- 只能分配一种角色
- 登录端口:Web 后台管理系统 + H5 移动端
权限范围:
- 查看和管理本店铺及下级店铺的数据
- 查看和管理本店铺及下级店铺的企业客户数据
- 不能查看上级店铺的数据
- 不能查看其他平级店铺的数据
数据权限过滤逻辑:
-- 查询当前店铺及所有下级店铺的 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)
典型用例:
// 创建代理账号示例
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 = 4shop_id = NULL,enterprise_id = 企业ID- 一个企业目前只有一个账号(未来可能扩展为多账号)
- 只能分配一种角色
- 登录端口:H5 移动端
权限范围:
- 查看和管理本企业的数据
- 查看本企业的物联网卡数据
- 不能查看其他企业的数据
数据权限过滤逻辑:
-- 企业账号只能看到自己企业的数据
WHERE enterprise_id = :current_enterprise_id
典型用例:
// 创建企业账号示例
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
权限范围:
- 查看和管理自己的个人资料
- 查看和管理自己的物联网卡数据
- 不能查看其他用户的数据
典型用例:
// 创建个人客户示例
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
核心字段:
type Shop struct {
gorm.Model // ID, CreatedAt, UpdatedAt, DeletedAt
BaseModel `gorm:"embedded"` // Creator, Updater
ShopName string // 店铺名称
ShopCode string `gorm:"uniqueIndex"` // 店铺编号(唯一)
ParentID *uint `gorm:"index"` // 上级店铺ID(NULL表示一级代理)
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)
递归查询下级店铺:
-- 查询店铺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 分钟
- 缓存失效:店铺创建、更新、删除时清除相关缓存
代码示例:
// 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
核心字段:
type Enterprise struct {
gorm.Model // ID, CreatedAt, UpdatedAt, DeletedAt
BaseModel `gorm:"embedded"` // Creator, Updater
EnterpriseName string // 企业名称
EnterpriseCode string `gorm:"uniqueIndex"` // 企业编号(唯一)
OwnerShopID *uint `gorm:"index"` // 归属店铺ID(NULL表示平台直属)
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(归属店铺A,owner_shop_id=店铺A.ID)
│ └── 企业Y(归属店铺A,owner_shop_id=店铺A.ID)
└── 店铺B
└── 店铺C
└── 企业W(归属店铺C,owner_shop_id=店铺C.ID)
代码示例:
// 创建平台直属企业
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(账号归属)。
设计理由:
- 同一店铺的所有账号应该能看到店铺的所有数据
- 上级店铺应该能看到下级店铺的数据
owner_id字段保留用于记录数据的创建者(审计用途)
4.2 过滤逻辑
平台用户(user_type = 1 或 2):
- 不受数据权限过滤限制
- 可以查看所有数据
代理账号(user_type = 3):
- 查询当前店铺及所有下级店铺的 ID 列表(递归查询 + Redis 缓存)
- 数据查询条件:
WHERE shop_id IN (:shop_ids_list)
企业账号(user_type = 4):
- 数据查询条件:
WHERE enterprise_id = :current_enterprise_id
4.3 代码实现示例
// 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(店铺表)
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(企业表)
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(个人客户表)
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(账号表 - 修改)
-- 添加字段
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
权限: 平台用户
请求示例:
{
"shop_name": "北京一级代理",
"shop_code": "BJ001",
"parent_id": null,
"level": 1,
"contact_name": "张三",
"contact_phone": "13800000001",
"province": "北京市",
"city": "北京市",
"district": "朝阳区",
"address": "朝阳路100号"
}
响应示例:
{
"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
权限: 平台用户或代理账号
请求示例(平台直属):
{
"enterprise_name": "测试科技有限公司",
"enterprise_code": "ENT001",
"owner_shop_id": null,
"legal_person": "李四",
"contact_name": "王五",
"contact_phone": "13800000002",
"business_license": "91110000MA001234",
"province": "北京市",
"city": "北京市",
"district": "海淀区",
"address": "中关村大街1号"
}
请求示例(归属店铺):
{
"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
权限: 平台用户
请求示例:
{
"username": "agent001",
"phone": "13800000004",
"password": "password123",
"user_type": 3,
"shop_id": 1,
"enterprise_id": null
}
6.4 查询下级店铺
接口: GET /api/v1/shops/{shop_id}/subordinates
权限: 平台用户或对应店铺的代理账号
响应示例:
{
"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: 这是项目的核心设计原则之一。原因包括:
- 灵活性: 业务逻辑完全在代码中控制,不受数据库约束限制
- 性能: 无外键约束意味着无数据库层面的引用完整性检查开销
- 可控性: 开发者完全掌控何时查询关联数据、查询哪些关联数据
- 分布式友好: 在微服务和分布式数据库场景下更容易扩展
Q2: 为什么不使用 GORM 的关联关系(如 hasMany、belongsTo)?
A: 与不使用外键的理由类似:
- 显式关联: 代码中显式执行关联查询,数据流向清晰可见
- 性能优化: 避免 GORM 的 N+1 查询问题和自动 JOIN 开销
- 简单直接: 手动查询关联数据更容易理解和调试
Q3: 代理账号的权限是如何继承的?
A: 代理账号的权限不是通过"继承"实现,而是通过数据过滤范围实现:
- 上级店铺可以查看下级店铺的数据(通过递归查询 shop_id 列表)
- 同一店铺的所有账号看到的数据范围相同
- 下级店铺不能查看上级店铺的数据
Q4: 如何查询某个店铺的所有下级店铺?
A: 使用 ShopStore.GetSubordinateShopIDs() 方法:
shopIDs, err := shopStore.GetSubordinateShopIDs(ctx, shopID)
// 返回的 shopIDs 包含当前店铺自己 + 所有下级店铺
该方法使用 PostgreSQL 的 WITH RECURSIVE 递归查询,并通过 Redis 缓存 30 分钟。
Q5: 个人客户为什么单独存储,不放在 tb_account 表?
A: 因为个人客户与其他用户类型有本质区别:
- 不参与 RBAC: 无角色、无权限,不需要 account_role、role_permission 等关联
- 字段差异: 需要微信绑定字段(wx_open_id、wx_union_id),不需要 username
- 数据量大: 个人客户数量可能远超账号数量,分表便于扩展和优化
Q6: 企业账号未来如何支持多账号?
A: 当前一个企业只有一个账号(enterprise_id 在 tb_account 中是唯一的)。
未来支持多账号时可以:
- 取消
enterprise_id的唯一约束 - 在
tb_account添加is_primary字段区分主账号和子账号 - 或者添加
tb_enterprise_account中间表管理企业的多个账号 - 在 Service 层实现企业内部的权限分配逻辑
Q7: 店铺层级为什么限制为 7 级?
A: 这是业务约束。技术上可以支持更多层级,但考虑到:
- 代理商管理的复杂度
- 递归查询的性能影响
- 实际业务场景中很少需要超过 7 级
Q8: Redis 缓存失效策略是什么?
A: 当前使用简单的 TTL 过期策略(30 分钟)。
更完善的缓存失效策略可以是:
- 店铺创建时:清除父店铺及所有祖先店铺的缓存
- 店铺更新时:如果
parent_id变更,清除新旧父店铺的缓存 - 店铺删除时:清除父店铺及所有祖先店铺的缓存
代码示例:
func (s *ShopStore) InvalidateSubordinateCache(ctx context.Context, shopID uint) error {
// 向上递归清除所有祖先店铺的缓存
// ...
}
八、后续优化建议
-
性能优化:
- 考虑在
tb_shop添加path字段存储完整路径(如/1/2/3/),减少递归查询 - 使用物化视图(Materialized View)缓存店铺层级关系
- 对高频查询的店铺 ID 列表使用 Redis Set 数据结构
- 考虑在
-
功能扩展:
- 实现企业多账号支持
- 添加店铺层级变更功能(需要复杂的业务审批流程)
- 添加组织架构树的可视化展示
-
监控和审计:
- 记录所有组织关系变更的审计日志
- 监控递归查询的性能指标
- 监控 Redis 缓存命中率
-
安全加固:
- 限制店铺层级修改的权限(只有超级管理员)
- 防止循环引用(
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
文档结束