2026-04-14 13:45:15 +08:00
|
|
|
|
package com.label.service;
|
2026-04-09 15:36:11 +08:00
|
|
|
|
|
|
|
|
|
|
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
|
|
|
|
|
import com.label.common.exception.BusinessException;
|
|
|
|
|
|
import com.label.common.shiro.TokenPrincipal;
|
|
|
|
|
|
import com.label.common.statemachine.StateValidator;
|
|
|
|
|
|
import com.label.common.statemachine.TaskStatus;
|
2026-04-14 13:39:24 +08:00
|
|
|
|
import com.label.entity.AnnotationTask;
|
|
|
|
|
|
import com.label.entity.AnnotationTaskHistory;
|
|
|
|
|
|
import com.label.mapper.AnnotationTaskMapper;
|
|
|
|
|
|
import com.label.mapper.TaskHistoryMapper;
|
2026-04-14 15:26:08 +08:00
|
|
|
|
import com.label.util.RedisUtil;
|
2026-04-14 14:59:46 +08:00
|
|
|
|
|
2026-04-09 15:36:11 +08:00
|
|
|
|
import lombok.RequiredArgsConstructor;
|
|
|
|
|
|
import lombok.extern.slf4j.Slf4j;
|
|
|
|
|
|
import org.springframework.http.HttpStatus;
|
|
|
|
|
|
import org.springframework.stereotype.Service;
|
|
|
|
|
|
import org.springframework.transaction.annotation.Transactional;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2026-04-14 13:31:50 +08:00
|
|
|
|
* 任务领取/放弃/重领服务。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 并发安全设计:
|
|
|
|
|
|
* 1. Redis SET NX 作为分布式预锁(TTL 30s),快速拒绝并发请求
|
|
|
|
|
|
* 2. DB UPDATE WHERE status='UNCLAIMED' 作为兜底原子操作
|
|
|
|
|
|
* 两层防护确保同一任务只有一人可领取
|
2026-04-09 15:36:11 +08:00
|
|
|
|
*/
|
|
|
|
|
|
@Slf4j
|
|
|
|
|
|
@Service
|
|
|
|
|
|
@RequiredArgsConstructor
|
|
|
|
|
|
public class TaskClaimService {
|
|
|
|
|
|
|
2026-04-14 13:31:50 +08:00
|
|
|
|
/** Redis 分布式锁 TTL(秒) */
|
2026-04-09 15:36:11 +08:00
|
|
|
|
private static final long CLAIM_LOCK_TTL = 30L;
|
|
|
|
|
|
|
|
|
|
|
|
private final AnnotationTaskMapper taskMapper;
|
|
|
|
|
|
private final TaskHistoryMapper historyMapper;
|
|
|
|
|
|
private final RedisService redisService;
|
|
|
|
|
|
|
2026-04-14 13:31:50 +08:00
|
|
|
|
// ------------------------------------------------------------------ 领取 --
|
2026-04-09 15:36:11 +08:00
|
|
|
|
|
|
|
|
|
|
/**
|
2026-04-14 13:31:50 +08:00
|
|
|
|
* 领取任务(双重防护:Redis NX + DB 原子更新)。
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param taskId 任务 ID
|
|
|
|
|
|
* @param principal 当前用户
|
|
|
|
|
|
* @throws BusinessException TASK_CLAIMED(409) 任务已被他人领取
|
2026-04-09 15:36:11 +08:00
|
|
|
|
*/
|
|
|
|
|
|
@Transactional
|
|
|
|
|
|
public void claim(Long taskId, TokenPrincipal principal) {
|
2026-04-14 15:26:08 +08:00
|
|
|
|
String lockKey = RedisUtil.taskClaimKey(taskId);
|
2026-04-09 15:36:11 +08:00
|
|
|
|
|
2026-04-14 13:31:50 +08:00
|
|
|
|
// 1. Redis SET NX 预锁(快速失败)
|
2026-04-09 15:36:11 +08:00
|
|
|
|
boolean lockAcquired = redisService.setIfAbsent(
|
|
|
|
|
|
lockKey, principal.getUserId().toString(), CLAIM_LOCK_TTL);
|
|
|
|
|
|
if (!lockAcquired) {
|
2026-04-14 13:31:50 +08:00
|
|
|
|
throw new BusinessException("TASK_CLAIMED", "任务已被他人领取,请选择其他任务", HttpStatus.CONFLICT);
|
2026-04-09 15:36:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-09 19:42:20 +08:00
|
|
|
|
try {
|
2026-04-14 13:31:50 +08:00
|
|
|
|
// 2. DB 原子更新(WHERE status='UNCLAIMED' 兜底)
|
|
|
|
|
|
int affected = taskMapper.claimTask(taskId, principal.getUserId(), principal.getCompanyId());
|
2026-04-09 19:42:20 +08:00
|
|
|
|
if (affected == 0) {
|
2026-04-14 13:31:50 +08:00
|
|
|
|
// DB 更新失败说明任务状态已变,清除刚设置的锁
|
|
|
|
|
|
redisService.delete(lockKey);
|
|
|
|
|
|
throw new BusinessException("TASK_CLAIMED", "任务已被他人领取,请选择其他任务", HttpStatus.CONFLICT);
|
2026-04-09 19:42:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-14 13:31:50 +08:00
|
|
|
|
// 3. 写入状态历史
|
|
|
|
|
|
insertHistory(taskId, principal.getCompanyId(),
|
2026-04-09 19:42:20 +08:00
|
|
|
|
"UNCLAIMED", "IN_PROGRESS",
|
|
|
|
|
|
principal.getUserId(), principal.getRole(), null);
|
|
|
|
|
|
|
2026-04-14 13:31:50 +08:00
|
|
|
|
log.info("任务领取成功: taskId={}, userId={}", taskId, principal.getUserId());
|
2026-04-09 19:42:20 +08:00
|
|
|
|
} catch (BusinessException e) {
|
2026-04-14 13:31:50 +08:00
|
|
|
|
throw e; // 业务异常直接上抛,锁已在上方清除
|
2026-04-09 19:42:20 +08:00
|
|
|
|
} catch (Exception e) {
|
2026-04-14 13:31:50 +08:00
|
|
|
|
// DB 写入异常(含 insertHistory 失败):清除 Redis 锁,事务回滚
|
2026-04-09 15:36:11 +08:00
|
|
|
|
redisService.delete(lockKey);
|
2026-04-09 19:42:20 +08:00
|
|
|
|
throw e;
|
2026-04-09 15:36:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-14 13:31:50 +08:00
|
|
|
|
// ------------------------------------------------------------------ 放弃 --
|
2026-04-09 15:36:11 +08:00
|
|
|
|
|
|
|
|
|
|
/**
|
2026-04-14 13:31:50 +08:00
|
|
|
|
* 放弃任务(IN_PROGRESS → UNCLAIMED)。
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param taskId 任务 ID
|
|
|
|
|
|
* @param principal 当前用户
|
2026-04-09 15:36:11 +08:00
|
|
|
|
*/
|
|
|
|
|
|
@Transactional
|
|
|
|
|
|
public void unclaim(Long taskId, TokenPrincipal principal) {
|
|
|
|
|
|
AnnotationTask task = taskMapper.selectById(taskId);
|
|
|
|
|
|
validateTaskExists(task, taskId);
|
|
|
|
|
|
|
|
|
|
|
|
StateValidator.assertTransition(TaskStatus.TRANSITIONS,
|
|
|
|
|
|
TaskStatus.valueOf(task.getStatus()), TaskStatus.UNCLAIMED);
|
|
|
|
|
|
|
|
|
|
|
|
taskMapper.update(null, new LambdaUpdateWrapper<AnnotationTask>()
|
|
|
|
|
|
.eq(AnnotationTask::getId, taskId)
|
|
|
|
|
|
.set(AnnotationTask::getStatus, "UNCLAIMED")
|
|
|
|
|
|
.set(AnnotationTask::getClaimedBy, null)
|
|
|
|
|
|
.set(AnnotationTask::getClaimedAt, null));
|
|
|
|
|
|
|
2026-04-14 13:31:50 +08:00
|
|
|
|
// 清除 Redis 分布式锁
|
2026-04-14 15:26:08 +08:00
|
|
|
|
redisService.delete(RedisUtil.taskClaimKey(taskId));
|
2026-04-09 15:36:11 +08:00
|
|
|
|
|
|
|
|
|
|
insertHistory(taskId, principal.getCompanyId(),
|
|
|
|
|
|
"IN_PROGRESS", "UNCLAIMED",
|
|
|
|
|
|
principal.getUserId(), principal.getRole(), null);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-14 13:31:50 +08:00
|
|
|
|
// ------------------------------------------------------------------ 重领 --
|
2026-04-09 15:36:11 +08:00
|
|
|
|
|
|
|
|
|
|
/**
|
2026-04-14 13:31:50 +08:00
|
|
|
|
* 重领任务(REJECTED → IN_PROGRESS,仅原领取人可重领)。
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param taskId 任务 ID
|
|
|
|
|
|
* @param principal 当前用户
|
2026-04-09 15:36:11 +08:00
|
|
|
|
*/
|
|
|
|
|
|
@Transactional
|
|
|
|
|
|
public void reclaim(Long taskId, TokenPrincipal principal) {
|
|
|
|
|
|
AnnotationTask task = taskMapper.selectById(taskId);
|
|
|
|
|
|
validateTaskExists(task, taskId);
|
|
|
|
|
|
|
|
|
|
|
|
if (!"REJECTED".equals(task.getStatus())) {
|
|
|
|
|
|
throw new BusinessException("INVALID_STATE_TRANSITION",
|
2026-04-14 13:31:50 +08:00
|
|
|
|
"只有 REJECTED 状态的任务可以重领", HttpStatus.CONFLICT);
|
2026-04-09 15:36:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!principal.getUserId().equals(task.getClaimedBy())) {
|
|
|
|
|
|
throw new BusinessException("FORBIDDEN",
|
2026-04-14 13:31:50 +08:00
|
|
|
|
"只有原领取人可以重领该任务", HttpStatus.FORBIDDEN);
|
2026-04-09 15:36:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
StateValidator.assertTransition(TaskStatus.TRANSITIONS,
|
|
|
|
|
|
TaskStatus.valueOf(task.getStatus()), TaskStatus.IN_PROGRESS);
|
|
|
|
|
|
|
|
|
|
|
|
taskMapper.update(null, new LambdaUpdateWrapper<AnnotationTask>()
|
|
|
|
|
|
.eq(AnnotationTask::getId, taskId)
|
2026-04-09 19:42:20 +08:00
|
|
|
|
.eq(AnnotationTask::getStatus, "REJECTED")
|
2026-04-09 15:36:11 +08:00
|
|
|
|
.set(AnnotationTask::getStatus, "IN_PROGRESS")
|
|
|
|
|
|
.set(AnnotationTask::getClaimedAt, java.time.LocalDateTime.now()));
|
|
|
|
|
|
|
2026-04-14 13:31:50 +08:00
|
|
|
|
// 重新设置 Redis 锁(防止并发再次争抢)
|
|
|
|
|
|
redisService.setIfAbsent(
|
2026-04-14 15:26:08 +08:00
|
|
|
|
RedisUtil.taskClaimKey(taskId),
|
2026-04-09 15:36:11 +08:00
|
|
|
|
principal.getUserId().toString(), CLAIM_LOCK_TTL);
|
|
|
|
|
|
|
|
|
|
|
|
insertHistory(taskId, principal.getCompanyId(),
|
|
|
|
|
|
"REJECTED", "IN_PROGRESS",
|
|
|
|
|
|
principal.getUserId(), principal.getRole(), null);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-14 13:31:50 +08:00
|
|
|
|
// ------------------------------------------------------------------ 私有工具 --
|
2026-04-09 15:36:11 +08:00
|
|
|
|
|
|
|
|
|
|
private void validateTaskExists(AnnotationTask task, Long taskId) {
|
|
|
|
|
|
if (task == null) {
|
2026-04-14 13:31:50 +08:00
|
|
|
|
throw new BusinessException("NOT_FOUND", "任务不存在: " + taskId, HttpStatus.NOT_FOUND);
|
2026-04-09 15:36:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2026-04-14 13:31:50 +08:00
|
|
|
|
* 向 annotation_task_history 追加一条历史记录(仅 INSERT,禁止 UPDATE/DELETE)。
|
|
|
|
|
|
*/
|
2026-04-09 15:36:11 +08:00
|
|
|
|
public void insertHistory(Long taskId, Long companyId,
|
|
|
|
|
|
String fromStatus, String toStatus,
|
|
|
|
|
|
Long operatorId, String operatorRole, String comment) {
|
|
|
|
|
|
historyMapper.insert(AnnotationTaskHistory.builder()
|
|
|
|
|
|
.taskId(taskId)
|
|
|
|
|
|
.companyId(companyId)
|
|
|
|
|
|
.fromStatus(fromStatus)
|
|
|
|
|
|
.toStatus(toStatus)
|
|
|
|
|
|
.operatorId(operatorId)
|
|
|
|
|
|
.operatorRole(operatorRole)
|
|
|
|
|
|
.comment(comment)
|
|
|
|
|
|
.build());
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|