Преглед на файлове

feat: add pointsUsed/moneyAmount to ProductOrderDTO + checkout points UI + noMoneyPayment handling

Xiaogang Liao преди 2 месеца
родител
ревизия
da960835a9

+ 6 - 0
cfc-backend/src/main/java/com/etotem/cfc/dto/ProductOrderDTO.java

@@ -23,6 +23,9 @@ public class ProductOrderDTO {
     private String remark;
     private Integer discountAmount;
     private Long couponId;
+    private Integer pointsUsed;
+    private Integer pointsCost;
+    private Integer moneyAmount;
     private Date cancelAt;
     private String transactionId;
     private String coverImage;
@@ -49,6 +52,9 @@ public class ProductOrderDTO {
         d.remark = o.getRemark();
         d.discountAmount = o.getDiscountAmount();
         d.couponId = o.getCouponId();
+        d.pointsUsed = o.getPointsUsed();
+        d.pointsCost = o.getPointsCost();
+        d.moneyAmount = o.getMoneyAmount();
         d.cancelAt = o.getCancelAt();
         d.transactionId = o.getTransactionId();
         d.coverImage = o.getCoverImage();

+ 115 - 4
cfc-frontend/pages/shop/checkout/checkout.vue

@@ -68,6 +68,28 @@
         </view>
       </view>
 
+      <!-- Points Deduction Section -->
+      <view class="section" v-if="userTotalPoints > 0">
+        <view class="info-row">
+          <text class="info-label">积分抵扣</text>
+          <text class="points-balance">可用 {{ userTotalPoints }} 积分</text>
+        </view>
+        <view class="points-input-row">
+          <input
+            class="points-input"
+            type="number"
+            :value="pointsUsed"
+            @input="onPointsInput"
+            placeholder="输入使用积分"
+            placeholder-style="color: #ccc; font-size: 26rpx;"
+          />
+          <text class="points-hint">≈ ¥{{ pointsYuanEquivalent }}</text>
+        </view>
+        <view class="points-tip">
+          <text class="points-tip-text">1积分 = ¥0.01,最多使用 {{ maxPoints }} 积分</text>
+        </view>
+      </view>
+
       <!-- Remark Section -->
       <view class="section">
         <view class="info-row">
@@ -151,7 +173,10 @@ export default {
       actualAmount: 0,
       coupons: [],
       selectedCoupon: null,
-      showCouponPicker: false
+      showCouponPicker: false,
+      pointsUsed: 0,
+      userTotalPoints: 0,
+      pointsLoading: false
     }
   },
   computed: {
@@ -159,10 +184,25 @@ export default {
       var total = (this.actualAmount || 0) + (this.freight || 0)
       if (this.selectedCoupon) {
         total = total - this.selectedCoupon.value
-        if (total < 0) total = 0
+      }
+      total = total - (this.pointsUsed || 0)
+      if (total < 0) total = 0
+      return total
+    },
+    maxPoints() {
+      var total = (this.actualAmount || 0) + (this.freight || 0)
+      if (this.selectedCoupon) {
+        total = total - this.selectedCoupon.value
+      }
+      if (total < 0) total = 0
+      if (this.userTotalPoints < total) {
+        return this.userTotalPoints
       }
       return total
     },
+    pointsYuanEquivalent() {
+      return (this.pointsUsed / 100).toFixed(2)
+    },
     availableCoupons() {
       var self = this
       return this.coupons.filter(function(c) {
@@ -196,6 +236,7 @@ export default {
     }
     this.loadDefaultAddress()
     this.loadCoupons()
+    this.loadUserPoints()
   },
   methods: {
     calcAmount() {
@@ -231,6 +272,16 @@ export default {
       this.selectedCoupon = null
       this.showCouponPicker = false
     },
+    onPointsInput(e) {
+      var val = parseInt(e.detail.value) || 0
+      if (val < 0) val = 0
+      var maxP = this.maxPoints
+      if (val > maxP) {
+        val = maxP
+        uni.showToast({ title: '最多使用 ' + maxP + ' 积分', icon: 'none' })
+      }
+      this.pointsUsed = val
+    },
     getConditionText(minSpend) {
       if (!minSpend || minSpend <= 0) return '无门槛'
       return '满' + (minSpend / 100).toFixed(2) + '元可用'
@@ -285,6 +336,23 @@ export default {
     goAddAddress() {
       uni.navigateTo({ url: '/pages/shop/address-edit/address-edit' })
     },
+    loadUserPoints() {
+      var that = this
+      uni.request({
+        url: config.api('/api/user/info'),
+        method: 'POST',
+        data: {},
+        header: {
+          'Content-Type': 'application/json',
+          'Authorization': 'Bearer ' + uni.getStorageSync('token')
+        },
+        success: function(res) {
+          if (res.data && res.data.code === 200) {
+            that.userTotalPoints = res.data.data.totalPoints || 0
+          }
+        }
+      })
+    },
     onSubmit() {
       if (!this.selectedAddress) {
         uni.showToast({ title: '请选择收货地址', icon: 'none' })
@@ -321,7 +389,8 @@ export default {
         data: {
           items: orderItems,
           remark: this.remark,
-          addressSnapshot: addressSnapshot
+          addressSnapshot: addressSnapshot,
+          pointsUsed: that.pointsUsed
         },
         header: {
           'Content-Type': 'application/json',
@@ -333,9 +402,11 @@ export default {
             var order = res.data.data
             uni.showToast({ title: '下单成功', icon: 'success' })
             uni.removeStorageSync('checkoutItems')
+            var moneyAmount = order.moneyAmount != null ? order.moneyAmount : order.totalAmount
+            var pointsUsed = order.pointsUsed || 0
             setTimeout(function() {
               uni.navigateTo({
-                url: '/pages/shop/payment/payment?orderNo=' + order.orderNo + '&amount=' + order.totalAmount
+                url: '/pages/shop/payment/payment?orderNo=' + order.orderNo + '&amount=' + (moneyAmount / 100).toFixed(2) + '&pointsUsed=' + pointsUsed
               })
             }, 1000)
           } else {
@@ -743,4 +814,44 @@ export default {
   background: #ddd;
   color: #999;
 }
+.points-balance {
+  font-size: 24rpx;
+  color: #F97316;
+}
+.points-input-row {
+  display: flex;
+  align-items: center;
+  margin-top: 16rpx;
+  padding: 12rpx 0;
+  border-top: 1rpx solid #f5f5f5;
+}
+.points-input {
+  flex: 1;
+  height: 60rpx;
+  font-size: 28rpx;
+  color: #333;
+  background: #f9f9f9;
+  border-radius: 8rpx;
+  padding: 0 16rpx;
+}
+.points-hint {
+  font-size: 24rpx;
+  color: #999;
+  margin-left: 12rpx;
+  min-width: 100rpx;
+  text-align: right;
+}
+.points-tip {
+  margin-top: 8rpx;
+}
+.points-tip-text {
+  font-size: 22rpx;
+  color: #bbb;
+}
+.total-original {
+  font-size: 24rpx;
+  color: #bbb;
+  text-decoration: line-through;
+  margin-left: 8rpx;
+}
 </style>

+ 59 - 20
cfc-frontend/pages/shop/payment/payment.vue

@@ -15,17 +15,21 @@
         </text>
       </view>
 
-      <!-- Order Info Card -->
-      <view class="section">
-        <view class="info-row">
-          <text class="info-label">订单编号</text>
-          <text class="info-value">{{ orderNo }}</text>
-        </view>
-        <view class="info-row">
-          <text class="info-label">订单金额</text>
-          <text class="order-amount">{{ formatPriceWithSymbol(amount) }}</text>
-        </view>
+    <!-- Order Info Card -->
+    <view class="section">
+      <view class="info-row">
+        <text class="info-label">订单编号</text>
+        <text class="info-value">{{ orderNo }}</text>
+      </view>
+      <view class="info-row" v-if="pointsUsed > 0">
+        <text class="info-label">积分抵扣</text>
+        <text class="info-value points-value">-{{ formatPrice(pointsUsed) }} ({{ pointsUsed }}积分)</text>
+      </view>
+      <view class="info-row">
+        <text class="info-label">订单金额</text>
+        <text class="order-amount">{{ formatPriceWithSymbol(amount) }}</text>
       </view>
+    </view>
 
       <!-- Payment Method -->
       <view class="section">
@@ -65,7 +69,9 @@ export default {
       countdown: 900,
       paying: false,
       loading: false,
-      timer: null
+      timer: null,
+      pointsUsed: 0,
+      pointsUsed: 0
     }
   },
   computed: {
@@ -82,6 +88,9 @@ export default {
     if (options.amount) {
       this.amount = parseFloat(options.amount)
     }
+    if (options.pointsUsed) {
+      this.pointsUsed = parseInt(options.pointsUsed)
+    }
     this.startCountdown()
   },
   onUnload() {
@@ -122,8 +131,35 @@ export default {
         success: function(res) {
           that.paying = false
           if (res.data && res.data.code === 200) {
-            var params = res.data.data && res.data.data.wechatPayParams
-            if (params) {
+            var data = res.data.data
+            if (data.noMoneyPayment) {
+              // Points fully covered the order, no WeChat needed
+              uni.request({
+                url: config.api('/api/product/order/handlePaymentSuccess'),
+                method: 'POST',
+                data: { orderNo: that.orderNo },
+                header: {
+                  'Content-Type': 'application/json',
+                  'Authorization': 'Bearer ' + uni.getStorageSync('token')
+                },
+                success: function(payRes) {
+                  if (payRes.data && payRes.data.code === 200) {
+                    uni.showToast({ title: '支付成功', icon: 'success' })
+                    setTimeout(function() {
+                      uni.redirectTo({
+                        url: '/pages/shop/order-list/order-list?tab=paid'
+                      })
+                    }, 1500)
+                  } else {
+                    uni.showToast({ title: payRes.data.message || '处理失败', icon: 'none' })
+                  }
+                },
+                fail: function() {
+                  uni.showToast({ title: '网络请求失败', icon: 'none' })
+                }
+              })
+            } else if (data.wechatPayParams) {
+              var params = data.wechatPayParams
               uni.requestPayment({
                 timeStamp: params.timeStamp,
                 nonceStr: params.nonceStr,
@@ -315,11 +351,14 @@ export default {
   background: #ddd;
   color: #999;
 }
-.cancel-link {
-  margin-top: 24rpx;
-  font-size: 26rpx;
-  color: #999;
-  text-decoration: underline;
-  padding: 10rpx;
-}
+  .cancel-link {
+    margin-top: 24rpx;
+    font-size: 26rpx;
+    color: #999;
+    text-decoration: underline;
+    padding: 10rpx;
+  },
+  .points-value {
+    color: #F97316;
+  }
 </style>