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

2
justfile Normal file
View File

@@ -0,0 +1,2 @@
iperf:
iperf3 -c 101.43.124.74

View File

@@ -24,6 +24,7 @@ import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice @RestControllerAdvice
public class GlobalExceptionHandler { public class GlobalExceptionHandler {
private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class); private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
public static final String PARAMETER_FORMAT_ERROR = "请求参数格式异常";
/** /**
* 业务异常 * 业务异常
@@ -87,20 +88,20 @@ public class GlobalExceptionHandler {
@ExceptionHandler(MismatchedInputException.class) @ExceptionHandler(MismatchedInputException.class)
public R mismatchedInputException(MismatchedInputException e) { public R mismatchedInputException(MismatchedInputException e) {
log.error("请求参数格式异常", e); log.error(PARAMETER_FORMAT_ERROR, e);
return R.error("请求参数格式异常"); return R.error(PARAMETER_FORMAT_ERROR);
} }
@ExceptionHandler(HttpMessageNotReadableException.class) @ExceptionHandler(HttpMessageNotReadableException.class)
public R httpMessageNotReadableException(HttpMessageNotReadableException e) { public R httpMessageNotReadableException(HttpMessageNotReadableException e) {
log.error("请求参数格式异常", e); log.error(PARAMETER_FORMAT_ERROR, e);
return R.error("请求参数格式异常"); return R.error(PARAMETER_FORMAT_ERROR);
} }
@ExceptionHandler(MissingServletRequestParameterException.class) @ExceptionHandler(MissingServletRequestParameterException.class)
public R missingServletRequestParameterException(MissingServletRequestParameterException e) { public R missingServletRequestParameterException(MissingServletRequestParameterException e) {
log.error("请求参数格式异常", e); log.error(PARAMETER_FORMAT_ERROR, e);
return R.error("请求参数格式异常"); return R.error(PARAMETER_FORMAT_ERROR);
} }
/** /**

View File

@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.core.metadata.IPage;
import com.starry.admin.modules.clerk.module.entity.PlayClerkPkEntity; import com.starry.admin.modules.clerk.module.entity.PlayClerkPkEntity;
import com.starry.admin.modules.clerk.service.IPlayClerkPkService; import com.starry.admin.modules.clerk.service.IPlayClerkPkService;
import com.starry.admin.modules.pk.dto.PkScoreBoardDto; import com.starry.admin.modules.pk.dto.PkScoreBoardDto;
import com.starry.admin.modules.pk.dto.PlayClerkPkForceStartRequest;
import com.starry.admin.modules.pk.service.ClerkPkLifecycleService; import com.starry.admin.modules.pk.service.ClerkPkLifecycleService;
import com.starry.admin.modules.pk.service.IPkScoreboardService; import com.starry.admin.modules.pk.service.IPkScoreboardService;
import com.starry.common.annotation.Log; import com.starry.common.annotation.Log;
@@ -100,6 +101,18 @@ public class PlayClerkPkController {
return R.ok(); return R.ok();
} }
/**
* 强制开始PK无需排期便于人工触发
*/
@ApiOperation(value = "强制开始PK", notes = "人工触发PK开始并直接进入进行中状态")
@ApiResponses({@ApiResponse(code = 200, message = "操作成功")})
@Log(title = "店员pk", businessType = BusinessType.INSERT)
@PostMapping(value = "/force-start")
public R forceStart(@ApiParam(value = "强制开始请求", required = true)
@RequestBody PlayClerkPkForceStartRequest request) {
return R.ok(clerkPkLifecycleService.forceStart(request));
}
/** /**
* 新增店员pk * 新增店员pk
*/ */

View File

@@ -1,7 +1,12 @@
package com.starry.admin.modules.clerk.mapper; package com.starry.admin.modules.clerk.mapper;
import com.baomidou.mybatisplus.annotation.InterceptorIgnore;
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.starry.admin.modules.clerk.module.entity.PlayClerkPkEntity; import com.starry.admin.modules.clerk.module.entity.PlayClerkPkEntity;
import java.util.Date;
import java.util.List;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
/** /**
* 店员pkMapper接口 * 店员pkMapper接口
@@ -11,4 +16,47 @@ import com.starry.admin.modules.clerk.module.entity.PlayClerkPkEntity;
*/ */
public interface PlayClerkPkMapper extends BaseMapper<PlayClerkPkEntity> { public interface PlayClerkPkMapper extends BaseMapper<PlayClerkPkEntity> {
@InterceptorIgnore(tenantLine = "1")
@Select("SELECT * FROM play_clerk_pk "
+ "WHERE status = #{status} "
+ "AND pk_begin_time >= #{beginTime} "
+ "AND pk_begin_time <= #{endTime}")
List<PlayClerkPkEntity> selectUpcomingByStatus(
@Param("status") String status,
@Param("beginTime") Date beginTime,
@Param("endTime") Date endTime);
@Select("<script>"
+ "SELECT * FROM play_clerk_pk "
+ "WHERE tenant_id = #{tenantId} "
+ " AND status = #{status} "
+ " AND ("
+ " (clerk_a = #{clerkAId} AND clerk_b = #{clerkBId}) "
+ " OR (clerk_a = #{clerkBId} AND clerk_b = #{clerkAId})"
+ " ) "
+ "ORDER BY pk_begin_time DESC "
+ "LIMIT #{limit}"
+ "</script>")
List<PlayClerkPkEntity> selectRecentFinishedBetweenClerks(
@Param("tenantId") String tenantId,
@Param("clerkAId") String clerkAId,
@Param("clerkBId") String clerkBId,
@Param("status") String status,
@Param("limit") int limit);
@Select("<script>"
+ "SELECT * FROM play_clerk_pk "
+ "WHERE tenant_id = #{tenantId} "
+ " AND status = #{status} "
+ " AND pk_begin_time &gt;= #{beginTime} "
+ " AND (clerk_a = #{clerkId} OR clerk_b = #{clerkId}) "
+ "ORDER BY pk_begin_time ASC "
+ "LIMIT #{limit}"
+ "</script>")
List<PlayClerkPkEntity> selectUpcomingForClerk(
@Param("tenantId") String tenantId,
@Param("clerkId") String clerkId,
@Param("status") String status,
@Param("beginTime") Date beginTime,
@Param("limit") int limit);
} }

View File

@@ -87,6 +87,11 @@ public class PlayClerkPkEntity extends BaseEntity<PlayClerkPkEntity> {
*/ */
private String status; private String status;
/**
* 排期设置ID
*/
private String settingId;
/** /**
* 店员A得分 * 店员A得分
*/ */

View File

@@ -4,6 +4,8 @@ import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.IService; import com.baomidou.mybatisplus.extension.service.IService;
import com.starry.admin.modules.clerk.module.entity.PlayClerkPkEntity; import com.starry.admin.modules.clerk.module.entity.PlayClerkPkEntity;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.Date;
import java.util.List;
import java.util.Optional; import java.util.Optional;
/** /**
@@ -75,4 +77,6 @@ public interface IPlayClerkPkService extends IService<PlayClerkPkEntity> {
* @return 存在则返回 PK 记录,否则返回空 * @return 存在则返回 PK 记录,否则返回空
*/ */
Optional<PlayClerkPkEntity> findActivePkForClerk(String clerkId, LocalDateTime occurredAt); Optional<PlayClerkPkEntity> findActivePkForClerk(String clerkId, LocalDateTime occurredAt);
List<PlayClerkPkEntity> selectUpcomingForClerk(String tenantId, String clerkId, Date beginTime, int limit);
} }

View File

@@ -7,19 +7,25 @@ import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.starry.admin.common.PageBuilder;
import com.starry.admin.common.exception.CustomException;
import com.starry.admin.modules.clerk.mapper.PlayClerkPkMapper; import com.starry.admin.modules.clerk.mapper.PlayClerkPkMapper;
import com.starry.admin.modules.clerk.module.entity.ClerkPkEnum; 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.PlayClerkPkEntity;
import com.starry.admin.modules.clerk.module.entity.PlayClerkUserInfoEntity; import com.starry.admin.modules.clerk.module.entity.PlayClerkUserInfoEntity;
import com.starry.admin.modules.clerk.service.IPlayClerkPkService; import com.starry.admin.modules.clerk.service.IPlayClerkPkService;
import com.starry.admin.modules.clerk.service.IPlayClerkUserInfoService; import com.starry.admin.modules.clerk.service.IPlayClerkUserInfoService;
import com.starry.admin.modules.pk.enums.PkLifecycleErrorCode;
import com.starry.admin.modules.pk.redis.PkRedisKeyConstants;
import com.starry.common.utils.IdUtils; import com.starry.common.utils.IdUtils;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.ZoneId; import java.time.ZoneId;
import java.util.Arrays; import java.util.Arrays;
import java.util.Date; import java.util.Date;
import java.util.List;
import java.util.Optional; import java.util.Optional;
import javax.annotation.Resource; import javax.annotation.Resource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
/** /**
@@ -36,6 +42,8 @@ public class PlayClerkPkServiceImpl extends ServiceImpl<PlayClerkPkMapper, PlayC
private PlayClerkPkMapper playClerkPkMapper; private PlayClerkPkMapper playClerkPkMapper;
@Resource @Resource
private IPlayClerkUserInfoService clerkUserInfoService; private IPlayClerkUserInfoService clerkUserInfoService;
@Resource
private StringRedisTemplate stringRedisTemplate;
/** /**
* 查询店员pk * 查询店员pk
@@ -58,8 +66,15 @@ public class PlayClerkPkServiceImpl extends ServiceImpl<PlayClerkPkMapper, PlayC
*/ */
@Override @Override
public IPage<PlayClerkPkEntity> selectPlayClerkPkByPage(PlayClerkPkEntity playClerkPk) { public IPage<PlayClerkPkEntity> selectPlayClerkPkByPage(PlayClerkPkEntity playClerkPk) {
Page<PlayClerkPkEntity> page = new Page<>(1, 10); Page<PlayClerkPkEntity> page = PageBuilder.build();
return this.baseMapper.selectPage(page, new LambdaQueryWrapper<>()); LambdaQueryWrapper<PlayClerkPkEntity> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(StrUtil.isNotBlank(playClerkPk.getStatus()), PlayClerkPkEntity::getStatus, playClerkPk.getStatus());
wrapper.eq(StrUtil.isNotBlank(playClerkPk.getClerkA()), PlayClerkPkEntity::getClerkA, playClerkPk.getClerkA());
wrapper.eq(StrUtil.isNotBlank(playClerkPk.getClerkB()), PlayClerkPkEntity::getClerkB, playClerkPk.getClerkB());
wrapper.eq(StrUtil.isNotBlank(playClerkPk.getSettingId()), PlayClerkPkEntity::getSettingId,
playClerkPk.getSettingId());
wrapper.orderByDesc(PlayClerkPkEntity::getPkBeginTime);
return this.baseMapper.selectPage(page, wrapper);
} }
/** /**
@@ -101,7 +116,11 @@ public class PlayClerkPkServiceImpl extends ServiceImpl<PlayClerkPkMapper, PlayC
} }
playClerkPk.setStatus(ClerkPkEnum.TO_BE_STARTED.name()); playClerkPk.setStatus(ClerkPkEnum.TO_BE_STARTED.name());
return save(playClerkPk); boolean saved = save(playClerkPk);
if (saved) {
scheduleStart(playClerkPk);
}
return saved;
} }
/** /**
@@ -140,6 +159,18 @@ public class PlayClerkPkServiceImpl extends ServiceImpl<PlayClerkPkMapper, PlayC
return playClerkPkMapper.deleteById(id); return playClerkPkMapper.deleteById(id);
} }
private void scheduleStart(PlayClerkPkEntity pk) {
if (pk == null || pk.getPkBeginTime() == null || pk.getId() == null) {
throw new CustomException(PkLifecycleErrorCode.REQUEST_INVALID.getMessage());
}
if (StrUtil.isBlank(pk.getTenantId())) {
throw new CustomException(PkLifecycleErrorCode.TENANT_MISSING.getMessage());
}
String scheduleKey = PkRedisKeyConstants.startScheduleKey(pk.getTenantId());
long startEpochSeconds = pk.getPkBeginTime().toInstant().getEpochSecond();
stringRedisTemplate.opsForZSet().add(scheduleKey, pk.getId(), startEpochSeconds);
}
@Override @Override
public Optional<PlayClerkPkEntity> findActivePkForClerk(String clerkId, LocalDateTime occurredAt) { public Optional<PlayClerkPkEntity> findActivePkForClerk(String clerkId, LocalDateTime occurredAt) {
if (StrUtil.isBlank(clerkId) || occurredAt == null) { if (StrUtil.isBlank(clerkId) || occurredAt == null) {
@@ -158,4 +189,18 @@ public class PlayClerkPkServiceImpl extends ServiceImpl<PlayClerkPkMapper, PlayC
PlayClerkPkEntity entity = this.getOne(wrapper, false); PlayClerkPkEntity entity = this.getOne(wrapper, false);
return Optional.ofNullable(entity); return Optional.ofNullable(entity);
} }
@Override
public List<PlayClerkPkEntity> selectUpcomingForClerk(String tenantId, String clerkId, Date beginTime,
int limit) {
if (StrUtil.isBlank(tenantId) || StrUtil.isBlank(clerkId) || beginTime == null || limit <= 0) {
throw new CustomException(PkLifecycleErrorCode.REQUEST_INVALID.getMessage());
}
return playClerkPkMapper.selectUpcomingForClerk(
tenantId,
clerkId,
ClerkPkEnum.TO_BE_STARTED.name(),
beginTime,
limit);
}
} }

View File

@@ -2,6 +2,12 @@ package com.starry.admin.modules.order.mapper;
import com.github.yulichang.base.MPJBaseMapper; import com.github.yulichang.base.MPJBaseMapper;
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity; import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
import com.starry.admin.modules.pk.dto.WxPkContributorDto;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
/** /**
* 订单Mapper接口 * 订单Mapper接口
@@ -11,4 +17,31 @@ import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
*/ */
public interface PlayOrderInfoMapper extends MPJBaseMapper<PlayOrderInfoEntity> { public interface PlayOrderInfoMapper extends MPJBaseMapper<PlayOrderInfoEntity> {
@Select("<script>"
+ "SELECT o.purchaser_by AS userId, "
+ " u.nickname AS nickname, "
+ " COALESCE(SUM(o.final_amount), 0) AS amount "
+ "FROM play_order_info o "
+ "LEFT JOIN play_custom_user_info u "
+ " ON u.id = o.purchaser_by "
+ " AND u.tenant_id = o.tenant_id "
+ "WHERE o.tenant_id = #{tenantId} "
+ " AND o.order_status = #{orderStatus} "
+ " AND o.final_amount &gt; #{minAmount} "
+ " AND o.accept_by IN (#{clerkAId}, #{clerkBId}) "
+ " AND o.order_end_time &gt;= #{startTime} "
+ " AND o.order_end_time &lt;= #{endTime} "
+ "GROUP BY o.purchaser_by, u.nickname "
+ "ORDER BY amount DESC "
+ "LIMIT #{limit}"
+ "</script>")
List<WxPkContributorDto> selectPkContributors(
@Param("tenantId") String tenantId,
@Param("clerkAId") String clerkAId,
@Param("clerkBId") String clerkBId,
@Param("startTime") LocalDateTime startTime,
@Param("endTime") LocalDateTime endTime,
@Param("orderStatus") String orderStatus,
@Param("minAmount") BigDecimal minAmount,
@Param("limit") int limit);
} }

