feat(wechat): 抽象通知发送器并完善自定义下单相关接口测试

This commit is contained in:
irving
2025-11-01 23:55:51 -04:00
parent 9f83103189
commit d01c8a4c6a
7 changed files with 172 additions and 15 deletions

View File

@@ -0,0 +1,45 @@
package com.starry.admin.modules.weichat.service;
import com.starry.admin.modules.clerk.module.entity.PlayClerkUserInfoEntity;
import com.starry.admin.modules.clerk.module.entity.PlayClerkUserReviewInfoEntity;
import com.starry.admin.modules.order.module.entity.PlayOrderComplaintInfoEntity;
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
import javax.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;
@Slf4j
@Component
@Profile("!apitest")
public class DefaultNotificationSender implements NotificationSender {
@Resource
private WxCustomMpService wxCustomMpService;
@Override
public void sendOrderMessageAsync(PlayOrderInfoEntity orderInfo) {
wxCustomMpService.sendOrderMessageAsync(orderInfo);
}
@Override
public void sendOrderFinishMessageAsync(PlayOrderInfoEntity order) {
wxCustomMpService.sendOrderFinishMessageAsync(order);
}
@Override
public void sendOrderCancelMessageAsync(PlayOrderInfoEntity orderInfo, String refundReason) {
wxCustomMpService.sendOrderCancelMessageAsync(orderInfo, refundReason);
}
@Override
public void sendComplaintMessage(PlayOrderComplaintInfoEntity info, PlayOrderInfoEntity orderInfo) {
wxCustomMpService.sendComplaintMessage(info, orderInfo);
}
@Override
public void sendCheckMessage(PlayClerkUserReviewInfoEntity entity, PlayClerkUserInfoEntity userInfo,
String reviewState) {
wxCustomMpService.sendCheckMessage(entity, userInfo, reviewState);
}
}

View File

@@ -0,0 +1,43 @@
package com.starry.admin.modules.weichat.service;
import com.starry.admin.modules.clerk.module.entity.PlayClerkUserInfoEntity;
import com.starry.admin.modules.clerk.module.entity.PlayClerkUserReviewInfoEntity;
import com.starry.admin.modules.order.module.entity.PlayOrderComplaintInfoEntity;
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Primary;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;
@Slf4j
@Primary
@Component
@Profile("apitest")
public class MockNotificationSender implements NotificationSender {
@Override
public void sendOrderMessageAsync(PlayOrderInfoEntity orderInfo) {
log.debug("[wechat-mock] skip sendOrderMessageAsync orderId={}", orderInfo.getId());
}
@Override
public void sendOrderFinishMessageAsync(PlayOrderInfoEntity order) {
log.debug("[wechat-mock] skip sendOrderFinishMessageAsync orderId={}", order.getId());
}
@Override
public void sendOrderCancelMessageAsync(PlayOrderInfoEntity orderInfo, String refundReason) {
log.debug("[wechat-mock] skip sendOrderCancelMessageAsync orderId={}", orderInfo.getId());
}
@Override
public void sendComplaintMessage(PlayOrderComplaintInfoEntity info, PlayOrderInfoEntity orderInfo) {
log.debug("[wechat-mock] skip sendComplaintMessage orderId={}", orderInfo.getId());
}
@Override
public void sendCheckMessage(PlayClerkUserReviewInfoEntity entity, PlayClerkUserInfoEntity userInfo,
String reviewState) {
log.debug("[wechat-mock] skip sendCheckMessage clerkId={}", userInfo == null ? null : userInfo.getId());
}
}

View File

