# GraphQL Product Review 图片上传到 S3 本文档说明如何通过 GraphQL mutation 创建产品评价并上传图片到 AWS S3。 ## 📋 目录 - [GraphQL Mutation 定义](#graphql-mutation-定义) - [使用方法](#使用方法) - [图片格式说明](#图片格式说明) - [完整示例](#完整示例) - [前端集成](#前端集成) ## GraphQL Mutation 定义 ### CreateProductReview Mutation ```graphql mutation CreateProductReview($input: CreateProductReviewInput!) { createProductReview(input: $input) { id productId title comment rating name status createdAt attachments } } ``` ### Input 类型字段 ```graphql input CreateProductReviewInput { productId: Int! # 产品ID(必填) title: String! # 评价标题(必填) comment: String! # 评价内容(必填) rating: Int! # 评分 1-5(必填) name: String! # 评价者姓名(必填) email: String # 邮箱(可选) status: Int # 状态:0=pending, 1=approved(可选,默认pending) # 旧方式:Base64 JSON 字符串(向后兼容) attachments: String # 新方式:图片数组(推荐,支持S3上传)⭐ images: [ImageInput] } input ImageInput { data: String! # Base64编码的图片数据 name: String # 文件名(可选) type: String # MIME类型(可选) } ``` ## 使用方法 ### 方法一:使用 images 数组(推荐)⭐ 这是新的推荐方式,会自动使用 ImageUploadService 上传到 S3。 #### 简单 Base64 数组 ```graphql mutation { createProductReview(input: { productId: 1 title: "Great Product!" comment: "I love this product. Highly recommended!" rating: 5 name: "John Doe" images: [ "data:image/jpeg;base64,/9j/4AAQSkZJRg...", "data:image/png;base64,iVBORw0KGgo..." ] }) { id title attachments } } ``` #### 结构化对象数组(更灵活) ```graphql mutation { createProductReview(input: { productId: 1 title: "Amazing Quality" comment: "The quality exceeded my expectations." rating: 5 name: "Jane Smith" images: [ { data: "/9j/4AAQSkZJRg..." # 不带 data URI 前缀的纯 Base64 name: "product_front.jpg" type: "image/jpeg" }, { data: "iVBORw0KGgo..." name: "product_side.png" type: "image/png" } ] }) { id title attachments } } ``` ### 方法二:使用 attachments JSON 字符串(向后兼容) 旧的方式仍然支持,用于向后兼容。 ```graphql mutation { createProductReview(input: { productId: 1 title: "Good Product" comment: "Works as expected." rating: 4 name: "Bob Wilson" attachments: "[\"data:image/jpeg;base64,/9j/4AAQ...\"]" }) { id title attachments } } ``` ## 图片格式说明 ### 支持的图片格式 - ✅ JPEG/JPG - ✅ PNG - ✅ GIF - ✅ WebP - ✅ SVG ### 文件大小限制 - 单张图片最大:**5 MB** - 建议数量:最多 **10 张**图片 ### Base64 格式 #### 带 Data URI 前缀(标准格式) ``` data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAYABgAAD... data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA... ``` #### 纯 Base64(不带前缀) ``` /9j/4AAQSkZJRgABAQEAYABgAAD... ``` 使用纯 Base64 时,需要在对象中指定 `type` 字段: ```json { "data": "/9j/4AAQSkZJRg...", "name": "photo.jpg", "type": "image/jpeg" } ``` ## 完整示例 ### JavaScript/Fetch 示例 ```javascript // 将文件转换为 Base64 function fileToBase64(file) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.readAsDataURL(file); reader.onload = () => resolve(reader.result); reader.onerror = error => reject(error); }); } // 创建评价并上传图片 async function createReviewWithImages(productId, reviewData, files) { // 转换所有文件为 Base64 const images = await Promise.all( files.map(async (file) => { const base64 = await fileToBase64(file); return { data: base64, name: file.name, type: file.type }; }) ); // GraphQL mutation const mutation = ` mutation CreateProductReview($input: CreateProductReviewInput!) { createProductReview(input: $input) { id productId title comment rating name status attachments createdAt } } `; const variables = { input: { productId: productId, title: reviewData.title, comment: reviewData.comment, rating: reviewData.rating, name: reviewData.name, email: reviewData.email, images: images // 使用新的 images 字段 } }; try { const response = await fetch('/graphql', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer YOUR_TOKEN' // 如果需要认证 }, body: JSON.stringify({ query: mutation, variables: variables }) }); const result = await response.json(); if (result.errors) { throw new Error(result.errors[0].message); } console.log('评价创建成功:', result.data.createProductReview); return result.data.createProductReview; } catch (error) { console.error('创建评价失败:', error); throw error; } } // 使用示例 document.getElementById('reviewForm').addEventListener('submit', async (e) => { e.preventDefault(); const files = Array.from(document.getElementById('images').files); const reviewData = { title: document.getElementById('title').value, comment: document.getElementById('comment').value, rating: parseInt(document.getElementById('rating').value), name: document.getElementById('name').value, email: document.getElementById('email').value }; try { const result = await createReviewWithImages(1, reviewData, files); alert('评价提交成功!'); console.log('附件信息:', result.attachments); } catch (error) { alert('提交失败: ' + error.message); } }); ``` ### Vue.js 示例 ```vue ``` ### React 示例 ```jsx import React, { useState } from 'react'; import { useMutation, gql } from '@apollo/client'; const CREATE_REVIEW_MUTATION = gql` mutation CreateProductReview($input: CreateProductReviewInput!) { createProductReview(input: $input) { id title comment rating attachments } } `; function ReviewForm({ productId }) { const [createReview] = useMutation(CREATE_REVIEW_MUTATION); const [form, setForm] = useState({ title: '', comment: '', rating: 5, name: '', email: '' }); const [files, setFiles] = useState([]); const [uploading, setUploading] = useState(false); const fileToBase64 = (file) => { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.readAsDataURL(file); reader.onload = () => resolve(reader.result); reader.onerror = reject; }); }; const handleSubmit = async (e) => { e.preventDefault(); setUploading(true); try { // 转换文件 const images = await Promise.all( files.map(async (file) => ({ data: await fileToBase64(file), name: file.name, type: file.type })) ); // 提交 mutation const { data } = await createReview({ variables: { input: { productId, ...form, images } } }); alert('评价提交成功!'); console.log('附件:', data.createProductReview.attachments); // 重置表单 setForm({ title: '', comment: '', rating: 5, name: '', email: '' }); setFiles([]); } catch (error) { alert('提交失败: ' + error.message); } finally { setUploading(false); } }; return (
); } export default ReviewForm; ``` ## 响应格式 成功的响应会包含上传后的图片信息: ```json { "data": { "createProductReview": { "id": 123, "productId": 1, "title": "Great Product!", "comment": "I love this product!", "rating": 5, "name": "John Doe", "status": 0, "attachments": "[{\"type\":\"image\",\"url\":\"https://bucket.s3.region.amazonaws.com/review/123/abc123.webp\",\"path\":\"review/123/abc123.webp\"}]", "createdAt": "2026-05-27 10:30:00" } } } ``` 解析 `attachments` 字段: ```javascript const attachments = JSON.parse(response.attachments); // [ // { // "type": "image", // "url": "https://bucket.s3.region.amazonaws.com/review/123/abc123.webp", // "path": "review/123/abc123.webp" // } // ] ``` ## 错误处理 ### 常见错误 1. **图片大小超限** ```json { "errors": [{ "message": "File size exceeds the 5MB limit" }] } ``` 2. **无效的图片格式** ```json { "errors": [{ "message": "Invalid file format" }] } ``` 3. **Base64 解码失败** ```json { "errors": [{ "message": "Invalid base64 encoding" }] } ``` ## 配置说明 确保 `.env` 文件中正确配置了 S3: ```env IMAGE_UPLOAD_DISK=s3 AWS_ACCESS_KEY_ID=your-access-key AWS_SECRET_ACCESS_KEY=your-secret-key AWS_DEFAULT_REGION=us-east-1 AWS_BUCKET=your-bucket-name AWS_URL=https://your-bucket.s3.us-east-1.amazonaws.com ``` ## 性能优化建议 1. **前端压缩图片**:在上传前压缩图片,减少传输时间 2. **限制图片数量**:建议最多上传 5-10 张 3. **显示上传进度**:对于大文件,显示进度条 4. **异步上传**:先提交评价文本,再异步上传图片 ## 安全注意事项 1. ✅ 服务端验证文件类型和大小 2. ✅ 使用 S3 预签名 URL(可选增强) 3. ✅ 限制每个评价的图片数量 4. ✅ 记录上传日志用于审计 --- **需要帮助?** 查看 [ImageUpload 模块文档](../ImageUpload/README.md)