fix+refactor: 代码审查修复(11 项安全/并发缺陷)+ log.debug → log.info(21 处)
代码审查修复:
- MybatisPlusConfig: video_process_job 加入 IGNORED_TABLES(修复回调路径多租户过滤导致全部回调静默丢失)
- TokenFilter: catch(Exception) 替代 catch(NumberFormatException),防止空指针泄漏为 500
- VideoController: createJob 空指针防护 + handleCallback 共享密钥校验(X-Callback-Secret)
- VideoProcessService: handleCallback 显式校验 companyId 非空;triggerAi 失败改为 error 级日志
- ExtractionService/QaService: validateAndGetTask 显式校验 companyId(纵深防御)
- TaskClaimService: reclaim 增加原子 WHERE status='REJECTED';claim 异常时释放 Redis 锁
- TaskService: reassign 校验 targetUserId 属于同一租户
- AuthService: user:sessions:{userId} Set 设置滑动 TTL,防止 Token 无限累积
- ExportService/SourceService: RustFS + DB 非原子操作增加失败回滚清理
- SourceService: getOriginalFilename 使用 Paths.get().getFileName() 防路径遍历
日志规范:
- 11 个 Service 类 21 处 log.debug 替换为 log.info
This commit is contained in:
@@ -19,8 +19,9 @@ public class MybatisPlusConfig {
|
|||||||
|
|
||||||
// Tables that do NOT need tenant isolation (either global or tenant root tables)
|
// Tables that do NOT need tenant isolation (either global or tenant root tables)
|
||||||
private static final List<String> IGNORED_TABLES = Arrays.asList(
|
private static final List<String> IGNORED_TABLES = Arrays.asList(
|
||||||
"sys_company", // the tenant root table itself
|
"sys_company", // the tenant root table itself
|
||||||
"sys_config" // has company_id=NULL for global defaults; service handles this manually
|
"sys_config", // has company_id=NULL for global defaults; service handles this manually
|
||||||
|
"video_process_job" // accessed by unauthenticated callback endpoint; service validates companyId manually
|
||||||
);
|
);
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ public class TokenFilter extends OncePerRequestFilter {
|
|||||||
request.setAttribute("__token_principal__", principal);
|
request.setAttribute("__token_principal__", principal);
|
||||||
|
|
||||||
filterChain.doFilter(request, response);
|
filterChain.doFilter(request, response);
|
||||||
} catch (NumberFormatException e) {
|
} catch (Exception e) {
|
||||||
log.error("解析 Token 数据失败: {}", e.getMessage());
|
log.error("解析 Token 数据失败: {}", e.getMessage());
|
||||||
writeUnauthorized(response, "令牌数据格式错误");
|
writeUnauthorized(response, "令牌数据格式错误");
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -1,22 +1,28 @@
|
|||||||
package com.label.common.storage;
|
package com.label.common.storage;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import java.io.InputStream;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.time.Duration;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import jakarta.annotation.PostConstruct;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
|
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
|
||||||
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
|
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
|
||||||
import software.amazon.awssdk.core.sync.RequestBody;
|
import software.amazon.awssdk.core.sync.RequestBody;
|
||||||
import software.amazon.awssdk.regions.Region;
|
import software.amazon.awssdk.regions.Region;
|
||||||
import software.amazon.awssdk.services.s3.S3Client;
|
import software.amazon.awssdk.services.s3.S3Client;
|
||||||
import software.amazon.awssdk.services.s3.model.*;
|
import software.amazon.awssdk.services.s3.model.CreateBucketRequest;
|
||||||
|
import software.amazon.awssdk.services.s3.model.DeleteObjectRequest;
|
||||||
|
import software.amazon.awssdk.services.s3.model.GetObjectRequest;
|
||||||
|
import software.amazon.awssdk.services.s3.model.HeadBucketRequest;
|
||||||
|
import software.amazon.awssdk.services.s3.model.NoSuchBucketException;
|
||||||
|
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
|
||||||
import software.amazon.awssdk.services.s3.presigner.S3Presigner;
|
import software.amazon.awssdk.services.s3.presigner.S3Presigner;
|
||||||
import software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest;
|
import software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest;
|
||||||
|
|
||||||
import jakarta.annotation.PostConstruct;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.net.URI;
|
|
||||||
import java.time.Duration;
|
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Component
|
@Component
|
||||||
public class RustFsClient {
|
public class RustFsClient {
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ public class ExtractionApprovedEventListener {
|
|||||||
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
|
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
|
||||||
@Transactional(propagation = Propagation.REQUIRES_NEW)
|
@Transactional(propagation = Propagation.REQUIRES_NEW)
|
||||||
public void onExtractionApproved(ExtractionApprovedEvent event) {
|
public void onExtractionApproved(ExtractionApprovedEvent event) {
|
||||||
log.debug("处理提取审批通过事件: taskId={}, sourceId={}", event.getTaskId(), event.getSourceId());
|
log.info("处理提取审批通过事件: taskId={}, sourceId={}", event.getTaskId(), event.getSourceId());
|
||||||
|
|
||||||
// 设置多租户上下文(新事务中 ThreadLocal 已清除)
|
// 设置多租户上下文(新事务中 ThreadLocal 已清除)
|
||||||
CompanyContext.set(event.getCompanyId());
|
CompanyContext.set(event.getCompanyId());
|
||||||
@@ -114,7 +114,7 @@ public class ExtractionApprovedEventListener {
|
|||||||
// 4. 更新 source_data 状态为 QA_REVIEW
|
// 4. 更新 source_data 状态为 QA_REVIEW
|
||||||
sourceDataMapper.updateStatus(event.getSourceId(), "QA_REVIEW", event.getCompanyId());
|
sourceDataMapper.updateStatus(event.getSourceId(), "QA_REVIEW", event.getCompanyId());
|
||||||
|
|
||||||
log.debug("审批通过后续处理完成: taskId={}, 新 QA 任务已创建", event.getTaskId());
|
log.info("审批通过后续处理完成: taskId={}, 新 QA 任务已创建", event.getTaskId());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -248,7 +248,7 @@ public class ExtractionService {
|
|||||||
*/
|
*/
|
||||||
private AnnotationTask validateAndGetTask(Long taskId, Long companyId) {
|
private AnnotationTask validateAndGetTask(Long taskId, Long companyId) {
|
||||||
AnnotationTask task = taskMapper.selectById(taskId);
|
AnnotationTask task = taskMapper.selectById(taskId);
|
||||||
if (task == null) {
|
if (task == null || !companyId.equals(task.getCompanyId())) {
|
||||||
throw new BusinessException("NOT_FOUND", "任务不存在: " + taskId, HttpStatus.NOT_FOUND);
|
throw new BusinessException("NOT_FOUND", "任务不存在: " + taskId, HttpStatus.NOT_FOUND);
|
||||||
}
|
}
|
||||||
return task;
|
return task;
|
||||||
|
|||||||
@@ -193,7 +193,7 @@ public class QaService {
|
|||||||
"SUBMITTED", "APPROVED",
|
"SUBMITTED", "APPROVED",
|
||||||
principal.getUserId(), principal.getRole(), null);
|
principal.getUserId(), principal.getRole(), null);
|
||||||
|
|
||||||
log.debug("QA 审批通过,整条流水线完成: taskId={}, sourceId={}", taskId, task.getSourceId());
|
log.info("QA 审批通过,整条流水线完成: taskId={}, sourceId={}", taskId, task.getSourceId());
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------ 驳回 --
|
// ------------------------------------------------------------------ 驳回 --
|
||||||
@@ -237,7 +237,7 @@ public class QaService {
|
|||||||
|
|
||||||
private AnnotationTask validateAndGetTask(Long taskId, Long companyId) {
|
private AnnotationTask validateAndGetTask(Long taskId, Long companyId) {
|
||||||
AnnotationTask task = taskMapper.selectById(taskId);
|
AnnotationTask task = taskMapper.selectById(taskId);
|
||||||
if (task == null) {
|
if (task == null || !companyId.equals(task.getCompanyId())) {
|
||||||
throw new BusinessException("NOT_FOUND", "任务不存在: " + taskId, HttpStatus.NOT_FOUND);
|
throw new BusinessException("NOT_FOUND", "任务不存在: " + taskId, HttpStatus.NOT_FOUND);
|
||||||
}
|
}
|
||||||
return task;
|
return task;
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ public class SysConfigService {
|
|||||||
}
|
}
|
||||||
existing.setUpdatedAt(LocalDateTime.now());
|
existing.setUpdatedAt(LocalDateTime.now());
|
||||||
configMapper.updateById(existing);
|
configMapper.updateById(existing);
|
||||||
log.debug("公司配置已更新: companyId={}, key={}, value={}", companyId, configKey, value);
|
log.info("公司配置已更新: companyId={}, key={}, value={}", companyId, configKey, value);
|
||||||
return existing;
|
return existing;
|
||||||
} else {
|
} else {
|
||||||
SysConfig cfg = new SysConfig();
|
SysConfig cfg = new SysConfig();
|
||||||
@@ -132,7 +132,7 @@ public class SysConfigService {
|
|||||||
cfg.setConfigValue(value);
|
cfg.setConfigValue(value);
|
||||||
cfg.setDescription(description);
|
cfg.setDescription(description);
|
||||||
configMapper.insert(cfg);
|
configMapper.insert(cfg);
|
||||||
log.debug("公司配置已创建: companyId={}, key={}, value={}", companyId, configKey, value);
|
log.info("公司配置已创建: companyId={}, key={}, value={}", companyId, configKey, value);
|
||||||
return cfg;
|
return cfg;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,14 +90,24 @@ public class ExportService {
|
|||||||
new ByteArrayInputStream(jsonlBytes), jsonlBytes.length,
|
new ByteArrayInputStream(jsonlBytes), jsonlBytes.length,
|
||||||
"application/jsonl");
|
"application/jsonl");
|
||||||
|
|
||||||
// 插入 export_batch 记录
|
// 插入 export_batch 记录(若 DB 写入失败,尝试清理 RustFS 孤儿文件)
|
||||||
ExportBatch batch = new ExportBatch();
|
ExportBatch batch = new ExportBatch();
|
||||||
batch.setCompanyId(principal.getCompanyId());
|
batch.setCompanyId(principal.getCompanyId());
|
||||||
batch.setBatchUuid(batchUuid);
|
batch.setBatchUuid(batchUuid);
|
||||||
batch.setSampleCount(samples.size());
|
batch.setSampleCount(samples.size());
|
||||||
batch.setDatasetFilePath(filePath);
|
batch.setDatasetFilePath(filePath);
|
||||||
batch.setFinetuneStatus("NOT_STARTED");
|
batch.setFinetuneStatus("NOT_STARTED");
|
||||||
exportBatchMapper.insert(batch);
|
try {
|
||||||
|
exportBatchMapper.insert(batch);
|
||||||
|
} catch (Exception e) {
|
||||||
|
// DB 插入失败:尝试删除已上传的 RustFS 文件,防止产生孤儿文件
|
||||||
|
try {
|
||||||
|
rustFsClient.delete(EXPORT_BUCKET, filePath);
|
||||||
|
} catch (Exception deleteEx) {
|
||||||
|
log.error("DB 写入失败后清理 RustFS 文件亦失败,孤儿文件: {}/{}", EXPORT_BUCKET, filePath, deleteEx);
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
// 批量更新 training_dataset.export_batch_id + exported_at
|
// 批量更新 training_dataset.export_batch_id + exported_at
|
||||||
datasetMapper.update(null, new LambdaUpdateWrapper<TrainingDataset>()
|
datasetMapper.update(null, new LambdaUpdateWrapper<TrainingDataset>()
|
||||||
@@ -106,7 +116,7 @@ public class ExportService {
|
|||||||
.set(TrainingDataset::getExportedAt, LocalDateTime.now())
|
.set(TrainingDataset::getExportedAt, LocalDateTime.now())
|
||||||
.set(TrainingDataset::getUpdatedAt, LocalDateTime.now()));
|
.set(TrainingDataset::getUpdatedAt, LocalDateTime.now()));
|
||||||
|
|
||||||
log.debug("导出批次已创建: batchId={}, sampleCount={}, path={}",
|
log.info("导出批次已创建: batchId={}, sampleCount={}, path={}",
|
||||||
batch.getId(), samples.size(), filePath);
|
batch.getId(), samples.size(), filePath);
|
||||||
return batch;
|
return batch;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ public class FinetuneService {
|
|||||||
exportBatchMapper.updateFinetuneInfo(batchId,
|
exportBatchMapper.updateFinetuneInfo(batchId,
|
||||||
response.getJobId(), "RUNNING", principal.getCompanyId());
|
response.getJobId(), "RUNNING", principal.getCompanyId());
|
||||||
|
|
||||||
log.debug("微调任务已提交: batchId={}, glmJobId={}", batchId, response.getJobId());
|
log.info("微调任务已提交: batchId={}, glmJobId={}", batchId, response.getJobId());
|
||||||
|
|
||||||
return Map.of(
|
return Map.of(
|
||||||
"glmJobId", response.getJobId(),
|
"glmJobId", response.getJobId(),
|
||||||
|
|||||||
@@ -62,7 +62,9 @@ public class SourceService {
|
|||||||
throw new BusinessException("INVALID_TYPE", "不支持的资料类型: " + dataType, HttpStatus.BAD_REQUEST);
|
throw new BusinessException("INVALID_TYPE", "不支持的资料类型: " + dataType, HttpStatus.BAD_REQUEST);
|
||||||
}
|
}
|
||||||
|
|
||||||
String originalName = file.getOriginalFilename() != null ? file.getOriginalFilename() : "unknown";
|
// 提取纯文件名,防止路径遍历(如 ../../admin/secret.txt)
|
||||||
|
String rawName = file.getOriginalFilename() != null ? file.getOriginalFilename() : "unknown";
|
||||||
|
String originalName = java.nio.file.Paths.get(rawName).getFileName().toString();
|
||||||
|
|
||||||
// 1. 先插入占位记录,拿到自增 ID
|
// 1. 先插入占位记录,拿到自增 ID
|
||||||
SourceData source = new SourceData();
|
SourceData source = new SourceData();
|
||||||
@@ -89,12 +91,21 @@ public class SourceService {
|
|||||||
throw new BusinessException("UPLOAD_FAILED", "文件上传失败,请重试", HttpStatus.INTERNAL_SERVER_ERROR);
|
throw new BusinessException("UPLOAD_FAILED", "文件上传失败,请重试", HttpStatus.INTERNAL_SERVER_ERROR);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. 更新 filePath
|
// 4. 更新 filePath(若失败则清理 RustFS 孤儿文件)
|
||||||
sourceDataMapper.update(null, new LambdaUpdateWrapper<SourceData>()
|
try {
|
||||||
.eq(SourceData::getId, source.getId())
|
sourceDataMapper.update(null, new LambdaUpdateWrapper<SourceData>()
|
||||||
.set(SourceData::getFilePath, objectKey));
|
.eq(SourceData::getId, source.getId())
|
||||||
|
.set(SourceData::getFilePath, objectKey));
|
||||||
|
} catch (Exception e) {
|
||||||
|
try {
|
||||||
|
rustFsClient.delete(bucket, objectKey);
|
||||||
|
} catch (Exception deleteEx) {
|
||||||
|
log.error("DB 更新失败后清理 RustFS 文件亦失败,孤儿文件: {}/{}", bucket, objectKey, deleteEx);
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
log.debug("资料上传成功: id={}, key={}", source.getId(), objectKey);
|
log.info("资料上传成功: id={}, key={}", source.getId(), objectKey);
|
||||||
return toUploadResponse(source, objectKey);
|
return toUploadResponse(source, objectKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,7 +201,7 @@ public class SourceService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sourceDataMapper.deleteById(id);
|
sourceDataMapper.deleteById(id);
|
||||||
log.debug("资料删除成功: id={}", id);
|
log.info("资料删除成功: id={}", id);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------ 私有工具 --
|
// ------------------------------------------------------------------ 私有工具 --
|
||||||
|
|||||||
@@ -57,20 +57,28 @@ public class TaskClaimService {
|
|||||||
throw new BusinessException("TASK_CLAIMED", "任务已被他人领取,请选择其他任务", HttpStatus.CONFLICT);
|
throw new BusinessException("TASK_CLAIMED", "任务已被他人领取,请选择其他任务", HttpStatus.CONFLICT);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. DB 原子更新(WHERE status='UNCLAIMED' 兜底)
|
try {
|
||||||
int affected = taskMapper.claimTask(taskId, principal.getUserId(), principal.getCompanyId());
|
// 2. DB 原子更新(WHERE status='UNCLAIMED' 兜底)
|
||||||
if (affected == 0) {
|
int affected = taskMapper.claimTask(taskId, principal.getUserId(), principal.getCompanyId());
|
||||||
// DB 更新失败说明任务状态已变,清除刚设置的锁
|
if (affected == 0) {
|
||||||
|
// DB 更新失败说明任务状态已变,清除刚设置的锁
|
||||||
|
redisService.delete(lockKey);
|
||||||
|
throw new BusinessException("TASK_CLAIMED", "任务已被他人领取,请选择其他任务", HttpStatus.CONFLICT);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 写入状态历史
|
||||||
|
insertHistory(taskId, principal.getCompanyId(),
|
||||||
|
"UNCLAIMED", "IN_PROGRESS",
|
||||||
|
principal.getUserId(), principal.getRole(), null);
|
||||||
|
|
||||||
|
log.info("任务领取成功: taskId={}, userId={}", taskId, principal.getUserId());
|
||||||
|
} catch (BusinessException e) {
|
||||||
|
throw e; // 业务异常直接上抛,锁已在上方清除
|
||||||
|
} catch (Exception e) {
|
||||||
|
// DB 写入异常(含 insertHistory 失败):清除 Redis 锁,事务回滚
|
||||||
redisService.delete(lockKey);
|
redisService.delete(lockKey);
|
||||||
throw new BusinessException("TASK_CLAIMED", "任务已被他人领取,请选择其他任务", HttpStatus.CONFLICT);
|
throw e;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 写入状态历史
|
|
||||||
insertHistory(taskId, principal.getCompanyId(),
|
|
||||||
"UNCLAIMED", "IN_PROGRESS",
|
|
||||||
principal.getUserId(), principal.getRole(), null);
|
|
||||||
|
|
||||||
log.debug("任务领取成功: taskId={}, userId={}", taskId, principal.getUserId());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------ 放弃 --
|
// ------------------------------------------------------------------ 放弃 --
|
||||||
@@ -131,6 +139,7 @@ public class TaskClaimService {
|
|||||||
|
|
||||||
taskMapper.update(null, new LambdaUpdateWrapper<AnnotationTask>()
|
taskMapper.update(null, new LambdaUpdateWrapper<AnnotationTask>()
|
||||||
.eq(AnnotationTask::getId, taskId)
|
.eq(AnnotationTask::getId, taskId)
|
||||||
|
.eq(AnnotationTask::getStatus, "REJECTED")
|
||||||
.set(AnnotationTask::getStatus, "IN_PROGRESS")
|
.set(AnnotationTask::getStatus, "IN_PROGRESS")
|
||||||
.set(AnnotationTask::getClaimedAt, java.time.LocalDateTime.now()));
|
.set(AnnotationTask::getClaimedAt, java.time.LocalDateTime.now()));
|
||||||
|
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ public class TaskService {
|
|||||||
task.setStatus("UNCLAIMED");
|
task.setStatus("UNCLAIMED");
|
||||||
task.setIsFinal(false);
|
task.setIsFinal(false);
|
||||||
taskMapper.insert(task);
|
taskMapper.insert(task);
|
||||||
log.debug("任务已创建: id={}, type={}, sourceId={}", task.getId(), taskType, sourceId);
|
log.info("任务已创建: id={}, type={}, sourceId={}", task.getId(), taskType, sourceId);
|
||||||
return task;
|
return task;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,7 +156,7 @@ public class TaskService {
|
|||||||
@Transactional
|
@Transactional
|
||||||
public void reassign(Long taskId, Long targetUserId, TokenPrincipal principal) {
|
public void reassign(Long taskId, Long targetUserId, TokenPrincipal principal) {
|
||||||
AnnotationTask task = taskMapper.selectById(taskId);
|
AnnotationTask task = taskMapper.selectById(taskId);
|
||||||
if (task == null) {
|
if (task == null || !principal.getCompanyId().equals(task.getCompanyId())) {
|
||||||
throw new BusinessException("NOT_FOUND", "任务不存在: " + taskId, HttpStatus.NOT_FOUND);
|
throw new BusinessException("NOT_FOUND", "任务不存在: " + taskId, HttpStatus.NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -86,9 +86,12 @@ public class AuthService {
|
|||||||
redisService.hSetAll(RedisKeyManager.tokenKey(token), tokenData, tokenTtlSeconds);
|
redisService.hSetAll(RedisKeyManager.tokenKey(token), tokenData, tokenTtlSeconds);
|
||||||
|
|
||||||
// 将 token 加入该用户的活跃会话集合(用于角色变更时批量更新/失效)
|
// 将 token 加入该用户的活跃会话集合(用于角色变更时批量更新/失效)
|
||||||
redisService.sAdd(RedisKeyManager.userSessionsKey(user.getId()), token);
|
String sessionsKey = RedisKeyManager.userSessionsKey(user.getId());
|
||||||
|
redisService.sAdd(sessionsKey, token);
|
||||||
|
// 防止 Set 无限增长:TTL = token 有效期(最后一次登录时滑动续期)
|
||||||
|
redisService.expire(sessionsKey, tokenTtlSeconds);
|
||||||
|
|
||||||
log.debug("用户登录成功: companyCode={}, username={}", request.getCompanyCode(), request.getUsername());
|
log.info("用户登录成功: companyCode={}, username={}", request.getCompanyCode(), request.getUsername());
|
||||||
return new LoginResponse(token, user.getId(), user.getUsername(), user.getRole(), tokenTtlSeconds);
|
return new LoginResponse(token, user.getId(), user.getUsername(), user.getRole(), tokenTtlSeconds);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,7 +110,7 @@ public class AuthService {
|
|||||||
redisService.sRemove(RedisKeyManager.userSessionsKey(Long.parseLong(userId)), token);
|
redisService.sRemove(RedisKeyManager.userSessionsKey(Long.parseLong(userId)), token);
|
||||||
} catch (NumberFormatException ignored) {}
|
} catch (NumberFormatException ignored) {}
|
||||||
}
|
}
|
||||||
log.debug("用户退出,Token 已删除: {}", token);
|
log.info("用户退出,Token 已删除: {}", token);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ public class UserService {
|
|||||||
user.setStatus("ACTIVE");
|
user.setStatus("ACTIVE");
|
||||||
userMapper.insert(user);
|
userMapper.insert(user);
|
||||||
|
|
||||||
log.debug("用户已创建: userId={}, username={}, role={}", user.getId(), username, role);
|
log.info("用户已创建: userId={}, username={}, role={}", user.getId(), username, role);
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,7 +133,7 @@ public class UserService {
|
|||||||
// 3. 删除权限缓存(如 Shiro 缓存存在)
|
// 3. 删除权限缓存(如 Shiro 缓存存在)
|
||||||
redisService.delete(RedisKeyManager.userPermKey(userId));
|
redisService.delete(RedisKeyManager.userPermKey(userId));
|
||||||
|
|
||||||
log.debug("用户角色已变更: userId={}, newRole={}, 更新 {} 个活跃 Token", userId, newRole, tokens.size());
|
log.info("用户角色已变更: userId={}, newRole={}, 更新 {} 个活跃 Token", userId, newRole, tokens.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------ 变更状态 --
|
// ------------------------------------------------------------------ 变更状态 --
|
||||||
@@ -163,7 +163,7 @@ public class UserService {
|
|||||||
Set<String> tokens = redisService.sMembers(RedisKeyManager.userSessionsKey(userId));
|
Set<String> tokens = redisService.sMembers(RedisKeyManager.userSessionsKey(userId));
|
||||||
tokens.forEach(token -> redisService.delete(RedisKeyManager.tokenKey(token)));
|
tokens.forEach(token -> redisService.delete(RedisKeyManager.tokenKey(token)));
|
||||||
redisService.delete(RedisKeyManager.userSessionsKey(userId));
|
redisService.delete(RedisKeyManager.userSessionsKey(userId));
|
||||||
log.debug("账号已禁用,已删除 {} 个活跃 Token: userId={}", tokens.size(), userId);
|
log.info("账号已禁用,已删除 {} 个活跃 Token: userId={}", tokens.size(), userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 删除权限缓存
|
// 删除权限缓存
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import jakarta.servlet.http.HttpServletRequest;
|
|||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.shiro.authz.annotation.RequiresRoles;
|
import org.apache.shiro.authz.annotation.RequiresRoles;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -27,13 +28,21 @@ public class VideoController {
|
|||||||
|
|
||||||
private final VideoProcessService videoProcessService;
|
private final VideoProcessService videoProcessService;
|
||||||
|
|
||||||
|
@Value("${video.callback-secret:}")
|
||||||
|
private String callbackSecret;
|
||||||
|
|
||||||
/** POST /api/video/process — 触发视频处理任务 */
|
/** POST /api/video/process — 触发视频处理任务 */
|
||||||
@PostMapping("/api/video/process")
|
@PostMapping("/api/video/process")
|
||||||
@RequiresRoles("ADMIN")
|
@RequiresRoles("ADMIN")
|
||||||
public Result<VideoProcessJob> createJob(@RequestBody Map<String, Object> body,
|
public Result<VideoProcessJob> createJob(@RequestBody Map<String, Object> body,
|
||||||
HttpServletRequest request) {
|
HttpServletRequest request) {
|
||||||
Long sourceId = Long.parseLong(body.get("sourceId").toString());
|
Object sourceIdVal = body.get("sourceId");
|
||||||
String jobType = (String) body.get("jobType");
|
Object jobTypeVal = body.get("jobType");
|
||||||
|
if (sourceIdVal == null || jobTypeVal == null) {
|
||||||
|
return Result.failure("INVALID_PARAMS", "sourceId 和 jobType 不能为空");
|
||||||
|
}
|
||||||
|
Long sourceId = Long.parseLong(sourceIdVal.toString());
|
||||||
|
String jobType = jobTypeVal.toString();
|
||||||
String params = body.containsKey("params") ? body.get("params").toString() : null;
|
String params = body.containsKey("params") ? body.get("params").toString() : null;
|
||||||
|
|
||||||
TokenPrincipal principal = principal(request);
|
TokenPrincipal principal = principal(request);
|
||||||
@@ -68,7 +77,16 @@ public class VideoController {
|
|||||||
* { "jobId": 123, "status": "FAILED", "errorMessage": "ffmpeg error: ..." }
|
* { "jobId": 123, "status": "FAILED", "errorMessage": "ffmpeg error: ..." }
|
||||||
*/
|
*/
|
||||||
@PostMapping("/api/video/callback")
|
@PostMapping("/api/video/callback")
|
||||||
public Result<Void> handleCallback(@RequestBody Map<String, Object> body) {
|
public Result<Void> handleCallback(@RequestBody Map<String, Object> body,
|
||||||
|
HttpServletRequest request) {
|
||||||
|
// 共享密钥校验(配置了 VIDEO_CALLBACK_SECRET 时强制校验)
|
||||||
|
if (callbackSecret != null && !callbackSecret.isBlank()) {
|
||||||
|
String provided = request.getHeader("X-Callback-Secret");
|
||||||
|
if (!callbackSecret.equals(provided)) {
|
||||||
|
return Result.failure("UNAUTHORIZED", "回调密钥无效");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Long jobId = Long.parseLong(body.get("jobId").toString());
|
Long jobId = Long.parseLong(body.get("jobId").toString());
|
||||||
String status = (String) body.get("status");
|
String status = (String) body.get("status");
|
||||||
String outputPath = body.containsKey("outputPath") ? (String) body.get("outputPath") : null;
|
String outputPath = body.containsKey("outputPath") ? (String) body.get("outputPath") : null;
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ public class VideoProcessService {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
log.debug("视频处理任务已创建(AI 将在事务提交后触发): jobId={}, sourceId={}", jobId, sourceId);
|
log.info("视频处理任务已创建(AI 将在事务提交后触发): jobId={}, sourceId={}", jobId, sourceId);
|
||||||
return job;
|
return job;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,15 +123,16 @@ public class VideoProcessService {
|
|||||||
@Transactional
|
@Transactional
|
||||||
public void handleCallback(Long jobId, String callbackStatus,
|
public void handleCallback(Long jobId, String callbackStatus,
|
||||||
String outputPath, String errorMessage) {
|
String outputPath, String errorMessage) {
|
||||||
|
// video_process_job 在 IGNORED_TABLES 中(回调无 CompanyContext),此处显式校验
|
||||||
VideoProcessJob job = jobMapper.selectById(jobId);
|
VideoProcessJob job = jobMapper.selectById(jobId);
|
||||||
if (job == null) {
|
if (job == null || job.getCompanyId() == null) {
|
||||||
log.warn("视频处理回调:job 不存在,jobId={}", jobId);
|
log.warn("视频处理回调:job 不存在,jobId={}", jobId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 幂等:已成功则忽略重复回调
|
// 幂等:已成功则忽略重复回调
|
||||||
if ("SUCCESS".equals(job.getStatus())) {
|
if ("SUCCESS".equals(job.getStatus())) {
|
||||||
log.debug("视频处理回调幂等:jobId={} 已为 SUCCESS,跳过", jobId);
|
log.info("视频处理回调幂等:jobId={} 已为 SUCCESS,跳过", jobId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,7 +176,7 @@ public class VideoProcessService {
|
|||||||
|
|
||||||
job.setStatus("PENDING");
|
job.setStatus("PENDING");
|
||||||
job.setRetryCount(0);
|
job.setRetryCount(0);
|
||||||
log.debug("视频处理任务已重置: jobId={}", jobId);
|
log.info("视频处理任务已重置: jobId={}", jobId);
|
||||||
return job;
|
return job;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,7 +207,7 @@ public class VideoProcessService {
|
|||||||
.set(SourceData::getStatus, "PENDING")
|
.set(SourceData::getStatus, "PENDING")
|
||||||
.set(SourceData::getUpdatedAt, LocalDateTime.now()));
|
.set(SourceData::getUpdatedAt, LocalDateTime.now()));
|
||||||
|
|
||||||
log.debug("视频处理成功:jobId={}, sourceId={}", job.getId(), job.getSourceId());
|
log.info("视频处理成功:jobId={}, sourceId={}", job.getId(), job.getSourceId());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleFailure(VideoProcessJob job, String errorMessage) {
|
private void handleFailure(VideoProcessJob job, String errorMessage) {
|
||||||
@@ -274,9 +275,9 @@ public class VideoProcessService {
|
|||||||
} else {
|
} else {
|
||||||
aiServiceClient.videoToText(req);
|
aiServiceClient.videoToText(req);
|
||||||
}
|
}
|
||||||
log.debug("AI 触发成功: jobId={}", jobId);
|
log.info("AI 触发成功: jobId={}", jobId);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warn("触发视频处理 AI 失败(jobId={}):{},job 保持当前状态等待重试", jobId, e.getMessage());
|
log.error("触发视频处理 AI 失败(jobId={}):{},job 保持当前状态,需管理员手动重置", jobId, e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -56,6 +56,9 @@ ai-service:
|
|||||||
token:
|
token:
|
||||||
ttl-seconds: 7200 # Token 默认有效期(秒),与 sys_config token_ttl_seconds 保持一致
|
ttl-seconds: 7200 # Token 默认有效期(秒),与 sys_config token_ttl_seconds 保持一致
|
||||||
|
|
||||||
|
video:
|
||||||
|
callback-secret: ${VIDEO_CALLBACK_SECRET:} # AI 服务回调共享密钥,为空时跳过校验(开发环境)
|
||||||
|
|
||||||
logging:
|
logging:
|
||||||
level:
|
level:
|
||||||
com.label: DEBUG
|
com.label: DEBUG
|
||||||
|
|||||||
Reference in New Issue
Block a user