View File

@@ -0,0 +1,24 @@
package com.starry.admin.modules.pk.constants;
import java.math.BigDecimal;
public final class PkWxQueryConstants {
public static final int CONTRIBUTOR_LIMIT = 10;
public static final int HISTORY_LIMIT = 10;
public static final int CLERK_HISTORY_PAGE_NUM = 1;
public static final int CLERK_HISTORY_PAGE_SIZE = 10;
public static final int CLERK_HISTORY_MIN_PAGE = 1;
public static final int CLERK_SCHEDULE_DEFAULT_LIMIT = 3;
public static final int CLERK_SCHEDULE_MIN_LIMIT = 1;
public static final int CLERK_SCHEDULE_MAX_LIMIT = 20;
public static final int TOP_CONTRIBUTOR_LIMIT = 1;
public static final int WIN_RATE_SCALE = 2;
public static final String PERCENT_SUFFIX = "%";
public static final BigDecimal MIN_CONTRIBUTION_AMOUNT = BigDecimal.ZERO;
public static final BigDecimal WIN_RATE_MULTIPLIER = new BigDecimal("100");
private PkWxQueryConstants() {
throw new IllegalStateException("Utility class");
}
}

View File

@@ -0,0 +1,19 @@
package com.starry.admin.modules.pk.dto;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@ApiModel(value = "PlayClerkPkForceStartRequest", description = "强制开始PK请求")
@Data
public class PlayClerkPkForceStartRequest {
@ApiModelProperty(value = "店员A ID", required = true)
private String clerkAId;
@ApiModelProperty(value = "店员B ID", required = true)
private String clerkBId;
@ApiModelProperty(value = "持续分钟数", required = true)
private Integer durationMinutes;
}

View File

@@ -0,0 +1,14 @@
package com.starry.admin.modules.pk.dto;
import java.util.ArrayList;
import java.util.List;
import lombok.Data;
@Data
public class WxPkClerkHistoryPageDto {
private List<WxPkHistoryDto> items = new ArrayList<>();
private WxPkClerkHistorySummaryDto summary = new WxPkClerkHistorySummaryDto();
private long totalCount;
private int pageNum;
private int pageSize;
}

View File

@@ -0,0 +1,12 @@
package com.starry.admin.modules.pk.dto;
import lombok.Data;
@Data
public class WxPkClerkHistorySummaryDto {
private static final String ZERO_PERCENT = "0%";
private long winCount;
private long totalCount;
private String winRate = ZERO_PERCENT;
}

View File

@@ -0,0 +1,13 @@
package com.starry.admin.modules.pk.dto;
import java.math.BigDecimal;
import lombok.Data;
@Data
public class WxPkContributorDto {
private static final String EMPTY_TEXT = "";
private String userId = EMPTY_TEXT;
private String nickname = EMPTY_TEXT;
private BigDecimal amount = BigDecimal.ZERO;
}

View File

@@ -0,0 +1,40 @@
package com.starry.admin.modules.pk.dto;
import com.starry.admin.modules.pk.enums.PkWxState;
import java.math.BigDecimal;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import lombok.Data;
@Data
public class WxPkDetailDto {
private static final String EMPTY_TEXT = "";
private static final long ZERO_SECONDS = 0L;
private static final Date EPOCH_DATE = Date.from(Instant.EPOCH);
private String id = EMPTY_TEXT;
private String state = EMPTY_TEXT;
private String clerkAId = EMPTY_TEXT;
private String clerkBId = EMPTY_TEXT;
private String clerkAName = EMPTY_TEXT;
private String clerkBName = EMPTY_TEXT;
private String clerkAAvatar = EMPTY_TEXT;
private String clerkBAvatar = EMPTY_TEXT;
private BigDecimal clerkAScore = BigDecimal.ZERO;
private BigDecimal clerkBScore = BigDecimal.ZERO;
private int clerkAOrderCount = 0;
private int clerkBOrderCount = 0;
private long remainingSeconds = ZERO_SECONDS;
private Date pkBeginTime = EPOCH_DATE;
private Date pkEndTime = EPOCH_DATE;
private List<WxPkContributorDto> contributors = new ArrayList<>();
private List<WxPkHistoryDto> history = new ArrayList<>();
public static WxPkDetailDto inactive() {
WxPkDetailDto dto = new WxPkDetailDto();
dto.setState(PkWxState.INACTIVE.getValue());
return dto;
}
}

View File

@@ -0,0 +1,24 @@
package com.starry.admin.modules.pk.dto;
import java.math.BigDecimal;
import java.time.Instant;
import java.util.Date;
import lombok.Data;
@Data
public class WxPkHistoryDto {
private static final String EMPTY_TEXT = "";
private static final Date EPOCH_DATE = Date.from(Instant.EPOCH);
private String id = EMPTY_TEXT;
private String clerkAId = EMPTY_TEXT;
private String clerkBId = EMPTY_TEXT;
private String winnerClerkId = EMPTY_TEXT;
private String clerkAName = EMPTY_TEXT;
private String clerkBName = EMPTY_TEXT;
private BigDecimal clerkAScore = BigDecimal.ZERO;
private BigDecimal clerkBScore = BigDecimal.ZERO;
private Date pkBeginTime = EPOCH_DATE;
private String topContributorName = EMPTY_TEXT;
private BigDecimal topContributorAmount = BigDecimal.ZERO;
}

View File

@@ -0,0 +1,33 @@
package com.starry.admin.modules.pk.dto;
import com.starry.admin.modules.pk.enums.PkWxState;
import java.math.BigDecimal;
import lombok.Data;
@Data
public class WxPkLiveDto {
private static final String EMPTY_TEXT = "";
private static final long ZERO_SECONDS = 0L;
private String id = EMPTY_TEXT;
private String state = EMPTY_TEXT;
private String clerkAId = EMPTY_TEXT;
private String clerkBId = EMPTY_TEXT;
private String clerkAName = EMPTY_TEXT;
private String clerkBName = EMPTY_TEXT;
private String clerkAAvatar = EMPTY_TEXT;
private String clerkBAvatar = EMPTY_TEXT;
private BigDecimal clerkAScore = BigDecimal.ZERO;
private BigDecimal clerkBScore = BigDecimal.ZERO;
private int clerkAOrderCount = 0;
private int clerkBOrderCount = 0;
private long remainingSeconds = ZERO_SECONDS;
private long pkEndEpochSeconds = ZERO_SECONDS;
private long serverEpochSeconds = ZERO_SECONDS;
public static WxPkLiveDto inactive() {
WxPkLiveDto dto = new WxPkLiveDto();
dto.setState(PkWxState.INACTIVE.getValue());
return dto;
}
}

View File

@@ -0,0 +1,28 @@
package com.starry.admin.modules.pk.dto;
import com.starry.admin.modules.pk.enums.PkWxState;
import java.time.Instant;
import java.util.Date;
import lombok.Data;
@Data
public class WxPkUpcomingDto {
private static final String EMPTY_TEXT = "";
private static final Date EPOCH_DATE = Date.from(Instant.EPOCH);
private String id = EMPTY_TEXT;
private String state = EMPTY_TEXT;
private String clerkAId = EMPTY_TEXT;
private String clerkBId = EMPTY_TEXT;
private String clerkAName = EMPTY_TEXT;
private String clerkBName = EMPTY_TEXT;
private String clerkAAvatar = EMPTY_TEXT;
private String clerkBAvatar = EMPTY_TEXT;
private Date pkBeginTime = EPOCH_DATE;
public static WxPkUpcomingDto inactive() {
WxPkUpcomingDto dto = new WxPkUpcomingDto();
dto.setState(PkWxState.INACTIVE.getValue());
return dto;
}
}

View File

@@ -0,0 +1,17 @@
package com.starry.admin.modules.pk.enums;
public enum PkLifecycleErrorCode {
REQUEST_INVALID("PK手动开始参数非法"),
CLERK_CONFLICT("店员排期冲突"),
TENANT_MISSING("租户ID缺失");
private final String message;
PkLifecycleErrorCode(String message) {
this.message = message;
}
public String getMessage() {
return message;
}
}

View File

@@ -0,0 +1,16 @@
package com.starry.admin.modules.pk.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
@AllArgsConstructor
public enum PkWxState {
ACTIVE("ACTIVE", "进行中"),
UPCOMING("UPCOMING", "即将开始"),
INACTIVE("INACTIVE", "无进行中PK");
@Getter
private final String value;
@Getter
private final String desc;
}

View File

@@ -8,11 +8,19 @@ public final class PkRedisKeyConstants {
private static final String SCORE_HASH_PREFIX = "pk:"; private static final String SCORE_HASH_PREFIX = "pk:";
private static final String SCORE_HASH_SUFFIX = ":score"; private static final String SCORE_HASH_SUFFIX = ":score";
private static final String DEDUP_KEY_PREFIX = "pk:dedup:"; private static final String DEDUP_KEY_PREFIX = "pk:dedup:";
private static final String UPCOMING_PREFIX = "pk:upcoming:";
private static final String START_SCHEDULE_PREFIX = "pk:scheduler:start:";
private static final String START_LOCK_PREFIX = "pk:scheduler:start:lock:";
private static final String FINISH_SCHEDULE_PREFIX = "pk:scheduler:finish:";
private static final String FINISH_RETRY_PREFIX = "pk:scheduler:finish:retry:";
private static final String FINISH_FAILED_PREFIX = "pk:scheduler:finish:failed:";
private static final String FINISH_LOCK_PREFIX = "pk:scheduler:finish:lock:";
/** /**
* 贡献幂等记录的存活时间(秒)。 * 贡献幂等记录的存活时间(秒)。
*/ */
public static final long CONTRIBUTION_DEDUP_TTL_SECONDS = 3600L; public static final long CONTRIBUTION_DEDUP_TTL_SECONDS = 3600L;
public static final long UPCOMING_REMINDER_TTL_SECONDS = 7200L;
public static final String FIELD_CLERK_A_SCORE = "clerk_a_score"; public static final String FIELD_CLERK_A_SCORE = "clerk_a_score";
public static final String FIELD_CLERK_B_SCORE = "clerk_b_score"; public static final String FIELD_CLERK_B_SCORE = "clerk_b_score";
@@ -29,4 +37,32 @@ public final class PkRedisKeyConstants {
public static String contributionDedupKey(String sourceCode, String referenceId) { public static String contributionDedupKey(String sourceCode, String referenceId) {
return DEDUP_KEY_PREFIX + sourceCode + ":" + referenceId; return DEDUP_KEY_PREFIX + sourceCode + ":" + referenceId;
} }
public static String upcomingKey(String tenantId) {
return UPCOMING_PREFIX + tenantId;
}
public static String startScheduleKey(String tenantId) {
return START_SCHEDULE_PREFIX + tenantId;
}
public static String startLockKey(String tenantId) {
return START_LOCK_PREFIX + tenantId;
}
public static String finishScheduleKey(String tenantId) {
return FINISH_SCHEDULE_PREFIX + tenantId;
}
public static String finishRetryKey(String tenantId) {
return FINISH_RETRY_PREFIX + tenantId;
}
public static String finishFailedKey(String tenantId) {
return FINISH_FAILED_PREFIX + tenantId;
}
public static String finishLockKey(String tenantId) {
return FINISH_LOCK_PREFIX + tenantId;
}
} }

View File

@@ -0,0 +1,10 @@
package com.starry.admin.modules.pk.reminder.constants;
public final class PkReminderConstants {
public static final long SCAN_INTERVAL_MILLIS = 300000L;
public static final long UPCOMING_WINDOW_MINUTES = 60L;
private PkReminderConstants() {
}
}

View File

@@ -0,0 +1,64 @@
package com.starry.admin.modules.pk.reminder.task;
import cn.hutool.core.util.StrUtil;
import com.starry.admin.modules.clerk.mapper.PlayClerkPkMapper;
import com.starry.admin.modules.clerk.module.entity.ClerkPkEnum;
import com.starry.admin.modules.clerk.module.entity.PlayClerkPkEntity;
import com.starry.admin.modules.pk.redis.PkRedisKeyConstants;
import com.starry.admin.modules.pk.reminder.constants.PkReminderConstants;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@Component
public class ClerkPkUpcomingReminderJob {
private static final double SCORE_MIN = Double.NEGATIVE_INFINITY;
private final PlayClerkPkMapper clerkPkMapper;
private final StringRedisTemplate stringRedisTemplate;
public ClerkPkUpcomingReminderJob(PlayClerkPkMapper clerkPkMapper, StringRedisTemplate stringRedisTemplate) {
this.clerkPkMapper = clerkPkMapper;
this.stringRedisTemplate = stringRedisTemplate;
}
@Scheduled(fixedDelay = PkReminderConstants.SCAN_INTERVAL_MILLIS)
public void refreshUpcomingPkReminders() {
LocalDateTime now = LocalDateTime.now();
LocalDateTime windowEnd = now.plusMinutes(PkReminderConstants.UPCOMING_WINDOW_MINUTES);
Date begin = Date.from(now.atZone(ZoneId.systemDefault()).toInstant());
Date end = Date.from(windowEnd.atZone(ZoneId.systemDefault()).toInstant());
List<PlayClerkPkEntity> upcoming = clerkPkMapper.selectUpcomingByStatus(
ClerkPkEnum.TO_BE_STARTED.name(),
begin,
end);
if (upcoming.isEmpty()) {
return;
}
Map<String, List<PlayClerkPkEntity>> byTenant = upcoming.stream()
.filter(pk -> StrUtil.isNotBlank(pk.getTenantId()))
.collect(Collectors.groupingBy(PlayClerkPkEntity::getTenantId));
long nowEpochSeconds = now.atZone(ZoneId.systemDefault()).toEpochSecond();
for (Map.Entry<String, List<PlayClerkPkEntity>> entry : byTenant.entrySet()) {
String key = PkRedisKeyConstants.upcomingKey(entry.getKey());
stringRedisTemplate.opsForZSet().removeRangeByScore(key, SCORE_MIN, nowEpochSeconds - 1);
for (PlayClerkPkEntity pk : entry.getValue()) {
if (pk.getPkBeginTime() == null || pk.getId() == null) {
continue;
}
long score = pk.getPkBeginTime().toInstant().getEpochSecond();
stringRedisTemplate.opsForZSet().add(key, pk.getId(), score);
}
stringRedisTemplate.expire(key, PkRedisKeyConstants.UPCOMING_REMINDER_TTL_SECONDS, TimeUnit.SECONDS);
}
}
}

View File

@@ -0,0 +1,19 @@
package com.starry.admin.modules.pk.scheduler.constants;
public final class PkSchedulerConstants {
public static final long START_SCAN_INTERVAL_MILLIS = 1000L;
public static final long FINISH_SCAN_INTERVAL_MILLIS = 1000L;
public static final long FALLBACK_SCAN_INTERVAL_MILLIS = 300000L;
public static final long START_LOCK_TTL_MILLIS = 5000L;
public static final long FINISH_LOCK_TTL_MILLIS = 5000L;
public static final int FINISH_RETRY_MAX_ATTEMPTS = 3;
public static final int[] FINISH_RETRY_BACKOFF_SECONDS = {5, 10, 20};
public static final int START_RETRY_DELAY_SECONDS = 5;
private PkSchedulerConstants() {
}
}

View File

