feat(pk): implement PK (Player-Killer) system with lifecycle management

- Add PK entity fields: winner, scores, and setting_id
- Implement force start/end API endpoints for clerk PK
- Add PK lifecycle service with auto-start/end scheduling
- Add Redis-based PK state management
- Implement PK detail service with live/history/upcoming queries
- Add WeChat PK controller with history and live PK endpoints
- Add comprehensive PK integration tests
- Create PK setting management with tenant-specific configs
- Add database migrations for PK scores, winner, settings, and menu
- Add PK-related DTOs and enums (status, menu paths)
- Add TenantScope utility for tenant context management
This commit is contained in:
irving
2026-01-02 01:34:03 -05:00
parent a7e567e9b4
commit 17a8c358a8
54 changed files with 5391 additions and 37 deletions

View File

@@ -0,0 +1,529 @@
package com.starry.admin.modules.pk;
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.starry.admin.api.AbstractApiTest;
import com.starry.admin.modules.clerk.module.entity.ClerkPkEnum;
import com.starry.admin.modules.clerk.module.entity.PlayClerkPkEntity;
import com.starry.admin.modules.clerk.service.IPlayClerkPkService;
import com.starry.admin.modules.pk.enums.PkLifecycleErrorCode;
import com.starry.admin.modules.pk.redis.PkRedisKeyConstants;
import com.starry.admin.utils.SecurityUtils;
import com.starry.common.result.ResultCodeEnum;
import com.starry.common.utils.IdUtils;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Date;
import javax.annotation.Resource;
import org.junit.jupiter.api.Test;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MvcResult;
class PlayClerkPkApiTest extends AbstractApiTest {
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
private static final String MSG_PK_NOT_FOUND = "PK不存在";
private static final String MSG_PK_START_NOT_REACHED = "PK开始时间尚未到达";
private static final String CLERK_PREFIX = "pk-api-clerk-";
private static final int MINUTES_BEFORE_START = 5;
private static final int MINUTES_AFTER_START = 30;
private static final int MINUTES_FUTURE_START = 15;
private static final int MINUTES_PAST_END = 10;
private static final int FORCE_START_DURATION_MINUTES = 30;
private static final int SETTLED_TRUE = 1;
private static final int SETTLED_FALSE = 0;
private static final BigDecimal TIE_SCORE = new BigDecimal("10.00");
private static final BigDecimal SCORE_A = new BigDecimal("12.50");
private static final BigDecimal SCORE_B = new BigDecimal("7.25");
private static final BigDecimal ZERO_SCORE = new BigDecimal("0.00");
private static final int ORDER_COUNT_A = 3;
private static final int ORDER_COUNT_B = 1;
private static final long REMAINING_SECONDS_ZERO = 0L;
private static final int ZERO_COUNT = 0;
private static final int ZERO_VALUE = 0;
@Resource
private IPlayClerkPkService clerkPkService;
@Resource
private StringRedisTemplate stringRedisTemplate;
@Test
void forceStartShouldCreateInProgressPk() throws Exception {
String clerkAId = newClerkId();
String clerkBId = newClerkId();
String requestBody = buildForceStartBody(clerkAId, clerkBId, FORCE_START_DURATION_MINUTES);
MvcResult result = mockMvc.perform(post(buildForceStartPath())
.contentType(MediaType.APPLICATION_JSON)
.content(requestBody)
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(ResultCodeEnum.SUCCESS.getCode()))
.andReturn();
String pkId = extractDataAsText(result);
PlayClerkPkEntity persisted = loadPk(pkId);
assertThat(persisted.getStatus()).isEqualTo(ClerkPkEnum.IN_PROGRESS.name());
assertThat(persisted.getClerkA()).isEqualTo(clerkAId);
assertThat(persisted.getClerkB()).isEqualTo(clerkBId);
assertThat(persisted.getPkBeginTime()).isNotNull();
assertThat(persisted.getPkEndTime()).isNotNull();
assertThat(persisted.getTenantId()).isEqualTo(DEFAULT_TENANT);
assertThat(persisted.getCreatedBy()).isNotNull();
assertThat(persisted.getPkEndTime()).isAfter(persisted.getPkBeginTime());
}
@Test
void forceStartShouldRejectWhenConflict() throws Exception {
String clerkAId = newClerkId();
String clerkBId = newClerkId();
LocalDateTime now = LocalDateTime.now();
createPkForClerks(clerkAId, clerkBId, now.minusMinutes(MINUTES_BEFORE_START),
now.plusMinutes(MINUTES_AFTER_START), ClerkPkEnum.IN_PROGRESS.name());
String requestBody = buildForceStartBody(clerkAId, clerkBId, FORCE_START_DURATION_MINUTES);
mockMvc.perform(post(buildForceStartPath())
.contentType(MediaType.APPLICATION_JSON)
.content(requestBody)
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(ResultCodeEnum.FAILED.getCode()))
.andExpect(jsonPath("$.message").value(PkLifecycleErrorCode.CLERK_CONFLICT.getMessage()));
}
@Test
void forceStartShouldRejectWhenMissingClerkA() throws Exception {
String requestBody = "{\"clerkAId\":\"\",\"clerkBId\":\"" + newClerkId()
+ "\",\"durationMinutes\":" + FORCE_START_DURATION_MINUTES + "}";
mockMvc.perform(post(buildForceStartPath())
.contentType(MediaType.APPLICATION_JSON)
.content(requestBody)
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(ResultCodeEnum.FAILED.getCode()))
.andExpect(jsonPath("$.message").value(PkLifecycleErrorCode.REQUEST_INVALID.getMessage()));
}
@Test
void forceStartShouldRejectWhenMissingClerkB() throws Exception {
String requestBody = "{\"clerkAId\":\"" + newClerkId()
+ "\",\"clerkBId\":\"\",\"durationMinutes\":" + FORCE_START_DURATION_MINUTES + "}";
mockMvc.perform(post(buildForceStartPath())
.contentType(MediaType.APPLICATION_JSON)
.content(requestBody)
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(ResultCodeEnum.FAILED.getCode()))
.andExpect(jsonPath("$.message").value(PkLifecycleErrorCode.REQUEST_INVALID.getMessage()));
}
@Test
void forceStartShouldRejectWhenMissingDuration() throws Exception {
String requestBody = "{\"clerkAId\":\"" + newClerkId()
+ "\",\"clerkBId\":\"" + newClerkId() + "\"}";
mockMvc.perform(post(buildForceStartPath())
.contentType(MediaType.APPLICATION_JSON)
.content(requestBody)
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(ResultCodeEnum.FAILED.getCode()))
.andExpect(jsonPath("$.message").value(PkLifecycleErrorCode.REQUEST_INVALID.getMessage()));
}
@Test
void forceStartShouldRejectWhenSameClerk() throws Exception {
String clerkId = newClerkId();
String requestBody = buildForceStartBody(clerkId, clerkId, FORCE_START_DURATION_MINUTES);
mockMvc.perform(post(buildForceStartPath())
.contentType(MediaType.APPLICATION_JSON)
.content(requestBody)
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(ResultCodeEnum.FAILED.getCode()))
.andExpect(jsonPath("$.message").value(PkLifecycleErrorCode.REQUEST_INVALID.getMessage()));
}
@Test
void forceStartShouldRejectWhenDurationInvalid() throws Exception {
String requestBody = buildForceStartBody(newClerkId(), newClerkId(), 0);
mockMvc.perform(post(buildForceStartPath())
.contentType(MediaType.APPLICATION_JSON)
.content(requestBody)
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(ResultCodeEnum.FAILED.getCode()))
.andExpect(jsonPath("$.message").value(PkLifecycleErrorCode.REQUEST_INVALID.getMessage()));
}
@Test
void forceStartShouldRejectWhenConflictOnClerkAOnly() throws Exception {
String clerkAId = newClerkId();
String clerkBId = newClerkId();
String otherClerk = newClerkId();
LocalDateTime now = LocalDateTime.now();
createPkForClerks(clerkAId, otherClerk, now.minusMinutes(MINUTES_BEFORE_START),
now.plusMinutes(MINUTES_AFTER_START), ClerkPkEnum.IN_PROGRESS.name());
String requestBody = buildForceStartBody(clerkAId, clerkBId, FORCE_START_DURATION_MINUTES);
mockMvc.perform(post(buildForceStartPath())
.contentType(MediaType.APPLICATION_JSON)
.content(requestBody)
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(ResultCodeEnum.FAILED.getCode()))
.andExpect(jsonPath("$.message").value(PkLifecycleErrorCode.CLERK_CONFLICT.getMessage()));
}
@Test
void forceStartShouldRejectWhenConflictOnClerkBOnly() throws Exception {
String clerkAId = newClerkId();
String clerkBId = newClerkId();
String otherClerk = newClerkId();
LocalDateTime now = LocalDateTime.now();
createPkForClerks(otherClerk, clerkBId, now.minusMinutes(MINUTES_BEFORE_START),
now.plusMinutes(MINUTES_AFTER_START), ClerkPkEnum.IN_PROGRESS.name());
String requestBody = buildForceStartBody(clerkAId, clerkBId, FORCE_START_DURATION_MINUTES);
mockMvc.perform(post(buildForceStartPath())
.contentType(MediaType.APPLICATION_JSON)
.content(requestBody)
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(ResultCodeEnum.FAILED.getCode()))
.andExpect(jsonPath("$.message").value(PkLifecycleErrorCode.CLERK_CONFLICT.getMessage()));
}
@Test
void startPkShouldRejectWhenStartTimeNotReached() throws Exception {
LocalDateTime now = LocalDateTime.now();
PlayClerkPkEntity pk = buildPk(ClerkPkEnum.TO_BE_STARTED.name(),
now.plusMinutes(MINUTES_FUTURE_START),
now.plusMinutes(MINUTES_AFTER_START),
SETTLED_FALSE);
mockMvc.perform(post(buildStartPath(pk.getId()))
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(ResultCodeEnum.FAILED.getCode()))
.andExpect(jsonPath("$.message").value(MSG_PK_START_NOT_REACHED));
}
@Test
void startPkShouldFailWhenMissing() throws Exception {
String pkId = IdUtils.getUuid();
mockMvc.perform(post(buildStartPath(pkId))
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(ResultCodeEnum.FAILED.getCode()))
.andExpect(jsonPath("$.message").value(MSG_PK_NOT_FOUND));
}
@Test
void startPkShouldAdvanceStatusWhenEligible() throws Exception {
LocalDateTime now = LocalDateTime.now();
PlayClerkPkEntity pk = buildPk(ClerkPkEnum.TO_BE_STARTED.name(),
now.minusMinutes(MINUTES_BEFORE_START),
now.plusMinutes(MINUTES_AFTER_START),
SETTLED_FALSE);
mockMvc.perform(post(buildStartPath(pk.getId()))
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(ResultCodeEnum.SUCCESS.getCode()));
PlayClerkPkEntity persisted = loadPk(pk.getId());
assertThat(persisted.getStatus()).isEqualTo(ClerkPkEnum.IN_PROGRESS.name());
}
@Test
void finishPkShouldFailWhenMissing() throws Exception {
String pkId = IdUtils.getUuid();
mockMvc.perform(post(buildFinishPath(pkId))
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(ResultCodeEnum.FAILED.getCode()))
.andExpect(jsonPath("$.message").value(MSG_PK_NOT_FOUND));
}
@Test
void finishPkShouldBeIdempotent() throws Exception {
LocalDateTime now = LocalDateTime.now();
PlayClerkPkEntity pk = buildPk(ClerkPkEnum.FINISHED.name(),
now.minusMinutes(MINUTES_BEFORE_START),
now.minusMinutes(MINUTES_BEFORE_START),
SETTLED_TRUE);
mockMvc.perform(post(buildFinishPath(pk.getId()))
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(ResultCodeEnum.SUCCESS.getCode()));
PlayClerkPkEntity persisted = loadPk(pk.getId());
assertThat(persisted.getStatus()).isEqualTo(ClerkPkEnum.FINISHED.name());
assertThat(persisted.getSettled()).isEqualTo(SETTLED_TRUE);
}
@Test
void finishPkShouldPersistZeroWhenRedisEmpty() throws Exception {
LocalDateTime now = LocalDateTime.now();
PlayClerkPkEntity pk = buildPk(ClerkPkEnum.IN_PROGRESS.name(),
now.minusMinutes(MINUTES_BEFORE_START),
now.minusMinutes(MINUTES_PAST_END),
SETTLED_FALSE);
mockMvc.perform(post(buildFinishPath(pk.getId()))
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(ResultCodeEnum.SUCCESS.getCode()));
PlayClerkPkEntity persisted = loadPk(pk.getId());
assertThat(persisted.getClerkAScore()).isEqualByComparingTo(ZERO_SCORE);
assertThat(persisted.getClerkBScore()).isEqualByComparingTo(ZERO_SCORE);
assertThat(persisted.getClerkAOrderCount()).isEqualTo(ZERO_COUNT);
assertThat(persisted.getClerkBOrderCount()).isEqualTo(ZERO_COUNT);
assertThat(persisted.getWinnerClerkId()).isNull();
}
@Test
void finishPkShouldAssignWinnerForClerkA() throws Exception {
LocalDateTime now = LocalDateTime.now();
PlayClerkPkEntity pk = buildPk(ClerkPkEnum.IN_PROGRESS.name(),
now.minusMinutes(MINUTES_BEFORE_START),
now.minusMinutes(MINUTES_PAST_END),
SETTLED_FALSE);
String scoreKey = PkRedisKeyConstants.scoreKey(pk.getId());
stringRedisTemplate.opsForHash().put(scoreKey, PkRedisKeyConstants.FIELD_CLERK_A_SCORE, SCORE_A.toString());
stringRedisTemplate.opsForHash().put(scoreKey, PkRedisKeyConstants.FIELD_CLERK_B_SCORE, SCORE_B.toString());
mockMvc.perform(post(buildFinishPath(pk.getId()))
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(ResultCodeEnum.SUCCESS.getCode()));
PlayClerkPkEntity persisted = loadPk(pk.getId());
assertThat(persisted.getWinnerClerkId()).isEqualTo(pk.getClerkA());
}
@Test
void finishPkShouldAssignWinnerForClerkB() throws Exception {
LocalDateTime now = LocalDateTime.now();
PlayClerkPkEntity pk = buildPk(ClerkPkEnum.IN_PROGRESS.name(),
now.minusMinutes(MINUTES_BEFORE_START),
now.minusMinutes(MINUTES_PAST_END),
SETTLED_FALSE);
String scoreKey = PkRedisKeyConstants.scoreKey(pk.getId());
stringRedisTemplate.opsForHash().put(scoreKey, PkRedisKeyConstants.FIELD_CLERK_A_SCORE, SCORE_B.toString());
stringRedisTemplate.opsForHash().put(scoreKey, PkRedisKeyConstants.FIELD_CLERK_B_SCORE, SCORE_A.toString());
mockMvc.perform(post(buildFinishPath(pk.getId()))
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(ResultCodeEnum.SUCCESS.getCode()));
PlayClerkPkEntity persisted = loadPk(pk.getId());
assertThat(persisted.getWinnerClerkId()).isEqualTo(pk.getClerkB());
}
@Test
void finishPkShouldKeepWinnerNullOnTie() throws Exception {
LocalDateTime now = LocalDateTime.now();
PlayClerkPkEntity pk = buildPk(ClerkPkEnum.IN_PROGRESS.name(),
now.minusMinutes(MINUTES_BEFORE_START),
now.minusMinutes(MINUTES_BEFORE_START),
SETTLED_FALSE);
String scoreKey = PkRedisKeyConstants.scoreKey(pk.getId());
stringRedisTemplate.opsForHash().put(scoreKey, PkRedisKeyConstants.FIELD_CLERK_A_SCORE, TIE_SCORE.toString());
stringRedisTemplate.opsForHash().put(scoreKey, PkRedisKeyConstants.FIELD_CLERK_B_SCORE, TIE_SCORE.toString());
mockMvc.perform(post(buildFinishPath(pk.getId()))
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(ResultCodeEnum.SUCCESS.getCode()));
PlayClerkPkEntity persisted = loadPk(pk.getId());
assertThat(persisted.getWinnerClerkId()).isNull();
}
@Test
void scoreboardShouldReturnRedisValues() throws Exception {
LocalDateTime now = LocalDateTime.now();
PlayClerkPkEntity pk = buildPk(ClerkPkEnum.IN_PROGRESS.name(),
now.minusMinutes(MINUTES_BEFORE_START),
now.plusMinutes(MINUTES_AFTER_START),
SETTLED_FALSE);
String scoreKey = PkRedisKeyConstants.scoreKey(pk.getId());
stringRedisTemplate.opsForHash().put(scoreKey, PkRedisKeyConstants.FIELD_CLERK_A_SCORE, SCORE_A.toString());
stringRedisTemplate.opsForHash().put(scoreKey, PkRedisKeyConstants.FIELD_CLERK_B_SCORE, SCORE_B.toString());
stringRedisTemplate.opsForHash().put(scoreKey,
PkRedisKeyConstants.FIELD_CLERK_A_ORDER_COUNT, String.valueOf(ORDER_COUNT_A));
stringRedisTemplate.opsForHash().put(scoreKey,
PkRedisKeyConstants.FIELD_CLERK_B_ORDER_COUNT, String.valueOf(ORDER_COUNT_B));
mockMvc.perform(get(buildScoreboardPath(pk.getId()))
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(ResultCodeEnum.SUCCESS.getCode()))
.andExpect(jsonPath("$.data.clerkAScore").value(SCORE_A.doubleValue()))
.andExpect(jsonPath("$.data.clerkBScore").value(SCORE_B.doubleValue()))
.andExpect(jsonPath("$.data.clerkAOrderCount").value(ORDER_COUNT_A))
.andExpect(jsonPath("$.data.clerkBOrderCount").value(ORDER_COUNT_B));
}
@Test
void scoreboardShouldReturnZeroWhenRedisEmpty() throws Exception {
LocalDateTime now = LocalDateTime.now();
PlayClerkPkEntity pk = buildPk(ClerkPkEnum.IN_PROGRESS.name(),
now.minusMinutes(MINUTES_BEFORE_START),
now.plusMinutes(MINUTES_AFTER_START),
SETTLED_FALSE);
mockMvc.perform(get(buildScoreboardPath(pk.getId()))
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(ResultCodeEnum.SUCCESS.getCode()))
.andExpect(jsonPath("$.data.clerkAScore").value(ZERO_VALUE))
.andExpect(jsonPath("$.data.clerkBScore").value(ZERO_VALUE))
.andExpect(jsonPath("$.data.clerkAOrderCount").value(ZERO_COUNT))
.andExpect(jsonPath("$.data.clerkBOrderCount").value(ZERO_COUNT));
}
@Test
void scoreboardShouldReturnZeroRemainingSecondsWhenEnded() throws Exception {
LocalDateTime now = LocalDateTime.now();
PlayClerkPkEntity pk = buildPk(ClerkPkEnum.IN_PROGRESS.name(),
now.minusMinutes(MINUTES_BEFORE_START),
now.minusMinutes(MINUTES_PAST_END),
SETTLED_FALSE);
mockMvc.perform(get(buildScoreboardPath(pk.getId()))
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(ResultCodeEnum.SUCCESS.getCode()))
.andExpect(jsonPath("$.data.remainingSeconds").value(REMAINING_SECONDS_ZERO));
}
@Test
void scoreboardShouldFailWhenMissingPk() throws Exception {
mockMvc.perform(get(buildScoreboardPath(IdUtils.getUuid()))
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(ResultCodeEnum.FAILED.getCode()))
.andExpect(jsonPath("$.message").value(MSG_PK_NOT_FOUND));
}
private PlayClerkPkEntity buildPk(String status, LocalDateTime beginTime, LocalDateTime endTime, int settled) {
String pkId = IdUtils.getUuid();
String clerkAId = newClerkId();
String clerkBId = newClerkId();
PlayClerkPkEntity pk = new PlayClerkPkEntity();
pk.setId(pkId);
pk.setTenantId(DEFAULT_TENANT);
pk.setClerkA(clerkAId);
pk.setClerkB(clerkBId);
pk.setPkBeginTime(Date.from(beginTime.atZone(ZoneId.systemDefault()).toInstant()));
pk.setPkEndTime(Date.from(endTime.atZone(ZoneId.systemDefault()).toInstant()));
pk.setStatus(status);
pk.setSettled(settled);
SecurityUtils.setTenantId(DEFAULT_TENANT);
clerkPkService.save(pk);
return pk;
}
private PlayClerkPkEntity createPkForClerks(String clerkAId, String clerkBId, LocalDateTime beginTime,
LocalDateTime endTime, String status) {
PlayClerkPkEntity pk = new PlayClerkPkEntity();
pk.setId(IdUtils.getUuid());
pk.setTenantId(DEFAULT_TENANT);
pk.setClerkA(clerkAId);
pk.setClerkB(clerkBId);
pk.setPkBeginTime(Date.from(beginTime.atZone(ZoneId.systemDefault()).toInstant()));
pk.setPkEndTime(Date.from(endTime.atZone(ZoneId.systemDefault()).toInstant()));
pk.setStatus(status);
pk.setSettled(SETTLED_FALSE);
SecurityUtils.setTenantId(DEFAULT_TENANT);
clerkPkService.save(pk);
return pk;
}
private PlayClerkPkEntity loadPk(String pkId) {
SecurityUtils.setTenantId(DEFAULT_TENANT);
PlayClerkPkEntity persisted = clerkPkService.selectPlayClerkPkById(pkId);
assertThat(persisted).isNotNull();
return persisted;
}
private static String newClerkId() {
return CLERK_PREFIX + IdUtils.getUuid();
}
private static String buildStartPath(String pkId) {
return "/play/pk/" + pkId + "/start";
}
private static String buildFinishPath(String pkId) {
return "/play/pk/" + pkId + "/finish";
}
private static String buildScoreboardPath(String pkId) {
return "/play/pk/" + pkId + "/scoreboard";
}
private static String buildForceStartPath() {
return "/play/pk/force-start";
}
private static String buildForceStartBody(String clerkAId, String clerkBId, int durationMinutes) {
StringBuilder builder = new StringBuilder();
builder.append("{");
builder.append("\"clerkAId\":\"").append(clerkAId).append("\",");
builder.append("\"clerkBId\":\"").append(clerkBId).append("\",");
builder.append("\"durationMinutes\":").append(durationMinutes);
builder.append("}");
return builder.toString();
}
private static String extractDataAsText(MvcResult result) throws Exception {
JsonNode root = OBJECT_MAPPER.readTree(result.getResponse().getContentAsString());
return root.path("data").asText();
}
}

