Files
peipei-backend/play-admin/src/test/java/com/starry/admin/api/PlayCommodityInfoApiTest.java

522 lines
22 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

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

package com.starry.admin.api;
import static org.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));
ensureTenantContext();
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));
ensureTenantContext();
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);
}
}