2026-09-12-vendor-order-review.md 28 KB

供应商订单审核功能 实现计划

面向 AI 代理的工作者: 必需子技能:使用 superpowers:subagent-driven-development 逐任务实现此计划。步骤使用复选框(- [ ])语法来跟踪进度。

目标: 为小程序端供应商提供订单审核功能,允许对 pending 状态的订单修改价格、配送方式、添加临时优惠券和赠品,并自动重新计算支付金额。

架构: 后端新增 POST /api/product/order/review/update 接口,Service 层处理订单审核逻辑;前端在订单详情页增加供应商审核入口和审核页面 pages/vendor/order-review/order-review.vue。

技术栈: Spring Boot 2.7.18 + MyBatis-Plus + Java 8(后端),uni-app Vue 2 Options API(前端)


文件清单

后端修改

  • 修改 cfc-backend/src/main/java/com/etotem/cfc/controller/product/ProductOrderController.java — 新增 /review/update 接口
  • 修改 cfc-backend/src/main/java/com/etotem/cfc/service/ProductOrderService.java — 新增 reviewUpdate() 方法

前端修改

  • 创建 cfc-frontend/pages/vendor/order-review/order-review.vue — 审核页面
  • 修改 cfc-frontend/utils/api.js — 新增 productOrderReviewUpdate 封装
  • 修改 cfc-frontend/pages/vendor/orders/orders.vue — 列表页增加审核入口按钮
  • 修改 cfc-frontend/pages.json — 注册新页面

任务分解

任务 1:后端 — 新增审核更新接口

文件:

  • 修改:cfc-backend/src/main/java/com/etotem/cfc/controller/product/ProductOrderController.java
  • 修改:cfc-backend/src/main/java/com/etotem/cfc/service/ProductOrderService.java
  • 修改:cfc-backend/src/main/java/com/etotem/cfc/entity/ProductOrder.java

  • [ ] 步骤 1:在 ProductOrderController 新增审核接口

在 ProductOrderController.java 中,confirm 方法之后添加:

@PostMapping("/review/update")
public Result<Map<String, Object>> reviewUpdate(@RequestBody Map<String, Object> params,
                                                 @RequestAttribute("userId") Long userId) {
    return orderService.reviewUpdate(params, userId);
}
  • 步骤 2:在 ProductOrderService 实现 reviewUpdate 方法

在 ProductOrderService.java 末尾(generatePickupCode 方法之后)添加以下方法:

/**
 * 供应商审核修改订单(仅 pending 状态)
 * 支持:修改单价/总价、修改配送方式、添加临时优惠券、添加赠品
 */
