382 lines
18 KiB
Java
382 lines
18 KiB
Java
package com.starry.admin.api;
|
||
|
||
import static org.assertj.core.api.Assertions.assertThat;
|
||
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.ObjectMapper;
|
||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||
import com.starry.admin.modules.clerk.module.entity.PlayClerkLevelInfoEntity;
|
||
import com.starry.admin.modules.clerk.module.entity.PlayClerkUserInfoEntity;
|
||
import com.starry.admin.modules.clerk.service.IPlayClerkLevelInfoService;
|
||
import com.starry.admin.modules.clerk.service.IPlayClerkUserInfoService;
|
||
import com.starry.admin.utils.SecurityUtils;
|
||
import com.starry.common.utils.IdUtils;
|
||
import java.time.LocalDateTime;
|
||
import java.util.ArrayList;
|
||
import java.util.List;
|
||
import java.util.concurrent.ThreadLocalRandom;
|
||
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 PlayClerkUserInfoApiTest extends AbstractApiTest {
|
||
|
||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||
|
||
@Autowired
|
||
private IPlayClerkLevelInfoService clerkLevelInfoService;
|
||
|
||
@Autowired
|
||
private IPlayClerkUserInfoService clerkUserInfoService;
|
||
|
||
private final List<String> levelIdsToCleanup = new ArrayList<>();
|
||
private final List<String> clerkIdsToCleanup = new ArrayList<>();
|
||
|
||
@AfterEach
|
||
void tearDown() {
|
||
ensureTenantContext();
|
||
if (!clerkIdsToCleanup.isEmpty()) {
|
||
clerkUserInfoService.removeByIds(clerkIdsToCleanup);
|
||
clerkIdsToCleanup.clear();
|
||
}
|
||
if (!levelIdsToCleanup.isEmpty()) {
|
||
clerkLevelInfoService.removeByIds(levelIdsToCleanup);
|
||
levelIdsToCleanup.clear();
|
||
}
|
||
}
|
||
|
||
@Test
|
||
// 场景:不同等级的排序号不同,接口应按照排序号升序返回,验证等级排序字段生效。
|
||
void listOrdersByLevelOrderNumberAscending() throws Exception {
|
||
ensureTenantContext();
|
||
PlayClerkLevelInfoEntity lowOrderLevel = createClerkLevel("low", 1L, 50);
|
||
PlayClerkLevelInfoEntity highOrderLevel = createClerkLevel("high", 5L, 60);
|
||
|
||
String filterToken = "order-sort-" + IdUtils.getUuid().substring(0, 8);
|
||
String lowOrderClerkId = createClerk(filterToken + "-low", lowOrderLevel.getId(), "0");
|
||
String highOrderClerkId = createClerk(filterToken + "-high", highOrderLevel.getId(), "1");
|
||
|
||
MvcResult result = mockMvc.perform(get("/clerk/user/list")
|
||
.param("pageNum", "1")
|
||
.param("pageSize", "20")
|
||
.param("nickname", filterToken)
|
||
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||
.header(USER_HEADER, DEFAULT_USER))
|
||
.andExpect(status().isOk())
|
||
.andReturn();
|
||
|
||
String responseBody = result.getResponse().getContentAsString();
|
||
JsonNode root = objectMapper.readTree(responseBody);
|
||
assertThat(root.get("code").asInt()).isEqualTo(200);
|
||
JsonNode records = root.path("data");
|
||
assertThat(records.isArray()).as("Response payload: %s", responseBody).isTrue();
|
||
|
||
List<String> orderedIds = new ArrayList<>();
|
||
for (JsonNode record : records) {
|
||
orderedIds.add(record.path("id").asText());
|
||
}
|
||
|
||
assertThat(orderedIds).contains(lowOrderClerkId, highOrderClerkId);
|
||
assertThat(orderedIds.indexOf(lowOrderClerkId))
|
||
.withFailMessage("Unexpected order for token %s: %s", filterToken, orderedIds)
|
||
.isLessThan(orderedIds.indexOf(highOrderClerkId));
|
||
}
|
||
|
||
@Test
|
||
// 场景:相同等级排序号相同,接口应按在线状态优先展示在线店员,验证排序二级规则。
|
||
void listOrdersByOnlineStateWhenOrderNumberMatches() throws Exception {
|
||
ensureTenantContext();
|
||
PlayClerkLevelInfoEntity level = createClerkLevel("tie", 3L, 70);
|
||
|
||
String filterToken = "online-priority-" + IdUtils.getUuid().substring(0, 8);
|
||
String onlineClerkId = createClerk(filterToken + "-online", level.getId(), "1");
|
||
String offlineClerkId = createClerk(filterToken + "-offline", level.getId(), "0");
|
||
|
||
MvcResult result = mockMvc.perform(get("/clerk/user/list")
|
||
.param("pageNum", "1")
|
||
.param("pageSize", "20")
|
||
.param("nickname", filterToken)
|
||
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||
.header(USER_HEADER, DEFAULT_USER))
|
||
.andExpect(status().isOk())
|
||
.andReturn();
|
||
|
||
String responseBody = result.getResponse().getContentAsString();
|
||
JsonNode root = objectMapper.readTree(responseBody);
|
||
assertThat(root.get("code").asInt()).isEqualTo(200);
|
||
JsonNode records = root.path("data");
|
||
assertThat(records.isArray()).as("Response payload: %s", responseBody).isTrue();
|
||
|
||
List<String> orderedIds = new ArrayList<>();
|
||
for (JsonNode record : records) {
|
||
orderedIds.add(record.path("id").asText());
|
||
}
|
||
|
||
assertThat(orderedIds).contains(onlineClerkId, offlineClerkId);
|
||
assertThat(orderedIds.indexOf(onlineClerkId))
|
||
.withFailMessage("Unexpected order for token %s: %s | response: %s",
|
||
filterToken, orderedIds, responseBody)
|
||
.isLessThan(orderedIds.indexOf(offlineClerkId));
|
||
}
|
||
|
||
@Test
|
||
// 场景:两个等级都未配置排序号时,接口仍可返回且在线客服优先排序。
|
||
void listOrdersByOnlineStateWhenOrderNumberMissing() throws Exception {
|
||
ensureTenantContext();
|
||
PlayClerkLevelInfoEntity onlineLevel = createClerkLevel("null-online", null, 20);
|
||
PlayClerkLevelInfoEntity offlineLevel = createClerkLevel("null-offline", null, 30);
|
||
|
||
String filterToken = "null-priority-" + IdUtils.getUuid().substring(0, 8);
|
||
String onlineClerkId = createClerk(filterToken + "-online", onlineLevel.getId(), "1");
|
||
String offlineClerkId = createClerk(filterToken + "-offline", offlineLevel.getId(), "0");
|
||
|
||
MvcResult result = mockMvc.perform(get("/clerk/user/list")
|
||
.param("pageNum", "1")
|
||
.param("pageSize", "20")
|
||
.param("nickname", filterToken)
|
||
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||
.header(USER_HEADER, DEFAULT_USER))
|
||
.andExpect(status().isOk())
|
||
.andReturn();
|
||
|
||
String responseBody = result.getResponse().getContentAsString();
|
||
JsonNode root = objectMapper.readTree(responseBody);
|
||
assertThat(root.get("code").asInt()).isEqualTo(200);
|
||
JsonNode records = root.path("data");
|
||
assertThat(records.isArray()).as("Response payload: %s", responseBody).isTrue();
|
||
|
||
List<String> orderedIds = new ArrayList<>();
|
||
for (JsonNode record : records) {
|
||
orderedIds.add(record.path("id").asText());
|
||
}
|
||
|
||
assertThat(orderedIds).contains(onlineClerkId, offlineClerkId);
|
||
assertThat(orderedIds.indexOf(onlineClerkId))
|
||
.withFailMessage("Online clerk should remain prioritized even without orderNumber. token=%s order=%s response=%s",
|
||
filterToken, orderedIds, responseBody)
|
||
.isLessThan(orderedIds.indexOf(offlineClerkId));
|
||
}
|
||
|
||
@Test
|
||
// 场景:存在未填写排序号的等级时,接口仍能返回,并默认将该等级排在有序的等级之后。
|
||
void listHandlesNullOrderNumberGracefully() throws Exception {
|
||
ensureTenantContext();
|
||
PlayClerkLevelInfoEntity orderedLevel = createClerkLevel("ordered", 2L, 40);
|
||
PlayClerkLevelInfoEntity nullLevel = createClerkLevel("null", null, 50);
|
||
|
||
String filterToken = "null-order-" + IdUtils.getUuid().substring(0, 8);
|
||
String orderedClerkId = createClerk(filterToken + "-ordered", orderedLevel.getId(), "1");
|
||
String nullOrderClerkId = createClerk(filterToken + "-null", nullLevel.getId(), "1");
|
||
|
||
MvcResult result = mockMvc.perform(get("/clerk/user/list")
|
||
.param("pageNum", "1")
|
||
.param("pageSize", "20")
|
||
.param("nickname", filterToken)
|
||
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||
.header(USER_HEADER, DEFAULT_USER))
|
||
.andExpect(status().isOk())
|
||
.andReturn();
|
||
|
||
String responseBody = result.getResponse().getContentAsString();
|
||
JsonNode root = objectMapper.readTree(responseBody);
|
||
assertThat(root.get("code").asInt()).isEqualTo(200);
|
||
JsonNode records = root.path("data");
|
||
assertThat(records.isArray()).as("Response payload: %s", responseBody).isTrue();
|
||
|
||
List<String> orderedIds = new ArrayList<>();
|
||
for (JsonNode record : records) {
|
||
orderedIds.add(record.path("id").asText());
|
||
}
|
||
|
||
assertThat(orderedIds).contains(orderedClerkId, nullOrderClerkId);
|
||
assertThat(orderedIds.indexOf(orderedClerkId))
|
||
.withFailMessage("Null orderNumber should fall back after populated ones. token=%s, order=%s, response=%s",
|
||
filterToken, orderedIds, responseBody)
|
||
.isLessThan(orderedIds.indexOf(nullOrderClerkId));
|
||
}
|
||
|
||
@Test
|
||
// 场景:更新店员等级时传入排序号,接口应成功持久化该值,验证新增字段的写入能力。
|
||
void updateLevelPersistsOrderNumber() throws Exception {
|
||
ensureTenantContext();
|
||
PlayClerkLevelInfoEntity level = createClerkLevel("update", 8L, 80);
|
||
|
||
long updatedOrderNumber = 42L;
|
||
ObjectNode payload = objectMapper.createObjectNode();
|
||
payload.put("id", level.getId());
|
||
payload.put("name", level.getName());
|
||
payload.put("firstRegularRatio", level.getFirstRegularRatio());
|
||
payload.put("notFirstRegularRatio", level.getNotFirstRegularRatio());
|
||
payload.put("firstRewardRatio", level.getFirstRewardRatio());
|
||
payload.put("notFirstRewardRatio", level.getNotFirstRewardRatio());
|
||
payload.put("firstRandomRadio", level.getFirstRandomRadio());
|
||
payload.put("notFirstRandomRadio", level.getNotFirstRandomRadio());
|
||
payload.put("styleType", level.getStyleType());
|
||
payload.put("orderNumber", updatedOrderNumber);
|
||
|
||
mockMvc.perform(post("/clerk/level/update")
|
||
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||
.header(USER_HEADER, DEFAULT_USER)
|
||
.contentType(MediaType.APPLICATION_JSON)
|
||
.content(payload.toString()))
|
||
.andExpect(status().isOk())
|
||
.andExpect(jsonPath("$.code").value(200));
|
||
|
||
ensureTenantContext();
|
||
PlayClerkLevelInfoEntity reloaded = clerkLevelInfoService.getById(level.getId());
|
||
assertThat(reloaded.getOrderNumber()).isEqualTo(updatedOrderNumber);
|
||
}
|
||
|
||
@Test
|
||
// 场景:更新时清空排序号,接口应允许写入null并持久化。
|
||
void updateLevelAllowsClearingOrderNumber() throws Exception {
|
||
ensureTenantContext();
|
||
PlayClerkLevelInfoEntity level = createClerkLevel("clear", 5L, 85);
|
||
|
||
ObjectNode payload = objectMapper.createObjectNode();
|
||
payload.put("id", level.getId());
|
||
payload.put("name", level.getName());
|
||
payload.put("firstRegularRatio", level.getFirstRegularRatio());
|
||
payload.put("notFirstRegularRatio", level.getNotFirstRegularRatio());
|
||
payload.put("firstRewardRatio", level.getFirstRewardRatio());
|
||
payload.put("notFirstRewardRatio", level.getNotFirstRewardRatio());
|
||
payload.put("firstRandomRadio", level.getFirstRandomRadio());
|
||
payload.put("notFirstRandomRadio", level.getNotFirstRandomRadio());
|
||
payload.put("styleType", level.getStyleType());
|
||
payload.putNull("orderNumber");
|
||
|
||
mockMvc.perform(post("/clerk/level/update")
|
||
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||
.header(USER_HEADER, DEFAULT_USER)
|
||
.contentType(MediaType.APPLICATION_JSON)
|
||
.content(payload.toString()))
|
||
.andExpect(status().isOk())
|
||
.andExpect(jsonPath("$.code").value(200));
|
||
|
||
ensureTenantContext();
|
||
PlayClerkLevelInfoEntity reloaded = clerkLevelInfoService.getById(level.getId());
|
||
assertThat(reloaded.getOrderNumber()).isNull();
|
||
}
|
||
|
||
private void ensureTenantContext() {
|
||
SecurityUtils.setTenantId(DEFAULT_TENANT);
|
||
}
|
||
|
||
private PlayClerkLevelInfoEntity createClerkLevel(String suffix, Long orderNumber, int levelValue) {
|
||
ensureTenantContext();
|
||
String levelId = IdUtils.getUuid();
|
||
PlayClerkLevelInfoEntity level = new PlayClerkLevelInfoEntity();
|
||
level.setId(levelId);
|
||
level.setTenantId(DEFAULT_TENANT);
|
||
level.setName("API测试等级-" + suffix);
|
||
level.setLevel(levelValue);
|
||
level.setFirstRegularRatio(60);
|
||
level.setNotFirstRegularRatio(50);
|
||
level.setFirstRewardRatio(45);
|
||
level.setNotFirstRewardRatio(35);
|
||
level.setFirstRandomRadio(55);
|
||
level.setNotFirstRandomRadio(40);
|
||
level.setStyleType(levelValue);
|
||
level.setOrderNumber(orderNumber);
|
||
clerkLevelInfoService.save(level);
|
||
levelIdsToCleanup.add(levelId);
|
||
return level;
|
||
}
|
||
|
||
private String createClerk(String suffix, String levelId, String onlineState) {
|
||
ensureTenantContext();
|
||
String clerkId = IdUtils.getUuid();
|
||
PlayClerkUserInfoEntity clerk = new PlayClerkUserInfoEntity();
|
||
clerk.setId(clerkId);
|
||
clerk.setTenantId(DEFAULT_TENANT);
|
||
clerk.setNickname("API测试店员-" + suffix);
|
||
clerk.setLevelId(levelId);
|
||
clerk.setClerkState("1");
|
||
clerk.setOnboardingState("1");
|
||
clerk.setListingState("1");
|
||
clerk.setDisplayState("1");
|
||
clerk.setRecommendationState("0");
|
||
clerk.setPinToTopState("0");
|
||
clerk.setRandomOrderState("1");
|
||
clerk.setFixingLevel("0");
|
||
clerk.setOnlineState(onlineState);
|
||
clerk.setPhone("138" + String.format("%08d", ThreadLocalRandom.current().nextInt(0, 100_000_000)));
|
||
clerk.setOpenid("openid-" + suffix + "-" + IdUtils.getUuid());
|
||
clerk.setWeiChatCode("wx-code-" + suffix);
|
||
clerk.setWeiChatAvatar("https://example.com/avatar/" + suffix);
|
||
clerk.setTypeId("api-type");
|
||
clerk.setProvince("API省");
|
||
clerk.setCity("API市");
|
||
clerk.setEntryTime(LocalDateTime.now());
|
||
clerk.setAddTime(LocalDateTime.now());
|
||
clerkUserInfoService.save(clerk);
|
||
clerkIdsToCleanup.add(clerkId);
|
||
return clerkId;
|
||
}
|
||
@Test
|
||
void listOrderingStableWithMultipleCriteria() throws Exception {
|
||
ensureTenantContext();
|
||
PlayClerkLevelInfoEntity level = createClerkLevel("stable", 10L, 80);
|
||
|
||
String filterToken = "stable-" + IdUtils.getUuid().substring(0, 6);
|
||
String pinnedOnline = createClerk(filterToken + "-pinned-online", level.getId(), "1");
|
||
togglePin(pinnedOnline, "1");
|
||
String pinnedOffline = createClerk(filterToken + "-pinned-offline", level.getId(), "0");
|
||
togglePin(pinnedOffline, "1");
|
||
String online1 = createClerk(filterToken + "-online-one", level.getId(), "1");
|
||
pause(50);
|
||
String online2 = createClerk(filterToken + "-online-two", level.getId(), "1");
|
||
String offline = createClerk(filterToken + "-offline", level.getId(), "0");
|
||
|
||
MvcResult result = mockMvc.perform(get("/clerk/user/list")
|
||
.param("pageNum", "1")
|
||
.param("pageSize", "20")
|
||
.param("nickname", filterToken)
|
||
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||
.header(USER_HEADER, DEFAULT_USER))
|
||
.andExpect(status().isOk())
|
||
.andReturn();
|
||
|
||
JsonNode root = objectMapper.readTree(result.getResponse().getContentAsString());
|
||
JsonNode records = root.path("data");
|
||
assertThat(records.isArray()).isTrue();
|
||
List<String> orderedIds = new ArrayList<>();
|
||
for (JsonNode record : records) {
|
||
orderedIds.add(record.path("id").asText());
|
||
}
|
||
|
||
assertThat(orderedIds.indexOf(pinnedOnline))
|
||
.isLessThan(orderedIds.indexOf(online1));
|
||
assertThat(orderedIds.indexOf(online1))
|
||
.withFailMessage("Online entries should stay ahead, list=%s", orderedIds)
|
||
.isLessThan(orderedIds.indexOf(online2));
|
||
assertThat(orderedIds.indexOf(online2))
|
||
.isLessThan(orderedIds.indexOf(pinnedOffline));
|
||
assertThat(orderedIds.indexOf(pinnedOffline))
|
||
.isLessThan(orderedIds.indexOf(offline));
|
||
}
|
||
|
||
|
||
private void togglePin(String clerkId, String pinState) {
|
||
ensureTenantContext();
|
||
PlayClerkUserInfoEntity update = new PlayClerkUserInfoEntity();
|
||
update.setId(clerkId);
|
||
update.setPinToTopState(pinState);
|
||
clerkUserInfoService.update(update);
|
||
}
|
||
|
||
private void pause(long millis) {
|
||
try {
|
||
Thread.sleep(millis);
|
||
} catch (InterruptedException ex) {
|
||
Thread.currentThread().interrupt();
|
||
}
|
||
}
|
||
}
|