diff --git a/packages/api/generated/openapi.json b/packages/api/generated/openapi.json index 41f46cc..74f0fcb 100644 --- a/packages/api/generated/openapi.json +++ b/packages/api/generated/openapi.json @@ -602,7 +602,7 @@ "get": { "operationId": "getBoard", "summary": "批量获取指定版面", - "description": "根据一个或多个版面 ID 批量返回版面信息,常用于补齐收藏和主题记录中的版面资料。", + "description": "批量查询一个或多个版面,返回值字段与 /board/{boardId} 相同。适合已有多个版面 ID 时一次补齐收藏、主题记录等列表中的版面资料,避免逐个请求。不存在的 ID 会被忽略,全部 ID 均不存在时返回 404;调用方应按 id 关联结果,不应依赖响应顺序。", "tags": ["Board"], "security": [], "x-cc98-risk": "read-only", @@ -617,15 +617,15 @@ "items": { "type": "number" }, - "description": "要查询的版面 ID,可重复传入多个 id 参数。" + "description": "要查询的版面 ID,可重复传入多个 id 参数;重复的 ID 只返回一次。" }, "required": true, - "description": "要查询的版面 ID,可重复传入多个 id 参数。" + "description": "要查询的版面 ID,可重复传入多个 id 参数;重复的 ID 只返回一次。" } ], "responses": { "200": { - "description": "成功获取指定版面列表", + "description": "成功获取至少一个匹配的版面详情列表", "content": { "application/json": { "schema": { @@ -680,7 +680,7 @@ "schema": { "type": "array", "items": { - "$ref": "#/components/schemas/Board" + "$ref": "#/components/schemas/BoardSummary" } } } @@ -703,7 +703,7 @@ "get": { "operationId": "getBoardBoardId", "summary": "获取版面详情", - "description": "返回版面公告、简介、统计、访问限制和当前访问者能力。登录后还会包含是否已关注等用户相关状态。", + "description": "查询单个版面并直接返回一个 Board 对象;需要同时查询多个版面时应使用 /board/ 批量接口,两者返回相同的版面字段。本接口包含版面公告、简介、统计、访问限制和当前访问者能力,登录后还会包含是否已关注等用户相关状态。", "tags": ["Board"], "security": [], "x-cc98-risk": "read-only", @@ -8643,13 +8643,70 @@ "description": "分组内的版面概要列表。", "type": "array", "items": { - "$ref": "#/components/schemas/Board" + "$ref": "#/components/schemas/BoardSummary" } } }, "additionalProperties": {}, "description": "论坛版面导航中的一级分组。" }, + "BoardSummary": { + "type": "object", + "properties": { + "id": { + "description": "版面 ID。", + "type": "number" + }, + "name": { + "description": "版面名称。", + "type": "string" + }, + "description": { + "description": "版面简介;未配置时可能为 null。", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "topicCount": { + "description": "版面主题总数。", + "type": "number" + }, + "postCount": { + "description": "版面回复总数。", + "type": "number" + }, + "todayCount": { + "description": "版面今日回复数。", + "type": "number" + }, + "boardMasters": { + "description": "版主用户名列表。", + "type": "array", + "items": { + "type": "string" + } + }, + "anonymousState": { + "description": "匿名发帖模式:0 为不可匿名,1 为只能匿名,2、3 均允许在发主题时选择是否匿名;2 与 3 在回复场景中的具体差异尚未完全确认。", + "type": "number" + }, + "showShareTip": { + "description": "是否展示版面分享提示,具体触发场景尚未确认。", + "type": "boolean" + }, + "canVote": { + "description": "当前访问者是否可以在版面发布投票主题。", + "type": "boolean" + } + }, + "additionalProperties": {}, + "description": "版面导航中的版面概要,仅包含 /board/all 实际返回的字段。" + }, "Board": { "type": "object", "properties": { diff --git a/packages/api/src/operations/board.ts b/packages/api/src/operations/board.ts index 23428cd..561ea53 100644 --- a/packages/api/src/operations/board.ts +++ b/packages/api/src/operations/board.ts @@ -5,6 +5,7 @@ import { boardGroupSchema, boardMutedUserSchema, boardSchema, + boardSummarySchema, boardTagDataSchema, createTopicRequestSchema, errorCodeSchema, @@ -51,13 +52,13 @@ export const boardOperations = defineOperations([ in: "query", required: true, schema: z.array(z.number()), - description: "要查询的版面 ID,可重复传入多个 id 参数。", + description: "要查询的版面 ID,可重复传入多个 id 参数;重复的 ID 只返回一次。", probeValue: 758, }, ], responses: { "200": { - description: "成功获取指定版面列表", + description: "成功获取至少一个匹配的版面详情列表", contentType: "application/json", schema: z.array(boardSchema), }, @@ -71,7 +72,8 @@ export const boardOperations = defineOperations([ risk: "read-only", verificationStatus: "verified-anonymous", sources: ["legacy-openapi", "live-probe"], - description: "根据一个或多个版面 ID 批量返回版面信息,常用于补齐收藏和主题记录中的版面资料。", + description: + "批量查询一个或多个版面,返回值字段与 /board/{boardId} 相同。适合已有多个版面 ID 时一次补齐收藏、主题记录等列表中的版面资料,避免逐个请求。不存在的 ID 会被忽略,全部 ID 均不存在时返回 404;调用方应按 id 关联结果,不应依赖响应顺序。", }, { method: "GET", @@ -93,7 +95,7 @@ export const boardOperations = defineOperations([ "200": { description: "成功获取名称或简介匹配关键词的版面", contentType: "application/json", - schema: z.array(boardSchema), + schema: z.array(boardSummarySchema), }, default: { description: "API 错误码", @@ -140,7 +142,7 @@ export const boardOperations = defineOperations([ verificationStatus: "verified-anonymous", sources: ["legacy-openapi", "live-probe"], description: - "返回版面公告、简介、统计、访问限制和当前访问者能力。登录后还会包含是否已关注等用户相关状态。", + "查询单个版面并直接返回一个 Board 对象;需要同时查询多个版面时应使用 /board/ 批量接口,两者返回相同的版面字段。本接口包含版面公告、简介、统计、访问限制和当前访问者能力,登录后还会包含是否已关注等用户相关状态。", }, { method: "GET", diff --git a/packages/api/src/schemas/board.ts b/packages/api/src/schemas/board.ts index a380a5e..567c9bb 100644 --- a/packages/api/src/schemas/board.ts +++ b/packages/api/src/schemas/board.ts @@ -85,6 +85,25 @@ export const boardSchema = z }); export type Board = z.infer; +export const boardSummarySchema = boardSchema + .pick({ + id: true, + name: true, + description: true, + topicCount: true, + postCount: true, + todayCount: true, + boardMasters: true, + anonymousState: true, + showShareTip: true, + canVote: true, + }) + .meta({ + id: "BoardSummary", + description: "版面导航中的版面概要,仅包含 /board/all 实际返回的字段。", + }); +export type BoardSummary = z.infer; + export const tagSchema = z .looseObject({ id: z.number().optional().meta({ description: "全局标签 ID。" }), @@ -132,7 +151,7 @@ export const boardGroupSchema = z name: z.string().optional().meta({ description: "版面分组名称。" }), order: z.number().optional().meta({ description: "版面分组的展示顺序。" }), masters: z.array(z.string()).optional().meta({ description: "分区主管用户名列表。" }), - boards: z.array(boardSchema).optional().meta({ description: "分组内的版面概要列表。" }), + boards: z.array(boardSummarySchema).optional().meta({ description: "分组内的版面概要列表。" }), }) .meta({ id: "BoardGroup", description: "论坛版面导航中的一级分组。" }); export type BoardGroup = z.infer; diff --git a/packages/api/tests/contract.test.ts b/packages/api/tests/contract.test.ts index dc55600..1b6797f 100644 --- a/packages/api/tests/contract.test.ts +++ b/packages/api/tests/contract.test.ts @@ -8,6 +8,7 @@ import openIdOpenapi from "../generated/openid.openapi.json" with { type: "json" import { endpointCatalog, boardEventPageSchema, + boardSummarySchema, boardSchema, createPostRequestSchema, favoriteTopicGroupSchema, @@ -108,6 +109,37 @@ describe("API 契约基线", () => { }); }); + it("版面概要不混入详情字段", async () => { + const boardAllFixture = JSON.parse( + await readFile( + resolve(import.meta.dirname, "../fixtures/anonymous/getBoardAll.json"), + "utf8", + ), + ); + const boardSearchFixture = JSON.parse( + await readFile( + resolve(import.meta.dirname, "../fixtures/anonymous/getBoardSearch.json"), + "utf8", + ), + ); + const summary = boardSummarySchema.parse(boardAllFixture[0].boards[0]); + const summaryProperties = openapi.components.schemas.BoardSummary.properties; + + expect(summary.showShareTip).toBeTypeOf("boolean"); + expect(boardSummarySchema.array().safeParse(boardSearchFixture).success).toBe(true); + expect(summaryProperties).toHaveProperty("showShareTip"); + expect(summaryProperties).not.toHaveProperty("logoUri"); + expect(openapi.components.schemas.BoardGroup.properties.boards.items).toEqual({ + $ref: "#/components/schemas/BoardSummary", + }); + expect( + openapi.paths["/board/search"].get.responses["200"].content["application/json"].schema, + ).toEqual({ + type: "array", + items: { $ref: "#/components/schemas/BoardSummary" }, + }); + }); + it("版务记录分页契约保留总数与事件列表", async () => { const fixture = JSON.parse( await readFile(