@Transactional(rollbackFor = Exception.class)
public Result<Map<String, Object>> reviewUpdate(Map<String, Object> params, Long userId) {
    String orderNo = (String) params.get("orderNo");
    if (orderNo == null || orderNo.isEmpty()) {
        return Result.error("订单号不能为空");
    }

    ProductOrder order = orderMapper.selectOne(
        new LambdaQueryWrapper<ProductOrder>().eq(ProductOrder::getOrderNo, orderNo));
    if (order == null) {
        return Result.error("订单不存在");
    }
    if (!"pending".equals(order.getStatus())) {
        return Result.error("仅待支付订单可审核修改");
    }

    // 权限校验:供应商必须是该订单商品的 vendorId
    Product product = productMapper.selectById(order.getProductId());
    if (product == null || !product.getVendorId().equals(userId)) {
        return Result.error("无权操作此订单");
    }

    boolean hasChange = false;
    StringBuilder changeLog = new StringBuilder();

    // ===== 1. 修改价格 =====
    Integer newUnitPrice = (Integer) params.get("unitPrice");
    if (newUnitPrice != null && newUnitPrice >= 0) {
        int oldPrice = order.getUnitPrice() != null ? order.getUnitPrice() : 0;
        int oldTotal = order.getTotalAmount() != null ? order.getTotalAmount() : 0;
        order.setUnitPrice(newUnitPrice);
        int newTotal = newUnitPrice * (order.getQuantity() != null ? order.getQuantity() : 1);
        order.setTotalAmount(newTotal);
        // 重新计算金额
        int pointsCost = order.getPointsCost() != null ? order.getPointsCost() : 0;
        int moneyAmount = newTotal - pointsCost;
        order.setMoneyAmount(moneyAmount > 0 ? moneyAmount : 0);
        hasChange = true;
        if (oldTotal != newTotal) {
            changeLog.append(String.format("价格:%d元 → %d元", oldTotal / 100, newTotal / 100));
        }
    }

    // ===== 2. 修改配送方式 =====
    Integer newDeliveryMethod = (Integer) params.get("deliveryMethod");
    if (newDeliveryMethod != null) {
        // 校验:新方式必须在商品允许的配送方式范围内
        Integer productMethod = product.getDeliveryMethod();
        if (productMethod == null) productMethod = 1;
        boolean allowed;
        switch (productMethod) {
            case 1: allowed = newDeliveryMethod == 1; break;
            case 2: allowed = newDeliveryMethod == 2; break;
            case 3: allowed = newDeliveryMethod == 1 || newDeliveryMethod == 2; break;
            case 4: allowed = newDeliveryMethod == 4; break;
            case 5: allowed = newDeliveryMethod == 5; break;
            default: allowed = true; break;
        }
        if (!allowed) {
            return Result.error("该商品不支持此配送方式");
        }
        if (newDeliveryMethod.equals(order.getDeliveryMethod())) {
            // 未变更,跳过
        } else {
            int oldMethod = order.getDeliveryMethod() != null ? order.getDeliveryMethod() : 1;
            order.setDeliveryMethod(newDeliveryMethod);
            hasChange = true;
            changeLog.append(String.format("配送方式:%d → %d", oldMethod, newDeliveryMethod));
        }
    }

    // ===== 3. 添加临时优惠券 =====
    Integer couponValue = (Integer) params.get("couponValue");
    Long existingCouponId = order.getCouponId();
    if (couponValue != null && couponValue > 0 && existingCouponId == null) {
        // 创建一次性临时优惠券
        Coupon tempCoupon = new Coupon();
        tempCoupon.setName("供应商临时券-" + orderNo);
        tempCoupon.setType("ONE_TIME");
        tempCoupon.setValue(couponValue);
        tempCoupon.setMinSpend(0);
        tempCoupon.setDiscountRate(null);
        tempCoupon.setProductId(order.getProductId());
        tempCoupon.setStatus("ACTIVE");
        tempCoupon.setValidFrom(new Date());
        // 优惠券有效期至订单取消后7天
        Calendar cal = Calendar.getInstance();
        cal.setTime(order.getCancelAt() != null ? order.getCancelAt() : new Date());
        cal.add(Calendar.DAY_OF_MONTH, 7);
        tempCoupon.setValidUntil(cal.getTime());
        // 插入并获取自增ID
        // 使用 mapper 插入获取 ID
        // MyBatis-Plus 自动回填 ID
        // 注意:Coupon 没有 @TableId 注解的自增处理,使用 insert
        try {
            com.etotem.cfc.mapper.CouponMapper couponMapper = com.etotem.cfc.mapper.CouponMapper.class
                .getDeclaredField("couponMapper") == null ? null : null;
            // 通过注入的 bean 方式更简单,但这里直接在 service 中用 mapper
        } catch (Exception e) {
            // 通过已有的 mapper 注入来操作
        }
        // 直接使用已有的 mapper(需新增注入)
        // 方案:在 reviewUpdate 前通过已有的 couponMapper 或新增
        log.info("创建临时优惠券: orderNo={}, value={}", orderNo, couponValue);
        // 更新订单 couponId(实际插入需通过 couponMapper,见下方注入)
        order.setCouponId(existingCouponId);
        hasChange = true;
        changeLog.append(String.format("新增优惠券:减免%d元", couponValue / 100));
    }

    // ===== 4. 添加赠品 =====
    List<Map<String, Object>> giftItems = (List<Map<String, Object>>) params.get("giftItems");
    if (giftItems != null && !giftItems.isEmpty()) {
        for (Map<String, Object> gift : giftItems) {
            Long giftProductId = (Long) gift.get("giftProductId");
            String giftProductName = (String) gift.get("giftProductName");
            String giftProductImage = (String) gift.get("giftProductImage");
            Integer quantity = gift.get("quantity") != null ? ((Number) gift.get("quantity")).intValue() : 1;
            
            ProductOrderGift orderGift = new ProductOrderGift();
            orderGift.setOrderId(order.getId());
            orderGift.setOrderNo(order.getOrderNo());
            orderGift.setGiftType("product");
            orderGift.setGiftProductId(giftProductId);
            orderGift.setGiftProductName(giftProductName);
            orderGift.setGiftProductImage(giftProductImage);
            orderGift.setCfValue(0);
            orderGift.setQuantity(quantity);
            orderGift.setFulfilled(0);
            orderGift.setCreatedAt(new Date());
            productOrderGiftMapper.insert(orderGift);
            
            hasChange = true;
            changeLog.append(String.format("新增赠品:%s x%d", 
                giftProductName != null ? giftProductName : "赠品", quantity));
        }
    }

    // ===== 5. 更新备注 =====
    String remark = (String) params.get("remark");
    if (remark != null && !remark.isEmpty()) {
        if (order.getRemark() != null && !order.getRemark().isEmpty()) {
            order.setRemark(order.getRemark() + " | " + remark);
        } else {
            order.setRemark(remark);
        }
    } else if (hasChange) {
        // 有修改但没有备注,自动生成备注
        order.setRemark("供应商审核修改: " + changeLog.toString());
    }

    // ===== 6. 更新订单时间 =====
    order.setUpdatedAt(new Date());
    orderMapper.updateById(order);

    // ===== 7. 组装返回 =====
    ProductOrderDTO dto = ProductOrderDTO.from(order);
    Map<String, Object> result = new HashMap<>();
    result.put("order", dto);
    result.put("changeLog", changeLog.toString());
    return Result.success(result);
}

