|
|
@@ -0,0 +1,914 @@
|
|
|
+# 供应商订单审核功能 实现计划
|
|
|
+
|
|
|
+> **面向 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` 方法之后添加:
|
|
|
+
|
|
|
+```java
|
|
|
+@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` 方法之后)添加以下方法:
|
|
|
+
|
|
|
+```java
|
|
|
+/**
|
|
|
+ * 供应商审核修改订单(仅 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 行附近新增注入:
|
|
|
+> ```java
|
|
|
+> @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
|
|
|
+<template>
|
|
|
+ <view class="container">
|
|
|
+ <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>
|
|
|
+ </view>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script>
|
|
|
+import { productOrderDetail, productOrderReviewUpdate } from '@/utils/api.js'
|
|
|
+
|
|
|
+export default {
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ order: {},
|
|
|
+ loading: false,
|
|
|
+ orderNo: '',
|
|
|
+ priceInput: false,
|
|
|
+ newUnitPrice: '',
|
|
|
+ deliveryInput: false,
|
|
|
+ newDeliveryMethod: null,
|
|
|
+ couponInput: false,
|
|
|
+ couponValue: '',
|
|
|
+ giftInput: false,
|
|
|
+ newGiftProduct: null,
|
|
|
+ remarkInput: ''
|
|
|
+ }
|
|
|
+ },
|
|
|
+ onLoad(options) {
|
|
|
+ if (options.orderNo) {
|
|
|
+ this.orderNo = options.orderNo
|
|
|
+ this.loadDetail()
|
|
|
+ }
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ loadDetail() {
|
|
|
+ this.loading = true
|
|
|
+ productOrderDetail({ orderNo: this.orderNo }).then(res => {
|
|
|
+ this.loading = false
|
|
|
+ if (res.code === 200 && res.data) {
|
|
|
+ this.order = res.data
|
|
|
+ this._checkVendorPermission()
|
|
|
+ } else {
|
|
|
+ uni.showToast({ title: res.message || '加载失败', icon: 'none' })
|
|
|
+ }
|
|
|
+ }).catch(() => {
|
|
|
+ this.loading = false
|
|
|
+ })
|
|
|
+ },
|
|
|
+ _checkVendorPermission() {
|
|
|
+ // 此处假设返回数据中有 vendorId 字段,若无则在后端 detail 接口补充
|
|
|
+ // 简化处理:订单状态非 pending 或无商品供应商信息则提示
|
|
|
+ if (!this.order.vendorId) {
|
|
|
+ // 后端需在 detail 接口中补充 vendorId 返回
|
|
|
+ // 这里先放行,由后端接口校验权限
|
|
|
+ }
|
|
|
+ },
|
|
|
+ showPriceModal() {
|
|
|
+ this.priceInput = true
|
|
|
+ this.newUnitPrice = String(this.order.unitPrice ? Math.round(this.order.unitPrice / 100) : '')
|
|
|
+ },
|
|
|
+ hidePriceModal() {
|
|
|
+ this.priceInput = false
|
|
|
+ this.newUnitPrice = ''
|
|
|
+ },
|
|
|
+ showDeliveryModal() {
|
|
|
+ this.deliveryInput = true
|
|
|
+ },
|
|
|
+ hideDeliveryModal() {
|
|
|
+ this.deliveryInput = false
|
|
|
+ this.newDeliveryMethod = null
|
|
|
+ },
|
|
|
+ showCouponModal() {
|
|
|
+ this.couponInput = true
|
|
|
+ this.couponValue = ''
|
|
|
+ },
|
|
|
+ hideCouponModal() {
|
|
|
+ this.couponInput = false
|
|
|
+ this.couponValue = ''
|
|
|
+ },
|
|
|
+ showGiftModal() {
|
|
|
+ this.giftInput = true
|
|
|
+ this.newGiftProduct = null
|
|
|
+ },
|
|
|
+ hideGiftModal() {
|
|
|
+ this.giftInput = false
|
|
|
+ this.newGiftProduct = null
|
|
|
+ },
|
|
|
+ onSave() {
|
|
|
+ var params = { orderNo: this.orderNo }
|
|
|
+
|
|
|
+ // 价格修改
|
|
|
+ if (this.priceInput && this.newUnitPrice) {
|
|
|
+ params.unitPrice = parseInt(this.newUnitPrice) * 100 // 转为分
|
|
|
+ }
|
|
|
+
|
|
|
+ // 配送方式修改
|
|
|
+ if (this.deliveryInput && this.newDeliveryMethod != null) {
|
|
|
+ params.deliveryMethod = this.newDeliveryMethod
|
|
|
+ }
|
|
|
+
|
|
|
+ // 优惠券添加
|
|
|
+ if (this.couponInput && this.couponValue && parseInt(this.couponValue) > 0) {
|
|
|
+ params.couponType = 'ONE_TIME'
|
|
|
+ params.couponValue = parseInt(this.couponValue) * 100
|
|
|
+ }
|
|
|
+
|
|
|
+ // 赠品添加
|
|
|
+ if (this.giftInput && this.newGiftProduct) {
|
|
|
+ if (!params.giftItems) params.giftItems = []
|
|
|
+ params.giftItems.push({
|
|
|
+ giftType: 'product',
|
|
|
+ giftProductId: this.newGiftProduct.id,
|
|
|
+ giftProductName: this.newGiftProduct.name,
|
|
|
+ giftProductImage: this.newGiftProduct.coverImage,
|
|
|
+ quantity: 1
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ // 备注
|
|
|
+ if (this.remarkInput) {
|
|
|
+ params.remark = this.remarkInput
|
|
|
+ }
|
|
|
+
|
|
|
+ // 无修改
|
|
|
+ if (!params.unitPrice && !params.deliveryMethod && !params.couponValue && !params.giftItems) {
|
|
|
+ uni.showToast({ title: '请至少修改一项', icon: 'none' })
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ uni.showLoading({ title: '保存中...' })
|
|
|
+ productOrderReviewUpdate(params).then(res => {
|
|
|
+ uni.hideLoading()
|
|
|
+ if (res.code === 200) {
|
|
|
+ uni.showToast({ title: '修改已保存', icon: 'success' })
|
|
|
+ setTimeout(() => {
|
|
|
+ uni.navigateBack()
|
|
|
+ }, 1500)
|
|
|
+ } else {
|
|
|
+ uni.showToast({ title: res.message || '保存失败', icon: 'none' })
|
|
|
+ }
|
|
|
+ }).catch(() => {
|
|
|
+ uni.hideLoading()
|
|
|
+ uni.showToast({ title: '网络错误', icon: 'none' })
|
|
|
+ })
|
|
|
+ },
|
|
|
+ statusLabel(status) {
|
|
|
+ var map = { pending: '待支付', paid: '已支付', completed: '已完成', cancelled: '已取消' }
|
|
|
+ return map[status] || status
|
|
|
+ },
|
|
|
+ deliveryLabel(method) {
|
|
|
+ var map = { 1: '快递配送', 2: '自提', 3: '快递/自提可选', 4: '线上-可选人', 5: '线上-单人' }
|
|
|
+ return map[method] || '未知'
|
|
|
+ },
|
|
|
+ formatPrice(value) {
|
|
|
+ if (!value && value !== 0) return '-'
|
|
|
+ return (value / 100).toFixed(2)
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.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;
|
|
|
+}
|
|
|
+</style>
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 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)"
|
|
|
+>审核修改</button>
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 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 已设置
|
|
|
+ - 测试:添加赠品 → 保存 → 确认赠品已记录
|