|
|
@@ -0,0 +1,361 @@
|
|
|
+# 账号与账套体系设计(Profile / Company / 三级可见性)
|
|
|
+
|
|
|
+| 字段 | 值 |
|
|
|
+|---|---|
|
|
|
+| 文档编号 | 设计-20261019-账号与账套体系 |
|
|
|
+| 版本 | v1.0 |
|
|
|
+| 创建日期 | 2026-10-19 |
|
|
|
+| 状态 | **待评审**(用户裁决:先出文档不改代码) |
|
|
|
+| 依据 | 参考项目 `E:\work\fmode-studio` 源码调研 + **生产库实测**(`server.fmode.cn/parse`) |
|
|
|
+| 关联 | 《数据访问层设计.md》· 《API-契约.md》· SPEC FR-20(超管可见范围)· FR-02(登录鉴权) |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 一、需求与结论
|
|
|
+
|
|
|
+### 1.1 用户需求
|
|
|
+
|
|
|
+1. 用户有自己的**个人账套** —— 登录后只看自己的容器
|
|
|
+2. 公司下面有**公司账套** —— 公司管理员登录后看到公司下面所有人的容器
|
|
|
+3. **超级管理员**能看到所有公司所有人的容器
|
|
|
+
|
|
|
+### 1.2 结论(先行)
|
|
|
+
|
|
|
+**数据库已有完整承载结构,不需要新建表。** 缺的是 App 侧没有把 `Profile` 这一层用起来。
|
|
|
+
|
|
|
+| 需求 | 数据层 | App 侧现状 |
|
|
|
+|---|---|---|
|
|
|
+| 个人账套 | ✅ `_User.company` + 容器 `user` 指针 | ✅ 已实现(`scope='mine'`) |
|
|
|
+| 公司账套 | ✅ `Company` + `_Role.<cid>_Admin` | ⚠️ 已实现但**判据不完整**(见 §四) |
|
|
|
+| 超管全局 | ✅ `_Role.FMODE_AGENT_SUPERADMIN` | ✅ 已实现(`scope='all'`) |
|
|
|
+| **成员身份(owner/member)** | ✅ **`Profile.identyType`** | 🔴 **完全未使用** |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 二、数据库真实结构(实测证据)
|
|
|
+
|
|
|
+> 全部结论均有**生产库实测**支撑;标的为实测字段与取值分布。
|
|
|
+
|
|
|
+### 2.1 四层身份模型
|
|
|
+
|
|
|
+```
|
|
|
+_User(登录账号 —— 只负责认证)
|
|
|
+ ├─ username / mobile / nickname / avatar
|
|
|
+ ├─ company ──→ Company # 主账套
|
|
|
+ └─ account ──→ Account # 计费账户(可选)
|
|
|
+
|
|
|
+Profile(成员档案 —— 真正的"某人在某公司"身份,一人可多条)
|
|
|
+ ├─ user ──→ _User
|
|
|
+ ├─ company ──→ Company # 该身份属于哪个账套
|
|
|
+ ├─ department ──→ Department # 部门(树形)
|
|
|
+ ├─ identyType : string # 🔴 身份/权限标签
|
|
|
+ └─ state / name / mobile / idcard / position / school …
|
|
|
+
|
|
|
+Company(账套/租户 —— 多租户根)
|
|
|
+ ├─ name / title / desc
|
|
|
+ └─(业务表一律带 company: Pointer<Company>)
|
|
|
+
|
|
|
+Department(部门 —— 树形,本期不用)
|
|
|
+ ├─ company ──→ Company
|
|
|
+ └─ parent ──→ Department
|
|
|
+
|
|
|
+_Role(角色)
|
|
|
+ ├─ <companyObjectId>_Admin ← 🔴 租户级管理员(大写 Admin,仅 2 个)
|
|
|
+ ├─ <companyObjectId>_admin ← 业务功能级(小写,69 个)
|
|
|
+ ├─ <companyObjectId>_public
|
|
|
+ ├─ FMODE_AGENT_SUPERADMIN ← 🔴 平台超管
|
|
|
+ └─ masters / Administrator ← 平台级
|
|
|
+```
|
|
|
+
|
|
|
+### 2.2 实测:`Profile` 表结构
|
|
|
+
|
|
|
+字段(`classes/Profile?limit=2`):
|
|
|
+```
|
|
|
+objectId, name, idcard, user(Pointer), company(Pointer), department(Pointer),
|
|
|
+position, positionPart, mobile, nation, education, school, sex, polity,
|
|
|
+studentID, isCross, identyType, nativePlace, studentType, SchoolMajor,
|
|
|
+departments, center, batch, openLesson, state
|
|
|
+```
|
|
|
+
|
|
|
+样本:
|
|
|
+```json
|
|
|
+{"name":"许诺","identyType":"试点","user":"xlKwSDFsO0",
|
|
|
+ "company":"1ErpDVc1u6","department":"0rlUv1wLEf"}
|
|
|
+```
|
|
|
+
|
|
|
+### 2.3 🔴 `identyType` 权威规范(**源码确证,来自 future-server**)
|
|
|
+
|
|
|
+> 权威源:`E:\work\future-server\api\api-ncloud\fmode\enterprise-service.js`
|
|
|
+> 这是**服务端**(fmode 平台企业服务)的实现,是 `identyType` 的**唯一权威定义源**。
|
|
|
+> 此前本文档基于实测数据的推测**已被本节取代**。
|
|
|
+
|
|
|
+#### (1)角色判据 —— 服务端原文
|
|
|
+
|
|
|
+```js
|
|
|
+// enterprise-service.js:123 —— 默认值与归一
|
|
|
+function profileRole(profile) {
|
|
|
+ return cleanText(profile?.get('identyType') || 'member', 40).toLowerCase();
|
|
|
+}
|
|
|
+
|
|
|
+// enterprise-service.js:127 —— 🔴 企业管理员判据(权威白名单)
|
|
|
+function isEnterpriseAdmin(profile) {
|
|
|
+ return ['owner', 'admin', 'enterprise_admin', '管理员'].includes(profileRole(profile));
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### (2)三级角色与权限(`enterprisePermissions`, enterprise-service.js:187-190)
|
|
|
+
|
|
|
+| `identyType` | 归一角色 | tabs | manageMembers | inviteAdmin | manageAgents |
|
|
|
+|---|---|---|---|---|---|
|
|
|
+| `owner` | **owner** | 全部 8 个 | ✅ | ✅ | ✅ |
|
|
|
+| `admin` / `enterprise_admin` / `管理员` | **admin** | 全部 8 个 | ✅ | ❌ | ✅ |
|
|
|
+| **其余一切值**(含 `member`、全部业务标签) | **member** | 6 个(无 `agents`/`members`)| ❌ | ❌ | ❌ |
|
|
|
+
|
|
|
+> 原文第 190 行 `return { role: 'member', ... }` —— **兜底为 member**,
|
|
|
+> 即「**不在白名单里的一律是普通成员**」。
|
|
|
+
|
|
|
+#### (3)写入点全清单(谁写、写什么)
|
|
|
+
|
|
|
+| 位置 | 场景 | 写入值 |
|
|
|
+|---|---|---|
|
|
|
+| `enterprise-service.js:662-663` | 平台审核企业申请 | **`join_existing` → `member`**;**`new_company` → `owner`** |
|
|
|
+| `enterprise-service.js:717` | 企业管理员批准成员加入 | 入参 或 **`member`** |
|
|
|
+| `enterprise-service.js:604` | 变更成员角色 | 入参 `role` |
|
|
|
+| `enterprise-service.js:979` | 邀请码加入 | `invite.role` 或 **`member`** |
|
|
|
+| `enterprise-subscription-service.js:536` | 订阅开通时建企业主档案 | **`owner`** |
|
|
|
+
|
|
|
+**关键**:`defaultRole` 由**申请类型**决定 —— 建新企业 = `owner`,加入现有 = `member`。
|
|
|
+
|
|
|
+#### (4)平台审核员(另一条独立线)
|
|
|
+
|
|
|
+```js
|
|
|
+// enterprise-service.js:152-163
|
|
|
+function isPlatformEnterpriseReviewerValue(value) {
|
|
|
+ return ['admin','superadmin','platform_admin','enterprise_reviewer',
|
|
|
+ 'enterprise_review_admin','fmode_enterprise_reviewer',
|
|
|
+ '管理员','超级管理员'].includes(value);
|
|
|
+}
|
|
|
+// 判据:user 的 type/role/roles/identyType/permissions 任一命中,
|
|
|
+// 或 _Role.name 命中(enterprise-service.js:165-177)
|
|
|
+```
|
|
|
+
|
|
|
+#### (5)实测:生产库取值分布(3000 条抽样,26 种)
|
|
|
+
|
|
|
+| 取值 | 数量 | 创建时间 | 类别 |
|
|
|
+|---|---|---|---|
|
|
|
+| `(字段不存在)` | 966 | — | — |
|
|
|
+| `学员` | 733 | 2021-02 ~ 2025-10 | 业务标签(100% 带学生字段)|
|
|
|
+| `user` | 684 | 2022-10 ~ 2025-12 | ⚠️ **业务标签,非权限角色**(见下)|
|
|
|
+| `试点` | 177 | 2022-08 ~ 2023-09 | 业务标签 |
|
|
|
+| **`owner`** | **134** | **2026-08 ~ 2026-10** | 🔴 **权限角色** |
|
|
|
+| `anchor` | 96 | 2025-04 ~ 2025-08 | 业务标签(唯一有专属判据:`ailiao/routes.js:530`)|
|
|
|
+| `二学历` / `招生老师` / `衔接` | 72/38/20 | 2022~2023 | 业务标签 |
|
|
|
+| **`member`** | **8** | **2026-09 ~ 2026-10** | 🔴 **权限角色** |
|
|
|
+| `children`/`benefit`/`student`/`kindness`/`公职人员`/`监理员`/`护士`/… | 1~12 | 业务 | 业务标签 |
|
|
|
+| `admin` | **1** | 2020-10 | 权限角色(历史遗留)|
|
|
|
+
|
|
|
+**判读**:
|
|
|
+- **权限角色**(`owner`/`admin`/`member`)**创建于 2026-08 之后** —— 企业体系是新上线的
|
|
|
+- **业务标签**(`学员`/`试点`/`anchor` 等)是 **2020~2025 老业务**的自定义标签,与权限无关
|
|
|
+- 🔴 **`user`(684 条,老体系)= 业务标签,不是权限角色** ——
|
|
|
+ 权威源码 `isEnterpriseAdmin` 白名单**不含** `user`,`enterprisePermissions` 对它兜底为 **member**
|
|
|
+
|
|
|
+#### (6)⚠️ 一处过时文档(勿依据)
|
|
|
+
|
|
|
+`E:\work\fmode-studio\docs\schema\project.md:308-316` 声明枚举为 `admin`/`coders`,
|
|
|
+但实测 3000 条中 **`coders` 仅 1 条**,而 `owner` 134 条。
|
|
|
+**该文档过时,不得作为判据依据。**
|
|
|
+
|
|
|
+#### (7)DB 层无约束
|
|
|
+
|
|
|
+`future-server` 全仓(含 `fmode-db/`)**无** SQL 枚举/CHECK 约束 → `identyType` 是
|
|
|
+**纯应用层约定**,取值域开放,判据**必须白名单精确匹配**。
|
|
|
+
|
|
|
+### 2.4 🔴 实测:角色命名两套并存
|
|
|
+
|
|
|
+| 模式 | 数量 | 语义 | 判据用途 |
|
|
|
+|---|---|---|---|
|
|
|
+| `<objectId>_Admin`(**大写**) | **2** | 租户级管理员 | ✅ **`acl.ts` 用的就是这个,正确** |
|
|
|
+| `<objectId>_admin`(小写) | **69** | 业务功能级(部门/审核/财务…) | ❌ 与权限分级无关 |
|
|
|
+| `<objectId>_public` | 多 | 公开读 | — |
|
|
|
+| `FMODE_AGENT_SUPERADMIN` | 1 | 平台超管 | ✅ 已用 |
|
|
|
+| `masters` / `Administrator` | 各有 | 平台级 | 已用 `masters` |
|
|
|
+
|
|
|
+> **修正记录**:调研中曾一度误判「`acl.ts` 把 `_Admin` 写成大写是 bug」。
|
|
|
+> **实测证明 `_Admin`(大写)才是租户级管理员角色**,`acl.ts` 判据正确。
|
|
|
+> 此误判已在此留痕,避免后续维护者重复踩坑。
|
|
|
+
|
|
|
+### 2.5 实测:超管账号的角色挂载
|
|
|
+
|
|
|
+| 账号 | 挂载角色 |
|
|
|
+|---|---|
|
|
|
+| `sr2WiPsDyQ`(刘雨飏) | `7pIbDBJmKx_Admin` + **`FMODE_AGENT_SUPERADMIN`** |
|
|
|
+| `9RDXB9RFqj`(新手机号账号) | **无任何角色** |
|
|
|
+| `sr2WiPsDyQ` 的 Profile 条数 | **0**(超管权限来自 `_Role`,不经 Profile) |
|
|
|
+
|
|
|
+**判读**:超管走 `_Role`,普通成员走 `Profile` —— **两条路径并存**,判级需同时覆盖。
|
|
|
+
|
|
|
+### 2.6 实测:`Profile.company` 与 `_User.company` 高度一致
|
|
|
+
|
|
|
+抽样 7 个用户,`Profile.company === _User.company` **全部一致**。
|
|
|
+
|
|
|
+**判读**:`Profile` 是「主账套」的强化扩展;**一人多公司是少数情况**(抽样 50 人中 1 例)。
|
|
|
+
|
|
|
+### 2.7 实测:规模
|
|
|
+
|
|
|
+| 表 | 数量 |
|
|
|
+|---|---|
|
|
|
+| 表总数 | 874(其中 `0`–`873` 为无关索引表) |
|
|
|
+| `Company` | **100+** |
|
|
|
+| `_Role` | 60+ 租户角色 + 平台角色 |
|
|
|
+| `FmodeAgent` | 52 个容器,分属 34 个 owner |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 三、三级可见性 —— 设计
|
|
|
+
|
|
|
+### 3.1 判定链(唯一实现,置于 `core/acl.ts`)
|
|
|
+
|
|
|
+```
|
|
|
+ ┌── ① 平台超管?
|
|
|
+ │ roles 含 'FMODE_AGENT_SUPERADMIN' | 'masters'
|
|
|
+ │ → scope 'all' 全平台所有公司所有人
|
|
|
+ 登录后 → 判级 ──┼── ② 租户管理员?
|
|
|
+ │ roles 含 '<myCompanyId>_Admin'
|
|
|
+ │ 或 Profile.identyType ∈ {owner, admin}
|
|
|
+ │ → scope 'company' 本公司在册全部成员
|
|
|
+ └── ③ 普通成员
|
|
|
+ → scope 'mine' 仅本人容器
|
|
|
+```
|
|
|
+
|
|
|
+**判定顺序不可调换**(超管优先于租户管理员)。
|
|
|
+
|
|
|
+### 3.2 与现状的差异
|
|
|
+
|
|
|
+| # | 现状 | 目标 | 差异 |
|
|
|
+|---|---|---|---|
|
|
|
+| 1 | `roles` 含 `_Admin` → company | 同 | ✅ 无差异 |
|
|
|
+| 2 | **未查 `Profile.identyType`** | 需查 | 🔴 **缺失** —— 仅靠 `Profile` 授权的成员会漏判 |
|
|
|
+| 3 | roles 缺失 → `mine` | 同 | ✅ 安全默认正确 |
|
|
|
+
|
|
|
+**影响**:若某租户**只写 `Profile.identyType=owner` 而不建 `_Admin` 角色**,该管理员会被判为 `mine` → 看不到同事。
|
|
|
+
|
|
|
+> 当前实测中 `E4KpGvTEto_Admin` / `7pIbDBJmKx_Admin` 都存在,故**现有数据下未暴露**;
|
|
|
+> 但这是**结构性缺口**,新租户若只走 Profile 就会踩到。
|
|
|
+
|
|
|
+### 3.3 判据的健壮性要求(依据 §2.3 权威规范)
|
|
|
+
|
|
|
+- `identyType` **必须白名单匹配**:**`['owner', 'admin', 'enterprise_admin', '管理员']`**
|
|
|
+ —— 🔴 **与服务端 `isEnterpriseAdmin` 逐字对齐**(enterprise-service.js:127)
|
|
|
+- **不得**把 `member` / `user` 当作管理员:
|
|
|
+ · `member` 是**普通成员**(服务端 `enterprisePermissions` 明确)
|
|
|
+ · `user` 是**老业务标签**(684 条,2022~2025),服务端白名单不含它
|
|
|
+- **不做**「非空即管理员」这类放开判定(会把「学员」「护士」「anchor」全变成管理员)
|
|
|
+- **大小写不敏感**:服务端 `profileRole()` 已 `.toLowerCase()`,前端须一致
|
|
|
+- **回落顺序**:`roles` 命中 → `Profile.identyType` 命中 → **`mine`**
|
|
|
+- **异常安全**:Profile 查询失败**不得**放开权限,一律回落 `mine`(与服务端兜底一致)
|
|
|
+
|
|
|
+> **与服务端对齐的价值**:前端判据与服务端 `isEnterpriseAdmin` **同源**,
|
|
|
+> 可避免「前端看到公司视图但服务端返回 403」这类不一致。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 四、App 侧改动方案(**本次不实施**,供评审与排期)
|
|
|
+
|
|
|
+### 4.1 P0 · 补 Profile 判据
|
|
|
+
|
|
|
+| 文件 | 改动 |
|
|
|
+|---|---|
|
|
|
+| `core/api/models.ts` | `Me` 增 `memberships?: Membership[]`(见 §4.2 结构) |
|
|
|
+| `core/api/parse-api.ts` | `fetchMe` 补查 `Profile`(`where={user:Pointer(me)}`,**可多条**) |
|
|
|
+| `core/acl.ts` | 判级链补 ②-Profile 分支(白名单 + 大小写不敏感) |
|
|
|
+| `core/acl.spec.ts` | 补用例:`owner` → company;`_Admin` → company;Profile 查询失败 → mine |
|
|
|
+
|
|
|
+**风险**:`fetchMe` 多一次请求(可缓存)。
|
|
|
+
|
|
|
+### 4.2 P1 · 多账套切换(**用户裁决:要做**)
|
|
|
+
|
|
|
+**数据**:`Profile[]` → `memberships`:
|
|
|
+
|
|
|
+```ts
|
|
|
+interface Membership {
|
|
|
+ profileId: string;
|
|
|
+ companyId: string;
|
|
|
+ companyName: string;
|
|
|
+ identyType: string; // owner | admin | member | user | 其他业务标签
|
|
|
+ current: boolean; // 是否当前账套
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**新增服务** `core/company.service.ts`:
|
|
|
+- 持有「当前账套」状态(`signal`)
|
|
|
+- 切换时写 `localStorage['company']`(沿用参考项目的键名,便于跨端一致)
|
|
|
+- 切换后触发 `LiveService.reset()` + 各域服务重载
|
|
|
+
|
|
|
+**UI**:
|
|
|
+- 桌面:导航栏顶部 / 「我的」页 —— **仅 `memberships.length > 1` 时显示**
|
|
|
+- 移动:「我的」页
|
|
|
+
|
|
|
+**服务端配合**:切换后所有 `scope` 请求需带新账套上下文。
|
|
|
+> ⚠️ **依赖声明**:需确认云函数 `xlosi3m46q` 是否支持按传入 `companyId` 过滤。
|
|
|
+> 若不支持,需服务端改造(与 FR-20 的依赖性质相同)。
|
|
|
+
|
|
|
+### 4.3 明确不做(用户裁决)
|
|
|
+
|
|
|
+| 项 | 裁决 |
|
|
|
+|---|---|
|
|
|
+| 部门维度(部门主管看本部门) | ❌ 不做 |
|
|
|
+| App 内建企 / 入企 / 成员管理 | ❌ 不做,走 fmode-studio |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 五、与「管理端 vs 客户端」的分工
|
|
|
+
|
|
|
+**本项目是客户端,不是管理端。**
|
|
|
+
|
|
|
+| 能力 | 归属 |
|
|
|
+|---|---|
|
|
|
+| 注册 / 建企 / 入企申请 / 审核 / 成员管理 / 角色授予 | **fmode-studio**(已有) |
|
|
|
+| 看自己的容器 / 对话 / 成果 / 多账套切换 | **fmode-app-v2**(本项目) |
|
|
|
+
|
|
|
+**依据**:SPEC §3.2 明确「自建后端」在范围外;本项目不实现注册与建企流程。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 六、验证方式(实施时)
|
|
|
+
|
|
|
+| # | 场景 | 期望 |
|
|
|
+|---|---|---|
|
|
|
+| 1 | 普通用户(`9RDXB9RFqj`,无角色无 Profile) | `scope=mine` → 只见自己容器(当前 0 个 → 空态) |
|
|
|
+| 2 | 租户管理员(roles 含 `<cid>_Admin`) | `scope=company` → 见本公司成员 |
|
|
|
+| 3 | 仅 Profile.identyType=owner 的账号 | `scope=company`(**当前会误判 mine,本方案修复点**) |
|
|
|
+| 4 | 超管(`sr2WiPsDyQ`) | `scope=all` → 全平台 52 个容器 |
|
|
|
+| 5 | Profile 查询失败 | 回落 `mine`(**不得放开**) |
|
|
|
+| 6 | 多账套账号 | 切换后容器列表随之变化 |
|
|
|
+
|
|
|
+```bash
|
|
|
+npm test # acl.spec 补 Profile 分支用例
|
|
|
+npm run smoke
|
|
|
+npm run verify:ui
|
|
|
+npm run build
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 七、待确认清单
|
|
|
+
|
|
|
+| # | 事项 | 影响 | 优先级 |
|
|
|
+|---|---|---|---|
|
|
|
+| 1 | 云函数 `xlosi3m46q` 是否支持按 `companyId` 过滤(多账套切换的前提) | 多账套功能可行性 | 🔴 高 |
|
|
|
+| 2 | `identyType` 的**权威取值域**是否只有 `owner`/`admin`/`member`/`user` | 判据白名单 | 🟡 中 |
|
|
|
+| 3 | 是否需要「一个 Profile 归属多账套」以外的形态 | 数据模型 | ⬜ 低 |
|
|
|
+| 4 | `localStorage['company']` 键名是否沿用参考项目 | 跨端一致性 | ⬜ 低 |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 八、关联文档
|
|
|
+
|
|
|
+- 《数据访问层设计.md》(`FmodeApi` 抽象层与领域模型)
|
|
|
+- 《API-契约.md》§二.1(云函数 `xlosi3m46q` 的 `scope` 口径)· §五(ACL 判级)
|
|
|
+- SPEC FR-02(登录鉴权)· FR-20(超管可见范围)
|
|
|
+- 参考实现:`E:\work\fmode-studio\server\claude-rules\rules\03-database-schema.md`
|