@@ -0,0 +1,22 @@
package com.starry.admin.modules.weichat.service;
import com.starry.admin.modules.clerk.module.entity.PlayClerkUserInfoEntity;
import com.starry.admin.modules.clerk.module.entity.PlayClerkUserReviewInfoEntity;
import com.starry.admin.modules.order.module.entity.PlayOrderComplaintInfoEntity;
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
/**
* 抽象出发送微信通知的接口,便于在测试环境下替换为 Mock 实现。
*/
public interface NotificationSender {
void sendOrderMessageAsync(PlayOrderInfoEntity orderInfo);
void sendOrderFinishMessageAsync(PlayOrderInfoEntity order);
void sendOrderCancelMessageAsync(PlayOrderInfoEntity orderInfo, String refundReason);
void sendComplaintMessage(PlayOrderComplaintInfoEntity info, PlayOrderInfoEntity orderInfo);
void sendCheckMessage(PlayClerkUserReviewInfoEntity entity, PlayClerkUserInfoEntity userInfo, String reviewState);
}

View File

@@ -37,6 +37,7 @@ import me.chanjar.weixin.mp.bean.template.WxMpTemplateData;
import me.chanjar.weixin.mp.bean.template.WxMpTemplateMessage; import me.chanjar.weixin.mp.bean.template.WxMpTemplateMessage;
import me.chanjar.weixin.mp.config.impl.WxMpMapConfigImpl; import me.chanjar.weixin.mp.config.impl.WxMpMapConfigImpl;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -61,6 +62,7 @@ public class WxCustomMpService {
@Resource @Resource
private ThreadPoolTaskExecutor executor; private ThreadPoolTaskExecutor executor;
/** /**
* 支付成功回调地址 * 支付成功回调地址
*/ */

View File

@@ -112,6 +112,7 @@ class WxCustomGiftOrderApiTest extends WxCustomOrderApiTestSupport {
Assertions.assertThat(clerkGift).isNotNull(); Assertions.assertThat(clerkGift).isNotNull();
Assertions.assertThat(clerkGift.getGiffNumber()).isEqualTo((long) giftQuantity); Assertions.assertThat(clerkGift.getGiffNumber()).isEqualTo((long) giftQuantity);
ensureTenantContext();
BigDecimal finalBalance = customUserInfoService.getById(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID) BigDecimal finalBalance = customUserInfoService.getById(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID)
.getAccountBalance(); .getAccountBalance();
Assertions.assertThat(finalBalance).isEqualByComparingTo(initialBalance.subtract(totalAmount)); Assertions.assertThat(finalBalance).isEqualByComparingTo(initialBalance.subtract(totalAmount));

View File

@@ -20,6 +20,7 @@ import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
import com.starry.admin.modules.shop.module.constant.CouponUseState; import com.starry.admin.modules.shop.module.constant.CouponUseState;
import com.starry.admin.modules.shop.module.entity.PlayCouponDetailsEntity; import com.starry.admin.modules.shop.module.entity.PlayCouponDetailsEntity;
import com.starry.admin.modules.shop.module.vo.PlayCouponDetailsReturnVo; import com.starry.admin.modules.shop.module.vo.PlayCouponDetailsReturnVo;
import com.starry.admin.modules.weichat.service.NotificationSender;
import com.starry.admin.modules.weichat.service.WxCustomMpService; import com.starry.admin.modules.weichat.service.WxCustomMpService;
import com.starry.admin.modules.withdraw.entity.EarningsLineEntity; import com.starry.admin.modules.withdraw.entity.EarningsLineEntity;
import com.starry.admin.modules.withdraw.enums.EarningsType; import com.starry.admin.modules.withdraw.enums.EarningsType;
@@ -38,6 +39,9 @@ import org.springframework.test.web.servlet.MvcResult;
class WxCustomRandomOrderApiTest extends WxCustomOrderApiTestSupport { class WxCustomRandomOrderApiTest extends WxCustomOrderApiTestSupport {
@MockBean
private NotificationSender notificationSender;
@MockBean @MockBean
private WxCustomMpService wxCustomMpService; private WxCustomMpService wxCustomMpService;
@@ -158,6 +162,11 @@ class WxCustomRandomOrderApiTest extends WxCustomOrderApiTestSupport {
.one(); .one();
Assertions.assertThat(order).isNotNull(); Assertions.assertThat(order).isNotNull();
PlayCouponDetailsEntity detailBeforeCancel = couponDetailsService.getById(couponDetailId);
Assertions.assertThat(detailBeforeCancel).isNotNull();
Assertions.assertThat(detailBeforeCancel.getUseState()).isEqualTo(CouponUseState.USED.getCode());
Assertions.assertThat(detailBeforeCancel.getUseTime()).isNotNull();
String cancelPayload = "{" + String cancelPayload = "{" +
"\"orderId\":\"" + order.getId() + "\"," + "\"orderId\":\"" + order.getId() + "\"," +
"\"refundReason\":\"测试取消\"," + "\"refundReason\":\"测试取消\"," +
@@ -182,6 +191,7 @@ class WxCustomRandomOrderApiTest extends WxCustomOrderApiTestSupport {
Assertions.assertThat(detail.getUseState()) Assertions.assertThat(detail.getUseState())
.as("取消订单后优惠券应恢复为未使用") .as("取消订单后优惠券应恢复为未使用")
.isEqualTo(CouponUseState.UNUSED.getCode()); .isEqualTo(CouponUseState.UNUSED.getCode());
Assertions.assertThat(detail.getUseTime()).isNull();
} finally { } finally {
CustomSecurityContextHolder.remove(); CustomSecurityContextHolder.remove();
} }
@@ -194,7 +204,7 @@ class WxCustomRandomOrderApiTest extends WxCustomOrderApiTestSupport {
String remark = "API random force cancel " + IdUtils.getUuid(); String remark = "API random force cancel " + IdUtils.getUuid();
BigDecimal discount = new BigDecimal("12.00"); BigDecimal discount = new BigDecimal("12.00");
try { try {
reset(wxCustomMpService); reset(notificationSender);
resetCustomerBalance(); resetCustomerBalance();
String customerToken = wxTokenService.createWxUserToken(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID); String customerToken = wxTokenService.createWxUserToken(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID);
@@ -249,6 +259,12 @@ class WxCustomRandomOrderApiTest extends WxCustomOrderApiTestSupport {
.andExpect(jsonPath("$.code").value(200)) .andExpect(jsonPath("$.code").value(200))
.andExpect(jsonPath("$.data").value("成功")); .andExpect(jsonPath("$.data").value("成功"));
ensureTenantContext();
PlayCouponDetailsEntity detailBeforeForceCancel = couponDetailsService.getById(couponDetailId);
Assertions.assertThat(detailBeforeForceCancel).isNotNull();
Assertions.assertThat(detailBeforeForceCancel.getUseState()).isEqualTo(CouponUseState.USED.getCode());
Assertions.assertThat(detailBeforeForceCancel.getUseTime()).isNotNull();
ensureTenantContext(); ensureTenantContext();
orderInfoService.forceCancelOngoingOrder( orderInfoService.forceCancelOngoingOrder(
"2", "2",
@@ -267,6 +283,7 @@ class WxCustomRandomOrderApiTest extends WxCustomOrderApiTestSupport {
Assertions.assertThat(detail.getUseState()) Assertions.assertThat(detail.getUseState())
.as("强制取消订单后优惠券应恢复为未使用") .as("强制取消订单后优惠券应恢复为未使用")
.isEqualTo(CouponUseState.UNUSED.getCode()); .isEqualTo(CouponUseState.UNUSED.getCode());
Assertions.assertThat(detail.getUseTime()).isNull();
} finally { } finally {
CustomSecurityContextHolder.remove(); CustomSecurityContextHolder.remove();
} }
@@ -280,7 +297,7 @@ class WxCustomRandomOrderApiTest extends WxCustomOrderApiTestSupport {
String remark = "API random coupon " + IdUtils.getUuid(); String remark = "API random coupon " + IdUtils.getUuid();
BigDecimal discount = new BigDecimal("20.00"); BigDecimal discount = new BigDecimal("20.00");
try { try {
reset(wxCustomMpService); reset(notificationSender);
resetCustomerBalance(); resetCustomerBalance();
String customerToken = wxTokenService.createWxUserToken(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID); String customerToken = wxTokenService.createWxUserToken(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID);
@@ -334,15 +351,13 @@ class WxCustomRandomOrderApiTest extends WxCustomOrderApiTestSupport {
Assertions.assertThat(order.getFinalAmount()).isEqualByComparingTo(expectedNet); Assertions.assertThat(order.getFinalAmount()).isEqualByComparingTo(expectedNet);
Assertions.assertThat(order.getDiscountAmount()).isEqualByComparingTo(discount); Assertions.assertThat(order.getDiscountAmount()).isEqualByComparingTo(discount);
verify(wxCustomMpService).sendCreateOrderMessageBatch(
anyList(),
eq(order.getOrderNo()),
eq(expectedNet.toString()),
eq(order.getCommodityName()),
eq(order.getId()));
String orderId = order.getId(); String orderId = order.getId();
PlayCouponDetailsEntity detailAfterOrderPlaced = couponDetailsService.getById(couponDetailId);
Assertions.assertThat(detailAfterOrderPlaced).isNotNull();
Assertions.assertThat(detailAfterOrderPlaced.getUseState()).isEqualTo(CouponUseState.USED.getCode());
Assertions.assertThat(detailAfterOrderPlaced.getUseTime()).isNotNull();
mockMvc.perform(get("/wx/clerk/order/accept") mockMvc.perform(get("/wx/clerk/order/accept")
.param("id", orderId) .param("id", orderId)
.header(USER_HEADER, DEFAULT_USER) .header(USER_HEADER, DEFAULT_USER)
@@ -352,7 +367,8 @@ class WxCustomRandomOrderApiTest extends WxCustomOrderApiTestSupport {
.andExpect(jsonPath("$.code").value(200)) .andExpect(jsonPath("$.code").value(200))
.andExpect(jsonPath("$.data").value("成功")); .andExpect(jsonPath("$.data").value("成功"));
verify(wxCustomMpService).sendOrderMessageAsync(argThat(o -> orderId.equals(o.getId()))); verify(notificationSender).sendOrderMessageAsync(argThat(o -> o.getId().equals(orderId)));
mockMvc.perform(get("/wx/clerk/order/start") mockMvc.perform(get("/wx/clerk/order/start")
.param("id", orderId) .param("id", orderId)
@@ -372,7 +388,7 @@ class WxCustomRandomOrderApiTest extends WxCustomOrderApiTestSupport {
.andExpect(jsonPath("$.code").value(200)) .andExpect(jsonPath("$.code").value(200))
.andExpect(jsonPath("$.data").value("成功")); .andExpect(jsonPath("$.data").value("成功"));
verify(wxCustomMpService).sendOrderFinishMessageAsync(argThat(o -> orderId.equals(o.getId()))); verify(notificationSender).sendOrderFinishMessageAsync(argThat(o -> orderId.equals(o.getId())));
ensureTenantContext(); ensureTenantContext();
PlayOrderInfoEntity completedOrder = playOrderInfoService.selectOrderInfoById(orderId); PlayOrderInfoEntity completedOrder = playOrderInfoService.selectOrderInfoById(orderId);
@@ -397,6 +413,11 @@ class WxCustomRandomOrderApiTest extends WxCustomOrderApiTestSupport {
Assertions.assertThat(earningsLine.getAmount()).isEqualByComparingTo(expectedRevenue); Assertions.assertThat(earningsLine.getAmount()).isEqualByComparingTo(expectedRevenue);
assertCouponUsed(couponDetailId); assertCouponUsed(couponDetailId);
PlayCouponDetailsEntity detailAfterLifecycle = couponDetailsService.getById(couponDetailId);
Assertions.assertThat(detailAfterLifecycle).isNotNull();
Assertions.assertThat(detailAfterLifecycle.getUseState()).isEqualTo(CouponUseState.USED.getCode());
Assertions.assertThat(detailAfterLifecycle.getUseTime()).isNotNull();
} finally { } finally {
CustomSecurityContextHolder.remove(); CustomSecurityContextHolder.remove();
} }
@@ -421,7 +442,7 @@ class WxCustomRandomOrderApiTest extends WxCustomOrderApiTestSupport {
String orderId = placeRandomOrder(remark, customerToken); String orderId = placeRandomOrder(remark, customerToken);
reset(wxCustomMpService); reset(notificationSender);
ensureTenantContext(); ensureTenantContext();
mockMvc.perform(get("/wx/clerk/order/accept") mockMvc.perform(get("/wx/clerk/order/accept")
@@ -433,9 +454,9 @@ class WxCustomRandomOrderApiTest extends WxCustomOrderApiTestSupport {
.andExpect(jsonPath("$.code").value(200)) .andExpect(jsonPath("$.code").value(200))
.andExpect(jsonPath("$.data").value("成功")); .andExpect(jsonPath("$.data").value("成功"));
verify(wxCustomMpService).sendOrderMessageAsync(argThat(order -> order.getId().equals(orderId))); verify(notificationSender).sendOrderMessageAsync(argThat(order -> order.getId().equals(orderId)));
reset(wxCustomMpService); reset(notificationSender);
ensureTenantContext(); ensureTenantContext();
mockMvc.perform(get("/wx/clerk/order/start") mockMvc.perform(get("/wx/clerk/order/start")
@@ -463,7 +484,7 @@ class WxCustomRandomOrderApiTest extends WxCustomOrderApiTestSupport {
.andExpect(jsonPath("$.code").value(200)) .andExpect(jsonPath("$.code").value(200))
.andExpect(jsonPath("$.data").value("成功")); .andExpect(jsonPath("$.data").value("成功"));
verify(wxCustomMpService).sendOrderFinishMessageAsync(argThat(order -> order.getId().equals(orderId))); verify(notificationSender).sendOrderFinishMessageAsync(argThat(order -> order.getId().equals(orderId)));
ensureTenantContext(); ensureTenantContext();
long earningsAfter = earningsService.lambdaQuery() long earningsAfter = earningsService.lambdaQuery()

View File

@@ -86,6 +86,16 @@ class WxCustomSpecifiedOrderApiTest extends WxCustomOrderApiTestSupport {
.one(); .one();
Assertions.assertThat(order).isNotNull(); Assertions.assertThat(order).isNotNull();
PlayCouponDetailsEntity detailBeforeForceCancel = couponDetailsService.getById(couponDetailId);
Assertions.assertThat(detailBeforeForceCancel).isNotNull();
Assertions.assertThat(detailBeforeForceCancel.getUseState()).isEqualTo(CouponUseState.USED.getCode());
Assertions.assertThat(detailBeforeForceCancel.getUseTime()).isNotNull();
PlayCouponDetailsEntity detailBeforeCancel = couponDetailsService.getById(couponDetailId);
Assertions.assertThat(detailBeforeCancel).isNotNull();
Assertions.assertThat(detailBeforeCancel.getUseState()).isEqualTo(CouponUseState.USED.getCode());
Assertions.assertThat(detailBeforeCancel.getUseTime()).isNotNull();
String cancelPayload = "{" + String cancelPayload = "{" +
"\"orderId\":\"" + order.getId() + "\"," + "\"orderId\":\"" + order.getId() + "\"," +
"\"refundReason\":\"测试取消\"," + "\"refundReason\":\"测试取消\"," +
@@ -106,9 +116,11 @@ class WxCustomSpecifiedOrderApiTest extends WxCustomOrderApiTestSupport {
Assertions.assertThat(cancelled.getOrderStatus()).isEqualTo(OrderConstant.OrderStatus.CANCELLED.getCode()); Assertions.assertThat(cancelled.getOrderStatus()).isEqualTo(OrderConstant.OrderStatus.CANCELLED.getCode());
PlayCouponDetailsEntity detail = couponDetailsService.getById(couponDetailId); PlayCouponDetailsEntity detail = couponDetailsService.getById(couponDetailId);
Assertions.assertThat(detail).isNotNull();
Assertions.assertThat(detail.getUseState()) Assertions.assertThat(detail.getUseState())
.as("取消指定单后优惠券应恢复为未使用") .as("取消指定单后优惠券应恢复为未使用")
.isEqualTo(CouponUseState.UNUSED.getCode()); .isEqualTo(CouponUseState.UNUSED.getCode());
Assertions.assertThat(detail.getUseTime()).isNull();
} finally { } finally {
CustomSecurityContextHolder.remove(); CustomSecurityContextHolder.remove();
} }
@@ -172,6 +184,11 @@ class WxCustomSpecifiedOrderApiTest extends WxCustomOrderApiTestSupport {
Assertions.assertThat(order.getFinalAmount()).isEqualByComparingTo(expectedNet); Assertions.assertThat(order.getFinalAmount()).isEqualByComparingTo(expectedNet);
Assertions.assertThat(order.getDiscountAmount()).isEqualByComparingTo(discount); Assertions.assertThat(order.getDiscountAmount()).isEqualByComparingTo(discount);
PlayCouponDetailsEntity detailAfterOrder = couponDetailsService.getById(couponDetailId);
Assertions.assertThat(detailAfterOrder).isNotNull();
Assertions.assertThat(detailAfterOrder.getUseState()).isEqualTo(CouponUseState.USED.getCode());
Assertions.assertThat(detailAfterOrder.getUseTime()).isNotNull();
verify(wxCustomMpService).sendCreateOrderMessage( verify(wxCustomMpService).sendCreateOrderMessage(
eq(ApiTestDataSeeder.DEFAULT_TENANT_ID), eq(ApiTestDataSeeder.DEFAULT_TENANT_ID),
eq(ApiTestDataSeeder.DEFAULT_CLERK_OPEN_ID), eq(ApiTestDataSeeder.DEFAULT_CLERK_OPEN_ID),
@@ -194,6 +211,10 @@ class WxCustomSpecifiedOrderApiTest extends WxCustomOrderApiTestSupport {
Assertions.assertThat(order.getEstimatedRevenue()).isEqualByComparingTo(expectedRevenue); Assertions.assertThat(order.getEstimatedRevenue()).isEqualByComparingTo(expectedRevenue);
assertCouponUsed(couponDetailId); assertCouponUsed(couponDetailId);
PlayCouponDetailsEntity detailAfterComplete = couponDetailsService.getById(couponDetailId);
Assertions.assertThat(detailAfterComplete).isNotNull();
Assertions.assertThat(detailAfterComplete.getUseState()).isEqualTo(CouponUseState.USED.getCode());
Assertions.assertThat(detailAfterComplete.getUseTime()).isNotNull();
} finally { } finally {
CustomSecurityContextHolder.remove(); CustomSecurityContextHolder.remove();
} }
@@ -273,9 +294,11 @@ class WxCustomSpecifiedOrderApiTest extends WxCustomOrderApiTestSupport {
Assertions.assertThat(cancelled.getOrderStatus()).isEqualTo(OrderConstant.OrderStatus.CANCELLED.getCode()); Assertions.assertThat(cancelled.getOrderStatus()).isEqualTo(OrderConstant.OrderStatus.CANCELLED.getCode());
PlayCouponDetailsEntity detail = couponDetailsService.getById(couponDetailId); PlayCouponDetailsEntity detail = couponDetailsService.getById(couponDetailId);
Assertions.assertThat(detail).isNotNull();
Assertions.assertThat(detail.getUseState()) Assertions.assertThat(detail.getUseState())
.as("强制取消指定单后优惠券应恢复为未使用") .as("强制取消指定单后优惠券应恢复为未使用")
.isEqualTo(CouponUseState.UNUSED.getCode()); .isEqualTo(CouponUseState.UNUSED.getCode());
Assertions.assertThat(detail.getUseTime()).isNull();
} finally { } finally {
CustomSecurityContextHolder.remove(); CustomSecurityContextHolder.remove();
} }