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(); } }