openapi: 3.0.3 info: title: 哈尔滨美食地图 API version: 0.2.0 description: | 统一约定: - 除 `/healthz` 外,所有 `/api/**` 接口都必须携带 `X-API-Key` - 管理端 `/api/admin/**`(除登录外)还必须携带 `Authorization: Bearer ` - 用户端 `/api/user/**`(需要登录的接口)必须携带 `Authorization: Bearer ` servers: - url: http://localhost:8080 tags: - name: Health description: 健康检查 - name: Captcha description: 验证码 - name: AdminAuth description: 管理员登录与身份 - name: Settings description: 系统设置(CORS) - name: APIKeys description: APIKey 管理 - name: Merchant description: 商家入驻与审核 - name: Categories description: 分类管理 - name: Stores description: 店铺管理与搜索/热榜 - name: Reviews description: 评论管理与用户发评 - name: Users description: 用户注册/登录/信息 - name: Rankings description: 排行(管理端) - name: Upload description: 上传(管理端) components: securitySchemes: ApiKeyAuth: type: apiKey in: header name: X-API-Key BearerAuth: type: http scheme: bearer schemas: RespBase: type: object properties: code: type: integer description: 0 表示成功;非 0 通常等于 HTTP 状态码 message: type: string data: {} meta: {} required: [code, message] PaginationMeta: type: object properties: page: { type: integer, example: 1 } page_size: { type: integer, example: 20 } total: { type: integer, example: 123 } total_page: { type: integer, example: 7 } AdminUser: type: object properties: id: { type: integer, example: 1 } username: { type: string, example: admin } role: { type: string, example: admin } enabled: { type: boolean, example: true } created_at: { type: string, format: date-time } updated_at: { type: string, format: date-time } User: type: object properties: id: { type: integer, example: 7 } username: { type: string, example: tiedanzi888 } status: { type: string, example: active } created_at: { type: string, format: date-time } updated_at: { type: string, format: date-time } Category: type: object properties: id: { type: integer, example: 1 } name: { type: string, example: 烧烤 } icon_url: { type: string, example: "http://localhost:8080/static/upload/2025/12/xx.png" } sort_order: { type: integer, example: 10 } enabled: { type: boolean, example: true } created_at: { type: string, format: date-time } updated_at: { type: string, format: date-time } StoreImage: type: object properties: id: { type: integer } url: { type: string, example: "http://localhost:8080/static/upload/2025/12/xx.jpg" } sort_order: { type: integer, example: 0 } SignatureDish: type: object properties: id: { type: integer } name: { type: string, example: 锅包肉 } description: { type: string, example: 必点 } image_url: { type: string, example: "http://localhost:8080/static/upload/2025/12/dish.jpg" } sort_order: { type: integer, example: 0 } Store: type: object properties: id: { type: integer, example: 1 } name: { type: string, example: 示例店铺 } category_id: { type: integer, example: 1 } category: $ref: "#/components/schemas/Category" address: { type: string, example: 哈尔滨市道里区xxx路 } open_hours: { type: string, example: "10:00-22:00" } phone: { type: string, example: "13000000000" } cover_url: { type: string, example: "http://localhost:8080/static/upload/2025/12/cover.jpg" } description: { type: string, example: 店铺描述 } status: { type: string, example: active } images: type: array items: { $ref: "#/components/schemas/StoreImage" } dishes: type: array items: { $ref: "#/components/schemas/SignatureDish" } created_at: { type: string, format: date-time } updated_at: { type: string, format: date-time } Review: type: object properties: id: { type: integer, example: 1 } store_id: { type: integer, example: 1 } store: $ref: "#/components/schemas/Store" user_id: { type: integer, nullable: true } user_name: { type: string, example: tiedanzi888 } rating: { type: integer, example: 5 } content: { type: string, example: 太好吃了 } image_urls: type: array items: { type: string } recommend_dishes: type: array items: type: object additionalProperties: true status: { type: string, example: pending } created_at: { type: string, format: date-time } updated_at: { type: string, format: date-time } MerchantApplication: type: object properties: id: { type: integer } store_name: { type: string } category_id: { type: integer } address: { type: string } open_hours: { type: string } phone: { type: string } cover_url: { type: string } image_urls: type: array items: { type: string } description: { type: string } contact_name: { type: string } contact_phone: { type: string } status: { type: string, example: pending } reject_reason: { type: string } reviewed_at: { type: string, format: date-time, nullable: true } reviewer_id: { type: integer, nullable: true } created_at: { type: string, format: date-time } updated_at: { type: string, format: date-time } APIKeyItem: type: object properties: id: { type: integer } name: { type: string } prefix: { type: string } status: { type: string, example: active } last_used_at: { type: string, format: date-time, nullable: true } revoked_at: { type: string, format: date-time, nullable: true } created_at: { type: string, format: date-time } updated_at: { type: string, format: date-time } # ---- Requests ---- CaptchaNewData: type: object properties: captcha_id: { type: string, example: c180df303849ea6dc75c670c4f1062b0 } svg: { type: string, description: SVG 字符串(可直接渲染) } required: [captcha_id, svg] AdminLoginReq: type: object required: [username, password, captcha_id, captcha_code] properties: username: { type: string, example: admin } password: { type: string, example: admin123456 } captcha_id: { type: string } captcha_code: { type: string, example: XHSXY } UserRegisterReq: type: object required: [username, password, captcha_id, captcha_code] properties: username: { type: string, example: tiedanzi888 } password: { type: string, example: malegebi } captcha_id: { type: string } captcha_code: { type: string, example: XHSXY } UserLoginReq: type: object required: [username, password, captcha_id, captcha_code] properties: username: { type: string } password: { type: string } captcha_id: { type: string } captcha_code: { type: string } CORSUpdateReq: type: object required: [origins] properties: origins: type: array items: { type: string } example: ["http://localhost:5173", "https://admin.example.com"] APIKeyCreateReq: type: object required: [name] properties: name: { type: string, example: 前端管理后台 } CategoryUpsertReq: type: object required: [name] properties: name: { type: string, example: 烧烤 } icon_url: { type: string } sort_order: { type: integer, example: 10 } enabled: { type: boolean, example: true } StoreUpsertReq: type: object required: [name, category_id, address] properties: name: { type: string } category_id: { type: integer } address: { type: string } open_hours: { type: string } phone: { type: string } cover_url: { type: string } description: { type: string } status: { type: string, description: active|inactive } images: type: array items: type: object required: [url] properties: url: { type: string } sort_order: { type: integer } dishes: type: array items: type: object required: [name] properties: name: { type: string } description: { type: string } image_url: { type: string } sort_order: { type: integer } StatusReq: type: object required: [status] properties: status: { type: string } MerchantApplyReq: type: object required: [store_name, category_id, address, contact_name, contact_phone] properties: store_name: { type: string } category_id: { type: integer } address: { type: string } open_hours: { type: string } phone: { type: string } cover_url: { type: string } image_urls: type: array items: { type: string } description: { type: string } contact_name: { type: string } contact_phone: { type: string } MerchantReviewReq: type: object required: [action] properties: action: { type: string, enum: [approve, reject] } reject_reason: { type: string } ReviewCreateReq: type: object required: [rating] properties: rating: { type: integer, minimum: 1, maximum: 5, example: 5 } content: { type: string, example: 太好吃了 } image_urls: type: array items: { type: string } recommend_dishes: type: array items: type: object additionalProperties: true AdminCreateReq: type: object required: [username, password] properties: username: { type: string } password: { type: string } enabled: { type: boolean, default: true } AdminUpdatePasswordReq: type: object required: [password] properties: password: { type: string } AdminUpdateEnabledReq: type: object required: [enabled] properties: enabled: { type: boolean } parameters: Page: in: query name: page schema: { type: integer, default: 1 } PageSize: in: query name: page_size schema: { type: integer, default: 20 } paths: /healthz: get: tags: [Health] summary: 健康检查 responses: "200": description: ok /api/captcha/new: get: tags: [Captcha] summary: 获取验证码(SVG) description: | - `captcha_id` 有效期 5 分钟 - 校验成功后删除(一次性使用) security: - ApiKeyAuth: [] responses: "200": description: ok content: application/json: schema: allOf: - $ref: "#/components/schemas/RespBase" - type: object properties: data: { $ref: "#/components/schemas/CaptchaNewData" } examples: ok: value: code: 0 message: ok data: captcha_id: c180df303849ea6dc75c670c4f1062b0 svg: "XHSXY" /api/user/register: post: tags: [Users] summary: 用户注册(需要验证码) security: - ApiKeyAuth: [] requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/UserRegisterReq" } responses: "200": description: ok content: application/json: schema: allOf: - $ref: "#/components/schemas/RespBase" - type: object properties: data: type: object properties: token: { type: string } user: { $ref: "#/components/schemas/User" } "409": description: username already exists /api/user/login: post: tags: [Users] summary: 用户登录(需要验证码) security: - ApiKeyAuth: [] requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/UserLoginReq" } responses: "200": description: ok content: application/json: schema: allOf: - $ref: "#/components/schemas/RespBase" - type: object properties: data: type: object properties: token: { type: string } user: type: object properties: id: { type: integer } username: { type: string } /api/user/me: get: tags: [Users] summary: 当前用户信息 security: - ApiKeyAuth: [] - BearerAuth: [] responses: "200": description: ok /api/user/reviews: get: tags: [Reviews] summary: 我的评论列表(用户端) security: - ApiKeyAuth: [] - BearerAuth: [] parameters: - $ref: "#/components/parameters/Page" - $ref: "#/components/parameters/PageSize" responses: "200": description: ok /api/user/upload: post: tags: [Upload] summary: 用户上传图片(用于评论等) security: - ApiKeyAuth: [] - BearerAuth: [] requestBody: required: true content: multipart/form-data: schema: type: object required: [file] properties: file: type: string format: binary responses: "200": description: ok /api/user/douyin/login: post: tags: [Users] summary: 抖音登录(预留) security: - ApiKeyAuth: [] responses: "501": description: not implemented /api/user/stores/{id}/reviews: post: tags: [Reviews] summary: 用户提交评论(进入待审核) security: - ApiKeyAuth: [] - BearerAuth: [] parameters: - in: path name: id required: true schema: { type: integer } requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/ReviewCreateReq" } responses: "200": description: ok /api/user/stores/{id}/like: post: tags: [Stores] summary: 用户点赞/取消点赞(toggle) security: - ApiKeyAuth: [] - BearerAuth: [] parameters: - in: path name: id required: true schema: { type: integer } responses: "200": description: ok /api/merchant/apply: post: tags: [Merchant] summary: 商家入驻提交 security: - ApiKeyAuth: [] requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/MerchantApplyReq" } responses: "200": description: ok /api/categories: get: tags: [Categories] summary: 分类列表(公开,只返回 enabled=true) security: - ApiKeyAuth: [] responses: "200": description: ok /api/stores: get: tags: [Stores] summary: 店铺列表(公开,只返回 active) security: - ApiKeyAuth: [] parameters: - in: query name: keyword schema: { type: string } - in: query name: category_id schema: { type: integer } - $ref: "#/components/parameters/Page" - $ref: "#/components/parameters/PageSize" responses: "200": description: ok /api/stores/{id}: get: tags: [Stores] summary: 店铺详情(公开,只返回 active) security: - ApiKeyAuth: [] parameters: - in: path name: id required: true schema: { type: integer } responses: "200": description: ok /api/stores/{id}/reviews: get: tags: [Reviews] summary: 店铺评论列表(公开,只返回 approved) security: - ApiKeyAuth: [] parameters: - in: path name: id required: true schema: { type: integer } - $ref: "#/components/parameters/Page" - $ref: "#/components/parameters/PageSize" responses: "200": description: ok /api/rankings/stores: get: tags: [Rankings] summary: 店铺排行(公开,只返回 active) security: - ApiKeyAuth: [] parameters: - in: query name: by schema: type: string enum: [hotness, likes, search, reviews] default: hotness - $ref: "#/components/parameters/Page" - $ref: "#/components/parameters/PageSize" responses: "200": description: ok /api/stores/search: get: tags: [Stores] summary: 店铺搜索(会累积热度) security: - ApiKeyAuth: [] parameters: - in: query name: keyword schema: { type: string } - $ref: "#/components/parameters/Page" - $ref: "#/components/parameters/PageSize" responses: "200": description: ok /api/stores/hot: get: tags: [Stores] summary: 店铺热榜 security: - ApiKeyAuth: [] parameters: - in: query name: by schema: type: string enum: [hotness, likes, search, reviews] default: hotness - in: query name: limit schema: { type: integer, default: 20 } responses: "200": description: ok /api/admin/login: post: tags: [AdminAuth] summary: 管理员登录 security: - ApiKeyAuth: [] requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/AdminLoginReq" } responses: "200": description: ok /api/admin/me: get: tags: [AdminAuth] summary: 当前管理员信息 security: - ApiKeyAuth: [] - BearerAuth: [] responses: "200": description: ok /api/admin/dashboard/overview: get: tags: [AdminAuth] summary: 后台概览(统计) security: - ApiKeyAuth: [] - BearerAuth: [] responses: "200": description: ok /api/admin/settings/cors: get: tags: [Settings] summary: 获取 CORS origins security: - ApiKeyAuth: [] - BearerAuth: [] responses: "200": description: ok put: tags: [Settings] summary: 更新 CORS origins security: - ApiKeyAuth: [] - BearerAuth: [] requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/CORSUpdateReq" } responses: "200": description: ok /api/admin/apikeys: get: tags: [APIKeys] summary: APIKey 列表 security: - ApiKeyAuth: [] - BearerAuth: [] parameters: - $ref: "#/components/parameters/Page" - $ref: "#/components/parameters/PageSize" responses: "200": description: ok post: tags: [APIKeys] summary: 创建 APIKey(只会返回一次明文 key) security: - ApiKeyAuth: [] - BearerAuth: [] requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/APIKeyCreateReq" } responses: "200": description: ok /api/admin/apikeys/{id}/revoke: patch: tags: [APIKeys] summary: 撤销 APIKey security: - ApiKeyAuth: [] - BearerAuth: [] parameters: - in: path name: id required: true schema: { type: integer } responses: "200": description: ok /api/admin/merchant/applications: get: tags: [Merchant] summary: 商家入驻申请列表 security: - ApiKeyAuth: [] - BearerAuth: [] parameters: - in: query name: status schema: { type: string, enum: [pending, approved, rejected] } - $ref: "#/components/parameters/Page" - $ref: "#/components/parameters/PageSize" responses: "200": description: ok /api/admin/merchant/applications/{id}/review: patch: tags: [Merchant] summary: 审核商家入驻(approve/reject) security: - ApiKeyAuth: [] - BearerAuth: [] parameters: - in: path name: id required: true schema: { type: integer } requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/MerchantReviewReq" } responses: "200": description: ok /api/admin/categories: get: tags: [Categories] summary: 分类列表 security: - ApiKeyAuth: [] - BearerAuth: [] parameters: - in: query name: keyword schema: { type: string } - $ref: "#/components/parameters/Page" - $ref: "#/components/parameters/PageSize" responses: "200": description: ok post: tags: [Categories] summary: 新增分类 security: - ApiKeyAuth: [] - BearerAuth: [] requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/CategoryUpsertReq" } responses: "200": description: ok /api/admin/categories/{id}: put: tags: [Categories] summary: 编辑分类 security: - ApiKeyAuth: [] - BearerAuth: [] parameters: - in: path name: id required: true schema: { type: integer } requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/CategoryUpsertReq" } responses: "200": description: ok delete: tags: [Categories] summary: 删除分类(软删) security: - ApiKeyAuth: [] - BearerAuth: [] parameters: - in: path name: id required: true schema: { type: integer } responses: "200": description: ok /api/admin/stores: get: tags: [Stores] summary: 店铺列表 security: - ApiKeyAuth: [] - BearerAuth: [] parameters: - in: query name: keyword schema: { type: string } - in: query name: status schema: { type: string, enum: [active, inactive] } - in: query name: category_id schema: { type: integer } - $ref: "#/components/parameters/Page" - $ref: "#/components/parameters/PageSize" responses: "200": description: ok post: tags: [Stores] summary: 新增店铺 security: - ApiKeyAuth: [] - BearerAuth: [] requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/StoreUpsertReq" } responses: "200": description: ok /api/admin/stores/{id}: get: tags: [Stores] summary: 店铺详情(含图片/招牌菜) security: - ApiKeyAuth: [] - BearerAuth: [] parameters: - in: path name: id required: true schema: { type: integer } responses: "200": description: ok put: tags: [Stores] summary: 编辑店铺 security: - ApiKeyAuth: [] - BearerAuth: [] parameters: - in: path name: id required: true schema: { type: integer } requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/StoreUpsertReq" } responses: "200": description: ok delete: tags: [Stores] summary: 删除店铺(软删) security: - ApiKeyAuth: [] - BearerAuth: [] parameters: - in: path name: id required: true schema: { type: integer } responses: "200": description: ok /api/admin/stores/{id}/status: patch: tags: [Stores] summary: 上下架店铺 security: - ApiKeyAuth: [] - BearerAuth: [] parameters: - in: path name: id required: true schema: { type: integer } requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/StatusReq" } examples: active: { value: { status: active } } inactive: { value: { status: inactive } } responses: "200": description: ok /api/admin/reviews: get: tags: [Reviews] summary: 评论列表 security: - ApiKeyAuth: [] - BearerAuth: [] parameters: - in: query name: status schema: { type: string, enum: [pending, approved, blocked] } - in: query name: store_id schema: { type: integer } - in: query name: user_id schema: { type: integer } - $ref: "#/components/parameters/Page" - $ref: "#/components/parameters/PageSize" responses: "200": description: ok /api/admin/reviews/{id}/status: patch: tags: [Reviews] summary: 修改评论状态(审核) security: - ApiKeyAuth: [] - BearerAuth: [] parameters: - in: path name: id required: true schema: { type: integer } requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/StatusReq" } examples: approve: { value: { status: approved } } block: { value: { status: blocked } } responses: "200": description: ok /api/admin/reviews/{id}: delete: tags: [Reviews] summary: 删除评论(软删) security: - ApiKeyAuth: [] - BearerAuth: [] parameters: - in: path name: id required: true schema: { type: integer } responses: "200": description: ok /api/admin/users: get: tags: [Users] summary: 用户列表(管理端) security: - ApiKeyAuth: [] - BearerAuth: [] parameters: - in: query name: keyword schema: { type: string } - in: query name: status schema: { type: string, enum: [active, disabled] } - $ref: "#/components/parameters/Page" - $ref: "#/components/parameters/PageSize" responses: "200": description: ok /api/admin/users/{id}: get: tags: [Users] summary: 用户详情(管理端) security: - ApiKeyAuth: [] - BearerAuth: [] parameters: - in: path name: id required: true schema: { type: integer } responses: "200": description: ok /api/admin/users/{id}/status: patch: tags: [Users] summary: 修改用户状态(启用/禁用) security: - ApiKeyAuth: [] - BearerAuth: [] parameters: - in: path name: id required: true schema: { type: integer } requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/StatusReq" } examples: disable: { value: { status: disabled } } active: { value: { status: active } } responses: "200": description: ok /api/admin/admins: get: tags: [AdminAuth] summary: 管理员列表 security: - ApiKeyAuth: [] - BearerAuth: [] parameters: - in: query name: keyword schema: { type: string } - $ref: "#/components/parameters/Page" - $ref: "#/components/parameters/PageSize" responses: "200": description: ok post: tags: [AdminAuth] summary: 创建管理员 security: - ApiKeyAuth: [] - BearerAuth: [] requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/AdminCreateReq" } responses: "200": description: ok /api/admin/admins/{id}/password: patch: tags: [AdminAuth] summary: 重置管理员密码 security: - ApiKeyAuth: [] - BearerAuth: [] parameters: - in: path name: id required: true schema: { type: integer } requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/AdminUpdatePasswordReq" } responses: "200": description: ok /api/admin/admins/{id}/enabled: patch: tags: [AdminAuth] summary: 启用/禁用管理员 security: - ApiKeyAuth: [] - BearerAuth: [] parameters: - in: path name: id required: true schema: { type: integer } requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/AdminUpdateEnabledReq" } responses: "200": description: ok /api/admin/rankings/stores: get: tags: [Rankings] summary: 店铺排行(管理端) security: - ApiKeyAuth: [] - BearerAuth: [] parameters: - in: query name: by schema: type: string enum: [hotness, likes, search, reviews] default: hotness - $ref: "#/components/parameters/Page" - $ref: "#/components/parameters/PageSize" responses: "200": description: ok /api/admin/rankings/stores/recalc: post: tags: [Rankings] summary: 重算综合分(score) security: - ApiKeyAuth: [] - BearerAuth: [] parameters: - in: query name: limit schema: { type: integer, default: 5000 } responses: "200": description: ok /api/admin/upload: post: tags: [Upload] summary: 上传图片(multipart/form-data) security: - ApiKeyAuth: [] - BearerAuth: [] requestBody: required: true content: multipart/form-data: schema: type: object required: [file] properties: file: type: string format: binary responses: "200": description: ok