3 Commits

Author SHA1 Message Date
litianxiang
b1e6183dd1 GlobalAward接口token验证,id更换为uuid 2026-01-21 14:34:43 +08:00
30d08356c0 Merge branch 'dev/dev_xp' into dev/3.1_release_merge 2026-01-21 14:14:17 +08:00
64cc29f456 TASK:Global Award邮箱验证 2026-01-21 14:13:33 +08:00
9 changed files with 142 additions and 35 deletions

View File

@@ -76,7 +76,9 @@ public class GlobalAwardController {
UploadCompleteResponse uploadCompleteResponse = uploadService.completePdfUpload( UploadCompleteResponse uploadCompleteResponse = uploadService.completePdfUpload(
request.getUploadId(), request.getUploadId(),
request.getFileName(), request.getFileName(),
request.getTotalSize()); request.getTotalSize(),
request.getEmail(),
request.getSecureToken());
return Response.success(uploadCompleteResponse); return Response.success(uploadCompleteResponse);
} }
@@ -123,7 +125,9 @@ public class GlobalAwardController {
UploadCompleteResponse uploadCompleteResponse = uploadService.completeVideoUpload( UploadCompleteResponse uploadCompleteResponse = uploadService.completeVideoUpload(
request.getUploadId(), request.getUploadId(),
request.getFileName(), request.getFileName(),
request.getTotalSize()); request.getTotalSize(),
request.getEmail(),
request.getSecureToken());
return Response.success(uploadCompleteResponse); return Response.success(uploadCompleteResponse);
} }
@@ -141,12 +145,12 @@ public class GlobalAwardController {
return Response.success(globalAwardService.saveContestant(request)); return Response.success(globalAwardService.saveContestant(request));
} }
@GetMapping("/contestants/by-email") // @GetMapping("/contestants/by-email")
@ApiOperation(value = "根据邮箱查询参赛者", notes = "根据邮箱地址获取参赛者信息") // @ApiOperation(value = "根据邮箱查询参赛者", notes = "根据邮箱地址获取参赛者信息")
public Response<ContestantDTO> getContestantByEmail(@ApiParam(value = "参赛者邮箱地址", required = true) @RequestParam("email") String email) { // public Response<ContestantDTO> getContestantByEmail(@ApiParam(value = "参赛者邮箱地址", required = true) @RequestParam("email") String email) {
ContestantDTO dto = globalAwardService.getContestantByEmail(email); // ContestantDTO dto = globalAwardService.getContestantByEmail(email);
return Response.success(dto); // return Response.success(dto);
} // }
@GetMapping("/checkEmail") @GetMapping("/checkEmail")
public Response<String> checkEmail(@RequestParam("email") String email) { public Response<String> checkEmail(@RequestParam("email") String email) {
@@ -155,8 +159,8 @@ public class GlobalAwardController {
} }
@GetMapping("/checkCode") @GetMapping("/checkCode")
public Response<CheckOTPVO> checkOTP(@RequestParam("email") String email, @RequestParam("code") String code) { public Response<CheckOTPVO> checkCode(@RequestParam("email") String email, @RequestParam("code") String code) {
return Response.success(globalAwardService.checkOTP(email, code)); return Response.success(globalAwardService.checkCode(email, code));
} }
} }

View File

