API-test-in-progress

This commit is contained in:
irving
2025-11-01 15:16:45 -04:00
parent f3480b6ba0
commit 04b9960e35
12 changed files with 2253 additions and 19 deletions

View File

@@ -0,0 +1,209 @@
package com.starry.admin.api;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.starry.admin.common.apitest.ApiTestDataSeeder;
import com.starry.admin.common.exception.CustomException;
import com.starry.admin.modules.blindbox.mapper.BlindBoxPoolMapper;
import com.starry.admin.modules.blindbox.mapper.BlindBoxRewardMapper;
import com.starry.admin.modules.blindbox.module.constant.BlindBoxConfigStatus;
import com.starry.admin.modules.blindbox.module.constant.BlindBoxPoolStatus;
import com.starry.admin.modules.blindbox.module.entity.BlindBoxConfigEntity;
import com.starry.admin.modules.blindbox.module.entity.BlindBoxPoolEntity;
import com.starry.admin.modules.blindbox.module.entity.BlindBoxRewardEntity;
import com.starry.admin.modules.blindbox.service.BlindBoxConfigService;
import com.starry.admin.modules.blindbox.service.BlindBoxService;
import com.starry.admin.modules.shop.module.constant.GiftHistory;
import com.starry.admin.modules.shop.module.constant.GiftState;
import com.starry.admin.modules.shop.module.constant.GiftType;
import com.starry.admin.modules.shop.module.entity.PlayGiftInfoEntity;
import com.starry.admin.modules.shop.service.IPlayGiftInfoService;
import com.starry.admin.utils.SecurityUtils;
import com.starry.common.context.CustomSecurityContextHolder;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
class BlindBoxServiceWeightTest extends WxCustomOrderApiTestSupport {
private static final String TEST_BLIND_BOX_ID = "blindbox-apitest";
private static final String PRIMARY_GIFT_ID = ApiTestDataSeeder.DEFAULT_GIFT_ID;
private static final String SECONDARY_GIFT_ID = "gift-blindbox-secondary";
private static final int DRAW_ATTEMPT_COUNT = 1_000;
private static final int PRIMARY_WEIGHT = 80;
private static final int SECONDARY_WEIGHT = 20;
private static final double PRIMARY_RATIO_MIN = 0.68;
private static final double PRIMARY_RATIO_MAX = 0.88;
private static final double SECONDARY_RATIO_MIN = 0.12;
private static final double SECONDARY_RATIO_MAX = 0.32;
@Autowired
private BlindBoxService blindBoxService;
@Autowired
private BlindBoxConfigService blindBoxConfigService;
@Autowired
private BlindBoxPoolMapper blindBoxPoolMapper;
@Autowired
private BlindBoxRewardMapper blindBoxRewardMapper;
@Autowired
private IPlayGiftInfoService giftInfoService;
@BeforeEach
void setUpTenant() {
SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
}
@AfterEach
void clearContext() {
CustomSecurityContextHolder.remove();
}
@Test
// 测试用例:在默认租户下补齐盲盒配置以及主、副礼物后,以 80/20 的权重写入奖池,
// 连续执行 1000 次盲盒抽奖,既校验每次抽奖都有奖励返回且两种礼物都被抽中,
// 也验证主礼物命中率位于 68%~88%、副礼物命中率位于 12%~32%,确保抽奖概率符合权重设定。
void blindBoxDrawRespectsWeightDistribution() {
ensureTenantContext();
ensureBlindBoxConfig();
ensurePrimaryGift();
ensureSecondaryGift();
resetCustomerBalance();
purgeRewards();
purgePool();
insertPoolEntry(PRIMARY_GIFT_ID, PRIMARY_WEIGHT);
insertPoolEntry(SECONDARY_GIFT_ID, SECONDARY_WEIGHT);
Map<String, Integer> frequency = new HashMap<>();
for (int i = 0; i < DRAW_ATTEMPT_COUNT; i++) {
BlindBoxRewardEntity reward = blindBoxService.drawReward(
ApiTestDataSeeder.DEFAULT_TENANT_ID,
"order-" + i,
ApiTestDataSeeder.DEFAULT_CUSTOMER_ID,
TEST_BLIND_BOX_ID,
"seed-" + i);
frequency.merge(reward.getRewardGiftId(), 1, Integer::sum);
}
int primaryCount = frequency.getOrDefault(PRIMARY_GIFT_ID, 0);
int secondaryCount = frequency.getOrDefault(SECONDARY_GIFT_ID, 0);
int total = primaryCount + secondaryCount;
Assertions.assertThat(total).isEqualTo(DRAW_ATTEMPT_COUNT);
Assertions.assertThat(primaryCount).isGreaterThan(0);
Assertions.assertThat(secondaryCount).isGreaterThan(0);
double primaryRatio = primaryCount / (double) total;
double secondaryRatio = secondaryCount / (double) total;
Assertions.assertThat(primaryRatio).isBetween(PRIMARY_RATIO_MIN, PRIMARY_RATIO_MAX);
Assertions.assertThat(secondaryRatio).isBetween(SECONDARY_RATIO_MIN, SECONDARY_RATIO_MAX);
purgeRewards();
purgePool();
}
private void ensureBlindBoxConfig() {
BlindBoxConfigEntity config = blindBoxConfigService.getById(TEST_BLIND_BOX_ID);
if (config != null) {
return;
}
BlindBoxConfigEntity entity = new BlindBoxConfigEntity();
entity.setId(TEST_BLIND_BOX_ID);
entity.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
entity.setName("API盲盒");
entity.setCoverUrl("https://example.com/apitest/blindbox-cover.png");
entity.setDescription("Seeded blind box for integration tests");
entity.setPrice(new BigDecimal("19.90"));
entity.setStatus(BlindBoxConfigStatus.ENABLED.getCode());
blindBoxConfigService.save(entity);
}
private void ensureSecondaryGift() {
PlayGiftInfoEntity existing = findGift(SECONDARY_GIFT_ID);
if (existing != null) {
return;
}
PlayGiftInfoEntity entity = new PlayGiftInfoEntity();
entity.setId(SECONDARY_GIFT_ID);
entity.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
entity.setHistory(GiftHistory.CURRENT.getCode());
entity.setName("API盲盒奖励");
entity.setType(GiftType.NORMAL.getCode());
entity.setUrl("https://example.com/apitest/blindbox.png");
entity.setPrice(new BigDecimal("9.99"));
entity.setUnit("CNY");
entity.setState(GiftState.ACTIVE.getCode());
entity.setListingTime(LocalDateTime.now());
entity.setRemark("Seeded secondary gift for blind box tests");
giftInfoService.save(entity);
}
private void ensurePrimaryGift() {
PlayGiftInfoEntity existing = findGift(PRIMARY_GIFT_ID);
if (existing != null) {
return;
}
PlayGiftInfoEntity entity = new PlayGiftInfoEntity();
entity.setId(PRIMARY_GIFT_ID);
entity.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
entity.setHistory(GiftHistory.CURRENT.getCode());
entity.setName(ApiTestDataSeeder.DEFAULT_GIFT_NAME);
entity.setType(GiftType.NORMAL.getCode());
entity.setUrl("https://example.com/apitest/gift-basic.png");
entity.setPrice(new BigDecimal("15.00"));
entity.setUnit("CNY");
entity.setState(GiftState.ACTIVE.getCode());
entity.setListingTime(LocalDateTime.now());
entity.setRemark("Seeded default gift for blind box tests");
giftInfoService.save(entity);
}
private void insertPoolEntry(String giftId, int weight) {
PlayGiftInfoEntity gift = findGift(giftId);
if (gift == null) {
throw new IllegalStateException("Expected gift to be seeded: " + giftId);
}
BlindBoxPoolEntity entry = new BlindBoxPoolEntity();
entry.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
entry.setBlindBoxId(TEST_BLIND_BOX_ID);
entry.setRewardGiftId(giftId);
entry.setRewardPrice(gift.getPrice());
entry.setWeight(weight);
entry.setRemainingStock(null);
entry.setValidFrom(null);
entry.setValidTo(null);
entry.setStatus(BlindBoxPoolStatus.ENABLED.getCode());
blindBoxPoolMapper.insert(entry);
}
private PlayGiftInfoEntity findGift(String giftId) {
try {
return giftInfoService.selectPlayGiftInfoById(giftId);
} catch (CustomException ignored) {
return null;
}
}
private void purgePool() {
blindBoxPoolMapper.delete(new LambdaQueryWrapper<BlindBoxPoolEntity>()
.eq(BlindBoxPoolEntity::getTenantId, ApiTestDataSeeder.DEFAULT_TENANT_ID)
.eq(BlindBoxPoolEntity::getBlindBoxId, TEST_BLIND_BOX_ID));
}
private void purgeRewards() {
blindBoxRewardMapper.delete(new LambdaQueryWrapper<BlindBoxRewardEntity>()
.eq(BlindBoxRewardEntity::getTenantId, ApiTestDataSeeder.DEFAULT_TENANT_ID)
.eq(BlindBoxRewardEntity::getBlindBoxId, TEST_BLIND_BOX_ID)
.eq(BlindBoxRewardEntity::getCustomerId, ApiTestDataSeeder.DEFAULT_CUSTOMER_ID));
}
}

