Files
peipei-backend/play-admin/src/test/java/com/starry/admin/api/WxCustomRandomOrderApiTest.java
2025-11-14 00:58:12 -05:00

691 lines
36 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package com.starry.admin.api;
import static org.hamcrest.Matchers.hasSize;
import static org.mockito.ArgumentMatchers.anyList;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.verify;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import com.fasterxml.jackson.databind.JsonNode;
import com.starry.admin.common.apitest.ApiTestDataSeeder;
import com.starry.admin.common.task.OverdueOrderHandlerTask;
import com.starry.admin.modules.clerk.module.entity.PlayClerkLevelInfoEntity;
import com.starry.admin.modules.order.module.constant.OrderConstant;
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.entity.PlayCouponDetailsEntity;
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.withdraw.entity.EarningsLineEntity;
import com.starry.admin.modules.withdraw.enums.EarningsType;
import com.starry.admin.utils.SecurityUtils;
import com.starry.common.constant.Constants;
import com.starry.common.context.CustomSecurityContextHolder;
import com.starry.common.utils.IdUtils;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDateTime;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MvcResult;
class WxCustomRandomOrderApiTest extends WxCustomOrderApiTestSupport {
@MockBean
private NotificationSender notificationSender;
@MockBean
private WxCustomMpService wxCustomMpService;
@MockBean
private OverdueOrderHandlerTask overdueOrderHandlerTask;
@org.springframework.beans.factory.annotation.Autowired
private com.starry.admin.modules.order.service.IPlayOrderInfoService orderInfoService;
@Test
void randomOrderFailsWhenBalanceInsufficient() throws Exception {
SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
String remark = "API random insufficient " + IdUtils.getUuid();
try {
setCustomerBalance(BigDecimal.ZERO);
String customerToken = wxTokenService.createWxUserToken(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID);
customUserInfoService.updateTokenById(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID, customerToken);
ensureTenantContext();
long beforeCount = playOrderInfoService.lambdaQuery()
.eq(PlayOrderInfoEntity::getPurchaserBy, ApiTestDataSeeder.DEFAULT_CUSTOMER_ID)
.eq(PlayOrderInfoEntity::getPlaceType, OrderConstant.PlaceType.RANDOM.getCode())
.count();
String payload = "{" +
"\"sex\":\"2\"," +
"\"levelId\":\"" + ApiTestDataSeeder.DEFAULT_CLERK_LEVEL_ID + "\"," +
"\"commodityId\":\"" + ApiTestDataSeeder.DEFAULT_COMMODITY_ID + "\"," +
"\"commodityQuantity\":1," +
"\"weiChatCode\":\"apitest-customer-wx\"," +
"\"excludeHistory\":\"0\"," +
"\"couponIds\":[]," +
"\"remark\":\"" + remark + "\"" +
"}";
mockMvc.perform(post("/wx/custom/order/random")
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT)
.header(Constants.CUSTOM_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + customerToken)
.contentType(MediaType.APPLICATION_JSON)
.content(payload))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(998));
ensureTenantContext();
long afterCount = playOrderInfoService.lambdaQuery()
.eq(PlayOrderInfoEntity::getPurchaserBy, ApiTestDataSeeder.DEFAULT_CUSTOMER_ID)
.eq(PlayOrderInfoEntity::getPlaceType, OrderConstant.PlaceType.RANDOM.getCode())
.count();
Assertions.assertThat(afterCount).isEqualTo(beforeCount);
} finally {
CustomSecurityContextHolder.remove();
}
}
@Test
// 测试用例:客户带随机下单请求命中默认等级与服务,接口应返回成功文案,
// 并新增一条处于待接单状态的随机订单,金额、商品信息与 remark 与提交参数保持一致。
void randomOrderCreatesPendingOrder() throws Exception {
SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
try {
resetCustomerBalance();
String rawToken = wxTokenService.createWxUserToken(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID);
customUserInfoService.updateTokenById(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID, rawToken);
String remark = "API test random order " + IdUtils.getUuid();
ensureTenantContext();
long beforeCount = playOrderInfoService.lambdaQuery()
.eq(PlayOrderInfoEntity::getPurchaserBy, ApiTestDataSeeder.DEFAULT_CUSTOMER_ID)
.eq(PlayOrderInfoEntity::getPlaceType, OrderConstant.PlaceType.RANDOM.getCode())
.count();
String payload = "{" +
"\"sex\":\"2\"," +
"\"levelId\":\"" + ApiTestDataSeeder.DEFAULT_CLERK_LEVEL_ID + "\"," +
"\"commodityId\":\"" + ApiTestDataSeeder.DEFAULT_COMMODITY_ID + "\"," +
"\"commodityQuantity\":1," +
"\"weiChatCode\":\"apitest-customer-wx\"," +
"\"excludeHistory\":\"0\"," +
"\"couponIds\":[]," +
"\"remark\":\"" + remark + "\"" +
"}";
mockMvc.perform(post("/wx/custom/order/random")
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT)
.header(Constants.CUSTOM_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + rawToken)
.contentType(MediaType.APPLICATION_JSON)
.content(payload))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200))
.andExpect(jsonPath("$.data").value("下单成功"));
ensureTenantContext();
long afterCount = playOrderInfoService.lambdaQuery()
.eq(PlayOrderInfoEntity::getPurchaserBy, ApiTestDataSeeder.DEFAULT_CUSTOMER_ID)
.eq(PlayOrderInfoEntity::getPlaceType, OrderConstant.PlaceType.RANDOM.getCode())
.count();
Assertions.assertThat(afterCount).isEqualTo(beforeCount + 1);
PlayOrderInfoEntity latest = playOrderInfoService.lambdaQuery()
.eq(PlayOrderInfoEntity::getPurchaserBy, ApiTestDataSeeder.DEFAULT_CUSTOMER_ID)
.eq(PlayOrderInfoEntity::getPlaceType, OrderConstant.PlaceType.RANDOM.getCode())
.eq(PlayOrderInfoEntity::getRemark, remark)
.orderByDesc(PlayOrderInfoEntity::getCreatedTime)
.last("limit 1")
.one();
Assertions.assertThat(latest).isNotNull();
Assertions.assertThat(latest.getOrderStatus()).isEqualTo(OrderConstant.OrderStatus.PENDING.getCode());
Assertions.assertThat(latest.getCommodityId()).isEqualTo(ApiTestDataSeeder.DEFAULT_COMMODITY_ID);
Assertions.assertThat(latest.getOrderMoney()).isNotNull();
} finally {
CustomSecurityContextHolder.remove();
}
}
@Test
void randomOrderCancellationReleasesCoupon() throws Exception {
SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
String remark = "API random cancel coupon " + IdUtils.getUuid();
BigDecimal discount = new BigDecimal("15.00");
try {
resetCustomerBalance();
String customerToken = wxTokenService.createWxUserToken(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID);
customUserInfoService.updateTokenById(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID, customerToken);
String couponInfoId = createCouponId("cpn-rc-");
ensureFixedReductionCoupon(
couponInfoId,
OrderConstant.PlaceType.RANDOM,
discount,
new BigDecimal("60.00"),
"0");
String couponDetailId = claimCouponForOrder(couponInfoId, OrderConstant.PlaceType.RANDOM, customerToken);
String payload = "{" +
"\"sex\":\"2\"," +
"\"levelId\":\"" + ApiTestDataSeeder.DEFAULT_CLERK_LEVEL_ID + "\"," +
"\"commodityId\":\"" + ApiTestDataSeeder.DEFAULT_COMMODITY_ID + "\"," +
"\"commodityQuantity\":1," +
"\"weiChatCode\":\"apitest-customer-wx\"," +
"\"excludeHistory\":\"0\"," +
"\"couponIds\":[\"" + couponDetailId + "\"]," +
"\"remark\":\"" + remark + "\"" +
"}";
mockMvc.perform(post("/wx/custom/order/random")
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT)
.header(Constants.CUSTOM_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + customerToken)
.contentType(MediaType.APPLICATION_JSON)
.content(payload))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200));
ensureTenantContext();
PlayOrderInfoEntity order = playOrderInfoService.lambdaQuery()
.eq(PlayOrderInfoEntity::getPurchaserBy, ApiTestDataSeeder.DEFAULT_CUSTOMER_ID)
.eq(PlayOrderInfoEntity::getRemark, remark)
.orderByDesc(PlayOrderInfoEntity::getCreatedTime)
.last("limit 1")
.one();
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 = "{" +
"\"orderId\":\"" + order.getId() + "\"," +
"\"refundReason\":\"测试取消\"," +
"\"images\":[]" +
"}";
mockMvc.perform(post("/wx/custom/order/cancellation")
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT)
.header(Constants.CUSTOM_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + customerToken)
.contentType(MediaType.APPLICATION_JSON)
.content(cancelPayload))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200))
.andExpect(jsonPath("$.data").value("取消成功"));
ensureTenantContext();
PlayOrderInfoEntity cancelled = playOrderInfoService.selectOrderInfoById(order.getId());
Assertions.assertThat(cancelled.getOrderStatus()).isEqualTo(OrderConstant.OrderStatus.CANCELLED.getCode());
PlayCouponDetailsEntity detail = couponDetailsService.getById(couponDetailId);
Assertions.assertThat(detail).isNotNull();
Assertions.assertThat(detail.getUseState())
.as("取消订单后优惠券应恢复为未使用")
.isEqualTo(CouponUseState.UNUSED.getCode());
Assertions.assertThat(detail.getUseTime()).isNull();
} finally {
CustomSecurityContextHolder.remove();
}
}
@Test
// 测试用例:订单已接单后由管理员强制取消,也应释放所使用的优惠券
void randomOrderForceCancelReleasesCoupon() throws Exception {
SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
String remark = "API random force cancel " + IdUtils.getUuid();
BigDecimal discount = new BigDecimal("12.00");
try {
reset(notificationSender);
resetCustomerBalance();
String customerToken = wxTokenService.createWxUserToken(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID);
customUserInfoService.updateTokenById(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID, customerToken);
String clerkToken = wxTokenService.createWxUserToken(ApiTestDataSeeder.DEFAULT_CLERK_ID);
clerkUserInfoService.updateTokenById(ApiTestDataSeeder.DEFAULT_CLERK_ID, clerkToken);
String couponInfoId = createCouponId("cpn-rf-");
ensureFixedReductionCoupon(
couponInfoId,
OrderConstant.PlaceType.RANDOM,
discount,
new BigDecimal("60.00"),
"0");
String couponDetailId = claimCouponForOrder(couponInfoId, OrderConstant.PlaceType.RANDOM, customerToken);
String payload = "{" +
"\"sex\":\"2\"," +
"\"levelId\":\"" + ApiTestDataSeeder.DEFAULT_CLERK_LEVEL_ID + "\"," +
"\"commodityId\":\"" + ApiTestDataSeeder.DEFAULT_COMMODITY_ID + "\"," +
"\"commodityQuantity\":1," +
"\"weiChatCode\":\"apitest-customer-wx\"," +
"\"excludeHistory\":\"0\"," +
"\"couponIds\":[\"" + couponDetailId + "\"]," +
"\"remark\":\"" + remark + "\"" +
"}";
mockMvc.perform(post("/wx/custom/order/random")
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT)
.header(Constants.CUSTOM_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + customerToken)
.contentType(MediaType.APPLICATION_JSON)
.content(payload))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200));
ensureTenantContext();
PlayOrderInfoEntity order = playOrderInfoService.lambdaQuery()
.eq(PlayOrderInfoEntity::getPurchaserBy, ApiTestDataSeeder.DEFAULT_CUSTOMER_ID)
.eq(PlayOrderInfoEntity::getRemark, remark)
.orderByDesc(PlayOrderInfoEntity::getCreatedTime)
.last("limit 1")
.one();
Assertions.assertThat(order).isNotNull();
mockMvc.perform(get("/wx/clerk/order/accept")
.param("id", order.getId())
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT)
.header(Constants.CLERK_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + clerkToken))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200))
.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();
orderInfoService.forceCancelOngoingOrder(
"2",
ApiTestDataSeeder.DEFAULT_ADMIN_USER_ID,
order.getId(),
order.getFinalAmount(),
"管理员强制取消",
java.util.Collections.emptyList());
ensureTenantContext();
PlayOrderInfoEntity cancelled = playOrderInfoService.selectOrderInfoById(order.getId());
Assertions.assertThat(cancelled.getOrderStatus()).isEqualTo(OrderConstant.OrderStatus.CANCELLED.getCode());
PlayCouponDetailsEntity detail = couponDetailsService.getById(couponDetailId);
Assertions.assertThat(detail).isNotNull();
Assertions.assertThat(detail.getUseState())
.as("强制取消订单后优惠券应恢复为未使用")
.isEqualTo(CouponUseState.UNUSED.getCode());
Assertions.assertThat(detail.getUseTime()).isNull();
} finally {
CustomSecurityContextHolder.remove();
}
}
@Test
// 测试用例:随机单携带店铺承担的满减券下单,需依据折后金额推送通知,
// 完整履约后优惠券应置为已使用且收益与预计工资保持一致。
void randomOrderLifecycleWithCouponAdjustsRevenueAndNotifications() throws Exception {
SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
String remark = "API random coupon " + IdUtils.getUuid();
BigDecimal discount = new BigDecimal("20.00");
try {
reset(notificationSender);
resetCustomerBalance();
String customerToken = wxTokenService.createWxUserToken(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID);
customUserInfoService.updateTokenById(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID, customerToken);
String clerkToken = wxTokenService.createWxUserToken(ApiTestDataSeeder.DEFAULT_CLERK_ID);
clerkUserInfoService.updateTokenById(ApiTestDataSeeder.DEFAULT_CLERK_ID, clerkToken);
BigDecimal grossAmount = ApiTestDataSeeder.DEFAULT_COMMODITY_PRICE;
String couponInfoId = createCouponId("cpn-r-");
ensureFixedReductionCoupon(
couponInfoId,
OrderConstant.PlaceType.RANDOM,
discount,
new BigDecimal("60.00"),
"0");
String couponDetailId = claimCouponForOrder(couponInfoId, OrderConstant.PlaceType.RANDOM, customerToken);
String payload = "{" +
"\"sex\":\"2\"," +
"\"levelId\":\"" + ApiTestDataSeeder.DEFAULT_CLERK_LEVEL_ID + "\"," +
"\"commodityId\":\"" + ApiTestDataSeeder.DEFAULT_COMMODITY_ID + "\"," +
"\"commodityQuantity\":1," +
"\"weiChatCode\":\"apitest-customer-wx\"," +
"\"excludeHistory\":\"0\"," +
"\"couponIds\":[\"" + couponDetailId + "\"]," +
"\"remark\":\"" + remark + "\"" +
"}";
mockMvc.perform(post("/wx/custom/order/random")
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT)
.header(Constants.CUSTOM_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + customerToken)
.contentType(MediaType.APPLICATION_JSON)
.content(payload))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200))
.andExpect(jsonPath("$.data").value("下单成功"));
ensureTenantContext();
PlayOrderInfoEntity order = playOrderInfoService.lambdaQuery()
.eq(PlayOrderInfoEntity::getPurchaserBy, ApiTestDataSeeder.DEFAULT_CUSTOMER_ID)
.eq(PlayOrderInfoEntity::getRemark, remark)
.orderByDesc(PlayOrderInfoEntity::getCreatedTime)
.last("limit 1")
.one();
Assertions.assertThat(order).isNotNull();
Assertions.assertThat(order.getCouponIds()).contains(couponDetailId);
BigDecimal expectedNet = grossAmount.subtract(discount).setScale(2, RoundingMode.HALF_UP);
Assertions.assertThat(order.getFinalAmount()).isEqualByComparingTo(expectedNet);
Assertions.assertThat(order.getDiscountAmount()).isEqualByComparingTo(discount);
verify(wxCustomMpService).sendCreateOrderMessageBatch(
anyList(),
eq(order.getOrderNo()),
eq(expectedNet.toString()),
eq(order.getCommodityName()),
eq(order.getId()),
eq(order.getPlaceType()),
eq(order.getRewardType()));
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")
.param("id", orderId)
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT)
.header(Constants.CLERK_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + clerkToken))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200))
.andExpect(jsonPath("$.data").value("成功"));
verify(notificationSender).sendOrderMessageAsync(argThat(o -> o.getId().equals(orderId)));
mockMvc.perform(get("/wx/clerk/order/start")
.param("id", orderId)
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT)
.header(Constants.CLERK_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + clerkToken))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200))
.andExpect(jsonPath("$.data").value("成功"));
mockMvc.perform(get("/wx/custom/order/end")
.param("id", orderId)
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT)
.header(Constants.CUSTOM_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + customerToken))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200))
.andExpect(jsonPath("$.data").value("成功"));
verify(notificationSender).sendOrderFinishMessageAsync(argThat(o -> orderId.equals(o.getId())));
ensureTenantContext();
PlayOrderInfoEntity completedOrder = playOrderInfoService.selectOrderInfoById(orderId);
int ratio = completedOrder.getEstimatedRevenueRatio();
BigDecimal baseRevenue = grossAmount
.multiply(BigDecimal.valueOf(ratio))
.divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP);
PlayCouponDetailsReturnVo detail = couponDetailsService.selectPlayCouponDetailsById(couponDetailId);
BigDecimal clerkDiscount = BigDecimal.ZERO;
if (detail != null && "0".equals(detail.getAttributionDiscounts())) {
BigDecimal discountAmount = detail.getDiscountAmount() == null ? BigDecimal.ZERO : detail.getDiscountAmount();
clerkDiscount = discountAmount;
}
BigDecimal expectedRevenue = baseRevenue.subtract(clerkDiscount).max(BigDecimal.ZERO).setScale(2, RoundingMode.HALF_UP);
Assertions.assertThat(completedOrder.getEstimatedRevenue()).isEqualByComparingTo(expectedRevenue);
EarningsLineEntity earningsLine = earningsService.lambdaQuery()
.eq(EarningsLineEntity::getOrderId, orderId)
.last("limit 1")
.one();
Assertions.assertThat(earningsLine).isNotNull();
Assertions.assertThat(earningsLine.getAmount()).isEqualByComparingTo(expectedRevenue);
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 {
CustomSecurityContextHolder.remove();
}
}
@Test
// 测试用例:模拟随机订单完整生命周期——客户下单、陪玩师接单/开局、客户完结,
// 期间验证微信通知被触发、收益记录生成、冻结解冻时间正确并校准日终统计接口返回的订单数、GMV 与预计收益。
void randomOrderLifecycleGeneratesEarningsAndNotifications() throws Exception {
SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
String remark = "API random lifecycle " + IdUtils.getUuid();
try {
resetCustomerBalance();
String customerToken = wxTokenService.createWxUserToken(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID);
customUserInfoService.updateTokenById(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID, customerToken);
String clerkToken = wxTokenService.createWxUserToken(ApiTestDataSeeder.DEFAULT_CLERK_ID);
clerkUserInfoService.updateTokenById(ApiTestDataSeeder.DEFAULT_CLERK_ID, clerkToken);
LocalDateTime overviewWindowStart = LocalDateTime.now().minusMinutes(5);
LocalDateTime overviewWindowEnd = LocalDateTime.now().plusMinutes(5);
OverviewSnapshot overviewBefore = fetchOverview(overviewWindowStart, overviewWindowEnd);
String orderId = placeRandomOrder(remark, customerToken);
reset(notificationSender);
ensureTenantContext();
mockMvc.perform(get("/wx/clerk/order/accept")
.param("id", orderId)
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT)
.header(Constants.CLERK_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + clerkToken))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200))
.andExpect(jsonPath("$.data").value("成功"));
verify(notificationSender).sendOrderMessageAsync(argThat(order -> order.getId().equals(orderId)));
reset(notificationSender);
ensureTenantContext();
mockMvc.perform(get("/wx/clerk/order/start")
.param("id", orderId)
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT)
.header(Constants.CLERK_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + clerkToken))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200))
.andExpect(jsonPath("$.data").value("成功"));
ensureTenantContext();
long earningsBefore = earningsService.lambdaQuery()
.eq(EarningsLineEntity::getOrderId, orderId)
.count();
Assertions.assertThat(earningsBefore).isZero();
ensureTenantContext();
mockMvc.perform(get("/wx/custom/order/end")
.param("id", orderId)
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT)
.header(Constants.CUSTOM_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + customerToken))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200))
.andExpect(jsonPath("$.data").value("成功"));
verify(notificationSender).sendOrderFinishMessageAsync(argThat(order -> order.getId().equals(orderId)));
ensureTenantContext();
long earningsAfter = earningsService.lambdaQuery()
.eq(EarningsLineEntity::getOrderId, orderId)
.count();
Assertions.assertThat(earningsAfter).isEqualTo(1);
ensureTenantContext();
PlayOrderInfoEntity completedOrder = playOrderInfoService.selectOrderInfoById(orderId);
Assertions.assertThat(completedOrder.getOrderStatus()).isEqualTo(OrderConstant.OrderStatus.COMPLETED.getCode());
Assertions.assertThat(completedOrder.getEstimatedRevenue()).isNotNull();
EarningsLineEntity earningsLine = earningsService.lambdaQuery()
.eq(EarningsLineEntity::getOrderId, orderId)
.one();
Assertions.assertThat(earningsLine).isNotNull();
Assertions.assertThat(earningsLine.getAmount()).isEqualByComparingTo(completedOrder.getEstimatedRevenue());
int freezeHours = freezePolicyService.resolveFreezeHours(
ApiTestDataSeeder.DEFAULT_TENANT_ID,
ApiTestDataSeeder.DEFAULT_CLERK_ID);
LocalDateTime expectedUnlock = completedOrder.getOrderEndTime().plusHours(freezeHours);
Assertions.assertThat(earningsLine.getUnlockTime()).isEqualTo(expectedUnlock);
Assertions.assertThat(earningsLine.getUnlockTime()).isAfter(LocalDateTime.now().minusMinutes(5));
Assertions.assertThat(earningsLine.getEarningType())
.isIn(EarningsType.ORDER, EarningsType.ADJUSTMENT);
OverviewSnapshot overviewAfter = fetchOverview(overviewWindowStart, overviewWindowEnd);
Assertions.assertThat(overviewAfter.totalOrderCount - overviewBefore.totalOrderCount)
.isEqualTo(1);
Assertions.assertThat(overviewAfter.totalGmv.subtract(overviewBefore.totalGmv).setScale(2, RoundingMode.HALF_UP))
.isEqualByComparingTo(completedOrder.getFinalAmount());
Assertions.assertThat(overviewAfter.totalEstimatedRevenue.subtract(overviewBefore.totalEstimatedRevenue).setScale(2, RoundingMode.HALF_UP))
.isEqualByComparingTo(completedOrder.getEstimatedRevenue());
Assertions.assertThat(overviewAfter.clerkOrderCount - overviewBefore.clerkOrderCount)
.isEqualTo(1);
Assertions.assertThat(overviewAfter.clerkGmv.subtract(overviewBefore.clerkGmv).setScale(2, RoundingMode.HALF_UP))
.isEqualByComparingTo(completedOrder.getFinalAmount());
} finally {
CustomSecurityContextHolder.remove();
}
}
private OverviewSnapshot fetchOverview(LocalDateTime start, LocalDateTime end) throws Exception {
String payload = "{" +
"\"includeSummary\":true," +
"\"includeRankings\":true," +
"\"limit\":5," +
"\"endOrderTime\":[\"" + start.format(DATE_TIME_FORMATTER) + "\",\""
+ end.format(DATE_TIME_FORMATTER) + "\"]" +
"}";
MvcResult result = mockMvc.perform(post("/statistics/performance/overview")
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT)
.contentType(MediaType.APPLICATION_JSON)
.content(payload))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200))
.andReturn();
JsonNode data = objectMapper.readTree(result.getResponse().getContentAsString()).path("data");
OverviewSnapshot snapshot = new OverviewSnapshot();
JsonNode summary = data.path("summary");
snapshot.totalOrderCount = summary.path("totalOrderCount").asInt();
snapshot.totalGmv = new BigDecimal(summary.path("totalGmv").asText("0"));
snapshot.totalEstimatedRevenue = new BigDecimal(summary.path("totalEstimatedRevenue").asText("0"));
JsonNode rankings = data.path("rankings");
if (rankings.isArray()) {
for (JsonNode node : rankings) {
if (ApiTestDataSeeder.DEFAULT_CLERK_ID.equals(node.path("clerkId").asText())) {
snapshot.clerkOrderCount = node.path("orderCount").asInt();
snapshot.clerkGmv = new BigDecimal(node.path("gmv").asText("0"));
break;
}
}
}
return snapshot;
}
private static class OverviewSnapshot {
private int totalOrderCount;
private BigDecimal totalGmv = BigDecimal.ZERO;
private BigDecimal totalEstimatedRevenue = BigDecimal.ZERO;
private int clerkOrderCount;
private BigDecimal clerkGmv = BigDecimal.ZERO;
}
private String placeRandomOrder(String remark, String customerToken) throws Exception {
ensureTenantContext();
long beforeCount = playOrderInfoService.lambdaQuery()
.eq(PlayOrderInfoEntity::getPurchaserBy, ApiTestDataSeeder.DEFAULT_CUSTOMER_ID)
.count();
String payload = "{" +
"\"sex\":\"2\"," +
"\"levelId\":\"" + ApiTestDataSeeder.DEFAULT_CLERK_LEVEL_ID + "\"," +
"\"commodityId\":\"" + ApiTestDataSeeder.DEFAULT_COMMODITY_ID + "\"," +
"\"commodityQuantity\":1," +
"\"weiChatCode\":\"apitest-customer-wx\"," +
"\"excludeHistory\":\"0\"," +
"\"couponIds\":[]," +
"\"remark\":\"" + remark + "\"" +
"}";
mockMvc.perform(post("/wx/custom/order/random")
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT)
.header(Constants.CUSTOM_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + customerToken)
.contentType(MediaType.APPLICATION_JSON)
.content(payload))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200))
.andExpect(jsonPath("$.data").value("下单成功"));
ensureTenantContext();
PlayOrderInfoEntity order = playOrderInfoService.lambdaQuery()
.eq(PlayOrderInfoEntity::getPurchaserBy, ApiTestDataSeeder.DEFAULT_CUSTOMER_ID)
.eq(PlayOrderInfoEntity::getRemark, remark)
.orderByDesc(PlayOrderInfoEntity::getCreatedTime)
.last("limit 1")
.one();
Assertions.assertThat(order).isNotNull();
Assertions.assertThat(order.getPlaceType()).isEqualTo(OrderConstant.PlaceType.RANDOM.getCode());
ensureTenantContext();
long afterCount = playOrderInfoService.lambdaQuery()
.eq(PlayOrderInfoEntity::getPurchaserBy, ApiTestDataSeeder.DEFAULT_CUSTOMER_ID)
.count();
Assertions.assertThat(afterCount).isEqualTo(beforeCount + 1);
verify(wxCustomMpService).sendCreateOrderMessageBatch(
anyList(),
eq(order.getOrderNo()),
eq(order.getFinalAmount().toString()),
eq(order.getCommodityName()),
eq(order.getId()),
eq(order.getPlaceType()),
eq(order.getRewardType()));
verify(overdueOrderHandlerTask).enqueue(order.getId() + "_" + ApiTestDataSeeder.DEFAULT_TENANT_ID);
reset(wxCustomMpService);
reset(overdueOrderHandlerTask);
return order.getId();
}
}