@@ -18,11 +18,11 @@ import java.time.LocalDateTime;
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
@Builder @Builder
@TableName("submissions") @TableName("contestants")
public class Contestant { public class Contestant {
@TableId(value = "id", type = IdType.AUTO) @TableId(value = "id", type = IdType.ASSIGN_UUID)
private Long id; private String id;
private String email; private String email;

View File

@@ -50,11 +50,11 @@ public class ContestantDTO {
@ApiModelProperty(value = "视频文件路径", required = false, example = "contestants/user@example.com/2024/01/video_1234567890.mp4") @ApiModelProperty(value = "视频文件路径", required = false, example = "contestants/user@example.com/2024/01/video_1234567890.mp4")
private String videoPath; private String videoPath;
/** // /**
* 是否确认覆盖已存在记录false 表示发现已有记录时仅返回 existingRecord不覆盖 // * 是否确认覆盖已存在记录false 表示发现已有记录时仅返回 existingRecord不覆盖
*/ // */
@ApiModelProperty(value = "是否确认覆盖已存在记录", required = false, example = "false") // @ApiModelProperty(value = "是否确认覆盖已存在记录", required = false, example = "false")
private Boolean confirm = false; // private Boolean confirm = false;
@NotBlank @NotBlank
private String secureToken; private String secureToken;

View File

@@ -36,4 +36,18 @@ public class UploadCompleteRequest {
@Positive(message = "文件大小必须大于0") @Positive(message = "文件大小必须大于0")
@ApiModelProperty(value = "文件总大小(字节)", required = true, example = "10485760") @ApiModelProperty(value = "文件总大小(字节)", required = true, example = "10485760")
private Long totalSize; private Long totalSize;
/**
* 用户邮箱
*/
@NotBlank(message = "用户邮箱不能为空")
@ApiModelProperty(value = "用户邮箱", required = true, example = "user@example.com")
private String email;
/**
* 安全令牌(邮箱验证令牌)
*/
@NotBlank(message = "安全令牌不能为空")
@ApiModelProperty(value = "安全令牌", required = true, example = "abc123def456")
private String secureToken;
} }

View File

@@ -41,6 +41,13 @@ public class UploadInitRequest {
/** /**
* 用户邮箱 * 用户邮箱
*/ */
@ApiModelProperty(value = "用户邮箱", required = false, example = "user@example.com") @ApiModelProperty(value = "用户邮箱", required = true, example = "user@example.com")
private String email; private String email;
/**
* 安全令牌(邮箱验证令牌)
*/
@NotBlank(message = "安全令牌不能为空")
@ApiModelProperty(value = "安全令牌", required = true, example = "abc123def456")
private String secureToken;
} }

View File

@@ -17,7 +17,9 @@ public interface GlobalAwardService {
void checkEmail(String email); void checkEmail(String email);
CheckOTPVO checkOTP(String email, String otp); CheckOTPVO checkCode(String email, String otp);
void checkSecurityToken(String email, String securityToken);
} }

View File

