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

25 KiB
Raw Permalink Blame History

用户体系设计文档

概述

本文档详细说明 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 = NULLenterprise_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 = 3
  • shop_id = 店铺IDenterprise_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 = 4
  • shop_id = NULLenterprise_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"`         // 上级店铺IDNULL表示一级代理
    Level        int                            // 层级1-7
    ContactName  string                         // 联系人姓名
    ContactPhone string                         // 联系人电话
    Province     string                         // 省份
    City         string                         // 城市
    District     string                         // 区县
    Address      string                         // 详细地址
    Status       int                            // 状态 0=禁用 1=启用
}

层级关系说明:

  • 一级代理:parent_id = NULLlevel = 1
  • 二级代理:parent_id = 一级店铺IDlevel = 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"`         // 归属店铺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

代码示例:

// 创建平台直属企业
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 关联关系(foreignKeyreferenceshasManybelongsTo 等标签)
  • 表之间的关联通过存储关联 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 代码实现示例

// 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: 这是项目的核心设计原则之一。原因包括:

  1. 灵活性: 业务逻辑完全在代码中控制,不受数据库约束限制
  2. 性能: 无外键约束意味着无数据库层面的引用完整性检查开销
  3. 可控性: 开发者完全掌控何时查询关联数据、查询哪些关联数据
  4. 分布式友好: 在微服务和分布式数据库场景下更容易扩展

Q2: 为什么不使用 GORM 的关联关系(如 hasMany、belongsTo

A: 与不使用外键的理由类似:

  1. 显式关联: 代码中显式执行关联查询,数据流向清晰可见
  2. 性能优化: 避免 GORM 的 N+1 查询问题和自动 JOIN 开销
  3. 简单直接: 手动查询关联数据更容易理解和调试

Q3: 代理账号的权限是如何继承的?

A: 代理账号的权限不是通过"继承"实现,而是通过数据过滤范围实现:

  • 上级店铺可以查看下级店铺的数据(通过递归查询 shop_id 列表)
  • 同一店铺的所有账号看到的数据范围相同
  • 下级店铺不能查看上级店铺的数据

Q4: 如何查询某个店铺的所有下级店铺?

A: 使用 ShopStore.GetSubordinateShopIDs() 方法:

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_idtb_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 变更,清除新旧父店铺的缓存
  • 店铺删除时:清除父店铺及所有祖先店铺的缓存

代码示例:

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

文档结束