Files
JX-BPOS/apps/api/src/modules/org/CONTRACT.md
T
ChaiHaoandClaude Fable 5 6f36f8340a 三面拆分⑩:工作组管理落员工区 —— ADR-011 欠账清账
建组/改组:逐岗位选人,留空即回落四策略。绑定先于策略,起单时快照进工作流;
实测改绑定后新起单的 work_group 快照即含新成员表,在途不受影响。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m
2026-07-27 21:28:30 +08:00

3.8 KiB

module: org

职责: 员工与岗位:账号、口令与登录会话(ADR-010)、岗位授权(客体三类 × 动作三种)、 停用前置检查、岗位在岗人员查询(负责人四策略解析的候选集来源)、 工作组(ADR-011:起单时的岗位绑定)、种子数据播种 不负责: 负责人解析算法本身(见 process)、任务与卡的在途判定(经 InFlightWorkProbe 端口由 app 注入)、 鉴权中间件与 cookie 的读写(app 层:§4「中间件在 app 层,身份判定读 org 出口」)

公开出口: index.ts

  • 类型:OrgService StaffWithPosition CreateStaffInput IssuedSession 与 @contracts 的 Staff / Position / Grant / GrantObject / GrantAction / DisableCheck / Session 转出
  • 错误:StaffNotFoundError PositionNotFoundError AccountTakenError DisableBlockedError
  • 端口类型:PositionRepository StaffRepository GrantRepository SessionRepository WorkGroupRepository PasswordHasher InFlightWorkProbe Clock IdGenerator
  • 用例工厂:createOrgService(deps: OrgServiceDeps): OrgService
  • 纯函数:isAuthorized actionCovers objectCovers
  • 常量:POSITION_SEED STAFF_SEED DEFAULT_LOGIN_ACCOUNT
  • adapter 工厂(供 app 装配):createPositionRepository createStaffRepository createGrantRepository createSessionRepository createWorkGroupRepository objectKeyOf
  • 口令散列(供 app 装 PasswordHasher):hashPassword verifyPassword newSessionToken
  • Drizzle 表(供 app 迁移与 join):positionTable staffTable grantTable sessionTable workGroupTable

层级: L1(编码规范 §4) 依赖: @contracts;@shared/db(Drizzle 句柄与 sys schema) port:InFlightWorkProbe(app 注入 task / human-tool 的只读用例)、IdGenerator 被依赖: process(负责人解析的候选集)、message、interaction、human-tool、report、 app(鉴权中间件与 /api/gov/staff 路由组装)

不变量:

  • 动作蕴含:治理 ⊃ 写 ⊃ 读;数据客体三级覆盖(域 ⊃ 对象 ⊃ 字段)
  • 停用是全有全无:前置检查未过即抛 DisableBlockedError,不做部分执行
  • authorize 对停用员工恒 false
  • ensureSeeded 幂等:按 code / account 判存在,已存在则跳过,不覆盖人工改动; 唯一例外是补种口令 —— 已存在但 password_hash 为空的员工会被补上初始口令, 否则 ADR-010 之前建的账号永远登不进(这不算覆盖人工改动:空不是改动)
  • 登录失败不区分原因:账号不存在 / 已停用 / 未设口令 / 口令错,一律 undefined; 且无论账号是否存在都走一遍散列比对,免得响应时间泄漏账号是否存在
  • 明文永不落库:口令只在 login / setPassword 的入参上停留一瞬,落库前即散列
  • 会话可即刻作废:服务端表而非签名 cookie —— logout 立刻生效; disableStaff 连带 revokeAllOf,被停用的人拿旧 cookie 也办不了事
  • 工作组是绑定不是策略(ADR-011):它给出岗位的人选,四策略于是无从发问; bindingOf 只出成员表快照,在岗与否由调用方(process)核 —— 岗位候选集核不了跨岗位的绑定

影响半径: contract 增 login / logout / resolveSession / setPassword 与 IssuedSession; port 增 SessionRepository / PasswordHasher,StaffRepository 增两个口令读写方法; 下游只有 app(装配 + 鉴权中间件),depcruise 已反查确认

近变(2026-07-27):模块面未变。app 层新增工作组读写路由 (GET|PUT /api/gov/work-groups),组装既有的 workGroupOf / saveWorkGroup —— ADR-011 落地时欠的那个治理界面就此清账,一人一组的口径由 app 层按 ownerId 保证。