|
|
@@ -0,0 +1,157 @@
|
|
|
+package com.etotem.cfc.service;
|
|
|
+
|
|
|
+import com.etotem.cfc.entity.ProductOrder;
|
|
|
+import com.etotem.cfc.entity.User;
|
|
|
+import com.etotem.cfc.mapper.ProductOrderMapper;
|
|
|
+import com.etotem.cfc.mapper.UserMapper;
|
|
|
+import org.junit.jupiter.api.BeforeEach;
|
|
|
+import org.junit.jupiter.api.Test;
|
|
|
+import org.mockito.ArgumentCaptor;
|
|
|
+import org.springframework.test.util.ReflectionTestUtils;
|
|
|
+
|
|
|
+import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
|
+import static org.junit.jupiter.api.Assertions.assertNull;
|
|
|
+import static org.mockito.ArgumentMatchers.any;
|
|
|
+import static org.mockito.ArgumentMatchers.eq;
|
|
|
+import static org.mockito.Mockito.mock;
|
|
|
+import static org.mockito.Mockito.verify;
|
|
|
+import static org.mockito.Mockito.when;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 改快递字段双轨回归测试:updateExpress() 必须与 ship() 统一写入 logistics_no/logistics_company 列。
|
|
|
+ *
|
|
|
+ * 根因:ship() 主流程写 logisticsNo/logisticsCompany,详情展示(ProductOrderService.getOrderDetail /
|
|
|
+ * ProductOrderDTO)读 logisticsNo/logisticsCompany,管理端 OrderDetail.vue 与小程序 logistics.vue
|
|
|
+ * 同样读 logisticsNo;但 updateExpress() 写的是 trackingNumber/expressCompany(另一组列)。
|
|
|
+ * 后果:
|
|
|
+ * 1) 改完快递,前端展示的物流信息不更新(读的是另一组列);
|
|
|
+ * 2) 微信 upload_shipping_info 同步在部分更新时回退读 trackingNumber 列(ship 落库列为
|
|
|
+ * logisticsNo),拿不到单号 -> log.info 静默跳过,微信侧永远收不到这次更新。
|
|
|
+ */
|
|
|
+public class ProductOrderServiceUpdateExpressFieldTest {
|
|
|
+
|
|
|
+ private static final String ORDER_NO = "O20261018001";
|
|
|
+
|
|
|
+ private ProductOrderService orderService;
|
|
|
+ private ProductOrderMapper orderMapper;
|
|
|
+ private UserMapper userMapper;
|
|
|
+ private WechatService wechatService;
|
|
|
+
|
|
|
+ @BeforeEach
|
|
|
+ public void setUp() {
|
|
|
+ orderService = new ProductOrderService();
|
|
|
+ orderMapper = mock(ProductOrderMapper.class);
|
|
|
+ userMapper = mock(UserMapper.class);
|
|
|
+ wechatService = mock(WechatService.class);
|
|
|
+ ReflectionTestUtils.setField(orderService, "orderMapper", orderMapper);
|
|
|
+ ReflectionTestUtils.setField(orderService, "userMapper", userMapper);
|
|
|
+ ReflectionTestUtils.setField(orderService, "wechatService", wechatService);
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 构造一笔已由 ship() 正常发货的订单:单号/公司落在 logisticsNo/logisticsCompany 列 */
|
|
|
+ private ProductOrder shippedOrder() {
|
|
|
+ ProductOrder order = new ProductOrder();
|
|
|
+ order.setOrderNo(ORDER_NO);
|
|
|
+ order.setBuyerId(100L);
|
|
|
+ order.setDeliveryMethod(1); // 快递,非自提
|
|
|
+ order.setProductName("复合维生素");
|
|
|
+ order.setQuantity(2);
|
|
|
+ order.setLogisticsNo("SF1234567890"); // ship() 落库列
|
|
|
+ order.setLogisticsCompany("顺丰速运");
|
|
|
+ return order;
|
|
|
+ }
|
|
|
+
|
|
|
+ private void stubBuyerWithOpenid() {
|
|
|
+ User buyer = new User();
|
|
|
+ buyer.setOpenid("oABC123");
|
|
|
+ when(userMapper.selectById(100L)).thenReturn(buyer);
|
|
|
+ when(wechatService.uploadShippingInfo(any(), any(), any(), any(), any())).thenReturn(null);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 故障复现:部分更新(只改快递公司不传单号)。
|
|
|
+ * 期望:单号回退到订单已存值 logisticsNo,微信同步用该单号;
|
|
|
+ * 实际(修复前):回退读 trackingNumber 列为空 -> 无单号静默跳过微信同步,且公司写进了另一组列。
|
|
|
+ */
|
|
|
+ @Test
|
|
|
+ public void updateExpress_onlyCompany_trackingFallsBackToPersistedLogisticsNo() {
|
|
|
+ ProductOrder existing = shippedOrder();
|
|
|
+ when(orderMapper.selectOne(any())).thenReturn(existing);
|
|
|
+ stubBuyerWithOpenid();
|
|
|
+
|
|
|
+ orderService.updateExpress(ORDER_NO, null, "中通快递", null);
|
|
|
+
|
|
|
+ // 落库:公司必须更新到 logisticsCompany 列,单号保留在 logisticsNo 列,不得写 trackingNumber/expressCompany
|
|
|
+ ArgumentCaptor<ProductOrder> persisted = ArgumentCaptor.forClass(ProductOrder.class);
|
|
|
+ verify(orderMapper).updateById(persisted.capture());
|
|
|
+ ProductOrder saved = persisted.getValue();
|
|
|
+ assertEquals("SF1234567890", saved.getLogisticsNo(),
|
|
|
+ "原单号必须保留在 logisticsNo 列(ship 落库列)");
|
|
|
+ assertEquals("中通快递", saved.getLogisticsCompany(),
|
|
|
+ "快递公司必须落在 logisticsCompany 列(与 ship/展示一致)");
|
|
|
+ assertNull(saved.getTrackingNumber(),
|
|
|
+ "不得再写 trackingNumber 列:该列与 ship()/前端展示链路不一致");
|
|
|
+ assertNull(saved.getExpressCompany(),
|
|
|
+ "不得再写 expressCompany 列:该列与 ship()/前端展示链路不一致");
|
|
|
+
|
|
|
+ // 微信同步:必须拿到订单已存单号(修复前回退读空列,微信侧漏同步)
|
|
|
+ ArgumentCaptor<String> trackingNo = ArgumentCaptor.forClass(String.class);
|
|
|
+ verify(wechatService).uploadShippingInfo(eq(ORDER_NO), eq("oABC123"),
|
|
|
+ trackingNo.capture(), eq("中通快递"), any());
|
|
|
+ assertEquals("SF1234567890", trackingNo.getValue(),
|
|
|
+ "部分更新时必须回退到订单已存单号并同步微信");
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 全量更新(单号+公司一起传):两个值都必须落到 logisticsNo/logisticsCompany 列并同步微信。
|
|
|
+ */
|
|
|
+ @Test
|
|
|
+ public void updateExpress_fullUpdate_writesLogisticsColumnsAndSyncsWechat() {
|
|
|
+ ProductOrder existing = shippedOrder();
|
|
|
+ when(orderMapper.selectOne(any())).thenReturn(existing);
|
|
|
+ stubBuyerWithOpenid();
|
|
|
+
|
|
|
+ orderService.updateExpress(ORDER_NO, "YT9876543210", "圆通速递", null);
|
|
|
+
|
|
|
+ ArgumentCaptor<ProductOrder> persisted = ArgumentCaptor.forClass(ProductOrder.class);
|
|
|
+ verify(orderMapper).updateById(persisted.capture());
|
|
|
+ ProductOrder saved = persisted.getValue();
|
|
|
+ assertEquals("YT9876543210", saved.getLogisticsNo(),
|
|
|
+ "新单号必须写入 logisticsNo 列");
|
|
|
+ assertEquals("圆通速递", saved.getLogisticsCompany(),
|
|
|
+ "新快递公司必须写入 logisticsCompany 列");
|
|
|
+ assertNull(saved.getTrackingNumber(), "不得写 trackingNumber 列");
|
|
|
+ assertNull(saved.getExpressCompany(), "不得写 expressCompany 列");
|
|
|
+
|
|
|
+ verify(wechatService).uploadShippingInfo(eq(ORDER_NO), eq("oABC123"),
|
|
|
+ eq("YT9876543210"), eq("圆通速递"), any());
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 回归保护:ship() 发货继续写 logisticsNo/logisticsCompany 列(本次修复不得改坏主流程)。
|
|
|
+ */
|
|
|
+ @Test
|
|
|
+ public void ship_writesLogisticsColumns() {
|
|
|
+ ProductOrder existing = new ProductOrder();
|
|
|
+ existing.setOrderNo(ORDER_NO);
|
|
|
+ existing.setBuyerId(100L);
|
|
|
+ existing.setDeliveryMethod(1);
|
|
|
+ existing.setStatus("paid");
|
|
|
+ existing.setProductName("复合维生素");
|
|
|
+ existing.setQuantity(1);
|
|
|
+ when(orderMapper.selectOne(any())).thenReturn(existing);
|
|
|
+ stubBuyerWithOpenid();
|
|
|
+
|
|
|
+ orderService.ship(ORDER_NO, "JD0000000001", "京东物流");
|
|
|
+
|
|
|
+ ArgumentCaptor<ProductOrder> persisted = ArgumentCaptor.forClass(ProductOrder.class);
|
|
|
+ verify(orderMapper).updateById(persisted.capture());
|
|
|
+ ProductOrder saved = persisted.getValue();
|
|
|
+ assertEquals("JD0000000001", saved.getLogisticsNo());
|
|
|
+ assertEquals("京东物流", saved.getLogisticsCompany());
|
|
|
+ assertNull(saved.getTrackingNumber());
|
|
|
+ assertNull(saved.getExpressCompany());
|
|
|
+ verify(wechatService).uploadShippingInfo(eq(ORDER_NO), eq("oABC123"),
|
|
|
+ eq("JD0000000001"), eq("京东物流"), any());
|
|
|
+ }
|
|
|
+}
|