Files
opsx-vpn/docs/specs/SDD-Feishu-directory-sync.md
T

70 lines
5.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 飞书组织与成员定时导入
| 字段 | 内容 |
|---|---|
| 版本 / 变更级别 | 0.1 / S3(通讯录权限、个人信息持久化与服务发布) |
| 设计状态 | 已形成实施方案 |
| 实现状态 | 已部署;当前飞书可见范围仅含一名成员,部门尚不可见 |
| 负责人 / 日期 | Codex / 2026-10-23 |
| 关联需求 | 飞书组织成员导入,启用后每 30 分钟同步 |
| 批准状态 | 用户要求开发;飞书应用权限由管理员自行授予 |
## 1. 背景、目标与范围
- 当前行为:[IdentityProfile](../../backend/internal/model/model.go)仅保存完成浏览器登录的成员姓名;[Profiles](../../backend/internal/identityprovider/grants.go)只返回已验证的成员供专线白名单挑选。飞书基础姓名接口已能返回结果,但个别姓名是“用户+编号”。
- 目标:按应用可见范围读取部门和直属成员,提供管理员手动同步、独立自动同步开关与最近结果;启用后每 30 分钟检查并同步。
- 范围:单租户、单应用;只导入成员的 open_id、姓名和部门关系。保留现有 VPN 拨号用户、登录档案、专线授权,不修改它们的认证语义。
- 非目标:自动开通 VPN、自动授予专线白名单、飞书事件订阅、手机号/邮箱导入、跨应用用户合并、反向写飞书。
## 2. 需求与约束
- FR-01:管理员可在 HTTPS 飞书身份页启用/关闭每 30 分钟同步,并手动触发一次同步;未启用身份服务时拒绝同步。
- FR-02:先读取通讯录可见范围,逐个遍历部门子节点、分页读取各部门直属成员,按 open_id 去重;无法完成任何一页则本次不写入。
- FR-03:只在完整读取成功后批量更新导入表和状态;不删除缺席记录,避免范围缩小或临时失权导致误删。不得从导入结果自动给 VPN 授权。
- FR-04:页面显示同步时间、部门/成员数及失败提示,并列出导入的成员和部门供管理员核对。
- NFR-01:限制单次运行时长、页数和条数,同一实例内互斥;外部错误不含密钥、token 或通讯录原始报文。
- NFR-02:仅管理权限、HTTPS 入口可读写导入信息;关闭自动同步不会删除已导入数据。
## 3. 架构与方案
- Go 身份模块获取应用 tenant token 后调用 `/contact/v3/scopes`、`/departments/:id/children`、`/users`,遍历 scopes 的可见部门和独立用户;仅保存足够显示的字段。
- 定时器每分钟检查一次启用状态和最近尝试时间;新启用后可立即手动同步,30 分钟后再次自动尝试。失败也等待 30 分钟以免持续打飞书 API。
- 导入表与已登录档案隔离;已登录档案与白名单保持原有含义。后续若要用导入表作为准入依据,需要另行设计身份绑定和离职策略。
## 4. 接口、数据与配置契约
- 新增 `GET /api/system/identity/directory-sync`、`PUT /api/system/identity/directory-sync`(enabled)、`POST /api/system/identity/directory-sync/run`。仅 admin + HTTPS;不返回凭据。
- 新增同步状态、部门、成员表,使用 `(issuer, app_id, id)` 区分应用;保留缺席成员,仅更新本次见到的数据和 `last_seen_at`。
- 最近同步状态还记录飞书 `/scopes` 返回的可见部门入口数和单独成员数;这不是企业通讯录总人数。
- 不修改原有登录、专线及 VPNUser API。Fly书权限以实际 API 报错为准:部门与成员列表通常要求部门和用户的应用身份读取权限及足够的通讯录可见范围,页面提供具体引导。
## 5. 风险与验收
| 风险 | 处理 | 信号 |
|---|---|---|
| 飞书权限不足/范围太窄 | 返回可定位的错误,不覆盖旧数据 | 最近失败状态、飞书错误码 |
| 分页中途失败 | 先读到内存再事务写入 | 上次成功时间不变 |
| 服务重启/重复操作 | 数据库上次尝试时间 + 单实例互斥 | 状态显示运行中或最近尝试时间 |
| 姓名字段被飞书脱敏 | 保留返回姓名,不推断真实身份 | 管理页显示飞书原值 |
| AC | 对应需求 | 前置/操作 | 预期 | 验证方式 | 结果 |
|---|---|---|---|---|---|
| AC-01 | FR-01 | 启用自动同步并重启 | 下次到期继续运行,关闭后停止 | 代码检查/人工验证 | 未启用;待权限配置完善后验证 |
| AC-02 | FR-02 | 模拟部门分页与成员重复 | 完整遍历、按 open_id 去重 | `go test ./backend/internal/identityprovider -run TestReadDirectory` | 通过 |
| AC-03 | FR-03 | 中间页错误 | 不接受不完整快照,不给新准入 | `TestReadDirectoryFailsClosedOnIncompletePage`;Go 事务代码检查 | 通过 |
| AC-04 | FR-04 | 打开身份页 | 可查看状态及成员摘要 | `npm run build`、远程 API 手动试读 | API 通过:1 名成员、0 个部门;页面待人工检查 |
2026-10-23 首次测试服务器部署后,手动同步成功,自动开关仍关闭。实际仅 1 位可见成员、0 个部门,无法据此认为企业只有 1 位员工;等待飞书开放平台配置部门权限与应用通讯录可见范围后复测全量组织。后续界面增补了可见范围计数与部门缺失提示,待再次部署复测。
## 6. 发布与恢复
- 先构建及测试;部署前核对数据库备份和应用进程。首次发布默认不启用定时同步;管理员授权、发布应用并核对可见范围后手动运行再开启。
- 验证管理健康检查、现有 OpenVPN 进程、同步状态。失败时关闭同步;回滚程序二进制,新增表可保留,不更动拨号数据。
- 不执行自动删除/反向写飞书;数据存在本地数据库,管理员可通过备份恢复。
## 7. 待决问题
| 问题 | 阻塞 | 推荐 |
|---|---|---|
| 是否让导入名单自动成为 VPN 准入 | 否 | 此次不变更,另立准入规则与离职失效 SLA |