@@ -0,0 +1,144 @@
package com.starry.admin.modules.pk.scheduler.task;
import cn.hutool.core.util.StrUtil;
import com.starry.admin.common.exception.CustomException;
import com.starry.admin.modules.pk.enums.PkLifecycleErrorCode;
import com.starry.admin.modules.pk.redis.PkRedisKeyConstants;
import com.starry.admin.modules.pk.scheduler.constants.PkSchedulerConstants;
import com.starry.admin.modules.pk.service.ClerkPkLifecycleService;
import com.starry.admin.modules.system.module.entity.SysTenantEntity;
import com.starry.admin.modules.system.service.ISysTenantService;
import com.starry.admin.utils.TenantScope;
import com.starry.common.utils.IdUtils;
import java.time.Instant;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@Slf4j
@Component
public class PkFinishSchedulerJob {
private static final double SCORE_MIN = Double.NEGATIVE_INFINITY;
private final StringRedisTemplate stringRedisTemplate;
private final ClerkPkLifecycleService clerkPkLifecycleService;
private final ISysTenantService sysTenantService;
public PkFinishSchedulerJob(StringRedisTemplate stringRedisTemplate,
ClerkPkLifecycleService clerkPkLifecycleService,
ISysTenantService sysTenantService) {
this.stringRedisTemplate = stringRedisTemplate;
this.clerkPkLifecycleService = clerkPkLifecycleService;
this.sysTenantService = sysTenantService;
}
@Scheduled(fixedDelay = PkSchedulerConstants.FINISH_SCAN_INTERVAL_MILLIS)
public void scanFinishSchedule() {
List<SysTenantEntity> tenantEntities = sysTenantService.listAll();
if (tenantEntities.isEmpty()) {
return;
}
long nowEpochSeconds = Instant.now().getEpochSecond();
for (SysTenantEntity tenantEntity : tenantEntities) {
String tenantId = tenantEntity.getTenantId();
if (StrUtil.isBlank(tenantId)) {
log.warn("PK结算调度跳过空租户, errorCode={}", PkLifecycleErrorCode.TENANT_MISSING.name());
continue;
}
try {
handleTenant(tenantId, nowEpochSeconds);
} catch (Exception ex) {
log.error("PK结算调度失败, tenantId={}", tenantId, ex);
continue;
}
}
}
private void handleTenant(String tenantId, long nowEpochSeconds) {
String lockKey = PkRedisKeyConstants.finishLockKey(tenantId);
String lockValue = IdUtils.getUuid();
Boolean locked = stringRedisTemplate.opsForValue().setIfAbsent(
lockKey,
lockValue,
PkSchedulerConstants.FINISH_LOCK_TTL_MILLIS,
TimeUnit.MILLISECONDS);
if (!Boolean.TRUE.equals(locked)) {
return;
}
try {
try (TenantScope scope = TenantScope.use(tenantId)) {
String scheduleKey = PkRedisKeyConstants.finishScheduleKey(tenantId);
Set<String> duePkIds = stringRedisTemplate.opsForZSet()
.rangeByScore(scheduleKey, SCORE_MIN, nowEpochSeconds);
if (duePkIds == null || duePkIds.isEmpty()) {
return;
}
for (String pkId : duePkIds) {
processFinish(scheduleKey, tenantId, pkId, nowEpochSeconds);
}
}
} finally {
releaseLock(lockKey, lockValue);
}
}
private void processFinish(String scheduleKey, String tenantId, String pkId, long nowEpochSeconds) {
if (StrUtil.isBlank(pkId)) {
return;
}
try {
clerkPkLifecycleService.finishPk(pkId);
clearRetry(tenantId, pkId);
removeFailed(tenantId, pkId);
stringRedisTemplate.opsForZSet().remove(scheduleKey, pkId);
} catch (Exception ex) {
handleRetry(scheduleKey, tenantId, pkId, nowEpochSeconds, ex);
}
}
private void handleRetry(String scheduleKey, String tenantId, String pkId, long nowEpochSeconds, Exception ex) {
String retryKey = PkRedisKeyConstants.finishRetryKey(tenantId);
Long attempt = stringRedisTemplate.opsForHash().increment(retryKey, pkId, 1L);
int retryCount = attempt == null ? 1 : attempt.intValue();
if (retryCount >= PkSchedulerConstants.FINISH_RETRY_MAX_ATTEMPTS) {
String failedKey = PkRedisKeyConstants.finishFailedKey(tenantId);
stringRedisTemplate.opsForZSet().add(failedKey, pkId, nowEpochSeconds);
stringRedisTemplate.opsForZSet().remove(scheduleKey, pkId);
clearRetry(tenantId, pkId);
log.error("PK 自动结算失败超过重试上限, pkId={}, retryCount={}", pkId, retryCount, ex);
return;
}
long backoffSeconds = resolveBackoffSeconds(retryCount);
long nextScore = nowEpochSeconds + backoffSeconds;
stringRedisTemplate.opsForZSet().add(scheduleKey, pkId, nextScore);
log.warn("PK 自动结算失败, pkId={}, retryCount={}, nextScore={}", pkId, retryCount, nextScore, ex);
}
private long resolveBackoffSeconds(int retryCount) {
int[] backoffs = PkSchedulerConstants.FINISH_RETRY_BACKOFF_SECONDS;
int index = Math.min(retryCount, backoffs.length) - 1;
return backoffs[index];
}
private void clearRetry(String tenantId, String pkId) {
String retryKey = PkRedisKeyConstants.finishRetryKey(tenantId);
stringRedisTemplate.opsForHash().delete(retryKey, pkId);
}
private void removeFailed(String tenantId, String pkId) {
String failedKey = PkRedisKeyConstants.finishFailedKey(tenantId);
stringRedisTemplate.opsForZSet().remove(failedKey, pkId);
}
private void releaseLock(String lockKey, String lockValue) {
String currentValue = stringRedisTemplate.opsForValue().get(lockKey);
if (lockValue.equals(currentValue)) {
stringRedisTemplate.delete(lockKey);
}
}
}

View File

@@ -0,0 +1,133 @@
package com.starry.admin.modules.pk.scheduler.task;
import cn.hutool.core.util.StrUtil;
import com.starry.admin.common.exception.CustomException;
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.modules.pk.scheduler.constants.PkSchedulerConstants;
import com.starry.admin.modules.pk.service.ClerkPkLifecycleService;
import com.starry.admin.modules.system.module.entity.SysTenantEntity;
import com.starry.admin.modules.system.service.ISysTenantService;
import com.starry.admin.utils.TenantScope;
import com.starry.common.utils.IdUtils;
import java.time.Instant;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@Slf4j
@Component
public class PkStartSchedulerJob {
private static final double SCORE_MIN = Double.NEGATIVE_INFINITY;
private final StringRedisTemplate stringRedisTemplate;
private final ClerkPkLifecycleService clerkPkLifecycleService;
private final IPlayClerkPkService clerkPkService;
private final ISysTenantService sysTenantService;
public PkStartSchedulerJob(StringRedisTemplate stringRedisTemplate,
ClerkPkLifecycleService clerkPkLifecycleService,
IPlayClerkPkService clerkPkService,
ISysTenantService sysTenantService) {
this.stringRedisTemplate = stringRedisTemplate;
this.clerkPkLifecycleService = clerkPkLifecycleService;
this.clerkPkService = clerkPkService;
this.sysTenantService = sysTenantService;
}
@Scheduled(fixedDelay = PkSchedulerConstants.START_SCAN_INTERVAL_MILLIS)
public void scanStartSchedule() {
List<SysTenantEntity> tenantEntities = sysTenantService.listAll();
if (tenantEntities.isEmpty()) {
return;
}
long nowEpochSeconds = Instant.now().getEpochSecond();
for (SysTenantEntity tenantEntity : tenantEntities) {
String tenantId = tenantEntity.getTenantId();
if (StrUtil.isBlank(tenantId)) {
log.warn("PK开始调度跳过空租户, errorCode={}", PkLifecycleErrorCode.TENANT_MISSING.name());
continue;
}
try {
handleTenant(tenantId, nowEpochSeconds);
} catch (Exception ex) {
log.error("PK开始调度失败, tenantId={}", tenantId, ex);
continue;
}
}
}
private void handleTenant(String tenantId, long nowEpochSeconds) {
String lockKey = PkRedisKeyConstants.startLockKey(tenantId);
String lockValue = IdUtils.getUuid();
Boolean locked = stringRedisTemplate.opsForValue().setIfAbsent(
lockKey,
lockValue,
PkSchedulerConstants.START_LOCK_TTL_MILLIS,
TimeUnit.MILLISECONDS);
if (!Boolean.TRUE.equals(locked)) {
return;
}
try {
try (TenantScope scope = TenantScope.use(tenantId)) {
String scheduleKey = PkRedisKeyConstants.startScheduleKey(tenantId);
Set<String> duePkIds = stringRedisTemplate.opsForZSet()
.rangeByScore(scheduleKey, SCORE_MIN, nowEpochSeconds);
if (duePkIds == null || duePkIds.isEmpty()) {
return;
}
for (String pkId : duePkIds) {
processStart(scheduleKey, pkId, nowEpochSeconds);
}
}
} finally {
releaseLock(lockKey, lockValue);
}
}
private void processStart(String scheduleKey, String pkId, long nowEpochSeconds) {
if (StrUtil.isBlank(pkId)) {
return;
}
PlayClerkPkEntity pk = clerkPkService.selectPlayClerkPkById(pkId);
if (pk == null) {
stringRedisTemplate.opsForZSet().remove(scheduleKey, pkId);
return;
}
if (!ClerkPkEnum.TO_BE_STARTED.name().equals(pk.getStatus())) {
stringRedisTemplate.opsForZSet().remove(scheduleKey, pkId);
return;
}
if (pk.getPkBeginTime() == null) {
stringRedisTemplate.opsForZSet().remove(scheduleKey, pkId);
return;
}
long beginEpochSeconds = pk.getPkBeginTime().toInstant().getEpochSecond();
if (beginEpochSeconds > nowEpochSeconds) {
stringRedisTemplate.opsForZSet().add(scheduleKey, pkId, beginEpochSeconds);
return;
}
try {
clerkPkLifecycleService.startPk(pkId);
} catch (Exception ex) {
long retryAt = nowEpochSeconds + PkSchedulerConstants.START_RETRY_DELAY_SECONDS;
stringRedisTemplate.opsForZSet().add(scheduleKey, pkId, retryAt);
log.warn("PK 自动开始失败, pkId={}, retryAt={}", pkId, retryAt, ex);
}
}
private void releaseLock(String lockKey, String lockValue) {
String currentValue = stringRedisTemplate.opsForValue().get(lockKey);
if (lockValue.equals(currentValue)) {
stringRedisTemplate.delete(lockKey);
}
}
}

View File

@@ -1,5 +1,7 @@
package com.starry.admin.modules.pk.service; package com.starry.admin.modules.pk.service;
import com.starry.admin.modules.pk.dto.PlayClerkPkForceStartRequest;
/** /**
* 店员 PK 生命周期管理服务。 * 店员 PK 生命周期管理服务。
*/ */
@@ -23,4 +25,12 @@ public interface ClerkPkLifecycleService {
* 扫描当前需要状态流转的 PK。 * 扫描当前需要状态流转的 PK。
*/ */
void scanAndUpdate(); void scanAndUpdate();
/**
* 强制开始PK用于测试无需排期
*
* @param request 强制开始请求
* @return PK ID
*/
String forceStart(PlayClerkPkForceStartRequest request);
} }

View File

@@ -0,0 +1,16 @@
package com.starry.admin.modules.pk.service;
import com.starry.admin.modules.clerk.module.entity.PlayClerkPkEntity;
import com.starry.admin.modules.pk.dto.WxPkClerkHistoryPageDto;
import com.starry.admin.modules.pk.dto.WxPkContributorDto;
import com.starry.admin.modules.pk.dto.WxPkHistoryDto;
import java.util.List;
public interface IPkDetailService {
List<WxPkContributorDto> getContributors(PlayClerkPkEntity pk);
List<WxPkHistoryDto> getHistory(PlayClerkPkEntity pk);
WxPkClerkHistoryPageDto getClerkHistory(String clerkId, int pageNum, int pageSize);
}

View File