View File

@@ -9,6 +9,7 @@ import org.junit.jupiter.api.Test;
class HealthControllerApiTest extends AbstractApiTest {
@Test
// 测试用例:模拟带上默认用户与租户请求 /health/ping 接口,校验接口必须返回 200 且 data = "pong",确认健康检查链路畅通。
void pingReturnsPong() throws Exception {
mockMvc.perform(get("/health/ping")
.header(USER_HEADER, DEFAULT_USER)

View File

@@ -0,0 +1,519 @@
package com.starry.admin.api;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.hasItem;
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.fasterxml.jackson.databind.node.ObjectNode;
import com.starry.admin.common.apitest.ApiTestDataSeeder;
import com.starry.admin.modules.clerk.module.entity.PlayClerkCommodityEntity;
import com.starry.admin.modules.clerk.module.entity.PlayClerkLevelInfoEntity;
import com.starry.admin.modules.clerk.service.IPlayClerkCommodityService;
import com.starry.admin.modules.clerk.service.IPlayClerkLevelInfoService;
import com.starry.admin.modules.order.module.constant.OrderConstant;
import com.starry.admin.modules.shop.module.entity.PlayCommodityAndLevelInfoEntity;
import com.starry.admin.modules.shop.module.entity.PlayCommodityInfoEntity;
import com.starry.admin.modules.shop.service.IPlayCommodityAndLevelInfoService;
import com.starry.admin.modules.shop.service.IPlayCommodityInfoService;
import com.starry.admin.utils.SecurityUtils;
import com.starry.common.constant.Constants;
import com.starry.common.utils.IdUtils;
import java.math.BigDecimal;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.List;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MvcResult;
class PlayCommodityInfoApiTest extends WxCustomOrderApiTestSupport {
private static final String ROOT_PARENT_ID = "00";
private static final long DEFAULT_PRICE_SORT = 1L;
private static final int SINGLE_COMMODITY_QUANTITY = 1;
private enum SwitchState {
ENABLED("1"),
DISABLED("0");
private final String code;
SwitchState(String code) {
this.code = code;
}
String getCode() {
return code;
}
}
private enum HistoryFilter {
INCLUDE("0"),
EXCLUDE("1");
private final String code;
HistoryFilter(String code) {
this.code = code;
}
String getCode() {
return code;
}
}
private enum SortPriority {
PRIMARY(1),
SECONDARY(2);
private final int value;
SortPriority(int value) {
this.value = value;
}
int getValue() {
return value;
}
}
private enum AutomaticSettlementPolicy {
DISABLED(-1),
TEN_MINUTES(600);
private final int seconds;
AutomaticSettlementPolicy(int seconds) {
this.seconds = seconds;
}
int getSeconds() {
return seconds;
}
}
@Autowired
private IPlayCommodityInfoService commodityInfoService;
@Autowired
private IPlayCommodityAndLevelInfoService commodityAndLevelInfoService;
@Autowired
private IPlayClerkCommodityService clerkCommodityService;
@Autowired
private IPlayClerkLevelInfoService clerkLevelInfoService;
private final Deque<String> commodityIdsToCleanup = new ArrayDeque<>();
private final List<String> priceIdsToCleanup = new ArrayList<>();
private final List<String> clerkCommodityIdsToCleanup = new ArrayList<>();
@AfterEach
void tearDown() {
ensureTenantContext();
if (!priceIdsToCleanup.isEmpty()) {
commodityAndLevelInfoService.removeByIds(priceIdsToCleanup);
priceIdsToCleanup.clear();
}
if (!clerkCommodityIdsToCleanup.isEmpty()) {
clerkCommodityService.removeByIds(clerkCommodityIdsToCleanup);
clerkCommodityIdsToCleanup.clear();
}
while (!commodityIdsToCleanup.isEmpty()) {
String commodityId = commodityIdsToCleanup.removeFirst();
commodityInfoService.removeById(commodityId);
}
}
@Test
// 测试用例:调用商品表头接口,验证默认店员等级会出现在表头列表中,确保管理端可正确展示等级价格列。
void tableNameEndpointReturnsClerkLevels() throws Exception {
ensureTenantContext();
PlayClerkLevelInfoEntity level =
clerkLevelInfoService.getById(ApiTestDataSeeder.DEFAULT_CLERK_LEVEL_ID);
mockMvc.perform(get("/shop/commodity/getTableName")
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200))
.andExpect(jsonPath("$.data[*].prop", hasItem(ApiTestDataSeeder.DEFAULT_CLERK_LEVEL_ID)))
.andExpect(jsonPath("$.data[*].name", hasItem(level.getName())));
}
@Test
// 测试用例:查询所有商品树,验证种子数据的父子节点与对应等级价格一起返回,确保层级结构与定价展示正确。
void listAllIncludesSeedCommodityWithLevelPrice() throws Exception {
ensureTenantContext();
MvcResult result = mockMvc.perform(get("/shop/commodity/listAll")
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200))
.andReturn();
JsonNode root = objectMapper.readTree(result.getResponse().getContentAsString());
JsonNode data = root.get("data");
assertThat(data.isArray()).isTrue();
JsonNode parentNode = findNodeById(data, ApiTestDataSeeder.DEFAULT_COMMODITY_PARENT_ID);
assertThat(parentNode).isNotNull();
JsonNode children = parentNode.get("children");
JsonNode childNode = findNodeById(children, ApiTestDataSeeder.DEFAULT_COMMODITY_ID);
assertThat(childNode).isNotNull();
BigDecimal price = new BigDecimal(childNode.get(ApiTestDataSeeder.DEFAULT_CLERK_LEVEL_ID).asText());
assertThat(price).isEqualByComparingTo(ApiTestDataSeeder.DEFAULT_COMMODITY_PRICE);
}
@Test
// 测试用例:调用价格更新接口,为新创建的子商品配置等级价格,验证价格落库并可复查,确保后台配置生效。
void updateInfoAssignsLevelPricing() throws Exception {
ensureTenantContext();
String parentId = generateId("svc-parent-");
String childId = generateId("svc-child-");
PlayCommodityInfoEntity parent = createCommodity(
parentId,
ROOT_PARENT_ID,
"API测试父类目",
"N/A",
SwitchState.ENABLED,
SortPriority.PRIMARY,
AutomaticSettlementPolicy.DISABLED);
PlayCommodityInfoEntity child = createCommodity(
childId,
parent.getId(),
"API测试子项目",
"45min",
SwitchState.ENABLED,
SortPriority.PRIMARY,
AutomaticSettlementPolicy.DISABLED);
ObjectNode payload = objectMapper.createObjectNode();
payload.put("id", child.getId());
payload.put(ApiTestDataSeeder.DEFAULT_CLERK_LEVEL_ID, "188.50");
mockMvc.perform(post("/shop/commodity/updateInfo")
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT)
.contentType(MediaType.APPLICATION_JSON)
.content(payload.toString()))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200));
PlayCommodityAndLevelInfoEntity pricing = commodityAndLevelInfoService.lambdaQuery()
.eq(PlayCommodityAndLevelInfoEntity::getCommodityId, child.getId())
.eq(PlayCommodityAndLevelInfoEntity::getLevelId, ApiTestDataSeeder.DEFAULT_CLERK_LEVEL_ID)
.one();
assertThat(pricing).isNotNull();
priceIdsToCleanup.add(pricing.getId());
assertThat(pricing.getPrice()).isEqualByComparingTo(new BigDecimal("188.50"));
}
@Test
// 测试用例:使用商品修改接口把自动结算等待时长从不限时(-1调整为10分钟验证更新后查询返回新的配置。
void updateEndpointSwitchesAutomaticSettlement() throws Exception {
ensureTenantContext();
String parentId = generateId("svc-parent-");
String childId = generateId("svc-child-");
PlayCommodityInfoEntity parent = createCommodity(
parentId,
ROOT_PARENT_ID,
"自动结算父类目",
"N/A",
SwitchState.ENABLED,
SortPriority.PRIMARY,
AutomaticSettlementPolicy.DISABLED);
PlayCommodityInfoEntity child = createCommodity(
childId,
parent.getId(),
"自动结算子项",
"30min",
SwitchState.ENABLED,
SortPriority.PRIMARY,
AutomaticSettlementPolicy.DISABLED);
PlayCommodityInfoEntity beforeUpdate = commodityInfoService.getById(child.getId());
assertThat(beforeUpdate.getAutomaticSettlementDuration())
.isEqualTo(AutomaticSettlementPolicy.DISABLED.getSeconds());
PlayCommodityInfoEntity updateBody = new PlayCommodityInfoEntity();
updateBody.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
updateBody.setPId(parent.getId());
updateBody.setItemType(child.getItemType());
updateBody.setItemName(child.getItemName());
updateBody.setServiceDuration("30min");
updateBody.setAutomaticSettlementDuration(AutomaticSettlementPolicy.TEN_MINUTES.getSeconds());
updateBody.setEnableStace(SwitchState.ENABLED.getCode());
updateBody.setSort(SortPriority.PRIMARY.getValue());
mockMvc.perform(post("/shop/commodity/update/" + child.getId())
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(updateBody)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200));
PlayCommodityInfoEntity updated = commodityInfoService.getById(child.getId());
assertThat(updated.getAutomaticSettlementDuration())
.isEqualTo(AutomaticSettlementPolicy.TEN_MINUTES.getSeconds());
}
@Test
// 测试用例:为店员构造包含定价与未定价子项的类目,调用按等级查询接口,验证未设置价格的子项被过滤,只返回可售商品。
void queryClerkAllCommodityByLevelSkipsUnpricedChildren() throws Exception {
ensureTenantContext();
String parentId = generateId("svc-parent-");
String pricedChildId = generateId("svc-child-priced-");
String unpricedChildId = generateId("svc-child-unpriced-");
PlayCommodityInfoEntity parent = createCommodity(
parentId,
ROOT_PARENT_ID,
"等级过滤父类目",
"N/A",
SwitchState.ENABLED,
SortPriority.SECONDARY,
AutomaticSettlementPolicy.DISABLED);
PlayCommodityInfoEntity pricedChild = createCommodity(
pricedChildId,
parent.getId(),
"有定价子项",
"60min",
SwitchState.ENABLED,
SortPriority.PRIMARY,
AutomaticSettlementPolicy.DISABLED);
createCommodity(
unpricedChildId,
parent.getId(),
"无定价子项",
"60min",
SwitchState.ENABLED,
SortPriority.SECONDARY,
AutomaticSettlementPolicy.DISABLED);
PlayCommodityAndLevelInfoEntity price = createPrice(pricedChild.getId(), new BigDecimal("99.90"));
linkCommodityToClerk(parent, SwitchState.ENABLED, SortPriority.PRIMARY);
MvcResult result = mockMvc.perform(get("/wx/commodity/custom/queryClerkAllCommodityByLevel")
.param("id", ApiTestDataSeeder.DEFAULT_CLERK_LEVEL_ID)
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200))
.andReturn();
JsonNode root = objectMapper.readTree(result.getResponse().getContentAsString());
JsonNode data = root.get("data");
JsonNode parentNode = findNodeById(data, parent.getId());
assertThat(parentNode).isNotNull();
JsonNode children = parentNode.get("child");
assertThat(children.isArray()).isTrue();
JsonNode pricedNode = findNodeById(children, pricedChild.getId());
assertThat(pricedNode).isNotNull();
assertThat(pricedNode.get("price").decimalValue()).isEqualByComparingTo(new BigDecimal("99.90"));
JsonNode removedNode = findNodeById(children, unpricedChildId);
assertThat(removedNode).isNull();
}
@Test
// 测试用例:为店员同时配置上架与下架的类目,调用顾客查询接口,确认停用类目被隐藏,避免顾客看到不可售项目。
void customerCommodityQueryFiltersDisabledParents() throws Exception {
ensureTenantContext();
String enabledParentId = generateId("svc-enabled-parent-");
String disabledParentId = generateId("svc-disabled-parent-");
PlayCommodityInfoEntity enabledParent = createCommodity(
enabledParentId,
ROOT_PARENT_ID,
"顾客可见父类目",
"N/A",
SwitchState.ENABLED,
SortPriority.PRIMARY,
AutomaticSettlementPolicy.DISABLED);
PlayCommodityInfoEntity enabledChild = createCommodity(
generateId("svc-enabled-child-"),
enabledParent.getId(),
"顾客可见子项",
"30min",
SwitchState.ENABLED,
SortPriority.PRIMARY,
AutomaticSettlementPolicy.DISABLED);
PlayCommodityAndLevelInfoEntity enabledPrice = createPrice(enabledChild.getId(), new BigDecimal("45.00"));
PlayCommodityInfoEntity disabledParent = createCommodity(
disabledParentId,
ROOT_PARENT_ID,
"顾客屏蔽父类目",
"N/A",
SwitchState.ENABLED,
SortPriority.SECONDARY,
AutomaticSettlementPolicy.DISABLED);
PlayCommodityInfoEntity disabledChild = createCommodity(
generateId("svc-disabled-child-"),
disabledParent.getId(),
"顾客屏蔽子项",
"30min",
SwitchState.ENABLED,
SortPriority.PRIMARY,
AutomaticSettlementPolicy.DISABLED);
PlayCommodityAndLevelInfoEntity disabledPrice = createPrice(disabledChild.getId(), new BigDecimal("55.00"));
linkCommodityToClerk(enabledParent, SwitchState.ENABLED, SortPriority.PRIMARY);
linkCommodityToClerk(disabledParent, SwitchState.DISABLED, SortPriority.SECONDARY);
MvcResult result = mockMvc.perform(get("/wx/commodity/custom/queryClerkAllCommodity")
.param("id", ApiTestDataSeeder.DEFAULT_CLERK_ID)
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200))
.andReturn();
JsonNode root = objectMapper.readTree(result.getResponse().getContentAsString());
JsonNode data = root.get("data");
JsonNode enabledNode = findNodeById(data, enabledParent.getId());
assertThat(enabledNode).isNotNull();
JsonNode disabledNode = findNodeById(data, disabledParent.getId());
assertThat(disabledNode).isNull();
}
@Test
// 测试用例:顾客随机下单选择未配置价格的商品时,接口应返回“服务项目不存在”的业务错误,验证错误商品被拒单。
void randomOrderRejectsCommodityWithoutPricing() throws Exception {
ensureTenantContext();
resetCustomerBalance();
String customerToken = wxTokenService.createWxUserToken(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID);
customUserInfoService.updateTokenById(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID, customerToken);
PlayCommodityInfoEntity parent = createCommodity(
generateId("svc-parent-"),
ROOT_PARENT_ID,
"未定价父类目",
"N/A",
SwitchState.ENABLED,
SortPriority.PRIMARY,
AutomaticSettlementPolicy.DISABLED);
PlayCommodityInfoEntity child = createCommodity(
generateId("svc-child-"),
parent.getId(),
"未定价子项",
"45min",
SwitchState.ENABLED,
SortPriority.PRIMARY,
AutomaticSettlementPolicy.DISABLED);
ObjectNode payload = objectMapper.createObjectNode();
payload.put("sex", OrderConstant.Gender.FEMALE.getCode());
payload.put("levelId", ApiTestDataSeeder.DEFAULT_CLERK_LEVEL_ID);
payload.put("commodityId", child.getId());
payload.put("commodityQuantity", SINGLE_COMMODITY_QUANTITY);
payload.put("weiChatCode", "apitest-customer-wx");
payload.put("excludeHistory", HistoryFilter.INCLUDE.getCode());
payload.putArray("couponIds");
payload.put("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.toString()))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(500))
.andExpect(jsonPath("$.message").value("服务项目不存在"));
}
private JsonNode findNodeById(JsonNode arrayNode, String id) {
if (arrayNode == null || !arrayNode.isArray()) {
return null;
}
for (JsonNode node : arrayNode) {
if (node.has("id") && id.equals(node.get("id").asText())) {
return node;
}
}
return null;
}
private PlayCommodityInfoEntity createCommodity(
String id,
String parentId,
String name,
String duration,
SwitchState switchState,
SortPriority sortPriority,
AutomaticSettlementPolicy settlementPolicy) {
ensureTenantContext();
PlayCommodityInfoEntity entity = new PlayCommodityInfoEntity();
entity.setId(id);
entity.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
entity.setPId(parentId);
entity.setItemType("api-test");
entity.setItemName(name);
entity.setServiceDuration(duration);
entity.setAutomaticSettlementDuration(settlementPolicy.getSeconds());
entity.setEnableStace(switchState.getCode());
entity.setSort(sortPriority.getValue());
commodityInfoService.save(entity);
commodityIdsToCleanup.addFirst(entity.getId());
return entity;
}
private PlayCommodityAndLevelInfoEntity createPrice(String commodityId, BigDecimal price) {
ensureTenantContext();
PlayCommodityAndLevelInfoEntity entity = new PlayCommodityAndLevelInfoEntity();
entity.setId(IdUtils.getUuid());
entity.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
entity.setCommodityId(commodityId);
entity.setLevelId(ApiTestDataSeeder.DEFAULT_CLERK_LEVEL_ID);
entity.setPrice(price);
entity.setSort(DEFAULT_PRICE_SORT);
commodityAndLevelInfoService.save(entity);
priceIdsToCleanup.add(entity.getId());
return entity;
}
private PlayClerkCommodityEntity linkCommodityToClerk(
PlayCommodityInfoEntity parent,
SwitchState switchState,
SortPriority sortPriority) {
ensureTenantContext();
PlayClerkCommodityEntity entity = new PlayClerkCommodityEntity();
entity.setId(IdUtils.getUuid());
entity.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
entity.setClerkId(ApiTestDataSeeder.DEFAULT_CLERK_ID);
entity.setCommodityId(parent.getId());
entity.setCommodityName(parent.getItemName());
entity.setEnablingState(switchState.getCode());
entity.setSort(sortPriority.getValue());
clerkCommodityService.save(entity);
clerkCommodityIdsToCleanup.add(entity.getId());
return entity;
}
private String generateId(String prefix) {
return prefix + IdUtils.getUuid().replace("-", "");
}
@Override
protected void ensureTenantContext() {
SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
}
}