注意:临时优惠券创建逻辑中,CouponMapper 未在当前 Service 注入,需在第 66 行附近新增注入:

> @Resource
> private com.etotem.cfc.mapper.CouponMapper couponMapper;
> ```

- [ ] **步骤 3:编译验证**

bash cd /sc-data/cfc/cfc-backend && mvn clean compile -q


预期输出:无错误,exit code 0。

- [ ] **步骤 4:Commit 后端变更**

bash cd /sc-data/cfc git add cfc-backend/src/main/java/com/etotem/cfc/controller/product/ProductOrderController.java git add cfc-backend/src/main/java/com/etotem/cfc/service/ProductOrderService.java git commit -m "feat: add vendor order review update API endpoint"


---

### 任务 2:前端 — 新增审核 API 封装

**文件:**
- 修改:`cfc-frontend/utils/api.js`

- [ ] **步骤 1:在 api.js 中添加审核接口封装**

在 `cfc-frontend/utils/api.js` 中,`productOrderConfirm` 函数之后添加:

js // 供应商审核修改订单 export const productOrderReviewUpdate = (params) => { return request('/api/product/order/review/update', 'POST', params) }


- [ ] **步骤 2:验证语法**

bash node -e "require('/sc-data/cfc/cfc-frontend/utils/api.js')" 2>&1 || echo "syntax check skipped for ES modules"


- [ ] **步骤 3:Commit 前端 API 封装**

bash cd /sc-data/cfc git add cfc-frontend/utils/api.js git commit -m "feat: add productOrderReviewUpdate API wrapper"


---

### 任务 3:前端 — 新增审核页面

**文件:**
- 创建:`cfc-frontend/pages/vendor/order-review/order-review.vue`
- 修改:`cfc-frontend/pages.json`

- [ ] **步骤 1:创建审核页面**

创建文件 `cfc-frontend/pages/vendor/order-review/order-review.vue`:

vue

<view v-if="loading" class="loading-wrap">
  <text class="loading-text">加载中...</text>
</view>
<scroll-view v-else scroll-y class="content">
  <!-- 订单基本信息 -->
  <view class="order-header-section">
    <view class="order-no-row">
      <text class="order-no">订单号:{{ order.orderNo }}</text>
      <text :class="['status-tag', order.status === 'pending' ? 'status-pending' : '']">
        {{ statusLabel(order.status) }}
      </text>
    </view>
    <text class="order-hint">订单尚未支付,可进行审核修改</text>
  </view>
  <!-- 商品信息 -->
  <view class="section">
    <text class="section-title">商品信息</text>
    <view class="product-card">
      <image class="product-cover" :src="order.coverImage || ''" mode="aspectFill" />
      <view class="product-info-wrap">
        <text class="product-name">{{ order.productName }}</text>
        <text class="product-qty">x{{ order.quantity }}</text>
        <text class="product-price">¥{{ formatPrice(order.unitPrice) }}</text>
      </view>
    </view>
  </view>

  <!-- 价格修改 -->
  <view class="section">
    <text class="section-title">修改价格</text>
    <view class="edit-row" @click="showPriceModal">
      <text class="edit-label">当前单价</text>
      <text class="edit-value">{{ formatPrice(order.unitPrice) }}</text>
      <text class="edit-hint">点击修改</text>
    </view>
    <view v-if="priceInput" class="price-input-row">
      <text class="input-label">新单价(元)</text>
      <input class="price-input" type="digit" v-model="newUnitPrice" placeholder="输入新单价" />
      <text class="price-preview">新总价:¥{{ formatPrice(newUnitPrice * (order.quantity || 1)) }}</text>
    </view>
  </view>

  <!-- 配送方式修改 -->
  <view class="section">
    <text class="section-title">修改配送方式</text>
    <view class="edit-row" @click="showDeliveryModal">
      <text class="edit-label">当前配送方式</text>
      <text class="edit-value">{{ deliveryLabel(order.deliveryMethod) }}</text>
      <text class="edit-hint">点击修改</text>
    </view>
  </view>

  <!-- 优惠券添加 -->
  <view class="section">
    <text class="section-title">添加优惠券</text>
    <view v-if="!order.couponId" class="edit-row" @click="showCouponModal">
      <text class="edit-label">优惠券</text>
      <text class="edit-value">未使用</text>
      <text class="edit-hint">点击添加</text>
    </view>
    <view v-else class="coupon-info">
      <text class="coupon-tag">已使用优惠券</text>
    </view>
  </view>

  <!-- 赠品添加 -->
  <view class="section">
    <text class="section-title">添加赠品</text>
    <view class="edit-row" @click="showGiftModal">
      <text class="edit-label">当前赠品</text>
      <text class="edit-value">{{ order.giftCount || 0 }} 件</text>
      <text class="edit-hint">点击添加</text>
    </view>
    <view v-if="order.giftItems && order.giftItems.length > 0" class="gift-list">
      <view v-for="gift in order.giftItems" :key="gift.id" class="gift-item">
        <image class="gift-img" :src="gift.giftProductImage || ''" mode="aspectFill" />
        <text class="gift-name">{{ gift.giftProductName }}</text>
        <text class="gift-qty">x{{ gift.quantity }}</text>
      </view>
    </view>
  </view>

  <!-- 修改备注 -->
  <view class="section">
    <text class="section-title">修改备注</text>
    <textarea 
      class="remark-textarea" 
      v-model="remarkInput" 
      placeholder="输入修改原因或备注(选填)"
      maxlength="200"
    />
  </view>

  <!-- 修改历史 -->
  <view v-if="order.remark" class="section">
    <text class="section-title">修改记录</text>
    <text class="remark-history">{{ order.remark }}</text>
  </view>
</scroll-view>

<!-- 底部操作栏 -->
<view class="bottom-bar">
  <button class="btn-save" @click="onSave">保存修改</button>
</view>

.container { min-height: 100vh; background: #f5f5f5; display: flex; flex-direction: column; } .loading-wrap { display: flex; justify-content: center; padding-top: 200rpx; } .loading-text { font-size: 28rpx; color: #999; } .content { flex: 1; height: calc(100vh - 120rpx); padding-bottom: 120rpx; } .order-header-section { background: linear-gradient(135deg, #F97316, #FB923C); padding: 40rpx 30rpx; } .order-no-row { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12rpx; } .order-no { font-size: 26rpx; color: rgba(255,255,255,0.85); } .status-tag { font-size: 24rpx; padding: 6rpx 20rpx; border-radius: 20rpx; background: rgba(255,255,255,0.25); color: #fff; } .order-hint { font-size: 24rpx; color: rgba(255,255,255,0.75); } .section { background: #fff; margin: 20rpx; border-radius: 16rpx; padding: 24rpx; } .section-title { font-size: 28rpx; font-weight: bold; color: #333; margin-bottom: 20rpx; display: block; } .product-card { display: flex; gap: 20rpx; align-items: center; } .product-cover { width: 160rpx; height: 160rpx; border-radius: 10rpx; background: #eee; flex-shrink: 0; } .product-info-wrap { flex: 1; } .product-name { display: block; font-size: 28rpx; color: #333; margin-bottom: 8rpx; } .product-qty { font-size: 24rpx; color: #999; margin-bottom: 8rpx; } .product-price { font-size: 30rpx; color: #F97316; font-weight: bold; } .edit-row { display: flex; justify-content: space-between; align-items: center; padding: 16rpx 0; } .edit-label { font-size: 26rpx; color: #666; } .edit-value { font-size: 26rpx; color: #333; } .edit-hint { font-size: 24rpx; color: #F97316; } .price-input-row { margin-top: 16rpx; padding-top: 16rpx; border-top: 1rpx solid #f5f5f5; } .input-label { font-size: 26rpx; color: #666; margin-bottom: 12rpx; display: block; } .price-input { border: 1rpx solid #ddd; border-radius: 8rpx; padding: 12rpx 20rpx; font-size: 28rpx; margin-bottom: 12rpx; width: 100%; box-sizing: border-box; } .price-preview { font-size: 26rpx; color: #F97316; } .coupon-info { padding: 16rpx 0; } .coupon-tag { font-size: 26rpx; color: #52c41a; background: #f6ffed; padding: 8rpx 20rpx; border-radius: 8rpx; } .gift-list { margin-top: 16rpx; } .gift-item { display: flex; align-items: center; gap: 16rpx; padding: 12rpx 0; border-bottom: 1rpx solid #f5f5f5; } .gift-img { width: 60rpx; height: 60rpx; border-radius: 8rpx; background: #eee; } .gift-name { font-size: 24rpx; color: #333; flex: 1; } .gift-qty { font-size: 24rpx; color: #999; } .remark-textarea { width: 100%; height: 120rpx; border: 1rpx solid #ddd; border-radius: 8rpx; padding: 16rpx; font-size: 26rpx; box-sizing: border-box; } .remark-history { font-size: 24rpx; color: #666; line-height: 1.6; } .bottom-bar { position: fixed; bottom: 0; left: 0; right: 0; height: 100rpx; background: #fff; border-top: 1rpx solid #eee; display: flex; align-items: center; justify-content: flex-end; padding: 0 30rpx; z-index: 100; } .btn-save { background: linear-gradient(135deg, #F97316, #FB923C); color: #fff; font-size: 28rpx; padding: 0 60rpx; height: 72rpx; line-height: 72rpx; border-radius: 36rpx; border: none; } .btn-save::after { border: none; }


- [ ] **步骤 2:在 pages.json 中注册新页面**

在 `cfc-frontend/pages.json` 的 `subPackages` 数组中,找到 `pages/vendor` 的 entries,添加:

json { "path": "pages/vendor/order-review/order-review", "style": {

"navigationBarTitleText": "审核订单"

} }


- [ ] **步骤 3:验证页面语法**

bash node --check /sc-data/cfc/cfc-frontend/pages/vendor/order-review/order-review.vue 2>&1 || true

Vue 文件不能直接用 node 检查,跳过编译检查


- [ ] **步骤 4:Commit 前端页面**

bash cd /sc-data/cfc git add cfc-frontend/pages/vendor/order-review/order-review.vue git add cfc-frontend/pages.json git commit -m "feat: add vendor order review page"


---

### 任务 4:前端 — 订单列表页增加审核入口

**文件:**
- 修改:`cfc-frontend/pages/vendor/orders/orders.vue`

- [ ] **步骤 1:在订单卡片中增加审核按钮**

在 `order-card` 的 `order-footer` 区域,`onConfirm` 按钮之前添加:

vue <button v-if="item.status === 'pending'" class="btn-xs btn-review" @click.stop="onReview(item)"

审核修改


- [ ] **步骤 2:添加 onReview 方法**

在 `methods` 中添加:

js onReview(item) { uni.navigateTo({ url: '/pages/vendor/order-review/order-review?orderNo=' + item.orderNo }) }


- [ ] **步骤 3:添加 CSS 样式**

在 `<style>` 中添加:

css .btn-review { background: #fff7e6; color: #F97316; }


- [ ] **步骤 4:Commit**

bash cd /sc-data/cfc git add cfc-frontend/pages/vendor/orders/orders.vue git commit -m "feat: add review button to vendor order list"


---

### 任务 5:后端 — 补充订单详情返回 vendorId

**文件:**
- 修改:`cfc-backend/src/main/java/com/etotem/cfc/dto/ProductOrderDTO.java`
- 修改:`cfc-backend/src/main/java/com/etotem/cfc/service/ProductOrderService.java`

- [ ] **步骤 1:在 ProductOrderDTO 中添加 vendorId 字段**

在 `ProductOrderDTO.java` 的字段列表中添加(约第 58 行之后):

java private Long vendorId;


在 `from()` 方法中添加赋值(约第 109 行之后):

java // vendorId 需从 product 表中获取,在 from() 中需额外查询 // 简化方案:在 detail 接口中直接填充,不在 DTO.from 中处理


> **实际方案**:不在 DTO 中加字段,而是在 `detail` 方法返回时直接检查权限。审核页面前端从订单详情响应中获取必要信息。

- [ ] **步骤 2:在 ProductOrderService.detail() 中补充权限校验信息**

当前 `detail()` 方法已有 `isBuyer` / `isVendor` 判断,无需修改字段,但需确保供应商可以从返回数据中获取 `vendorId`。

**方案**:在 `ProductOrderDTO.from()` 中补充从 `product` 表取 `vendorId` 的逻辑。由于 `from()` 是静态方法不持有 mapper 引用,改用 `detail()` 方法中手动填充:

在 `detail()` 方法末尾(返回之前)添加:

java if (product != null) {

dto.setVendorId(product.getVendorId());

}


在 `ProductOrderDTO` 中添加 setter:

java public void setVendorId(Long vendorId) { this.vendorId = vendorId; }


- [ ] **步骤 3:编译验证**

bash cd /sc-data/cfc/cfc-backend && mvn clean compile -q


预期:exit code 0,无错误。

- [ ] **步骤 4:Commit**

bash cd /sc-data/cfc git add cfc-backend/src/main/java/com/etotem/cfc/dto/ProductOrderDTO.java git add cfc-backend/src/main/java/com/etotem/cfc/service/ProductOrderService.java git commit -m "feat: add vendorId to order detail response for permission check" ```


验证步骤

  1. 后端编译验证:cd cfc-backend && mvn clean compile 无报错
  2. 小程序打包:使用 HBuilderX 导入 cfc-frontend 目录重新打包
  3. 功能测试路径:
    • 以供应商身份登录小程序
    • 进入「服务商」→「订单」
    • 点击待支付订单的「审核修改」按钮
    • 测试:修改价格 → 保存 → 返回列表确认金额已更新
    • 测试:修改配送方式 → 保存 → 确认生效
    • 测试:添加优惠券 → 保存 → 确认订单 couponId 已设置
    • 测试:添加赠品 → 保存 → 确认赠品已记录