@@ -1,16 +1,27 @@
package com.starry.admin.modules.pk.service.impl; package com.starry.admin.modules.pk.service.impl;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.starry.admin.common.exception.CustomException; import com.starry.admin.common.exception.CustomException;
import com.starry.admin.modules.clerk.module.entity.ClerkPkEnum; 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.PlayClerkPkEntity;
import com.starry.admin.modules.clerk.service.IPlayClerkPkService; import com.starry.admin.modules.clerk.service.IPlayClerkPkService;
import com.starry.admin.modules.pk.dto.PlayClerkPkForceStartRequest;
import com.starry.admin.modules.pk.enums.PkLifecycleErrorCode;
import com.starry.admin.modules.pk.redis.PkRedisKeyConstants; import com.starry.admin.modules.pk.redis.PkRedisKeyConstants;
import com.starry.admin.modules.pk.scheduler.constants.PkSchedulerConstants;
import com.starry.admin.modules.pk.service.ClerkPkLifecycleService; import com.starry.admin.modules.pk.service.ClerkPkLifecycleService;
import com.starry.admin.modules.pk.setting.constants.PkSettingValidationConstants;
import com.starry.admin.modules.system.module.entity.SysTenantEntity;
import com.starry.admin.modules.system.service.ISysTenantService;
import com.starry.admin.utils.SecurityUtils;
import com.starry.admin.utils.TenantScope;
import com.starry.common.utils.IdUtils;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.ZoneId; import java.time.ZoneId;
import java.util.Date; import java.util.Date;
import java.util.List;
import java.util.Map; import java.util.Map;
import javax.annotation.Resource; import javax.annotation.Resource;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@@ -29,6 +40,9 @@ public class ClerkPkLifecycleServiceImpl implements ClerkPkLifecycleService {
@Resource @Resource
private StringRedisTemplate stringRedisTemplate; private StringRedisTemplate stringRedisTemplate;
@Resource
private ISysTenantService sysTenantService;
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public void startPk(String pkId) { public void startPk(String pkId) {
@@ -37,6 +51,7 @@ public class ClerkPkLifecycleServiceImpl implements ClerkPkLifecycleService {
throw new CustomException("PK不存在"); throw new CustomException("PK不存在");
} }
if (!ClerkPkEnum.TO_BE_STARTED.name().equals(pk.getStatus())) { if (!ClerkPkEnum.TO_BE_STARTED.name().equals(pk.getStatus())) {
removeStartSchedule(pk);
return; return;
} }
LocalDateTime now = LocalDateTime.now(); LocalDateTime now = LocalDateTime.now();
@@ -46,6 +61,8 @@ public class ClerkPkLifecycleServiceImpl implements ClerkPkLifecycleService {
} }
pk.setStatus(ClerkPkEnum.IN_PROGRESS.name()); pk.setStatus(ClerkPkEnum.IN_PROGRESS.name());
clerkPkService.updateById(pk); clerkPkService.updateById(pk);
removeStartSchedule(pk);
scheduleFinish(pk);
} }
@Override @Override
@@ -56,6 +73,7 @@ public class ClerkPkLifecycleServiceImpl implements ClerkPkLifecycleService {
throw new CustomException("PK不存在"); throw new CustomException("PK不存在");
} }
if (ClerkPkEnum.FINISHED.name().equals(pk.getStatus())) { if (ClerkPkEnum.FINISHED.name().equals(pk.getStatus())) {
removeFinishSchedule(pk);
return; return;
} }
String scoreKey = PkRedisKeyConstants.scoreKey(pkId); String scoreKey = PkRedisKeyConstants.scoreKey(pkId);
@@ -80,6 +98,7 @@ public class ClerkPkLifecycleServiceImpl implements ClerkPkLifecycleService {
pk.setWinnerClerkId(null); pk.setWinnerClerkId(null);
} }
clerkPkService.updateById(pk); clerkPkService.updateById(pk);
removeFinishSchedule(pk);
} }
@Override @Override
@@ -113,10 +132,91 @@ public class ClerkPkLifecycleServiceImpl implements ClerkPkLifecycleService {
}); });
} }
@Scheduled(fixedDelay = 30000) @Override
@Transactional(rollbackFor = Exception.class)
public String forceStart(PlayClerkPkForceStartRequest request) {
validateForceStartRequest(request);
String tenantId = SecurityUtils.getTenantId();
if (StrUtil.isBlank(tenantId)) {
throw new CustomException(PkLifecycleErrorCode.TENANT_MISSING.getMessage());
}
LocalDateTime now = LocalDateTime.now();
LocalDateTime endTime = now.plusMinutes(request.getDurationMinutes());
Date beginDate = Date.from(now.atZone(ZoneId.systemDefault()).toInstant());
Date endDate = Date.from(endTime.atZone(ZoneId.systemDefault()).toInstant());
if (hasClerkConflict(request.getClerkAId(), request.getClerkBId(), beginDate, endDate)) {
throw new CustomException(PkLifecycleErrorCode.CLERK_CONFLICT.getMessage());
}
PlayClerkPkEntity pk = new PlayClerkPkEntity();
pk.setId(IdUtils.getUuid());
pk.setTenantId(tenantId);
pk.setClerkA(request.getClerkAId());
pk.setClerkB(request.getClerkBId());
pk.setPkBeginTime(beginDate);
pk.setPkEndTime(endDate);
pk.setStatus(ClerkPkEnum.IN_PROGRESS.name());
pk.setSettled(0);
pk.setCreatedBy(SecurityUtils.getUserId());
clerkPkService.save(pk);
scheduleFinish(pk);
return pk.getId();
}
@Scheduled(fixedDelay = PkSchedulerConstants.FALLBACK_SCAN_INTERVAL_MILLIS)
public void scheduledScan() { public void scheduledScan() {
scanAndUpdateForAllTenants();
}
private void scanAndUpdateForAllTenants() {
List<SysTenantEntity> tenantEntities = sysTenantService.listAll();
if (tenantEntities.isEmpty()) {
return;
}
for (SysTenantEntity tenantEntity : tenantEntities) {
String tenantId = tenantEntity.getTenantId();
if (StrUtil.isBlank(tenantId)) {
log.warn("PK扫描跳过空租户, errorCode={}", PkLifecycleErrorCode.TENANT_MISSING.name());
continue;
}
try (TenantScope scope = TenantScope.use(tenantId)) {
scanAndUpdate(); scanAndUpdate();
} }
}
}
private void scheduleFinish(PlayClerkPkEntity pk) {
if (pk == null || pk.getPkEndTime() == null) {
throw new CustomException(PkLifecycleErrorCode.REQUEST_INVALID.getMessage());
}
String tenantId = pk.getTenantId();
if (StrUtil.isBlank(tenantId)) {
throw new CustomException(PkLifecycleErrorCode.TENANT_MISSING.getMessage());
}
long endEpochSeconds = pk.getPkEndTime().toInstant().getEpochSecond();
String scheduleKey = PkRedisKeyConstants.finishScheduleKey(tenantId);
stringRedisTemplate.opsForZSet().add(scheduleKey, pk.getId(), endEpochSeconds);
}
private void removeStartSchedule(PlayClerkPkEntity pk) {
if (pk == null || StrUtil.isBlank(pk.getTenantId())) {
return;
}
String scheduleKey = PkRedisKeyConstants.startScheduleKey(pk.getTenantId());
stringRedisTemplate.opsForZSet().remove(scheduleKey, pk.getId());
}
private void removeFinishSchedule(PlayClerkPkEntity pk) {
if (pk == null || StrUtil.isBlank(pk.getTenantId())) {
return;
}
String tenantId = pk.getTenantId();
String scheduleKey = PkRedisKeyConstants.finishScheduleKey(tenantId);
stringRedisTemplate.opsForZSet().remove(scheduleKey, pk.getId());
String retryKey = PkRedisKeyConstants.finishRetryKey(tenantId);
stringRedisTemplate.opsForHash().delete(retryKey, pk.getId());
String failedKey = PkRedisKeyConstants.finishFailedKey(tenantId);
stringRedisTemplate.opsForZSet().remove(failedKey, pk.getId());
}
private static BigDecimal parseDecimal(Object value) { private static BigDecimal parseDecimal(Object value) {
if (value == null) { if (value == null) {
@@ -139,4 +239,35 @@ public class ClerkPkLifecycleServiceImpl implements ClerkPkLifecycleService {
return 0; return 0;
} }
} }
private void validateForceStartRequest(PlayClerkPkForceStartRequest request) {
if (request == null
|| StrUtil.isBlank(request.getClerkAId())
|| StrUtil.isBlank(request.getClerkBId())
|| request.getDurationMinutes() == null) {
throw new CustomException(PkLifecycleErrorCode.REQUEST_INVALID.getMessage());
}
if (request.getClerkAId().equals(request.getClerkBId())) {
throw new CustomException(PkLifecycleErrorCode.REQUEST_INVALID.getMessage());
}
if (request.getDurationMinutes() < PkSettingValidationConstants.MIN_DURATION_MINUTES) {
throw new CustomException(PkLifecycleErrorCode.REQUEST_INVALID.getMessage());
}
}
private boolean hasClerkConflict(String clerkAId, String clerkBId, Date beginTime, Date endTime) {
return clerkPkService.count(Wrappers.<PlayClerkPkEntity>lambdaQuery()
.in(PlayClerkPkEntity::getStatus,
ClerkPkEnum.TO_BE_STARTED.name(),
ClerkPkEnum.IN_PROGRESS.name())
.and(time -> time.le(PlayClerkPkEntity::getPkBeginTime, endTime)
.ge(PlayClerkPkEntity::getPkEndTime, beginTime))
.and(clerk -> clerk.eq(PlayClerkPkEntity::getClerkA, clerkAId)
.or()
.eq(PlayClerkPkEntity::getClerkB, clerkAId)
.or()
.eq(PlayClerkPkEntity::getClerkA, clerkBId)
.or()
.eq(PlayClerkPkEntity::getClerkB, clerkBId))) > 0;
}
} }

View File

@@ -0,0 +1,265 @@
package com.starry.admin.modules.pk.service.impl;
import cn.hutool.core.util.StrUtil;
import com.starry.admin.common.exception.CustomException;
import com.starry.admin.modules.clerk.mapper.PlayClerkPkMapper;
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.IPlayClerkUserInfoService;
import com.starry.admin.modules.order.mapper.PlayOrderInfoMapper;
import com.starry.admin.modules.order.module.constant.OrderConstant;
import com.starry.admin.modules.pk.constants.PkWxQueryConstants;
import com.starry.admin.modules.pk.dto.WxPkClerkHistoryPageDto;
import com.starry.admin.modules.pk.dto.WxPkClerkHistorySummaryDto;
import com.starry.admin.modules.pk.dto.WxPkContributorDto;
import com.starry.admin.modules.pk.dto.WxPkHistoryDto;
import com.starry.admin.modules.pk.service.IPkDetailService;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.annotation.Resource;
import org.springframework.stereotype.Service;
@Service
public class PkDetailServiceImpl implements IPkDetailService {
private static final ZoneId DEFAULT_ZONE = ZoneId.systemDefault();
@Resource
private PlayOrderInfoMapper orderInfoMapper;
@Resource
private PlayClerkPkMapper clerkPkMapper;
@Resource
private IPlayClerkUserInfoService clerkUserInfoService;
@Override
public List<WxPkContributorDto> getContributors(PlayClerkPkEntity pk) {
validatePk(pk);
LocalDateTime startTime = toLocalDateTime(pk.getPkBeginTime());
LocalDateTime endTime = toLocalDateTime(pk.getPkEndTime());
LocalDateTime now = LocalDateTime.now(DEFAULT_ZONE);
if (endTime.isAfter(now)) {
endTime = now;
}
if (endTime.isBefore(startTime)) {
return Collections.emptyList();
}
List<WxPkContributorDto> contributors = orderInfoMapper.selectPkContributors(
pk.getTenantId(),
pk.getClerkA(),
pk.getClerkB(),
startTime,
endTime,
OrderConstant.OrderStatus.COMPLETED.getCode(),
PkWxQueryConstants.MIN_CONTRIBUTION_AMOUNT,
PkWxQueryConstants.CONTRIBUTOR_LIMIT);
if (contributors == null || contributors.isEmpty()) {
return Collections.emptyList();
}
List<WxPkContributorDto> normalized = new ArrayList<>();
for (WxPkContributorDto contributor : contributors) {
normalized.add(normalizeContributor(contributor));
}
return normalized;
}
@Override
public List<WxPkHistoryDto> getHistory(PlayClerkPkEntity pk) {
validatePk(pk);
List<PlayClerkPkEntity> history = clerkPkMapper.selectRecentFinishedBetweenClerks(
pk.getTenantId(),
pk.getClerkA(),
pk.getClerkB(),
ClerkPkEnum.FINISHED.name(),
PkWxQueryConstants.HISTORY_LIMIT);
if (history == null || history.isEmpty()) {
return Collections.emptyList();
}
Map<String, String> clerkNames = loadClerkNames(pk.getClerkA(), pk.getClerkB());
List<WxPkHistoryDto> items = new ArrayList<>();
for (PlayClerkPkEntity item : history) {
items.add(toHistoryDto(item, clerkNames));
}
return items;
}
@Override
public WxPkClerkHistoryPageDto getClerkHistory(String clerkId, int pageNum, int pageSize) {
if (StrUtil.isBlank(clerkId)) {
throw new CustomException("店员ID不能为空");
}
int safePageNum = normalizePageParam(pageNum, PkWxQueryConstants.CLERK_HISTORY_PAGE_NUM);
int safePageSize = normalizePageParam(pageSize, PkWxQueryConstants.CLERK_HISTORY_PAGE_SIZE);
com.baomidou.mybatisplus.extension.plugins.pagination.Page<PlayClerkPkEntity> page =
new com.baomidou.mybatisplus.extension.plugins.pagination.Page<>(safePageNum, safePageSize);
com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<PlayClerkPkEntity> wrapper =
com.baomidou.mybatisplus.core.toolkit.Wrappers.lambdaQuery(PlayClerkPkEntity.class)
.eq(PlayClerkPkEntity::getStatus, ClerkPkEnum.FINISHED.name())
.and(w -> w.eq(PlayClerkPkEntity::getClerkA, clerkId)
.or()
.eq(PlayClerkPkEntity::getClerkB, clerkId))
.orderByDesc(PlayClerkPkEntity::getPkBeginTime);
com.baomidou.mybatisplus.extension.plugins.pagination.Page<PlayClerkPkEntity> result =
clerkPkMapper.selectPage(page, wrapper);
List<WxPkHistoryDto> items = new ArrayList<>();
if (result.getRecords() != null) {
for (PlayClerkPkEntity item : result.getRecords()) {
items.add(toClerkHistoryDto(item));
}
}
WxPkClerkHistoryPageDto pageDto = new WxPkClerkHistoryPageDto();
pageDto.setItems(items);
pageDto.setTotalCount(result.getTotal());
pageDto.setPageNum(safePageNum);
pageDto.setPageSize(safePageSize);
pageDto.setSummary(buildSummary(clerkId, result.getTotal()));
return pageDto;
}
private void validatePk(PlayClerkPkEntity pk) {
if (pk == null) {
throw new CustomException("PK不存在");
}
if (StrUtil.isBlank(pk.getTenantId())) {
throw new CustomException("租户信息缺失");
}
if (StrUtil.isBlank(pk.getClerkA()) || StrUtil.isBlank(pk.getClerkB())) {
throw new CustomException("PK店员信息缺失");
}
if (pk.getPkBeginTime() == null || pk.getPkEndTime() == null) {
throw new CustomException("PK时间信息缺失");
}
}
private LocalDateTime toLocalDateTime(java.util.Date date) {
if (date == null) {
throw new CustomException("时间信息缺失");
}
return LocalDateTime.ofInstant(date.toInstant(), DEFAULT_ZONE);
}
private WxPkContributorDto normalizeContributor(WxPkContributorDto source) {
WxPkContributorDto dto = new WxPkContributorDto();
if (source == null) {
return dto;
}
if (StrUtil.isNotBlank(source.getUserId())) {
dto.setUserId(source.getUserId());
}
if (StrUtil.isNotBlank(source.getNickname())) {
dto.setNickname(source.getNickname());
}
if (source.getAmount() != null) {
dto.setAmount(source.getAmount());
}
return dto;
}
private Map<String, String> loadClerkNames(String clerkAId, String clerkBId) {
Map<String, String> names = new HashMap<>();
PlayClerkUserInfoEntity clerkA = clerkUserInfoService.getById(clerkAId);
PlayClerkUserInfoEntity clerkB = clerkUserInfoService.getById(clerkBId);
names.put(clerkAId, clerkA == null ? "" : safeText(clerkA.getNickname()));
names.put(clerkBId, clerkB == null ? "" : safeText(clerkB.getNickname()));
return names;
}
private WxPkHistoryDto toHistoryDto(PlayClerkPkEntity item, Map<String, String> clerkNames) {
WxPkHistoryDto dto = new WxPkHistoryDto();
if (item == null) {
return dto;
}
dto.setClerkAName(safeText(clerkNames.get(item.getClerkA())));
dto.setClerkBName(safeText(clerkNames.get(item.getClerkB())));
dto.setClerkAScore(safeAmount(item.getClerkAScore()));
dto.setClerkBScore(safeAmount(item.getClerkBScore()));
dto.setId(safeText(item.getId()));
dto.setClerkAId(safeText(item.getClerkA()));
dto.setClerkBId(safeText(item.getClerkB()));
dto.setWinnerClerkId(safeText(item.getWinnerClerkId()));
if (item.getPkBeginTime() != null) {
dto.setPkBeginTime(item.getPkBeginTime());
}
return dto;
}
private String safeText(String value) {
return value == null ? "" : value;
}
private BigDecimal safeAmount(BigDecimal value) {
return value == null ? BigDecimal.ZERO : value;
}
private int normalizePageParam(int value, int defaultValue) {
if (value < PkWxQueryConstants.CLERK_HISTORY_MIN_PAGE) {
return defaultValue;
}
return value;
}
private WxPkHistoryDto toClerkHistoryDto(PlayClerkPkEntity item) {
WxPkHistoryDto dto = toHistoryDto(item, loadClerkNames(item.getClerkA(), item.getClerkB()));
WxPkContributorDto topContributor = loadTopContributor(item);
if (topContributor != null) {
dto.setTopContributorName(safeText(topContributor.getNickname()));
dto.setTopContributorAmount(safeAmount(topContributor.getAmount()));
}
return dto;
}
private WxPkContributorDto loadTopContributor(PlayClerkPkEntity pk) {
if (pk == null || pk.getPkBeginTime() == null || pk.getPkEndTime() == null) {
return null;
}
LocalDateTime startTime = toLocalDateTime(pk.getPkBeginTime());
LocalDateTime endTime = toLocalDateTime(pk.getPkEndTime());
List<WxPkContributorDto> contributors = orderInfoMapper.selectPkContributors(
pk.getTenantId(),
pk.getClerkA(),
pk.getClerkB(),
startTime,
endTime,
OrderConstant.OrderStatus.COMPLETED.getCode(),
PkWxQueryConstants.MIN_CONTRIBUTION_AMOUNT,
PkWxQueryConstants.TOP_CONTRIBUTOR_LIMIT);
if (contributors == null || contributors.isEmpty()) {
return null;
}
return normalizeContributor(contributors.get(0));
}
private WxPkClerkHistorySummaryDto buildSummary(String clerkId, long totalCount) {
WxPkClerkHistorySummaryDto summary = new WxPkClerkHistorySummaryDto();
summary.setTotalCount(totalCount);
if (totalCount <= 0) {
return summary;
}
com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<PlayClerkPkEntity> winWrapper =
com.baomidou.mybatisplus.core.toolkit.Wrappers.lambdaQuery(PlayClerkPkEntity.class)
.eq(PlayClerkPkEntity::getStatus, ClerkPkEnum.FINISHED.name())
.and(w -> w.eq(PlayClerkPkEntity::getClerkA, clerkId)
.or()
.eq(PlayClerkPkEntity::getClerkB, clerkId))
.eq(PlayClerkPkEntity::getWinnerClerkId, clerkId);
long winCount = clerkPkMapper.selectCount(winWrapper);
summary.setWinCount(winCount);
BigDecimal rate = BigDecimal.valueOf(winCount)
.multiply(PkWxQueryConstants.WIN_RATE_MULTIPLIER)
.divide(BigDecimal.valueOf(totalCount), PkWxQueryConstants.WIN_RATE_SCALE, RoundingMode.HALF_UP);
summary.setWinRate(rate.stripTrailingZeros().toPlainString() + PkWxQueryConstants.PERCENT_SUFFIX);
return summary;
}
}