@@ -134,13 +134,7 @@ public class GlobalAwardServiceImpl implements GlobalAwardService {
throw new BusinessException("Email is required."); throw new BusinessException("Email is required.");
} }
String key = tokenCacheKey + request.getEmail(); checkSecurityToken(request.getEmail(), request.getSecureToken());
String tokenCache = redisUtil.getFromString(key);
if (StringUtils.isBlank(tokenCache)) {
throw new BusinessException("请先完成邮箱认证");
} else if (!tokenCache.equals(request.getSecureToken())){
throw new BusinessException("身份认证失败,请先完成邮箱认证");
}
QueryWrapper<Contestant> qw = new QueryWrapper<>(); QueryWrapper<Contestant> qw = new QueryWrapper<>();
qw.eq("email", request.getEmail()); qw.eq("email", request.getEmail());
@@ -217,6 +211,10 @@ public class GlobalAwardServiceImpl implements GlobalAwardService {
return dto; return dto;
} }
/**
* 检查邮箱是否符合申请要求,发送验证码
* @param email AiDA邮箱
*/
public void checkEmail(String email) { public void checkEmail(String email) {
List<Integer> validRole = Arrays.asList(1, 2, 7, 8); List<Integer> validRole = Arrays.asList(1, 2, 7, 8);
// 1. 验证邮箱在aida中有无账号 // 1. 验证邮箱在aida中有无账号
@@ -224,7 +222,7 @@ public class GlobalAwardServiceImpl implements GlobalAwardService {
queryWrapper.lambda().eq(Account::getUserEmail, email); queryWrapper.lambda().eq(Account::getUserEmail, email);
List<Account> accounts = accountMapper.selectList(queryWrapper); List<Account> accounts = accountMapper.selectList(queryWrapper);
if (accounts.isEmpty()) { if (accounts.isEmpty()) {
throw new BusinessException("请注册并订阅AiDA再重新提交申请"); throw new BusinessException("Please register and subscribe to AiDA, then resubmit your application.");
} }
// 2. 验证账号是否是付费用户如果首次提交是但是修改的时候已经不是了how?不允许修改吗) // 2. 验证账号是否是付费用户如果首次提交是但是修改的时候已经不是了how?不允许修改吗)
@@ -235,11 +233,17 @@ public class GlobalAwardServiceImpl implements GlobalAwardService {
SendEmailUtil.send(email, null, SendEmailUtil.send(email, null,
SendEmailUtil.LOGIN_TEMPLATE_ID, randomVerifyCode); SendEmailUtil.LOGIN_TEMPLATE_ID, randomVerifyCode);
} else { } else {
throw new BusinessException("请订阅AiDA再重新提交申请"); throw new BusinessException("Please subscribe to AiDA, then resubmit your application.");
} }
} }
public CheckOTPVO checkOTP(String email, String otp) { /**
* 验证验证码是否正确
* @param email 邮箱
* @param otp 一次性验证码
* @return 临时token和之前提交的表单内容
*/
public CheckOTPVO checkCode(String email, String otp) {
String otpCache = LocalCacheUtils.getVerifyCodeCache(AuthenticationOperationTypeEnum.GLOBAL_AWARD.name() + "_" + email); String otpCache = LocalCacheUtils.getVerifyCodeCache(AuthenticationOperationTypeEnum.GLOBAL_AWARD.name() + "_" + email);
assert otpCache != null; assert otpCache != null;
if (otpCache.equals(otp)) { if (otpCache.equals(otp)) {
@@ -249,7 +253,24 @@ public class GlobalAwardServiceImpl implements GlobalAwardService {
return new CheckOTPVO(secureToken, getContestantByEmail(email)); return new CheckOTPVO(secureToken, getContestantByEmail(email));
} else { } else {
throw new BusinessException("验证码错误,请重试"); throw new BusinessException("Verification code is incorrect. Please try again.");
}
}
public void checkSecurityToken(String email, String securityToken) {
String key = tokenCacheKey + email;
if (StringUtils.isBlank(securityToken)) {
log.error("security token 缺失");
throw new BusinessException("Please complete email verification first.");
}
String tokenCache = redisUtil.getFromString(key);
if (StringUtils.isBlank(tokenCache)) {
log.error("security token 过期");
throw new BusinessException("Email verification has expired. Please verify again.");
} else if (!tokenCache.equals(securityToken)){
log.error("security token 与缓存不符");
throw new BusinessException("Identity verification failed. Please complete email verification first.");
} }
} }

View File

@@ -37,7 +37,7 @@ public interface UploadService {
* @param totalSize 文件总大小 * @param totalSize 文件总大小
* @return 完成上传结果 * @return 完成上传结果
*/ */
UploadCompleteResponse completePdfUpload(String uploadId, String fileName, long totalSize); UploadCompleteResponse completePdfUpload(String uploadId, String fileName, long totalSize, String email, String secureToken);
/** /**
* 查询PDF上传状态 * 查询PDF上传状态
@@ -74,7 +74,7 @@ public interface UploadService {
* @param totalSize 文件总大小 * @param totalSize 文件总大小
* @return 完成上传结果 * @return 完成上传结果
*/ */
UploadCompleteResponse completeVideoUpload(String uploadId, String fileName, long totalSize); UploadCompleteResponse completeVideoUpload(String uploadId, String fileName, long totalSize, String email, String secureToken);
/** /**
* 查询视频上传状态 * 查询视频上传状态

View File

@@ -14,6 +14,7 @@ import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.IOException; import java.io.IOException;
@@ -62,16 +63,65 @@ public class UploadServiceImpl implements UploadService {
@Value("${minio.bucketName:globalAward}") @Value("${minio.bucketName:globalAward}")
private String minioBucket; private String minioBucket;
@Resource
private com.ai.da.service.GlobalAwardService globalAwardService;
// 内存存储上传任务状态 // 内存存储上传任务状态
private final ConcurrentHashMap<String, UploadTask> uploadTasks = new ConcurrentHashMap<>(); private final ConcurrentHashMap<String, UploadTask> uploadTasks = new ConcurrentHashMap<>();
// JSON序列化工具 // JSON序列化工具
private final ObjectMapper objectMapper = new ObjectMapper(); private final ObjectMapper objectMapper = new ObjectMapper();
/**
* 应用启动时加载现有上传任务
*/
@PostConstruct
public void loadExistingTasks() {
try {
Path tempPath = Paths.get(tempDir);
if (!Files.exists(tempPath)) {
return;
}
Files.list(tempPath)
.filter(Files::isDirectory)
.forEach(uploadDir -> {
try {
String uploadId = uploadDir.getFileName().toString();
Path metadataPath = uploadDir.resolve("metadata.json");
if (Files.exists(metadataPath)) {
String json = Files.readString(metadataPath);
UploadTask task = objectMapper.readValue(json, UploadTask.class);
// 检查任务是否已过期
if (task.getExpiresAt().isAfter(LocalDateTime.now())) {
uploadTasks.put(uploadId, task);
log.info("加载现有上传任务: uploadId={}, status={}", uploadId, task.getStatus());
} else {
// 清理过期任务
cleanupTempFiles(uploadId);
log.info("清理过期上传任务: uploadId={}", uploadId);
}
}
} catch (Exception e) {
log.warn("加载上传任务失败: {}", uploadDir.getFileName(), e);
}
});
log.info("成功加载 {} 个现有上传任务", uploadTasks.size());
} catch (Exception e) {
log.error("加载现有上传任务时发生错误", e);
}
}
// ===== PDF上传实现 ===== // ===== PDF上传实现 =====
@Override @Override
public UploadTask initPdfUpload(UploadInitRequest request) { public UploadTask initPdfUpload(UploadInitRequest request) {
// 验证安全令牌
globalAwardService.checkSecurityToken(request.getEmail(), request.getSecureToken());
// 验证PDF文件 // 验证PDF文件
validatePdfFile(request); validatePdfFile(request);
@@ -115,7 +165,10 @@ public class UploadServiceImpl implements UploadService {
} }
@Override @Override
public UploadCompleteResponse completePdfUpload(String uploadId, String fileName, long totalSize) { public UploadCompleteResponse completePdfUpload(String uploadId, String fileName, long totalSize, String email, String secureToken) {
// 验证安全令牌
globalAwardService.checkSecurityToken(email, secureToken);
UploadTask task = validateAndGetTask(uploadId, "pdf"); UploadTask task = validateAndGetTask(uploadId, "pdf");
log.info("开始PDF文件合并: uploadId={}, fileName={}", uploadId, fileName); log.info("开始PDF文件合并: uploadId={}, fileName={}", uploadId, fileName);
@@ -175,6 +228,9 @@ public class UploadServiceImpl implements UploadService {
@Override @Override
public UploadTask initVideoUpload(UploadInitRequest request) { public UploadTask initVideoUpload(UploadInitRequest request) {
// 验证安全令牌
globalAwardService.checkSecurityToken(request.getEmail(), request.getSecureToken());
// 验证视频文件 // 验证视频文件
validateVideoFile(request); validateVideoFile(request);
@@ -218,7 +274,10 @@ public class UploadServiceImpl implements UploadService {
} }
@Override @Override
public UploadCompleteResponse completeVideoUpload(String uploadId, String fileName, long totalSize) { public UploadCompleteResponse completeVideoUpload(String uploadId, String fileName, long totalSize, String email, String secureToken) {
// 验证安全令牌
globalAwardService.checkSecurityToken(email, secureToken);
UploadTask task = validateAndGetTask(uploadId, "video"); UploadTask task = validateAndGetTask(uploadId, "video");
log.info("开始视频文件合并: uploadId={}, fileName={}", uploadId, fileName); log.info("开始视频文件合并: uploadId={}, fileName={}", uploadId, fileName);