949 字
5 分钟
在 MedusaJS 与 Next.js 店面中实现产品评论功能的完整教程
核心架构设计
在 Headless 电商架构中,实现产品评论功能涉及三个核心部分的联动:
- Medusa Server:定义
ProductReview实体数据结构,提供 RESTful API 供前后台调用。 - Medusa Admin:供商家审核、删除不当评论。
- Next.js Storefront:供消费者查看评论并提交心得。
第一步:设置 Medusa 服务端实体
1. 创建实体模型 (Entity)
在 src/models/product-review.ts 中定义评论的数据结构。
import { BaseEntity, Product, generateEntityId } from "@medusajs/medusa";import { BeforeInsert, Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm";import { Max, Min, IsString, IsNotEmpty } from "class-validator";
@Entity()export class ProductReview extends BaseEntity { @Index() @Column({ type: "varchar" }) product_id: string;
@ManyToOne(() => Product) @JoinColumn({ name: "product_id" }) product: Product;
@Column({ type: "varchar" }) @IsNotEmpty() title: string;
@Column({ type: "varchar" }) @IsNotEmpty() user_name: string;
@Column({ type: "int" }) @Min(1) @Max(5) rating: number;
@Column({ type: "text" }) content: string;
@BeforeInsert() private beforeInsert(): void { this.id = generateEntityId(this.id, "prev"); }}2. 创建数据库迁移 (Migration)
运行命令生成迁移文件,并在 up 方法中定义表结构。建议使用 TypeORM 的 Table 类以获得更好的类型检查:
import { MigrationInterface, QueryRunner, Table, TableForeignKey } from "typeorm";
export class ProductReview1621234567890 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise<void> { await queryRunner.createTable(new Table({ name: "product_review", columns: [ { name: "id", type: "varchar", isPrimary: true }, { name: "product_id", type: "varchar" }, { name: "user_name", type: "varchar" }, { name: "title", type: "varchar" }, { name: "rating", type: "integer" }, { name: "content", type: "text" }, { name: "created_at", type: "timestamp with time zone", default: "now()" }, { name: "updated_at", type: "timestamp with time zone", default: "now()" }, ] }), true);
await queryRunner.createForeignKey("product_review", new TableForeignKey({ columnNames: ["product_id"], referencedColumnNames: ["id"], referencedTableName: "product", onDelete: "CASCADE" })); }
public async down(queryRunner: QueryRunner): Promise<void> { await queryRunner.dropTable("product_review"); }}第二步:编写业务逻辑层 (Service)
在 src/services/product-review.ts 中封装对数据库的操作逻辑。
import { TransactionBaseService } from "@medusajs/medusa";import { ProductReviewRepository } from "../repositories/product-review";import { ProductReview } from "../models/product-review";import { EntityManager } from "typeorm";
class ProductReviewService extends TransactionBaseService { protected productReviewRepository_: typeof ProductReviewRepository;
constructor(container) { super(container); this.productReviewRepository_ = container.productReviewRepository; }
async getProductReviews(product_id: string): Promise<ProductReview[]> { const reviewRepo = this.activeManager_.withRepository(this.productReviewRepository_); return await reviewRepo.find({ where: { product_id } }); }
async addProductReview(product_id: string, data: Partial<ProductReview>): Promise<ProductReview> { return await this.atomicPhase_(async (manager) => { const reviewRepo = manager.withRepository(this.productReviewRepository_); const createdReview = reviewRepo.create({ ...data, product_id }); return await reviewRepo.save(createdReview); }); }}
export default ProductReviewService;第三步:暴露 API 路由
在 src/api/index.ts 中注册 Store 端点,记得配置跨域 (CORS)。
import { Router } from "express";import { wrapHandler } from "@medusajs/medusa";
export default (rootDirectory, options) => { const router = Router();
// 获取产品评论 router.get("/store/products/:id/reviews", wrapHandler(async (req, res) => { const { id } = req.params; const reviewService = req.scope.resolve("productReviewService"); const reviews = await reviewService.getProductReviews(id); res.json({ reviews }); }));
// 提交产品评论 router.post("/store/products/:id/reviews", wrapHandler(async (req, res) => { const { id } = req.params; const reviewService = req.scope.resolve("productReviewService"); const review = await reviewService.addProductReview(id, req.body); res.status(201).json({ review }); }));
return router;};第四步:Next.js 店面集成
在店面的产品详情页 (/pages/products/[handle].tsx),我们使用 TanStack Query (React Query) 来处理请求,这比直接使用 useEffect 更优雅。
1. 评论提交表单
import { useForm } from "react-hook-form";import axios from "axios";
const ReviewForm = ({ productId, refetch }) => { const { register, handleSubmit, reset } = useForm();
const onSubmit = async (data) => { try { await axios.post(`${process.env.NEXT_PUBLIC_MEDUSA_URL}/store/products/${productId}/reviews`, data); reset(); refetch(); // 刷新评论列表 alert("Review submitted!"); } catch (e) { console.error("Submission failed", e); } };
return ( <form onSubmit={handleSubmit(onSubmit)} className="space-y-4 border p-4 rounded-lg"> <input {...register("user_name")} placeholder="Your Name" className="w-full border p-2" required /> <input {...register("title")} placeholder="Review Title" className="w-full border p-2" required /> <select {...register("rating")} className="w-full border p-2"> {[5, 4, 3, 2, 1].map(n => <option key={n} value={n}>{n} Stars</option>)} </select> <textarea {...register("content")} placeholder="Share your thoughts..." className="w-full border p-2" /> <button type="submit" className="bg-black text-white px-4 py-2 rounded">Submit Review</button> </form> );};2. 展示列表
使用 Tailwind CSS 绘制星级评分,让界面更专业。
const ReviewList = ({ reviews }) => ( <div className="mt-8 space-y-6"> <h3 className="text-xl font-bold">Customer Reviews ({reviews.length})</h3> {reviews.map((review) => ( <div key={review.id} className="border-b pb-4"> <div className="flex items-center mb-2"> <div className="flex text-yellow-400"> {Array.from({ length: review.rating }).map((_, i) => ( <StarIcon key={i} className="h-5 w-5 fill-current" /> ))} </div> <span className="ml-2 font-semibold">{review.title}</span> </div> <p className="text-gray-600 text-sm mb-1">By {review.user_name}</p> <p className="text-gray-800">{review.content}</p> </div> ))} </div>);总结与避坑指南
- CORS 配置:如果在前端遇到跨域错误,请检查 Medusa Server 的
medusa-config.js中的store_cors是否包含了你的店面 URL。 - 数据验证:我们在后端使用了
class-validator,前端使用了react-hook-form。双重验证能确保非法评分(如 6 分)不会进入数据库。 - 性能优化:当评论数量增多时,建议在
getProductReviews服务中加入分页逻辑。
在 MedusaJS 与 Next.js 店面中实现产品评论功能的完整教程
https://sw.rscclub.website/posts/medusajsnextjs/