View File

@@ -10,6 +10,8 @@ import org.junit.jupiter.api.Test;
class SysTenantPackageControllerApiTest extends AbstractApiTest {
@Test
// 测试用例:以默认平台管理员身份拉取套餐精简列表,应返回 200
// 并且数据集中必须包含种子数据 pkg-basic验证套餐列表接口能正确曝光基础套餐。
void getSimpleListReturnsSeededPackage() throws Exception {
mockMvc.perform(get("/platform/package/get-simple-list")
.header(USER_HEADER, DEFAULT_USER)

View File

@@ -0,0 +1,122 @@
package com.starry.admin.api;
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.fasterxml.jackson.databind.ObjectMapper;
import com.starry.admin.common.apitest.ApiTestDataSeeder;
import com.starry.admin.modules.custom.module.entity.PlayCustomGiftInfoEntity;
import com.starry.admin.modules.custom.service.IPlayCustomGiftInfoService;
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.entity.PlayClerkGiftInfoEntity;
import com.starry.admin.modules.shop.module.entity.PlayGiftInfoEntity;
import com.starry.admin.modules.shop.service.IPlayClerkGiftInfoService;
import com.starry.admin.modules.shop.service.IPlayGiftInfoService;
import com.starry.admin.modules.withdraw.entity.EarningsLineEntity;
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 org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
class WxCustomGiftOrderApiTest extends WxCustomOrderApiTestSupport {
@Autowired
private IPlayGiftInfoService playGiftInfoService;
@Autowired
private IPlayCustomGiftInfoService playCustomGiftInfoService;
@Autowired
private IPlayClerkGiftInfoService playClerkGiftInfoService;
private final ObjectMapper objectMapper = new ObjectMapper();
@Test
// 测试用例:用户余额充足且携带有效登录态时,请求 /wx/custom/order/gift 下单指定礼物,
// 期望生成已完成的礼物奖励订单、产生对应收益记录,同时校验用户/陪玩师礼物计数与账户余额随订单金额同步更新。
void giftOrderCreatesCompletedRewardAndUpdatesGiftCounters() throws Exception {
SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
try {
resetCustomerBalance();
String customerToken = wxTokenService.createWxUserToken(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID);
customUserInfoService.updateTokenById(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID, customerToken);
PlayGiftInfoEntity gift = playGiftInfoService.selectPlayGiftInfoById(ApiTestDataSeeder.DEFAULT_GIFT_ID);
Assertions.assertThat(gift).as("seeded gift should exist").isNotNull();
int giftQuantity = 2;
String remark = "API gift order " + IdUtils.getUuid();
BigDecimal totalAmount = gift.getPrice().multiply(BigDecimal.valueOf(giftQuantity));
BigDecimal initialBalance = customUserInfoService.getById(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID).getAccountBalance();
String payload = "{" +
"\"clerkId\":\"" + ApiTestDataSeeder.DEFAULT_CLERK_ID + "\"," +
"\"giftId\":\"" + ApiTestDataSeeder.DEFAULT_GIFT_ID + "\"," +
"\"giftQuantity\":" + giftQuantity + "," +
"\"weiChatCode\":\"apitest-customer-wx\"," +
"\"remark\":\"" + remark + "\"" +
"}";
String response = mockMvc.perform(post("/wx/custom/order/gift")
.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").isNotEmpty())
.andReturn()
.getResponse()
.getContentAsString();
JsonNode root = objectMapper.readTree(response);
String orderId = root.path("data").asText();
ensureTenantContext();
PlayOrderInfoEntity order = playOrderInfoService.selectOrderInfoById(orderId);
Assertions.assertThat(order).isNotNull();
Assertions.assertThat(order.getRemark()).isEqualTo(remark);
Assertions.assertThat(order.getOrderStatus()).isEqualTo(OrderConstant.OrderStatus.COMPLETED.getCode());
Assertions.assertThat(order.getPlaceType()).isEqualTo(OrderConstant.PlaceType.REWARD.getCode());
Assertions.assertThat(order.getRewardType()).isEqualTo(OrderConstant.RewardType.GIFT.getCode());
Assertions.assertThat(order.getFinalAmount()).isEqualByComparingTo(totalAmount);
ensureTenantContext();
long earningsCount = earningsService.lambdaQuery()
.eq(EarningsLineEntity::getOrderId, orderId)
.count();
Assertions.assertThat(earningsCount).isEqualTo(1);
ensureTenantContext();
PlayCustomGiftInfoEntity customerGift = playCustomGiftInfoService.lambdaQuery()
.eq(PlayCustomGiftInfoEntity::getCustomId, ApiTestDataSeeder.DEFAULT_CUSTOMER_ID)
.eq(PlayCustomGiftInfoEntity::getGiffId, ApiTestDataSeeder.DEFAULT_GIFT_ID)
.one();
Assertions.assertThat(customerGift).isNotNull();
Assertions.assertThat(customerGift.getGiffNumber()).isEqualTo((long) giftQuantity);
ensureTenantContext();
PlayClerkGiftInfoEntity clerkGift = playClerkGiftInfoService.lambdaQuery()
.eq(PlayClerkGiftInfoEntity::getClerkId, ApiTestDataSeeder.DEFAULT_CLERK_ID)
.eq(PlayClerkGiftInfoEntity::getGiffId, ApiTestDataSeeder.DEFAULT_GIFT_ID)
.one();
Assertions.assertThat(clerkGift).isNotNull();
Assertions.assertThat(clerkGift.getGiffNumber()).isEqualTo((long) giftQuantity);
BigDecimal finalBalance = customUserInfoService.getById(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID)
.getAccountBalance();
Assertions.assertThat(finalBalance).isEqualByComparingTo(initialBalance.subtract(totalAmount));
} finally {
CustomSecurityContextHolder.remove();
}
}
}

View File

@@ -0,0 +1,202 @@
package com.starry.admin.api;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.starry.admin.common.apitest.ApiTestDataSeeder;
import com.starry.admin.modules.clerk.module.entity.PlayClerkLevelInfoEntity;
import com.starry.admin.modules.clerk.service.IPlayClerkLevelInfoService;
import com.starry.admin.modules.clerk.service.IPlayClerkUserInfoService;
import com.starry.admin.modules.custom.module.entity.PlayCustomUserInfoEntity;
import com.starry.admin.modules.custom.service.IPlayCustomUserInfoService;
import com.starry.admin.modules.order.module.constant.OrderConstant;
import com.starry.admin.modules.order.module.constant.OrderConstant.PlaceType;
import com.starry.admin.modules.order.service.IPlayOrderInfoService;
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.PlayCouponInfoEntity;
import com.starry.admin.modules.shop.module.enums.CouponClaimConditionType;
import com.starry.admin.modules.shop.module.enums.CouponDiscountType;
import com.starry.admin.modules.shop.module.enums.CouponOnlineState;
import com.starry.admin.modules.shop.module.enums.CouponValidityPeriodType;
import com.starry.admin.modules.shop.service.IPlayCouponDetailsService;
import com.starry.admin.modules.shop.service.IPlayCouponInfoService;
import com.starry.admin.modules.weichat.service.WxTokenService;
import com.starry.admin.modules.withdraw.service.IEarningsService;
import com.starry.admin.modules.withdraw.service.IFreezePolicyService;
import com.starry.admin.utils.SecurityUtils;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.format.DateTimeFormatter;
import java.util.Collections;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.util.StringUtils;
abstract class WxCustomOrderApiTestSupport extends AbstractApiTest {
protected static final DateTimeFormatter DATE_TIME_FORMATTER =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
protected final ObjectMapper objectMapper = new ObjectMapper();
@Autowired
protected WxTokenService wxTokenService;
@Autowired
protected IPlayOrderInfoService playOrderInfoService;
@Autowired
protected IPlayCustomUserInfoService customUserInfoService;
@Autowired
protected IPlayClerkUserInfoService clerkUserInfoService;
@Autowired
protected IEarningsService earningsService;
@Autowired
protected IFreezePolicyService freezePolicyService;
@Autowired
protected IPlayClerkLevelInfoService clerkLevelInfoService;
@Autowired
protected IPlayCouponInfoService couponInfoService;
@Autowired
protected IPlayCouponDetailsService couponDetailsService;
protected void resetCustomerBalance() {
BigDecimal balance = new BigDecimal("200.00");
customUserInfoService.updateAccountBalanceById(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID, balance);
customUserInfoService.lambdaUpdate()
.set(PlayCustomUserInfoEntity::getAccumulatedConsumptionAmount, BigDecimal.ZERO)
.set(PlayCustomUserInfoEntity::getAccumulatedRechargeAmount, balance)
.set(PlayCustomUserInfoEntity::getAccountState, "1")
.set(PlayCustomUserInfoEntity::getSubscribeState, "1")
.set(PlayCustomUserInfoEntity::getPurchaseState, "1")
.set(PlayCustomUserInfoEntity::getMobilePhoneState, "1")
.eq(PlayCustomUserInfoEntity::getId, ApiTestDataSeeder.DEFAULT_CUSTOMER_ID)
.update();
}
protected void ensureTenantContext() {
SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
}
protected String createCouponId(String prefix) {
String raw = com.starry.common.utils.IdUtils.getUuid().replace("-", "");
String suffix = raw.length() > 10 ? raw.substring(0, 10) : raw;
return prefix + suffix;
}
protected String ensureFixedReductionCoupon(
String couponId,
OrderConstant.PlaceType placeType,
BigDecimal discountAmount,
BigDecimal minAmount,
String attributionDiscounts) {
ensureTenantContext();
PlayCouponInfoEntity existing = couponInfoService.getById(couponId);
if (existing != null) {
return couponId;
}
PlayCouponInfoEntity coupon = new PlayCouponInfoEntity();
coupon.setId(couponId);
coupon.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
coupon.setCouponName(couponId);
coupon.setValidityPeriodType(CouponValidityPeriodType.PERMANENT.getCode());
coupon.setUseMinAmount(minAmount);
coupon.setDiscountType(CouponDiscountType.FULL_REDUCTION.getCode());
coupon.setDiscountAmount(discountAmount);
coupon.setDiscountContent("立减" + discountAmount.setScale(2, RoundingMode.HALF_UP) + "");
coupon.setAttributionDiscounts(attributionDiscounts);
coupon.setPlaceType(Collections.singletonList(placeType.getCode()));
coupon.setClerkType("0");
coupon.setCouponQuantity(200);
coupon.setIssuedQuantity(0);
coupon.setRemainingQuantity(200);
coupon.setClerkObtainedMaxQuantity(5);
coupon.setClaimConditionType(CouponClaimConditionType.ALL.code());
coupon.setCustomLevelCheckType("0");
coupon.setCustomSexCheckType("0");
coupon.setCustomLevel(Collections.emptyList());
coupon.setCustomSex(Collections.emptyList());
coupon.setCustomFollowStatusCheckType("0");
coupon.setCustomFollowStatus("1");
coupon.setNewUser("0");
coupon.setCouponOnLineState(CouponOnlineState.ONLINE.getCode());
couponInfoService.save(coupon);
return couponId;
}
protected String claimCouponForOrder(
String couponInfoId,
PlaceType placeType,
String customerToken) throws Exception {
ensureTenantContext();
mockMvc.perform(org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get("/wx/coupon/custom/obtainCoupon")
.param("id", couponInfoId)
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT)
.header(com.starry.common.constant.Constants.CUSTOM_USER_LOGIN_TOKEN,
com.starry.common.constant.Constants.TOKEN_PREFIX + customerToken))
.andExpect(org.springframework.test.web.servlet.result.MockMvcResultMatchers.status().isOk())
.andExpect(org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath("$.code").value(200))
.andExpect(org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath("$.data.success").value(true));
String levelId = placeType == PlaceType.SPECIFIED ? "" : ApiTestDataSeeder.DEFAULT_CLERK_LEVEL_ID;
String clerkId = placeType == PlaceType.SPECIFIED ? ApiTestDataSeeder.DEFAULT_CLERK_ID : "";
String queryPayload = "{" +
"\"commodityId\":\"" + ApiTestDataSeeder.DEFAULT_COMMODITY_ID + "\"," +
"\"levelId\":\"" + levelId + "\"," +
"\"clerkId\":\"" + clerkId + "\"," +
"\"placeType\":\"" + placeType.getCode() + "\"," +
"\"commodityQuantity\":1" +
"}";
MvcResult result = mockMvc.perform(org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post("/wx/coupon/custom/queryByOrder")
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT)
.header(com.starry.common.constant.Constants.CUSTOM_USER_LOGIN_TOKEN,
com.starry.common.constant.Constants.TOKEN_PREFIX + customerToken)
.contentType(MediaType.APPLICATION_JSON)
.content(queryPayload))
.andExpect(org.springframework.test.web.servlet.result.MockMvcResultMatchers.status().isOk())
.andExpect(org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath("$.code").value(200))
.andReturn();
String responseBody = result.getResponse().getContentAsString();
JsonNode dataNode = objectMapper.readTree(responseBody).path("data");
if (!dataNode.isArray()) {
throw new AssertionError("未返回优惠券列表");
}
String detailId = null;
for (JsonNode node : dataNode) {
if (!couponInfoId.equals(node.path("couponName").asText())) {
continue;
}
if (!"1".equals(node.path("available").asText())) {
throw new AssertionError("优惠券不可用: " + node.path("reasonForUnavailableUse").asText());
}
detailId = node.path("id").asText();
if (!StringUtils.hasText(detailId)) {
throw new AssertionError("优惠券详情ID缺失");
}
return detailId;
}
throw new AssertionError("未在可用列表中找到优惠券 " + couponInfoId + ",响应:" + responseBody);
}
protected void assertCouponUsed(String couponDetailId) {
ensureTenantContext();
PlayCouponDetailsEntity detail = couponDetailsService.getById(couponDetailId);
if (detail == null) {
throw new AssertionError("优惠券详情不存在: " + couponDetailId);
}
if (!CouponUseState.USED.getCode().equals(detail.getUseState())) {
throw new AssertionError("优惠券未标记为已使用");
}
}
}

View File

@@ -0,0 +1,612 @@
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.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 WxCustomMpService wxCustomMpService;
@MockBean
private OverdueOrderHandlerTask overdueOrderHandlerTask;
@org.springframework.beans.factory.annotation.Autowired
private com.starry.admin.modules.order.service.IPlayOrderInfoService orderInfoService;
@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();
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());
} 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(wxCustomMpService);
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();
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());
} 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(wxCustomMpService);
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()));
String orderId = order.getId();
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(wxCustomMpService).sendOrderMessageAsync(argThat(o -> orderId.equals(o.getId())));
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(wxCustomMpService).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);
} 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(wxCustomMpService);
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(wxCustomMpService).sendOrderMessageAsync(argThat(order -> order.getId().equals(orderId)));
reset(wxCustomMpService);
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(wxCustomMpService).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()).isEqualTo(EarningsType.ORDER);
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()));
verify(overdueOrderHandlerTask).enqueue(order.getId() + "_" + ApiTestDataSeeder.DEFAULT_TENANT_ID);
reset(wxCustomMpService);
reset(overdueOrderHandlerTask);
return order.getId();
}
}

