# 用户体系设计文档 ## 概述 本文档详细说明 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"` // 上级店铺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) ``` **递归查询下级店铺**: ```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"` // 归属店铺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) ``` **代码示例**: ```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` --- **文档结束**