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)定向下发。workerPUBLISH {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 文档同步规则)。