View File

@@ -0,0 +1,16 @@
package com.starry.admin.modules.pk.setting.constants;
public final class PkSettingApiConstants {
public static final String BASE_PATH = "/play/pk/settings";
public static final String LIST_PATH = "/list";
public static final String DETAIL_PATH = "/{id}";
public static final String CREATE_PATH = "/create";
public static final String BULK_CREATE_PATH = "/bulk-create";
public static final String UPDATE_PATH = "/update/{id}";
public static final String ENABLE_PATH = "/{id}/enable";
public static final String DISABLE_PATH = "/{id}/disable";
private PkSettingApiConstants() {
}
}

View File

@@ -0,0 +1,16 @@
package com.starry.admin.modules.pk.setting.constants;
public final class PkSettingValidationConstants {
public static final int MIN_DURATION_MINUTES = 1;
public static final int MAX_YEARS_AHEAD = 3;
public static final int MIN_DAY_OF_MONTH = 1;
public static final int MAX_DAY_OF_MONTH = 31;
public static final int MIN_MONTH_OF_YEAR = 1;
public static final int MAX_MONTH_OF_YEAR = 12;
public static final int DEFAULT_PAGE_NUMBER = 1;
public static final int DEFAULT_PAGE_SIZE = 10;
private PkSettingValidationConstants() {
}
}

View File

@@ -0,0 +1,118 @@
package com.starry.admin.modules.pk.setting.controller;
import com.starry.admin.modules.pk.setting.constants.PkSettingApiConstants;
import com.starry.admin.modules.pk.setting.dto.PlayClerkPkSettingBulkCreateRequest;
import com.starry.admin.modules.pk.setting.dto.PlayClerkPkSettingUpsertRequest;
import com.starry.admin.modules.pk.setting.service.IPlayClerkPkSettingService;
import com.starry.common.annotation.Log;
import com.starry.common.enums.BusinessType;
import com.starry.common.result.R;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import io.swagger.annotations.ApiResponse;
import io.swagger.annotations.ApiResponses;
import javax.annotation.Resource;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Api(tags = "店员PK排期设置", description = "店员PK排期设置相关接口")
@RestController
@RequestMapping(PkSettingApiConstants.BASE_PATH)
public class PlayClerkPkSettingController {
@Resource
private IPlayClerkPkSettingService pkSettingService;
/**
* 查询PK排期设置列表
*/
@ApiOperation(value = "查询PK排期设置列表", notes = "分页查询PK排期设置")
@ApiResponses({@ApiResponse(code = 200, message = "操作成功")})
@GetMapping(PkSettingApiConstants.LIST_PATH)
public R list() {
return R.ok(pkSettingService.listSettings());
}
/**
* 获取PK排期设置详情
*/
@ApiOperation(value = "获取PK排期设置详情", notes = "根据ID获取PK排期设置详情")
@ApiImplicitParam(name = "id", value = "设置ID", required = true, paramType = "path", dataType = "String")
@ApiResponses({@ApiResponse(code = 200, message = "操作成功")})
@GetMapping(PkSettingApiConstants.DETAIL_PATH)
public R getInfo(@PathVariable("id") String id) {
return R.ok(pkSettingService.getSetting(id));
}
/**
* 新增PK排期设置
*/
@ApiOperation(value = "新增PK排期设置", notes = "创建新的PK排期设置")
@ApiResponses({@ApiResponse(code = 200, message = "操作成功"), @ApiResponse(code = 500, message = "添加失败")})
@Log(title = "店员PK排期设置", businessType = BusinessType.INSERT)
@PostMapping(PkSettingApiConstants.CREATE_PATH)
public R create(
@ApiParam(value = "PK排期设置", required = true) @RequestBody PlayClerkPkSettingUpsertRequest request) {
String id = pkSettingService.createSetting(request);
return R.ok(id);
}
/**
* 批量新增PK排期设置
*/
@ApiOperation(value = "批量新增PK排期设置", notes = "批量创建多个PK排期设置")
@ApiResponses({@ApiResponse(code = 200, message = "操作成功"), @ApiResponse(code = 500, message = "添加失败")})
@Log(title = "店员PK排期设置", businessType = BusinessType.INSERT)
@PostMapping(PkSettingApiConstants.BULK_CREATE_PATH)
public R bulkCreate(
@ApiParam(value = "PK排期设置列表", required = true)
@RequestBody PlayClerkPkSettingBulkCreateRequest request) {
return R.ok(pkSettingService.createSettings(request));
}
/**
* 修改PK排期设置
*/
@ApiOperation(value = "修改PK排期设置", notes = "根据ID修改PK排期设置")
@ApiImplicitParam(name = "id", value = "设置ID", required = true, paramType = "path", dataType = "String")
@ApiResponses({@ApiResponse(code = 200, message = "操作成功"), @ApiResponse(code = 500, message = "修改失败")})
@Log(title = "店员PK排期设置", businessType = BusinessType.UPDATE)
@PostMapping(PkSettingApiConstants.UPDATE_PATH)
public R update(@PathVariable("id") String id,
@ApiParam(value = "PK排期设置", required = true) @RequestBody PlayClerkPkSettingUpsertRequest request) {
pkSettingService.updateSetting(id, request);
return R.ok();
}
/**
* 启用PK排期设置
*/
@ApiOperation(value = "启用PK排期设置", notes = "启用指定PK排期设置")
@ApiImplicitParam(name = "id", value = "设置ID", required = true, paramType = "path", dataType = "String")
@ApiResponses({@ApiResponse(code = 200, message = "操作成功")})
@Log(title = "店员PK排期设置", businessType = BusinessType.UPDATE)
@PostMapping(PkSettingApiConstants.ENABLE_PATH)
public R enable(@PathVariable("id") String id) {
pkSettingService.enableSetting(id);
return R.ok();
}
/**
* 停用PK排期设置
*/
@ApiOperation(value = "停用PK排期设置", notes = "停用指定PK排期设置")
@ApiImplicitParam(name = "id", value = "设置ID", required = true, paramType = "path", dataType = "String")
@ApiResponses({@ApiResponse(code = 200, message = "操作成功")})
@Log(title = "店员PK排期设置", businessType = BusinessType.UPDATE)
@PostMapping(PkSettingApiConstants.DISABLE_PATH)
public R disable(@PathVariable("id") String id) {
pkSettingService.disableSetting(id);
return R.ok();
}
}

View File

@@ -0,0 +1,14 @@
package com.starry.admin.modules.pk.setting.dto;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import java.util.List;
import lombok.Data;
@ApiModel(value = "PlayClerkPkSettingBulkCreateRequest", description = "店员PK排期设置批量创建请求")
@Data
public class PlayClerkPkSettingBulkCreateRequest {
@ApiModelProperty(value = "批量排期设置列表", required = true)
private List<PlayClerkPkSettingUpsertRequest> settings;
}

View File

@@ -0,0 +1,54 @@
package com.starry.admin.modules.pk.setting.dto;
import com.starry.admin.modules.pk.setting.enums.PkRecurrenceType;
import com.starry.admin.modules.pk.setting.enums.PkScheduleDayOfWeek;
import com.starry.admin.modules.pk.setting.enums.PkSettingStatus;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import java.time.LocalDate;
import java.time.LocalTime;
import lombok.Data;
@ApiModel(value = "PlayClerkPkSettingUpsertRequest", description = "店员PK排期设置创建/更新请求")
@Data
public class PlayClerkPkSettingUpsertRequest {
@ApiModelProperty(value = "设置名称", required = true)
private String name;
@ApiModelProperty(value = "循环类型", required = true)
private PkRecurrenceType recurrenceType;
@ApiModelProperty(value = "星期几")
private PkScheduleDayOfWeek dayOfWeek;
@ApiModelProperty(value = "每月第N天")
private Integer dayOfMonth;
@ApiModelProperty(value = "每年月份")
private Integer monthOfYear;
@ApiModelProperty(value = "每日开始时间", required = true)
private LocalTime startTimeOfDay;
@ApiModelProperty(value = "持续分钟数", required = true)
private Integer durationMinutes;
@ApiModelProperty(value = "生效开始日期", required = true)
private LocalDate effectiveStartDate;
@ApiModelProperty(value = "生效结束日期")
private LocalDate effectiveEndDate;
@ApiModelProperty(value = "时区", required = true)
private String timezone;
@ApiModelProperty(value = "店员A ID", required = true)
private String clerkAId;
@ApiModelProperty(value = "店员B ID", required = true)
private String clerkBId;
@ApiModelProperty(value = "状态", required = true)
private PkSettingStatus status;
}

View File

@@ -0,0 +1,62 @@
package com.starry.admin.modules.pk.setting.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.starry.common.domain.BaseEntity;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import java.time.LocalDate;
import java.time.LocalTime;
import lombok.Data;
import lombok.EqualsAndHashCode;
@ApiModel(value = "PlayClerkPkSettingEntity", description = "店员PK排期设置")
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("play_clerk_pk_settings")
public class PlayClerkPkSettingEntity extends BaseEntity<PlayClerkPkSettingEntity> {
@ApiModelProperty(value = "UUID", required = true)
private String id;
@ApiModelProperty(value = "租户ID", required = true)
private String tenantId;
@ApiModelProperty(value = "设置名称", required = true)
private String name;
@ApiModelProperty(value = "循环类型", required = true)
private String recurrenceType;
@ApiModelProperty(value = "星期几")
private String dayOfWeek;
@ApiModelProperty(value = "每月第N天")
private Integer dayOfMonth;
@ApiModelProperty(value = "每年月份")
private Integer monthOfYear;
@ApiModelProperty(value = "每日开始时间", required = true)
private LocalTime startTimeOfDay;
@ApiModelProperty(value = "持续分钟数", required = true)
private Integer durationMinutes;
@ApiModelProperty(value = "生效开始日期", required = true)
private LocalDate effectiveStartDate;
@ApiModelProperty(value = "生效结束日期")
private LocalDate effectiveEndDate;
@ApiModelProperty(value = "时区", required = true)
private String timezone;
@ApiModelProperty(value = "店员A ID", required = true)
private String clerkAId;
@ApiModelProperty(value = "店员B ID", required = true)
private String clerkBId;
@ApiModelProperty(value = "状态", required = true)
private String status;
}

View File

@@ -0,0 +1,18 @@
package com.starry.admin.modules.pk.setting.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
@AllArgsConstructor
public enum PkRecurrenceType {
ONCE("ONCE", "单次"),
DAILY("DAILY", "每日"),
WEEKLY("WEEKLY", "每周"),
MONTHLY("MONTHLY", "每月"),
YEARLY("YEARLY", "每年");
@Getter
private final String value;
@Getter
private final String desc;
}

View File

@@ -0,0 +1,20 @@
package com.starry.admin.modules.pk.setting.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
@AllArgsConstructor
public enum PkScheduleDayOfWeek {
MONDAY("MONDAY", "周一"),
TUESDAY("TUESDAY", "周二"),
WEDNESDAY("WEDNESDAY", "周三"),
THURSDAY("THURSDAY", "周四"),
FRIDAY("FRIDAY", "周五"),
SATURDAY("SATURDAY", "周六"),
SUNDAY("SUNDAY", "周日");
@Getter
private final String value;
@Getter
private final String desc;
}

View File

@@ -0,0 +1,20 @@
package com.starry.admin.modules.pk.setting.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
@AllArgsConstructor
public enum PkSettingErrorCode {
NOT_IMPLEMENTED("PK_SETTING_NOT_IMPLEMENTED", "PK排期设置功能尚未实现"),
TENANT_MISSING("PK_SETTING_TENANT_MISSING", "租户信息不能为空"),
SETTING_NOT_FOUND("PK_SETTING_NOT_FOUND", "PK排期设置不存在"),
REQUEST_INVALID("PK_SETTING_REQUEST_INVALID", "PK排期设置参数非法"),
RECURRENCE_INVALID("PK_SETTING_RECURRENCE_INVALID", "PK排期循环规则非法"),
TIME_RANGE_INVALID("PK_SETTING_TIME_RANGE_INVALID", "PK排期时间范围非法"),
CLERK_CONFLICT("PK_SETTING_CLERK_CONFLICT", "店员排期冲突");
@Getter
private final String code;
@Getter
private final String message;
}

View File

@@ -0,0 +1,15 @@
package com.starry.admin.modules.pk.setting.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
@AllArgsConstructor
public enum PkSettingStatus {
ENABLED("ENABLED", "启用"),
DISABLED("DISABLED", "停用");
@Getter
private final String value;
@Getter
private final String desc;
}

View File

@@ -0,0 +1,7 @@
package com.starry.admin.modules.pk.setting.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.starry.admin.modules.pk.setting.entity.PlayClerkPkSettingEntity;
public interface PlayClerkPkSettingMapper extends BaseMapper<PlayClerkPkSettingEntity> {
}

View File

@@ -0,0 +1,27 @@
package com.starry.admin.modules.pk.setting.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.IService;
import com.starry.admin.modules.pk.setting.dto.PlayClerkPkSettingBulkCreateRequest;
import com.starry.admin.modules.pk.setting.dto.PlayClerkPkSettingUpsertRequest;
import com.starry.admin.modules.pk.setting.entity.PlayClerkPkSettingEntity;
import java.util.List;
public interface IPlayClerkPkSettingService extends IService<PlayClerkPkSettingEntity> {
String createSetting(PlayClerkPkSettingUpsertRequest request);
List<String> createSettings(PlayClerkPkSettingBulkCreateRequest request);
void updateSetting(String id, PlayClerkPkSettingUpsertRequest request);
void enableSetting(String id);
void disableSetting(String id);
PlayClerkPkSettingEntity getSetting(String id);
IPage<PlayClerkPkSettingEntity> listSettings();
int generateInstances(String id);
}