View File

@@ -0,0 +1,81 @@
package com.starry.admin.api;
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.starry.admin.common.apitest.ApiTestDataSeeder;
import com.starry.admin.modules.order.module.constant.OrderConstant;
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
import com.starry.admin.modules.withdraw.entity.EarningsLineEntity;
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 org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.http.MediaType;
class WxCustomRewardOrderApiTest extends WxCustomOrderApiTestSupport {
@Test
// 测试用例:客户指定打赏金额下单时,应即时扣减账户余额、生成已完成的打赏订单并同步收益记录,
// 同时校验订单归属陪玩师正确且金额与输入一致,确保余额打赏流程闭环。
void rewardOrderConsumesBalanceAndGeneratesEarnings() throws Exception {
SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
try {
resetCustomerBalance();
String customerToken = wxTokenService.createWxUserToken(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID);
customUserInfoService.updateTokenById(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID, customerToken);
BigDecimal initialBalance = customUserInfoService.getById(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID).getAccountBalance();
String remark = "API reward order " + IdUtils.getUuid();
BigDecimal rewardAmount = new BigDecimal("18.75");
String payload = "{" +
"\"clerkId\":\"" + ApiTestDataSeeder.DEFAULT_CLERK_ID + "\"," +
"\"money\":\"" + rewardAmount + "\"," +
"\"weiChatCode\":\"apitest-customer-wx\"," +
"\"remark\":\"" + remark + "\"" +
"}";
mockMvc.perform(post("/wx/custom/order/reward")
.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.getOrderStatus()).isEqualTo(OrderConstant.OrderStatus.COMPLETED.getCode());
Assertions.assertThat(order.getPlaceType()).isEqualTo(OrderConstant.PlaceType.REWARD.getCode());
Assertions.assertThat(order.getRewardType()).isEqualTo(OrderConstant.RewardType.BALANCE.getCode());
Assertions.assertThat(order.getAcceptBy()).isEqualTo(ApiTestDataSeeder.DEFAULT_CLERK_ID);
Assertions.assertThat(order.getFinalAmount()).isEqualByComparingTo(rewardAmount);
ensureTenantContext();
long earningsCount = earningsService.lambdaQuery()
.eq(EarningsLineEntity::getOrderId, order.getId())
.count();
Assertions.assertThat(earningsCount).isEqualTo(1);
BigDecimal expectedBalance = initialBalance.subtract(rewardAmount);
Assertions.assertThat(customUserInfoService.getById(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID).getAccountBalance())
.isEqualByComparingTo(expectedBalance);
} finally {
CustomSecurityContextHolder.remove();
}
}
}