View File

@@ -0,0 +1,583 @@
package com.starry.admin.modules.pk;
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.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.starry.admin.api.AbstractApiTest;
import com.starry.admin.modules.clerk.module.entity.ClerkPkEnum;
import com.starry.admin.modules.clerk.module.entity.PlayClerkPkEntity;
import com.starry.admin.modules.clerk.service.IPlayClerkPkService;
import com.starry.admin.modules.pk.setting.constants.PkSettingApiConstants;
import com.starry.admin.modules.pk.setting.enums.PkRecurrenceType;
import com.starry.admin.modules.pk.setting.enums.PkSettingErrorCode;
import com.starry.admin.modules.pk.setting.enums.PkSettingStatus;
import com.starry.admin.utils.SecurityUtils;
import com.starry.common.result.ResultCodeEnum;
import com.starry.common.utils.IdUtils;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.ZoneId;
import java.util.Date;
import java.util.List;
import javax.annotation.Resource;
import org.junit.jupiter.api.Test;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MvcResult;
class PlayClerkPkSettingApiTest extends AbstractApiTest {
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
private static final String SETTING_NAME = "PK排期测试";
private static final String START_TIME_OF_DAY = "08:00:00";
private static final int DURATION_MINUTES = 30;
private static final String EFFECTIVE_START_DATE = "2025-01-01";
private static final String EFFECTIVE_END_DATE = "2025-01-03";
private static final String TIMEZONE = "Asia/Shanghai";
private static final int EXPECTED_GENERATED_COUNT = 3;
private static final int EXPECTED_ONCE_COUNT = 1;
private static final int EXPECTED_ZERO_COUNT = 0;
private static final int EXPECTED_BULK_COUNT = 2;
private static final int INVALID_DAY_OF_MONTH = 32;
private static final int INVALID_MONTH_OF_YEAR = 13;
private static final int MONTH_OF_YEAR_JANUARY = 1;
private static final int DAY_OF_MONTH_FIRST = 1;
private static final int DAY_OF_MONTH_THIRTY_FIRST = 31;
private static final int SETTLED_FALSE = 0;
private static final String INVALID_TIMEZONE = "Invalid/Zone";
private static final String EARLY_END_DATE = "2024-12-31";
private static final String LONG_RANGE_START_DATE = "2025-01-01";
private static final String LONG_RANGE_END_DATE = "2029-01-01";
private static final String MONTHLY_SHORT_START_DATE = "2025-04-01";
private static final String MONTHLY_SHORT_END_DATE = "2025-04-30";
private static final String CONFLICT_START_DATE = "2025-01-01";
private static final String CONFLICT_END_DATE = "2025-01-02";
private static final String UPDATED_NAME = "PK排期更新";
@Resource
private IPlayClerkPkService clerkPkService;
@Test
void pkSettingFlowShouldWorkEndToEnd() throws Exception {
String clerkAId = "clerk-" + IdUtils.getUuid();
String clerkBId = "clerk-" + IdUtils.getUuid();
String createRequestBody = buildRequestBody(SETTING_NAME, PkRecurrenceType.DAILY,
null, null, null, PkSettingStatus.ENABLED, clerkAId, clerkBId, EFFECTIVE_START_DATE,
EFFECTIVE_END_DATE, TIMEZONE);
String updateRequestBody = buildRequestBody(UPDATED_NAME, PkRecurrenceType.DAILY,
null, null, null, PkSettingStatus.DISABLED, clerkAId, clerkBId, EFFECTIVE_START_DATE,
EFFECTIVE_END_DATE, TIMEZONE);
String createPath = PkSettingApiConstants.BASE_PATH + PkSettingApiConstants.CREATE_PATH;
MvcResult createResult = mockMvc.perform(post(createPath)
.contentType(MediaType.APPLICATION_JSON)
.content(createRequestBody)
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(ResultCodeEnum.SUCCESS.getCode()))
.andReturn();
String settingId = extractDataAsText(createResult);
String listPath = PkSettingApiConstants.BASE_PATH + PkSettingApiConstants.LIST_PATH;
mockMvc.perform(get(listPath)
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(ResultCodeEnum.SUCCESS.getCode()))
.andExpect(jsonPath("$.data").isArray());
String detailPath = PkSettingApiConstants.BASE_PATH + "/"
+ settingId;
mockMvc.perform(get(detailPath)
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(ResultCodeEnum.SUCCESS.getCode()))
.andExpect(jsonPath("$.data.id").value(settingId))
.andExpect(jsonPath("$.data.name").value(SETTING_NAME));
String updatePath = PkSettingApiConstants.BASE_PATH + "/update/" + settingId;
mockMvc.perform(post(updatePath)
.contentType(MediaType.APPLICATION_JSON)
.content(updateRequestBody)
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(ResultCodeEnum.SUCCESS.getCode()));
mockMvc.perform(get(detailPath)
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(ResultCodeEnum.SUCCESS.getCode()))
.andExpect(jsonPath("$.data.name").value(UPDATED_NAME));
String enablePath = PkSettingApiConstants.BASE_PATH + "/" + settingId + "/enable";
mockMvc.perform(post(enablePath)
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(ResultCodeEnum.SUCCESS.getCode()));
SecurityUtils.setTenantId(DEFAULT_TENANT);
List<PlayClerkPkEntity> generated = clerkPkService.list(
Wrappers.<PlayClerkPkEntity>lambdaQuery().eq(PlayClerkPkEntity::getSettingId, settingId));
assertThat(generated).hasSize(EXPECTED_GENERATED_COUNT);
}
@Test
void createSettingShouldRejectMissingWeeklyFields() throws Exception {
String createPath = PkSettingApiConstants.BASE_PATH + PkSettingApiConstants.CREATE_PATH;
String clerkAId = "clerk-" + IdUtils.getUuid();
String clerkBId = "clerk-" + IdUtils.getUuid();
String invalidWeeklyRequestBody = buildRequestBody("无效周排期", PkRecurrenceType.WEEKLY,
null, null, null, PkSettingStatus.ENABLED, clerkAId, clerkBId, EFFECTIVE_START_DATE,
EFFECTIVE_END_DATE, TIMEZONE);
mockMvc.perform(post(createPath)
.contentType(MediaType.APPLICATION_JSON)
.content(invalidWeeklyRequestBody)
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(ResultCodeEnum.FAILED.getCode()))
.andExpect(jsonPath("$.message").value(PkSettingErrorCode.RECURRENCE_INVALID.getMessage()));
}
@Test
void createSettingShouldRejectInvalidMonthlyDay() throws Exception {
String createPath = PkSettingApiConstants.BASE_PATH + PkSettingApiConstants.CREATE_PATH;
String clerkAId = "clerk-" + IdUtils.getUuid();
String clerkBId = "clerk-" + IdUtils.getUuid();
String invalidMonthlyRequestBody = buildRequestBody("无效月排期", PkRecurrenceType.MONTHLY,
null, INVALID_DAY_OF_MONTH, null, PkSettingStatus.ENABLED, clerkAId, clerkBId, EFFECTIVE_START_DATE,
EFFECTIVE_END_DATE, TIMEZONE);
mockMvc.perform(post(createPath)
.contentType(MediaType.APPLICATION_JSON)
.content(invalidMonthlyRequestBody)
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(ResultCodeEnum.FAILED.getCode()))
.andExpect(jsonPath("$.message").value(PkSettingErrorCode.RECURRENCE_INVALID.getMessage()));
}
@Test
void generateShouldRejectClerkConflict() throws Exception {
String createPath = PkSettingApiConstants.BASE_PATH + PkSettingApiConstants.CREATE_PATH;
String clerkAId = "clerk-" + IdUtils.getUuid();
String clerkBId = "clerk-" + IdUtils.getUuid();
String createRequestBody = buildRequestBody(SETTING_NAME, PkRecurrenceType.DAILY,
null, null, null, PkSettingStatus.ENABLED, clerkAId, clerkBId, EFFECTIVE_START_DATE,
EFFECTIVE_END_DATE, TIMEZONE);
String conflictRequestBody = buildRequestBody("冲突排期", PkRecurrenceType.DAILY,
null, null, null, PkSettingStatus.ENABLED, clerkAId, clerkBId, EFFECTIVE_START_DATE,
EFFECTIVE_END_DATE, TIMEZONE);
mockMvc.perform(post(createPath)
.contentType(MediaType.APPLICATION_JSON)
.content(createRequestBody)
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(ResultCodeEnum.SUCCESS.getCode()));
mockMvc.perform(post(createPath)
.contentType(MediaType.APPLICATION_JSON)
.content(conflictRequestBody)
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(ResultCodeEnum.FAILED.getCode()))
.andExpect(jsonPath("$.message").value(PkSettingErrorCode.CLERK_CONFLICT.getMessage()));
}
@Test
void bulkCreateShouldCreateMultipleSettings() throws Exception {
String bulkCreatePath = PkSettingApiConstants.BASE_PATH + PkSettingApiConstants.BULK_CREATE_PATH;
String clerkAId = "clerk-" + IdUtils.getUuid();
String clerkBId = "clerk-" + IdUtils.getUuid();
String clerkCId = "clerk-" + IdUtils.getUuid();
String clerkDId = "clerk-" + IdUtils.getUuid();
String firstRequestBody = buildRequestBody("批量排期A", PkRecurrenceType.DAILY,
null, null, null, PkSettingStatus.ENABLED, clerkAId, clerkBId, EFFECTIVE_START_DATE,
EFFECTIVE_END_DATE, TIMEZONE);
String secondRequestBody = buildRequestBody("批量排期B", PkRecurrenceType.DAILY,
null, null, null, PkSettingStatus.ENABLED, clerkCId, clerkDId, EFFECTIVE_START_DATE,
EFFECTIVE_END_DATE, TIMEZONE);
String bulkRequestBody = buildBulkRequestBody(List.of(firstRequestBody, secondRequestBody));
MvcResult result = mockMvc.perform(post(bulkCreatePath)
.contentType(MediaType.APPLICATION_JSON)
.content(bulkRequestBody)
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(ResultCodeEnum.SUCCESS.getCode()))
.andReturn();
JsonNode data = OBJECT_MAPPER.readTree(result.getResponse().getContentAsString()).path("data");
assertThat(data.isArray()).isTrue();
assertThat(data.size()).isEqualTo(EXPECTED_BULK_COUNT);
}
@Test
void bulkCreateShouldRejectDuplicatePairs() throws Exception {
String bulkCreatePath = PkSettingApiConstants.BASE_PATH + PkSettingApiConstants.BULK_CREATE_PATH;
String clerkAId = "clerk-" + IdUtils.getUuid();
String clerkBId = "clerk-" + IdUtils.getUuid();
String firstRequestBody = buildRequestBody("批量排期重复A", PkRecurrenceType.DAILY,
null, null, null, PkSettingStatus.ENABLED, clerkAId, clerkBId, EFFECTIVE_START_DATE,
EFFECTIVE_END_DATE, TIMEZONE);
String secondRequestBody = buildRequestBody("批量排期重复B", PkRecurrenceType.DAILY,
null, null, null, PkSettingStatus.ENABLED, clerkBId, clerkAId, EFFECTIVE_START_DATE,
EFFECTIVE_END_DATE, TIMEZONE);
String bulkRequestBody = buildBulkRequestBody(List.of(firstRequestBody, secondRequestBody));
mockMvc.perform(post(bulkCreatePath)
.contentType(MediaType.APPLICATION_JSON)
.content(bulkRequestBody)
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(ResultCodeEnum.FAILED.getCode()))
.andExpect(jsonPath("$.message").value(PkSettingErrorCode.REQUEST_INVALID.getMessage()));
}
@Test
void createSettingShouldRejectMissingMonthlyDay() throws Exception {
String createPath = PkSettingApiConstants.BASE_PATH + PkSettingApiConstants.CREATE_PATH;
String clerkAId = "clerk-" + IdUtils.getUuid();
String clerkBId = "clerk-" + IdUtils.getUuid();
String invalidMonthlyRequestBody = buildRequestBody("缺失月日", PkRecurrenceType.MONTHLY,
null, null, null, PkSettingStatus.ENABLED, clerkAId, clerkBId, EFFECTIVE_START_DATE,
EFFECTIVE_END_DATE, TIMEZONE);
mockMvc.perform(post(createPath)
.contentType(MediaType.APPLICATION_JSON)
.content(invalidMonthlyRequestBody)
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(ResultCodeEnum.FAILED.getCode()))
.andExpect(jsonPath("$.message").value(PkSettingErrorCode.RECURRENCE_INVALID.getMessage()));
}
@Test
void createSettingShouldRejectMissingYearlyFields() throws Exception {
String createPath = PkSettingApiConstants.BASE_PATH + PkSettingApiConstants.CREATE_PATH;
String clerkAId = "clerk-" + IdUtils.getUuid();
String clerkBId = "clerk-" + IdUtils.getUuid();
String invalidYearlyRequestBody = buildRequestBody("缺失年字段", PkRecurrenceType.YEARLY,
null, DAY_OF_MONTH_FIRST, null, PkSettingStatus.ENABLED, clerkAId, clerkBId, EFFECTIVE_START_DATE,
EFFECTIVE_END_DATE, TIMEZONE);
mockMvc.perform(post(createPath)
.contentType(MediaType.APPLICATION_JSON)
.content(invalidYearlyRequestBody)
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(ResultCodeEnum.FAILED.getCode()))
.andExpect(jsonPath("$.message").value(PkSettingErrorCode.RECURRENCE_INVALID.getMessage()));
}
@Test
void createSettingShouldRejectInvalidMonthOfYear() throws Exception {
String createPath = PkSettingApiConstants.BASE_PATH + PkSettingApiConstants.CREATE_PATH;
String clerkAId = "clerk-" + IdUtils.getUuid();
String clerkBId = "clerk-" + IdUtils.getUuid();
String invalidYearlyRequestBody = buildRequestBody("无效月份", PkRecurrenceType.YEARLY,
null, DAY_OF_MONTH_FIRST, INVALID_MONTH_OF_YEAR, PkSettingStatus.ENABLED, clerkAId, clerkBId,
EFFECTIVE_START_DATE, EFFECTIVE_END_DATE, TIMEZONE);
mockMvc.perform(post(createPath)
.contentType(MediaType.APPLICATION_JSON)
.content(invalidYearlyRequestBody)
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(ResultCodeEnum.FAILED.getCode()))
.andExpect(jsonPath("$.message").value(PkSettingErrorCode.RECURRENCE_INVALID.getMessage()));
}
@Test
void createSettingShouldRejectInvalidTimeRange() throws Exception {
String createPath = PkSettingApiConstants.BASE_PATH + PkSettingApiConstants.CREATE_PATH;
String clerkAId = "clerk-" + IdUtils.getUuid();
String clerkBId = "clerk-" + IdUtils.getUuid();
String invalidRangeRequestBody = buildRequestBody("无效时间范围", PkRecurrenceType.DAILY,
null, null, null, PkSettingStatus.ENABLED, clerkAId, clerkBId, EFFECTIVE_START_DATE,
EARLY_END_DATE, TIMEZONE);
mockMvc.perform(post(createPath)
.contentType(MediaType.APPLICATION_JSON)
.content(invalidRangeRequestBody)
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(ResultCodeEnum.FAILED.getCode()))
.andExpect(jsonPath("$.message").value(PkSettingErrorCode.TIME_RANGE_INVALID.getMessage()));
}
@Test
void createSettingShouldRejectSameClerk() throws Exception {
String createPath = PkSettingApiConstants.BASE_PATH + PkSettingApiConstants.CREATE_PATH;
String clerkId = "clerk-" + IdUtils.getUuid();
String invalidRequestBody = buildRequestBody("同店员", PkRecurrenceType.DAILY,
null, null, null, PkSettingStatus.ENABLED, clerkId, clerkId, EFFECTIVE_START_DATE,
EFFECTIVE_END_DATE, TIMEZONE);
mockMvc.perform(post(createPath)
.contentType(MediaType.APPLICATION_JSON)
.content(invalidRequestBody)
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(ResultCodeEnum.FAILED.getCode()))
.andExpect(jsonPath("$.message").value(PkSettingErrorCode.REQUEST_INVALID.getMessage()));
}
@Test
void createSettingShouldRejectInvalidTimezone() throws Exception {
String createPath = PkSettingApiConstants.BASE_PATH + PkSettingApiConstants.CREATE_PATH;
String clerkAId = "clerk-" + IdUtils.getUuid();
String clerkBId = "clerk-" + IdUtils.getUuid();
String invalidRequestBody = buildRequestBody("无效时区", PkRecurrenceType.DAILY,
null, null, null, PkSettingStatus.ENABLED, clerkAId, clerkBId, EFFECTIVE_START_DATE,
EFFECTIVE_END_DATE, INVALID_TIMEZONE);
mockMvc.perform(post(createPath)
.contentType(MediaType.APPLICATION_JSON)
.content(invalidRequestBody)
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(ResultCodeEnum.FAILED.getCode()))
.andExpect(jsonPath("$.message").value(PkSettingErrorCode.REQUEST_INVALID.getMessage()));
}
@Test
void generateShouldRespectThreeYearLimit() throws Exception {
String clerkAId = "clerk-" + IdUtils.getUuid();
String clerkBId = "clerk-" + IdUtils.getUuid();
String requestBody = buildRequestBody("三年上限", PkRecurrenceType.YEARLY,
null, DAY_OF_MONTH_FIRST, MONTH_OF_YEAR_JANUARY, PkSettingStatus.ENABLED, clerkAId, clerkBId,
LONG_RANGE_START_DATE, LONG_RANGE_END_DATE, TIMEZONE);
String createPath = PkSettingApiConstants.BASE_PATH + PkSettingApiConstants.CREATE_PATH;
String settingId = extractDataAsText(mockMvc.perform(post(createPath)
.contentType(MediaType.APPLICATION_JSON)
.content(requestBody)
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(ResultCodeEnum.SUCCESS.getCode()))
.andReturn());
SecurityUtils.setTenantId(DEFAULT_TENANT);
List<PlayClerkPkEntity> generated = clerkPkService.list(
Wrappers.<PlayClerkPkEntity>lambdaQuery().eq(PlayClerkPkEntity::getSettingId, settingId));
assertThat(generated).isNotEmpty();
LocalDate maxDate = generated.stream()
.map(PlayClerkPkEntity::getPkBeginTime)
.filter(date -> date != null)
.map(date -> date.toInstant().atZone(java.time.ZoneId.systemDefault()).toLocalDate())
.max(LocalDate::compareTo)
.orElse(LocalDate.parse(LONG_RANGE_START_DATE));
LocalDate maxAllowed = LocalDate.parse(LONG_RANGE_START_DATE).plusYears(3);
assertThat(maxDate).isBefore(maxAllowed.plusDays(1));
}
@Test
void monthlyInvalidDayShouldSkipWithoutError() throws Exception {
String clerkAId = "clerk-" + IdUtils.getUuid();
String clerkBId = "clerk-" + IdUtils.getUuid();
String requestBody = buildRequestBody("无效日期跳过", PkRecurrenceType.MONTHLY,
null, DAY_OF_MONTH_THIRTY_FIRST, null, PkSettingStatus.ENABLED, clerkAId, clerkBId,
MONTHLY_SHORT_START_DATE, MONTHLY_SHORT_END_DATE, TIMEZONE);
String createPath = PkSettingApiConstants.BASE_PATH + PkSettingApiConstants.CREATE_PATH;
String settingId = extractDataAsText(mockMvc.perform(post(createPath)
.contentType(MediaType.APPLICATION_JSON)
.content(requestBody)
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(ResultCodeEnum.SUCCESS.getCode()))
.andReturn());
SecurityUtils.setTenantId(DEFAULT_TENANT);
List<PlayClerkPkEntity> generated = clerkPkService.list(
Wrappers.<PlayClerkPkEntity>lambdaQuery().eq(PlayClerkPkEntity::getSettingId, settingId));
assertThat(generated).hasSize(EXPECTED_ZERO_COUNT);
}
@Test
void onceScheduleShouldGenerateSingleInstance() throws Exception {
String clerkAId = "clerk-" + IdUtils.getUuid();
String clerkBId = "clerk-" + IdUtils.getUuid();
String requestBody = buildRequestBody("单次排期", PkRecurrenceType.ONCE,
null, null, null, PkSettingStatus.ENABLED, clerkAId, clerkBId, EFFECTIVE_START_DATE,
EFFECTIVE_START_DATE, TIMEZONE);
String createPath = PkSettingApiConstants.BASE_PATH + PkSettingApiConstants.CREATE_PATH;
String settingId = extractDataAsText(mockMvc.perform(post(createPath)
.contentType(MediaType.APPLICATION_JSON)
.content(requestBody)
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(ResultCodeEnum.SUCCESS.getCode()))
.andReturn());
SecurityUtils.setTenantId(DEFAULT_TENANT);
List<PlayClerkPkEntity> generated = clerkPkService.list(
Wrappers.<PlayClerkPkEntity>lambdaQuery().eq(PlayClerkPkEntity::getSettingId, settingId));
assertThat(generated).hasSize(EXPECTED_ONCE_COUNT);
}
@Test
void disabledSettingShouldNotGenerateInstances() throws Exception {
String clerkAId = "clerk-" + IdUtils.getUuid();
String clerkBId = "clerk-" + IdUtils.getUuid();
String requestBody = buildRequestBody("禁用生成", PkRecurrenceType.DAILY,
null, null, null, PkSettingStatus.DISABLED, clerkAId, clerkBId, EFFECTIVE_START_DATE,
EFFECTIVE_END_DATE, TIMEZONE);
String createPath = PkSettingApiConstants.BASE_PATH + PkSettingApiConstants.CREATE_PATH;
String settingId = extractDataAsText(mockMvc.perform(post(createPath)
.contentType(MediaType.APPLICATION_JSON)
.content(requestBody)
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(ResultCodeEnum.SUCCESS.getCode()))
.andReturn());
SecurityUtils.setTenantId(DEFAULT_TENANT);
List<PlayClerkPkEntity> generated = clerkPkService.list(
Wrappers.<PlayClerkPkEntity>lambdaQuery().eq(PlayClerkPkEntity::getSettingId, settingId));
assertThat(generated).hasSize(EXPECTED_ZERO_COUNT);
}
@Test
void generateShouldRollbackWhenConflictDetected() throws Exception {
String clerkAId = "clerk-" + IdUtils.getUuid();
String clerkBId = "clerk-" + IdUtils.getUuid();
String requestBody = buildRequestBody("冲突回滚", PkRecurrenceType.DAILY,
null, null, null, PkSettingStatus.DISABLED, clerkAId, clerkBId, CONFLICT_START_DATE,
CONFLICT_END_DATE, TIMEZONE);
String createPath = PkSettingApiConstants.BASE_PATH + PkSettingApiConstants.CREATE_PATH;
String settingId = extractDataAsText(mockMvc.perform(post(createPath)
.contentType(MediaType.APPLICATION_JSON)
.content(requestBody)
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(ResultCodeEnum.SUCCESS.getCode()))
.andReturn());
LocalDateTime conflictStart = LocalDateTime.of(LocalDate.parse(CONFLICT_END_DATE),
LocalTime.parse(START_TIME_OF_DAY));
LocalDateTime conflictEnd = conflictStart.plusMinutes(DURATION_MINUTES);
ZoneId zoneId = ZoneId.of(TIMEZONE);
PlayClerkPkEntity conflictPk = new PlayClerkPkEntity();
conflictPk.setId(IdUtils.getUuid());
conflictPk.setTenantId(DEFAULT_TENANT);
conflictPk.setClerkA(clerkAId);
conflictPk.setClerkB(clerkBId);
conflictPk.setPkBeginTime(Date.from(conflictStart.atZone(zoneId).toInstant()));
conflictPk.setPkEndTime(Date.from(conflictEnd.atZone(zoneId).toInstant()));
conflictPk.setStatus(ClerkPkEnum.TO_BE_STARTED.name());
conflictPk.setSettled(SETTLED_FALSE);
SecurityUtils.setTenantId(DEFAULT_TENANT);
clerkPkService.save(conflictPk);
String enablePath = PkSettingApiConstants.BASE_PATH + "/" + settingId + "/enable";
mockMvc.perform(post(enablePath)
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(ResultCodeEnum.FAILED.getCode()))
.andExpect(jsonPath("$.message").value(PkSettingErrorCode.CLERK_CONFLICT.getMessage()));
SecurityUtils.setTenantId(DEFAULT_TENANT);
List<PlayClerkPkEntity> generated = clerkPkService.list(
Wrappers.<PlayClerkPkEntity>lambdaQuery().eq(PlayClerkPkEntity::getSettingId, settingId));
assertThat(generated).isEmpty();
}
@Test
void listShouldExcludeOtherTenantSettings() throws Exception {
String tenantA = "tenant-" + IdUtils.getUuid();
String tenantB = "tenant-" + IdUtils.getUuid();
String clerkAId = "clerk-" + IdUtils.getUuid();
String clerkBId = "clerk-" + IdUtils.getUuid();
String requestBody = buildRequestBody("多租户排期", PkRecurrenceType.DAILY,
null, null, null, PkSettingStatus.ENABLED, clerkAId, clerkBId, EFFECTIVE_START_DATE,
EFFECTIVE_END_DATE, TIMEZONE);
String createPath = PkSettingApiConstants.BASE_PATH + PkSettingApiConstants.CREATE_PATH;
mockMvc.perform(post(createPath)
.contentType(MediaType.APPLICATION_JSON)
.content(requestBody)
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, tenantB))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(ResultCodeEnum.SUCCESS.getCode()));
String listPath = PkSettingApiConstants.BASE_PATH + PkSettingApiConstants.LIST_PATH;
MvcResult listResult = mockMvc.perform(get(listPath)
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, tenantA))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(ResultCodeEnum.SUCCESS.getCode()))
.andReturn();
JsonNode data = OBJECT_MAPPER.readTree(listResult.getResponse().getContentAsString()).path("data");
if (data.isArray()) {
for (JsonNode node : data) {
String tenantId = node.path("tenantId").asText();
assertThat(tenantId).isNotEqualTo(tenantB);
}
}
}
private static String extractDataAsText(MvcResult result) throws Exception {
JsonNode root = OBJECT_MAPPER.readTree(result.getResponse().getContentAsString());
return root.path("data").asText();
}
private static String buildRequestBody(String name, PkRecurrenceType recurrenceType, String dayOfWeek,
Integer dayOfMonth, Integer monthOfYear, PkSettingStatus status, String clerkAId, String clerkBId,
String effectiveStartDate, String effectiveEndDate, String timezone) {
StringBuilder builder = new StringBuilder();
builder.append("{");
builder.append("\"name\":\"").append(name).append("\",");
builder.append("\"recurrenceType\":\"").append(recurrenceType.getValue()).append("\",");
if (dayOfWeek != null) {
builder.append("\"dayOfWeek\":\"").append(dayOfWeek).append("\",");
}
if (dayOfMonth != null) {
builder.append("\"dayOfMonth\":").append(dayOfMonth).append(",");
}
if (monthOfYear != null) {
builder.append("\"monthOfYear\":").append(monthOfYear).append(",");
}
builder.append("\"startTimeOfDay\":\"").append(START_TIME_OF_DAY).append("\",");
builder.append("\"durationMinutes\":").append(DURATION_MINUTES).append(",");
builder.append("\"effectiveStartDate\":\"").append(effectiveStartDate).append("\",");
builder.append("\"effectiveEndDate\":\"").append(effectiveEndDate).append("\",");
builder.append("\"timezone\":\"").append(timezone).append("\",");
builder.append("\"clerkAId\":\"").append(clerkAId).append("\",");
builder.append("\"clerkBId\":\"").append(clerkBId).append("\",");
builder.append("\"status\":\"").append(status.getValue()).append("\"");
builder.append("}");
return builder.toString();
}
private static String buildBulkRequestBody(List<String> settings) {
StringBuilder builder = new StringBuilder();
builder.append("{\"settings\":[");
for (int i = 0; i < settings.size(); i++) {
builder.append(settings.get(i));
if (i < settings.size() - 1) {
builder.append(",");
}
}
builder.append("]}");
return builder.toString();
}
}

