跳转至

08 - API 设计(REST 契约)

本文是人读的 API 契约;机器可读的事实源是 packages/shared 中的 zod schema(类型、请求校验、错误码枚举均由它生成/校验)。二者冲突时以代码为准,并在同一 commit 内回改本文档。架构背景(鉴权形态、工作区隔离、通知路由、SSE 链路)见 03-architecture.md §8、§11。

1. 通用约定

项 约定
前缀 /api/v1(反向代理只代理该前缀到 api)
鉴权 better-auth httpOnly session cookie(同域反代下浏览器请求与 SSE 自动携带);程序化客户端可经 jwt 插件签发 Bearer。/auth/*、/admin/* 契约由 better-auth 定义(§3.1)
工作区上下文 除认证、组织级管理与 /workspaces 本身外,所有业务端点必须带 X-Workspace-Id 头。api 校验请求者是该工作区活动成员后,将其注入 service 层、Prisma scope 与 RLS 事务变量;不得信任 body/query 中的 workspaceId
ID 主键 uuid(v7,时间有序),路径参数与响应均为字符串形式
响应 成功直接返回资源 JSON / 204 No Content,不包 envelope
错误 统一结构 { code, message, details? }(§2);code 为机器可读枚举,定义在 packages/shared
校验 请求体/查询参数经 zod schema 校验(schema 即契约),失败返回 400 + 字段级 details
金额 契约层一律传分(integer),前端格式化为元——金额红线贯穿到接口
时间 时间戳 ISO 8601 UTC(2026-09-25T08:30:00Z);发票业务日期 YYYY-MM-DD
分页 ?page=1&pageSize=50&sort=-created_at(- 前缀降序);响应 { items, total, page, pageSize };内网数据量用 offset 分页
上传限制 multipart files[]:单次 ≤ 20 个、单文件 ≤ 10MB(对齐百度 OCR 请求体上限,可配)、扩展名 pdf/jpg/png。Web 客户端可递归展开拖入的文件夹并分批调用本端点;目录名与相对路径不进入 API。
幂等 重复上传由 workspace_file(workspace_id, blob_id) 唯一约束兜底 → 逐文件 DUPLICATE_FILE;命中文件的原发票已软删则恢复该发票(restored: true);跨工作区哈希命中不暴露文件存在性;写操作不引入 Idempotency-Key

2. 错误码

2.1 HTTP 状态码语义

状态码 含义
400 请求结构错误(非校验类,如 multipart 畸形)
401 未认证(无/失效 session)
403 已认证但无权限(非成员 / 角色不足 / system 非管理员)
404 资源不存在,或跨工作区资源一律表现为不存在(不泄露存在性)
409 冲突:重复文件、重复发票号、批次状态不允许操作
400 参数校验失败(zod),details 为字段级错误数组
429 触发限流(上传过频、OCR 配额排队拒绝等)
500 未预期错误(结构化日志记录 requestId)

2.2 错误码枚举(packages/shared 定义,与代码同源)

通用

code HTTP 场景
UNAUTHENTICATED 401 未登录 / session 过期
WORKSPACE_REQUIRED 400 缺 X-Workspace-Id 头
NOT_WORKSPACE_MEMBER 403 请求者不是该工作区活动成员
ROLE_INSUFFICIENT 403 成员角色不足以执行该操作
SYSTEM_ADMIN_REQUIRED 403 需要 system admin(组织级配置)
VALIDATION_FAILED 422 参数校验失败
RATE_LIMITED 429 限流
INTERNAL 500 未预期错误

文件与解析

code HTTP 场景
DUPLICATE_FILE 409 同工作区文件哈希已存在(uploads 响应里为逐文件 reason,非整体失败)
FILE_TOO_LARGE / FILE_TYPE_UNSUPPORTED 422 超大小 / 扩展名白名单外
TASK_NOT_FOUND 404 任务不存在或跨工作区
WORKSPACE_FILE_NOT_FOUND 404 文件不存在或跨工作区(成员与排障下载一致口径,不泄露存在性)
TASK_NOT_RETRYABLE 409 仅 failed 可重试
OCR_PROVIDER_ERROR 500 百度侧异常(worker 落库,api 透出为任务状态而非此码)
OCR_PROVIDER_NOT_CONFIGURED 500 OCR_PROVIDER 未设置或取值非法(永久性,配置后新任务生效)
OCR_NOT_INVOICE 422 票据模板不匹配(百度 282103,M1 终态 failed,M2 转附件子管道)
QUOTA_EXCEEDED_OCR 429 百度免费额度月耗尽(任务终态 failed)
USAGE_LIMIT_EXCEEDED 429 平台用量配额超限(组织配额 block;OCR 任务终态 failed,模型回退规则)
USAGE_ENTITLEMENT_NOT_FOUND 404 用量配额不存在(跨组织/已删除)

发票 / 附件 / 批次

code HTTP 场景
INVOICE_NOT_FOUND 404 发票不存在或跨工作区
DUPLICATE_INVOICE_NUM 409 业务级票号防重命中(invoice_dedup_key 组织级唯一)
ATTACHMENT_NOT_FOUND 404 附件不存在或跨工作区
ATTACHMENT_NOT_MATCHABLE 409 附件非 unmatched 状态,不能人工配对
BATCH_NOT_FOUND 404 批次不存在或跨工作区
BATCH_STATE_INVALID 409 非 collecting 批次不允许入批/排序/删批;collecting → paid 之外的状态流转;发票已入批或非待报销
EXPORT_NOT_READY 409 导出任务未完成即请求下载
EXPORT_FAILED 409 导出任务终态失败(details.jobId 供排查)

工作区 / 邮箱 / 通知

code HTTP 场景
WORKSPACE_NOT_DELETABLE 409 工作区内仍有发票/批次,仅空空间可删
MEMBER_ALREADY_EXISTS 409 用户已是成员
MEMBER_ROLE_INVALID 422 角色枚举外 / owner 数量约束
MAIL_CREDENTIAL_INVALID 422 测试连接失败(details.reason 区分网络/鉴权)
MEMBER_NOT_FOUND 404 目标用户不是该工作区活动成员
NOTIFY_CHANNEL_NOT_FOUND 404 频道不存在或跨工作区(workspaceId 与请求头不一致同响应)
CHANNEL_SCOPE_INVALID 403 频道 scope 鉴权:个人仅本人、workspace 须 owner;organization 不再支持
CHANNEL_DELIVERY_FAILED 422 测试发送失败(details.reason:network / http / secret)

财务门户

code HTTP 场景
FINANCE_ACCOUNT_NOT_FOUND 404 账户不存在或跨工作区(不泄露存在性)
FINANCE_ACCOUNT_DUPLICATE 409 同工作区同厂商同标签已存在
FINANCE_CREDENTIAL_INVALID 422 登录测试失败(凭据/弱密码/锁定)或库内密文损坏(details.reason)
FINANCE_PORTAL_UNAVAILABLE 422 门户维护/网络/会话异常,稍后重试
FINANCE_SOLVER_NOT_CONFIGURED 503 验证码求解 Provider 未配置(CAPTCHA_SOLVER 非法)

3. 端点定义

3.1 认证与用户(better-auth 接管)

/auth/*、/admin/* 由 better-auth 处理(绕过 Nest 守卫与 zod 管道),错误结构为库自有格式,前端统一经官方 React client 调用。关键端点:

方法 路径 说明
POST /auth/sign-in/username 学号/工号 + 密码 → session(httpOnly cookie)
POST /auth/sign-in/social OAuth 登录(GitHub / CC98 等,provider 指定)
POST /auth/link-social 为当前账号绑定一个 OAuth 登录方式(provider 指定)
GET /auth/list-accounts 当前账号已绑定的登录方式列表
POST /auth/unlink-account 解绑一个登录方式 { accountId }(须保留至少一个)
POST /auth/sign-out 吊销当前 session
GET /auth/get-session 当前会话(含 system role),前端 useSession
POST /auth/update-user 改显示名(密码经 /account/password)
POST /admin/create-user、/admin/set-role、/admin/ban-user 建号 / system 角色 / 停用(admin only)

3.1.1 邀请码(平台建号)

开放注册关闭。新用户须有效 8 位邀请码(字符集 A-Z2-9 去 0O1I)。

方法 路径 权限 说明
POST /invites/preview 公开 { code } → 有效性(不消耗)
POST /invites/bind 公开 校验码并种 poco.pending_invite cookie(OAuth 建号前,15 分钟)
POST /invites/register 公开 { code, email, password, name } 核销建号并种 session
POST /admin/invites/batch system admin { count, maxUses?, expiresInDays?, note? } 批量生成,明文仅此响应
GET /admin/invites system admin 列表(无明文,含后缀/用量/状态)
POST /admin/invites/:id/revoke system admin 吊销

错误码:INVITE_REQUIRED / INVITE_INVALID / INVITE_EXPIRED / INVITE_EXHAUSTED / INVITE_REVOKED / INVITE_NOT_FOUND。

set-password 在 better-auth 中是 serverOnly、不经 HTTP 暴露;OAuth-only 用户补「邮箱密码」登录方式走 Nest 桥接 POST /account/password(登录态,body { newPassword },至少 8 位;要求 session 新鲜,默认 24h)。

3.2 工作区与成员(/workspaces,无需 X-Workspace-Id)

方法 路径 最低角色 说明
GET /workspaces 登录 我的工作区列表(自动个人空间 + 已加入空间),含摘要(文件/任务/活跃成员数);UI 统一称「工作区」,多人时前端以云图标标识
POST /workspaces 登录 新建工作区 { name, description? },创建者为 owner(库内 kind=shared)
GET /workspaces/:id member 空间详情 + 摘要统计
PATCH /workspaces/:id manager 改名 / 描述
DELETE /workspaces/:id owner 删除空间(实现为归档 status=archived,成员即刻失权;须为空,否则 WORKSPACE_NOT_DELETABLE;个人空间不可删)
GET /workspaces/:id/members member 成员列表(username、显示名、角色、加入时间)
POST /workspaces/:id/members owner 添加成员 { username, role: member\|viewer\|manager }(不能直接加 owner)
PATCH /workspaces/:id/members/:userId owner 改成员角色
DELETE /workspaces/:id/members/:userId owner(或自移) 移除成员(软删 status=disabled 立即失权;owner 自移前须先转移)
POST /workspaces/:id/transfer-owner owner 转让 owner { userId },原 owner 降为 manager

3.3 上传与解析任务

方法 路径 最低角色 说明
POST /uploads member multipart 多文件 → 落盘 + 入队;逐文件返回结果,整体 200(见示例);命中同空间同文件且原发票/附件已软删时只恢复该记录并置 restored: true / restoredKind(不跨发票/附件连带恢复)
GET /tasks viewer 任务列表 ?status=pending/processing/done/failed/duplicate/skipped,监控积压
GET /tasks/:id viewer 单任务状态(SSE 为主的轮询兜底)
POST /tasks/:id/retry member 重试 failed 任务(重新入队,沿用既有 workspace_file)
// POST /uploads 响应示例
{
  "items": [
    { "file": "滴滴出行.pdf", "accepted": true, "taskId": "0192a7f0-…" },
    { "file": "误删重传.pdf", "accepted": true, "restored": true },
    { "file": "旧发票.jpg", "accepted": false, "reason": "DUPLICATE_FILE" },
    { "file": "note.exe", "accepted": false, "reason": "FILE_TYPE_UNSUPPORTED" },
  ],
}

3.4 发票与附件

方法 路径 最低角色 说明
GET /invoices viewer 筛选 ?status&category&dateFrom&dateTo&batchId&keyword&uncategorized + 分页排序
GET /invoices/:id viewer 返回指定发票的 InvoiceView(与列表项同字段,含提醒及附件摘要;不含 OCR 原始返回);跨工作区、软删或不存在返回 404
PATCH /invoices/:id member 手动修正(白名单字段:标题/卖方/类别/日期/金额不开放),置 is_manual_edited
DELETE /invoices/:id member 软删(deleted_at)并解除附件配对:附件转待配对(status=unmatched、invoice_id=null、路径回 Archive/unmatched/)一并软删;重传附件只恢复该附件并用已存 OCR 重跑匹配(命中活动发票自动挂载,否则留待配对),不连带恢复发票
POST /invoices/:id/re-parse member { mode: "replay" \| "refetch" }——重放 ocr_raw_json(免费)或重调 OCR,走队列
POST /invoices/:id/re-recognize member 用新端点重识别该发票的活动附件(docs/12 §8.3):逐附件经 parse 队列重跑结构化识别 + 重匹配(付费、异步、可重试)→ { queued }(提交的附件数)
GET /attachments?status=unmatched viewer 待配对列表(含 OCR 全文与提取信号)
POST /attachments/:id/match member 人工配对 { invoiceId } → 同步写库更新挂载与 archive_path 前缀(纯写库)
POST /attachments/rematch member 回放重匹配:对本工作区 unmatched 附件用已存 ocr_raw_text + 原文件名重跑匹配(不重新调 OCR、不额外付费),返回 { scanned, matched }
POST /invoices/:id/attachments member 详情页补传附件(multipart,走附件子管道匹配/直接挂载)
DELETE /attachments/:id member 软删附件(deleted_at + 路径进 Archive/.trash/);同文件重传恢复原附件(restoredKind=attachment)

GET /invoices 列表项含只读 reminders 数组(docs/03 §5.5 决策层):每项为 { ruleId, severity: info|warning|critical, title, message, provider },由规则 provider(必要时叠加 System One 决策模型,provider 记为 rule/jev/clef)对金额、票面关键字段、分类、商品词与附件类型实时求值,不落库;无违规为空数组。列表项另含 requirements: { ruleId, status }[],显示需人工处理的材料规则已持久化状态;历史发票尚未生成材料要求时可为空数组。休闲娱乐业或礼品的明确关键词直接产生 critical;模型仅对模糊休闲词给出待人工复核的 warning。

列表项含只读 invoiceKind(规范化票种,docs/12 §2.4;历史数据为 null),以及只读 attachments 数组(只读、排除软删):{ id, attachmentType, originalName, sourceFileId, routes },供发票库悬停预览截图/行程单;routes 为行程类附件的路段摘要数组 { city, origin, destination }[](docs/12 §7,多段行程按段展开:非悬停用首段「城市-起点-终点」命名,悬停展示全部;无路线为空数组)。文件经 /files/workspace-files/:sourceFileId/download 受控下载。

GET /attachments 列表项含只读 previewAvailable(boolean):该附件源文件是否已有服务端渲染的首页缩略图(PDF)。为 true 时前端经 /files/workspace-files/:sourceFileId/thumbnail 取图填卡片缩略图;为 false(非 PDF、渲染失败或历史数据未生成)时回退文件图标,PDF 仍可悬停/点击走原件预览。

3.5 批次与导出

方法 路径 最低角色 说明
GET/POST /batches viewer / member 列表 / 建批 { name, note?, targetAmountFen?, kind? }(kind 默认 daily;金额聚合走 SQL,不冗余存储)
POST /batches/suggest member 选票策略建议 { priorities?, targetAmountFen?, maxSingleAmountFen?, maxCount? }(docs/02 §4:优先打车/3C/小额、单张上限排除大额、目标装满)→ { selected, selectedTotalFen, excluded, skipped, candidateCount };只建议不落库、不自动入批
GET/PATCH/DELETE /batches/:id viewer / manager / manager 详情(聚合 + 批内发票)/ 改名、备注、目标额度、报销类型与状态流转(collecting → paid 单步)/ 删批(发票回 parsed 与 unbatched/ 前缀、该批导出任务与产物一并在库内清理,仅 collecting)
POST /batches/:id/invoices member 入批 { invoiceIds } → 写库更新批次关联与 archive_path 前缀(无文件移动,批次须 collecting)
DELETE /batches/:id/invoices/:invoiceId member 拆批迁回 unbatched/ 虚拟路径,发票回 parsed(任意批次状态均可退回归集区)
PATCH /batches/:id/order member 批次内拖拽排序 { invoiceIds }(全量序列)
POST /batches/:id/export/xlsx member 触发 Excel 导出(异步)→ { exportId },完成经 SSE 通知
POST /batches/:id/export/pdf member 触发打印版 PDF 合并(异步)
POST /batches/:id/export/zip member 触发批次文件夹整体打包(异步,含发票+附件+导出物)
GET /exports/:id viewer 导出任务状态(processing/done/failed + 产物元信息)
GET /exports/:id/download viewer 导出物下载(?download=1 触发浏览器另存)
GET /files/workspace-files/:id/download viewer 原件/附件受控下载(附件 ?attachmentId=;反向代理不做静态直出)
GET /files/workspace-files/:id/thumbnail viewer PDF 首页缩略图受控读取(image/png);未生成返回 PREVIEW_NOT_FOUND,前端回退文件图标。派生资源不写审计
GET /admin/files/workspace-files/:id/download system admin 排障下载:?reason= 必填(≥4 字符),成功即写 access_audit

3.6 邮箱账户 / 规则 / 通知 / 额度 / 统计

方法 路径 最低角色 说明
GET/POST /mail-accounts viewer / manager 邮箱账户列表 / 添加(授权码信封加密,掩码回显)
PATCH/DELETE /mail-accounts/:id manager 编辑(提交空值不覆盖密文)/ 删除
POST /mail-accounts/:id/test manager 测试连接(IMAP 登录 + 可见邮件数;失败 MAIL_CREDENTIAL_INVALID)
POST /mail-accounts/:id/fetch manager 手动触发拉取(Redis 按账户加锁防并发重扫)
GET/POST /finance-accounts viewer / manager 财务门户账户列表 / 添加(provider=zju,密码信封加密,掩码回显;pollIntervalSeconds 下限 300)
PATCH/DELETE /finance-accounts/:id manager 编辑(password 空串不覆盖密文)/ 删除(纯库操作)
POST /finance-accounts/:id/test manager 门户现场登录探测(验证码求解 + doLogin;失败 FINANCE_CREDENTIAL_INVALID / FINANCE_PORTAL_UNAVAILABLE)
POST /finance-accounts/:id/sync manager 手动触发同步(finance-sync 队列,Redis 按账户加锁)
GET /finance-accounts/:id/portal viewer 现场只读门户概览:登录人姓名、学工号、手机号,日常报销项目的余额/额度/已报销,当前报销单,以及最近 3 个月的历史报销单;每张单附预约物流与进度摘要(每类最多预取 15 张)。不落库、不回密码。失败与测试连接同一错误码。
GET /finance-accounts/:id/claims/:source/:reqNo/logistics viewer 单张报销单预约物流(source=current\|history),概览未预取时点击兜底。返回 { source, reqNo, logistics, progress }。失败与 portal 同一错误码。
GET /finance-accounts/:id/claims/:serialNo/print viewer 现场拉 findMultiPrintData 原样 JSON({ data, serialNo, reqNo })。门户不下发 PDF;前端用门户 createForm 版式渲染后下载。失败与 portal 同一错误码。
GET/POST /rules viewer / member 分类规则列表 / 新建,scope=organization\|workspace\|personal
PATCH/DELETE /rules/:id member(scope 内) 组织级须 system admin;优先级口径见 04 文档
GET /notifications 登录 站内通知 ?unreadOnly(个人维度,无工作区头);三个业务主题的通知项可根据 key 与 workspaceId 跳到所属工作区的发票或计财处面板。
PATCH /notifications/read 登录 批量已读 { ids } 或 { all: true }
GET/POST /notify-channels viewer / 登录用户 返回本人全局 user 频道 + 当前 workspace 共享频道;任意用户可创建 scope=user,workspace 须 owner;organization 创建返回 400 VALIDATION_FAILED。请求可带 topics: invoice_material[] 等订阅。
PATCH/DELETE /notify-channels/:id 同频道管理 user 频道仅本人;共享频道沿用 scope 权限。编辑 secret 缺省/空串不覆盖密文,跨用户/工作区或不存在均 404。
POST /notify-channels/:id/test 同频道管理 真实投递一次测试通知;失败 CHANNEL_DELIVERY_FAILED。

invoice_material 的通知正文包含工作区、发票号码、商户或票面内容、金额与问题;机器人 Webhook 对共享频道及 active owner/manager 的个人频道再附上传人姓名。普通个人频道与站内通知不显示上传人;NotificationDelivery 持久记录的仍是同一业务事件,投递时按频道生成可见正文。

个人 type=dingtalk 频道也接受 HTTPS connector.dingtalk.com/webhook/flow/<id> 自动化流程地址,其他 connector 路径不在白名单。此入口发送兼容钉钉机器人结构的 Markdown JSON,不使用旧机器人加签参数;签名密钥留空。流程配置使用 Text 参数格式、关键词匹配,以及后续「发送消息到该群组」的「源数据解析」。工作区/组织频道可按该明确地址自动识别协议。

钉钉 invoice_material 有详情链接时使用 msgtype=actionCard,载荷 actionCard={title,text,singleTitle:"查看详情",singleURL};文字摘要和字段顺序与飞书材料卡片共用,不展示冗长票号,金额/问题加粗。没有详情链接时使用相同排版的 Markdown。旧机器人加签参数仍置于 URL;工作流入口同样可通过源数据解析接收机器人兼容消息。

将材料要求从 resolved/waived 改为 open 会增加 generation 并发送一条新事件,正文仍含完整票面摘要和源文件上传时间;问题说明包含人工重新打开动作及请求中填写的备注。对已经 open 的要求再次设 open 不重复创建事件。

通知深链接登录后保留目标工作区/发票,仅规范 UUID 被接受;无权限工作区不查询目标发票。详情支持已入批发票,软删/跨工作区仍返回 404。viewer 在详情只能读取,API 状态操作按上传者或管理者授权。待发送个人频道在投递时再次验证 active 工作区成员关系,失效频道记 skipped;机器人缺失成功业务码的响应按失败处理并重试。

finance_claim_progress 事件 payload 的 previousStatus/status 使用去除空状态字段和首尾空白后的组合值;活动/历史列表转换中的纯格式差异不产生事件,真实状态往返仍按 revision 创建独立事件。

飞书 invoice_material 的 Webhook 请求为 msg_type=interactive、card.schema=2.0,card.header.title 是带色标题「📌 发票材料提醒」,card.body.elements[0].text 以 text_size=heading-4 加粗「问题类型 · 发票内容」摘要,后续 lark_md 详情各行带语义表情,按工作区、商户、金额、具体问题、可见的上传人、源文件上传时间排序;问题显示完整规则理由,仅金额数值与问题加粗,不重复内容,也不展示票号。底部蓝色 primary 按钮「查看详情」通过 open_url 打开 /?workspaceId=<uuid>&invoiceId=<uuid>。首页验证工作区成员关系后定位发票;GET /invoices/:id 在 workspace context 下返回与列表项相同的 InvoiceView,跨工作区、软删或不存在返回 404。飞书财务提醒仍为 text,其他渠道不使用卡片。飞书签名字段 timestamp/sign 及投递成功判定与文本消息相同。 | GET | /invoices/:id/requirements | viewer | 五类材料要求及 open/resolved/waived 状态、解决人/时间、可选证明附件。 | | POST | /invoices/:id/evidence-attachments | 上传者或 manager+ | multipart file,上传 PDF/JPG/PNG(≤10MB)并直接挂载为当前发票的 proof 附件;返回 { attachmentId, originalName }。同一文件重复上传当前发票返回已有附件,文件已被其他记录使用则拒绝;不进入 OCR 解析队列。 | | PATCH | /invoices/:id/requirements/:ruleId | 上传者或 manager+ | 请求体含 status(resolved、waived 或 open)、可选 note 和 evidenceAttachmentId;证明附件必须已挂载到本工作区当前发票,写入状态历史。扫码验证与固定资产认定可人工完成或重新打开。 | | GET | /quota/summary | viewer | 各额度剩余/已用(口径见 02 文档) | | POST/PATCH/DELETE | /quota/plans | member / manager | 额度计划 CRUD(共享空间额度 manager+) | | GET | /quota/plans/:id/snapshots | viewer | 余额快照序列(M3) | | GET | /usage/summary | viewer | 平台用量汇总:按 provider/operation/unitKind 聚合,周期 YYYY-MM(docs/03 §5.7);与报销额度无关 | | GET | /usage/events | viewer | 平台用量明细(分页,按工作区 RLS 隔离) | | GET/POST/PATCH/DELETE | /usage/entitlements | viewer / owner | 组织级用量配额 CRUD(写需 owner):{ provider, period, includedUnits, overagePolicy } | | GET/PUT/DELETE | /ocr-credential | 登录用户 | 用户自备百度 OCR 凭据(docs/03 §5.7):GET 返回凭据掩码 + 当月免费额度;PUT 保存(信封加密);DELETE 删除;明文永不回显(红线 4) | | GET | /stats/dashboard | viewer | 页头指标:待报销总额与张数(同口径 parsed)、累计已解析数、未匹配附件数、队列积压 | | GET | /admin/dashboard | system admin | 管理大屏全局聚合(docs §5.8):用户/租户、各 API 用量与成本、各功能使用;?days=7\|14\|30(默认 14)。跨组织只读,服务角色聚合 | | GET | /healthz | 无鉴权 | 探活,恒 200(不探测下游) |

3.7 角色能力速查(与 04 文档 §3 能力映射一致)

端点组 owner manager member viewer system admin
工作区设置 / 成员 / owner 转让 ✅ ❌ ❌ ❌ ❌
批次管理(建/删/改名)、邮箱、财务门户、频道(workspace 级)、共享额度维护 ✅ ✅ ❌ ❌ ❌
平台用量配额维护(/usage/entitlements 写) ✅ ❌ ❌ ❌ ❌
管理大屏(GET /admin/dashboard) ❌ ❌ ❌ ❌ ✅
上传 / 修正 / 入批 / 导出触发 ✅ ✅ ✅ ❌ ❌
查询与下载 ✅ ✅ ✅ ✅ ❌(排障走审计流程,写 access_audit)
组织级规则 / 用户与 system 角色 — — — — ✅

4. SSE 事件流

  • GET /events(text/event-stream;session cookie 同域自动携带,无需 ticket)
  • 订阅范围:按请求者成员关系过滤——仅下发其所属工作区的事件;个人通知(recipient_user_id)定向下发。worker PUBLISH {workspaceId | recipientUserId, type, payload} → api 订阅后扇出。
  • 心跳:每 25s 注释帧 : ping(同时用于 Caddy/中间设备保活与断连探测)。
  • 重连:浏览器 EventSource 自动重连;重连成功后前端补拉 /tasks、/notifications 兜底错过的事件。
事件类型 payload 触发
task.updated { taskId, workspaceId, status, invoiceId?, attachmentId?, error? } 解析任务每次状态迁移(含 duplicate/failed)
invoice.created { invoiceId, workspaceId, category, amountFen } 发票入库(parsed)
notification { id, level, title, body, workspaceId? } 站内通知落库(个人定向)
stats.updated { workspaceId, pendingTasks, unmatchedAttachments } 仪表盘指标变化(节流 5s)
export.completed { exportId, workspaceId, kind, downloadUrl } 导出任务终态(failed 时 error 替代 downloadUrl)
event: task.updated
id: 0192a7f1-…
data: {"taskId":"0192a7f0-…","workspaceId":"0191…","status":"done","invoiceId":"0192…"}

: ping

5. 版本与变更管理

  • /api/v1 内只做向后兼容变更:加字段、加端点、加错误码、加事件类型;不改既有字段语义、不删端点、不收窄角色。
  • 破坏性变更 → 升 /api/v2,v1 至少保留一个大版本周期并在响应头 Deprecation/Sunset 标注。
  • 契约变更流程:改 packages/shared 的 zod schema + 本文档 + 受影响端点的集成测试,三者同一 commit(对齐 AGENTS.md 文档同步规则)。