openapi: 3.1.0 info: title: 唐侦探独立章节存档 version: 1.0.0 description: >- 使用原小程序 token,仅操作登录用户自己的 season-01 ID 存档。 不接收健康回答、正文、快照或客户端用户身份。业务错误沿用 HTTP 200/code 约定。 servers: - url: /api security: - MiniProgramToken: [] paths: /tang/catalog: get: operationId: getTangDetectiveCatalog summary: 读取部署版本的 ID 白名单 responses: '200': description: code=1 为目录,code=0/-1 为错误 content: application/json: schema: oneOf: - type: object required: [code, show, msg, data] properties: code: {const: 1} show: {const: 0} msg: {type: string} data: {$ref: '#/components/schemas/Catalog'} - {$ref: '#/components/schemas/Failure'} /tang/progress: get: operationId: getTangDetectiveProgress summary: 只读当前登录用户存档,无存档返回 revision=0 的默认值 responses: '200': $ref: '#/components/responses/ProgressResponse' /tang/saveProgress: post: operationId: saveTangDetectiveProgress summary: 版本匹配时替换整份 ID 投影或重新开始故事 description: >- 原始请求体最多 32768 字节。最后一次 request_id 的相同规范化内容可幂等重试。 必须匹配 base_revision 和 story_generation;冲突不覆盖、不自动合并。 reset_story 必须提交空故事字段,收藏为当前服务器与该请求的已验证卡 ID 并集。 requestBody: required: true content: application/json: schema: {$ref: '#/components/schemas/SaveRequest'} responses: '200': $ref: '#/components/responses/ProgressResponse' components: securitySchemes: MiniProgramToken: type: apiKey in: header name: token description: 沿用现有 LoginMiddleware;不是 Authorization,也不放入请求正文。 responses: ProgressResponse: description: >- 必须检查 code;code=1 的 data 是本人确认存档。 此控制器的响应含 Cache-Control no-store。 headers: Cache-Control: schema: {type: string, const: no-store} content: application/json: schema: oneOf: - {$ref: '#/components/schemas/ProgressSuccess'} - {$ref: '#/components/schemas/Failure'} schemas: ChapterId: type: string pattern: '^S01-C(0[1-9]|1[0-5])$' EventId: type: string pattern: '^S01-H(0[1-9]|[1-5][0-9]|60)$' PageId: type: string pattern: '^S01-C(0[1-9]|1[0-5])-P0[1-8]$' CardId: type: string pattern: '^S01-C(0[1-9]|1[0-5])-MC01$' Catalog: type: object additionalProperties: false required: [game_id, schema_version, content_version, max_body_bytes, chapters] properties: game_id: {const: tang-detective} schema_version: {const: 1} content_version: {const: season-01} max_body_bytes: {const: 32768} chapters: type: array minItems: 15 maxItems: 15 items: type: object additionalProperties: false required: [chapter_id, chapter_number, hotspot_ids, page_ids, memory_card_id] properties: chapter_id: {$ref: '#/components/schemas/ChapterId'} chapter_number: {type: integer, minimum: 1, maximum: 15} hotspot_ids: type: array minItems: 4 maxItems: 4 uniqueItems: true items: {$ref: '#/components/schemas/EventId'} page_ids: type: array minItems: 8 maxItems: 8 uniqueItems: true items: {$ref: '#/components/schemas/PageId'} memory_card_id: {$ref: '#/components/schemas/CardId'} Reader: type: object additionalProperties: false required: [currentPageId, completedEventIds, chapterFinished] properties: currentPageId: {$ref: '#/components/schemas/PageId'} completedEventIds: type: array maxItems: 4 uniqueItems: true items: {$ref: '#/components/schemas/EventId'} chapterFinished: {type: boolean} description: >- 事件必须是所属章节的连续前缀且与 completedHotspots 相同; chapterFinished 必须与 completedChapters 成员关系一致,为 true 时须有四事件。 未 finished 仅允许 P01 至 P(03+事件数),最多 P07;P08 必须 finished。 Progress: type: object additionalProperties: false required: [completedHotspots, completedChapters, lastChapter, collectedMemoryCards, comicReaderByChapter, lastPageId] properties: completedHotspots: type: object maxProperties: 15 propertyNames: {$ref: '#/components/schemas/ChapterId'} additionalProperties: type: array maxItems: 4 uniqueItems: true items: {$ref: '#/components/schemas/EventId'} completedChapters: type: array maxItems: 15 uniqueItems: true items: {$ref: '#/components/schemas/ChapterId'} lastChapter: {type: integer, minimum: 1, maximum: 15} collectedMemoryCards: type: array maxItems: 15 uniqueItems: true items: {$ref: '#/components/schemas/CardId'} comicReaderByChapter: type: object maxProperties: 15 propertyNames: {$ref: '#/components/schemas/ChapterId'} additionalProperties: {$ref: '#/components/schemas/Reader'} lastPageId: oneOf: - {const: ''} - {$ref: '#/components/schemas/PageId'} description: >- 空映射必须是 {},不是 []。有已完成事件或 finished 的章节必须有 reader。 非空 lastPageId 必须等于 lastChapter 对应 reader.currentPageId。 跨字段成员关系、顺序和解锁约束由 TangDetectiveProgress 验证。 SaveRequest: type: object additionalProperties: false required: [schema_version, content_version, base_revision, story_generation, request_id, operation, progress] properties: schema_version: {type: integer, const: 1} content_version: {const: season-01} base_revision: {type: integer, minimum: 0, maximum: 2147483646} story_generation: {type: integer, minimum: 0, maximum: 2147483646} request_id: {type: string, pattern: '^[A-Za-z0-9_-]{16,64}$'} operation: {enum: [replace, reset_story]} progress: {$ref: '#/components/schemas/Progress'} allOf: - if: properties: operation: {const: reset_story} then: properties: progress: properties: completedHotspots: {maxProperties: 0} completedChapters: {maxItems: 0} lastChapter: {const: 1} comicReaderByChapter: {maxProperties: 0} lastPageId: {const: ''} State: type: object additionalProperties: false required: [user_id, schema_version, content_version, revision, story_generation, progress] properties: user_id: {type: integer, minimum: 1} schema_version: {const: 1} content_version: {const: season-01} revision: {type: integer, minimum: 0} story_generation: {type: integer, minimum: 0} progress: {$ref: '#/components/schemas/Progress'} idempotent: type: boolean description: 仅保存响应提供;true 表示重复确认最后一次提交,未重复写入。 ProgressSuccess: type: object required: [code, show, msg, data] properties: code: {const: 1} show: {const: 0} msg: {type: string} data: {$ref: '#/components/schemas/State'} Failure: type: object required: [code, show, msg, data] properties: code: {type: integer, enum: [0, -1]} show: {type: integer, enum: [0, 1]} msg: {type: string} data: oneOf: - type: object additionalProperties: false required: [error_code] properties: error_code: enum: [INVALID_REQUEST, PAYLOAD_TOO_LARGE, UNSUPPORTED_CONTENT_VERSION, PROGRESS_CONFLICT, IDEMPOTENCY_CONFLICT, METHOD_NOT_ALLOWED, UNSUPPORTED_MEDIA_TYPE, AUTH_REQUIRED, STORAGE_UNAVAILABLE] - type: array maxItems: 0 description: 原登录中间件可能提前返回空 data;登录过期 code=-1。