# 问题反馈 API 接口定义 ## 1. 文档状态 本文档定义 CozSweet 问题反馈提交接口,供后端实现和前后端联调使用。 前端已经按本文协议完成以下功能: - 反馈分类和文字提交; - 最多 3 张截图; - 图片压缩和格式限制; - Login Token 与 Guest Token 认证; - 提交成功后展示 `feedbackId`。 后端需要实现: ```http POST /api/feedback ``` 环境地址: | 环境 | API Base URL | | --- | --- | | test 测试环境 | `https://testapi.banlv-ai.com` | | pro 预发环境 | `https://proapi.banlv-ai.com` | | production 生产环境 | `https://api.banlv-ai.com` | ## 2. 身份认证 接口需要 Bearer Token,支持以下两种身份: 1. 正式用户 Login Token; 2. 游客 Guest Token。 ```http Authorization: Bearer ``` 后端必须从 Token 中解析用户或游客身份,不接受前端传递 `userId`、`guestId` 或其他身份字段。 Token 缺失、无效或过期时返回 HTTP `401`。 ## 3. 请求定义 ### 3.1 请求地址 ```http POST /api/feedback ``` ### 3.2 Content-Type 请求使用: ```http Content-Type: multipart/form-data; boundary= ``` 调用方不能手工固定 `boundary`。浏览器会根据 FormData 自动生成完整 Content-Type。 ### 3.3 Multipart 字段 | 字段 | 类型 | 是否必填 | 说明 | | --- | --- | --- | --- | | `category` | string | 是 | 反馈分类,只允许标准枚举值。 | | `content` | string | 是 | 用户反馈正文,去除首尾空白后长度为 10–2000。 | | `context` | string | 是 | JSON 字符串,结构见下文。 | | `images` | file[] | 否 | 重复字段,最多 3 张图片。没有图片时不发送该字段。 | 标准 `category`: | 值 | 含义 | | --- | --- | | `problem` | 功能异常或使用问题 | | `suggestion` | 产品建议 | | `payment` | 充值、订阅或支付问题 | | `other` | 其他反馈 | 不支持分类别名。非法分类返回 HTTP `400`。 ### 3.4 Context JSON `context` 在 multipart 中是一个 JSON 字符串: ```json { "appVersion": "0.1.0", "platform": "android Android 15", "browser": "facebook IAB / Chrome 135.0", "viewport": "392x760@2.75" } ``` | 字段 | 类型 | 最大长度 | 说明 | | --- | --- | ---: | --- | | `appVersion` | string | 64 | 前端应用版本。 | | `platform` | string | 128 | 平台、系统名称和版本。 | | `browser` | string | 256 | 浏览器或应用内浏览器信息。 | | `viewport` | string | 64 | CSS viewport 宽高和设备像素比。 | 后端只需要保存上述 4 个字段。不要要求前端增加 User-Agent、设备型号、IP、聊天内容、联系方式或 Token。 ### 3.5 图片规则 | 规则 | 要求 | | --- | --- | | 数量 | 0–3 张 | | 单文件大小 | 不超过 2 MiB,即 `2 * 1024 * 1024` bytes | | 接受格式 | JPEG、PNG、WebP | | 接受 MIME | `image/jpeg`、`image/png`、`image/webp` | | 推荐请求总大小上限 | 8 MiB | 前端允许用户选择不超过 5 MiB 的源文件,但提交前会压缩到最长边不超过 1600px、目标大小不超过 2 MiB。后端仍需独立执行数量、大小、扩展名、MIME 和文件魔数校验,不能信任浏览器提供的文件名或 Content-Type。 建议后端移除 EXIF 等元数据,并将图片保存到私有存储桶。反馈截图不得使用永久公开 URL。 ## 4. 请求示例 ### 4.1 仅文字反馈 ```bash curl -X POST 'https://api.banlv-ai.com/api/feedback' \ -H 'Authorization: Bearer ' \ -F 'category=problem' \ -F 'content=The send button did not respond after I entered a message.' \ -F 'context={"appVersion":"0.1.0","platform":"android Android 15","browser":"Chrome 135.0","viewport":"392x760@2.75"}' ``` ### 4.2 带多张截图 ```bash curl -X POST 'https://api.banlv-ai.com/api/feedback' \ -H 'Authorization: Bearer ' \ -F 'category=payment' \ -F 'content=The payment completed but my balance did not update.' \ -F 'context={"appVersion":"0.1.0","platform":"android Android 15","browser":"facebook IAB / Chrome 135.0","viewport":"392x760@2.75"}' \ -F 'images=@payment-screen-1.webp;type=image/webp' \ -F 'images=@payment-screen-2.jpg;type=image/jpeg' ``` ## 5. 成功响应 成功时返回 HTTP `200`: ```json { "success": true, "message": "Feedback submitted", "data": { "feedbackId": "c5db8e32-60f0-4f47-81bb-c22254c0562f" } } ``` 前端公开响应字段只有: | 字段 | 类型 | 是否必填 | 说明 | | --- | --- | --- | --- | | `data.feedbackId` | string | 是 | 非空反馈编号,推荐使用 UUID。 | 前端不需要后端返回用户信息、反馈正文、图片地址、状态、创建时间或诊断上下文。 ## 6. 失败响应 失败响应使用统一 envelope: ```json { "success": false, "message": "Feedback content must contain at least 10 characters", "error": "INVALID_FEEDBACK_CONTENT" } ``` 建议状态码: | HTTP 状态 | error | 场景 | | ---: | --- | --- | | `400` | `INVALID_FEEDBACK_CATEGORY` | 分类不在标准枚举中。 | | `400` | `INVALID_FEEDBACK_CONTENT` | 正文为空或长度不在 10–2000。 | | `400` | `INVALID_FEEDBACK_CONTEXT` | Context 不是合法 JSON 或字段不合法。 | | `400` | `TOO_MANY_FEEDBACK_IMAGES` | 图片超过 3 张。 | | `401` | `UNAUTHORIZED` | Token 缺失、无效或过期。 | | `413` | `FEEDBACK_PAYLOAD_TOO_LARGE` | 单文件或请求总大小超过限制。 | | `415` | `UNSUPPORTED_FEEDBACK_IMAGE` | 图片格式、MIME 或文件魔数不支持。 | | `429` | `FEEDBACK_RATE_LIMITED` | 用户或游客提交过于频繁。 | | `500` | `FEEDBACK_SUBMIT_FAILED` | 持久化或文件存储失败。 | `message` 应当是可展示给用户的简短英文文本。不得在响应中返回堆栈、存储路径、数据库错误或内部实现信息。 ## 7. 后端处理流程 推荐处理顺序: 1. 验证 Bearer Token,解析正式用户或游客身份。 2. 读取 multipart 字段并验证分类、正文和 Context。 3. 在读取完整文件前检查图片数量和请求大小。 4. 校验每张图片的真实格式、文件魔数、尺寸和大小。 5. 使用服务端生成的随机对象名写入私有存储。 6. 创建反馈记录并关联图片对象。 7. 返回非空 `feedbackId`。 8. 任一步失败时清理本次已经上传但未关联的图片。 每个成功请求创建一条反馈记录。当前前端没有发送 Idempotency-Key,因此后端不能依赖客户端幂等键去重。 ## 8. 推荐数据模型 反馈主记录最少需要: | 字段 | 说明 | | --- | --- | | `id` | UUID,作为 `feedbackId` 返回。 | | `user_id` | 正式用户 ID,可为空。 | | `guest_id` | Guest 身份 ID,可为空。 | | `category` | 标准反馈分类。 | | `content` | 反馈正文。 | | `context` | JSON/JSONB,只保存定义的 4 个字段。 | | `status` | 内部处理状态,例如 `open`、`resolved`。不返回前端。 | | `created_at` | 服务端创建时间。 | 图片记录最少需要反馈 ID、私有对象 key、MIME、字节大小、宽高和创建时间。不要把用户原始文件名作为存储对象名。 正式用户必须只写入 `user_id`,Guest 必须只写入 `guest_id`。身份信息由 Token 派生。 ## 9. 安全与日志 - 建议按用户或 Guest 身份限流,例如每小时不超过 10 次。 - 普通应用日志不得记录反馈正文、Token、图片内容、原始文件名或完整 Context。 - 可以记录 `feedbackId`、分类、图片数量、处理时长和错误码。 - 图片下载和后台查看必须经过授权,不允许匿名公开访问。 - 数据保留、删除和后台访问需要遵循现有隐私政策。 ## 10. OpenAPI 参考 ```yaml paths: /api/feedback: post: summary: Submit user feedback security: - bearerAuth: [] requestBody: required: true content: multipart/form-data: schema: type: object required: - category - content - context properties: category: type: string enum: [problem, suggestion, payment, other] content: type: string minLength: 10 maxLength: 2000 context: type: string description: JSON-encoded FeedbackContext images: type: array maxItems: 3 items: type: string format: binary responses: "200": description: Feedback submitted content: application/json: schema: type: object required: [success, data] properties: success: type: boolean const: true message: type: string data: type: object required: [feedbackId] properties: feedbackId: type: string minLength: 1 "400": description: Invalid feedback payload "401": description: Missing or invalid token "413": description: Payload too large "415": description: Unsupported image "429": description: Too many requests "500": description: Feedback persistence failed ``` ## 11. 联调验收 后端完成后至少验证: 1. Login Token 和 Guest Token 都能成功提交。 2. 无图片、1 张图片和 3 张图片均能创建反馈。 3. 第 4 张图片、超大图片、伪造 MIME 和非法 Context 被拒绝。 4. 成功响应始终包含非空 `data.feedbackId`。 5. 存储失败时不留下孤立图片对象或半成品反馈记录。 6. 日志中不出现反馈正文、Token、图片内容和原始文件名。 7. 同一身份超过限流阈值后返回 HTTP `429`。