Просмотр исходного кода

feat(shop.review.create): S-cfc-388 商品评价创建契约使绿(ProductReviewController/Service/Impl/DTO 落地,10 跑 0 失败)

Administrator 12 часов назад
Родитель
Сommit
24db1f48c0

+ 35 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/product/ProductReviewController.java

@@ -0,0 +1,35 @@
+package com.etotem.cfc.controller.product;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.dto.ProductReviewRequest;
+import com.etotem.cfc.service.ProductReviewService;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestAttribute;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.annotation.Resource;
+
+/**
+ * 普通商品评价控制器(能力键 {@code shop.review.create},R1)。
+ *
+ * <p>设计 {@code docs/superpowers/specs/2026-11-14-product-order-gap-arch-design.md} §3.1 R1:
+ * {@code POST /api/product/review/create},鉴权=登录(买家本人),入参
+ * {@code {orderId, rating(1-5), reviewText(≤500), images[]≤3}},出参 {@code Result<String>}。
+ * 对齐 {@code ProductOrderController} 同点位 {@code @RequestAttribute("userId")} 写法。</p>
+ */
+@RestController
+@RequestMapping("/api/product/review")
+public class ProductReviewController {
+
+    @Resource
+    private ProductReviewService reviewService;
+
+    @PostMapping("/create")
+    public Result<String> create(@RequestBody ProductReviewRequest request,
+                                 @RequestAttribute("userId") Long userId) {
+        String result = reviewService.createReview(request, userId);
+        return Result.success(result);
+    }
+}

+ 28 - 0
cfc-backend/src/main/java/com/etotem/cfc/dto/ProductReviewRequest.java

@@ -0,0 +1,28 @@
+package com.etotem.cfc.dto;
+
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * 普通商品订单评价创建入参(能力键 {@code shop.review.create},R1)。
+ *
+ * <p>设计 {@code docs/superpowers/specs/2026-11-14-product-order-gap-arch-design.md} §3.1 R1:
+ * {@code {orderId, rating(1-5), reviewText(≤500), images[]≤3}};晒图 URL 由前端先走
+ * {@code /api/media/upload} 获得。</p>
+ */
+@Data
+public class ProductReviewRequest {
+
+    /** 订单ID(product_orders.id)。 */
+    private Long orderId;
+
+    /** 综合星级 1-5。 */
+    private Integer rating;
+
+    /** 评价文字 ≤500字。 */
+    private String reviewText;
+
+    /** 晒图 URL 数组 ≤3张。 */
+    private List<String> images;
+}

+ 21 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/ProductReviewService.java

@@ -0,0 +1,21 @@
+package com.etotem.cfc.service;
+
+import com.etotem.cfc.dto.ProductReviewRequest;
+
+/**
+ * 普通商品评价服务(能力键 {@code shop.review.create})。
+ *
+ * <p>独立于活动评价 {@link ReviewOrderService}(PRD P0-A:活动链路零改动,不走复用)。
+ * 设计 {@code docs/superpowers/specs/2026-11-14-product-order-gap-arch-design.md} §5.3 类图。</p>
+ */
+public interface ProductReviewService {
+
+    /**
+     * 创建商品订单评价(一单一评,90 天窗口)。
+     *
+     * @param request    R1 入参 {orderId, rating(1-5), reviewText(≤500), images[]≤3}
+     * @param reviewerId 评价人ID(登录买家 userId)
+     * @return 成功消息;校验不通过抛 {@link RuntimeException}
+     */
+    String createReview(ProductReviewRequest request, Long reviewerId);
+}

+ 107 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/ProductReviewServiceImpl.java

