openapi: 3.1.0 info: title: Mach-CMS API description: | Mach-CMS 后端 API 规范。前后端契约文件,任何接口变更必须同步修改此文件。 - 所有业务接口返回统一包装体 `Response` - 列表查询返回 `PageResult` - 认证方式:JWT Bearer Token - 时间格式:ISO-8601(`2024-01-15T08:30:00Z`) version: 0.3.1 contact: name: Mach-CMS Team servers: - url: http://localhost:8080 description: 本地开发环境 security: - bearerAuth: [] tags: - name: Auth description: 认证与授权 - name: Public description: 公开接口(无需认证) - name: Article description: 文章(前台只读) - name: ArticleAdmin description: 文章管理(需 ADMIN/EDITOR) - name: Tag description: 标签(前台只读) - name: TagAdmin description: 标签管理(需 ADMIN) - name: Media description: 媒体文件上传 - name: Search description: 全文搜索 - name: Comment description: 评论(前台免登录提交,展示已审核) - name: CommentAdmin description: 评论管理(需 ADMIN) - name: User description: 用户管理(需 ADMIN) paths: # ==================== Public ==================== /api/public/health: get: tags: [Public] summary: 健康检查 security: [] responses: '200': description: 服务正常 content: application/json: schema: $ref: '#/components/schemas/ResponseString' example: code: 0 message: success data: UP # ==================== Auth ==================== /api/auth/register: post: tags: [Auth] summary: 用户注册 security: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/RegisterRequest' responses: '200': description: 注册成功 content: application/json: schema: $ref: '#/components/schemas/ResponseTokenPair' /api/auth/login: post: tags: [Auth] summary: 用户登录 security: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/LoginRequest' responses: '200': description: 登录成功,返回双 Token content: application/json: schema: $ref: '#/components/schemas/ResponseTokenPair' /api/auth/refresh: post: tags: [Auth] summary: 刷新 Access Token security: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/RefreshTokenRequest' responses: '200': description: 刷新成功 content: application/json: schema: $ref: '#/components/schemas/ResponseTokenPair' /api/auth/logout: post: tags: [Auth] summary: 退出登录(吊销 Refresh Token) responses: '200': description: 退出成功 content: application/json: schema: $ref: '#/components/schemas/ResponseVoid' # ==================== Article (Public) ==================== /api/articles: get: tags: [Article] summary: 文章列表(分页) security: [] parameters: - name: page in: query schema: type: integer default: 1 - name: size in: query schema: type: integer default: 10 maximum: 100 - name: status in: query schema: $ref: '#/components/schemas/ArticleStatus' default: PUBLISHED - name: tag in: query description: 标签 slug 过滤 schema: type: string responses: '200': description: 文章列表 content: application/json: schema: $ref: '#/components/schemas/ResponsePageArticleListItem' /api/articles/popular: get: tags: [Article] summary: 热门文章(按浏览量降序,仅 PUBLISHED) security: [] parameters: - name: limit in: query schema: type: integer default: 5 maximum: 10 minimum: 1 responses: '200': description: 热门文章列表 content: application/json: schema: $ref: '#/components/schemas/ResponseListArticleListItem' /api/articles/{slug}: get: tags: [Article] summary: 文章详情(按 slug) security: [] parameters: - name: slug in: path required: true schema: type: string responses: '200': description: 文章详情 content: application/json: schema: $ref: '#/components/schemas/ResponseArticleDetail' '404': description: 文章不存在 content: application/json: schema: $ref: '#/components/schemas/ResponseVoid' /api/articles/{slug}/meta: get: tags: [Article] summary: 文章 OpenGraph 元数据(分享预览) security: [] parameters: - name: slug in: path required: true schema: type: string responses: '200': description: 元数据(文章不存在时为 null) content: application/json: schema: $ref: '#/components/schemas/ResponseOpenGraphMeta' /api/articles/{slug}/star: post: tags: [Article] summary: 点赞文章(已登录) parameters: - name: slug in: path required: true schema: type: string responses: '200': description: 点赞成功 content: application/json: schema: $ref: '#/components/schemas/ResponseVoid' '401': description: 未登录 delete: tags: [Article] summary: 取消点赞文章(已登录) parameters: - name: slug in: path required: true schema: type: string responses: '200': description: 取消成功 content: application/json: schema: $ref: '#/components/schemas/ResponseVoid' '401': description: 未登录 /api/comments/{id}/star: post: tags: [Comment] summary: 点赞评论(已登录) parameters: - name: id in: path required: true schema: type: integer format: int64 responses: '200': description: 点赞成功 content: application/json: schema: $ref: '#/components/schemas/ResponseVoid' '401': description: 未登录 delete: tags: [Comment] summary: 取消点赞评论(已登录) parameters: - name: id in: path required: true schema: type: integer format: int64 responses: '200': description: 取消成功 content: application/json: schema: $ref: '#/components/schemas/ResponseVoid' '401': description: 未登录 # ==================== Comment (Public) ==================== /api/articles/{slug}/comments: get: tags: [Comment] summary: 文章评论列表(仅返回已审核 APPROVED) security: [] parameters: - name: slug in: path required: true schema: type: string responses: '200': description: 评论列表 content: application/json: schema: $ref: '#/components/schemas/ResponseListComment' post: tags: [Comment] summary: 提交评论(免登录,提交后为 PENDING 待审核) security: [] parameters: - name: slug in: path required: true schema: type: string requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/CommentSubmitRequest' responses: '200': description: 提交成功,返回待审核评论 content: application/json: schema: $ref: '#/components/schemas/ResponseComment' /api/articles/search: get: tags: [Search] summary: 全文搜索文章 security: [] parameters: - name: q in: query required: true description: 搜索关键词 schema: type: string minLength: 1 maxLength: 100 - name: page in: query schema: type: integer default: 1 - name: size in: query schema: type: integer default: 10 maximum: 50 responses: '200': description: 搜索结果 content: application/json: schema: $ref: '#/components/schemas/ResponsePageArticleListItem' # ==================== ArticleAdmin ==================== /api/admin/articles: post: tags: [ArticleAdmin] summary: 创建文章 requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/ArticleCreateRequest' responses: '200': description: 创建成功 content: application/json: schema: $ref: '#/components/schemas/ResponseArticleDetail' /api/admin/articles/{id}: get: tags: [ArticleAdmin] summary: 文章详情(按 id,管理端,含草稿/归档等任意状态) parameters: - name: id in: path required: true schema: type: integer format: int64 responses: '200': description: 文章详情 content: application/json: schema: $ref: '#/components/schemas/ResponseArticleDetail' '404': description: 文章不存在 content: application/json: schema: $ref: '#/components/schemas/ResponseVoid' put: tags: [ArticleAdmin] summary: 更新文章 parameters: - name: id in: path required: true schema: type: integer format: int64 requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/ArticleUpdateRequest' responses: '200': description: 更新成功 content: application/json: schema: $ref: '#/components/schemas/ResponseArticleDetail' delete: tags: [ArticleAdmin] summary: 删除文章 parameters: - name: id in: path required: true schema: type: integer format: int64 responses: '200': description: 删除成功 content: application/json: schema: $ref: '#/components/schemas/ResponseVoid' /api/admin/articles/{id}/publish: patch: tags: [ArticleAdmin] summary: 发布文章(状态机:DRAFT → PUBLISHED) parameters: - name: id in: path required: true schema: type: integer format: int64 responses: '200': description: 发布成功 content: application/json: schema: $ref: '#/components/schemas/ResponseArticleDetail' /api/admin/articles/{id}/archive: patch: tags: [ArticleAdmin] summary: 归档文章 parameters: - name: id in: path required: true schema: type: integer format: int64 responses: '200': description: 归档成功 content: application/json: schema: $ref: '#/components/schemas/ResponseArticleDetail' # ==================== Tag (Public) ==================== /api/tags: get: tags: [Tag] summary: 标签列表(带文章计数) security: [] responses: '200': description: 标签列表 content: application/json: schema: $ref: '#/components/schemas/ResponseListTagWithCount' # ==================== TagAdmin ==================== /api/admin/tags: post: tags: [TagAdmin] summary: 创建标签 requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/TagCreateRequest' responses: '200': description: 创建成功 content: application/json: schema: $ref: '#/components/schemas/ResponseTag' /api/admin/tags/{id}: put: tags: [TagAdmin] summary: 更新标签 parameters: - name: id in: path required: true schema: type: integer format: int64 requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/TagUpdateRequest' responses: '200': description: 更新成功 content: application/json: schema: $ref: '#/components/schemas/ResponseTag' delete: tags: [TagAdmin] summary: 删除标签 parameters: - name: id in: path required: true schema: type: integer format: int64 responses: '200': description: 删除成功 content: application/json: schema: $ref: '#/components/schemas/ResponseVoid' # ==================== Media ==================== /api/media/upload: post: tags: [Media] summary: 上传文件 requestBody: required: true content: multipart/form-data: schema: type: object properties: file: type: string format: binary description: 文件内容 directory: type: string default: images description: 存储目录 responses: '200': description: 上传成功 content: application/json: schema: $ref: '#/components/schemas/ResponseStoredFile' # ==================== CommentAdmin ==================== /api/admin/comments: get: tags: [CommentAdmin] summary: 待审核评论列表(分页,状态 PENDING) parameters: - name: page in: query schema: type: integer default: 1 - name: size in: query schema: type: integer default: 20 responses: '200': description: 待审核评论列表 content: application/json: schema: $ref: '#/components/schemas/ResponsePageComment' /api/admin/comments/{id}: delete: tags: [CommentAdmin] summary: 删除评论 parameters: - name: id in: path required: true schema: type: integer format: int64 responses: '200': description: 删除成功 content: application/json: schema: $ref: '#/components/schemas/ResponseVoid' /api/admin/comments/{id}/approve: patch: tags: [CommentAdmin] summary: 审核通过评论(PENDING → APPROVED) parameters: - name: id in: path required: true schema: type: integer format: int64 responses: '200': description: 审核成功 content: application/json: schema: $ref: '#/components/schemas/ResponseComment' /api/admin/comments/{id}/reject: patch: tags: [CommentAdmin] summary: 驳回评论(PENDING → REJECTED) parameters: - name: id in: path required: true schema: type: integer format: int64 responses: '200': description: 驳回成功 content: application/json: schema: $ref: '#/components/schemas/ResponseComment' # ==================== User ==================== /api/admin/users: get: tags: [User] summary: 用户列表(分页) parameters: - name: page in: query schema: type: integer default: 1 - name: size in: query schema: type: integer default: 20 responses: '200': description: 用户列表 content: application/json: schema: $ref: '#/components/schemas/ResponsePageUserInfo' /api/admin/users/{id}/role: patch: tags: [User] summary: 修改用户角色 parameters: - name: id in: path required: true schema: type: integer format: int64 requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/UpdateRoleRequest' responses: '200': description: 修改成功 content: application/json: schema: $ref: '#/components/schemas/ResponseUserInfo' /api/admin/users/{id}/status: patch: tags: [User] summary: 启用/禁用用户 parameters: - name: id in: path required: true schema: type: integer format: int64 requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/UpdateStatusRequest' responses: '200': description: 修改成功 content: application/json: schema: $ref: '#/components/schemas/ResponseUserInfo' components: securitySchemes: bearerAuth: type: http scheme: bearer bearerFormat: JWT description: | 在请求头中携带 `Authorization: Bearer `。 Access Token 有效期 15 分钟,过期后用 Refresh Token 调用 `/api/auth/refresh` 换取新的 Access Token。 schemas: # ---------- 通用包装体 ---------- Response: type: object properties: code: type: integer description: 0 表示成功,非 0 为业务错误码 message: type: string data: description: 业务数据,类型由具体接口决定 required: [code, message] PageResult: type: object properties: list: type: array items: {} total: type: integer format: int64 page: type: integer size: type: integer totalPages: type: integer required: [list, total, page, size, totalPages] # ---------- 枚举 ---------- ArticleStatus: type: string enum: [DRAFT, PUBLISHED, ARCHIVED] description: | - DRAFT: 草稿,仅后台可见 - PUBLISHED: 已发布,前台可见 - ARCHIVED: 已归档,前台不可见但保留数据 UserRole: type: string enum: [ADMIN, EDITOR, VISITOR] # ---------- Auth ---------- RegisterRequest: type: object properties: username: type: string minLength: 3 maxLength: 50 password: type: string minLength: 6 maxLength: 100 email: type: string format: email required: [username, password] LoginRequest: type: object properties: username: type: string password: type: string required: [username, password] RefreshTokenRequest: type: object properties: refreshToken: type: string required: [refreshToken] TokenPair: type: object properties: accessToken: type: string refreshToken: type: string expiresIn: type: integer description: Access Token 有效期(秒) required: [accessToken, refreshToken, expiresIn] # ---------- Article ---------- ArticleCreateRequest: type: object properties: title: type: string maxLength: 200 summary: type: string maxLength: 500 content: type: string coverImage: type: string description: 封面图 URL tagIds: type: array items: type: integer format: int64 required: [title, content] ArticleUpdateRequest: type: object properties: title: type: string maxLength: 200 summary: type: string maxLength: 500 content: type: string coverImage: type: string tagIds: type: array items: type: integer format: int64 required: [title, content] ArticleListItem: type: object properties: id: type: integer format: int64 title: type: string slug: type: string summary: type: string coverImage: type: string status: $ref: '#/components/schemas/ArticleStatus' publishedAt: type: string format: date-time createdAt: type: string format: date-time authorName: type: string tagNames: type: array items: type: string starCount: type: integer format: int64 description: 点赞数 hasStarred: type: boolean nullable: true description: 当前用户是否已点赞(null 表示未登录) required: [id, title, slug, status, createdAt, authorName, tagNames, starCount] ArticleDetail: type: object properties: id: type: integer format: int64 title: type: string slug: type: string summary: type: string content: type: string coverImage: type: string status: $ref: '#/components/schemas/ArticleStatus' viewCount: type: integer format: int64 publishedAt: type: string format: date-time createdAt: type: string format: date-time updatedAt: type: string format: date-time authorName: type: string tagNames: type: array items: type: string starCount: type: integer format: int64 description: 点赞数 hasStarred: type: boolean nullable: true description: 当前用户是否已点赞(null 表示未登录) required: [id, title, slug, content, status, viewCount, createdAt, updatedAt, authorName, tagNames, starCount] OpenGraphMeta: type: object properties: title: type: string description: type: string image: type: string url: type: string type: type: string default: article required: [title, description, url, type] # ---------- Tag ---------- Tag: type: object properties: id: type: integer format: int64 name: type: string slug: type: string description: type: string articleCount: type: integer description: 关联文章数量(查询时计算) createdAt: type: string format: date-time required: [id, name, slug, articleCount, createdAt] TagCreateRequest: type: object properties: name: type: string maxLength: 50 description: type: string maxLength: 200 required: [name] TagUpdateRequest: type: object properties: name: type: string maxLength: 50 description: type: string maxLength: 200 required: [name] # ---------- Media ---------- StoredFile: type: object properties: originalName: type: string storedPath: type: string url: type: string thumbnailUrl: type: string description: 图片缩略图 URL(非图片为 null) size: type: integer format: int64 contentType: type: string required: [originalName, storedPath, url, size] # ---------- User ---------- UserInfo: type: object properties: id: type: integer format: int64 username: type: string email: type: string avatar: type: string role: $ref: '#/components/schemas/UserRole' enabled: type: boolean createdAt: type: string format: date-time required: [id, username, role, enabled, createdAt] # ---------- Comment ---------- CommentStatus: type: string enum: [PENDING, APPROVED, REJECTED] description: | - PENDING: 待审核 - APPROVED: 已通过,前台可见 - REJECTED: 已驳回 CommentSubmitRequest: type: object properties: parentId: type: integer format: int64 description: 父评论 ID(回复场景,可空) authorName: type: string maxLength: 100 authorEmail: type: string format: email content: type: string maxLength: 2000 required: [authorName, content] CommentResponse: type: object properties: id: type: integer format: int64 articleId: type: integer format: int64 parentId: type: integer format: int64 authorName: type: string authorEmail: type: string content: type: string status: $ref: '#/components/schemas/CommentStatus' createdAt: type: string format: date-time starCount: type: integer format: int64 description: 评论点赞数 hasStarred: type: boolean nullable: true description: 当前用户是否已点赞(null 表示未登录) required: [id, articleId, authorName, content, status, createdAt] # ---------- User Admin ---------- UpdateRoleRequest: type: object properties: role: $ref: '#/components/schemas/UserRole' required: [role] UpdateStatusRequest: type: object properties: enabled: type: boolean required: [enabled] # ---------- 包装体实例化 ---------- ResponseString: allOf: - $ref: '#/components/schemas/Response' properties: data: type: string ResponseVoid: allOf: - $ref: '#/components/schemas/Response' properties: data: type: "null" ResponseTokenPair: allOf: - $ref: '#/components/schemas/Response' properties: data: $ref: '#/components/schemas/TokenPair' ResponseArticleDetail: allOf: - $ref: '#/components/schemas/Response' properties: data: $ref: '#/components/schemas/ArticleDetail' ResponseOpenGraphMeta: allOf: - $ref: '#/components/schemas/Response' properties: data: $ref: '#/components/schemas/OpenGraphMeta' ResponsePageArticleListItem: allOf: - $ref: '#/components/schemas/Response' properties: data: allOf: - $ref: '#/components/schemas/PageResult' properties: list: type: array items: $ref: '#/components/schemas/ArticleListItem' ResponseListArticleListItem: allOf: - $ref: '#/components/schemas/Response' properties: data: type: array items: $ref: '#/components/schemas/ArticleListItem' ResponseTag: allOf: - $ref: '#/components/schemas/Response' properties: data: $ref: '#/components/schemas/Tag' ResponseListTagWithCount: allOf: - $ref: '#/components/schemas/Response' properties: data: type: array items: $ref: '#/components/schemas/Tag' ResponseStoredFile: allOf: - $ref: '#/components/schemas/Response' properties: data: $ref: '#/components/schemas/StoredFile' ResponsePageUserInfo: allOf: - $ref: '#/components/schemas/Response' properties: data: allOf: - $ref: '#/components/schemas/PageResult' properties: list: type: array items: $ref: '#/components/schemas/UserInfo' ResponseComment: allOf: - $ref: '#/components/schemas/Response' properties: data: $ref: '#/components/schemas/CommentResponse' ResponseListComment: allOf: - $ref: '#/components/schemas/Response' properties: data: type: array items: $ref: '#/components/schemas/CommentResponse' ResponseUserInfo: allOf: - $ref: '#/components/schemas/Response' properties: data: $ref: '#/components/schemas/UserInfo' ResponsePageComment: allOf: - $ref: '#/components/schemas/Response' properties: data: allOf: - $ref: '#/components/schemas/PageResult' properties: list: type: array items: $ref: '#/components/schemas/CommentResponse'