View File

@@ -0,0 +1,354 @@
package com.starry.admin.api;
import static org.mockito.ArgumentMatchers.anyString;
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.starry.admin.common.apitest.ApiTestDataSeeder;
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.WxCustomMpService;
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.util.Collections;
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;
class WxCustomSpecifiedOrderApiTest extends WxCustomOrderApiTestSupport {
@MockBean
private WxCustomMpService wxCustomMpService;
@org.springframework.beans.factory.annotation.Autowired
private com.starry.admin.modules.order.service.IPlayOrderInfoService orderInfoService;
@org.springframework.beans.factory.annotation.Autowired
private com.starry.admin.modules.weichat.service.WxTokenService clerkWxTokenService;
@Test
// 测试用例:指定单取消后优惠券应恢复为未使用
void specifiedOrderCancellationReleasesCoupon() throws Exception {
SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
String remark = "API specified cancel " + IdUtils.getUuid();
BigDecimal discount = new BigDecimal("10.00");
try {
resetCustomerBalance();
String customerToken = wxTokenService.createWxUserToken(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID);
customUserInfoService.updateTokenById(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID, customerToken);
String couponInfoId = createCouponId("cpn-sc-");
ensureFixedReductionCoupon(
couponInfoId,
OrderConstant.PlaceType.SPECIFIED,
discount,
new BigDecimal("60.00"),
"0");
String couponDetailId = claimCouponForOrder(couponInfoId, OrderConstant.PlaceType.SPECIFIED, customerToken);
String payload = "{" +
"\"clerkId\":\"" + ApiTestDataSeeder.DEFAULT_CLERK_ID + "\"," +
"\"commodityId\":\"" + ApiTestDataSeeder.DEFAULT_COMMODITY_ID + "\"," +
"\"commodityQuantity\":1," +
"\"weiChatCode\":\"apitest-customer-wx\"," +
"\"couponIds\":[\"" + couponDetailId + "\"]," +
"\"remark\":\"" + remark + "\"" +
"}";
mockMvc.perform(post("/wx/custom/order/commodity")
.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();
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.getUseState())
.as("取消指定单后优惠券应恢复为未使用")
.isEqualTo(CouponUseState.UNUSED.getCode());
} finally {
CustomSecurityContextHolder.remove();
}
}
@Test
// 测试用例:指定单使用满减券下单后,应按折后金额推送创建通知,
// 并将预计收益扣除优惠金额,同时把优惠券详情标记为已使用。
void specifiedOrderWithCouponAdjustsAmountAndRevenue() throws Exception {
SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
String remark = "API specified coupon " + IdUtils.getUuid();
BigDecimal discount = new BigDecimal("15.00");
try {
reset(wxCustomMpService);
resetCustomerBalance();
String customerToken = wxTokenService.createWxUserToken(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID);
customUserInfoService.updateTokenById(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID, customerToken);
BigDecimal grossAmount = ApiTestDataSeeder.DEFAULT_COMMODITY_PRICE;
String couponInfoId = createCouponId("cpn-s-");
ensureFixedReductionCoupon(
couponInfoId,
OrderConstant.PlaceType.SPECIFIED,
discount,
new BigDecimal("60.00"),
"0");
String couponDetailId = claimCouponForOrder(couponInfoId, OrderConstant.PlaceType.SPECIFIED, customerToken);
String payload = "{" +
"\"clerkId\":\"" + ApiTestDataSeeder.DEFAULT_CLERK_ID + "\"," +
"\"commodityId\":\"" + ApiTestDataSeeder.DEFAULT_COMMODITY_ID + "\"," +
"\"commodityQuantity\":1," +
"\"weiChatCode\":\"apitest-customer-wx\"," +
"\"couponIds\":[\"" + couponDetailId + "\"]," +
"\"remark\":\"" + remark + "\"" +
"}";
mockMvc.perform(post("/wx/custom/order/commodity")
.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).sendCreateOrderMessage(
eq(ApiTestDataSeeder.DEFAULT_TENANT_ID),
eq(ApiTestDataSeeder.DEFAULT_CLERK_OPEN_ID),
anyString(),
eq(expectedNet.toString()),
eq(order.getCommodityName()),
eq(order.getId()));
int ratio = order.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(order.getEstimatedRevenue()).isEqualByComparingTo(expectedRevenue);
assertCouponUsed(couponDetailId);
} finally {
CustomSecurityContextHolder.remove();
}
}
@Test
// 测试用例:指定单在接单后由管理员强制取消时,优惠券应恢复为未使用
void specifiedOrderForceCancelReleasesCoupon() throws Exception {
SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
String remark = "API specified force cancel " + IdUtils.getUuid();
BigDecimal discount = new BigDecimal("8.00");
try {
reset(wxCustomMpService);
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-sf-");
ensureFixedReductionCoupon(
couponInfoId,
OrderConstant.PlaceType.SPECIFIED,
discount,
new BigDecimal("60.00"),
"0");
String couponDetailId = claimCouponForOrder(couponInfoId, OrderConstant.PlaceType.SPECIFIED, customerToken);
String payload = "{" +
"\"clerkId\":\"" + ApiTestDataSeeder.DEFAULT_CLERK_ID + "\"," +
"\"commodityId\":\"" + ApiTestDataSeeder.DEFAULT_COMMODITY_ID + "\"," +
"\"commodityQuantity\":1," +
"\"weiChatCode\":\"apitest-customer-wx\"," +
"\"couponIds\":[\"" + couponDetailId + "\"]," +
"\"remark\":\"" + remark + "\"" +
"}";
mockMvc.perform(post("/wx/custom/order/commodity")
.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();
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.getUseState())
.as("强制取消指定单后优惠券应恢复为未使用")
.isEqualTo(CouponUseState.UNUSED.getCode());
} finally {
CustomSecurityContextHolder.remove();
}
}
@Test
// 测试用例:客户携带指定陪玩师和服务下单时,接口需返回成功,并生成待支付状态的指定订单,
// 验证订单金额与种子服务价格一致、陪玩师被正确指派,同时触发微信创建订单通知。
void specifiedOrderCreatesPendingOrder() throws Exception {
SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
try {
resetCustomerBalance();
String customerToken = wxTokenService.createWxUserToken(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID);
customUserInfoService.updateTokenById(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID, customerToken);
String remark = "API specified order " + IdUtils.getUuid();
ensureTenantContext();
long beforeCount = playOrderInfoService.lambdaQuery()
.eq(PlayOrderInfoEntity::getPurchaserBy, ApiTestDataSeeder.DEFAULT_CUSTOMER_ID)
.eq(PlayOrderInfoEntity::getPlaceType, OrderConstant.PlaceType.SPECIFIED.getCode())
.count();
String payload = "{" +
"\"clerkId\":\"" + ApiTestDataSeeder.DEFAULT_CLERK_ID + "\"," +
"\"commodityId\":\"" + ApiTestDataSeeder.DEFAULT_COMMODITY_ID + "\"," +
"\"commodityQuantity\":1," +
"\"weiChatCode\":\"apitest-customer-wx\"," +
"\"couponIds\":[]," +
"\"remark\":\"" + remark + "\"" +
"}";
reset(wxCustomMpService);
mockMvc.perform(post("/wx/custom/order/commodity")
.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.SPECIFIED.getCode());
Assertions.assertThat(order.getOrderStatus()).isEqualTo(OrderConstant.OrderStatus.PENDING.getCode());
Assertions.assertThat(order.getAcceptBy()).isEqualTo(ApiTestDataSeeder.DEFAULT_CLERK_ID);
Assertions.assertThat(order.getFinalAmount()).isEqualByComparingTo(ApiTestDataSeeder.DEFAULT_COMMODITY_PRICE);
verify(wxCustomMpService).sendCreateOrderMessage(
eq(ApiTestDataSeeder.DEFAULT_TENANT_ID),
eq(ApiTestDataSeeder.DEFAULT_CLERK_OPEN_ID),
anyString(),
eq(order.getFinalAmount().toString()),
eq(order.getCommodityName()),
eq(order.getId()));
ensureTenantContext();
long afterCount = playOrderInfoService.lambdaQuery()
.eq(PlayOrderInfoEntity::getPurchaserBy, ApiTestDataSeeder.DEFAULT_CUSTOMER_ID)
.eq(PlayOrderInfoEntity::getPlaceType, OrderConstant.PlaceType.SPECIFIED.getCode())
.count();
Assertions.assertThat(afterCount).isEqualTo(beforeCount + 1);
} finally {
CustomSecurityContextHolder.remove();
}
}
}