# CMA 账套模式开发规范（2026-08-11 定稿）

> 依据：研学Bot《cma-account-mode-research-20260811.md》+ 全栈Bot落地改造。
> 适用范围：cma-management 仓库全部后端/前端开发。

---

## 1. 铁律：新表必须带 entity_id

**任何新增业务数据表（租户表）必须包含 `entity_id` 列**，用于账套隔离。

```python
class 新模型(Base):
    __tablename__ = "xxx"
    id = Column(Integer, primary_key=True, index=True)
    entity_id = Column(Integer, ForeignKey("entities.id"), nullable=False, index=True, comment="企业ID（账套）")
    # ... 业务字段
```

### 1.1 全局表 vs 租户表归类

| 类型 | 说明 | 示例 | entity_id |
|------|------|------|:---:|
| **全局表** | 系统级、跨账套共享 | users / entities / user_entities / role_permissions / system_configs / notification_channels / kpi_templates / okr_templates / bi_report_templates | ❌ 不带 |
| **租户表** | 业务数据、按账套隔离 | kpi_definitions / kpi_values / alerts / data_source_config / objectives / action_plans / org_nodes / budget_plans / cost_* / bsc_layer_config / kpi_hierarchy / cash_* / mpm_results / bi_reports / kpi_causality / report_history | ✅ **必须带** |

**判断口诀**：数据属于"某个企业/账套" → 租户表带 entity_id；数据属于"系统/平台" → 全局表。

### 1.2 新表检查清单

- [ ] 建表 SQL / ORM 模型包含 `entity_id`（`nullable=False` + index）
- [ ] 查询接口默认按 entity_id 过滤（通过 `Depends(get_entity_id)` 或显式参数）
- [ ] 写入接口强制使用当前 token 绑定的 entity_id（禁止客户端传入）
- [ ] 存量表缺 entity_id 的，补列并回填默认值后设为 NOT NULL

---

## 2. 账套解析规则（后端）

```text
get_entity_id 解析链（backend/app/deps.py）：
  1. 有 Bearer token（登录用户）→ 强制使用 token 绑定的 entity_id（忽略 query/header/body，防越权）
  2. 无 token（Bot服务通道白名单）→ query → header → body，校验 entity 状态 = active
  3. 兜底默认 1（酣客）
```

### 2.1 API 开发约定

- **业务 API**：一律使用 `entity_id: int = Depends(get_entity_id)` 获取当前账套，禁止自己解析 query 参数
- **禁止**在业务 API 里让客户端传 `entity_id` 来指定账套（token 绑定才是唯一来源）
- 需要"某账套数据"的 Bot/服务通道：无 token 时通过 query/header 传 entity_id，后端校验 `Entity.status == "active"`

### 2.2 用户授权

- 用户可访问哪些账套 = `user_entities` 表（user_id ↔ entity_id 多对多）
- 登录/切换时必须校验 `user_has_entity(db, user_id, entity_id)`，未授权返回 **403**
- 新用户注册默认授权 entity_id=1（酣客）

---

## 3. 账套切换（前端）

- 切换走 `POST /api/cma/auth/switch-entity` → 后端校验授权 → **重新签发 token**（绑定新 entity）→ 整页刷新
- **禁止**直接改 `localStorage['cma_entity_id']` 切换（token 未变会导致数据错乱）
- 拦截器**不自动附加** `X-Entity-Id` / `entity_id`（账套由 token 绑定，删除自动附加逻辑）
- 登录页公司下拉数据源：`GET /api/cma/auth/login-entities?username=xxx`（按授权过滤）

---

## 4. 安全红线

1. 任何请求携带的 `entity_id` 与 token 绑定不一致时，**以 token 为准**（忽略 query/header）
2. 未授权账套访问返回 403（登录、切换、业务 API 三层都校验）
3. Redis token 存储结构 = `JSON {user_id, entity_id}`；旧 int 格式 token 一律视为无效（强制重新登录）
4. Bot 通道（X-BOT-KEY）不走用户 token，靠 query/header 指定账套时仍须校验 entity active

---

## 5. 验证要点（每次改动后）

```bash
# 1. 隔离验证：登录账套A → 全系统只显示A的数据
curl -s -X POST http://127.0.0.1:8010/api/cma/auth/login \
  -H "Content-Type: application/json" \
  -d '{"username":"admin","password":"admin123","entity_id":2}'
# → token；用该token访问 /api/cma/kpis 只返回博海数据

# 2. 越权验证：未授权entity请求返回403
curl -s -X POST http://127.0.0.1:8010/api/cma/auth/switch-entity \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"entity_id":999}'   # → 403

# 3. Bot通道：无token带query entity_id 仍可用
curl -s -H "X-BOT-KEY: cma-bot-finance-2026" \
  "http://127.0.0.1:8010/api/cma/bot/overview"   # → 200
```
