# 用户体系设计文档

## 概述

本文档详细说明 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`

---

**文档结束**
