新增店员等级排序功能
Some checks failed
Build and Push Backend / docker (push) Failing after 6s

- 添加数据库迁移脚本,为 play_clerk_level_info 表新增 order_number 字段
- 更新测试数据种子,设置默认等级的排序号
- 新增店员用户API测试,验证按等级排序号和在线状态的排序逻辑
This commit is contained in:
irving
2025-11-04 21:20:42 -05:00
parent d961e62cc2
commit a8cdb27e8e
3 changed files with 343 additions and 0 deletions

View File

@@ -259,6 +259,7 @@ public class ApiTestDataSeeder implements CommandLineRunner {
entity.setNotFirstRandomRadio(45); entity.setNotFirstRandomRadio(45);
entity.setFirstRewardRatio(40); entity.setFirstRewardRatio(40);
entity.setNotFirstRewardRatio(35); entity.setNotFirstRewardRatio(35);
entity.setOrderNumber(1L);
clerkLevelInfoService.save(entity); clerkLevelInfoService.save(entity);
log.info("Inserted API test clerk level {}", DEFAULT_CLERK_LEVEL_ID); log.info("Inserted API test clerk level {}", DEFAULT_CLERK_LEVEL_ID);
} }

View File

@@ -0,0 +1,20 @@
SET @column_exists := (
SELECT COUNT(*)
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'play_clerk_level_info'
AND COLUMN_NAME = 'order_number'
);
SET @ddl := IF(
@column_exists = 0,
'ALTER TABLE `play_clerk_level_info` ADD COLUMN `order_number` bigint NULL COMMENT ''等级排序号''',
'SELECT 1'
);
PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
UPDATE `play_clerk_level_info`
SET `order_number` = COALESCE(`order_number`, `level`);

View File

@@ -0,0 +1,322 @@
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;
}
}