484 lines
18 KiB
Java
484 lines
18 KiB
Java
package com.designhub.service;
|
||
|
||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||
import com.designhub.common.ResultCode;
|
||
import com.designhub.entity.Contest;
|
||
import com.designhub.entity.ContestParticipant;
|
||
import com.designhub.entity.User;
|
||
import com.designhub.exception.BusinessException;
|
||
import com.designhub.mapper.ContestMapper;
|
||
import com.designhub.mapper.ContestParticipantMapper;
|
||
import com.designhub.mapper.UserMapper;
|
||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||
import com.fasterxml.jackson.core.type.TypeReference;
|
||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||
import lombok.RequiredArgsConstructor;
|
||
import lombok.extern.slf4j.Slf4j;
|
||
import org.springframework.scheduling.annotation.Scheduled;
|
||
import org.springframework.stereotype.Service;
|
||
import org.springframework.transaction.annotation.Transactional;
|
||
|
||
import java.time.LocalDateTime;
|
||
import java.util.List;
|
||
import java.util.Map;
|
||
|
||
/**
|
||
* 竞赛活动Service
|
||
*/
|
||
@Slf4j
|
||
@Service
|
||
@RequiredArgsConstructor
|
||
public class ContestService {
|
||
|
||
private final ContestMapper contestMapper;
|
||
private final ContestParticipantMapper participantMapper;
|
||
private final UserMapper userMapper;
|
||
private final NotificationService notificationService;
|
||
private final ObjectMapper objectMapper;
|
||
|
||
/**
|
||
* 创建竞赛
|
||
*/
|
||
@Transactional
|
||
public Contest createContest(Contest contest) {
|
||
// 验证时间
|
||
if (contest.getStartTime().isAfter(contest.getEndTime())) {
|
||
throw new BusinessException(ResultCode.PARAM_ERROR, "开始时间不能晚于结束时间");
|
||
}
|
||
|
||
contest.setStatus("draft");
|
||
contest.setParticipantCount(0);
|
||
contest.setDeleted(0);
|
||
contestMapper.insert(contest);
|
||
|
||
log.info("创建竞赛成功: id={}, name={}", contest.getId(), contest.getName());
|
||
return contest;
|
||
}
|
||
|
||
/**
|
||
* 更新竞赛
|
||
*/
|
||
@Transactional
|
||
public Contest updateContest(Long id, Contest contest) {
|
||
Contest existing = contestMapper.selectById(id);
|
||
if (existing == null) {
|
||
throw new BusinessException(ResultCode.NOT_FOUND, "竞赛不存在");
|
||
}
|
||
|
||
// 进行中的竞赛不能修改规则
|
||
if ("active".equals(existing.getStatus()) && contest.getRules() != null) {
|
||
throw new BusinessException(ResultCode.PARAM_ERROR, "进行中的竞赛不能修改规则");
|
||
}
|
||
|
||
contest.setId(id);
|
||
contestMapper.updateById(contest);
|
||
|
||
return contestMapper.selectById(id);
|
||
}
|
||
|
||
/**
|
||
* 发布竞赛(从草稿变为待开始或进行中)
|
||
*/
|
||
@Transactional
|
||
public Contest publishContest(Long id) {
|
||
Contest contest = contestMapper.selectById(id);
|
||
if (contest == null) {
|
||
throw new BusinessException(ResultCode.NOT_FOUND, "竞赛不存在");
|
||
}
|
||
|
||
if (!"draft".equals(contest.getStatus())) {
|
||
throw new BusinessException(ResultCode.PARAM_ERROR, "只有草稿状态的竞赛可以发布");
|
||
}
|
||
|
||
LocalDateTime now = LocalDateTime.now();
|
||
if (contest.getStartTime().isBefore(now)) {
|
||
contest.setStatus("active");
|
||
} else {
|
||
contest.setStatus("scheduled");
|
||
}
|
||
|
||
contestMapper.updateById(contest);
|
||
|
||
// 初始化参与者
|
||
initParticipants(contest);
|
||
|
||
// 发送通知
|
||
notifyContestStart(contest);
|
||
|
||
return contest;
|
||
}
|
||
|
||
/**
|
||
* 初始化竞赛参与者
|
||
*/
|
||
private void initParticipants(Contest contest) {
|
||
List<User> users;
|
||
|
||
if ("all".equals(contest.getParticipantScope())) {
|
||
// 全体设计师
|
||
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
|
||
wrapper.eq(User::getDeleted, 0);
|
||
users = userMapper.selectList(wrapper);
|
||
} else if ("department".equals(contest.getParticipantScope())) {
|
||
// 指定部门
|
||
try {
|
||
List<Long> deptIds = objectMapper.readValue(contest.getDepartmentIds(),
|
||
new TypeReference<List<Long>>() {});
|
||
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
|
||
wrapper.in(User::getDeptId, deptIds).eq(User::getDeleted, 0);
|
||
users = userMapper.selectList(wrapper);
|
||
} catch (JsonProcessingException e) {
|
||
throw new BusinessException(ResultCode.PARAM_ERROR, "部门ID格式错误");
|
||
}
|
||
} else {
|
||
// 指定用户
|
||
try {
|
||
List<Long> userIds = objectMapper.readValue(contest.getUserIds(),
|
||
new TypeReference<List<Long>>() {});
|
||
users = userMapper.selectBatchIds(userIds);
|
||
} catch (JsonProcessingException e) {
|
||
throw new BusinessException(ResultCode.PARAM_ERROR, "用户ID格式错误");
|
||
}
|
||
}
|
||
|
||
for (User user : users) {
|
||
ContestParticipant participant = new ContestParticipant();
|
||
participant.setContestId(contest.getId());
|
||
participant.setUserId(user.getId());
|
||
participant.setUserName(user.getRealName());
|
||
participant.setDeptId(user.getDeptId());
|
||
participant.setDeptName(user.getDeptName());
|
||
participant.setTaskCompleted(0);
|
||
participant.setFiveStarCount(0);
|
||
participant.setEarlyCompletionCount(0);
|
||
participant.setPraiseCount(0);
|
||
participant.setNoDelayDays(0);
|
||
participant.setTotalScore(0);
|
||
participant.setCurrentRank(0);
|
||
participant.setRewardClaimed(0);
|
||
participant.setRewardScore(0);
|
||
participantMapper.insert(participant);
|
||
}
|
||
|
||
// 更新参与人数
|
||
contest.setParticipantCount(users.size());
|
||
contestMapper.updateById(contest);
|
||
}
|
||
|
||
/**
|
||
* 发送竞赛开始通知
|
||
*/
|
||
private void notifyContestStart(Contest contest) {
|
||
LambdaQueryWrapper<ContestParticipant> wrapper = new LambdaQueryWrapper<>();
|
||
wrapper.eq(ContestParticipant::getContestId, contest.getId());
|
||
List<ContestParticipant> participants = participantMapper.selectList(wrapper);
|
||
|
||
for (ContestParticipant p : participants) {
|
||
notificationService.sendNotification(
|
||
p.getUserId(),
|
||
"竞赛活动开始",
|
||
String.format("竞赛「%s」已开始!快来参与吧!", contest.getName()),
|
||
"contest",
|
||
contest.getId()
|
||
);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取竞赛列表
|
||
*/
|
||
public IPage<Contest> getContestList(Integer page, Integer pageSize, String status) {
|
||
Page<Contest> pageParam = new Page<>(page, pageSize);
|
||
LambdaQueryWrapper<Contest> wrapper = new LambdaQueryWrapper<>();
|
||
|
||
if (status != null && !status.isEmpty()) {
|
||
wrapper.eq(Contest::getStatus, status);
|
||
}
|
||
|
||
wrapper.orderByDesc(Contest::getCreateTime);
|
||
return contestMapper.selectPage(pageParam, wrapper);
|
||
}
|
||
|
||
/**
|
||
* 获取竞赛详情
|
||
*/
|
||
public Contest getContestById(Long id) {
|
||
Contest contest = contestMapper.selectById(id);
|
||
if (contest == null) {
|
||
throw new BusinessException(ResultCode.NOT_FOUND, "竞赛不存在");
|
||
}
|
||
return contest;
|
||
}
|
||
|
||
/**
|
||
* 获取竞赛排行榜
|
||
*/
|
||
public List<ContestParticipant> getLeaderboard(Long contestId, Integer limit) {
|
||
List<ContestParticipant> list = participantMapper.getLeaderboard(contestId);
|
||
if (limit != null && list.size() > limit) {
|
||
return list.subList(0, limit);
|
||
}
|
||
return list;
|
||
}
|
||
|
||
/**
|
||
* 获取用户在竞赛中的排名和数据
|
||
*/
|
||
public ContestParticipant getUserContestData(Long contestId, Long userId) {
|
||
LambdaQueryWrapper<ContestParticipant> wrapper = new LambdaQueryWrapper<>();
|
||
wrapper.eq(ContestParticipant::getContestId, contestId)
|
||
.eq(ContestParticipant::getUserId, userId);
|
||
ContestParticipant participant = participantMapper.selectOne(wrapper);
|
||
|
||
if (participant != null) {
|
||
// 计算排名
|
||
Integer rank = participantMapper.getUserRank(contestId, userId);
|
||
participant.setCurrentRank(rank);
|
||
}
|
||
|
||
return participant;
|
||
}
|
||
|
||
/**
|
||
* 更新参与者数据(当任务完成时调用)
|
||
*/
|
||
@Transactional
|
||
public void updateParticipantStats(Long userId, boolean fiveStar, boolean earlyCompletion) {
|
||
// 获取用户参与的所有进行中的竞赛
|
||
LocalDateTime now = LocalDateTime.now();
|
||
LambdaQueryWrapper<Contest> contestWrapper = new LambdaQueryWrapper<>();
|
||
contestWrapper.eq(Contest::getStatus, "active");
|
||
List<Contest> activeContests = contestMapper.selectList(contestWrapper);
|
||
|
||
for (Contest contest : activeContests) {
|
||
LambdaQueryWrapper<ContestParticipant> wrapper = new LambdaQueryWrapper<>();
|
||
wrapper.eq(ContestParticipant::getContestId, contest.getId())
|
||
.eq(ContestParticipant::getUserId, userId);
|
||
ContestParticipant participant = participantMapper.selectOne(wrapper);
|
||
|
||
if (participant != null) {
|
||
participant.setTaskCompleted(participant.getTaskCompleted() + 1);
|
||
if (fiveStar) {
|
||
participant.setFiveStarCount(participant.getFiveStarCount() + 1);
|
||
}
|
||
if (earlyCompletion) {
|
||
participant.setEarlyCompletionCount(participant.getEarlyCompletionCount() + 1);
|
||
}
|
||
|
||
// 重新计算总分
|
||
int totalScore = calculateScore(contest, participant);
|
||
participant.setTotalScore(totalScore);
|
||
|
||
participantMapper.updateById(participant);
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 计算参与者得分
|
||
*/
|
||
private int calculateScore(Contest contest, ContestParticipant participant) {
|
||
try {
|
||
Map<String, Map<String, Integer>> rules = objectMapper.readValue(
|
||
contest.getRules(), new TypeReference<Map<String, Map<String, Integer>>>() {});
|
||
|
||
int score = 0;
|
||
|
||
if (rules.containsKey("taskCompleted")) {
|
||
score += participant.getTaskCompleted() * rules.get("taskCompleted").getOrDefault("score", 2);
|
||
}
|
||
if (rules.containsKey("fiveStarReview")) {
|
||
score += participant.getFiveStarCount() * rules.get("fiveStarReview").getOrDefault("score", 10);
|
||
}
|
||
if (rules.containsKey("earlyCompletion")) {
|
||
score += participant.getEarlyCompletionCount() * rules.get("earlyCompletion").getOrDefault("score", 5);
|
||
}
|
||
if (rules.containsKey("praise")) {
|
||
score += participant.getPraiseCount() * rules.get("praise").getOrDefault("score", 15);
|
||
}
|
||
|
||
return score;
|
||
} catch (JsonProcessingException e) {
|
||
log.error("解析竞赛规则失败", e);
|
||
return 0;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 结束竞赛并发放奖励
|
||
*/
|
||
@Transactional
|
||
public void endContest(Long id) {
|
||
Contest contest = contestMapper.selectById(id);
|
||
if (contest == null) {
|
||
throw new BusinessException(ResultCode.NOT_FOUND, "竞赛不存在");
|
||
}
|
||
|
||
if (!"active".equals(contest.getStatus())) {
|
||
throw new BusinessException(ResultCode.PARAM_ERROR, "只有进行中的竞赛可以结束");
|
||
}
|
||
|
||
contest.setStatus("ended");
|
||
contestMapper.updateById(contest);
|
||
|
||
// 发放奖励
|
||
distributeRewards(contest);
|
||
|
||
log.info("竞赛结束: id={}, name={}", contest.getId(), contest.getName());
|
||
}
|
||
|
||
/**
|
||
* 发放竞赛奖励
|
||
*/
|
||
private void distributeRewards(Contest contest) {
|
||
try {
|
||
List<Map<String, Object>> rewards = objectMapper.readValue(
|
||
contest.getRewards(), new TypeReference<List<Map<String, Object>>>() {});
|
||
|
||
List<ContestParticipant> leaderboard = participantMapper.getLeaderboard(contest.getId());
|
||
|
||
for (int i = 0; i < leaderboard.size(); i++) {
|
||
ContestParticipant participant = leaderboard.get(i);
|
||
int rank = i + 1;
|
||
participant.setCurrentRank(rank);
|
||
|
||
// 查找对应排名的奖励
|
||
for (Map<String, Object> reward : rewards) {
|
||
int rewardRank = (Integer) reward.get("rank");
|
||
if (rank == rewardRank) {
|
||
int rewardScore = (Integer) reward.get("score");
|
||
participant.setRewardScore(rewardScore);
|
||
|
||
// 发送通知
|
||
notificationService.sendNotification(
|
||
participant.getUserId(),
|
||
"竞赛获奖通知",
|
||
String.format("恭喜!您在竞赛「%s」中获得第%d名,奖励%d积分!",
|
||
contest.getName(), rank, rewardScore),
|
||
"contest",
|
||
contest.getId()
|
||
);
|
||
break;
|
||
}
|
||
}
|
||
|
||
participantMapper.updateById(participant);
|
||
}
|
||
} catch (JsonProcessingException e) {
|
||
log.error("解析竞赛奖励配置失败", e);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 领取竞赛奖励
|
||
*/
|
||
@Transactional
|
||
public boolean claimReward(Long contestId, Long userId) {
|
||
LambdaQueryWrapper<ContestParticipant> wrapper = new LambdaQueryWrapper<>();
|
||
wrapper.eq(ContestParticipant::getContestId, contestId)
|
||
.eq(ContestParticipant::getUserId, userId);
|
||
ContestParticipant participant = participantMapper.selectOne(wrapper);
|
||
|
||
if (participant == null) {
|
||
throw new BusinessException(ResultCode.NOT_FOUND, "您未参与此竞赛");
|
||
}
|
||
|
||
if (participant.getRewardClaimed() == 1) {
|
||
throw new BusinessException(ResultCode.PARAM_ERROR, "奖励已领取");
|
||
}
|
||
|
||
if (participant.getRewardScore() == null || participant.getRewardScore() <= 0) {
|
||
throw new BusinessException(ResultCode.PARAM_ERROR, "暂无奖励可领取");
|
||
}
|
||
|
||
// 更新用户积分
|
||
User user = userMapper.selectById(userId);
|
||
int currentScore = user.getScore() != null ? user.getScore() : 0;
|
||
user.setScore(currentScore + participant.getRewardScore());
|
||
userMapper.updateById(user);
|
||
|
||
// 标记已领取
|
||
participant.setRewardClaimed(1);
|
||
participant.setRewardClaimTime(LocalDateTime.now());
|
||
participantMapper.updateById(participant);
|
||
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* 定时任务:自动开始和结束竞赛
|
||
*/
|
||
@Scheduled(fixedRate = 60000) // 每分钟检查一次
|
||
@Transactional
|
||
public void checkContestStatus() {
|
||
LocalDateTime now = LocalDateTime.now();
|
||
|
||
// 自动开始待开始的竞赛
|
||
LambdaQueryWrapper<Contest> startWrapper = new LambdaQueryWrapper<>();
|
||
startWrapper.eq(Contest::getStatus, "scheduled")
|
||
.le(Contest::getStartTime, now);
|
||
List<Contest> toStart = contestMapper.selectList(startWrapper);
|
||
|
||
for (Contest contest : toStart) {
|
||
contest.setStatus("active");
|
||
contestMapper.updateById(contest);
|
||
notifyContestStart(contest);
|
||
log.info("竞赛自动开始: id={}, name={}", contest.getId(), contest.getName());
|
||
}
|
||
|
||
// 自动结束已到期的竞赛
|
||
LambdaQueryWrapper<Contest> endWrapper = new LambdaQueryWrapper<>();
|
||
endWrapper.eq(Contest::getStatus, "active")
|
||
.le(Contest::getEndTime, now);
|
||
List<Contest> toEnd = contestMapper.selectList(endWrapper);
|
||
|
||
for (Contest contest : toEnd) {
|
||
endContest(contest.getId());
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 取消竞赛
|
||
*/
|
||
@Transactional
|
||
public void cancelContest(Long id) {
|
||
Contest contest = contestMapper.selectById(id);
|
||
if (contest == null) {
|
||
throw new BusinessException(ResultCode.NOT_FOUND, "竞赛不存在");
|
||
}
|
||
|
||
if ("ended".equals(contest.getStatus())) {
|
||
throw new BusinessException(ResultCode.PARAM_ERROR, "已结束的竞赛不能取消");
|
||
}
|
||
|
||
contest.setStatus("cancelled");
|
||
contestMapper.updateById(contest);
|
||
|
||
log.info("竞赛已取消: id={}, name={}", contest.getId(), contest.getName());
|
||
}
|
||
|
||
/**
|
||
* 删除竞赛
|
||
*/
|
||
@Transactional
|
||
public void deleteContest(Long id) {
|
||
Contest contest = contestMapper.selectById(id);
|
||
if (contest == null) {
|
||
throw new BusinessException(ResultCode.NOT_FOUND, "竞赛不存在");
|
||
}
|
||
|
||
if ("active".equals(contest.getStatus())) {
|
||
throw new BusinessException(ResultCode.PARAM_ERROR, "进行中的竞赛不能删除");
|
||
}
|
||
|
||
contestMapper.deleteById(id);
|
||
|
||
// 删除参与者数据
|
||
LambdaQueryWrapper<ContestParticipant> wrapper = new LambdaQueryWrapper<>();
|
||
wrapper.eq(ContestParticipant::getContestId, id);
|
||
participantMapper.delete(wrapper);
|
||
}
|
||
}
|