working but not tested
This commit is contained in:
@@ -25,6 +25,10 @@
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-websocket</artifactId>
|
||||
</dependency>
|
||||
<!-- Flyway -->
|
||||
<dependency>
|
||||
<groupId>org.flywaydb</groupId>
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.starry.admin.common.config;
|
||||
|
||||
import com.starry.admin.modules.weichat.constant.WebSocketConstant;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
|
||||
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
|
||||
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
|
||||
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
|
||||
|
||||
/**
|
||||
* WebSocket 配置,基于 STOMP 的简单消息代理。
|
||||
*/
|
||||
@Configuration
|
||||
@EnableWebSocketMessageBroker
|
||||
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
|
||||
|
||||
private static final String APPLICATION_DESTINATION_PREFIX = "/app";
|
||||
private static final String TOPIC_DESTINATION_PREFIX = "/topic";
|
||||
private static final String PK_ENDPOINT = "/ws/pk";
|
||||
|
||||
@Override
|
||||
public void configureMessageBroker(MessageBrokerRegistry registry) {
|
||||
registry.enableSimpleBroker(TOPIC_DESTINATION_PREFIX);
|
||||
registry.setApplicationDestinationPrefixes(APPLICATION_DESTINATION_PREFIX);
|
||||
registry.setUserDestinationPrefix(WebSocketConstant.USER_DESTINATION_PREFIX);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerStompEndpoints(StompEndpointRegistry registry) {
|
||||
registry.addEndpoint(PK_ENDPOINT).setAllowedOriginPatterns("*");
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,9 @@ package com.starry.admin.modules.clerk.controller;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.starry.admin.modules.clerk.module.entity.PlayClerkPkEntity;
|
||||
import com.starry.admin.modules.clerk.service.IPlayClerkPkService;
|
||||
import com.starry.admin.modules.pk.dto.PkScoreBoardDto;
|
||||
import com.starry.admin.modules.pk.service.ClerkPkLifecycleService;
|
||||
import com.starry.admin.modules.pk.service.IPkScoreboardService;
|
||||
import com.starry.common.annotation.Log;
|
||||
import com.starry.common.enums.BusinessType;
|
||||
import com.starry.common.result.R;
|
||||
@@ -13,7 +16,13 @@ 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.*;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
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;
|
||||
|
||||
/**
|
||||
* 店员pkController
|
||||
@@ -28,6 +37,12 @@ public class PlayClerkPkController {
|
||||
@Resource
|
||||
private IPlayClerkPkService playClerkPkService;
|
||||
|
||||
@Resource
|
||||
private IPkScoreboardService pkScoreboardService;
|
||||
|
||||
@Resource
|
||||
private ClerkPkLifecycleService clerkPkLifecycleService;
|
||||
|
||||
/**
|
||||
* 查询店员pk列表
|
||||
*/
|
||||
@@ -51,6 +66,40 @@ public class PlayClerkPkController {
|
||||
return R.ok(playClerkPkService.selectPlayClerkPkById(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取店员PK实时比分
|
||||
*/
|
||||
@ApiOperation(value = "获取PK实时比分", notes = "根据ID获取店员PK当前比分")
|
||||
@ApiImplicitParam(name = "id", value = "PK记录ID", required = true, paramType = "path", dataType = "String", example = "1")
|
||||
@ApiResponses({@ApiResponse(code = 200, message = "操作成功", response = PkScoreBoardDto.class)})
|
||||
@GetMapping(value = "/{id}/scoreboard")
|
||||
public R getScoreboard(@PathVariable("id") String id) {
|
||||
PkScoreBoardDto scoreboard = pkScoreboardService.getScoreboard(id);
|
||||
return R.ok(scoreboard);
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动开始PK
|
||||
*/
|
||||
@ApiOperation(value = "开始PK", notes = "将指定PK从待开始状态切换为进行中")
|
||||
@ApiImplicitParam(name = "id", value = "PK记录ID", required = true, paramType = "path", dataType = "String", example = "1")
|
||||
@PostMapping(value = "/{id}/start")
|
||||
public R startPk(@PathVariable("id") String id) {
|
||||
clerkPkLifecycleService.startPk(id);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动结束并结算PK
|
||||
*/
|
||||
@ApiOperation(value = "结束PK并结算", notes = "将指定PK标记为已完成,并写入最终比分和胜者信息")
|
||||
@ApiImplicitParam(name = "id", value = "PK记录ID", required = true, paramType = "path", dataType = "String", example = "1")
|
||||
@PostMapping(value = "/{id}/finish")
|
||||
public R finishPk(@PathVariable("id") String id) {
|
||||
clerkPkLifecycleService.finishPk(id);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增店员pk
|
||||
*/
|
||||
|
||||
@@ -87,4 +87,34 @@ public class PlayClerkPkEntity extends BaseEntity<PlayClerkPkEntity> {
|
||||
*/
|
||||
private String status;
|
||||
|
||||
/**
|
||||
* 店员A得分
|
||||
*/
|
||||
private java.math.BigDecimal clerkAScore;
|
||||
|
||||
/**
|
||||
* 店员B得分
|
||||
*/
|
||||
private java.math.BigDecimal clerkBScore;
|
||||
|
||||
/**
|
||||
* 店员A订单数
|
||||
*/
|
||||
private Integer clerkAOrderCount;
|
||||
|
||||
/**
|
||||
* 店员B订单数
|
||||
*/
|
||||
private Integer clerkBOrderCount;
|
||||
|
||||
/**
|
||||
* 获胜店员ID
|
||||
*/
|
||||
private String winnerClerkId;
|
||||
|
||||
/**
|
||||
* 是否已结算(1:是;0:否)
|
||||
*/
|
||||
private Integer settled;
|
||||
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ package com.starry.admin.modules.clerk.service;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.starry.admin.modules.clerk.module.entity.PlayClerkPkEntity;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 店员pkService接口
|
||||
@@ -64,4 +66,13 @@ public interface IPlayClerkPkService extends IService<PlayClerkPkEntity> {
|
||||
* @return 结果
|
||||
*/
|
||||
int deletePlayClerkPkById(String id);
|
||||
|
||||
/**
|
||||
* 查询某个店员在指定时间是否存在进行中的 PK。
|
||||
*
|
||||
* @param clerkId 店员ID
|
||||
* @param occurredAt 发生时间
|
||||
* @return 存在则返回 PK 记录,否则返回空
|
||||
*/
|
||||
Optional<PlayClerkPkEntity> findActivePkForClerk(String clerkId, LocalDateTime occurredAt);
|
||||
}
|
||||
|
||||
@@ -14,8 +14,11 @@ 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.common.utils.IdUtils;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.Optional;
|
||||
import javax.annotation.Resource;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@@ -136,4 +139,23 @@ public class PlayClerkPkServiceImpl extends ServiceImpl<PlayClerkPkMapper, PlayC
|
||||
public int deletePlayClerkPkById(String id) {
|
||||
return playClerkPkMapper.deleteById(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<PlayClerkPkEntity> findActivePkForClerk(String clerkId, LocalDateTime occurredAt) {
|
||||
if (StrUtil.isBlank(clerkId) || occurredAt == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
Date eventTime =
|
||||
Date.from(occurredAt.atZone(ZoneId.systemDefault()).toInstant());
|
||||
LambdaQueryWrapper<PlayClerkPkEntity> wrapper = Wrappers.lambdaQuery(PlayClerkPkEntity.class)
|
||||
.in(PlayClerkPkEntity::getStatus,
|
||||
Arrays.asList(ClerkPkEnum.TO_BE_STARTED.name(), ClerkPkEnum.IN_PROGRESS.name()))
|
||||
.and(w -> w.eq(PlayClerkPkEntity::getClerkA, clerkId)
|
||||
.or()
|
||||
.eq(PlayClerkPkEntity::getClerkB, clerkId))
|
||||
.le(PlayClerkPkEntity::getPkBeginTime, eventTime)
|
||||
.ge(PlayClerkPkEntity::getPkEndTime, eventTime);
|
||||
PlayClerkPkEntity entity = this.getOne(wrapper, false);
|
||||
return Optional.ofNullable(entity);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@ import com.starry.admin.modules.order.service.IOrderLifecycleService;
|
||||
import com.starry.admin.modules.order.service.IPlayOrderRefundInfoService;
|
||||
import com.starry.admin.modules.order.service.support.ClerkRevenueCalculator;
|
||||
import com.starry.admin.modules.personnel.service.IPlayBalanceDetailsInfoService;
|
||||
import com.starry.admin.modules.pk.event.PkContributionEvent;
|
||||
import com.starry.admin.modules.shop.module.constant.CouponUseState;
|
||||
import com.starry.admin.modules.shop.module.entity.PlayCouponDetailsEntity;
|
||||
import com.starry.admin.modules.shop.module.entity.PlayCouponInfoEntity;
|
||||
@@ -504,6 +505,17 @@ public class OrderLifecycleServiceImpl implements IOrderLifecycleService {
|
||||
if (shouldNotify) {
|
||||
notificationSender.sendOrderFinishMessageAsync(latest);
|
||||
}
|
||||
|
||||
if (OrderStatus.COMPLETED.getCode().equals(latest.getOrderStatus())
|
||||
&& latest.getFinalAmount() != null
|
||||
&& latest.getFinalAmount().compareTo(BigDecimal.ZERO) > 0
|
||||
&& StrUtil.isNotBlank(latest.getAcceptBy())) {
|
||||
LocalDateTime contributionTime =
|
||||
latest.getOrderEndTime() != null ? latest.getOrderEndTime() : endTime;
|
||||
applicationEventPublisher.publishEvent(
|
||||
PkContributionEvent.orderContribution(latest.getId(), latest.getAcceptBy(),
|
||||
latest.getFinalAmount(), contributionTime));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
package com.starry.admin.modules.pk.dto;
|
||||
|
||||
/**
|
||||
* 通过 WebSocket 推送的 PK 事件消息。
|
||||
*/
|
||||
public class PkEventMessage<T> {
|
||||
|
||||
private PkEventType type;
|
||||
private String pkId;
|
||||
private long timestamp;
|
||||
private T payload;
|
||||
|
||||
public static <T> PkEventMessage<T> of(PkEventType type, String pkId, T payload, long timestamp) {
|
||||
PkEventMessage<T> message = new PkEventMessage<>();
|
||||
message.setType(type);
|
||||
message.setPkId(pkId);
|
||||
message.setPayload(payload);
|
||||
message.setTimestamp(timestamp);
|
||||
return message;
|
||||
}
|
||||
|
||||
public PkEventType getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public void setType(PkEventType type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public String getPkId() {
|
||||
return pkId;
|
||||
}
|
||||
|
||||
public void setPkId(String pkId) {
|
||||
this.pkId = pkId;
|
||||
}
|
||||
|
||||
public long getTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
public void setTimestamp(long timestamp) {
|
||||
this.timestamp = timestamp;
|
||||
}
|
||||
|
||||
public T getPayload() {
|
||||
return payload;
|
||||
}
|
||||
|
||||
public void setPayload(T payload) {
|
||||
this.payload = payload;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.starry.admin.modules.pk.dto;
|
||||
|
||||
/**
|
||||
* PK WebSocket 事件类型。
|
||||
*/
|
||||
public enum PkEventType {
|
||||
SCORE_UPDATE,
|
||||
STATE_CHANGE
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package com.starry.admin.modules.pk.dto;
|
||||
|
||||
import io.swagger.annotations.ApiModel;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import java.math.BigDecimal;
|
||||
|
||||
@ApiModel(value = "PkScoreBoardDto", description = "店员 PK 实时比分")
|
||||
public class PkScoreBoardDto {
|
||||
|
||||
@ApiModelProperty(value = "PK ID", required = true)
|
||||
private String pkId;
|
||||
|
||||
@ApiModelProperty(value = "店员A ID", required = true)
|
||||
private String clerkAId;
|
||||
|
||||
@ApiModelProperty(value = "店员B ID", required = true)
|
||||
private String clerkBId;
|
||||
|
||||
@ApiModelProperty(value = "店员A得分", required = true)
|
||||
private BigDecimal clerkAScore;
|
||||
|
||||
@ApiModelProperty(value = "店员B得分", required = true)
|
||||
private BigDecimal clerkBScore;
|
||||
|
||||
@ApiModelProperty(value = "店员A订单数", required = true)
|
||||
private int clerkAOrderCount;
|
||||
|
||||
@ApiModelProperty(value = "店员B订单数", required = true)
|
||||
private int clerkBOrderCount;
|
||||
|
||||
@ApiModelProperty(value = "PK 剩余秒数", required = true)
|
||||
private long remainingSeconds;
|
||||
|
||||
public String getPkId() {
|
||||
return pkId;
|
||||
}
|
||||
|
||||
public void setPkId(String pkId) {
|
||||
this.pkId = pkId;
|
||||
}
|
||||
|
||||
public String getClerkAId() {
|
||||
return clerkAId;
|
||||
}
|
||||
|
||||
public void setClerkAId(String clerkAId) {
|
||||
this.clerkAId = clerkAId;
|
||||
}
|
||||
|
||||
public String getClerkBId() {
|
||||
return clerkBId;
|
||||
}
|
||||
|
||||
public void setClerkBId(String clerkBId) {
|
||||
this.clerkBId = clerkBId;
|
||||
}
|
||||
|
||||
public BigDecimal getClerkAScore() {
|
||||
return clerkAScore;
|
||||
}
|
||||
|
||||
public void setClerkAScore(BigDecimal clerkAScore) {
|
||||
this.clerkAScore = clerkAScore;
|
||||
}
|
||||
|
||||
public BigDecimal getClerkBScore() {
|
||||
return clerkBScore;
|
||||
}
|
||||
|
||||
public void setClerkBScore(BigDecimal clerkBScore) {
|
||||
this.clerkBScore = clerkBScore;
|
||||
}
|
||||
|
||||
public int getClerkAOrderCount() {
|
||||
return clerkAOrderCount;
|
||||
}
|
||||
|
||||
public void setClerkAOrderCount(int clerkAOrderCount) {
|
||||
this.clerkAOrderCount = clerkAOrderCount;
|
||||
}
|
||||
|
||||
public int getClerkBOrderCount() {
|
||||
return clerkBOrderCount;
|
||||
}
|
||||
|
||||
public void setClerkBOrderCount(int clerkBOrderCount) {
|
||||
this.clerkBOrderCount = clerkBOrderCount;
|
||||
}
|
||||
|
||||
public long getRemainingSeconds() {
|
||||
return remainingSeconds;
|
||||
}
|
||||
|
||||
public void setRemainingSeconds(long remainingSeconds) {
|
||||
this.remainingSeconds = remainingSeconds;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package com.starry.admin.modules.pk.event;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 店员在 PK 期间产生的一次贡献事件。
|
||||
*/
|
||||
public final class PkContributionEvent {
|
||||
|
||||
/**
|
||||
* 贡献来源的业务唯一ID,例如订单ID、礼物记录ID等。
|
||||
*/
|
||||
private final String referenceId;
|
||||
|
||||
private final String clerkId;
|
||||
private final BigDecimal amount;
|
||||
private final PkContributionSource source;
|
||||
private final LocalDateTime occurredAt;
|
||||
|
||||
private PkContributionEvent(String referenceId, String clerkId, BigDecimal amount, PkContributionSource source,
|
||||
LocalDateTime occurredAt) {
|
||||
this.referenceId = Objects.requireNonNull(referenceId, "referenceId cannot be null");
|
||||
this.clerkId = Objects.requireNonNull(clerkId, "clerkId cannot be null");
|
||||
this.amount = Objects.requireNonNull(amount, "amount cannot be null");
|
||||
this.source = Objects.requireNonNull(source, "source cannot be null");
|
||||
this.occurredAt = Objects.requireNonNull(occurredAt, "occurredAt cannot be null");
|
||||
if (this.referenceId.isEmpty()) {
|
||||
throw new IllegalArgumentException("referenceId cannot be empty");
|
||||
}
|
||||
if (this.clerkId.isEmpty()) {
|
||||
throw new IllegalArgumentException("clerkId cannot be empty");
|
||||
}
|
||||
if (this.amount.compareTo(BigDecimal.ZERO) < 0) {
|
||||
throw new IllegalArgumentException("amount cannot be negative");
|
||||
}
|
||||
}
|
||||
|
||||
public static PkContributionEvent orderContribution(String orderId, String clerkId, BigDecimal amount,
|
||||
LocalDateTime occurredAt) {
|
||||
return new PkContributionEvent(orderId, clerkId, amount, PkContributionSource.ORDER, occurredAt);
|
||||
}
|
||||
|
||||
public static PkContributionEvent giftContribution(String giftRecordId, String clerkId, BigDecimal amount,
|
||||
LocalDateTime occurredAt) {
|
||||
return new PkContributionEvent(giftRecordId, clerkId, amount, PkContributionSource.GIFT, occurredAt);
|
||||
}
|
||||
|
||||
public static PkContributionEvent rechargeContribution(String rechargeRecordId, String clerkId, BigDecimal amount,
|
||||
LocalDateTime occurredAt) {
|
||||
return new PkContributionEvent(rechargeRecordId, clerkId, amount, PkContributionSource.RECHARGE, occurredAt);
|
||||
}
|
||||
|
||||
public String getReferenceId() {
|
||||
return referenceId;
|
||||
}
|
||||
|
||||
public String getClerkId() {
|
||||
return clerkId;
|
||||
}
|
||||
|
||||
public BigDecimal getAmount() {
|
||||
return amount;
|
||||
}
|
||||
|
||||
public PkContributionSource getSource() {
|
||||
return source;
|
||||
}
|
||||
|
||||
public LocalDateTime getOccurredAt() {
|
||||
return occurredAt;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.starry.admin.modules.pk.event;
|
||||
|
||||
/**
|
||||
* PK 贡献来源类型。
|
||||
*/
|
||||
public enum PkContributionSource {
|
||||
ORDER,
|
||||
GIFT,
|
||||
RECHARGE
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.starry.admin.modules.pk.event;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 某一场 PK 的比分发生变化。
|
||||
*/
|
||||
public final class PkScoreChangedEvent {
|
||||
|
||||
private final String pkId;
|
||||
|
||||
public PkScoreChangedEvent(String pkId) {
|
||||
this.pkId = Objects.requireNonNull(pkId, "pkId cannot be null");
|
||||
if (this.pkId.isEmpty()) {
|
||||
throw new IllegalArgumentException("pkId cannot be empty");
|
||||
}
|
||||
}
|
||||
|
||||
public String getPkId() {
|
||||
return pkId;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package com.starry.admin.modules.pk.listener;
|
||||
|
||||
import com.starry.admin.modules.clerk.module.entity.PlayClerkPkEntity;
|
||||
import com.starry.admin.modules.clerk.service.IPlayClerkPkService;
|
||||
import com.starry.admin.modules.pk.event.PkContributionEvent;
|
||||
import com.starry.admin.modules.pk.event.PkScoreChangedEvent;
|
||||
import com.starry.admin.modules.pk.redis.PkRedisKeyConstants;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import javax.annotation.Resource;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 监听订单/礼物/充值产生的 PK 贡献事件,更新 Redis 中的比分。
|
||||
*/
|
||||
@Component
|
||||
public class PkContributionListener {
|
||||
|
||||
private static final long SINGLE_CONTRIBUTION_COUNT = 1L;
|
||||
|
||||
@Resource
|
||||
private IPlayClerkPkService clerkPkService;
|
||||
|
||||
@Resource
|
||||
private StringRedisTemplate stringRedisTemplate;
|
||||
|
||||
@Resource
|
||||
private ApplicationEventPublisher applicationEventPublisher;
|
||||
|
||||
@EventListener
|
||||
public void onContribution(PkContributionEvent event) {
|
||||
if (!acquireDedup(event)) {
|
||||
return;
|
||||
}
|
||||
Optional<PlayClerkPkEntity> pkOptional =
|
||||
clerkPkService.findActivePkForClerk(event.getClerkId(), event.getOccurredAt());
|
||||
if (!pkOptional.isPresent()) {
|
||||
return;
|
||||
}
|
||||
PlayClerkPkEntity pk = pkOptional.get();
|
||||
updateRedisScore(pk, event);
|
||||
applicationEventPublisher.publishEvent(new PkScoreChangedEvent(pk.getId()));
|
||||
}
|
||||
|
||||
private boolean acquireDedup(PkContributionEvent event) {
|
||||
String dedupKey =
|
||||
PkRedisKeyConstants.contributionDedupKey(event.getSource().name(), event.getReferenceId());
|
||||
Boolean firstSeen = stringRedisTemplate.opsForValue()
|
||||
.setIfAbsent(dedupKey, "1", PkRedisKeyConstants.CONTRIBUTION_DEDUP_TTL_SECONDS, TimeUnit.SECONDS);
|
||||
return firstSeen == null || firstSeen;
|
||||
}
|
||||
|
||||
private void updateRedisScore(PlayClerkPkEntity pk, PkContributionEvent event) {
|
||||
String pkId = pk.getId();
|
||||
if (pkId == null || pkId.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
boolean isClerkA = event.getClerkId().equals(pk.getClerkA());
|
||||
String scoreField = isClerkA ? PkRedisKeyConstants.FIELD_CLERK_A_SCORE : PkRedisKeyConstants.FIELD_CLERK_B_SCORE;
|
||||
String countField =
|
||||
isClerkA ? PkRedisKeyConstants.FIELD_CLERK_A_ORDER_COUNT : PkRedisKeyConstants.FIELD_CLERK_B_ORDER_COUNT;
|
||||
|
||||
String scoreKey = PkRedisKeyConstants.scoreKey(pkId);
|
||||
stringRedisTemplate.opsForHash().increment(scoreKey, scoreField, event.getAmount().doubleValue());
|
||||
stringRedisTemplate.opsForHash().increment(scoreKey, countField, SINGLE_CONTRIBUTION_COUNT);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.starry.admin.modules.pk.listener;
|
||||
|
||||
import com.starry.admin.modules.pk.dto.PkEventMessage;
|
||||
import com.starry.admin.modules.pk.dto.PkEventType;
|
||||
import com.starry.admin.modules.pk.dto.PkScoreBoardDto;
|
||||
import com.starry.admin.modules.pk.event.PkScoreChangedEvent;
|
||||
import com.starry.admin.modules.pk.service.IPkScoreboardService;
|
||||
import javax.annotation.Resource;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.messaging.simp.SimpMessagingTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 监听比分变化事件,通过 WebSocket 将最新比分推送给订阅方。
|
||||
*/
|
||||
@Component
|
||||
public class PkScoreChangedListener {
|
||||
|
||||
private static final String PK_TOPIC_PREFIX = "/topic/pk/";
|
||||
|
||||
@Resource
|
||||
private IPkScoreboardService pkScoreboardService;
|
||||
|
||||
@Resource
|
||||
private SimpMessagingTemplate messagingTemplate;
|
||||
|
||||
@EventListener
|
||||
public void onScoreChanged(PkScoreChangedEvent event) {
|
||||
PkScoreBoardDto scoreboard = pkScoreboardService.getScoreboard(event.getPkId());
|
||||
PkEventMessage<PkScoreBoardDto> message = PkEventMessage.of(
|
||||
PkEventType.SCORE_UPDATE,
|
||||
event.getPkId(),
|
||||
scoreboard,
|
||||
System.currentTimeMillis());
|
||||
messagingTemplate.convertAndSend(PK_TOPIC_PREFIX + event.getPkId(), message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.starry.admin.modules.pk.redis;
|
||||
|
||||
/**
|
||||
* PK 相关 Redis key 常量。
|
||||
*/
|
||||
public final class PkRedisKeyConstants {
|
||||
|
||||
private static final String SCORE_HASH_PREFIX = "pk:";
|
||||
private static final String SCORE_HASH_SUFFIX = ":score";
|
||||
private static final String DEDUP_KEY_PREFIX = "pk:dedup:";
|
||||
|
||||
/**
|
||||
* 贡献幂等记录的存活时间(秒)。
|
||||
*/
|
||||
public static final long CONTRIBUTION_DEDUP_TTL_SECONDS = 3600L;
|
||||
|
||||
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_A_ORDER_COUNT = "clerk_a_order_count";
|
||||
public static final String FIELD_CLERK_B_ORDER_COUNT = "clerk_b_order_count";
|
||||
|
||||
private PkRedisKeyConstants() {
|
||||
}
|
||||
|
||||
public static String scoreKey(String pkId) {
|
||||
return SCORE_HASH_PREFIX + pkId + SCORE_HASH_SUFFIX;
|
||||
}
|
||||
|
||||
public static String contributionDedupKey(String sourceCode, String referenceId) {
|
||||
return DEDUP_KEY_PREFIX + sourceCode + ":" + referenceId;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.starry.admin.modules.pk.service;
|
||||
|
||||
/**
|
||||
* 店员 PK 生命周期管理服务。
|
||||
*/
|
||||
public interface ClerkPkLifecycleService {
|
||||
|
||||
/**
|
||||
* 启动指定 PK(从待开始进入进行中)。
|
||||
*
|
||||
* @param pkId PK ID
|
||||
*/
|
||||
void startPk(String pkId);
|
||||
|
||||
/**
|
||||
* 完成并结算指定 PK。
|
||||
*
|
||||
* @param pkId PK ID
|
||||
*/
|
||||
void finishPk(String pkId);
|
||||
|
||||
/**
|
||||
* 扫描当前需要状态流转的 PK。
|
||||
*/
|
||||
void scanAndUpdate();
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.starry.admin.modules.pk.service;
|
||||
|
||||
import com.starry.admin.modules.pk.dto.PkScoreBoardDto;
|
||||
|
||||
/**
|
||||
* PK 实时比分查询服务。
|
||||
*/
|
||||
public interface IPkScoreboardService {
|
||||
|
||||
/**
|
||||
* 查询指定 PK 的实时比分。
|
||||
*
|
||||
* @param pkId PK ID
|
||||
* @return 比分信息(不会为 null)
|
||||
*/
|
||||
PkScoreBoardDto getScoreboard(String pkId);
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
package com.starry.admin.modules.pk.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
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.service.ClerkPkLifecycleService;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
import javax.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class ClerkPkLifecycleServiceImpl implements ClerkPkLifecycleService {
|
||||
|
||||
@Resource
|
||||
private IPlayClerkPkService clerkPkService;
|
||||
|
||||
@Resource
|
||||
private StringRedisTemplate stringRedisTemplate;
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void startPk(String pkId) {
|
||||
PlayClerkPkEntity pk = clerkPkService.selectPlayClerkPkById(pkId);
|
||||
if (pk == null) {
|
||||
throw new CustomException("PK不存在");
|
||||
}
|
||||
if (!ClerkPkEnum.TO_BE_STARTED.name().equals(pk.getStatus())) {
|
||||
return;
|
||||
}
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
if (pk.getPkBeginTime() != null
|
||||
&& LocalDateTime.ofInstant(pk.getPkBeginTime().toInstant(), ZoneId.systemDefault()).isAfter(now)) {
|
||||
throw new CustomException("PK开始时间尚未到达");
|
||||
}
|
||||
pk.setStatus(ClerkPkEnum.IN_PROGRESS.name());
|
||||
clerkPkService.updateById(pk);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void finishPk(String pkId) {
|
||||
PlayClerkPkEntity pk = clerkPkService.selectPlayClerkPkById(pkId);
|
||||
if (pk == null) {
|
||||
throw new CustomException("PK不存在");
|
||||
}
|
||||
if (ClerkPkEnum.FINISHED.name().equals(pk.getStatus())) {
|
||||
return;
|
||||
}
|
||||
String scoreKey = PkRedisKeyConstants.scoreKey(pkId);
|
||||
Map<Object, Object> entries = stringRedisTemplate.opsForHash().entries(scoreKey);
|
||||
|
||||
BigDecimal clerkAScore = parseDecimal(entries.get(PkRedisKeyConstants.FIELD_CLERK_A_SCORE));
|
||||
BigDecimal clerkBScore = parseDecimal(entries.get(PkRedisKeyConstants.FIELD_CLERK_B_SCORE));
|
||||
int clerkAOrderCount = parseInt(entries.get(PkRedisKeyConstants.FIELD_CLERK_A_ORDER_COUNT));
|
||||
int clerkBOrderCount = parseInt(entries.get(PkRedisKeyConstants.FIELD_CLERK_B_ORDER_COUNT));
|
||||
|
||||
pk.setClerkAScore(clerkAScore);
|
||||
pk.setClerkBScore(clerkBScore);
|
||||
pk.setClerkAOrderCount(clerkAOrderCount);
|
||||
pk.setClerkBOrderCount(clerkBOrderCount);
|
||||
pk.setStatus(ClerkPkEnum.FINISHED.name());
|
||||
pk.setSettled(1);
|
||||
if (clerkAScore.compareTo(clerkBScore) > 0) {
|
||||
pk.setWinnerClerkId(pk.getClerkA());
|
||||
} else if (clerkBScore.compareTo(clerkAScore) > 0) {
|
||||
pk.setWinnerClerkId(pk.getClerkB());
|
||||
} else {
|
||||
pk.setWinnerClerkId(null);
|
||||
}
|
||||
clerkPkService.updateById(pk);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void scanAndUpdate() {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
Date nowDate = Date.from(now.atZone(ZoneId.systemDefault()).toInstant());
|
||||
|
||||
clerkPkService.list(Wrappers.<PlayClerkPkEntity>lambdaQuery()
|
||||
.eq(PlayClerkPkEntity::getStatus, ClerkPkEnum.TO_BE_STARTED.name())
|
||||
.le(PlayClerkPkEntity::getPkBeginTime, nowDate))
|
||||
.forEach(pk -> {
|
||||
try {
|
||||
startPk(pk.getId());
|
||||
} catch (Exception e) {
|
||||
log.warn("自动开始PK失败, pkId={}", pk.getId(), e);
|
||||
}
|
||||
});
|
||||
|
||||
clerkPkService.list(Wrappers.<PlayClerkPkEntity>lambdaQuery()
|
||||
.in(PlayClerkPkEntity::getStatus,
|
||||
ClerkPkEnum.IN_PROGRESS.name(), ClerkPkEnum.TO_BE_STARTED.name())
|
||||
.le(PlayClerkPkEntity::getPkEndTime, nowDate)
|
||||
.eq(PlayClerkPkEntity::getSettled, 0))
|
||||
.forEach(pk -> {
|
||||
try {
|
||||
finishPk(pk.getId());
|
||||
} catch (Exception e) {
|
||||
log.warn("自动结束PK失败, pkId={}", pk.getId(), e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Scheduled(fixedDelay = 30000)
|
||||
public void scheduledScan() {
|
||||
scanAndUpdate();
|
||||
}
|
||||
|
||||
private static BigDecimal parseDecimal(Object value) {
|
||||
if (value == null) {
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
try {
|
||||
return new BigDecimal(value.toString());
|
||||
} catch (NumberFormatException ex) {
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
}
|
||||
|
||||
private static int parseInt(Object value) {
|
||||
if (value == null) {
|
||||
return 0;
|
||||
}
|
||||
try {
|
||||
return Integer.parseInt(value.toString());
|
||||
} catch (NumberFormatException ex) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package com.starry.admin.modules.pk.service.impl;
|
||||
|
||||
import com.starry.admin.common.exception.CustomException;
|
||||
import com.starry.admin.modules.clerk.module.entity.PlayClerkPkEntity;
|
||||
import com.starry.admin.modules.clerk.service.IPlayClerkPkService;
|
||||
import com.starry.admin.modules.pk.dto.PkScoreBoardDto;
|
||||
import com.starry.admin.modules.pk.redis.PkRedisKeyConstants;
|
||||
import com.starry.admin.modules.pk.service.IPkScoreboardService;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.Duration;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.util.Map;
|
||||
import javax.annotation.Resource;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class PkScoreboardServiceImpl implements IPkScoreboardService {
|
||||
|
||||
@Resource
|
||||
private IPlayClerkPkService clerkPkService;
|
||||
|
||||
@Resource
|
||||
private StringRedisTemplate stringRedisTemplate;
|
||||
|
||||
@Override
|
||||
public PkScoreBoardDto getScoreboard(String pkId) {
|
||||
PlayClerkPkEntity pk = clerkPkService.selectPlayClerkPkById(pkId);
|
||||
if (pk == null) {
|
||||
throw new CustomException("PK不存在");
|
||||
}
|
||||
PkScoreBoardDto dto = new PkScoreBoardDto();
|
||||
dto.setPkId(pk.getId());
|
||||
dto.setClerkAId(pk.getClerkA());
|
||||
dto.setClerkBId(pk.getClerkB());
|
||||
|
||||
String scoreKey = PkRedisKeyConstants.scoreKey(pkId);
|
||||
Map<Object, Object> entries = stringRedisTemplate.opsForHash().entries(scoreKey);
|
||||
|
||||
BigDecimal clerkAScore = parseDecimal(entries.get(PkRedisKeyConstants.FIELD_CLERK_A_SCORE));
|
||||
BigDecimal clerkBScore = parseDecimal(entries.get(PkRedisKeyConstants.FIELD_CLERK_B_SCORE));
|
||||
int clerkAOrderCount = parseInt(entries.get(PkRedisKeyConstants.FIELD_CLERK_A_ORDER_COUNT));
|
||||
int clerkBOrderCount = parseInt(entries.get(PkRedisKeyConstants.FIELD_CLERK_B_ORDER_COUNT));
|
||||
|
||||
dto.setClerkAScore(clerkAScore);
|
||||
dto.setClerkBScore(clerkBScore);
|
||||
dto.setClerkAOrderCount(clerkAOrderCount);
|
||||
dto.setClerkBOrderCount(clerkBOrderCount);
|
||||
dto.setRemainingSeconds(calculateRemainingSeconds(pk));
|
||||
return dto;
|
||||
}
|
||||
|
||||
private static BigDecimal parseDecimal(Object value) {
|
||||
if (value == null) {
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
try {
|
||||
return new BigDecimal(value.toString());
|
||||
} catch (NumberFormatException ex) {
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
}
|
||||
|
||||
private static int parseInt(Object value) {
|
||||
if (value == null) {
|
||||
return 0;
|
||||
}
|
||||
try {
|
||||
return Integer.parseInt(value.toString());
|
||||
} catch (NumberFormatException ex) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private static long calculateRemainingSeconds(PlayClerkPkEntity pk) {
|
||||
if (pk.getPkEndTime() == null) {
|
||||
return 0L;
|
||||
}
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
LocalDateTime endTime = LocalDateTime.ofInstant(pk.getPkEndTime().toInstant(), ZoneId.systemDefault());
|
||||
if (endTime.isBefore(now)) {
|
||||
return 0L;
|
||||
}
|
||||
return Duration.between(now, endTime).getSeconds();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
ALTER TABLE `play_clerk_pk`
|
||||
ADD COLUMN `clerk_a_score` decimal(10, 2) NOT NULL DEFAULT 0.00 COMMENT '店员A得分' AFTER `status`,
|
||||
ADD COLUMN `clerk_b_score` decimal(10, 2) NOT NULL DEFAULT 0.00 COMMENT '店员B得分' AFTER `clerk_a_score`,
|
||||
ADD COLUMN `clerk_a_order_count` int NOT NULL DEFAULT 0 COMMENT '店员A订单数' AFTER `clerk_b_score`,
|
||||
ADD COLUMN `clerk_b_order_count` int NOT NULL DEFAULT 0 COMMENT '店员B订单数' AFTER `clerk_a_order_count`,
|
||||
ADD COLUMN `winner_clerk_id` varchar(32) DEFAULT NULL COMMENT '获胜店员ID' AFTER `clerk_b_order_count`,
|
||||
ADD COLUMN `settled` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否已结算(1:是;0:否)' AFTER `winner_clerk_id`;
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
package com.starry.admin.modules.pk;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
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.dto.PkScoreBoardDto;
|
||||
import com.starry.admin.modules.pk.event.PkContributionEvent;
|
||||
import com.starry.admin.modules.pk.redis.PkRedisKeyConstants;
|
||||
import com.starry.admin.modules.pk.service.ClerkPkLifecycleService;
|
||||
import com.starry.admin.modules.pk.service.IPkScoreboardService;
|
||||
import com.starry.admin.utils.SecurityUtils;
|
||||
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.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
|
||||
/**
|
||||
* 集成测试:验证 PK 贡献事件、Redis 计分、结算生命周期的整体行为。
|
||||
*/
|
||||
class PkIntegrationTest extends AbstractApiTest {
|
||||
|
||||
@Resource
|
||||
private IPlayClerkPkService clerkPkService;
|
||||
|
||||
@Resource
|
||||
private IPkScoreboardService pkScoreboardService;
|
||||
|
||||
@Resource
|
||||
private ClerkPkLifecycleService clerkPkLifecycleService;
|
||||
|
||||
@Resource
|
||||
private com.starry.admin.modules.pk.listener.PkContributionListener pkContributionListener;
|
||||
|
||||
@Resource
|
||||
private StringRedisTemplate stringRedisTemplate;
|
||||
|
||||
@BeforeEach
|
||||
void clearPkKeys() {
|
||||
if (stringRedisTemplate.getConnectionFactory() == null) {
|
||||
return;
|
||||
}
|
||||
stringRedisTemplate.getConnectionFactory().getConnection().flushDb();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("PK 贡献事件应累加到 Redis 中的比分和订单数")
|
||||
void contributionEventsShouldAccumulateScoresInRedis() {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
String pkId = IdUtils.getUuid();
|
||||
String clerkAId = "pk-clerk-a";
|
||||
String clerkBId = "pk-clerk-b";
|
||||
|
||||
SecurityUtils.setTenantId(DEFAULT_TENANT);
|
||||
PlayClerkPkEntity pk = buildActivePk(pkId, clerkAId, clerkBId, now);
|
||||
clerkPkService.save(pk);
|
||||
|
||||
String scoreKey = PkRedisKeyConstants.scoreKey(pkId);
|
||||
stringRedisTemplate.delete(scoreKey);
|
||||
|
||||
assertThat(clerkPkService.findActivePkForClerk(clerkAId, now)).isPresent();
|
||||
assertThat(clerkPkService.findActivePkForClerk(clerkBId, now)).isPresent();
|
||||
|
||||
pkContributionListener.onContribution(
|
||||
PkContributionEvent.orderContribution("order-1", clerkAId, new BigDecimal("100.50"), now));
|
||||
// duplicate event for same order should be ignored
|
||||
pkContributionListener.onContribution(
|
||||
PkContributionEvent.orderContribution("order-1", clerkAId, new BigDecimal("100.50"), now));
|
||||
pkContributionListener.onContribution(
|
||||
PkContributionEvent.orderContribution("order-2", clerkBId, new BigDecimal("50.25"), now));
|
||||
pkContributionListener.onContribution(
|
||||
PkContributionEvent.orderContribution("order-3", clerkAId, new BigDecimal("20.00"), now));
|
||||
|
||||
PkScoreBoardDto scoreboard = pkScoreboardService.getScoreboard(pkId);
|
||||
|
||||
assertThat(scoreboard.getPkId()).isEqualTo(pkId);
|
||||
assertThat(scoreboard.getClerkAId()).isEqualTo(clerkAId);
|
||||
assertThat(scoreboard.getClerkBId()).isEqualTo(clerkBId);
|
||||
assertThat(scoreboard.getClerkAScore()).isEqualByComparingTo(new BigDecimal("120.50"));
|
||||
assertThat(scoreboard.getClerkBScore()).isEqualByComparingTo(new BigDecimal("50.25"));
|
||||
assertThat(scoreboard.getClerkAOrderCount()).isEqualTo(2);
|
||||
assertThat(scoreboard.getClerkBOrderCount()).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("finishPk 应从 Redis 读取比分并写回数据库,同时标记赢家")
|
||||
void finishPkShouldPersistScoresAndWinner() {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
String pkId = IdUtils.getUuid();
|
||||
String clerkAId = "pk-clerk-a2";
|
||||
String clerkBId = "pk-clerk-b2";
|
||||
|
||||
SecurityUtils.setTenantId(DEFAULT_TENANT);
|
||||
PlayClerkPkEntity pk = buildActivePk(pkId, clerkAId, clerkBId, now);
|
||||
clerkPkService.save(pk);
|
||||
|
||||
String scoreKey = PkRedisKeyConstants.scoreKey(pkId);
|
||||
stringRedisTemplate.delete(scoreKey);
|
||||
|
||||
pkContributionListener.onContribution(
|
||||
PkContributionEvent.orderContribution("order-a1", clerkAId, new BigDecimal("80.00"), now));
|
||||
pkContributionListener.onContribution(
|
||||
PkContributionEvent.orderContribution("order-b1", clerkBId, new BigDecimal("30.00"), now));
|
||||
pkContributionListener.onContribution(
|
||||
PkContributionEvent.orderContribution("order-a2", clerkAId, new BigDecimal("40.00"), now));
|
||||
|
||||
PkScoreBoardDto beforeFinish = pkScoreboardService.getScoreboard(pkId);
|
||||
|
||||
clerkPkLifecycleService.finishPk(pkId);
|
||||
PlayClerkPkEntity persisted = clerkPkService.selectPlayClerkPkById(pkId);
|
||||
|
||||
assertThat(persisted.getStatus()).isEqualTo(ClerkPkEnum.FINISHED.name());
|
||||
assertThat(persisted.getSettled()).isEqualTo(1);
|
||||
assertThat(persisted.getClerkAScore()).isEqualByComparingTo(beforeFinish.getClerkAScore());
|
||||
assertThat(persisted.getClerkBScore()).isEqualByComparingTo(beforeFinish.getClerkBScore());
|
||||
assertThat(persisted.getClerkAOrderCount()).isEqualTo(beforeFinish.getClerkAOrderCount());
|
||||
assertThat(persisted.getClerkBOrderCount()).isEqualTo(beforeFinish.getClerkBOrderCount());
|
||||
String expectedWinner;
|
||||
if (beforeFinish.getClerkAScore().compareTo(beforeFinish.getClerkBScore()) > 0) {
|
||||
expectedWinner = clerkAId;
|
||||
} else if (beforeFinish.getClerkBScore().compareTo(beforeFinish.getClerkAScore()) > 0) {
|
||||
expectedWinner = clerkBId;
|
||||
} else {
|
||||
expectedWinner = null;
|
||||
}
|
||||
assertThat(persisted.getWinnerClerkId()).isEqualTo(expectedWinner);
|
||||
}
|
||||
|
||||
private static PlayClerkPkEntity buildActivePk(String pkId, String clerkAId, String clerkBId,
|
||||
LocalDateTime now) {
|
||||
PlayClerkPkEntity pk = new PlayClerkPkEntity();
|
||||
pk.setId(pkId);
|
||||
pk.setTenantId("tenant-apitest");
|
||||
pk.setClerkA(clerkAId);
|
||||
pk.setClerkB(clerkBId);
|
||||
pk.setPkBeginTime(Date.from(now.minusMinutes(5).atZone(ZoneId.systemDefault()).toInstant()));
|
||||
pk.setPkEndTime(Date.from(now.plusMinutes(30).atZone(ZoneId.systemDefault()).toInstant()));
|
||||
pk.setStatus(ClerkPkEnum.IN_PROGRESS.name());
|
||||
pk.setSettled(0);
|
||||
return pk;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user