working but not tested

This commit is contained in:
irving
2025-12-24 10:55:26 -05:00
parent 90849e5267
commit a7e567e9b4
22 changed files with 997 additions and 1 deletions

View File

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

View File

@@ -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
*/

View File

@@ -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;
}

View File

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

View File

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

View File

@@ -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

View File

@@ -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;
}
}

View File

@@ -0,0 +1,9 @@
package com.starry.admin.modules.pk.dto;
/**
* PK WebSocket 事件类型。
*/
public enum PkEventType {
SCORE_UPDATE,
STATE_CHANGE
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,10 @@
package com.starry.admin.modules.pk.event;
/**
* PK 贡献来源类型。
*/
public enum PkContributionSource {
ORDER,
GIFT,
RECHARGE
}

View File

@@ -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;
}
}

View File

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

View File

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

View File

@@ -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;
}
}

View File

@@ -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();
}

View File

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

View File

@@ -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;
}
}
}

View File

@@ -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();
}
}

View File

@@ -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`;