View File

@@ -0,0 +1,642 @@
package com.starry.admin.modules.pk;
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.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.api.AbstractApiTest;
import com.starry.admin.common.exception.handler.GlobalExceptionHandler;
import com.starry.admin.modules.clerk.module.entity.ClerkPkEnum;
import com.starry.admin.modules.clerk.module.entity.PlayClerkPkEntity;
import com.starry.admin.modules.clerk.module.entity.PlayClerkUserInfoEntity;
import com.starry.admin.modules.clerk.service.IPlayClerkPkService;
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.entity.PlayOrderInfoEntity;
import com.starry.admin.modules.order.service.IPlayOrderInfoService;
import com.starry.admin.modules.pk.enums.PkWxState;
import com.starry.admin.modules.pk.redis.PkRedisKeyConstants;
import com.starry.admin.utils.SecurityUtils;
import com.starry.common.result.ResultCodeEnum;
import com.starry.common.utils.IdUtils;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Date;
import javax.annotation.Resource;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.test.web.servlet.MvcResult;
class WxPkApiTest extends AbstractApiTest {
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
private static final String CLERK_PREFIX = "wx-pk-clerk-";
private static final String CUSTOM_PREFIX = "wx-pk-custom-";
private static final String OPENID_PREFIX = "openid-";
private static final String CLERK_NAME_PREFIX = "店员-";
private static final String CUSTOM_NAME_PREFIX = "贡献者-";
private static final String PK_PREFIX = "wx-pk-";
private static final String PARAM_CLERK_ID = "clerkId";
private static final String PARAM_ID = "id";
private static final String PARAM_LIMIT = "limit";
private static final String MSG_PARAM_INVALID = GlobalExceptionHandler.PARAMETER_FORMAT_ERROR;
private static final String PARAM_PAGE_NUM = "pageNum";
private static final String PARAM_PAGE_SIZE = "pageSize";
private static final int MINUTES_BEFORE_START = 5;
private static final int MINUTES_AFTER_START = 30;
private static final int MINUTES_FUTURE_START = 10;
private static final int MINUTES_ORDER_OFFSET = 2;
private static final int HISTORY_FIRST_DAY_OFFSET = 2;
private static final int HISTORY_SECOND_DAY_OFFSET = 1;
private static final int FUTURE_FIRST_OFFSET_MINUTES = 30;
private static final int FUTURE_SECOND_OFFSET_MINUTES = 60;
private static final int PAST_OFFSET_MINUTES = 10;
private static final int FUTURE_THIRD_OFFSET_MINUTES = 90;
private static final int DEFAULT_LIMIT = 3;
private static final int LIMIT_ZERO = 0;
private static final int LIMIT_NEGATIVE = -1;
private static final int LIMIT_LARGE = 100;
private static final int SCHEDULE_LIMIT = 2;
private static final int HISTORY_PAGE_NUM = 1;
private static final int HISTORY_PAGE_SIZE = 10;
private static final long REMAINING_SECONDS_MIN = 1L;
private static final int SETTLED_FALSE = 0;
private static final int SETTLED_TRUE = 1;
private static final double ZSET_SCORE_PAST = 1D;
private static final BigDecimal AMOUNT_HIGH = new BigDecimal("120.00");
private static final BigDecimal AMOUNT_MEDIUM = new BigDecimal("100.00");
private static final BigDecimal AMOUNT_LOW = new BigDecimal("50.00");
private static final int EXPECTED_HISTORY_COUNT = 2;
@Resource
private IPlayClerkPkService clerkPkService;
@Resource
private StringRedisTemplate stringRedisTemplate;
@Resource
private IPlayClerkUserInfoService clerkUserInfoService;
@Resource
private IPlayCustomUserInfoService customUserInfoService;
@Resource
private IPlayOrderInfoService orderInfoService;
@BeforeEach
void clearRedis() {
if (stringRedisTemplate.getConnectionFactory() == null) {
return;
}
stringRedisTemplate.getConnectionFactory().getConnection().flushDb();
}
@Test
void clerkLiveShouldRejectWhenClerkIdMissing() throws Exception {
mockMvc.perform(get("/wx/pk/clerk/live")
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(ResultCodeEnum.FAILED.getCode()))
.andExpect(jsonPath("$.message").value(MSG_PARAM_INVALID));
}
@Test
void clerkLiveShouldReturnInactiveWhenNoActivePk() throws Exception {
String clerkId = newClerkId();
mockMvc.perform(get("/wx/pk/clerk/live")
.param(PARAM_CLERK_ID, clerkId)
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.state").value(PkWxState.INACTIVE.getValue()));
}
@Test
void clerkLiveShouldReturnActiveWhenInProgress() throws Exception {
LocalDateTime now = LocalDateTime.now();
String pkId = newPkId();
String clerkAId = newClerkId();
String clerkBId = newClerkId();
SecurityUtils.setTenantId(DEFAULT_TENANT);
PlayClerkPkEntity pk = buildPk(pkId, clerkAId, clerkBId,
now.minusMinutes(MINUTES_BEFORE_START),
now.plusMinutes(MINUTES_AFTER_START),
ClerkPkEnum.IN_PROGRESS.name());
clerkPkService.save(pk);
MvcResult result = mockMvc.perform(get("/wx/pk/clerk/live")
.param(PARAM_CLERK_ID, clerkAId)
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.state").value(PkWxState.ACTIVE.getValue()))
.andExpect(jsonPath("$.data.id").value(pkId))
.andReturn();
JsonNode data = extractData(result);
assertThat(data.get("remainingSeconds").asLong()).isGreaterThanOrEqualTo(REMAINING_SECONDS_MIN);
assertThat(data.get("serverEpochSeconds").asLong()).isGreaterThanOrEqualTo(REMAINING_SECONDS_MIN);
assertThat(data.get("pkEndEpochSeconds").asLong()).isGreaterThanOrEqualTo(REMAINING_SECONDS_MIN);
}
@Test
void clerkLiveShouldReturnInactiveWhenToBeStarted() throws Exception {
LocalDateTime now = LocalDateTime.now();
String clerkId = newClerkId();
PlayClerkPkEntity pk = buildPk(newPkId(), clerkId, newClerkId(),
now.plusMinutes(MINUTES_FUTURE_START),
now.plusMinutes(MINUTES_AFTER_START),
ClerkPkEnum.TO_BE_STARTED.name());
SecurityUtils.setTenantId(DEFAULT_TENANT);
clerkPkService.save(pk);
mockMvc.perform(get("/wx/pk/clerk/live")
.param(PARAM_CLERK_ID, clerkId)
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.state").value(PkWxState.INACTIVE.getValue()));
}
@Test
void upcomingShouldReturnInactiveWhenRedisEmpty() throws Exception {
mockMvc.perform(get("/wx/pk/upcoming")
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.state").value(PkWxState.INACTIVE.getValue()));
}
@Test
void upcomingShouldReturnUpcomingWhenRedisHasPk() throws Exception {
LocalDateTime now = LocalDateTime.now();
String pkId = newPkId();
PlayClerkPkEntity pk = buildPk(pkId, newClerkId(), newClerkId(),
now.plusMinutes(MINUTES_FUTURE_START),
now.plusMinutes(MINUTES_AFTER_START),
ClerkPkEnum.TO_BE_STARTED.name());
SecurityUtils.setTenantId(DEFAULT_TENANT);
clerkPkService.save(pk);
String key = PkRedisKeyConstants.upcomingKey(DEFAULT_TENANT);
long score = pk.getPkBeginTime().toInstant().getEpochSecond();
stringRedisTemplate.opsForZSet().add(key, pkId, score);
mockMvc.perform(get("/wx/pk/upcoming")
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.state").value(PkWxState.UPCOMING.getValue()))
.andExpect(jsonPath("$.data.id").value(pkId));
}
@Test
void upcomingShouldReturnInactiveWhenPkMissing() throws Exception {
String key = PkRedisKeyConstants.upcomingKey(DEFAULT_TENANT);
String pkId = newPkId();
stringRedisTemplate.opsForZSet().add(key, pkId, ZSET_SCORE_PAST);
mockMvc.perform(get("/wx/pk/upcoming")
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.state").value(PkWxState.INACTIVE.getValue()));
}
@Test
void detailShouldReturnInactiveWhenNotFound() throws Exception {
mockMvc.perform(get("/wx/pk/detail")
.param(PARAM_ID, newPkId())
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.state").value(PkWxState.INACTIVE.getValue()));
}
@Test
void detailShouldReturnActiveWhenInProgress() throws Exception {
LocalDateTime now = LocalDateTime.now();
String pkId = newPkId();
PlayClerkPkEntity pk = buildPk(pkId, newClerkId(), newClerkId(),
now.minusMinutes(MINUTES_BEFORE_START),
now.plusMinutes(MINUTES_AFTER_START),
ClerkPkEnum.IN_PROGRESS.name());
SecurityUtils.setTenantId(DEFAULT_TENANT);
clerkPkService.save(pk);
MvcResult result = mockMvc.perform(get("/wx/pk/detail")
.param(PARAM_ID, pkId)
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.state").value(PkWxState.ACTIVE.getValue()))
.andExpect(jsonPath("$.data.id").value(pkId))
.andReturn();
JsonNode data = extractData(result);
assertThat(data.get("remainingSeconds").asLong()).isGreaterThanOrEqualTo(REMAINING_SECONDS_MIN);
}
@Test
void detailShouldReturnUpcomingWhenToBeStarted() throws Exception {
LocalDateTime now = LocalDateTime.now();
String pkId = newPkId();
PlayClerkPkEntity pk = buildPk(pkId, newClerkId(), newClerkId(),
now.plusMinutes(MINUTES_FUTURE_START),
now.plusMinutes(MINUTES_AFTER_START),
ClerkPkEnum.TO_BE_STARTED.name());
SecurityUtils.setTenantId(DEFAULT_TENANT);
clerkPkService.save(pk);
mockMvc.perform(get("/wx/pk/detail")
.param(PARAM_ID, pkId)
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.state").value(PkWxState.UPCOMING.getValue()))
.andExpect(jsonPath("$.data.id").value(pkId));
}
@Test
void detailShouldReturnContributorsAndHistory() throws Exception {
LocalDateTime now = LocalDateTime.now();
String pkId = newPkId();
String clerkAId = newClerkId();
String clerkBId = newClerkId();
SecurityUtils.setTenantId(DEFAULT_TENANT);
PlayClerkPkEntity pk = buildPk(pkId, clerkAId, clerkBId,
now.minusMinutes(MINUTES_AFTER_START),
now.plusMinutes(MINUTES_AFTER_START),
ClerkPkEnum.IN_PROGRESS.name());
clerkPkService.save(pk);
String clerkAName = CLERK_NAME_PREFIX + clerkAId;
String clerkBName = CLERK_NAME_PREFIX + clerkBId;
clerkUserInfoService.save(buildClerk(clerkAId, clerkAName));
clerkUserInfoService.save(buildClerk(clerkBId, clerkBName));
String customAId = newCustomId();
String customBId = newCustomId();
customUserInfoService.save(buildCustomUser(customAId, CUSTOM_NAME_PREFIX + "A"));
customUserInfoService.save(buildCustomUser(customBId, CUSTOM_NAME_PREFIX + "B"));
orderInfoService.save(buildOrder(customAId, clerkAId, AMOUNT_MEDIUM, now.minusMinutes(MINUTES_ORDER_OFFSET)));
orderInfoService.save(buildOrder(customBId, clerkAId, AMOUNT_LOW, now.minusMinutes(MINUTES_ORDER_OFFSET)));
orderInfoService.save(buildOrder(customBId, clerkBId, AMOUNT_HIGH, now.minusMinutes(MINUTES_ORDER_OFFSET)));
clerkPkService.save(buildFinishedPk(newPkId(), clerkAId, clerkBId,
now.minusDays(HISTORY_FIRST_DAY_OFFSET), AMOUNT_LOW, AMOUNT_HIGH));
clerkPkService.save(buildFinishedPk(newPkId(), clerkBId, clerkAId,
now.minusDays(HISTORY_SECOND_DAY_OFFSET), AMOUNT_HIGH, AMOUNT_LOW));
mockMvc.perform(get("/wx/pk/detail")
.param(PARAM_ID, pkId)
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.contributors[0].userId").value(customBId))
.andExpect(jsonPath("$.data.contributors[0].amount").value(AMOUNT_HIGH.add(AMOUNT_LOW).doubleValue()))
.andExpect(jsonPath("$.data.history.length()").value(EXPECTED_HISTORY_COUNT))
.andExpect(jsonPath("$.data.history[0].clerkAName").value(clerkBName));
}
@Test
void clerkHistoryShouldReturnSummaryAndTopContributor() throws Exception {
LocalDateTime now = LocalDateTime.now();
String clerkAId = newClerkId();
String clerkBId = newClerkId();
SecurityUtils.setTenantId(DEFAULT_TENANT);
PlayClerkPkEntity newerPk = buildFinishedPk(newPkId(), clerkAId, clerkBId,
now.minusDays(HISTORY_SECOND_DAY_OFFSET), AMOUNT_HIGH, AMOUNT_LOW);
newerPk.setWinnerClerkId(clerkAId);
clerkPkService.save(newerPk);
PlayClerkPkEntity olderPk = buildFinishedPk(newPkId(), clerkAId, clerkBId,
now.minusDays(HISTORY_FIRST_DAY_OFFSET), AMOUNT_LOW, AMOUNT_HIGH);
olderPk.setWinnerClerkId(clerkBId);
clerkPkService.save(olderPk);
String customHighId = newCustomId();
String customLowId = newCustomId();
customUserInfoService.save(buildCustomUser(customHighId, CUSTOM_NAME_PREFIX + ""));
customUserInfoService.save(buildCustomUser(customLowId, CUSTOM_NAME_PREFIX + ""));
orderInfoService.save(buildOrder(customLowId, clerkAId, AMOUNT_LOW, now.minusDays(HISTORY_SECOND_DAY_OFFSET)
.plusMinutes(MINUTES_ORDER_OFFSET)));
orderInfoService.save(buildOrder(customHighId, clerkBId, AMOUNT_HIGH, now.minusDays(HISTORY_SECOND_DAY_OFFSET)
.plusMinutes(MINUTES_ORDER_OFFSET)));
mockMvc.perform(get("/wx/pk/clerk/history")
.param(PARAM_CLERK_ID, clerkAId)
.param(PARAM_PAGE_NUM, String.valueOf(HISTORY_PAGE_NUM))
.param(PARAM_PAGE_SIZE, String.valueOf(HISTORY_PAGE_SIZE))
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.items.length()").value(EXPECTED_HISTORY_COUNT))
.andExpect(jsonPath("$.data.summary.totalCount").value(EXPECTED_HISTORY_COUNT))
.andExpect(jsonPath("$.data.summary.winCount").value(1))
.andExpect(jsonPath("$.data.items[0].topContributorName").value(CUSTOM_NAME_PREFIX + ""))
.andExpect(jsonPath("$.data.items[0].winnerClerkId").value(clerkAId))
.andExpect(jsonPath("$.data.items[0].id").isNotEmpty())
.andExpect(jsonPath("$.data.items[0].clerkAId").value(clerkAId))
.andExpect(jsonPath("$.data.items[0].clerkBId").value(clerkBId))
.andExpect(jsonPath("$.data.items[0].topContributorAmount").value(AMOUNT_HIGH.doubleValue()));
}
@Test
void clerkScheduleShouldRejectWhenClerkIdMissing() throws Exception {
mockMvc.perform(get("/wx/pk/clerk/schedule")
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(ResultCodeEnum.FAILED.getCode()))
.andExpect(jsonPath("$.message").value(MSG_PARAM_INVALID));
}
@Test
void clerkScheduleShouldReturnEmptyWhenNoFuturePk() throws Exception {
String clerkId = newClerkId();
mockMvc.perform(get("/wx/pk/clerk/schedule")
.param(PARAM_CLERK_ID, clerkId)
.param(PARAM_LIMIT, String.valueOf(SCHEDULE_LIMIT))
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.length()").value(0));
}
@Test
void clerkScheduleShouldReturnUpcomingOrdered() throws Exception {
LocalDateTime now = LocalDateTime.now();
String clerkId = newClerkId();
SecurityUtils.setTenantId(DEFAULT_TENANT);
PlayClerkPkEntity first = buildPk(newPkId(), clerkId, newClerkId(),
now.plusMinutes(FUTURE_FIRST_OFFSET_MINUTES),
now.plusMinutes(FUTURE_FIRST_OFFSET_MINUTES + MINUTES_AFTER_START),
ClerkPkEnum.TO_BE_STARTED.name());
PlayClerkPkEntity second = buildPk(newPkId(), clerkId, newClerkId(),
now.plusMinutes(FUTURE_SECOND_OFFSET_MINUTES),
now.plusMinutes(FUTURE_SECOND_OFFSET_MINUTES + MINUTES_AFTER_START),
ClerkPkEnum.TO_BE_STARTED.name());
clerkPkService.save(second);
clerkPkService.save(first);
mockMvc.perform(get("/wx/pk/clerk/schedule")
.param(PARAM_CLERK_ID, clerkId)
.param(PARAM_LIMIT, String.valueOf(SCHEDULE_LIMIT))
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.length()").value(SCHEDULE_LIMIT))
.andExpect(jsonPath("$.data[0].id").value(first.getId()))
.andExpect(jsonPath("$.data[1].id").value(second.getId()))
.andExpect(jsonPath("$.data[0].state").value(PkWxState.UPCOMING.getValue()));
}
@Test
void clerkScheduleShouldApplyLimitDefaultWhenMissing() throws Exception {
LocalDateTime now = LocalDateTime.now();
String clerkId = newClerkId();
SecurityUtils.setTenantId(DEFAULT_TENANT);
clerkPkService.save(buildPk(newPkId(), clerkId, newClerkId(),
now.plusMinutes(FUTURE_FIRST_OFFSET_MINUTES),
now.plusMinutes(FUTURE_FIRST_OFFSET_MINUTES + MINUTES_AFTER_START),
ClerkPkEnum.TO_BE_STARTED.name()));
clerkPkService.save(buildPk(newPkId(), clerkId, newClerkId(),
now.plusMinutes(FUTURE_SECOND_OFFSET_MINUTES),
now.plusMinutes(FUTURE_SECOND_OFFSET_MINUTES + MINUTES_AFTER_START),
ClerkPkEnum.TO_BE_STARTED.name()));
clerkPkService.save(buildPk(newPkId(), clerkId, newClerkId(),
now.plusMinutes(FUTURE_THIRD_OFFSET_MINUTES),
now.plusMinutes(FUTURE_THIRD_OFFSET_MINUTES + MINUTES_AFTER_START),
ClerkPkEnum.TO_BE_STARTED.name()));
mockMvc.perform(get("/wx/pk/clerk/schedule")
.param(PARAM_CLERK_ID, clerkId)
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.length()").value(DEFAULT_LIMIT));
}
@Test
void clerkScheduleShouldClampLimitWhenZeroOrNegative() throws Exception {
LocalDateTime now = LocalDateTime.now();
String clerkId = newClerkId();
SecurityUtils.setTenantId(DEFAULT_TENANT);
clerkPkService.save(buildPk(newPkId(), clerkId, newClerkId(),
now.plusMinutes(FUTURE_FIRST_OFFSET_MINUTES),
now.plusMinutes(FUTURE_FIRST_OFFSET_MINUTES + MINUTES_AFTER_START),
ClerkPkEnum.TO_BE_STARTED.name()));
mockMvc.perform(get("/wx/pk/clerk/schedule")
.param(PARAM_CLERK_ID, clerkId)
.param(PARAM_LIMIT, String.valueOf(LIMIT_ZERO))
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.length()").value(1));
mockMvc.perform(get("/wx/pk/clerk/schedule")
.param(PARAM_CLERK_ID, clerkId)
.param(PARAM_LIMIT, String.valueOf(LIMIT_NEGATIVE))
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.length()").value(1));
}
@Test
void clerkScheduleShouldClampLimitWhenTooLarge() throws Exception {
LocalDateTime now = LocalDateTime.now();
String clerkId = newClerkId();
SecurityUtils.setTenantId(DEFAULT_TENANT);
clerkPkService.save(buildPk(newPkId(), clerkId, newClerkId(),
now.plusMinutes(FUTURE_FIRST_OFFSET_MINUTES),
now.plusMinutes(FUTURE_FIRST_OFFSET_MINUTES + MINUTES_AFTER_START),
ClerkPkEnum.TO_BE_STARTED.name()));
mockMvc.perform(get("/wx/pk/clerk/schedule")
.param(PARAM_CLERK_ID, clerkId)
.param(PARAM_LIMIT, String.valueOf(LIMIT_LARGE))
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.length()").value(1));
}
@Test
void clerkScheduleShouldExcludePastOrNonToBeStarted() throws Exception {
LocalDateTime now = LocalDateTime.now();
String clerkId = newClerkId();
SecurityUtils.setTenantId(DEFAULT_TENANT);
clerkPkService.save(buildPk(newPkId(), clerkId, newClerkId(),
now.minusMinutes(PAST_OFFSET_MINUTES),
now.minusMinutes(PAST_OFFSET_MINUTES - MINUTES_AFTER_START),
ClerkPkEnum.TO_BE_STARTED.name()));
clerkPkService.save(buildPk(newPkId(), clerkId, newClerkId(),
now.plusMinutes(FUTURE_FIRST_OFFSET_MINUTES),
now.plusMinutes(FUTURE_FIRST_OFFSET_MINUTES + MINUTES_AFTER_START),
ClerkPkEnum.IN_PROGRESS.name()));
PlayClerkPkEntity future = buildPk(newPkId(), clerkId, newClerkId(),
now.plusMinutes(FUTURE_SECOND_OFFSET_MINUTES),
now.plusMinutes(FUTURE_SECOND_OFFSET_MINUTES + MINUTES_AFTER_START),
ClerkPkEnum.TO_BE_STARTED.name());
clerkPkService.save(future);
mockMvc.perform(get("/wx/pk/clerk/schedule")
.param(PARAM_CLERK_ID, clerkId)
.param(PARAM_LIMIT, String.valueOf(SCHEDULE_LIMIT))
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.length()").value(1))
.andExpect(jsonPath("$.data[0].id").value(future.getId()));
}
@Test
void clerkScheduleShouldIsolateTenants() throws Exception {
LocalDateTime now = LocalDateTime.now();
String clerkId = newClerkId();
SecurityUtils.setTenantId(DEFAULT_TENANT);
clerkPkService.save(buildPk(newPkId(), clerkId, newClerkId(),
now.plusMinutes(FUTURE_FIRST_OFFSET_MINUTES),
now.plusMinutes(FUTURE_FIRST_OFFSET_MINUTES + MINUTES_AFTER_START),
ClerkPkEnum.TO_BE_STARTED.name()));
String otherTenant = "tenant-other";
PlayClerkPkEntity other = buildPk(newPkId(), clerkId, newClerkId(),
now.plusMinutes(FUTURE_SECOND_OFFSET_MINUTES),
now.plusMinutes(FUTURE_SECOND_OFFSET_MINUTES + MINUTES_AFTER_START),
ClerkPkEnum.TO_BE_STARTED.name());
other.setTenantId(otherTenant);
clerkPkService.save(other);
mockMvc.perform(get("/wx/pk/clerk/schedule")
.param(PARAM_CLERK_ID, clerkId)
.param(PARAM_LIMIT, String.valueOf(SCHEDULE_LIMIT))
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.length()").value(1));
}
@Test
void clerkScheduleShouldIgnoreNonParticipant() throws Exception {
LocalDateTime now = LocalDateTime.now();
String clerkId = newClerkId();
SecurityUtils.setTenantId(DEFAULT_TENANT);
clerkPkService.save(buildPk(newPkId(), newClerkId(), newClerkId(),
now.plusMinutes(FUTURE_FIRST_OFFSET_MINUTES),
now.plusMinutes(FUTURE_FIRST_OFFSET_MINUTES + MINUTES_AFTER_START),
ClerkPkEnum.TO_BE_STARTED.name()));
mockMvc.perform(get("/wx/pk/clerk/schedule")
.param(PARAM_CLERK_ID, clerkId)
.param(PARAM_LIMIT, String.valueOf(SCHEDULE_LIMIT))
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.length()").value(0));
}
private static PlayClerkPkEntity buildPk(String pkId, String clerkAId, String clerkBId,
LocalDateTime begin, LocalDateTime end, String status) {
PlayClerkPkEntity pk = new PlayClerkPkEntity();
pk.setId(pkId);
pk.setTenantId(DEFAULT_TENANT);
pk.setClerkA(clerkAId);
pk.setClerkB(clerkBId);
pk.setPkBeginTime(Date.from(begin.atZone(ZoneId.systemDefault()).toInstant()));
pk.setPkEndTime(Date.from(end.atZone(ZoneId.systemDefault()).toInstant()));
pk.setStatus(status);
pk.setSettled(SETTLED_FALSE);
return pk;
}
private static PlayClerkPkEntity buildFinishedPk(String pkId, String clerkAId, String clerkBId,
LocalDateTime begin, BigDecimal scoreA, BigDecimal scoreB) {
PlayClerkPkEntity pk = new PlayClerkPkEntity();
pk.setId(pkId);
pk.setTenantId(DEFAULT_TENANT);
pk.setClerkA(clerkAId);
pk.setClerkB(clerkBId);
pk.setPkBeginTime(Date.from(begin.atZone(ZoneId.systemDefault()).toInstant()));
pk.setPkEndTime(Date.from(begin.plusMinutes(MINUTES_AFTER_START).atZone(ZoneId.systemDefault()).toInstant()));
pk.setStatus(ClerkPkEnum.FINISHED.name());
pk.setClerkAScore(scoreA);
pk.setClerkBScore(scoreB);
pk.setSettled(SETTLED_TRUE);
return pk;
}
private static PlayClerkUserInfoEntity buildClerk(String id, String nickname) {
PlayClerkUserInfoEntity clerk = new PlayClerkUserInfoEntity();
clerk.setId(id);
clerk.setTenantId(DEFAULT_TENANT);
clerk.setOpenid(OPENID_PREFIX + id);
clerk.setNickname(nickname);
clerk.setDeleted(false);
return clerk;
}
private static PlayCustomUserInfoEntity buildCustomUser(String id, String nickname) {
PlayCustomUserInfoEntity custom = new PlayCustomUserInfoEntity();
custom.setId(id);
custom.setTenantId(DEFAULT_TENANT);
custom.setNickname(nickname);
custom.setDeleted(false);
return custom;
}
private static PlayOrderInfoEntity buildOrder(String customerId, String clerkId, BigDecimal amount,
LocalDateTime endTime) {
PlayOrderInfoEntity order = new PlayOrderInfoEntity();
order.setId(IdUtils.getUuid());
order.setTenantId(DEFAULT_TENANT);
order.setOrderStatus(OrderConstant.OrderStatus.COMPLETED.getCode());
order.setOrderType(OrderConstant.OrderType.NORMAL.getCode());
order.setPlaceType(OrderConstant.PlaceType.SPECIFIED.getCode());
order.setPurchaserBy(customerId);
order.setAcceptBy(clerkId);
order.setFinalAmount(amount);
order.setOrderEndTime(endTime);
order.setDeleted(false);
return order;
}
private static String newClerkId() {
return CLERK_PREFIX + IdUtils.getUuid();
}
private static String newCustomId() {
return CUSTOM_PREFIX + IdUtils.getUuid();
}
private static String newPkId() {
return PK_PREFIX + IdUtils.getUuid();
}
private static JsonNode extractData(MvcResult result) throws Exception {
JsonNode root = OBJECT_MAPPER.readTree(result.getResponse().getContentAsString());
return root.get("data");
}
}