@@ -0,0 +1,107 @@
+package com.etotem.cfc.service;
+
+import com.alibaba.fastjson.JSON;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.etotem.cfc.dto.ProductReviewRequest;
+import com.etotem.cfc.entity.ProductOrder;
+import com.etotem.cfc.entity.ProductReview;
+import com.etotem.cfc.mapper.ProductOrderMapper;
+import com.etotem.cfc.mapper.ProductReviewMapper;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.annotation.Resource;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 普通商品评价服务实现(能力键 {@code shop.review.create},R1)。
+ *
+ * <p>设计 {@code docs/superpowers/specs/2026-11-14-product-order-gap-arch-design.md} §3.1 R1 校验链:
+ * 订单存在 / {@code productType != 'activity'}(活动评价零污染)/ {@code status=completed} /
+ * {@code buyerId==userId}(评价人为下单人本人)/ {@code confirmTime >= now-90d}(Q4 窗口)/
+ * {@link ProductReviewMapper#findByOrderId} 幂等(一单一评,表唯一键 {@code uk_order_id})。</p>
+ */
+@Service
+public class ProductReviewServiceImpl extends ServiceImpl<ProductReviewMapper, ProductReview>
+        implements ProductReviewService {
+
+    /** 90 天评价窗口(Q4 默认口径)。 */
+    private static final int REVIEW_WINDOW_DAYS = 90;
+
+    @Resource
+    private ProductReviewMapper reviewMapper;
+
+    @Resource
+    private ProductOrderMapper productOrderMapper;
+
+    @Override
+    @Transactional
+    public String createReview(ProductReviewRequest request, Long reviewerId) {
+        // 入参守卫
+        if (request == null || request.getOrderId() == null) {
+            throw new RuntimeException("订单ID不能为空");
+        }
+        if (request.getRating() == null || request.getRating() < 1 || request.getRating() > 5) {
+            throw new RuntimeException("评价星级必须在 1-5 之间");
+        }
+        if (request.getReviewText() != null && request.getReviewText().length() > 500) {
+            throw new RuntimeException("评价文字不能超过500字");
+        }
+        List<String> images = request.getImages();
+        if (images != null && images.size() > 3) {
+            throw new RuntimeException("晒图最多3张");
+        }
+
+        Long orderId = request.getOrderId();
+
+        // 1) 订单存在
+        ProductOrder order = productOrderMapper.selectById(orderId);
+        if (order == null) {
+            throw new RuntimeException("订单不存在");
+        }
+        // 2) 活动评价零污染(普通商品评价域,活动订单走 ReviewOrderService)
+        if ("activity".equals(order.getProductType())) {
+            throw new RuntimeException("活动订单不支持商品评价");
+        }
+        // 3) 状态 = completed
+        if (!"completed".equals(order.getStatus())) {
+            throw new RuntimeException("订单未完成,暂不可评价");
+        }
+        // 4) 评价人为下单人本人
+        if (order.getBuyerId() == null || !order.getBuyerId().equals(reviewerId)) {
+            throw new RuntimeException("只能评价本人订单");
+        }
+        // 5) 90 天窗口:confirmTime >= now - 90d
+        Date confirmTime = order.getConfirmTime();
+        if (confirmTime == null) {
+            throw new RuntimeException("订单缺少确认收货时间,暂不可评价");
+        }
+        Calendar cal = Calendar.getInstance();
+        cal.add(Calendar.DAY_OF_MONTH, -REVIEW_WINDOW_DAYS);
+        if (confirmTime.before(cal.getTime())) {
+            throw new RuntimeException("评价窗口已过期(确认收货后90天内可评价)");
+        }
+        // 6) 一单一评幂等(findByOrderId 查重,唯一键 uk_order_id 兜底)
+        ProductReview existing = reviewMapper.findByOrderId(orderId);
+        if (existing != null) {
+            throw new RuntimeException("该订单已评价");
+        }
+
+        Date now = new Date();
+        ProductReview review = new ProductReview();
+        review.setOrderId(orderId);
+        review.setProductId(order.getProductId());
+        review.setReviewerId(reviewerId);
+        review.setRating(request.getRating());
+        review.setReviewText(request.getReviewText() != null ? request.getReviewText() : "");
+        review.setImages(images != null && !images.isEmpty() ? JSON.toJSONString(images) : null);
+        review.setStatus(1);
+        review.setReviewAt(now);
+        review.setCreatedAt(now);
+        review.setUpdatedAt(now);
+        reviewMapper.insert(review);
+        return "评价提交成功";
+    }
+}

+ 1 - 0
docs/superpowers/api/API_REFERENCE.md

@@ -669,6 +669,7 @@ find cfc-backend/src/main/java -name "*XxxService.java" -o -name "*XxxController
 | `POST /api/product/order/vendor-confirm` | 服务商确认发出/服务完成 |
 | `POST /api/product/order/ship` | 服务商/管理端发货(填物流单号+快递公司)。发货成功后自动调用微信「小程序交易管理服务」`upload_shipping_info` 同步物流到微信订单管理(用户在微信侧收发货通知/查物流/确认收货);自提订单或无单号跳过;失败只记日志不阻塞(fail-open);`wechat.test-mode=true` 或 `wechat.fulfillment-enabled=false`(环境变量 `WECHAT_FULFILLMENT_ENABLED`)时跳过 |
 | `POST /api/product/order/review/update` | 供应商审核修改待支付订单(修改价格/配送方式/添加优惠券/赠品) |
+| `POST /api/product/review/create` | 创建商品订单评价(能力键 `shop.review.create`;普通商品订单 completed 后一单一评:综合星级1-5+文字≤500字+晒图≤3张、确认收货90天内,独立表 product_reviews,活动评价零污染) |
 | `POST /api/cart/add` | 加入购物车 |
 | `POST /api/cart/list` | 购物车列表 |
 | `POST /api/cart/update` | 更新数量 |