Browse Source

fix(product-order): 下单支持user_address地址簿,商品订单落库收货地址快照并完善管理端展示

Xiaogang Liao 1 day ago
parent
commit
0897fd755e

+ 3 - 0
cfc-backend/src/main/java/com/etotem/cfc/dto/CreateProductOrderDTO.java

@@ -18,6 +18,7 @@ public class CreateProductOrderDTO {
     private Integer deliveryMethod = 1; // 1=快递 2=自提
     private Integer pointsUsed; // 积分抵扣数量
     private Long consigneeId;         // 收货人ID
+    private Long addressId;             // 收货地址ID(user_address.id),与 addressSnapshot 二选一
     private Map<String, String> purchaseInfo; // 购买信息(字段key→值)
 
     private List<Long> giftItemIds; // 选中的赠品项ID列表(来自 product_gift_items.id)
@@ -42,5 +43,7 @@ public class CreateProductOrderDTO {
     public Integer getDeliveryMethod() { return deliveryMethod; }
     public Integer getPointsUsed() { return pointsUsed; }
     public Long getConsigneeId() { return consigneeId; }
+    public Long getAddressId() { return addressId; }
+    public void setAddressId(Long addressId) { this.addressId = addressId; }
     public Map<String, String> getPurchaseInfo() { return purchaseInfo; }
 }

+ 61 - 11
cfc-backend/src/main/java/com/etotem/cfc/service/ProductOrderService.java

@@ -10,6 +10,8 @@ import com.etotem.cfc.dto.ProductOrderDTO;
 import com.etotem.cfc.entity.AfterSalesRequest;
 import com.etotem.cfc.entity.AssessmentProduct;
 import com.etotem.cfc.entity.Consignee;
+import com.etotem.cfc.entity.UserAddress;
+import com.etotem.cfc.mapper.UserAddressMapper;
 import com.etotem.cfc.entity.Coupon;
 import com.etotem.cfc.entity.FamilyCoupon;
 import com.etotem.cfc.entity.Product;
@@ -181,6 +183,9 @@ public class ProductOrderService {
     @Resource
     private com.etotem.cfc.mapper.ConsigneeMapper consigneeMapper;
 
+    @Resource
+    private UserAddressMapper userAddressMapper;
+
     @Resource
     private ProductOrderGiftMapper productOrderGiftMapper;
 
@@ -256,8 +261,7 @@ public class ProductOrderService {
         order.setCoverImage(product.getCoverImage());
         order.setDiscountAmount(discountAmount);
         order.setCouponId(dto.getCouponId());
-        order.setAddressSnapshot(dto.getAddressSnapshot());
-        order.setConsigneeId(dto.getConsigneeId());
+        resolveOrderAddress(order, dto, buyerId);
         if (dto.getPurchaseInfo() != null && !dto.getPurchaseInfo().isEmpty()) {
             order.setPurchaseInfo(JSON.toJSONString(dto.getPurchaseInfo()));
         }
@@ -297,6 +301,44 @@ public class ProductOrderService {
      * 配送方式与履约校验(设计文档十一节)
      * 分支校验 + 生成快照 + 扣减时间段余量;返回 error!=null 表示校验失败
      */
+    /** 落库前统一整理收货地址:优先直接快照,其次 addressId(user_address),最后 consigneeId(consignees) */
+    private void resolveOrderAddress(ProductOrder order, CreateProductOrderDTO dto, Long buyerId) {
+        String snapshot = dto.getAddressSnapshot();
+        Long consigneeId = dto.getConsigneeId();
+        if ((snapshot == null || snapshot.isEmpty()) && dto.getAddressId() != null) {
+            UserAddress ua = userAddressMapper.selectById(dto.getAddressId());
+            if (ua != null && buyerId.equals(ua.getUserId())) {
+                Map<String, Object> snap = new HashMap<>();
+                snap.put("receiverName", ua.getReceiverName());
+                snap.put("phone", ua.getPhone());
+                snap.put("province", ua.getProvince());
+                snap.put("city", ua.getCity());
+                snap.put("district", ua.getDistrict());
+                snap.put("street", ua.getStreet());
+                snap.put("isDefault", ua.getIsDefault());
+                snap.put("source", "user_address");
+                snapshot = JSON.toJSONString(snap);
+                consigneeId = ua.getId();
+            }
+        } else if ((snapshot == null || snapshot.isEmpty()) && dto.getConsigneeId() != null) {
+            Consignee c = consigneeMapper.selectById(dto.getConsigneeId());
+            if (c != null && buyerId.equals(c.getUserId())) {
+                Map<String, Object> snap = new HashMap<>();
+                snap.put("receiverName", c.getName());
+                snap.put("phone", c.getPhone());
+                snap.put("province", c.getProvince());
+                snap.put("city", c.getCity());
+                snap.put("district", c.getDistrict());
+                snap.put("street", c.getStreet());
+                snap.put("source", "consignee");
+                snapshot = JSON.toJSONString(snap);
+                consigneeId = c.getId();
+            }
+        }
+        order.setAddressSnapshot(snapshot);
+        order.setConsigneeId(consigneeId);
+    }
+
     private FulfillValidation validateFulfill(Product product, CreateProductOrderDTO dto, Long buyerId) {
         FulfillValidation fv = new FulfillValidation();
         Integer productMethod = product.getDeliveryMethod() != null ? product.getDeliveryMethod() : 1;
@@ -316,16 +358,25 @@ public class ProductOrderService {
 
         switch (actualMethod) {
             case 1:
-                // 快递:校验收货地址存在且属于当前用户
-                if (dto.getConsigneeId() == null) {
+                // 快递:收货地址三选一校验:直接快照 > addressId(user_address) > consigneeId(consignees)
+                if (dto.getAddressSnapshot() != null && !dto.getAddressSnapshot().isEmpty()) {
+                    // 直接提供快照,校验通过
+                } else if (dto.getAddressId() != null) {
+                    UserAddress ua = userAddressMapper.selectById(dto.getAddressId());
+                    if (ua == null || !buyerId.equals(ua.getUserId())) {
+                        fv.error = "收货地址不存在";
+                        return fv;
+                    }
+                } else if (dto.getConsigneeId() != null) {
+                    Consignee consignee = consigneeMapper.selectById(dto.getConsigneeId());
+                    if (consignee == null || !buyerId.equals(consignee.getUserId())) {
+                        fv.error = "收货地址不存在";
+                        return fv;
+                    }
+                } else {
                     fv.error = "请选择收货地址";
                     return fv;
                 }
-                Consignee consignee = consigneeMapper.selectById(dto.getConsigneeId());
-                if (consignee == null || !buyerId.equals(consignee.getUserId())) {
-                    fv.error = "收货地址不存在";
-                    return fv;
-                }
                 break;
             case 2: {
                 // 自提:校验自取地点 + 时间段
@@ -558,8 +609,7 @@ public class ProductOrderService {
         order.setCoverImage(firstItem.getCoverImage() != null ? firstItem.getCoverImage() : firstProduct.getCoverImage());
         order.setDiscountAmount(multiDiscountAmount);
         order.setCouponId(dto.getCouponId());
-        order.setAddressSnapshot(dto.getAddressSnapshot());
-        order.setConsigneeId(dto.getConsigneeId());
+        resolveOrderAddress(order, dto, buyerId);
         if (dto.getPurchaseInfo() != null && !dto.getPurchaseInfo().isEmpty()) {
             order.setPurchaseInfo(JSON.toJSONString(dto.getPurchaseInfo()));
         }

+ 23 - 1
cfc-web/src/views/admin/OrderDetail.vue

@@ -32,7 +32,9 @@
         <el-descriptions :column="3" border>
           <el-descriptions-item label="配送方式">{{ order.deliveryMethod == 2 ? '自提' : '快递' }}</el-descriptions-item>
           <el-descriptions-item label="自提码">{{ order.pickupCode || '-' }}</el-descriptions-item>
-          <el-descriptions-item label="收货地址">{{ order.addressSnapshot || '-' }}</el-descriptions-item>
+          <el-descriptions-item label="收货人">{{ addressInfo.receiverName || '-' }}</el-descriptions-item>
+          <el-descriptions-item label="联系电话">{{ addressInfo.phone || '-' }}</el-descriptions-item>
+          <el-descriptions-item label="收货地址">{{ addressInfo.fullAddress }}</el-descriptions-item>
           <el-descriptions-item label="物流公司">{{ order.logisticsCompany || '-' }}</el-descriptions-item>
           <el-descriptions-item label="物流单号">{{ order.logisticsNo || order.trackingNumber || '-' }}</el-descriptions-item>
         </el-descriptions>
@@ -112,6 +114,26 @@ export default {
       refundRejectReason: ''
     }
   },
+  computed: {
+    addressInfo() {
+      var s = this.order && this.order.addressSnapshot
+      if (!s) return { receiverName: '', phone: '', fullAddress: '-' }
+      try {
+        var a = JSON.parse(s)
+        if (a.receiverName || a.phone || a.province || a.street) {
+          var full = String((a.province || '') + (a.city || '') + (a.district || '') + (a.street || '')).trim()
+          return {
+            receiverName: a.receiverName || '',
+            phone: a.phone || '',
+            fullAddress: full || '-'
+          }
+        }
+        return { receiverName: '', phone: '', fullAddress: s }
+      } catch (e) {
+        return { receiverName: '', phone: '', fullAddress: s }
+      }
+    }
+  },
   mounted() {
     this.loadOrder()
   },

+ 5 - 0
cfc-web/src/views/admin/OrderManage.vue

@@ -129,6 +129,11 @@
           <el-descriptions-item label="履约状态" v-if="detail.fulfillStatus">
             <el-tag :type="fulfillType(detail.fulfillStatus)" size="mini">{{ fulfillLabel(detail.fulfillStatus) }}</el-tag>
           </el-descriptions-item>
+          <el-descriptions-item label="收货人" v-if="getJsonField(detail.addressSnapshot, 'receiverName') !== '-'">{{ getJsonField(detail.addressSnapshot, 'receiverName') }}</el-descriptions-item>
+          <el-descriptions-item label="联系电话" v-if="getJsonField(detail.addressSnapshot, 'phone') !== '-'">{{ getJsonField(detail.addressSnapshot, 'phone') }}</el-descriptions-item>
+          <el-descriptions-item label="收货地址" v-if="getJsonField(detail.addressSnapshot, 'province') !== '-' || getJsonField(detail.addressSnapshot, 'street') !== '-'">
+            {{ getJsonField(detail.addressSnapshot, 'province') }}{{ getJsonField(detail.addressSnapshot, 'city') }}{{ getJsonField(detail.addressSnapshot, 'district') }}{{ getJsonField(detail.addressSnapshot, 'street') }}
+          </el-descriptions-item>
           <el-descriptions-item label="自取地点" v-if="detail.pickupLocationSnapshot">{{ getJsonField(detail.pickupLocationSnapshot, 'name') }}</el-descriptions-item>
           <el-descriptions-item label="服务者" v-if="detail.servicePersonSnapshot">{{ getJsonField(detail.servicePersonSnapshot, 'name') }}</el-descriptions-item>
           <el-descriptions-item label="确认时间" v-if="detail.fulfillConfirmedAt">{{ formatTime(detail.fulfillConfirmedAt) }}</el-descriptions-item>