From 5804d7bbbf9676e07ef105effb66f5fcc16fd4ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E5=BF=A0=E6=9E=97?= <170083662@qq.com> Date: Sun, 10 Aug 2025 23:18:31 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=EF=BC=9A=E6=94=B6=E8=B4=A7?= =?UTF-8?q?=E4=BF=A1=E6=81=AF=E8=AE=BE=E8=AE=A1=E6=96=B9=E6=A1=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/DELIVERY_ADDRESS_DESIGN.md | 252 ++++++++++++++++++ .../shop/service/OrderBusinessService.java | 120 ++++++++- .../shop/service/ShopUserAddressService.java | 16 ++ .../impl/ShopUserAddressServiceImpl.java | 21 ++ 4 files changed, 401 insertions(+), 8 deletions(-) create mode 100644 docs/DELIVERY_ADDRESS_DESIGN.md diff --git a/docs/DELIVERY_ADDRESS_DESIGN.md b/docs/DELIVERY_ADDRESS_DESIGN.md new file mode 100644 index 0000000..8d3a887 --- /dev/null +++ b/docs/DELIVERY_ADDRESS_DESIGN.md @@ -0,0 +1,252 @@ +# 收货信息设计方案 + +## 概述 + +本文档详细说明了电商系统中收货信息的设计方案,采用**地址快照 + 地址引用混合模式**,确保订单收货信息的完整性和一致性。 + +## 设计原则 + +1. **数据一致性**:用户下单时保存收货地址快照,避免后续地址修改影响历史订单 +2. **用户体验**:自动读取用户默认地址,减少用户输入 +3. **灵活性**:支持用户在下单时临时修改收货信息 +4. **可追溯性**:保留地址ID引用关系,便于数据分析和问题排查 + +## 数据库设计 + +### 1. 用户地址表 (shop_user_address) + +```sql +CREATE TABLE `shop_user_address` ( + `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `name` varchar(100) DEFAULT NULL COMMENT '姓名', + `phone` varchar(20) DEFAULT NULL COMMENT '手机号码', + `country` varchar(50) DEFAULT NULL COMMENT '所在国家', + `province` varchar(50) DEFAULT NULL COMMENT '所在省份', + `city` varchar(50) DEFAULT NULL COMMENT '所在城市', + `region` varchar(50) DEFAULT NULL COMMENT '所在辖区', + `address` varchar(500) DEFAULT NULL COMMENT '收货地址', + `full_address` varchar(500) DEFAULT NULL COMMENT '完整地址', + `lat` varchar(50) DEFAULT NULL COMMENT '纬度', + `lng` varchar(50) DEFAULT NULL COMMENT '经度', + `gender` int(11) DEFAULT NULL COMMENT '1先生 2女士', + `type` varchar(20) DEFAULT NULL COMMENT '家、公司、学校', + `is_default` tinyint(1) DEFAULT 0 COMMENT '默认收货地址', + `sort_number` int(11) DEFAULT NULL COMMENT '排序号', + `user_id` int(11) DEFAULT NULL COMMENT '用户ID', + `tenant_id` int(11) DEFAULT NULL COMMENT '租户id', + `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '注册时间', + PRIMARY KEY (`id`), + KEY `idx_user_id` (`user_id`), + KEY `idx_is_default` (`is_default`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='收货地址'; +``` + +### 2. 订单表收货信息字段 (shop_order) + +```sql +-- 订单表中的收货信息字段 +`address_id` int(11) DEFAULT NULL COMMENT '收货地址ID(引用关系)', +`address` varchar(500) DEFAULT NULL COMMENT '收货地址快照', +`real_name` varchar(100) DEFAULT NULL COMMENT '收货人姓名快照', +`address_lat` varchar(50) DEFAULT NULL COMMENT '地址纬度', +`address_lng` varchar(50) DEFAULT NULL COMMENT '地址经度', +``` + +## 业务流程设计 + +### 1. 下单时收货地址处理流程 + +```mermaid +flowchart TD + A[用户下单] --> B{前端是否传入完整地址?} + B -->|是| C[使用前端传入地址] + B -->|否| D{是否指定地址ID?} + D -->|是| E[根据地址ID获取地址] + E --> F{地址是否存在且属于当前用户?} + F -->|是| G[使用指定地址] + F -->|否| H[获取用户默认地址] + D -->|否| H[获取用户默认地址] + H --> I{是否有默认地址?} + I -->|是| J[使用默认地址] + I -->|否| K[获取用户第一个地址] + K --> L{是否有地址?} + L -->|是| M[使用第一个地址] + L -->|否| N[抛出异常:请先添加收货地址] + C --> O[创建地址快照] + G --> O + J --> O + M --> O + O --> P[保存订单] +``` + +### 2. 地址优先级 + +1. **前端传入的完整地址信息**(最高优先级) +2. **指定的地址ID对应的地址** +3. **用户默认收货地址** +4. **用户的第一个收货地址** +5. **无地址时抛出异常** + +## 核心实现 + +### 1. 收货地址处理方法 + +```java +/** + * 处理收货地址信息 + * 优先级:前端传入地址 > 指定地址ID > 用户默认地址 + */ +private void processDeliveryAddress(ShopOrder shopOrder, OrderCreateRequest request, User loginUser) { + // 1. 如果前端已经传入了完整的收货地址信息,直接使用 + if (isAddressInfoComplete(request)) { + return; + } + + // 2. 如果指定了地址ID,获取该地址信息 + if (request.getAddressId() != null) { + ShopUserAddress userAddress = shopUserAddressService.getById(request.getAddressId()); + if (userAddress != null && userAddress.getUserId().equals(loginUser.getUserId())) { + copyAddressToOrder(userAddress, shopOrder, request); + return; + } + } + + // 3. 获取用户默认收货地址 + ShopUserAddress defaultAddress = shopUserAddressService.getDefaultAddress(loginUser.getUserId()); + if (defaultAddress != null) { + copyAddressToOrder(defaultAddress, shopOrder, request); + return; + } + + // 4. 如果没有默认地址,获取用户的第一个地址 + List userAddresses = shopUserAddressService.getUserAddresses(loginUser.getUserId()); + if (!userAddresses.isEmpty()) { + copyAddressToOrder(userAddresses.get(0), shopOrder, request); + return; + } + + // 5. 如果用户没有任何收货地址,抛出异常 + throw new BusinessException("请先添加收货地址"); +} +``` + +### 2. 地址快照创建 + +```java +/** + * 将用户地址信息复制到订单中(创建快照) + */ +private void copyAddressToOrder(ShopUserAddress userAddress, ShopOrder shopOrder, OrderCreateRequest request) { + // 保存地址ID引用关系 + shopOrder.setAddressId(userAddress.getId()); + request.setAddressId(userAddress.getId()); + + // 创建地址信息快照 + if (request.getAddress() == null || request.getAddress().trim().isEmpty()) { + StringBuilder fullAddress = new StringBuilder(); + if (userAddress.getProvince() != null) fullAddress.append(userAddress.getProvince()); + if (userAddress.getCity() != null) fullAddress.append(userAddress.getCity()); + if (userAddress.getRegion() != null) fullAddress.append(userAddress.getRegion()); + if (userAddress.getAddress() != null) fullAddress.append(userAddress.getAddress()); + + shopOrder.setAddress(fullAddress.toString()); + request.setAddress(fullAddress.toString()); + } + + // 复制收货人信息 + if (request.getRealName() == null || request.getRealName().trim().isEmpty()) { + shopOrder.setRealName(userAddress.getName()); + request.setRealName(userAddress.getName()); + } + + // 复制经纬度信息 + if (request.getAddressLat() == null && userAddress.getLat() != null) { + shopOrder.setAddressLat(userAddress.getLat()); + request.setAddressLat(userAddress.getLat()); + } + if (request.getAddressLng() == null && userAddress.getLng() != null) { + shopOrder.setAddressLng(userAddress.getLng()); + request.setAddressLng(userAddress.getLng()); + } +} +``` + +## 前端集成建议 + +### 1. 下单页面地址选择 + +```javascript +// 获取用户地址列表 +const getUserAddresses = async () => { + const response = await api.get('/api/shop/user-address/my'); + return response.data; +}; + +// 获取默认地址 +const getDefaultAddress = async () => { + const addresses = await getUserAddresses(); + return addresses.find(addr => addr.isDefault) || addresses[0]; +}; + +// 下单时的地址处理 +const createOrder = async (orderData) => { + // 如果用户没有选择地址,使用默认地址 + if (!orderData.addressId && !orderData.address) { + const defaultAddress = await getDefaultAddress(); + if (defaultAddress) { + orderData.addressId = defaultAddress.id; + } + } + + return api.post('/api/shop/order/create', orderData); +}; +``` + +### 2. 地址选择组件 + +```vue + +``` + +## 优势分析 + +### 1. 数据一致性 +- 订单创建时保存地址快照,确保历史订单信息不受用户后续地址修改影响 +- 同时保留地址ID引用,便于数据关联和分析 + +### 2. 用户体验 +- 自动读取用户默认地址,减少用户操作步骤 +- 支持临时修改收货信息,满足特殊需求 +- 智能地址选择逻辑,确保总能找到合适的收货地址 + +### 3. 系统稳定性 +- 完善的异常处理机制,避免因地址问题导致下单失败 +- 详细的日志记录,便于问题排查和系统监控 + +### 4. 扩展性 +- 支持多种地址类型(家、公司、学校等) +- 预留经纬度字段,支持地图定位功能 +- 灵活的排序和默认地址设置 + +## 注意事项 + +1. **地址验证**:建议在前端和后端都进行地址完整性验证 +2. **默认地址管理**:确保用户只能有一个默认地址 +3. **地址数量限制**:建议限制用户地址数量,避免数据冗余 +4. **隐私保护**:敏感信息如手机号需要适当脱敏处理 +5. **性能优化**:对于高频查询的地址信息,可考虑适当缓存 + +## 总结 + +本设计方案通过地址快照机制确保了订单数据的一致性,通过智能地址选择提升了用户体验,通过完善的异常处理保证了系统稳定性。该方案已在 `OrderBusinessService` 中实现,可以直接投入使用。 diff --git a/src/main/java/com/gxwebsoft/shop/service/OrderBusinessService.java b/src/main/java/com/gxwebsoft/shop/service/OrderBusinessService.java index f8ba6d3..10099d7 100644 --- a/src/main/java/com/gxwebsoft/shop/service/OrderBusinessService.java +++ b/src/main/java/com/gxwebsoft/shop/service/OrderBusinessService.java @@ -5,10 +5,7 @@ import com.gxwebsoft.common.core.exception.BusinessException; import com.gxwebsoft.common.system.entity.User; import com.gxwebsoft.shop.config.OrderConfigProperties; import com.gxwebsoft.shop.dto.OrderCreateRequest; -import com.gxwebsoft.shop.entity.ShopGoods; -import com.gxwebsoft.shop.entity.ShopGoodsSku; -import com.gxwebsoft.shop.entity.ShopOrder; -import com.gxwebsoft.shop.entity.ShopOrderGoods; +import com.gxwebsoft.shop.entity.*; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.BeanUtils; import org.springframework.stereotype.Service; @@ -47,6 +44,9 @@ public class OrderBusinessService { @Resource private OrderConfigProperties orderConfig; + @Resource + private ShopUserAddressService shopUserAddressService; + /** * 创建订单 * @@ -63,19 +63,22 @@ public class OrderBusinessService { // 2. 构建订单对象 ShopOrder shopOrder = buildShopOrder(request, loginUser); - // 3. 应用业务规则 + // 3. 处理收货地址信息 + processDeliveryAddress(shopOrder, request, loginUser); + + // 4. 应用业务规则 applyBusinessRules(shopOrder, loginUser); - // 4. 保存订单 + // 5. 保存订单 boolean saved = shopOrderService.save(shopOrder); if (!saved) { throw new BusinessException("订单保存失败"); } - // 5. 保存订单商品 + // 6. 保存订单商品 saveOrderGoods(request, shopOrder); - // 6. 创建微信支付订单 + // 7. 创建微信支付订单 try { return shopOrderService.createWxOrder(shopOrder); } catch (Exception e) { @@ -274,6 +277,107 @@ public class OrderBusinessService { return shopOrder; } + /** + * 处理收货地址信息 + * 优先级:前端传入地址 > 指定地址ID > 用户默认地址 + */ + private void processDeliveryAddress(ShopOrder shopOrder, OrderCreateRequest request, User loginUser) { + try { + // 1. 如果前端已经传入了完整的收货地址信息,直接使用 + if (isAddressInfoComplete(request)) { + log.info("使用前端传入的收货地址信息,用户ID:{}", loginUser.getUserId()); + return; + } + + // 2. 如果指定了地址ID,获取该地址信息 + if (request.getAddressId() != null) { + ShopUserAddress userAddress = shopUserAddressService.getById(request.getAddressId()); + if (userAddress != null && userAddress.getUserId().equals(loginUser.getUserId())) { + copyAddressToOrder(userAddress, shopOrder, request); + log.info("使用指定地址ID:{},用户ID:{}", request.getAddressId(), loginUser.getUserId()); + return; + } + log.warn("指定的地址ID不存在或不属于当前用户,地址ID:{},用户ID:{}", + request.getAddressId(), loginUser.getUserId()); + } + + // 3. 获取用户默认收货地址 + ShopUserAddress defaultAddress = shopUserAddressService.getDefaultAddress(loginUser.getUserId()); + if (defaultAddress != null) { + copyAddressToOrder(defaultAddress, shopOrder, request); + log.info("使用用户默认收货地址,地址ID:{},用户ID:{}", defaultAddress.getId(), loginUser.getUserId()); + return; + } + + // 4. 如果没有默认地址,获取用户的第一个地址 + List userAddresses = shopUserAddressService.getUserAddresses(loginUser.getUserId()); + if (!userAddresses.isEmpty()) { + ShopUserAddress firstAddress = userAddresses.get(0); + copyAddressToOrder(firstAddress, shopOrder, request); + log.info("使用用户第一个收货地址,地址ID:{},用户ID:{}", firstAddress.getId(), loginUser.getUserId()); + return; + } + + // 5. 如果用户没有任何收货地址,抛出异常 + throw new BusinessException("请先添加收货地址"); + + } catch (BusinessException e) { + throw e; + } catch (Exception e) { + log.error("处理收货地址信息失败,用户ID:{}", loginUser.getUserId(), e); + throw new BusinessException("处理收货地址信息失败:" + e.getMessage()); + } + } + + /** + * 检查前端传入的地址信息是否完整 + */ + private boolean isAddressInfoComplete(OrderCreateRequest request) { + return request.getAddress() != null && !request.getAddress().trim().isEmpty() && + request.getRealName() != null && !request.getRealName().trim().isEmpty(); + } + + /** + * 将用户地址信息复制到订单中(创建快照) + */ + private void copyAddressToOrder(ShopUserAddress userAddress, ShopOrder shopOrder, OrderCreateRequest request) { + // 保存地址ID引用关系 + shopOrder.setAddressId(userAddress.getId()); + request.setAddressId(userAddress.getId()); + + // 创建地址信息快照 + if (request.getAddress() == null || request.getAddress().trim().isEmpty()) { + // 构建完整地址 + StringBuilder fullAddress = new StringBuilder(); + if (userAddress.getProvince() != null) fullAddress.append(userAddress.getProvince()); + if (userAddress.getCity() != null) fullAddress.append(userAddress.getCity()); + if (userAddress.getRegion() != null) fullAddress.append(userAddress.getRegion()); + if (userAddress.getAddress() != null) fullAddress.append(userAddress.getAddress()); + + shopOrder.setAddress(fullAddress.toString()); + request.setAddress(fullAddress.toString()); + } + + // 复制收货人信息 + if (request.getRealName() == null || request.getRealName().trim().isEmpty()) { + shopOrder.setRealName(userAddress.getName()); + request.setRealName(userAddress.getName()); + } + + // 复制经纬度信息 + if (request.getAddressLat() == null && userAddress.getLat() != null) { + shopOrder.setAddressLat(userAddress.getLat()); + request.setAddressLat(userAddress.getLat()); + } + if (request.getAddressLng() == null && userAddress.getLng() != null) { + shopOrder.setAddressLng(userAddress.getLng()); + request.setAddressLng(userAddress.getLng()); + } + + log.debug("地址信息快照创建完成 - 地址ID:{},收货人:{},地址:{}", + userAddress.getId(), userAddress.getName(), shopOrder.getAddress()); + } + /** * 应用业务规则 */ diff --git a/src/main/java/com/gxwebsoft/shop/service/ShopUserAddressService.java b/src/main/java/com/gxwebsoft/shop/service/ShopUserAddressService.java index 6c00c3f..70880e4 100644 --- a/src/main/java/com/gxwebsoft/shop/service/ShopUserAddressService.java +++ b/src/main/java/com/gxwebsoft/shop/service/ShopUserAddressService.java @@ -39,4 +39,20 @@ public interface ShopUserAddressService extends IService { */ ShopUserAddress getByIdRel(Integer id); + /** + * 获取用户默认收货地址 + * + * @param userId 用户ID + * @return ShopUserAddress + */ + ShopUserAddress getDefaultAddress(Integer userId); + + /** + * 获取用户所有收货地址 + * + * @param userId 用户ID + * @return List + */ + List getUserAddresses(Integer userId); + } diff --git a/src/main/java/com/gxwebsoft/shop/service/impl/ShopUserAddressServiceImpl.java b/src/main/java/com/gxwebsoft/shop/service/impl/ShopUserAddressServiceImpl.java index ad53f27..cf599c6 100644 --- a/src/main/java/com/gxwebsoft/shop/service/impl/ShopUserAddressServiceImpl.java +++ b/src/main/java/com/gxwebsoft/shop/service/impl/ShopUserAddressServiceImpl.java @@ -1,5 +1,6 @@ package com.gxwebsoft.shop.service.impl; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.gxwebsoft.shop.mapper.ShopUserAddressMapper; import com.gxwebsoft.shop.service.ShopUserAddressService; @@ -44,4 +45,24 @@ public class ShopUserAddressServiceImpl extends ServiceImpl wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(ShopUserAddress::getUserId, userId) + .eq(ShopUserAddress::getIsDefault, true) + .orderByDesc(ShopUserAddress::getCreateTime) + .last("LIMIT 1"); + return getOne(wrapper); + } + + @Override + public List getUserAddresses(Integer userId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(ShopUserAddress::getUserId, userId) + .orderByDesc(ShopUserAddress::getIsDefault) + .orderByAsc(ShopUserAddress::getSortNumber) + .orderByDesc(ShopUserAddress::getCreateTime); + return list(wrapper); + } + }