View File

@@ -0,0 +1,419 @@
package com.starry.admin.modules.pk.setting.service.impl;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.starry.admin.common.exception.CustomException;
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.redis.PkRedisKeyConstants;
import com.starry.admin.modules.pk.setting.constants.PkSettingValidationConstants;
import com.starry.admin.modules.pk.setting.dto.PlayClerkPkSettingBulkCreateRequest;
import com.starry.admin.modules.pk.setting.dto.PlayClerkPkSettingUpsertRequest;
import com.starry.admin.modules.pk.setting.entity.PlayClerkPkSettingEntity;
import com.starry.admin.modules.pk.setting.enums.PkRecurrenceType;
import com.starry.admin.modules.pk.setting.enums.PkScheduleDayOfWeek;
import com.starry.admin.modules.pk.setting.enums.PkSettingErrorCode;
import com.starry.admin.modules.pk.setting.enums.PkSettingStatus;
import com.starry.admin.modules.pk.setting.mapper.PlayClerkPkSettingMapper;
import com.starry.admin.modules.pk.setting.service.IPlayClerkPkSettingService;
import com.starry.admin.utils.SecurityUtils;
import com.starry.common.utils.IdUtils;
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.YearMonth;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class PlayClerkPkSettingServiceImpl
extends ServiceImpl<PlayClerkPkSettingMapper, PlayClerkPkSettingEntity>
implements IPlayClerkPkSettingService {
private final IPlayClerkPkService clerkPkService;
private final StringRedisTemplate stringRedisTemplate;
private static final String PAIR_KEY_SEPARATOR = "::";
public PlayClerkPkSettingServiceImpl(IPlayClerkPkService clerkPkService,
StringRedisTemplate stringRedisTemplate) {
this.clerkPkService = clerkPkService;
this.stringRedisTemplate = stringRedisTemplate;
}
@Override
@Transactional(rollbackFor = Exception.class)
public String createSetting(PlayClerkPkSettingUpsertRequest request) {
PlayClerkPkSettingEntity entity = buildEntity(request, null);
entity.setId(IdUtils.getUuid());
if (!save(entity)) {
throw new CustomException(PkSettingErrorCode.REQUEST_INVALID.getMessage());
}
if (PkSettingStatus.ENABLED.getValue().equals(entity.getStatus())) {
generateInstances(entity.getId());
}
return entity.getId();
}
@Override
@Transactional(rollbackFor = Exception.class)
public List<String> createSettings(PlayClerkPkSettingBulkCreateRequest request) {
List<PlayClerkPkSettingUpsertRequest> settings = validateBulkRequest(request);
List<String> createdIds = new ArrayList<>();
for (PlayClerkPkSettingUpsertRequest settingRequest : settings) {
PlayClerkPkSettingEntity entity = buildEntity(settingRequest, null);
entity.setId(IdUtils.getUuid());
if (!save(entity)) {
throw new CustomException(PkSettingErrorCode.REQUEST_INVALID.getMessage());
}
if (PkSettingStatus.ENABLED.getValue().equals(entity.getStatus())) {
generateInstances(entity.getId());
}
createdIds.add(entity.getId());
}
return createdIds;
}
@Override
@Transactional(rollbackFor = Exception.class)
public void updateSetting(String id, PlayClerkPkSettingUpsertRequest request) {
PlayClerkPkSettingEntity existing = getSetting(id);
PlayClerkPkSettingEntity entity = buildEntity(request, existing);
entity.setId(existing.getId());
entity.setTenantId(existing.getTenantId());
if (!updateById(entity)) {
throw new CustomException(PkSettingErrorCode.REQUEST_INVALID.getMessage());
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public void enableSetting(String id) {
PlayClerkPkSettingEntity setting = getSetting(id);
setting.setStatus(PkSettingStatus.ENABLED.getValue());
updateById(setting);
generateInstances(id);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void disableSetting(String id) {
PlayClerkPkSettingEntity setting = getSetting(id);
setting.setStatus(PkSettingStatus.DISABLED.getValue());
updateById(setting);
}
@Override
public PlayClerkPkSettingEntity getSetting(String id) {
if (StrUtil.isBlank(id)) {
throw new CustomException(PkSettingErrorCode.REQUEST_INVALID.getMessage());
}
PlayClerkPkSettingEntity setting = getById(id);
if (setting == null) {
throw new CustomException(PkSettingErrorCode.SETTING_NOT_FOUND.getMessage());
}
return setting;
}
@Override
public IPage<PlayClerkPkSettingEntity> listSettings() {
Page<PlayClerkPkSettingEntity> page = new Page<>(
PkSettingValidationConstants.DEFAULT_PAGE_NUMBER,
PkSettingValidationConstants.DEFAULT_PAGE_SIZE);
return baseMapper.selectPage(page, new LambdaQueryWrapper<>());
}
@Override
@Transactional(rollbackFor = Exception.class)
public int generateInstances(String id) {
PlayClerkPkSettingEntity setting = getSetting(id);
if (!PkSettingStatus.ENABLED.getValue().equals(setting.getStatus())) {
throw new CustomException(PkSettingErrorCode.REQUEST_INVALID.getMessage());
}
List<LocalDate> dates = generateScheduleDates(setting);
ZoneId zoneId = ZoneId.of(setting.getTimezone());
int createdCount = 0;
for (LocalDate date : dates) {
LocalDateTime startTime = LocalDateTime.of(date, setting.getStartTimeOfDay());
LocalDateTime endTime = startTime.plusMinutes(setting.getDurationMinutes());
Date beginTime = Date.from(startTime.atZone(zoneId).toInstant());
Date finishTime = Date.from(endTime.atZone(zoneId).toInstant());
if (existsForSettingAt(setting.getId(), beginTime)) {
continue;
}
if (hasClerkConflict(setting, beginTime, finishTime)) {
throw new CustomException(PkSettingErrorCode.CLERK_CONFLICT.getMessage());
}
PlayClerkPkEntity pk = new PlayClerkPkEntity();
pk.setId(IdUtils.getUuid());
pk.setTenantId(setting.getTenantId());
pk.setClerkA(setting.getClerkAId());
pk.setClerkB(setting.getClerkBId());
pk.setPkBeginTime(beginTime);
pk.setPkEndTime(finishTime);
pk.setStatus(ClerkPkEnum.TO_BE_STARTED.name());
pk.setSettled(0);
pk.setSettingId(setting.getId());
clerkPkService.save(pk);
scheduleStart(pk);
createdCount++;
}
return createdCount;
}
private PlayClerkPkSettingEntity buildEntity(PlayClerkPkSettingUpsertRequest request,
PlayClerkPkSettingEntity existing) {
validateRequest(request);
PlayClerkPkSettingEntity entity = Optional.ofNullable(existing).orElseGet(PlayClerkPkSettingEntity::new);
entity.setName(request.getName());
entity.setRecurrenceType(request.getRecurrenceType().getValue());
entity.setDayOfWeek(Optional.ofNullable(request.getDayOfWeek())
.map(PkScheduleDayOfWeek::getValue)
.orElse(null));
entity.setDayOfMonth(request.getDayOfMonth());
entity.setMonthOfYear(request.getMonthOfYear());
entity.setStartTimeOfDay(request.getStartTimeOfDay());
entity.setDurationMinutes(request.getDurationMinutes());
entity.setEffectiveStartDate(request.getEffectiveStartDate());
entity.setEffectiveEndDate(request.getEffectiveEndDate());
entity.setTimezone(request.getTimezone());
entity.setClerkAId(request.getClerkAId());
entity.setClerkBId(request.getClerkBId());
entity.setStatus(request.getStatus().getValue());
if (existing == null) {
entity.setTenantId(resolveTenantId());
}
return entity;
}
private void validateRequest(PlayClerkPkSettingUpsertRequest request) {
if (request == null) {
throw new CustomException(PkSettingErrorCode.REQUEST_INVALID.getMessage());
}
if (StrUtil.isBlank(request.getName())
|| request.getRecurrenceType() == null
|| request.getStartTimeOfDay() == null
|| request.getDurationMinutes() == null
|| request.getEffectiveStartDate() == null
|| request.getTimezone() == null
|| StrUtil.isBlank(request.getClerkAId())
|| StrUtil.isBlank(request.getClerkBId())
|| request.getStatus() == null) {
throw new CustomException(PkSettingErrorCode.REQUEST_INVALID.getMessage());
}
if (request.getDurationMinutes() < PkSettingValidationConstants.MIN_DURATION_MINUTES) {
throw new CustomException(PkSettingErrorCode.REQUEST_INVALID.getMessage());
}
if (request.getClerkAId().equals(request.getClerkBId())) {
throw new CustomException(PkSettingErrorCode.REQUEST_INVALID.getMessage());
}
ensureZoneValid(request.getTimezone());
validateRecurrenceFields(request);
validateEffectiveRange(request.getEffectiveStartDate(), request.getEffectiveEndDate());
}
private void scheduleStart(PlayClerkPkEntity pk) {
if (pk == null || pk.getPkBeginTime() == null || pk.getId() == null) {
throw new CustomException(PkSettingErrorCode.REQUEST_INVALID.getMessage());
}
if (StrUtil.isBlank(pk.getTenantId())) {
throw new CustomException(PkSettingErrorCode.TENANT_MISSING.getMessage());
}
String scheduleKey = PkRedisKeyConstants.startScheduleKey(pk.getTenantId());
long startEpochSeconds = pk.getPkBeginTime().toInstant().getEpochSecond();
stringRedisTemplate.opsForZSet().add(scheduleKey, pk.getId(), startEpochSeconds);
}
private List<PlayClerkPkSettingUpsertRequest> validateBulkRequest(PlayClerkPkSettingBulkCreateRequest request) {
if (request == null || request.getSettings() == null || request.getSettings().isEmpty()) {
throw new CustomException(PkSettingErrorCode.REQUEST_INVALID.getMessage());
}
Set<String> pairKeys = new HashSet<>();
for (PlayClerkPkSettingUpsertRequest setting : request.getSettings()) {
validateRequest(setting);
String pairKey = buildPairKey(setting.getClerkAId(), setting.getClerkBId());
if (!pairKeys.add(pairKey)) {
throw new CustomException(PkSettingErrorCode.REQUEST_INVALID.getMessage());
}
}
return request.getSettings();
}
private String buildPairKey(String clerkAId, String clerkBId) {
if (clerkAId.compareTo(clerkBId) <= 0) {
return clerkAId + PAIR_KEY_SEPARATOR + clerkBId;
}
return clerkBId + PAIR_KEY_SEPARATOR + clerkAId;
}
private void ensureZoneValid(String timezone) {
try {
ZoneId.of(timezone);
} catch (Exception ex) {
throw new CustomException(PkSettingErrorCode.REQUEST_INVALID.getMessage());
}
}
private void validateRecurrenceFields(PlayClerkPkSettingUpsertRequest request) {
PkRecurrenceType type = request.getRecurrenceType();
if (type == PkRecurrenceType.WEEKLY && request.getDayOfWeek() == null) {
throw new CustomException(PkSettingErrorCode.RECURRENCE_INVALID.getMessage());
}
if (type == PkRecurrenceType.MONTHLY && request.getDayOfMonth() == null) {
throw new CustomException(PkSettingErrorCode.RECURRENCE_INVALID.getMessage());
}
if (type == PkRecurrenceType.YEARLY
&& (request.getDayOfMonth() == null || request.getMonthOfYear() == null)) {
throw new CustomException(PkSettingErrorCode.RECURRENCE_INVALID.getMessage());
}
if (request.getDayOfMonth() != null) {
int dayOfMonth = request.getDayOfMonth();
if (dayOfMonth < PkSettingValidationConstants.MIN_DAY_OF_MONTH
|| dayOfMonth > PkSettingValidationConstants.MAX_DAY_OF_MONTH) {
throw new CustomException(PkSettingErrorCode.RECURRENCE_INVALID.getMessage());
}
}
if (request.getMonthOfYear() != null) {
int monthOfYear = request.getMonthOfYear();
if (monthOfYear < PkSettingValidationConstants.MIN_MONTH_OF_YEAR
|| monthOfYear > PkSettingValidationConstants.MAX_MONTH_OF_YEAR) {
throw new CustomException(PkSettingErrorCode.RECURRENCE_INVALID.getMessage());
}
}
}
private void validateEffectiveRange(LocalDate start, LocalDate end) {
if (end != null && end.isBefore(start)) {
throw new CustomException(PkSettingErrorCode.TIME_RANGE_INVALID.getMessage());
}
}
private List<LocalDate> generateScheduleDates(PlayClerkPkSettingEntity setting) {
LocalDate start = setting.getEffectiveStartDate();
LocalDate maxEnd = start.plusYears(PkSettingValidationConstants.MAX_YEARS_AHEAD);
LocalDate end = Optional.ofNullable(setting.getEffectiveEndDate()).orElse(maxEnd);
if (end.isAfter(maxEnd)) {
end = maxEnd;
}
PkRecurrenceType type = PkRecurrenceType.valueOf(setting.getRecurrenceType());
List<LocalDate> dates = new ArrayList<>();
switch (type) {
case ONCE:
if (!start.isAfter(end)) {
dates.add(start);
}
break;
case DAILY:
for (LocalDate cursor = start; !cursor.isAfter(end); cursor = cursor.plusDays(1)) {
dates.add(cursor);
}
break;
case WEEKLY:
dates.addAll(generateWeeklyDates(setting, start, end));
break;
case MONTHLY:
dates.addAll(generateMonthlyDates(setting, start, end));
break;
case YEARLY:
dates.addAll(generateYearlyDates(setting, start, end));
break;
default:
throw new CustomException(PkSettingErrorCode.RECURRENCE_INVALID.getMessage());
}
return dates;
}
private List<LocalDate> generateWeeklyDates(PlayClerkPkSettingEntity setting, LocalDate start, LocalDate end) {
List<LocalDate> dates = new ArrayList<>();
DayOfWeek target = DayOfWeek.valueOf(setting.getDayOfWeek());
LocalDate cursor = start;
while (cursor.getDayOfWeek() != target) {
cursor = cursor.plusDays(1);
}
for (LocalDate date = cursor; !date.isAfter(end); date = date.plusWeeks(1)) {
dates.add(date);
}
return dates;
}
private List<LocalDate> generateMonthlyDates(PlayClerkPkSettingEntity setting, LocalDate start, LocalDate end) {
List<LocalDate> dates = new ArrayList<>();
int dayOfMonth = setting.getDayOfMonth();
YearMonth startMonth = YearMonth.from(start);
YearMonth endMonth = YearMonth.from(end);
for (YearMonth cursor = startMonth; !cursor.isAfter(endMonth); cursor = cursor.plusMonths(1)) {
if (dayOfMonth > cursor.lengthOfMonth()) {
continue;
}
LocalDate date = cursor.atDay(dayOfMonth);
if (date.isBefore(start) || date.isAfter(end)) {
continue;
}
dates.add(date);
}
return dates;
}
private List<LocalDate> generateYearlyDates(PlayClerkPkSettingEntity setting, LocalDate start, LocalDate end) {
List<LocalDate> dates = new ArrayList<>();
int dayOfMonth = setting.getDayOfMonth();
int monthOfYear = setting.getMonthOfYear();
for (int year = start.getYear(); year <= end.getYear(); year++) {
YearMonth yearMonth = YearMonth.of(year, monthOfYear);
if (dayOfMonth > yearMonth.lengthOfMonth()) {
continue;
}
LocalDate date = LocalDate.of(year, monthOfYear, dayOfMonth);
if (date.isBefore(start) || date.isAfter(end)) {
continue;
}
dates.add(date);
}
return dates;
}
private boolean existsForSettingAt(String settingId, Date beginTime) {
LambdaQueryWrapper<PlayClerkPkEntity> wrapper = Wrappers.lambdaQuery(PlayClerkPkEntity.class)
.eq(PlayClerkPkEntity::getSettingId, settingId)
.eq(PlayClerkPkEntity::getPkBeginTime, beginTime);
return clerkPkService.count(wrapper) > 0;
}
private boolean hasClerkConflict(PlayClerkPkSettingEntity setting, Date beginTime, Date endTime) {
LambdaQueryWrapper<PlayClerkPkEntity> wrapper = Wrappers.lambdaQuery(PlayClerkPkEntity.class)
.in(PlayClerkPkEntity::getStatus,
ClerkPkEnum.TO_BE_STARTED.name(),
ClerkPkEnum.IN_PROGRESS.name())
.and(time -> time.le(PlayClerkPkEntity::getPkBeginTime, endTime)
.ge(PlayClerkPkEntity::getPkEndTime, beginTime))
.and(clerk -> clerk.eq(PlayClerkPkEntity::getClerkA, setting.getClerkAId())
.or()
.eq(PlayClerkPkEntity::getClerkB, setting.getClerkAId())
.or()
.eq(PlayClerkPkEntity::getClerkA, setting.getClerkBId())
.or()
.eq(PlayClerkPkEntity::getClerkB, setting.getClerkBId()));
return clerkPkService.count(wrapper) > 0;
}
private String resolveTenantId() {
String tenantId = SecurityUtils.getTenantId();
if (StrUtil.isBlank(tenantId)) {
throw new CustomException(PkSettingErrorCode.TENANT_MISSING.getMessage());
}
return tenantId;
}
}

View File

@@ -17,6 +17,7 @@ import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiResponse; import io.swagger.annotations.ApiResponse;
import io.swagger.annotations.ApiResponses; import io.swagger.annotations.ApiResponses;
import java.util.ArrayList;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@@ -40,6 +41,9 @@ import org.springframework.web.bind.annotation.RestController;
@RequestMapping("/wx/commodity/") @RequestMapping("/wx/commodity/")
public class WxClerkCommodityController { public class WxClerkCommodityController {
private static final String ROOT_PARENT_ID = "00";
private static final String CLERK_COMMODITY_ENABLED = "1";
@Resource @Resource
private IPlayCommodityInfoService playCommodityInfoService; private IPlayCommodityInfoService playCommodityInfoService;
@@ -63,6 +67,12 @@ public class WxClerkCommodityController {
if (levelId == null || levelId.isEmpty()) { if (levelId == null || levelId.isEmpty()) {
return R.ok(tree); return R.ok(tree);
} }
if (levelInfoEntities == null) {
throw new CustomException("商品等级信息缺失");
}
if (tree == null) {
throw new CustomException("商品树缺失");
}
tree = formatPlayCommodityReturnVoTree(tree, levelInfoEntities, levelId); tree = formatPlayCommodityReturnVoTree(tree, levelInfoEntities, levelId);
tree = formatPlayCommodityReturnVoTree(tree, null); tree = formatPlayCommodityReturnVoTree(tree, null);
return R.ok(tree); return R.ok(tree);
@@ -84,11 +94,23 @@ public class WxClerkCommodityController {
throw new CustomException("请求参数异常,id不能为空"); throw new CustomException("请求参数异常,id不能为空");
} }
PlayClerkUserInfoEntity clerkUserInfo = clerkUserInfoService.selectById(clerkId); PlayClerkUserInfoEntity clerkUserInfo = clerkUserInfoService.selectById(clerkId);
if (clerkUserInfo == null) {
throw new CustomException("店员不存在");
}
if (clerkUserInfo.getLevelId() == null || clerkUserInfo.getLevelId().isEmpty()) {
throw new CustomException("店员等级信息缺失");
}
Map<String, List<PlayClerkCommodityEntity>> clerkCommodityEntities = playClerkCommodityService Map<String, List<PlayClerkCommodityEntity>> clerkCommodityEntities = playClerkCommodityService
.selectCommodityTypeByUser(clerkId, "1").stream() .selectCommodityTypeByUser(clerkId, CLERK_COMMODITY_ENABLED).stream()
.collect(Collectors.groupingBy(PlayClerkCommodityEntity::getCommodityId)); .collect(Collectors.groupingBy(PlayClerkCommodityEntity::getCommodityId));
List<PlayCommodityAndLevelInfoEntity> levelInfoEntities = iPlayCommodityAndLevelInfoService.selectAll(); List<PlayCommodityAndLevelInfoEntity> levelInfoEntities = iPlayCommodityAndLevelInfoService.selectAll();
List<PlayCommodityReturnVo> tree = playCommodityInfoService.selectTree(); List<PlayCommodityReturnVo> tree = playCommodityInfoService.selectTree();
if (levelInfoEntities == null) {
throw new CustomException("商品等级信息缺失");
}
if (tree == null) {
throw new CustomException("商品树缺失");
}
tree = formatPlayCommodityReturnVoTree(tree, levelInfoEntities, clerkUserInfo.getLevelId()); tree = formatPlayCommodityReturnVoTree(tree, levelInfoEntities, clerkUserInfo.getLevelId());
tree = formatPlayCommodityReturnVoTree(tree, clerkCommodityEntities); tree = formatPlayCommodityReturnVoTree(tree, clerkCommodityEntities);
return R.ok(tree); return R.ok(tree);
@@ -108,10 +130,16 @@ public class WxClerkCommodityController {
String levelId = ThreadLocalRequestDetail.getClerkUserInfo().getLevelId(); String levelId = ThreadLocalRequestDetail.getClerkUserInfo().getLevelId();
List<PlayCommodityAndLevelInfoEntity> levelInfoEntities = iPlayCommodityAndLevelInfoService.selectAll(); List<PlayCommodityAndLevelInfoEntity> levelInfoEntities = iPlayCommodityAndLevelInfoService.selectAll();
Map<String, List<PlayClerkCommodityEntity>> clerkCommodityEntities = playClerkCommodityService Map<String, List<PlayClerkCommodityEntity>> clerkCommodityEntities = playClerkCommodityService
.selectCommodityTypeByUser(ThreadLocalRequestDetail.getClerkUserInfo().getId(), "1").stream() .selectCommodityTypeByUser(ThreadLocalRequestDetail.getClerkUserInfo().getId(), CLERK_COMMODITY_ENABLED).stream()
.collect(Collectors.groupingBy(PlayClerkCommodityEntity::getCommodityId)); .collect(Collectors.groupingBy(PlayClerkCommodityEntity::getCommodityId));
List<PlayCommodityReturnVo> tree = playCommodityInfoService.selectTree(); List<PlayCommodityReturnVo> tree = playCommodityInfoService.selectTree();
if (levelInfoEntities == null) {
throw new CustomException("商品等级信息缺失");
}
if (tree == null) {
throw new CustomException("商品树缺失");
}
tree = formatPlayCommodityReturnVoTree(tree, levelInfoEntities, levelId); tree = formatPlayCommodityReturnVoTree(tree, levelInfoEntities, levelId);
tree = formatPlayCommodityReturnVoTree(tree, clerkCommodityEntities); tree = formatPlayCommodityReturnVoTree(tree, clerkCommodityEntities);
return R.ok(tree); return R.ok(tree);
@@ -119,9 +147,21 @@ public class WxClerkCommodityController {
public List<PlayCommodityReturnVo> formatPlayCommodityReturnVoTree(List<PlayCommodityReturnVo> tree, public List<PlayCommodityReturnVo> formatPlayCommodityReturnVoTree(List<PlayCommodityReturnVo> tree,
List<PlayCommodityAndLevelInfoEntity> levelInfoEntities, String levelId) { List<PlayCommodityAndLevelInfoEntity> levelInfoEntities, String levelId) {
if (tree == null) {
throw new CustomException("商品树缺失");
}
if (levelInfoEntities == null) {
throw new CustomException("商品等级信息缺失");
}
if (levelId == null || levelId.isEmpty()) {
throw new CustomException("等级信息缺失");
}
Iterator<PlayCommodityReturnVo> it = tree.iterator(); Iterator<PlayCommodityReturnVo> it = tree.iterator();
while (it.hasNext()) { while (it.hasNext()) {
PlayCommodityReturnVo item = it.next(); PlayCommodityReturnVo item = it.next();
if (item.getChild() == null) {
item.setChild(new ArrayList<>());
}
// 查找当前服务项目对应的价格 // 查找当前服务项目对应的价格
for (PlayCommodityAndLevelInfoEntity levelInfoEntity : levelInfoEntities) { for (PlayCommodityAndLevelInfoEntity levelInfoEntity : levelInfoEntities) {
if (item.getId().equals(levelInfoEntity.getCommodityId()) if (item.getId().equals(levelInfoEntity.getCommodityId())
@@ -130,7 +170,7 @@ public class WxClerkCommodityController {
} }
} }
// 如果未设置价格,删除元素 // 如果未设置价格,删除元素
if (!"00".equals(item.getPId()) && item.getPrice() == null) { if (!ROOT_PARENT_ID.equals(item.getPId()) && item.getPrice() == null) {
it.remove(); it.remove();
} }
formatPlayCommodityReturnVoTree(item.getChild(), levelInfoEntities, levelId); formatPlayCommodityReturnVoTree(item.getChild(), levelInfoEntities, levelId);
@@ -140,12 +180,18 @@ public class WxClerkCommodityController {
public List<PlayCommodityReturnVo> formatPlayCommodityReturnVoTree(List<PlayCommodityReturnVo> tree, public List<PlayCommodityReturnVo> formatPlayCommodityReturnVoTree(List<PlayCommodityReturnVo> tree,
Map<String, List<PlayClerkCommodityEntity>> clerkCommodityEntities) { Map<String, List<PlayClerkCommodityEntity>> clerkCommodityEntities) {
if (tree == null) {
throw new CustomException("商品树缺失");
}
Iterator<PlayCommodityReturnVo> it = tree.iterator(); Iterator<PlayCommodityReturnVo> it = tree.iterator();
while (it.hasNext()) { while (it.hasNext()) {
PlayCommodityReturnVo item = it.next(); PlayCommodityReturnVo item = it.next();
if ("00".equals(item.getPId()) && item.getChild().isEmpty()) { if (item.getChild() == null) {
item.setChild(new ArrayList<>());
}
if (ROOT_PARENT_ID.equals(item.getPId()) && item.getChild().isEmpty()) {
it.remove(); it.remove();
} else if (clerkCommodityEntities != null && "00".equals(item.getPId()) } else if (clerkCommodityEntities != null && ROOT_PARENT_ID.equals(item.getPId())
&& !clerkCommodityEntities.containsKey(item.getId())) { && !clerkCommodityEntities.containsKey(item.getId())) {
it.remove(); it.remove();
} }

View File

@@ -0,0 +1,279 @@
package com.starry.admin.modules.weichat.controller;
import cn.hutool.core.util.StrUtil;
import com.starry.admin.common.exception.CustomException;
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.pk.constants.PkWxQueryConstants;
import com.starry.admin.modules.pk.dto.PkScoreBoardDto;
import com.starry.admin.modules.pk.dto.WxPkClerkHistoryPageDto;
import com.starry.admin.modules.pk.dto.WxPkDetailDto;
import com.starry.admin.modules.pk.dto.WxPkLiveDto;
import com.starry.admin.modules.pk.dto.WxPkUpcomingDto;
import com.starry.admin.modules.pk.enums.PkWxState;
import com.starry.admin.modules.pk.redis.PkRedisKeyConstants;
import com.starry.admin.modules.pk.service.IPkDetailService;
import com.starry.admin.modules.pk.service.IPkScoreboardService;
import com.starry.admin.utils.SecurityUtils;
import com.starry.common.result.R;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import java.time.Instant;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@Api(tags = "微信PK接口", description = "微信端PK展示相关接口")
@RestController
@RequestMapping("/wx/pk")
public class WxPkController {
private final IPlayClerkPkService clerkPkService;
private final IPlayClerkUserInfoService clerkUserInfoService;
private final IPkScoreboardService pkScoreboardService;
private final IPkDetailService pkDetailService;
private final StringRedisTemplate stringRedisTemplate;
public WxPkController(IPlayClerkPkService clerkPkService,
IPlayClerkUserInfoService clerkUserInfoService,
IPkScoreboardService pkScoreboardService,
IPkDetailService pkDetailService,
StringRedisTemplate stringRedisTemplate) {
this.clerkPkService = clerkPkService;
this.clerkUserInfoService = clerkUserInfoService;
this.pkScoreboardService = pkScoreboardService;
this.pkDetailService = pkDetailService;
this.stringRedisTemplate = stringRedisTemplate;
}
@ApiOperation(value = "店员PK实时状态", notes = "查询指定店员是否正在PK")
@GetMapping("/clerk/live")
public R getClerkLive(@RequestParam("clerkId") String clerkId) {
if (StrUtil.isBlank(clerkId)) {
throw new CustomException("店员ID不能为空");
}
Optional<PlayClerkPkEntity> pkOptional =
clerkPkService.findActivePkForClerk(clerkId, LocalDateTime.now());
if (!pkOptional.isPresent()) {
return R.ok(WxPkLiveDto.inactive());
}
PlayClerkPkEntity pk = pkOptional.get();
if (!ClerkPkEnum.IN_PROGRESS.name().equals(pk.getStatus())) {
return R.ok(WxPkLiveDto.inactive());
}
PkScoreBoardDto scoreboard = pkScoreboardService.getScoreboard(pk.getId());
return R.ok(buildLiveDto(pk, scoreboard));
}
@ApiOperation(value = "即将开战PK", notes = "返回即将开始的PK信息")
@GetMapping("/upcoming")
public R getUpcoming() {
String tenantId = SecurityUtils.getTenantId();
if (StrUtil.isBlank(tenantId)) {
throw new CustomException("租户信息缺失");
}
String key = PkRedisKeyConstants.upcomingKey(tenantId);
long nowEpochSeconds = Instant.now().getEpochSecond();
Set<String> pkIds = stringRedisTemplate.opsForZSet()
.rangeByScore(key, nowEpochSeconds, Double.POSITIVE_INFINITY, 0, 1);
if (pkIds == null || pkIds.isEmpty()) {
return R.ok(WxPkUpcomingDto.inactive());
}
String pkId = pkIds.iterator().next();
PlayClerkPkEntity pk = clerkPkService.selectPlayClerkPkById(pkId);
if (pk == null) {
return R.ok(WxPkUpcomingDto.inactive());
}
return R.ok(buildUpcomingDto(pk));
}
@ApiOperation(value = "PK详情", notes = "查询PK详情用于详情页")
@GetMapping("/detail")
public R getDetail(@RequestParam("id") String id) {
if (StrUtil.isBlank(id)) {
throw new CustomException("PK ID不能为空");
}
PlayClerkPkEntity pk = clerkPkService.selectPlayClerkPkById(id);
if (pk == null) {
return R.ok(WxPkDetailDto.inactive());
}
PkScoreBoardDto scoreboard = pkScoreboardService.getScoreboard(pk.getId());
return R.ok(buildDetailDto(pk, scoreboard));
}
@ApiOperation(value = "店员PK历史", notes = "查询指定店员PK历史")
@GetMapping("/clerk/history")
public R getClerkHistory(@RequestParam("clerkId") String clerkId,
@RequestParam(value = "pageNum", required = false) Integer pageNum,
@RequestParam(value = "pageSize", required = false) Integer pageSize) {
if (StrUtil.isBlank(clerkId)) {
throw new CustomException("店员ID不能为空");
}
int safePageNum = pageNum == null ? PkWxQueryConstants.CLERK_HISTORY_PAGE_NUM : pageNum;
int safePageSize = pageSize == null ? PkWxQueryConstants.CLERK_HISTORY_PAGE_SIZE : pageSize;
WxPkClerkHistoryPageDto data = pkDetailService.getClerkHistory(clerkId, safePageNum, safePageSize);
return R.ok(data);
}
@ApiOperation(value = "店员PK日程", notes = "查询指定店员未来PK安排")
@GetMapping("/clerk/schedule")
public R getClerkSchedule(@RequestParam("clerkId") String clerkId,
@RequestParam(value = "limit", required = false) Integer limit) {
if (StrUtil.isBlank(clerkId)) {
throw new CustomException("店员ID不能为空");
}
String tenantId = SecurityUtils.getTenantId();
if (StrUtil.isBlank(tenantId)) {
throw new CustomException("租户信息缺失");
}
int safeLimit = normalizeLimit(limit);
List<PlayClerkPkEntity> items = clerkPkService.selectUpcomingForClerk(
tenantId,
clerkId,
new Date(),
safeLimit);
if (items == null || items.isEmpty()) {
return R.ok(new ArrayList<>());
}
List<WxPkUpcomingDto> result = new ArrayList<>();
for (PlayClerkPkEntity pk : items) {
result.add(buildUpcomingDto(pk));
}
return R.ok(result);
}
private WxPkLiveDto buildLiveDto(PlayClerkPkEntity pk, PkScoreBoardDto scoreboard) {
WxPkLiveDto dto = new WxPkLiveDto();
fillBase(dto, pk);
dto.setState(PkWxState.ACTIVE.getValue());
dto.setClerkAScore(scoreboard.getClerkAScore());
dto.setClerkBScore(scoreboard.getClerkBScore());
dto.setClerkAOrderCount(scoreboard.getClerkAOrderCount());
dto.setClerkBOrderCount(scoreboard.getClerkBOrderCount());
dto.setRemainingSeconds(scoreboard.getRemainingSeconds());
dto.setServerEpochSeconds(Instant.now().getEpochSecond());
if (pk.getPkEndTime() != null) {
dto.setPkEndEpochSeconds(pk.getPkEndTime().toInstant().getEpochSecond());
}
return dto;
}
private WxPkUpcomingDto buildUpcomingDto(PlayClerkPkEntity pk) {
WxPkUpcomingDto dto = new WxPkUpcomingDto();
fillBase(dto, pk);
dto.setState(PkWxState.UPCOMING.getValue());
dto.setPkBeginTime(pk.getPkBeginTime());
return dto;
}
private WxPkDetailDto buildDetailDto(PlayClerkPkEntity pk, PkScoreBoardDto scoreboard) {
WxPkDetailDto dto = new WxPkDetailDto();
fillBase(dto, pk);
dto.setState(resolveState(pk.getStatus()));
dto.setClerkAScore(scoreboard.getClerkAScore());
dto.setClerkBScore(scoreboard.getClerkBScore());
dto.setClerkAOrderCount(scoreboard.getClerkAOrderCount());
dto.setClerkBOrderCount(scoreboard.getClerkBOrderCount());
dto.setRemainingSeconds(scoreboard.getRemainingSeconds());
dto.setPkBeginTime(pk.getPkBeginTime());
dto.setPkEndTime(pk.getPkEndTime());
dto.setContributors(pkDetailService.getContributors(pk));
dto.setHistory(pkDetailService.getHistory(pk));
return dto;
}
private int normalizeLimit(Integer limit) {
if (limit == null) {
return PkWxQueryConstants.CLERK_SCHEDULE_DEFAULT_LIMIT;
}
if (limit < PkWxQueryConstants.CLERK_SCHEDULE_MIN_LIMIT) {
return PkWxQueryConstants.CLERK_SCHEDULE_MIN_LIMIT;
}
if (limit > PkWxQueryConstants.CLERK_SCHEDULE_MAX_LIMIT) {
return PkWxQueryConstants.CLERK_SCHEDULE_MAX_LIMIT;
}
return limit;
}
private String resolveState(String status) {
if (ClerkPkEnum.IN_PROGRESS.name().equals(status)) {
return PkWxState.ACTIVE.getValue();
}
if (ClerkPkEnum.TO_BE_STARTED.name().equals(status)) {
return PkWxState.UPCOMING.getValue();
}
return PkWxState.INACTIVE.getValue();
}
private void fillBase(WxPkLiveDto dto, PlayClerkPkEntity pk) {
dto.setId(pk.getId());
dto.setClerkAId(pk.getClerkA());
dto.setClerkBId(pk.getClerkB());
fillClerkInfo(dto);
}
private void fillBase(WxPkUpcomingDto dto, PlayClerkPkEntity pk) {
dto.setId(pk.getId());
dto.setClerkAId(pk.getClerkA());
dto.setClerkBId(pk.getClerkB());
fillClerkInfo(dto);
}
private void fillBase(WxPkDetailDto dto, PlayClerkPkEntity pk) {
dto.setId(pk.getId());
dto.setClerkAId(pk.getClerkA());
dto.setClerkBId(pk.getClerkB());
fillClerkInfo(dto);
}
private void fillClerkInfo(WxPkLiveDto dto) {
fillClerkInfo(dto.getClerkAId(), dto.getClerkBId(), dto);
}
private void fillClerkInfo(WxPkUpcomingDto dto) {
fillClerkInfo(dto.getClerkAId(), dto.getClerkBId(), dto);
}
private void fillClerkInfo(WxPkDetailDto dto) {
fillClerkInfo(dto.getClerkAId(), dto.getClerkBId(), dto);
}
private void fillClerkInfo(String clerkAId, String clerkBId, Object target) {
PlayClerkUserInfoEntity clerkA = clerkUserInfoService.getById(clerkAId);
PlayClerkUserInfoEntity clerkB = clerkUserInfoService.getById(clerkBId);
if (target instanceof WxPkLiveDto) {
WxPkLiveDto dto = (WxPkLiveDto) target;
dto.setClerkAName(clerkA == null ? "" : clerkA.getNickname());
dto.setClerkBName(clerkB == null ? "" : clerkB.getNickname());
dto.setClerkAAvatar(clerkA == null ? "" : clerkA.getAvatar());
dto.setClerkBAvatar(clerkB == null ? "" : clerkB.getAvatar());
return;
}
if (target instanceof WxPkUpcomingDto) {
WxPkUpcomingDto dto = (WxPkUpcomingDto) target;
dto.setClerkAName(clerkA == null ? "" : clerkA.getNickname());
dto.setClerkBName(clerkB == null ? "" : clerkB.getNickname());
dto.setClerkAAvatar(clerkA == null ? "" : clerkA.getAvatar());
dto.setClerkBAvatar(clerkB == null ? "" : clerkB.getAvatar());
return;
}
if (target instanceof WxPkDetailDto) {
WxPkDetailDto dto = (WxPkDetailDto) target;
dto.setClerkAName(clerkA == null ? "" : clerkA.getNickname());
dto.setClerkBName(clerkB == null ? "" : clerkB.getNickname());
dto.setClerkAAvatar(clerkA == null ? "" : clerkA.getAvatar());
dto.setClerkBAvatar(clerkB == null ? "" : clerkB.getAvatar());
}
}
}

View File

@@ -0,0 +1,19 @@
package com.starry.admin.utils;
public final class TenantScope implements AutoCloseable {
private final String previousTenantId;
private TenantScope(String tenantId) {
this.previousTenantId = SecurityUtils.getTenantId();
SecurityUtils.setTenantId(tenantId);
}
public static TenantScope use(String tenantId) {
return new TenantScope(tenantId);
}
@Override
public void close() {
SecurityUtils.setTenantId(previousTenantId);
}
}

View File

@@ -0,0 +1,26 @@
CREATE TABLE `play_clerk_pk_settings` (
`id` varchar(32) NOT NULL COMMENT 'UUID',
`tenant_id` varchar(32) NOT NULL COMMENT '租户ID',
`name` varchar(255) NOT NULL COMMENT '设置名称',
`recurrence_type` varchar(32) NOT NULL COMMENT '循环类型',
`day_of_week` varchar(16) DEFAULT NULL COMMENT '星期几',
`day_of_month` int DEFAULT NULL COMMENT '每月第N天',
`month_of_year` int DEFAULT NULL COMMENT '每年月份',
`start_time_of_day` time NOT NULL COMMENT '每日开始时间',
`duration_minutes` int NOT NULL COMMENT '持续分钟数',
`effective_start_date` date NOT NULL COMMENT '生效开始日期',
`effective_end_date` date DEFAULT NULL COMMENT '生效结束日期',
`timezone` varchar(64) NOT NULL COMMENT '时区',
`clerk_a_id` varchar(255) NOT NULL COMMENT '店员A ID',
`clerk_b_id` varchar(255) NOT NULL COMMENT '店员B ID',
`status` varchar(32) NOT NULL COMMENT '状态',
`created_by` varchar(32) DEFAULT NULL COMMENT '创建人的id',
`created_time` datetime DEFAULT NULL COMMENT '创建时间',
`updated_by` varchar(32) DEFAULT NULL COMMENT '修改人的id',
`updated_time` datetime DEFAULT NULL COMMENT '修改时间',
`deleted` varchar(1) NOT NULL DEFAULT '0' COMMENT '逻辑删除 1已删除 0未删除',
`version` int NOT NULL DEFAULT '1' COMMENT '数据版本',
PRIMARY KEY (`id`) USING BTREE,
KEY `key_tenant_id` (`tenant_id`) USING BTREE,
KEY `idx_pk_settings_status` (`status`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='店员PK排期设置';

View File

@@ -0,0 +1,3 @@
ALTER TABLE `play_clerk_pk`
ADD COLUMN `setting_id` varchar(32) DEFAULT NULL COMMENT 'PK排期设置ID' AFTER `status`,
ADD KEY `idx_pk_setting_id` (`setting_id`);

View File

@@ -0,0 +1,288 @@
-- Add PK management menus for PC tenant.
SET @pk_root_id := (
SELECT menu_id
FROM sys_menu
WHERE menu_name = 'PK管理' AND parent_id = 0 AND deleted = 0
LIMIT 1
);
INSERT INTO sys_menu (
menu_name,
menu_code,
icon,
permission,
menu_level,
sort,
parent_id,
menu_type,
status,
remark,
path,
component,
router_query,
is_frame,
visible,
updated_time,
updated_by,
created_time,
created_by,
deleted,
version,
perms
)
SELECT
'PK管理',
'PkManage',
'el-icon-trophy',
'',
1,
90,
0,
0,
0,
'PK管理',
'play/pk',
'Layout',
'',
0,
1,
NOW(),
'migration_v22_pk_menu',
NOW(),
'migration_v22_pk_menu',
0,
1,
''
WHERE @pk_root_id IS NULL;
SET @pk_root_id := (
SELECT menu_id
FROM sys_menu
WHERE menu_name = 'PK管理' AND parent_id = 0 AND deleted = 0
LIMIT 1
);
SET @pk_schedule_id := (
SELECT menu_id
FROM sys_menu
WHERE menu_name = '排期管理' AND parent_id = @pk_root_id AND deleted = 0
LIMIT 1
);
INSERT INTO sys_menu (
menu_name,
menu_code,
icon,
permission,
menu_level,
sort,
parent_id,
menu_type,
status,
remark,
path,
component,
router_query,
is_frame,
visible,
updated_time,
updated_by,
created_time,
created_by,
deleted,
version,
perms
)
SELECT
'排期管理',
'PkSchedule',
'el-icon-date',
'play:pk:schedule:list',
2,
1,
@pk_root_id,
1,
0,
'排期管理',
'schedule',
'play/pk/schedule/index',
'',
0,
1,
NOW(),
'migration_v22_pk_menu',
NOW(),
'migration_v22_pk_menu',
0,
1,
''
WHERE @pk_root_id IS NOT NULL AND @pk_schedule_id IS NULL;
SET @pk_live_id := (
SELECT menu_id
FROM sys_menu
WHERE menu_name = '实时监控' AND parent_id = @pk_root_id AND deleted = 0
LIMIT 1
);
INSERT INTO sys_menu (
menu_name,
menu_code,
icon,
permission,
menu_level,
sort,
parent_id,
menu_type,
status,
remark,
path,
component,
router_query,
is_frame,
visible,
updated_time,
updated_by,
created_time,
created_by,
deleted,
version,
perms
)
SELECT
'实时监控',
'PkLive',
'el-icon-video-play',
'play:pk:live:list',
2,
2,
@pk_root_id,
1,
0,
'实时监控',
'live',
'play/pk/live/index',
'',
0,
1,
NOW(),
'migration_v22_pk_menu',
NOW(),
'migration_v22_pk_menu',
0,
1,
''
WHERE @pk_root_id IS NOT NULL AND @pk_live_id IS NULL;
SET @pk_history_id := (
SELECT menu_id
FROM sys_menu
WHERE menu_name = '战绩列表' AND parent_id = @pk_root_id AND deleted = 0
LIMIT 1
);
INSERT INTO sys_menu (
menu_name,
menu_code,
icon,
permission,
menu_level,
sort,
parent_id,
menu_type,
status,
remark,
path,
component,
router_query,
is_frame,
visible,
updated_time,
updated_by,
created_time,
created_by,
deleted,
version,
perms
)
SELECT
'战绩列表',
'PkHistory',
'el-icon-data-analysis',
'play:pk:history:list',
2,
3,
@pk_root_id,
1,
0,
'战绩列表',
'history',
'play/pk/history/index',
'',
0,
1,
NOW(),
'migration_v22_pk_menu',
NOW(),
'migration_v22_pk_menu',
0,
1,
''
WHERE @pk_root_id IS NOT NULL AND @pk_history_id IS NULL;
SET @pk_settings_id := (
SELECT menu_id
FROM sys_menu
WHERE menu_name = '规则设置' AND parent_id = @pk_root_id AND deleted = 0
LIMIT 1
);
INSERT INTO sys_menu (
menu_name,
menu_code,
icon,
permission,
menu_level,
sort,
parent_id,
menu_type,
status,
remark,
path,
component,
router_query,
is_frame,
visible,
updated_time,
updated_by,
created_time,
created_by,
deleted,
version,
perms
)
SELECT
'规则设置',
'PkSettings',
'el-icon-setting',
'play:pk:settings:list',
2,
4,
@pk_root_id,
1,
0,
'规则设置',
'settings',
'play/pk/settings/index',
'',
0,
1,
NOW(),
'migration_v22_pk_menu',
NOW(),
'migration_v22_pk_menu',
0,
1,
''
WHERE @pk_root_id IS NOT NULL AND @pk_settings_id IS NULL;

View File

@@ -0,0 +1,10 @@
-- Fix PK root menu path to ensure correct routing.
UPDATE sys_menu
SET path = '/play/pk',
updated_time = NOW(),
updated_by = 'migration_v23_fix_pk_path'
WHERE menu_name = 'PK管理'
AND parent_id = 0
AND deleted = 0
AND path = 'play/pk';

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");
}
}