面向 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 — 注册新页面文件:
cfc-backend/src/main/java/com/etotem/cfc/controller/product/ProductOrderController.javacfc-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);
}
在 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
- [ ] **步骤 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" ```
cd cfc-backend && mvn clean compile 无报错cfc-frontend 目录重新打包