# 飞书组织与成员定时导入 | 字段 | 内容 | |---|---| | 版本 / 变更级别 | 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:页面显示同步时间、部门/成员数及失败提示,并列出导入的成员和部门供管理员核对。 - FR-05:飞书身份页突出提醒管理员发布应用,并将应用可用范围设为“全体员工”;打开页面即通过现有应用身份凭据检查实时通讯录可见范围,存在可见部门时探测部门子节点及成员列表权限。范围为空或接口被拒绝时分别提示,不能把接口可用误报为全员可见。 - 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;不返回凭据。 - 新增 `GET /api/system/identity/directory-sync/check`,只返回 `/scopes` 的入口计数和部门/用户列表探测状态、受控错误码,不返回姓名、open_id 或 access token。可用范围“全体员工”和应用发布状态无法由这些接口直接证明,须在 UI 明确标为人工设置。 - 新增同步状态、部门、成员表,使用 `(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 个部门;页面待人工检查 | | AC-05 | FR-05 | 打开身份页(部门入口 0,单独成员 3) | 红色提示全体员工/发布;自动显示范围不足且不误判部门接口权限 | `TestDirectoryAccessCheckDistinguishesVisibilityAndEndpointPermission`、前端构建、远程只读检查 | 测试及远程 API 通过;页面待人工检查 | 2026-10-23 测试服务器部署 `594bb61` 后,手动同步成功,自动开关仍关闭。`/scopes` 实际返回可见部门入口 0 个、单独可见成员 1 位;成功导入成员 1 位、部门 0 个。这与用户截图中的企业通讯录 3 位成员不同,不能将可见范围误认为企业总人数。界面已增补范围计数与部门缺失提示;等待管理员在飞书开放平台配置部门权限、应用通讯录可见范围并发布后复测组织树。HTTPS 登录页返回 200、管理健康检查通过、OpenVPN 原进程持续运行。 2026-10-23 部署 `e259771` 后,实时权限检查读到部门入口 0 个、单独成员 3 位;由于部门不可见,部门和直属成员列表接口均标记“未验证”,不误报权限通过。手动同步成功导入 3 位成员、0 个部门;查询时自动同步开关已为启用状态(本轮未更改)。仍需管理员在飞书控制台核对发布状态、可用范围“全体员工”和部门通讯录可见范围。管理健康检查与 HTTPS 登录页正常,OpenVPN 原进程持续运行。 ## 6. 发布与恢复 - 先构建及测试;部署前核对数据库备份和应用进程。首次发布默认不启用定时同步;管理员授权、发布应用并核对可见范围后手动运行再开启。 - 验证管理健康检查、现有 OpenVPN 进程、同步状态。失败时关闭同步;回滚程序二进制,新增表可保留,不更动拨号数据。 - 不执行自动删除/反向写飞书;数据存在本地数据库,管理员可通过备份恢复。 ## 7. 待决问题 | 问题 | 阻塞 | 推荐 | |---|---|---| | 是否让导入名单自动成为 VPN 准入 | 否 | 此次不变更,另立准入规则与离职失效 SLA |