openapi: 3.1.0 info: title: 标博士用户端接口 version: 0.1.0 summary: MVP 用户端(网页版 / 桌面版)的核心接口契约草案 description: | 约定见 `docs/architecture/api-design.md`;数据模型见 `data-model.md`。 这是设计阶段的契约草案。后端骨架搭好后,以 FastAPI 生成的 OpenAPI 快照为准(api-design §9 API1),本文件不再维护。 不含运营后台接口(独立应用,另行定义)。 servers: - url: /api/v1 description: 网页版同源;桌面版由用户选择服务器地址(架构 §5.2) security: - cookieSession: [] - bearerToken: [] tags: - name: auth description: 登录、注册、会话(identity) - name: me description: 当前用户与企业 - name: billing description: 套餐、额度、开通申请 - name: projects description: 项目、基本信息字段、分包 - name: uploads description: 分片上传 - name: documents description: 文档、页、句子、出处、解析任务 - name: checklist description: 项目检查清单 - name: checks description: 检查批次、问题、人工核对 - name: reports description: 检查报告 - name: qualifications description: 最小资质库 - name: notifications description: 站内通知、事件流、反馈 paths: # ------------------------------------------------------------------ auth /auth/sms-codes: post: tags: [auth] operationId: sendSmsCode summary: 发送短信验证码 security: [] requestBody: required: true content: application/json: schema: type: object required: [phone, purpose] properties: phone: { $ref: '#/components/schemas/Phone' } purpose: { type: string, enum: [login, register, close_tenant] } responses: '202': description: 已发送(号码是否注册都返回 202,不泄露注册状态) content: application/json: schema: type: object required: [resend_after_s] properties: resend_after_s: { type: integer, description: 多少秒后可以重发 } '422': { $ref: '#/components/responses/ValidationFailed' } '429': { $ref: '#/components/responses/RateLimited' } /auth/register: post: tags: [auth] operationId: register summary: 手机号注册(同时创建企业主体和免费版订阅) security: [] requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/RegisterRequest' } responses: '201': description: 注册成功并登录。网页版设置会话 cookie;桌面版在响应体里拿到令牌(只返回这一次) headers: Set-Cookie: { $ref: '#/components/headers/SetSessionCookie' } content: application/json: schema: { $ref: '#/components/schemas/LoginResult' } '409': description: '`phone_already_registered`' content: { application/problem+json: { schema: { $ref: '#/components/schemas/Problem' } } } '422': { $ref: '#/components/responses/ValidationFailed' } /auth/login: post: tags: [auth] operationId: login summary: 手机号 + 验证码登录 description: | 同一账号已有其他设备在线时返回 409 `concurrent_session`,附在线设备列表; 用户确认后带 `force: true` 重发,其他会话被挤下线(API3)。 security: [] requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/LoginRequest' } responses: '200': description: 登录成功 headers: Set-Cookie: { $ref: '#/components/headers/SetSessionCookie' } content: application/json: schema: { $ref: '#/components/schemas/LoginResult' } '404': description: '`phone_not_registered`(验证码校验通过后才会返回)' content: { application/problem+json: { schema: { $ref: '#/components/schemas/Problem' } } } '409': description: '`concurrent_session`' content: application/problem+json: schema: allOf: - $ref: '#/components/schemas/Problem' - type: object properties: sessions: type: array items: { $ref: '#/components/schemas/SessionInfo' } '422': { $ref: '#/components/responses/ValidationFailed' } '429': { $ref: '#/components/responses/RateLimited' } /auth/logout: post: tags: [auth] operationId: logout summary: 退出登录(吊销当前会话) responses: '204': { description: 已退出 } '401': { $ref: '#/components/responses/Unauthenticated' } # ------------------------------------------------------------------ me /me: get: tags: [me] operationId: getMe summary: 当前用户、企业、套餐、权限点 responses: '200': description: OK content: application/json: schema: { $ref: '#/components/schemas/Me' } '401': { $ref: '#/components/responses/Unauthenticated' } patch: tags: [me] operationId: updateMe summary: 修改显示名 requestBody: required: true content: application/json: schema: type: object properties: display_name: { type: string, minLength: 1, maxLength: 40 } responses: '200': description: OK content: application/json: schema: { $ref: '#/components/schemas/Me' } '422': { $ref: '#/components/responses/ValidationFailed' } /me/sessions: get: tags: [me] operationId: listMySessions summary: 我的在线会话 responses: '200': description: OK content: application/json: schema: type: object required: [items] properties: items: type: array items: { $ref: '#/components/schemas/SessionInfo' } /me/sessions/{session_id}: delete: tags: [me] operationId: revokeMySession summary: 让某个设备下线 parameters: - $ref: '#/components/parameters/SessionId' responses: '204': { description: 已吊销 } '404': { $ref: '#/components/responses/NotFound' } /tenant/close: post: tags: [me] operationId: closeTenant summary: 注销账号(删除企业下全部业务数据,不可恢复) description: data-model §6.2。需要短信验证码和企业名称两次确认。成功后当前会话失效。 requestBody: required: true content: application/json: schema: type: object required: [sms_code, confirm_tenant_name] properties: sms_code: { type: string, pattern: '^[0-9]{6}$' } confirm_tenant_name: { type: string } responses: '202': { description: 已受理,后台删除文件 } '422': { $ref: '#/components/responses/ValidationFailed' } # ------------------------------------------------------------------ billing /plans: get: tags: [billing] operationId: listPlans summary: 套餐列表(含额度、文件大小和页数上限,购买页和上传页展示用) security: [] responses: '200': description: OK content: application/json: schema: type: object required: [items] properties: items: type: array items: { $ref: '#/components/schemas/Plan' } /quota: get: tags: [billing] operationId: getQuota summary: 额度总览:本期已用、剩余、按功能分布 responses: '200': description: OK content: application/json: schema: { $ref: '#/components/schemas/QuotaOverview' } /quota/entries: get: tags: [billing] operationId: listQuotaEntries summary: 额度流水 parameters: - name: unit in: query schema: { $ref: '#/components/schemas/QuotaUnit' } - name: project_id in: query schema: { $ref: '#/components/schemas/Id' } - $ref: '#/components/parameters/Cursor' - $ref: '#/components/parameters/Limit' responses: '200': description: 按时间倒序 content: application/json: schema: allOf: - $ref: '#/components/schemas/Page' - type: object required: [items] properties: items: type: array items: { $ref: '#/components/schemas/QuotaEntry' } /activation-requests: get: tags: [billing] operationId: listActivationRequests summary: 我的专业版开通申请 responses: '200': description: OK content: application/json: schema: type: object required: [items] properties: items: type: array items: { $ref: '#/components/schemas/ActivationRequest' } post: tags: [billing] operationId: createActivationRequest summary: 提交专业版开通申请(之后对公转账,运营确认到账后开通,G8) requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/ActivationRequestCreate' } responses: '201': description: 已提交 content: application/json: schema: { $ref: '#/components/schemas/ActivationRequest' } '409': description: '`activation_pending`:已有一份待处理的申请' content: { application/problem+json: { schema: { $ref: '#/components/schemas/Problem' } } } '422': { $ref: '#/components/responses/ValidationFailed' } # ------------------------------------------------------------------ projects /projects: get: tags: [projects] operationId: listProjects summary: 工作台项目列表(FR-M6-01) parameters: - name: status in: query style: form explode: false schema: type: array items: { $ref: '#/components/schemas/ProjectStatus' } - name: deadline_from in: query schema: { type: string, format: date-time } - name: deadline_to in: query schema: { type: string, format: date-time } - name: q in: query description: 按项目名称、编号、采购人搜索 schema: { type: string, maxLength: 100 } - name: sort in: query schema: { type: string, enum: [deadline, -deadline, updated, -updated], default: deadline } - $ref: '#/components/parameters/Cursor' - $ref: '#/components/parameters/Limit' responses: '200': description: OK content: application/json: schema: allOf: - $ref: '#/components/schemas/Page' - type: object required: [items] properties: items: type: array items: { $ref: '#/components/schemas/ProjectListItem' } post: tags: [projects] operationId: createProject summary: 新建项目 requestBody: required: true content: application/json: schema: type: object properties: name: type: string maxLength: 200 description: 选填;不填时为「未命名项目」,解析后按招标文件自动填写 responses: '201': description: 已创建;建项目的人自动成为负责人(AR12) content: application/json: schema: { $ref: '#/components/schemas/ProjectDetail' } '409': description: '`plan_limit_exceeded`:免费版限 1 个项目' content: { application/problem+json: { schema: { $ref: '#/components/schemas/Problem' } } } /projects/{project_id}: parameters: - $ref: '#/components/parameters/ProjectId' get: tags: [projects] operationId: getProject summary: 项目概览 responses: '200': description: OK headers: ETag: { $ref: '#/components/headers/ETag' } content: application/json: schema: { $ref: '#/components/schemas/ProjectDetail' } '404': { $ref: '#/components/responses/NotFound' } delete: tags: [projects] operationId: deleteProject summary: 删除项目(彻底删除业务内容,不可恢复;额度流水保留,G10) responses: '204': { description: 已删除;对象存储里的文件由后台清理 } '404': { $ref: '#/components/responses/NotFound' } /projects/{project_id}/fields/{field_key}: parameters: - $ref: '#/components/parameters/ProjectId' - name: field_key in: path required: true schema: { $ref: '#/components/schemas/ProjectFieldKey' } put: tags: [projects] operationId: setProjectField summary: 人工修改项目基本信息字段(FR-M2-03) description: '`value` 为 null 表示撤销人工值,恢复为 AI 提取值。' parameters: - $ref: '#/components/parameters/IfMatch' requestBody: required: true content: application/json: schema: type: object required: [value] properties: value: { $ref: '#/components/schemas/FieldValue' } responses: '200': description: OK content: application/json: schema: { $ref: '#/components/schemas/ProjectField' } '409': { $ref: '#/components/responses/ProjectReadOnly' } '412': { $ref: '#/components/responses/PreconditionFailed' } '422': { $ref: '#/components/responses/ValidationFailed' } /projects/{project_id}/fields/{field_key}/pending: parameters: - $ref: '#/components/parameters/ProjectId' - name: field_key in: path required: true schema: { $ref: '#/components/schemas/ProjectFieldKey' } post: tags: [projects] operationId: resolvePendingField summary: 确认或驳回补遗带来的新值(FR-M2-10) requestBody: required: true content: application/json: schema: type: object required: [accept] properties: accept: { type: boolean } responses: '200': description: OK content: application/json: schema: { $ref: '#/components/schemas/ProjectField' } '409': description: '`no_pending_value` 或 `project_read_only`' content: { application/problem+json: { schema: { $ref: '#/components/schemas/Problem' } } } /projects/{project_id}/transitions: parameters: - $ref: '#/components/parameters/ProjectId' post: tags: [projects] operationId: transitionProject summary: 手动变更项目状态(递交 / 撤销递交 / 归档 / 撤销归档,FR-M6-02) description: 致命问题未处理也可以递交(红线 4);响应里带当时的就绪状态,由前端提示。 requestBody: required: true content: application/json: schema: type: object required: [action] properties: action: { type: string, enum: [submit, unsubmit, archive, unarchive] } responses: '200': description: OK content: application/json: schema: { $ref: '#/components/schemas/ProjectDetail' } '409': description: '`invalid_transition`' content: { application/problem+json: { schema: { $ref: '#/components/schemas/Problem' } } } /projects/{project_id}/lots: parameters: - $ref: '#/components/parameters/ProjectId' get: tags: [projects] operationId: listLots summary: 分包列表 responses: '200': description: OK content: application/json: schema: type: object required: [items] properties: items: type: array items: { $ref: '#/components/schemas/Lot' } post: tags: [projects] operationId: createLot summary: 手工增加分包(FR-M2-04) requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/LotInput' } responses: '201': description: OK content: application/json: schema: { $ref: '#/components/schemas/Lot' } '409': { $ref: '#/components/responses/ProjectReadOnly' } '422': { $ref: '#/components/responses/ValidationFailed' } /projects/{project_id}/bidding-lots: parameters: - $ref: '#/components/parameters/ProjectId' put: tags: [projects] operationId: setBiddingLots summary: 选择要投的包 requestBody: required: true content: application/json: schema: type: object required: [lot_ids] properties: lot_ids: type: array minItems: 1 items: { $ref: '#/components/schemas/Id' } responses: '200': description: OK content: application/json: schema: type: object required: [items] properties: items: type: array items: { $ref: '#/components/schemas/Lot' } '409': { $ref: '#/components/responses/ProjectReadOnly' } /lots/{lot_id}: parameters: - name: lot_id in: path required: true schema: { $ref: '#/components/schemas/Id' } patch: tags: [projects] operationId: updateLot summary: 修改分包 requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/LotInput' } responses: '200': description: OK content: application/json: schema: { $ref: '#/components/schemas/Lot' } '409': { $ref: '#/components/responses/ProjectReadOnly' } '422': { $ref: '#/components/responses/ValidationFailed' } delete: tags: [projects] operationId: deleteLot summary: 删除分包(原属这个包的清单项变为各包通用) responses: '204': { description: 已删除 } '409': description: '`last_lot`:至少保留一个包;或 `project_read_only`' content: { application/problem+json: { schema: { $ref: '#/components/schemas/Problem' } } } # ------------------------------------------------------------------ uploads /uploads: post: tags: [uploads] operationId: createUpload summary: 开始一次分片上传(架构 §5.2 第 7 条) description: 先按套餐校验文件大小;超出上限直接返回 409,不建上传(FR-M2-01)。 requestBody: required: true content: application/json: schema: type: object required: [filename, size_bytes, purpose] properties: filename: { type: string, maxLength: 255 } size_bytes: { type: integer, minimum: 1 } sha256: { type: string, pattern: '^[0-9a-f]{64}$', description: 选填;提供时完成上传时校验 } purpose: { type: string, enum: [project_document, qualification_file] } responses: '201': description: OK content: application/json: schema: { $ref: '#/components/schemas/Upload' } '409': description: '`plan_limit_exceeded`:超出单文件大小上限,`detail` 写明上限值' content: { application/problem+json: { schema: { $ref: '#/components/schemas/Problem' } } } /uploads/{upload_id}: parameters: - $ref: '#/components/parameters/UploadId' get: tags: [uploads] operationId: getUpload summary: 查询已收到的分片(断点续传用) responses: '200': description: OK content: application/json: schema: { $ref: '#/components/schemas/Upload' } '404': { $ref: '#/components/responses/NotFound' } delete: tags: [uploads] operationId: abortUpload summary: 放弃上传 responses: '204': { description: 已放弃 } /uploads/{upload_id}/parts/{part_no}: parameters: - $ref: '#/components/parameters/UploadId' - name: part_no in: path required: true schema: { type: integer, minimum: 1 } put: tags: [uploads] operationId: putUploadPart summary: 上传一个分片(重复上传同一分片会覆盖) requestBody: required: true content: application/octet-stream: schema: { type: string, format: binary } responses: '204': { description: 已收到 } '404': { $ref: '#/components/responses/NotFound' } '422': { $ref: '#/components/responses/ValidationFailed' } /uploads/{upload_id}/complete: parameters: - $ref: '#/components/parameters/UploadId' post: tags: [uploads] operationId: completeUpload summary: 完成上传:合并分片、按文件头识别格式、读页数 description: | 格式、加密、损坏、页数超限都在这里拦截,不建任务、不扣点(FR-M2-01)。 识别出电子投标平台的加密格式时返回 `bid_file_platform_encrypted`(FR-M3-01)。 responses: '200': description: OK content: application/json: schema: { $ref: '#/components/schemas/UploadCompleted' } '409': description: '`upload_incomplete`' content: { application/problem+json: { schema: { $ref: '#/components/schemas/Problem' } } } '422': description: | `file_format_unsupported`、`file_encrypted`、`file_corrupted`、`bid_file_platform_encrypted`、 `upload_checksum_mismatch`、`plan_limit_exceeded`(页数) content: { application/problem+json: { schema: { $ref: '#/components/schemas/Problem' } } } # ------------------------------------------------------------------ documents /projects/{project_id}/documents: parameters: - $ref: '#/components/parameters/ProjectId' get: tags: [documents] operationId: listDocuments summary: 项目的文档(招标文件、补遗、澄清、投标分册的各个版本) parameters: - name: role in: query schema: { $ref: '#/components/schemas/DocumentRole' } responses: '200': description: OK content: application/json: schema: type: object required: [items] properties: items: type: array items: { $ref: '#/components/schemas/Document' } post: tags: [documents] operationId: createDocument summary: 把上传好的文件挂到项目上(不扣点;招标侧文件要另外发起解析) requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/DocumentCreate' } responses: '201': description: OK content: application/json: schema: { $ref: '#/components/schemas/Document' } '409': { $ref: '#/components/responses/ProjectReadOnly' } '422': { $ref: '#/components/responses/ValidationFailed' } /documents/{document_id}: parameters: - $ref: '#/components/parameters/DocumentId' get: tags: [documents] operationId: getDocument summary: 文档详情 responses: '200': description: OK content: application/json: schema: { $ref: '#/components/schemas/Document' } '404': { $ref: '#/components/responses/NotFound' } delete: tags: [documents] operationId: deleteDocument summary: 删除文档 description: 已被清单项或检查批次引用的文档不能删除,返回 `document_in_use`。 responses: '204': { description: 已删除 } '409': description: '`document_in_use` 或 `project_read_only`' content: { application/problem+json: { schema: { $ref: '#/components/schemas/Problem' } } } /documents/{document_id}/file: parameters: - $ref: '#/components/parameters/DocumentId' get: tags: [documents] operationId: getDocumentFileUrl summary: 取文件的短时下载地址(pdf.js 直接加载,不需要带鉴权头) parameters: - name: variant in: query required: true schema: { type: string, enum: [normalized, original] } responses: '200': description: OK content: application/json: schema: { $ref: '#/components/schemas/DownloadUrl' } '409': description: '`document_not_ready`:规范化 PDF 还没生成' content: { application/problem+json: { schema: { $ref: '#/components/schemas/Problem' } } } /documents/{document_id}/pages: parameters: - $ref: '#/components/parameters/DocumentId' get: tags: [documents] operationId: listDocumentPages summary: 逐页摘要(阅读器的页尺寸、OCR 标记) responses: '200': description: OK content: application/json: schema: type: object required: [normalized_sha256, items] properties: normalized_sha256: { type: string } items: type: array items: { $ref: '#/components/schemas/DocumentPage' } /documents/{document_id}/pages/{page_no}/sentences: parameters: - $ref: '#/components/parameters/DocumentId' - name: page_no in: path required: true schema: { type: integer, minimum: 1 } get: tags: [documents] operationId: listPageSentences summary: 一页的句子(阅读器里选句补出处用;数据来自对象存储的文字层,DM1) responses: '200': description: OK content: application/json: schema: type: object required: [items] properties: items: type: array items: { $ref: '#/components/schemas/Sentence' } '404': { $ref: '#/components/responses/NotFound' } /projects/{project_id}/citations: parameters: - $ref: '#/components/parameters/ProjectId' post: tags: [documents] operationId: createCitation summary: 人工建一条出处(在阅读器里选中句子) requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/CitationCreate' } responses: '201': description: OK content: application/json: schema: { $ref: '#/components/schemas/Citation' } '422': description: '`sentence_not_found`:句子编号不在这份文档里,或不在同一页' content: { application/problem+json: { schema: { $ref: '#/components/schemas/Problem' } } } /documents/{document_id}/parse-jobs: parameters: - $ref: '#/components/parameters/DocumentId' post: tags: [documents] operationId: startParseJob summary: 发起解析(预扣 1 次解析;失败、超时、取消都退回) description: | 只用于招标文件、补遗、澄清;投标分册的解析算在检查里(01 §4)。 失败后重试也调用这个接口,会建一条新任务并重新预扣,上一条已经退回(FR-M2-02)。 requestBody: content: application/json: schema: type: object properties: expected_charge: type: integer enum: [1] description: 前端提示用户的扣点数;和服务端算的不一致时返回 `billing_changed`(API4) responses: '201': description: 已排队 content: application/json: schema: { $ref: '#/components/schemas/ParseJob' } '409': description: '`job_already_running`(附 `job_id`)、`quota_exhausted`、`billing_changed`、`document_role_not_parsable`' content: { application/problem+json: { schema: { $ref: '#/components/schemas/Problem' } } } /parse-jobs/{job_id}: parameters: - $ref: '#/components/parameters/JobId' get: tags: [documents] operationId: getParseJob summary: 解析进度(事件流断线时的轮询兜底) responses: '200': description: OK content: application/json: schema: { $ref: '#/components/schemas/ParseJob' } '404': { $ref: '#/components/responses/NotFound' } /parse-jobs/{job_id}/cancel: parameters: - $ref: '#/components/parameters/JobId' post: tags: [documents] operationId: cancelParseJob summary: 取消解析(退回额度) responses: '202': description: 已受理;worker 在下一个检查点停下 content: application/json: schema: { $ref: '#/components/schemas/ParseJob' } '409': description: '`job_finished`' content: { application/problem+json: { schema: { $ref: '#/components/schemas/Problem' } } } # ------------------------------------------------------------------ checklist /projects/{project_id}/checklist: parameters: - $ref: '#/components/parameters/ProjectId' get: tags: [checklist] operationId: getChecklist summary: 清单概况:状态、版本、各类数量、标注规则、数量核对提示 responses: '200': description: OK content: application/json: schema: { $ref: '#/components/schemas/Checklist' } '404': { $ref: '#/components/responses/NotFound' } /projects/{project_id}/checklist/items: parameters: - $ref: '#/components/parameters/ProjectId' get: tags: [checklist] operationId: listChecklistItems summary: 清单项(一个项目几百条,一次返回全部,不分页) parameters: - name: zone in: query schema: { $ref: '#/components/schemas/Zone' } - name: category in: query schema: { $ref: '#/components/schemas/Category' } - name: lot_scope in: query description: '`bidding` = 通用 + 所投的包(默认,FR-M2-04);`all` = 全部' schema: { type: string, enum: [bidding, all], default: bidding } - name: include_deleted in: query schema: { type: boolean, default: false } responses: '200': description: 按原文顺序 content: application/json: schema: type: object required: [items] properties: items: type: array items: { $ref: '#/components/schemas/ChecklistItem' } post: tags: [checklist] operationId: createChecklistItem summary: 人工新增清单项(标注「人工添加」,可以没有出处) requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/ChecklistItemCreate' } responses: '201': description: OK content: application/json: schema: { $ref: '#/components/schemas/ChecklistItem' } '409': { $ref: '#/components/responses/ProjectReadOnly' } '422': { $ref: '#/components/responses/ValidationFailed' } /checklist-items/{item_id}: parameters: - $ref: '#/components/parameters/ItemId' get: tags: [checklist] operationId: getChecklistItem summary: 清单项详情 responses: '200': description: OK headers: ETag: { $ref: '#/components/headers/ETag' } content: application/json: schema: { $ref: '#/components/schemas/ChecklistItem' } '404': { $ref: '#/components/responses/NotFound' } patch: tags: [checklist] operationId: updateChecklistItem summary: 修改清单项(改过的字段记入 edited_fields;已确认的清单回到草稿) parameters: - $ref: '#/components/parameters/IfMatch' requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/ChecklistItemUpdate' } responses: '200': description: OK headers: ETag: { $ref: '#/components/headers/ETag' } content: application/json: schema: { $ref: '#/components/schemas/ChecklistItem' } '409': { $ref: '#/components/responses/ProjectReadOnly' } '412': { $ref: '#/components/responses/PreconditionFailed' } '422': { $ref: '#/components/responses/ValidationFailed' } delete: tags: [checklist] operationId: deleteChecklistItem summary: 删除(软删除,可以恢复;待确认区的条目就是「丢弃」) responses: '204': { description: 已删除 } '409': { $ref: '#/components/responses/ProjectReadOnly' } /checklist-items/{item_id}/restore: parameters: - $ref: '#/components/parameters/ItemId' post: tags: [checklist] operationId: restoreChecklistItem summary: 恢复已删除的条目 responses: '200': description: OK content: application/json: schema: { $ref: '#/components/schemas/ChecklistItem' } /checklist-items/{item_id}/accept: parameters: - $ref: '#/components/parameters/ItemId' post: tags: [checklist] operationId: acceptPendingItem summary: 从待确认区采纳进正式清单(FR-M2-07) description: 必须补上出处,或者声明「人工确认、无出处」;可以同时修改字段(修改后采纳)。 requestBody: required: true content: application/json: schema: type: object properties: citation_ids: type: array items: { $ref: '#/components/schemas/Id' } no_citation_ack: { type: boolean, default: false } changes: { $ref: '#/components/schemas/ChecklistItemUpdate' } responses: '200': description: OK content: application/json: schema: { $ref: '#/components/schemas/ChecklistItem' } '409': description: '`item_not_pending` 或 `project_read_only`' content: { application/problem+json: { schema: { $ref: '#/components/schemas/Problem' } } } '422': description: '`citation_required`:既没有出处也没有声明无出处' content: { application/problem+json: { schema: { $ref: '#/components/schemas/Problem' } } } /checklist-items/{item_id}/citations: parameters: - $ref: '#/components/parameters/ItemId' put: tags: [checklist] operationId: setItemCitations summary: 替换清单项的出处列表(按顺序) requestBody: required: true content: application/json: schema: type: object required: [citation_ids] properties: citation_ids: type: array items: { $ref: '#/components/schemas/Id' } responses: '200': description: OK content: application/json: schema: { $ref: '#/components/schemas/ChecklistItem' } '409': { $ref: '#/components/responses/ProjectReadOnly' } /checklist-items/{item_id}/supersession: parameters: - $ref: '#/components/parameters/ItemId' post: tags: [checklist] operationId: resolveSupersession summary: 确认或驳回「已被补遗 N 更新」(FR-M2-10) description: 接受时旧条目软删除,新条目保留;驳回时解除关联,两条都保留,由用户自己处理。 requestBody: required: true content: application/json: schema: type: object required: [accept] properties: accept: { type: boolean } responses: '200': description: 返回旧条目 content: application/json: schema: { $ref: '#/components/schemas/ChecklistItem' } '409': description: '`not_superseded`' content: { application/problem+json: { schema: { $ref: '#/components/schemas/Problem' } } } /checklist-items/{item_id}/history: parameters: - $ref: '#/components/parameters/ItemId' get: tags: [checklist] operationId: getItemHistory summary: 修改记录(谁、什么时候、改前、改后;来自审计事件) responses: '200': description: 按时间倒序 content: application/json: schema: type: object required: [items] properties: items: type: array items: { $ref: '#/components/schemas/HistoryEntry' } /projects/{project_id}/checklist/confirm: parameters: - $ref: '#/components/parameters/ProjectId' post: tags: [checklist] operationId: confirmChecklist summary: 确认清单(生成不可变版本) responses: '200': description: OK content: application/json: schema: { $ref: '#/components/schemas/Checklist' } '409': description: '`checklist_empty` 或 `project_read_only`' content: { application/problem+json: { schema: { $ref: '#/components/schemas/Problem' } } } /projects/{project_id}/checklist/versions: parameters: - $ref: '#/components/parameters/ProjectId' get: tags: [checklist] operationId: listChecklistVersions summary: 清单版本 responses: '200': description: OK content: application/json: schema: type: object required: [items] properties: items: type: array items: { $ref: '#/components/schemas/ChecklistVersion' } # ------------------------------------------------------------------ checks /projects/{project_id}/check-preview: parameters: - $ref: '#/components/parameters/ProjectId' get: tags: [checks] operationId: getCheckPreview summary: 发起检查前的预览:这次扣不扣点、用哪些文件、有什么提示 description: 「开始检查」按钮旁的扣点说明来自这里(红线 9)。 responses: '200': description: OK content: application/json: schema: { $ref: '#/components/schemas/CheckPreview' } /projects/{project_id}/check-runs: parameters: - $ref: '#/components/parameters/ProjectId' get: tags: [checks] operationId: listCheckRuns summary: 检查记录 responses: '200': description: 按时间倒序 content: application/json: schema: type: object required: [items] properties: items: type: array items: { $ref: '#/components/schemas/CheckRun' } post: tags: [checks] operationId: startCheckRun summary: 发起检查(FR-M3-02) requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/CheckRunCreate' } responses: '201': description: 已排队 content: application/json: schema: { $ref: '#/components/schemas/CheckRun' } '409': description: | `job_already_running`(附 `job_id`)、`quota_exhausted`、`billing_changed`(附最新的 `preview`)、 `no_parsed_tender`、`no_bid_documents`、`project_read_only` content: { application/problem+json: { schema: { $ref: '#/components/schemas/Problem' } } } /check-runs/{run_id}: parameters: - $ref: '#/components/parameters/RunId' get: tags: [checks] operationId: getCheckRun summary: 检查批次(进度、覆盖范围、各级别数量) responses: '200': description: OK content: application/json: schema: { $ref: '#/components/schemas/CheckRun' } '404': { $ref: '#/components/responses/NotFound' } /check-runs/{run_id}/cancel: parameters: - $ref: '#/components/parameters/RunId' post: tags: [checks] operationId: cancelCheckRun summary: 取消检查(退回额度) responses: '202': description: 已受理 content: application/json: schema: { $ref: '#/components/schemas/CheckRun' } '409': description: '`job_finished`' content: { application/problem+json: { schema: { $ref: '#/components/schemas/Problem' } } } /check-runs/{run_id}/findings: parameters: - $ref: '#/components/parameters/RunId' get: tags: [checks] operationId: listFindings summary: 本批次的全部问题(含「本次已解决」,一次返回全部) parameters: - name: severity in: query schema: { $ref: '#/components/schemas/Severity' } - name: status in: query schema: { $ref: '#/components/schemas/FindingStatus' } - name: presence in: query schema: { $ref: '#/components/schemas/Presence' } responses: '200': description: 按级别、再按原文顺序 content: application/json: schema: type: object required: [items] properties: items: type: array items: { $ref: '#/components/schemas/Finding' } /check-runs/{run_id}/findings/bulk-resolve: parameters: - $ref: '#/components/parameters/RunId' post: tags: [checks] operationId: bulkResolveFindings summary: 批量处理(只限「提示」级,FR-M3-07) requestBody: required: true content: application/json: schema: type: object required: [finding_ids, status] properties: finding_ids: type: array minItems: 1 maxItems: 500 items: { $ref: '#/components/schemas/Id' } status: { type: string, enum: [confirmed_ok, ignored] } note: { type: string, maxLength: 500 } responses: '200': description: 全部成功才提交,否则一条都不改 content: application/json: schema: type: object required: [items, project] properties: items: type: array items: { $ref: '#/components/schemas/Finding' } project: { $ref: '#/components/schemas/ProjectReadiness' } '409': description: '`run_superseded`、`project_read_only`' content: { application/problem+json: { schema: { $ref: '#/components/schemas/Problem' } } } '422': description: '`bulk_only_hint`(附不符合的问题编号)、`note_required`' content: { application/problem+json: { schema: { $ref: '#/components/schemas/Problem' } } } /findings/{finding_id}/resolve: parameters: - $ref: '#/components/parameters/FindingId' post: tags: [checks] operationId: resolveFinding summary: 处理一个问题 description: | 忽略必须写原因;致命问题选「确认无误」也必须写说明,说明同时进入误报反馈(U4)。 只有最近一次检查的问题可以处理,历史批次只读。 requestBody: required: true content: application/json: schema: type: object required: [status] properties: status: { type: string, enum: [confirmed_ok, modified, ignored] } note: { type: string, maxLength: 500 } responses: '200': description: 返回问题和项目最新的就绪状态 content: application/json: schema: type: object required: [finding, project] properties: finding: { $ref: '#/components/schemas/Finding' } project: { $ref: '#/components/schemas/ProjectReadiness' } '409': description: '`run_superseded`、`finding_resolved_this_run`、`project_read_only`' content: { application/problem+json: { schema: { $ref: '#/components/schemas/Problem' } } } '422': description: '`note_required`' content: { application/problem+json: { schema: { $ref: '#/components/schemas/Problem' } } } /findings/{finding_id}/reopen: parameters: - $ref: '#/components/parameters/FindingId' post: tags: [checks] operationId: reopenFinding summary: 撤销处理,回到待处理(U 键) responses: '200': description: OK content: application/json: schema: type: object required: [finding, project] properties: finding: { $ref: '#/components/schemas/Finding' } project: { $ref: '#/components/schemas/ProjectReadiness' } '409': description: '`run_superseded`、`project_read_only`' content: { application/problem+json: { schema: { $ref: '#/components/schemas/Problem' } } } /projects/{project_id}/manual-checks: parameters: - $ref: '#/components/parameters/ProjectId' get: tags: [checks] operationId: listManualChecks summary: 需人工核对的条目和勾选结果(按项目存,P2) responses: '200': description: OK content: application/json: schema: type: object required: [items] properties: items: type: array items: { $ref: '#/components/schemas/ManualCheck' } put: tags: [checks] operationId: setManualCheck summary: 勾选或取消勾选一条 requestBody: required: true content: application/json: schema: type: object required: [check_key, checked] properties: check_key: { type: string, description: 'item:<清单项编号> 或 rule:<规则编号>' } checked: { type: boolean } note: { type: string, maxLength: 500 } responses: '200': description: OK content: application/json: schema: { $ref: '#/components/schemas/ManualCheck' } '409': { $ref: '#/components/responses/ProjectReadOnly' } '422': { $ref: '#/components/responses/ValidationFailed' } # ------------------------------------------------------------------ reports /check-runs/{run_id}/reports: parameters: - $ref: '#/components/parameters/RunId' post: tags: [reports] operationId: generateReport summary: 生成检查报告 PDF(后台生成,完成后推送事件) description: 致命问题未处理也可以导出(红线 4)。每次生成都是一份新报告。 responses: '202': description: 已排队 content: application/json: schema: { $ref: '#/components/schemas/ReportJob' } '409': description: '`run_not_succeeded`' content: { application/problem+json: { schema: { $ref: '#/components/schemas/Problem' } } } /report-jobs/{job_id}: parameters: - $ref: '#/components/parameters/JobId' get: tags: [reports] operationId: getReportJob summary: 报告生成进度(轮询兜底) responses: '200': description: OK content: application/json: schema: { $ref: '#/components/schemas/ReportJob' } '404': { $ref: '#/components/responses/NotFound' } /projects/{project_id}/reports: parameters: - $ref: '#/components/parameters/ProjectId' get: tags: [reports] operationId: listReports summary: 项目的报告(随项目归档,可以重复下载) responses: '200': description: 按时间倒序 content: application/json: schema: type: object required: [items] properties: items: type: array items: { $ref: '#/components/schemas/Report' } /reports/{report_id}/file: parameters: - name: report_id in: path required: true schema: { $ref: '#/components/schemas/Id' } get: tags: [reports] operationId: getReportFileUrl summary: 报告的短时下载地址 responses: '200': description: OK content: application/json: schema: { $ref: '#/components/schemas/DownloadUrl' } '404': { $ref: '#/components/responses/NotFound' } # ------------------------------------------------------------------ qualifications /qualifications: get: tags: [qualifications] operationId: listQualifications summary: 资质库 parameters: - name: kind in: query schema: { $ref: '#/components/schemas/QualificationKind' } responses: '200': description: OK content: application/json: schema: type: object required: [items] properties: items: type: array items: { $ref: '#/components/schemas/Qualification' } post: tags: [qualifications] operationId: createQualification summary: 新增资质 requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/QualificationInput' } responses: '201': description: OK content: application/json: schema: { $ref: '#/components/schemas/Qualification' } '422': { $ref: '#/components/responses/ValidationFailed' } /qualifications/{qualification_id}: parameters: - $ref: '#/components/parameters/QualificationId' put: tags: [qualifications] operationId: updateQualification summary: 修改资质 requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/QualificationInput' } responses: '200': description: OK content: application/json: schema: { $ref: '#/components/schemas/Qualification' } '404': { $ref: '#/components/responses/NotFound' } '422': { $ref: '#/components/responses/ValidationFailed' } delete: tags: [qualifications] operationId: deleteQualification summary: 删除资质(连同附件) responses: '204': { description: 已删除 } /qualifications/{qualification_id}/files: parameters: - $ref: '#/components/parameters/QualificationId' post: tags: [qualifications] operationId: attachQualificationFile summary: 挂上一个已上传的附件 requestBody: required: true content: application/json: schema: type: object required: [upload_id] properties: upload_id: { $ref: '#/components/schemas/Id' } responses: '201': description: OK content: application/json: schema: { $ref: '#/components/schemas/Qualification' } /qualifications/{qualification_id}/files/{file_id}: parameters: - $ref: '#/components/parameters/QualificationId' - name: file_id in: path required: true schema: { $ref: '#/components/schemas/Id' } get: tags: [qualifications] operationId: getQualificationFileUrl summary: 附件的短时下载地址 responses: '200': description: OK content: application/json: schema: { $ref: '#/components/schemas/DownloadUrl' } delete: tags: [qualifications] operationId: deleteQualificationFile summary: 删除附件 responses: '204': { description: 已删除 } # ------------------------------------------------------------------ notifications / events / feedback /notifications: get: tags: [notifications] operationId: listNotifications summary: 站内通知 parameters: - name: unread in: query schema: { type: boolean } - $ref: '#/components/parameters/Cursor' - $ref: '#/components/parameters/Limit' responses: '200': description: 按时间倒序 content: application/json: schema: allOf: - $ref: '#/components/schemas/Page' - type: object required: [items, unread_count] properties: unread_count: { type: integer } items: type: array items: { $ref: '#/components/schemas/Notification' } /notifications/read: post: tags: [notifications] operationId: markNotificationsRead summary: 标为已读(不传编号就是全部已读) requestBody: content: application/json: schema: type: object properties: ids: type: array items: { $ref: '#/components/schemas/Id' } responses: '204': { description: OK } /events: get: tags: [notifications] operationId: streamEvents summary: 事件流(SSE) description: | 当前用户在当前企业下的事件。前端用 fetch 读流(可以带 Bearer 头),不用浏览器的 EventSource(API2)。 每条事件带 `id`;断线重连时带 `Last-Event-ID`,服务端尽量补发,补不了时发 `resync`,前端重新拉取数据。 事件只是「有变化」的提示,权威状态以对应资源的 GET 为准。 parameters: - name: Last-Event-ID in: header schema: { type: string } responses: '200': description: '`text/event-stream`;`event` 字段是 Event.type,`data` 是 Event 的 JSON' content: text/event-stream: schema: { $ref: '#/components/schemas/Event' } /feedback: post: tags: [notifications] operationId: createFeedback summary: 反馈判断有误(误报 / 漏报,J5) requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/FeedbackCreate' } responses: '201': { description: 已提交 } '422': { $ref: '#/components/responses/ValidationFailed' } components: securitySchemes: cookieSession: type: apiKey in: cookie name: bd_session description: 网页版。HttpOnly、Secure、SameSite=Lax;写操作还要带 `X-CSRF-Token`(等于 cookie `bd_csrf` 的值) bearerToken: type: http scheme: bearer description: 桌面版。令牌存系统钥匙串;不需要 CSRF 头 headers: SetSessionCookie: description: 网页版登录时设置 `bd_session`(HttpOnly)和 `bd_csrf`(前端可读) schema: { type: string } ETag: description: 资源版本;修改时放进 If-Match schema: { type: string } parameters: ProjectId: name: project_id in: path required: true schema: { $ref: '#/components/schemas/Id' } DocumentId: name: document_id in: path required: true schema: { $ref: '#/components/schemas/Id' } UploadId: name: upload_id in: path required: true schema: { $ref: '#/components/schemas/Id' } JobId: name: job_id in: path required: true schema: { $ref: '#/components/schemas/Id' } RunId: name: run_id in: path required: true schema: { $ref: '#/components/schemas/Id' } ItemId: name: item_id in: path required: true schema: { $ref: '#/components/schemas/Id' } FindingId: name: finding_id in: path required: true schema: { $ref: '#/components/schemas/Id' } QualificationId: name: qualification_id in: path required: true schema: { $ref: '#/components/schemas/Id' } SessionId: name: session_id in: path required: true schema: { $ref: '#/components/schemas/Id' } Cursor: name: cursor in: query description: 上一页返回的 next_cursor schema: { type: string } Limit: name: limit in: query schema: { type: integer, minimum: 1, maximum: 100, default: 20 } IfMatch: name: If-Match in: header description: 选填;带上时版本不一致返回 412(多人编辑时防止覆盖) schema: { type: string } responses: Unauthenticated: description: '`unauthenticated`、`session_expired`、`session_revoked`(被其他设备挤下线时附 `reason: concurrent_login`)' content: { application/problem+json: { schema: { $ref: '#/components/schemas/Problem' } } } NotFound: description: '`not_found`;访问别的企业的资源也返回这个(架构 §3.3)' content: { application/problem+json: { schema: { $ref: '#/components/schemas/Problem' } } } ValidationFailed: description: '`validation_failed`;`errors` 列出每个字段的问题' content: { application/problem+json: { schema: { $ref: '#/components/schemas/Problem' } } } RateLimited: description: '`rate_limited`;响应头 Retry-After' content: { application/problem+json: { schema: { $ref: '#/components/schemas/Problem' } } } ProjectReadOnly: description: '`project_read_only`:项目已递交或已归档,要先撤销' content: { application/problem+json: { schema: { $ref: '#/components/schemas/Problem' } } } PreconditionFailed: description: '`precondition_failed`:资源已被修改,请刷新后再改' content: { application/problem+json: { schema: { $ref: '#/components/schemas/Problem' } } } schemas: # ---------- 基础 Id: type: string format: uuid Timestamp: type: string format: date-time description: RFC 3339,带时区 Money: type: string pattern: '^-?[0-9]+(\.[0-9]{1,2})?$' description: 金额用字符串传,避免浮点误差;单位元 Phone: type: string pattern: '^1[3-9][0-9]{9}$' description: 中国大陆手机号,11 位 Page: type: object required: [next_cursor] properties: next_cursor: type: [string, 'null'] description: 为 null 表示没有下一页 Problem: type: object description: RFC 9457 problem details,`code` 是给前端判断用的稳定编码(api-design §5) required: [type, title, status, code, request_id] properties: type: { type: string, description: '固定为 about:blank 或错误码文档地址' } title: { type: string, description: 给用户看的一句话(中文) } status: { type: integer } code: { type: string, examples: [quota_exhausted] } detail: { type: string } request_id: { type: string } errors: type: array items: type: object required: [field, code, message] properties: field: { type: string, description: '字段路径,如 spec.tech_param.value' } code: { type: string } message: { type: string } additionalProperties: true DownloadUrl: type: object required: [url, expires_at] properties: url: { type: string, format: uri } expires_at: { $ref: '#/components/schemas/Timestamp' } sha256: { type: string } UserRef: type: object description: 操作人;人离职或注销后仍显示当时的姓名 required: [id, name] properties: id: { $ref: '#/components/schemas/Id' } name: { type: string } # ---------- 枚举(值与数据库一致) ProjectStatus: { type: string, enum: [tracking, drafting, checked, submitted, archived] } LegalRegime: { type: string, enum: [gov_procurement, tendering, unknown] } DocumentRole: { type: string, enum: [tender, addendum, clarification, bid_volume] } DetectedFormat: { type: string, enum: [pdf, docx, doc, wps, ofd, encrypted_bid, unknown] } JobStatus: { type: string, enum: [queued, running, succeeded, failed, cancelled] } Category: { type: string, enum: [qualification, invalidity, scoring, schedule, tech_param, format_doc, price] } Importance: type: string enum: [substantive, important, normal] description: 界面显示为 ★ / ▲ / 一般 Zone: { type: string, enum: [pending, formal] } PendingReason: { type: string, enum: [no_citation, low_confidence, verify_failed, marker_uncovered, absent_in_rerun] } Origin: { type: string, enum: [ai, rule, human] } FindingType: type: string enum: [unresponded, negative_deviation, suspected_unmet, formality, qualification, price_form, manual_review] Severity: { type: string, enum: [fatal, important, hint] } FindingStatus: { type: string, enum: [pending, confirmed_ok, modified, ignored] } Presence: type: string enum: [new, carried, resolved] description: 新增 / 沿用 / 本次已解决 BillingMode: type: string enum: [charged, waived_recheck, free_plan] QuotaUnit: { type: string, enum: [parse, check, generate, dedupe] } QuotaEntryKind: { type: string, enum: [grant, hold, settle, release, expire, adjust, waive] } PlanCode: { type: string, enum: [free, pro, enterprise] } QualificationKind: { type: string, enum: [certificate, system_cert, software_copyright, other] } ProjectFieldKey: type: string enum: [name, code, purchaser, agency, procurement_method, legal_regime, budget_total, ceiling_total, eval_method, accept_consortium, accept_imported, sme_only, deadline_at] NextAction: type: string description: 工作台「下一步」,由服务端按项目状态算出 enum: [upload_tender, wait_parse, select_lots, confirm_checklist, upload_bid, run_check, handle_fatal, export_report, archive, none] Permission: type: string description: 权限点(AR12)。MVP 的所有者拥有全部;前端只用来决定按钮显不显示,服务端仍会检查 enum: [project.create, project.edit, project.delete, checklist.edit, checklist.confirm, check.run, finding.resolve, report.export, quota.view, billing.manage, qualification.edit, tenant.close] # ---------- auth / me RegisterRequest: type: object required: [phone, sms_code, tenant_name, display_name, terms_version, client_kind] properties: phone: { $ref: '#/components/schemas/Phone' } sms_code: { type: string, pattern: '^[0-9]{6}$' } tenant_name: { type: string, minLength: 2, maxLength: 100, description: 企业名称(MVP 不做认证,G9) } display_name: { type: string, minLength: 1, maxLength: 40 } terms_version: { type: string, description: 同意的服务协议版本(含围串标禁止条款) } client_kind: { type: string, enum: [web, desktop] } LoginRequest: type: object required: [phone, sms_code, client_kind] properties: phone: { $ref: '#/components/schemas/Phone' } sms_code: { type: string, pattern: '^[0-9]{6}$' } client_kind: { type: string, enum: [web, desktop] } force: type: boolean default: false description: 已有其他设备在线时,确认挤下线后重发 LoginResult: type: object required: [me] properties: me: { $ref: '#/components/schemas/Me' } token: type: string description: 只有 client_kind = desktop 时返回,且只返回这一次 expires_at: { $ref: '#/components/schemas/Timestamp' } SessionInfo: type: object required: [id, client_kind, created_at, last_seen_at, current] properties: id: { $ref: '#/components/schemas/Id' } client_kind: { type: string, enum: [web, desktop] } device: { type: string, description: 从 User-Agent 归纳,如「Chrome · macOS」 } ip_region: { type: string, description: 'IP 归属地,如「江苏南京」;不返回完整 IP' } created_at: { $ref: '#/components/schemas/Timestamp' } last_seen_at: { $ref: '#/components/schemas/Timestamp' } current: { type: boolean } Me: type: object required: [user, tenant, role, plan, permissions] properties: user: type: object required: [id, display_name, phone_hint] properties: id: { $ref: '#/components/schemas/Id' } display_name: { type: string } phone_hint: { type: string, examples: ['138****5678'] } tenant: type: object required: [id, name] properties: id: { $ref: '#/components/schemas/Id' } name: { type: string } role: { type: string, enum: [owner, admin, member] } plan: { $ref: '#/components/schemas/PlanSummary' } permissions: type: array items: { $ref: '#/components/schemas/Permission' } # ---------- billing PlanSummary: type: object required: [code, name, read_only] properties: code: { $ref: '#/components/schemas/PlanCode' } name: { type: string } starts_at: { $ref: '#/components/schemas/Timestamp' } ends_at: oneOf: - $ref: '#/components/schemas/Timestamp' - type: 'null' read_only: type: boolean description: 套餐到期后数据只读(FR-M6-05) Plan: type: object required: [code, version, name, quotas, limits] properties: code: { $ref: '#/components/schemas/PlanCode' } version: { type: integer } name: { type: string } quotas: type: object description: 每个周期的额度;专业版的检查从解析额度扣 additionalProperties: { type: integer } limits: type: object properties: max_file_bytes: { type: integer } max_pages: { type: integer } max_projects: { type: [integer, 'null'] } check_rule: type: string description: 给用户看的检查计费说明,如「检查从解析额度扣 1 次;同一项目 24 小时内复查不扣」 QuotaOverview: type: object required: [plan, accounts] properties: plan: { $ref: '#/components/schemas/PlanSummary' } accounts: type: array items: type: object required: [unit, balance, period_granted, period_used] properties: unit: { $ref: '#/components/schemas/QuotaUnit' } balance: { type: integer, description: 剩余,含正在预扣的 } held: { type: integer, description: 正在预扣、还没结算的 } period_granted: { type: integer } period_used: { type: integer } used_ratio: { type: number, description: '0~1;≥ 0.8 时界面变赭色(Q9)' } by_feature: type: array description: 按功能分布,如解析、检查 items: type: object required: [feature, used] properties: feature: { type: string, enum: [parse, check] } used: { type: integer } QuotaEntry: type: object required: [id, unit, kind, amount, created_at] properties: id: { $ref: '#/components/schemas/Id' } unit: { $ref: '#/components/schemas/QuotaUnit' } kind: { $ref: '#/components/schemas/QuotaEntryKind' } amount: { type: integer, description: 对余额的影响;结算、免扣为 0 } reason: { type: string, description: '调整原因;免扣时为规则说明,如「24 小时内复查」' } project: type: [object, 'null'] properties: id: { $ref: '#/components/schemas/Id' } name: { type: [string, 'null'], description: '项目已删除时为 null,界面显示「(已删除的项目)」' } deleted: { type: boolean } ref: type: [object, 'null'] properties: type: { type: string, enum: [parse_job, check_run] } id: { $ref: '#/components/schemas/Id' } label: { type: [string, 'null'], description: '如「招标文件解析」「第 3 次检查」' } created_at: { $ref: '#/components/schemas/Timestamp' } ActivationRequestCreate: type: object required: [plan_code, invoice_info] properties: plan_code: { type: string, enum: [pro] } invoice_info: { $ref: '#/components/schemas/InvoiceInfo' } InvoiceInfo: type: object required: [title, tax_no] properties: title: { type: string, description: 发票抬头 } tax_no: { type: string, description: 纳税人识别号 } address_phone: { type: string } bank_account: { type: string } invoice_type: { type: string, enum: [general, special] } contact_name: { type: string } contact_phone: { type: string } ActivationRequest: type: object required: [id, plan_code, status, created_at] properties: id: { $ref: '#/components/schemas/Id' } plan_code: { $ref: '#/components/schemas/PlanCode' } status: { type: string, enum: [submitted, paid, activated, rejected, cancelled] } invoice_info: { $ref: '#/components/schemas/InvoiceInfo' } payment_instructions: { type: string, description: 对公转账账户和备注要求 } ops_note: { type: string } created_at: { $ref: '#/components/schemas/Timestamp' } # ---------- projects ProjectReadiness: type: object description: 就绪状态只在 status = checked 时有意义(G7);未就绪也不阻止导出和递交(红线 4) required: [status, open_fatal_count, ready] properties: status: { $ref: '#/components/schemas/ProjectStatus' } open_fatal_count: { type: integer } ready: { type: [boolean, 'null'], description: '不是 checked 状态时为 null' } ProjectListItem: type: object required: [id, name, status, readiness, next_action, bidding_lots, updated_at] properties: id: { $ref: '#/components/schemas/Id' } name: { type: string } code: { type: [string, 'null'] } purchaser: { type: [string, 'null'] } deadline_at: oneOf: - $ref: '#/components/schemas/Timestamp' - type: 'null' status: { $ref: '#/components/schemas/ProjectStatus' } readiness: { $ref: '#/components/schemas/ProjectReadiness' } bidding_lots: type: array items: { type: string, description: 包号 } pending_item_count: { type: integer, description: 待确认区条目数 } active_job: type: [object, 'null'] description: 正在跑的解析或检查 properties: kind: { type: string, enum: [parse_job, check_run] } id: { $ref: '#/components/schemas/Id' } progress: { type: integer } label: { type: string } next_action: { $ref: '#/components/schemas/NextAction' } updated_at: { $ref: '#/components/schemas/Timestamp' } ProjectDetail: allOf: - $ref: '#/components/schemas/ProjectListItem' - type: object required: [legal_regime, fields, key_dates, created_by, created_at] properties: legal_regime: { $ref: '#/components/schemas/LegalRegime' } agency: { type: [string, 'null'] } procurement_method: { type: [string, 'null'] } fields: type: array description: 基本信息字段(FR-M2-03),每个都带出处 items: { $ref: '#/components/schemas/ProjectField' } key_dates: type: array description: 关键日期时间线(FR-M2-09),来自「日期与递交」类清单项 items: type: object required: [item_id, label, at] properties: item_id: { $ref: '#/components/schemas/Id' } label: { type: string, examples: [投标截止] } at: { $ref: '#/components/schemas/Timestamp' } superseded_pending: { type: boolean, description: 补遗带来了新值、等用户确认 } latest_check_run_id: oneOf: - $ref: '#/components/schemas/Id' - type: 'null' members: type: array items: type: object properties: user: { $ref: '#/components/schemas/UserRef' } role: { type: string, enum: [lead, editor, viewer] } created_by: { $ref: '#/components/schemas/UserRef' } created_at: { $ref: '#/components/schemas/Timestamp' } FieldValue: description: 按字段:文本、布尔、金额(Money 字符串)或时间 type: [string, boolean, 'null'] ProjectField: type: object required: [key, value, source] properties: key: { $ref: '#/components/schemas/ProjectFieldKey' } value: { $ref: '#/components/schemas/FieldValue' } source: { type: string, enum: [ai, human, none] } ai_value: { $ref: '#/components/schemas/FieldValue' } citation: oneOf: - $ref: '#/components/schemas/Citation' - type: 'null' updated_by: oneOf: - $ref: '#/components/schemas/UserRef' - type: 'null' pending: type: [object, 'null'] description: 补遗带来的新值(FR-M2-10) properties: value: { $ref: '#/components/schemas/FieldValue' } document_label: { type: string, examples: [补遗 1] } citation: { $ref: '#/components/schemas/Citation' } LotInput: type: object properties: lot_no: { type: string, maxLength: 40 } name: { type: string, maxLength: 200 } budget_amount: oneOf: - $ref: '#/components/schemas/Money' - type: 'null' ceiling_amount: oneOf: - $ref: '#/components/schemas/Money' - type: 'null' Lot: type: object required: [id, lot_no, is_default, is_bidding, origin, edited_fields] properties: id: { $ref: '#/components/schemas/Id' } lot_no: { type: string } name: { type: [string, 'null'] } budget_amount: oneOf: - $ref: '#/components/schemas/Money' - type: 'null' ceiling_amount: oneOf: - $ref: '#/components/schemas/Money' - type: 'null' is_default: { type: boolean, description: 没有分包时自动建的默认包,界面跳过选包 } is_bidding: { type: boolean } origin: { $ref: '#/components/schemas/Origin' } edited_fields: type: array items: { type: string } # ---------- uploads / documents Upload: type: object required: [id, part_size, part_count, received_parts, expires_at] properties: id: { $ref: '#/components/schemas/Id' } part_size: { type: integer, description: 字节,最后一片可以更小 } part_count: { type: integer } received_parts: type: array items: { type: integer } expires_at: { $ref: '#/components/schemas/Timestamp' } UploadCompleted: type: object required: [id, filename, size_bytes, detected_format, sha256] properties: id: { $ref: '#/components/schemas/Id' } filename: { type: string } size_bytes: { type: integer } sha256: { type: string } detected_format: { $ref: '#/components/schemas/DetectedFormat' } extension_mismatch: type: boolean description: 扩展名和真实格式不一致(07 F2),界面只做提示 page_count: type: [integer, 'null'] description: PDF 能直接读出;Word 要转换后才知道,这里为 null DocumentCreate: type: object required: [upload_id, role] properties: upload_id: { $ref: '#/components/schemas/Id' } role: { $ref: '#/components/schemas/DocumentRole' } volume_name: { type: string, maxLength: 40, description: '投标分册名:商务 / 技术 / 报价 / 资格证明' } replaces_document_id: $ref: '#/components/schemas/Id' description: 投标分册重新上传时填上一版的编号,生成新版本(FR-M3-01) Document: type: object required: [id, role, label, original_filename, detected_format, size_bytes, status, created_at] properties: id: { $ref: '#/components/schemas/Id' } role: { $ref: '#/components/schemas/DocumentRole' } label: { type: string, description: '如「招标文件」「补遗 1」「技术分册 v2」' } seq_no: { type: integer } volume_name: { type: [string, 'null'] } lineage_id: { $ref: '#/components/schemas/Id' } is_latest_version: { type: boolean } original_filename: { type: string } detected_format: { $ref: '#/components/schemas/DetectedFormat' } size_bytes: { type: integer } page_count: { type: [integer, 'null'] } normalized_sha256: { type: [string, 'null'] } status: { type: string, enum: [uploaded, normalizing, ready, failed] } latest_parse_job: oneOf: - $ref: '#/components/schemas/ParseJob' - type: 'null' uploaded_by: { $ref: '#/components/schemas/UserRef' } created_at: { $ref: '#/components/schemas/Timestamp' } DocumentPage: type: object required: [page_no, width_pt, height_pt, rotation, text_source] properties: page_no: { type: integer } width_pt: { type: number } height_pt: { type: number } rotation: { type: integer, enum: [0, 90, 180, 270] } text_source: { type: string, enum: [text, ocr, mixed, empty] } Quad: type: array description: 一个矩形 [x0, y0, x1, y1],单位 pt,原点在页面左上角 items: { type: number } minItems: 4 maxItems: 4 Sentence: type: object required: [key, text, quads, location] properties: key: { type: string, examples: [':p38:s4'] } text: { type: string } quads: type: array items: { $ref: '#/components/schemas/Quad' } location: { type: string, enum: [body, table, header_footer] } format_runs: type: array description: 文字格式片段 [起, 止, 格式],格式取 b / i / u 的组合 items: type: array prefixItems: - { type: integer } - { type: integer } - { type: string } section_path: { type: string, description: '所在章节,如「第三章 采购需求 > 3.2 技术参数」' } CitationCreate: type: object required: [document_id, sentence_keys] properties: document_id: { $ref: '#/components/schemas/Id' } sentence_keys: type: array minItems: 1 description: 同一页上连续的句子 items: { type: string } char_start: { type: integer, minimum: 0 } char_end: { type: integer, minimum: 1 } Citation: type: object description: 出处,不可变(data-model §5.1) required: [id, document_id, document_label, page_no, sentence_keys, quote, quads, normalized_sha256, created_by_kind] properties: id: { $ref: '#/components/schemas/Id' } document_id: { $ref: '#/components/schemas/Id' } document_label: { type: string, examples: [招标文件] } normalized_sha256: { type: string, description: 和文档当前哈希不一致时前端不画高亮 } page_no: { type: integer } sentence_keys: type: array items: { type: string } char_start: { type: [integer, 'null'] } char_end: { type: [integer, 'null'] } quote: { type: string, description: 原句 } quads: type: array items: { $ref: '#/components/schemas/Quad' } created_by_kind: { $ref: '#/components/schemas/Origin' } JobError: type: object description: 给用户看的失败原因;不含文档内容 required: [code, message, refunded] properties: code: { type: string, examples: [model_timeout, converter_crashed] } message: { type: string } refunded: { type: boolean, description: 额度是否已退回 } ParseJob: type: object required: [id, document_id, attempt_no, status, progress, created_at] properties: id: { $ref: '#/components/schemas/Id' } document_id: { $ref: '#/components/schemas/Id' } attempt_no: { type: integer } status: { $ref: '#/components/schemas/JobStatus' } stage: type: [string, 'null'] enum: [detect, convert, text_layer, ocr, structure, extract, verify, merge, null] progress: { type: integer, minimum: 0, maximum: 100 } cancel_requested: { type: boolean } error: oneOf: - $ref: '#/components/schemas/JobError' - type: 'null' result: type: [object, 'null'] description: 成功后的摘要 properties: items_added: { type: integer } items_updated: { type: integer } items_pending: { type: integer } items_absent: { type: integer, description: 重新解析时没再出现的旧条目 } lots_found: { type: integer } created_at: { $ref: '#/components/schemas/Timestamp' } finished_at: oneOf: - $ref: '#/components/schemas/Timestamp' - type: 'null' # ---------- checklist MarkingRule: type: object required: [id, marker, meaning, citation] properties: id: { $ref: '#/components/schemas/Id' } marker: { type: string, enum: [star, asterisk, triangle, italic_underline, bold, underline, keyword] } marker_text: { type: [string, 'null'] } meaning: { type: string, enum: [substantive, important] } declared_count: { type: [integer, 'null'] } found_count: { type: integer, description: 实际抽到的条数 } citation: { $ref: '#/components/schemas/Citation' } Checklist: type: object required: [status, version_no, counts, marking_rules] properties: status: { type: string, enum: [draft, confirmed] } version_no: { type: integer, description: 最近一次确认的版本号;没确认过为 0 } confirmed_by: oneOf: - $ref: '#/components/schemas/UserRef' - type: 'null' confirmed_at: oneOf: - $ref: '#/components/schemas/Timestamp' - type: 'null' counts: type: object required: [formal, pending, deleted] properties: formal: { type: integer } pending: { type: integer } deleted: { type: integer } superseded_pending: { type: integer } by_category: type: object additionalProperties: { type: integer } marking_rules: type: array items: { $ref: '#/components/schemas/MarkingRule' } warnings: type: array description: '如「文件写明 ★ 项共 14 条,实际抽到 13 条」' items: type: object required: [code, message] properties: code: { type: string, enum: [declared_count_mismatch, marker_rule_not_found, legal_regime_unknown] } message: { type: string } ItemSpec: type: object description: 类别特有字段(05 §3.3),按类别分组;一条清单项有几个类别就可以有几组 properties: qualification: type: object properties: kind: { type: string, enum: [general, specific, credit] } validity_requirement: { type: string, examples: [近 3 个月社保缴纳证明] } invalidity: type: object properties: source: { type: string, enum: [statute, tender_text, substantive_marker] } scoring: type: object properties: factor: { type: string } points: { type: string, description: '分值,可能是「0~5」这样的区间,所以用字符串' } criteria: { type: string } part: { type: string, enum: [price, technical, commercial] } schedule: type: object properties: date_kind: { type: string, enum: [question_deadline, bid_deadline, opening, deposit_deadline, other] } at: { $ref: '#/components/schemas/Timestamp' } deposit_amount: { $ref: '#/components/schemas/Money' } submission_mode: { type: string, enum: [electronic, paper, ca_encrypted, mixed] } copies: { type: string } tech_param: type: object properties: param: { type: string } op: { type: string, enum: ['>=', '<=', '=', contains, supports] } value: { type: string } unit: { type: string } device: { type: string } format_doc: type: object properties: doc_name: { type: string, examples: [投标函] } modifiable: { type: boolean } price: type: object description: 分项限价、单价上限(DM7);只存招标文件写明的限价,系统不给任何报价建议(红线 6) properties: kind: { type: string, enum: [item_ceiling, unit_price_ceiling] } target: { type: string } op: { type: string, enum: ['<='] } value: { $ref: '#/components/schemas/Money' } ChecklistItemFields: type: object properties: categories: type: array minItems: 1 uniqueItems: true items: { $ref: '#/components/schemas/Category' } title: { type: string, minLength: 1, maxLength: 200 } requirement_text: { type: [string, 'null'] } importance: { $ref: '#/components/schemas/Importance' } lot_id: oneOf: - $ref: '#/components/schemas/Id' - type: 'null' evidence_required: { type: [string, 'null'] } spec: { $ref: '#/components/schemas/ItemSpec' } ChecklistItemCreate: allOf: - $ref: '#/components/schemas/ChecklistItemFields' - type: object required: [categories, title] properties: categories: type: array minItems: 1 uniqueItems: true items: { $ref: '#/components/schemas/Category' } title: { type: string, minLength: 1, maxLength: 200 } citation_ids: type: array items: { $ref: '#/components/schemas/Id' } ChecklistItemUpdate: $ref: '#/components/schemas/ChecklistItemFields' ChecklistItem: allOf: - $ref: '#/components/schemas/ChecklistItemFields' - type: object required: [id, categories, title, importance, origin, zone, citations, edited_fields, updated_at] properties: categories: type: array minItems: 1 uniqueItems: true items: { $ref: '#/components/schemas/Category' } title: { type: string, minLength: 1, maxLength: 200 } importance: { $ref: '#/components/schemas/Importance' } id: { $ref: '#/components/schemas/Id' } summary: type: [string, 'null'] description: AI 摘要,界面标注 AI 生成 importance_basis: { type: [string, 'null'], enum: [marker, statute, tender_text, human, null] } marking_rule_id: oneOf: - $ref: '#/components/schemas/Id' - type: 'null' lot_no: { type: [string, 'null'], description: 空表示各包通用 } origin: { $ref: '#/components/schemas/Origin' } origin_label: { type: string, description: '如「AI」「人工添加」「补遗 1」' } zone: { $ref: '#/components/schemas/Zone' } pending_reason: oneOf: - $ref: '#/components/schemas/PendingReason' - type: 'null' no_citation_ack: { type: boolean, description: 人工确认、无出处 } citations: type: array items: { $ref: '#/components/schemas/Citation' } ai_values: type: [object, 'null'] description: AI 原值;和当前值不同的字段界面可以显示「AI 原为……」 edited_fields: type: array items: { type: string } confidence: { type: [number, 'null'] } superseded_by: type: [object, 'null'] description: 已被补遗更新,等用户确认 properties: item_id: { $ref: '#/components/schemas/Id' } document_label: { type: string } confirmed: { type: boolean } accepted_by: oneOf: - $ref: '#/components/schemas/UserRef' - type: 'null' deleted_at: oneOf: - $ref: '#/components/schemas/Timestamp' - type: 'null' updated_at: { $ref: '#/components/schemas/Timestamp' } ChecklistVersion: type: object required: [id, version_no, kind, item_count, created_by, created_at] properties: id: { $ref: '#/components/schemas/Id' } version_no: { type: integer } kind: { type: string, enum: [confirmed, check] } item_count: { type: integer } created_by: { $ref: '#/components/schemas/UserRef' } created_at: { $ref: '#/components/schemas/Timestamp' } HistoryEntry: type: object required: [action, actor, at] properties: action: { type: string, examples: [checklist_item.update, checklist_item.accept, checklist_item.delete] } actor: { $ref: '#/components/schemas/UserRef' } at: { $ref: '#/components/schemas/Timestamp' } changes: type: array items: type: object required: [field] properties: field: { type: string } before: {} after: {} # ---------- checks CheckPreview: type: object required: [can_start, billing, charge, checklist_confirmed, bid_documents, lot_ids] properties: can_start: { type: boolean } blockers: type: array description: can_start 为 false 的原因 items: type: string enum: [no_parsed_tender, no_bid_documents, job_already_running, quota_exhausted, project_read_only] billing: { $ref: '#/components/schemas/BillingMode' } charge: { type: integer, enum: [0, 1], description: 这次检查扣几次 } charge_unit: { $ref: '#/components/schemas/QuotaUnit' } waive_until: oneOf: - $ref: '#/components/schemas/Timestamp' - type: 'null' description: 24 小时免扣窗口的结束时间(从上一次扣点的检查算起,P1) checklist_confirmed: { type: boolean, description: 为 false 时提示「清单未确认,检查结果以当前清单为准」 } pending_item_count: { type: integer, description: 待确认区条目不参与检查 } bid_documents: type: array description: 默认用每个分册的最新版本 items: { $ref: '#/components/schemas/Document' } lot_ids: type: array items: { $ref: '#/components/schemas/Id' } CheckRunCreate: type: object required: [document_ids, expected_charge] properties: document_ids: type: array minItems: 1 description: 本次检查的投标文件版本 items: { $ref: '#/components/schemas/Id' } expected_charge: type: integer enum: [0, 1] description: 前端按 check-preview 提示用户的扣点数;和服务端当下算的不一致时返回 `billing_changed`,不扣点(API4) CheckRun: type: object required: [id, run_no, status, progress, billing, checklist_version_no, checklist_confirmed, rule_set_version, created_by, created_at] properties: id: { $ref: '#/components/schemas/Id' } run_no: { type: integer } status: { $ref: '#/components/schemas/JobStatus' } stage: type: [string, 'null'] enum: [prepare_bid, match, rules, verify, carry_over, summarize, null] progress: { type: integer, minimum: 0, maximum: 100 } cancel_requested: { type: boolean } error: oneOf: - $ref: '#/components/schemas/JobError' - type: 'null' billing: { $ref: '#/components/schemas/BillingMode' } is_latest: { type: boolean, description: 只有最近一次检查的问题可以处理 } checklist_version_no: { type: integer } checklist_confirmed: { type: boolean } rule_set_version: { type: string } legal_regime: { $ref: '#/components/schemas/LegalRegime' } documents: type: array items: { $ref: '#/components/schemas/Document' } coverage: type: [object, 'null'] description: 查了什么、没查什么(报告首页,FR-M3-09) properties: checked: type: array items: { type: string } not_checked: type: array items: { type: string } counts: type: [object, 'null'] properties: fatal: { type: integer } important: { type: integer } hint: { type: integer } open_fatal: { type: integer } resolved_this_run: { type: integer } new: { type: integer } created_by: { $ref: '#/components/schemas/UserRef' } created_at: { $ref: '#/components/schemas/Timestamp' } finished_at: oneOf: - $ref: '#/components/schemas/Timestamp' - type: 'null' Finding: type: object required: [id, type, severity, title, explanation, detector, presence, status, tender_citations, bid_citations] properties: id: { $ref: '#/components/schemas/Id' } type: { $ref: '#/components/schemas/FindingType' } severity: { $ref: '#/components/schemas/Severity' } title: { type: string } explanation: { type: string } detector: { type: string, enum: [ai, rule] } checklist_item: type: [object, 'null'] description: AI 判断的问题必有;规则问题没有对应条目时为 null,显示 rule(DM6) properties: id: { $ref: '#/components/schemas/Id' } title: { type: string } importance: { $ref: '#/components/schemas/Importance' } categories: type: array items: { $ref: '#/components/schemas/Category' } rule: type: [object, 'null'] properties: code: { type: string, examples: [PRICE.CAPITAL_MISMATCH] } description: { type: string } detail: type: object description: 按类型的补充信息:参数的要求值和响应值、资质预警种类(expired / insufficient_level / not_covered)、从投标文件读到的金额 tender_citations: type: array items: { $ref: '#/components/schemas/Citation' } bid_citations: type: array items: { $ref: '#/components/schemas/Citation' } search_scope: type: [object, 'null'] description: 「未找到响应」时找过哪些分册、哪些章节(FR-M3-03) properties: documents: type: array items: { type: string } sections: type: array items: { type: string } presence: { $ref: '#/components/schemas/Presence' } carried_status: oneOf: - $ref: '#/components/schemas/FindingStatus' - type: 'null' description: 沿用时,上一次的处理状态(上次「已修改」的这次回到待处理) status: { $ref: '#/components/schemas/FindingStatus' } resolution_note: { type: [string, 'null'] } resolved_by: oneOf: - $ref: '#/components/schemas/UserRef' - type: 'null' resolved_at: oneOf: - $ref: '#/components/schemas/Timestamp' - type: 'null' ManualCheck: type: object required: [check_key, label, checked] properties: check_key: { type: string } label: { type: string, examples: [正副本份数与装订] } requirement_text: { type: [string, 'null'] } citation: oneOf: - $ref: '#/components/schemas/Citation' - type: 'null' checked: { type: boolean } note: { type: [string, 'null'] } checked_by: oneOf: - $ref: '#/components/schemas/UserRef' - type: 'null' checked_at: oneOf: - $ref: '#/components/schemas/Timestamp' - type: 'null' # ---------- reports ReportJob: type: object required: [id, check_run_id, status] properties: id: { $ref: '#/components/schemas/Id' } check_run_id: { $ref: '#/components/schemas/Id' } status: { $ref: '#/components/schemas/JobStatus' } report_id: oneOf: - $ref: '#/components/schemas/Id' - type: 'null' error: oneOf: - $ref: '#/components/schemas/JobError' - type: 'null' Report: type: object required: [id, check_run_id, run_no, page_count, template_version, generated_by, created_at] properties: id: { $ref: '#/components/schemas/Id' } check_run_id: { $ref: '#/components/schemas/Id' } run_no: { type: integer } page_count: { type: integer } sha256: { type: string } template_version: { type: string } open_fatal_at_export: { type: integer, description: 导出时还没处理的致命问题数 } generated_by: { $ref: '#/components/schemas/UserRef' } created_at: { $ref: '#/components/schemas/Timestamp' } # ---------- qualifications QualificationInput: type: object required: [name, kind] properties: name: { type: string, minLength: 1, maxLength: 200 } kind: { $ref: '#/components/schemas/QualificationKind' } level: { type: [string, 'null'] } cert_no: { type: [string, 'null'] } issuer: { type: [string, 'null'] } valid_from: { type: [string, 'null'], format: date } valid_to: { type: [string, 'null'], format: date, description: 空表示长期有效 } Qualification: allOf: - $ref: '#/components/schemas/QualificationInput' - type: object required: [id, files, created_at, updated_at] properties: id: { $ref: '#/components/schemas/Id' } files: type: array items: type: object required: [id, filename, size_bytes] properties: id: { $ref: '#/components/schemas/Id' } filename: { type: string } size_bytes: { type: integer } created_at: { $ref: '#/components/schemas/Timestamp' } updated_at: { $ref: '#/components/schemas/Timestamp' } # ---------- notifications / events / feedback Notification: type: object required: [id, kind, title, created_at] properties: id: { $ref: '#/components/schemas/Id' } kind: type: string enum: [parse_succeeded, parse_failed, check_succeeded, check_failed, quota_80, quota_100, subscription_activated, subscription_expiring] title: { type: string } project_id: oneOf: - $ref: '#/components/schemas/Id' - type: 'null' link: { type: [string, 'null'], description: '前端路由,如 /projects//results' } read_at: oneOf: - $ref: '#/components/schemas/Timestamp' - type: 'null' created_at: { $ref: '#/components/schemas/Timestamp' } Event: type: object required: [type, at] properties: type: type: string enum: [job.progress, job.finished, report.ready, notification.created, quota.changed, project.changed, resync] at: { $ref: '#/components/schemas/Timestamp' } project_id: oneOf: - $ref: '#/components/schemas/Id' - type: 'null' job: type: [object, 'null'] properties: kind: { type: string, enum: [parse_job, check_run, report_job] } id: { $ref: '#/components/schemas/Id' } status: { $ref: '#/components/schemas/JobStatus' } stage: { type: [string, 'null'] } progress: { type: integer } notification_id: oneOf: - $ref: '#/components/schemas/Id' - type: 'null' FeedbackCreate: type: object required: [project_id, object_type, kind] properties: project_id: { $ref: '#/components/schemas/Id' } object_type: { type: string, enum: [finding, checklist_item, checklist] } object_id: { $ref: '#/components/schemas/Id' } kind: { type: string, enum: [false_positive, false_negative] } note: { type: string, maxLength: 1000 }