提交代码实现v1.0

This commit is contained in:
wh
2026-04-27 10:27:57 +08:00
parent 849e78658d
commit 5662c1fda9
67 changed files with 2343 additions and 5 deletions

View File

@@ -0,0 +1,140 @@
package com.labelsys.backend.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.labelsys.backend.common.ResultCode;
import com.labelsys.backend.common.exception.BusinessException;
import com.labelsys.backend.context.LoginUser;
import com.labelsys.backend.dto.request.MergeReviewResultRequest;
import com.labelsys.backend.dto.response.MergeReviewResultResponse;
import com.labelsys.backend.entity.AnnotationResult;
import com.labelsys.backend.entity.AnnotationResultHistory;
import com.labelsys.backend.enums.QaContentStorageMode;
import com.labelsys.backend.enums.UserPosition;
import com.labelsys.backend.mapper.AnnotationResultHistoryMapper;
import com.labelsys.backend.mapper.AnnotationResultMapper;
import com.labelsys.backend.util.IdGenerator;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Slf4j
@Service
@RequiredArgsConstructor
public class AnnotationResultArchiveService {
private static final String MANUAL_ARCHIVE_REASON = "MANUAL_REVIEW";
private final AnnotationResultMapper annotationResultMapper;
private final AnnotationResultHistoryMapper annotationResultHistoryMapper;
@Value("${labelsys.annotation.auto-archive-timeout:PT2H}")
private Duration autoArchiveTimeout;
@Transactional
public MergeReviewResultResponse mergeReview(LoginUser currentUser, Long resultId, MergeReviewResultRequest request) {
assertReviewer(currentUser);
AnnotationResult result = annotationResultMapper.findActiveByIdAndCompanyId(resultId, currentUser.companyId());
if (result == null) {
throw new BusinessException(ResultCode.NOT_FOUND, "运行态结果不存在");
}
LocalDateTime archivedAt = LocalDateTime.now();
AnnotationResultHistory history = AnnotationResultHistory.builder()
.id(IdGenerator.nextId())
.companyId(result.getCompanyId())
.creatorId(result.getCreatorId())
.creatorRole(result.getCreatorRole())
.sourceResultId(result.getId())
.taskId(result.getTaskId())
.resourceId(result.getResourceId())
.qaContentJson(request.qaContentJson())
.qaContentStorageMode(resolveStorageMode(result))
.qaContentFilePath(result.getQaContentFilePath())
.archiveReason(MANUAL_ARCHIVE_REASON)
.archivedBy(currentUser.userId())
.archivedAt(archivedAt)
.build();
annotationResultHistoryMapper.insert(history);
int updated = annotationResultMapper.markArchived(
result.getId(),
currentUser.companyId(),
currentUser.userId(),
request.reviewComment(),
archivedAt);
if (updated == 0) {
throw new BusinessException(ResultCode.CONFLICT, "结果已被其他操作处理");
}
log.info("merged review result, companyId={}, reviewerId={}, resultId={}, historyId={}",
currentUser.companyId(), currentUser.userId(), resultId, history.getId());
return new MergeReviewResultResponse(resultId, history.getId(), MANUAL_ARCHIVE_REASON, archivedAt);
}
@Transactional
public int autoArchiveEligibleResults() {
LocalDateTime cutoff = LocalDateTime.now().minus(autoArchiveTimeout);
List<AnnotationResult> results = annotationResultMapper.selectList(new LambdaQueryWrapper<AnnotationResult>()
.eq(AnnotationResult::getIsDeleted, false)
.eq(AnnotationResult::getRequiresManualReview, false)
.lt(AnnotationResult::getCreatedAt, cutoff));
int archivedCount = 0;
for (AnnotationResult result : results) {
if (archiveRuntimeResult(result, null, "AUTO_ARCHIVE", null) != null) {
archivedCount++;
}
}
return archivedCount;
}
private void assertReviewer(LoginUser currentUser) {
if (currentUser.position() != UserPosition.REVIEWER && currentUser.position() != UserPosition.ADMIN) {
throw new BusinessException(ResultCode.FORBIDDEN, "当前用户没有审核权限");
}
}
private String resolveStorageMode(AnnotationResult result) {
if (QaContentStorageMode.isValid(result.getQaContentStorageMode())) {
return result.getQaContentStorageMode();
}
return QaContentStorageMode.INLINE.name();
}
private MergeReviewResultResponse archiveRuntimeResult(AnnotationResult result,
Long reviewerId,
String archiveReason,
String reviewComment) {
LocalDateTime archivedAt = LocalDateTime.now();
AnnotationResultHistory history = AnnotationResultHistory.builder()
.id(IdGenerator.nextId())
.companyId(result.getCompanyId())
.creatorId(result.getCreatorId())
.creatorRole(result.getCreatorRole())
.sourceResultId(result.getId())
.taskId(result.getTaskId())
.resourceId(result.getResourceId())
.qaContentJson(result.getQaContentJson())
.qaContentStorageMode(resolveStorageMode(result))
.qaContentFilePath(result.getQaContentFilePath())
.archiveReason(archiveReason)
.archivedBy(reviewerId)
.archivedAt(archivedAt)
.build();
annotationResultHistoryMapper.insert(history);
int updated = annotationResultMapper.markArchived(
result.getId(),
result.getCompanyId(),
reviewerId,
reviewComment,
archivedAt);
if (updated == 0) {
return null;
}
return new MergeReviewResultResponse(result.getId(), history.getId(), archiveReason, archivedAt);
}
}

View File

@@ -0,0 +1,99 @@
package com.labelsys.backend.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.labelsys.backend.common.ResultCode;
import com.labelsys.backend.common.exception.BusinessException;
import com.labelsys.backend.context.LoginUser;
import com.labelsys.backend.dto.common.PageResult;
import com.labelsys.backend.dto.request.AnnotationResultPageQuery;
import com.labelsys.backend.dto.response.AnnotationResultCompareResponse;
import com.labelsys.backend.dto.response.AnnotationResultResponse;
import com.labelsys.backend.entity.AnnotationResult;
import com.labelsys.backend.entity.SourceResource;
import com.labelsys.backend.enums.RuntimeResultStatus;
import com.labelsys.backend.mapper.AnnotationResultMapper;
import com.labelsys.backend.mapper.SourceResourceMapper;
import java.util.Comparator;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class AnnotationResultService {
private final AnnotationResultMapper annotationResultMapper;
private final SourceResourceMapper sourceResourceMapper;
public PageResult<AnnotationResultResponse> pageResults(LoginUser currentUser, AnnotationResultPageQuery query) {
LambdaQueryWrapper<AnnotationResult> wrapper = new LambdaQueryWrapper<AnnotationResult>()
.eq(AnnotationResult::getCompanyId, currentUser.companyId())
.eq(query.taskId() != null, AnnotationResult::getTaskId, query.taskId())
.eq(query.resourceId() != null, AnnotationResult::getResourceId, query.resourceId())
.eq(query.requiresManualReview() != null, AnnotationResult::getRequiresManualReview, query.requiresManualReview())
.orderByDesc(AnnotationResult::getCreatedAt);
List<AnnotationResultResponse> records = annotationResultMapper.selectList(wrapper).stream()
.map(this::toResponse)
.filter(response -> query.runtimeStatus() == null || query.runtimeStatus().equals(response.runtimeStatus()))
.sorted(Comparator.comparing(AnnotationResultResponse::createdAt, Comparator.nullsLast(Comparator.naturalOrder())).reversed())
.toList();
return paginate(records, query.pageNo(), query.pageSize());
}
public AnnotationResultResponse getResult(LoginUser currentUser, Long resultId) {
AnnotationResult result = annotationResultMapper.selectById(resultId);
if (result == null || !currentUser.companyId().equals(result.getCompanyId())) {
throw new BusinessException(ResultCode.NOT_FOUND, "结果不存在");
}
return toResponse(result);
}
public AnnotationResultCompareResponse compareResult(LoginUser currentUser, Long resultId) {
AnnotationResult result = annotationResultMapper.selectById(resultId);
if (result == null || !currentUser.companyId().equals(result.getCompanyId())) {
throw new BusinessException(ResultCode.NOT_FOUND, "结果不存在");
}
SourceResource resource = sourceResourceMapper.selectById(result.getResourceId());
return new AnnotationResultCompareResponse(
result.getId(),
result.getTaskId(),
result.getResourceId(),
result.getQaContentJson(),
result.getDiffSummary(),
result.getQaContentStorageMode(),
result.getQaContentFilePath(),
resource == null ? null : resource.getFilePath());
}
private AnnotationResultResponse toResponse(AnnotationResult result) {
return new AnnotationResultResponse(
result.getId(),
result.getTaskId(),
result.getResourceId(),
deriveStatus(result),
result.getRequiresManualReview(),
result.getIsDeleted(),
result.getQaContentStorageMode(),
result.getReviewComment(),
result.getReviewedAt(),
result.getCreatedAt());
}
private String deriveStatus(AnnotationResult result) {
if (Boolean.TRUE.equals(result.getIsDeleted())) {
return RuntimeResultStatus.ARCHIVED.name();
}
if (Boolean.TRUE.equals(result.getRequiresManualReview())) {
return RuntimeResultStatus.MANUAL_REVIEW_PENDING.name();
}
return RuntimeResultStatus.AUTO_ARCHIVE_PENDING.name();
}
private <T> PageResult<T> paginate(List<T> records, Integer pageNo, Integer pageSize) {
int actualPageNo = pageNo == null || pageNo < 1 ? 1 : pageNo;
int actualPageSize = pageSize == null || pageSize < 1 ? 10 : pageSize;
int fromIndex = Math.min((actualPageNo - 1) * actualPageSize, records.size());
int toIndex = Math.min(fromIndex + actualPageSize, records.size());
return new PageResult<>(records.subList(fromIndex, toIndex), (long) records.size(), actualPageNo, actualPageSize);
}
}

View File

@@ -0,0 +1,284 @@
package com.labelsys.backend.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.labelsys.backend.common.ResultCode;
import com.labelsys.backend.common.exception.BusinessException;
import com.labelsys.backend.context.LoginUser;
import com.labelsys.backend.dto.common.PageResult;
import com.labelsys.backend.dto.request.AnnotationTaskPageQuery;
import com.labelsys.backend.dto.request.CreateAnnotationTaskRequest;
import com.labelsys.backend.dto.request.UpdateAnnotationTaskRequest;
import com.labelsys.backend.dto.response.AnnotationTaskResponse;
import com.labelsys.backend.dto.response.TaskModelConfigResponse;
import com.labelsys.backend.dto.response.TaskPromptConfigResponse;
import com.labelsys.backend.entity.AnnotationTask;
import com.labelsys.backend.entity.AnnotationTaskResource;
import com.labelsys.backend.entity.SourceResource;
import com.labelsys.backend.enums.SourceStatus;
import com.labelsys.backend.enums.TaskStatus;
import com.labelsys.backend.mapper.AnnotationTaskMapper;
import com.labelsys.backend.mapper.AnnotationTaskResourceMapper;
import com.labelsys.backend.mapper.SourceResourceMapper;
import com.labelsys.backend.util.IdGenerator;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
@Slf4j
@Service
@RequiredArgsConstructor
public class AnnotationTaskService {
private final AnnotationTaskMapper annotationTaskMapper;
private final AnnotationTaskResourceMapper annotationTaskResourceMapper;
private final SourceResourceMapper sourceResourceMapper;
private final SysConfigService sysConfigService;
private final DataPermissionService dataPermissionService;
@Transactional
public AnnotationTaskResponse createTask(LoginUser currentUser, CreateAnnotationTaskRequest request) {
List<SourceResource> resources = loadAndValidateResources(currentUser, request.resourceIds());
SysConfigService.ResolvedModelConfig extractModel = sysConfigService.resolveModelConfig(currentUser, request.extractModel());
SysConfigService.ResolvedModelConfig verifyModel = sysConfigService.resolveModelConfig(currentUser, request.verifyModel());
SysConfigService.ResolvedPromptConfig extractPrompt = sysConfigService.resolvePromptConfig(currentUser, request.extractPrompt());
SysConfigService.ResolvedPromptConfig verifyPrompt = sysConfigService.resolvePromptConfig(currentUser, request.verifyPrompt());
AnnotationTask task = AnnotationTask.builder()
.id(IdGenerator.nextId())
.companyId(currentUser.companyId())
.creatorId(currentUser.userId())
.creatorRole(currentUser.role())
.taskName(request.taskName())
.industryType(defaultIndustryType(request.industryType()))
.taskType(defaultTaskType(request.taskType()))
.extractModelConfigId(extractModel.configId())
.extractModelName(extractModel.modelName())
.extractModelUrl(extractModel.modelUrl())
.extractModelApiKey(extractModel.apiKey())
.verifyModelConfigId(verifyModel.configId())
.verifyModelName(verifyModel.modelName())
.verifyModelUrl(verifyModel.modelUrl())
.verifyModelApiKey(verifyModel.apiKey())
.extractPromptConfigId(extractPrompt.configId())
.extractPrompt(extractPrompt.promptText())
.verifyPromptConfigId(verifyPrompt.configId())
.verifyPrompt(verifyPrompt.promptText())
.taskStatus(TaskStatus.PENDING.name())
.isDeleted(false)
.build();
annotationTaskMapper.insert(task);
saveTaskBindings(task.getId(), currentUser.companyId(), resources);
log.info("created annotation task, companyId={}, userId={}, taskId={}, resourceCount={}",
currentUser.companyId(), currentUser.userId(), task.getId(), resources.size());
return buildTaskResponse(task, resourceIds(resources), extractModel, verifyModel, extractPrompt, verifyPrompt);
}
@Transactional
public AnnotationTaskResponse updateTask(LoginUser currentUser, Long taskId, UpdateAnnotationTaskRequest request) {
AnnotationTask task = annotationTaskMapper.findByIdAndCompanyId(taskId, currentUser.companyId());
if (task == null) {
throw new BusinessException(ResultCode.NOT_FOUND, "任务不存在");
}
assertTaskPermission(currentUser, task);
List<Long> currentResourceIds = normalizeIds(annotationTaskResourceMapper.listResourceIdsByTaskId(taskId));
List<Long> targetResourceIds = normalizeIds(request.resourceIds());
boolean resourcesChanged = !currentResourceIds.equals(targetResourceIds);
if (TaskStatus.RUNNING.name().equals(task.getTaskStatus()) && resourcesChanged) {
throw new BusinessException(ResultCode.CONFLICT, "运行中的任务不允许修改资源");
}
List<SourceResource> resources = loadAndValidateResources(currentUser, request.resourceIds());
SysConfigService.ResolvedModelConfig extractModel = sysConfigService.resolveModelConfig(currentUser, request.extractModel());
SysConfigService.ResolvedModelConfig verifyModel = sysConfigService.resolveModelConfig(currentUser, request.verifyModel());
SysConfigService.ResolvedPromptConfig extractPrompt = sysConfigService.resolvePromptConfig(currentUser, request.extractPrompt());
SysConfigService.ResolvedPromptConfig verifyPrompt = sysConfigService.resolvePromptConfig(currentUser, request.verifyPrompt());
task.setIndustryType(defaultIndustryType(request.industryType()));
task.setTaskType(defaultTaskType(request.taskType()));
task.setExtractModelConfigId(extractModel.configId());
task.setExtractModelName(extractModel.modelName());
task.setExtractModelUrl(extractModel.modelUrl());
task.setExtractModelApiKey(extractModel.apiKey());
task.setVerifyModelConfigId(verifyModel.configId());
task.setVerifyModelName(verifyModel.modelName());
task.setVerifyModelUrl(verifyModel.modelUrl());
task.setVerifyModelApiKey(verifyModel.apiKey());
task.setExtractPromptConfigId(extractPrompt.configId());
task.setExtractPrompt(extractPrompt.promptText());
task.setVerifyPromptConfigId(verifyPrompt.configId());
task.setVerifyPrompt(verifyPrompt.promptText());
annotationTaskMapper.updateById(task);
if (resourcesChanged) {
annotationTaskResourceMapper.deleteByTaskId(taskId);
saveTaskBindings(taskId, currentUser.companyId(), resources);
}
log.info("updated annotation task, companyId={}, userId={}, taskId={}, resourcesChanged={}",
currentUser.companyId(), currentUser.userId(), taskId, resourcesChanged);
return buildTaskResponse(task, resourceIds(resources), extractModel, verifyModel, extractPrompt, verifyPrompt);
}
public AnnotationTaskResponse getTask(LoginUser currentUser, Long taskId) {
AnnotationTask task = annotationTaskMapper.findByIdAndCompanyId(taskId, currentUser.companyId());
if (task == null) {
throw new BusinessException(ResultCode.NOT_FOUND, "任务不存在");
}
assertTaskPermission(currentUser, task);
return buildTaskResponse(task, normalizeIds(annotationTaskResourceMapper.listResourceIdsByTaskId(taskId)));
}
public PageResult<AnnotationTaskResponse> pageTasks(LoginUser currentUser, AnnotationTaskPageQuery query) {
LambdaQueryWrapper<AnnotationTask> wrapper = new LambdaQueryWrapper<AnnotationTask>()
.eq(AnnotationTask::getCompanyId, currentUser.companyId())
.eq(StringUtils.hasText(query.taskType()), AnnotationTask::getTaskType, query.taskType())
.eq(StringUtils.hasText(query.taskStatus()), AnnotationTask::getTaskStatus, query.taskStatus())
.eq(query.isDeleted() != null, AnnotationTask::getIsDeleted, query.isDeleted())
.like(StringUtils.hasText(query.keyword()), AnnotationTask::getTaskName, query.keyword())
.orderByDesc(AnnotationTask::getCreatedAt);
List<AnnotationTaskResponse> records = annotationTaskMapper.selectList(wrapper).stream()
.filter(task -> dataPermissionService.canAccessCreator(currentUser, task.getCreatorId(), task.getCreatorRole()))
.filter(task -> query.resourceId() == null || annotationTaskResourceMapper.listResourceIdsByTaskId(task.getId()).contains(query.resourceId()))
.sorted(Comparator.comparing(AnnotationTask::getCreatedAt, Comparator.nullsLast(Comparator.naturalOrder())).reversed())
.map(task -> buildTaskResponse(task, normalizeIds(annotationTaskResourceMapper.listResourceIdsByTaskId(task.getId()))))
.toList();
return paginate(records, query.pageNo(), query.pageSize());
}
@Transactional
public void deleteTask(LoginUser currentUser, Long taskId) {
AnnotationTask task = annotationTaskMapper.findByIdAndCompanyId(taskId, currentUser.companyId());
if (task == null) {
throw new BusinessException(ResultCode.NOT_FOUND, "任务不存在");
}
assertTaskPermission(currentUser, task);
if (TaskStatus.RUNNING.name().equals(task.getTaskStatus())) {
throw new BusinessException(ResultCode.CONFLICT, "运行中的任务不允许删除");
}
task.setIsDeleted(true);
annotationTaskMapper.updateById(task);
log.info("deleted annotation task logically, companyId={}, userId={}, taskId={}",
currentUser.companyId(), currentUser.userId(), taskId);
}
private List<SourceResource> loadAndValidateResources(LoginUser currentUser, List<Long> resourceIds) {
if (resourceIds == null || resourceIds.isEmpty()) {
throw new BusinessException(ResultCode.BAD_REQUEST, "任务资源不能为空");
}
List<Long> normalizedIds = normalizeIds(resourceIds);
List<SourceResource> resources = sourceResourceMapper.selectByCompanyIdAndIds(currentUser.companyId(), normalizedIds);
if (resources.size() != normalizedIds.size()) {
throw new BusinessException(ResultCode.BAD_REQUEST, "存在无效资源");
}
for (SourceResource resource : resources) {
if (!dataPermissionService.canAccessCreator(currentUser, resource.getCreatorId(), resource.getCreatorRole())) {
throw new BusinessException(ResultCode.FORBIDDEN, "无权访问资源");
}
if (!SourceStatus.READY.name().equals(resource.getSourceStatus())) {
throw new BusinessException(ResultCode.BAD_REQUEST, "仅允许选择已就绪资源");
}
}
resources.sort(Comparator.comparing(SourceResource::getId));
return resources;
}
private void saveTaskBindings(Long taskId, Long companyId, List<SourceResource> resources) {
for (SourceResource resource : resources) {
annotationTaskResourceMapper.insert(AnnotationTaskResource.builder()
.id(IdGenerator.nextId())
.companyId(companyId)
.taskId(taskId)
.resourceId(resource.getId())
.build());
}
}
private AnnotationTaskResponse buildTaskResponse(AnnotationTask task,
List<Long> resourceIds,
SysConfigService.ResolvedModelConfig extractModel,
SysConfigService.ResolvedModelConfig verifyModel,
SysConfigService.ResolvedPromptConfig extractPrompt,
SysConfigService.ResolvedPromptConfig verifyPrompt) {
return new AnnotationTaskResponse(
task.getId(),
task.getTaskName(),
task.getIndustryType(),
task.getTaskType(),
task.getTaskStatus(),
resourceIds,
sysConfigService.toResponse(extractModel),
sysConfigService.toResponse(verifyModel),
sysConfigService.toResponse(extractPrompt),
sysConfigService.toResponse(verifyPrompt),
task.getCreatedAt(),
task.getUpdatedAt());
}
private AnnotationTaskResponse buildTaskResponse(AnnotationTask task, List<Long> resourceIds) {
return new AnnotationTaskResponse(
task.getId(),
task.getTaskName(),
task.getIndustryType(),
task.getTaskType(),
task.getTaskStatus(),
resourceIds,
new TaskModelConfigResponse(task.getExtractModelConfigId(), null, task.getExtractModelName(),
task.getExtractModelUrl(), maskSecret(task.getExtractModelApiKey())),
new TaskModelConfigResponse(task.getVerifyModelConfigId(), null, task.getVerifyModelName(),
task.getVerifyModelUrl(), maskSecret(task.getVerifyModelApiKey())),
new TaskPromptConfigResponse(task.getExtractPromptConfigId(), null, task.getExtractPrompt()),
new TaskPromptConfigResponse(task.getVerifyPromptConfigId(), null, task.getVerifyPrompt()),
task.getCreatedAt(),
task.getUpdatedAt());
}
private List<Long> resourceIds(List<SourceResource> resources) {
return resources.stream().map(SourceResource::getId).sorted().toList();
}
private List<Long> normalizeIds(List<Long> resourceIds) {
Set<Long> uniqueIds = new HashSet<>(resourceIds);
List<Long> sortedIds = new ArrayList<>(uniqueIds);
sortedIds.sort(Long::compareTo);
return sortedIds;
}
private void assertTaskPermission(LoginUser currentUser, AnnotationTask task) {
if (!dataPermissionService.canAccessCreator(currentUser, task.getCreatorId(), task.getCreatorRole())) {
throw new BusinessException(ResultCode.FORBIDDEN, "无权操作任务");
}
}
private String defaultIndustryType(String industryType) {
return StringUtils.hasText(industryType) ? industryType : "transport";
}
private String defaultTaskType(String taskType) {
return StringUtils.hasText(taskType) ? taskType : "EXTRACT_QA";
}
private String maskSecret(String secret) {
if (!StringUtils.hasText(secret)) {
return null;
}
if (secret.length() <= 4) {
return "****";
}
return "****" + secret.substring(secret.length() - 4);
}
private <T> PageResult<T> paginate(List<T> records, Integer pageNo, Integer pageSize) {
int actualPageNo = pageNo == null || pageNo < 1 ? 1 : pageNo;
int actualPageSize = pageSize == null || pageSize < 1 ? 10 : pageSize;
int fromIndex = Math.min((actualPageNo - 1) * actualPageSize, records.size());
int toIndex = Math.min(fromIndex + actualPageSize, records.size());
return new PageResult<>(records.subList(fromIndex, toIndex), (long) records.size(), actualPageNo, actualPageSize);
}
}

View File

@@ -1,15 +1,21 @@
package com.labelsys.backend.service;
import com.labelsys.backend.context.LoginUser;
import com.labelsys.backend.entity.BizDataRecord;
import com.labelsys.backend.enums.UserRole;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.jdbc.core.JdbcTemplate;
@Service
@RequiredArgsConstructor
public class DataPermissionService {
private final JdbcTemplate jdbcTemplate;
public boolean canAccessCreator(LoginUser currentUser, Long creatorId, UserRole creatorRole) {
return switch (currentUser.role()) {
case EMPLOYEE -> currentUser.userId().equals(creatorId);
@@ -65,4 +71,25 @@ public class DataPermissionService {
public boolean shouldFilterByUserId(LoginUser currentUser) {
return currentUser.role() == UserRole.EMPLOYEE;
}
public List<BizDataRecord> listVisibleRecords(LoginUser currentUser) {
List<BizDataRecord> allRecords = jdbcTemplate.query("""
select id, company_id, creator_id, creator_role, record_name, created_at, updated_at
from biz_data_record
where company_id = ?
order by id
""",
(rs, rowNum) -> BizDataRecord.builder()
.id(rs.getLong("id"))
.companyId(rs.getLong("company_id"))
.creatorId(rs.getLong("creator_id"))
.creatorRole(UserRole.valueOf(rs.getString("creator_role")))
.recordName(rs.getString("record_name"))
.createdAt(rs.getTimestamp("created_at") == null ? null : rs.getTimestamp("created_at").toLocalDateTime())
.updatedAt(rs.getTimestamp("updated_at") == null ? null : rs.getTimestamp("updated_at").toLocalDateTime())
.build(),
currentUser.companyId());
return filterByRole(currentUser, allRecords, BizDataRecord::getCreatorRole, BizDataRecord::getCreatorId);
}
}

View File

@@ -0,0 +1,8 @@
package com.labelsys.backend.service;
public interface ObjectStorageService {
String upload(String bucketName, String objectKey, byte[] content, String contentType);
void delete(String bucketName, String objectKey);
}

View File

@@ -0,0 +1,45 @@
package com.labelsys.backend.service;
import com.labelsys.backend.common.ResultCode;
import com.labelsys.backend.common.exception.BusinessException;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import software.amazon.awssdk.core.sync.RequestBody;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.DeleteObjectRequest;
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
@Service
@RequiredArgsConstructor
public class RustfsObjectStorageService implements ObjectStorageService {
private final S3Client s3Client;
@Override
public String upload(String bucketName, String objectKey, byte[] content, String contentType) {
try {
PutObjectRequest.Builder requestBuilder = PutObjectRequest.builder()
.bucket(bucketName)
.key(objectKey);
if (contentType != null && !contentType.isBlank()) {
requestBuilder.contentType(contentType);
}
s3Client.putObject(requestBuilder.build(), RequestBody.fromBytes(content));
return objectKey;
} catch (Exception ex) {
throw new BusinessException(ResultCode.ERROR, "对象存储上传失败");
}
}
@Override
public void delete(String bucketName, String objectKey) {
try {
s3Client.deleteObject(DeleteObjectRequest.builder()
.bucket(bucketName)
.key(objectKey)
.build());
} catch (Exception ex) {
throw new BusinessException(ResultCode.ERROR, "对象存储删除失败");
}
}
}

View File

@@ -0,0 +1,179 @@
package com.labelsys.backend.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.labelsys.backend.common.ResultCode;
import com.labelsys.backend.common.exception.BusinessException;
import com.labelsys.backend.context.LoginUser;
import com.labelsys.backend.dto.common.PageResult;
import com.labelsys.backend.dto.request.SourceResourcePageQuery;
import com.labelsys.backend.dto.request.SourceUploadRequest;
import com.labelsys.backend.dto.response.SourceResourceResponse;
import com.labelsys.backend.dto.response.SourceUploadResponse;
import com.labelsys.backend.entity.SourceResource;
import com.labelsys.backend.entity.SysUser;
import com.labelsys.backend.enums.ResourceType;
import com.labelsys.backend.enums.SourceStatus;
import com.labelsys.backend.mapper.AnnotationTaskResourceMapper;
import com.labelsys.backend.mapper.SourceResourceMapper;
import com.labelsys.backend.mapper.SysUserMapper;
import com.labelsys.backend.util.IdGenerator;
import com.labelsys.backend.util.ObjectStoragePathBuilder;
import java.io.IOException;
import java.util.Comparator;
import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;
@Slf4j
@Service
@RequiredArgsConstructor
public class SourceResourceService {
private final SourceResourceMapper sourceResourceMapper;
private final AnnotationTaskResourceMapper annotationTaskResourceMapper;
private final SysUserMapper sysUserMapper;
private final DataPermissionService dataPermissionService;
private final ObjectStorageService objectStorageService;
private final com.labelsys.backend.config.ObjectStorageProperties objectStorageProperties;
@Transactional
public SourceUploadResponse upload(LoginUser currentUser, SourceUploadRequest request) {
MultipartFile file = request.getFile();
if (file == null || file.isEmpty()) {
throw new BusinessException(ResultCode.BAD_REQUEST, "上传文件不能为空");
}
if (!ResourceType.isValid(request.getResourceType())) {
throw new BusinessException(ResultCode.BAD_REQUEST, "资源类型非法");
}
long resourceId = IdGenerator.nextId();
String extension = resolveExtension(file.getOriginalFilename(), request.getResourceType());
String objectKey = ObjectStoragePathBuilder.sourceObjectKey(
currentUser.companyId(), request.getResourceType(), resourceId, extension);
try {
objectStorageService.upload(
objectStorageProperties.getSourceBucket(),
objectKey,
file.getBytes(),
file.getContentType());
} catch (IOException ex) {
throw new BusinessException(ResultCode.BAD_REQUEST, "读取上传文件失败");
}
SourceResource resource = SourceResource.builder()
.id(resourceId)
.companyId(currentUser.companyId())
.creatorId(currentUser.userId())
.creatorRole(currentUser.role())
.resourceName(StringUtils.hasText(request.getResourceName()) ? request.getResourceName() : file.getOriginalFilename())
.resourceType(request.getResourceType())
.bucketName(objectStorageProperties.getSourceBucket())
.filePath(objectKey)
.fileSize(file.getSize())
.sourceStatus(SourceStatus.READY.name())
.storageProvider("rustfs")
.remark(request.getRemark())
.build();
sourceResourceMapper.insert(resource);
log.info("uploaded source resource, companyId={}, userId={}, resourceId={}",
currentUser.companyId(), currentUser.userId(), resourceId);
return new SourceUploadResponse(
resource.getId(),
resource.getResourceName(),
resource.getResourceType(),
resource.getBucketName(),
resource.getFilePath(),
resource.getFileSize(),
resource.getSourceStatus(),
resource.getCreatedAt());
}
public PageResult<SourceResourceResponse> pageResources(LoginUser currentUser, SourceResourcePageQuery query) {
LambdaQueryWrapper<SourceResource> wrapper = new LambdaQueryWrapper<SourceResource>()
.eq(SourceResource::getCompanyId, currentUser.companyId())
.eq(StringUtils.hasText(query.resourceType()), SourceResource::getResourceType, query.resourceType())
.eq(StringUtils.hasText(query.sourceStatus()), SourceResource::getSourceStatus, query.sourceStatus())
.like(StringUtils.hasText(query.keyword()), SourceResource::getResourceName, query.keyword())
.orderByDesc(SourceResource::getCreatedAt);
List<SourceResourceResponse> records = sourceResourceMapper.selectList(wrapper).stream()
.filter(resource -> dataPermissionService.canAccessCreator(currentUser, resource.getCreatorId(), resource.getCreatorRole()))
.sorted(Comparator.comparing(SourceResource::getCreatedAt, Comparator.nullsLast(Comparator.naturalOrder())).reversed())
.map(this::toResponse)
.toList();
return paginate(records, query.pageNo(), query.pageSize());
}
public SourceResourceResponse getResource(LoginUser currentUser, Long resourceId) {
SourceResource resource = sourceResourceMapper.selectById(resourceId);
if (resource == null || !currentUser.companyId().equals(resource.getCompanyId())) {
throw new BusinessException(ResultCode.NOT_FOUND, "资源不存在");
}
if (!dataPermissionService.canAccessCreator(currentUser, resource.getCreatorId(), resource.getCreatorRole())) {
throw new BusinessException(ResultCode.FORBIDDEN, "无权访问资源");
}
return toResponse(resource);
}
@Transactional
public void deleteResource(LoginUser currentUser, Long resourceId) {
SourceResource resource = sourceResourceMapper.selectById(resourceId);
if (resource == null || !currentUser.companyId().equals(resource.getCompanyId())) {
throw new BusinessException(ResultCode.NOT_FOUND, "资源不存在");
}
if (!dataPermissionService.canAccessCreator(currentUser, resource.getCreatorId(), resource.getCreatorRole())) {
throw new BusinessException(ResultCode.FORBIDDEN, "无权删除资源");
}
int bindCount = annotationTaskResourceMapper.countByResourceId(resourceId);
if (bindCount > 0) {
resource.setSourceStatus(SourceStatus.ARCHIVED.name());
sourceResourceMapper.updateById(resource);
log.info("archived referenced source resource, companyId={}, userId={}, resourceId={}",
currentUser.companyId(), currentUser.userId(), resourceId);
return;
}
objectStorageService.delete(resource.getBucketName(), resource.getFilePath());
sourceResourceMapper.deleteById(resourceId);
log.info("deleted source resource, companyId={}, userId={}, resourceId={}",
currentUser.companyId(), currentUser.userId(), resourceId);
}
private SourceResourceResponse toResponse(SourceResource resource) {
SysUser creator = sysUserMapper.selectById(resource.getCreatorId());
return new SourceResourceResponse(
resource.getId(),
resource.getResourceName(),
resource.getResourceType(),
resource.getBucketName(),
resource.getFilePath(),
resource.getFileSize(),
resource.getSourceStatus(),
resource.getStorageProvider(),
resource.getRemark(),
creator == null ? null : creator.getRealName(),
resource.getCreatedAt(),
resource.getUpdatedAt());
}
private String resolveExtension(String originalFilename, String resourceType) {
if (StringUtils.hasText(originalFilename) && originalFilename.contains(".")) {
return originalFilename.substring(originalFilename.lastIndexOf('.') + 1);
}
return switch (resourceType) {
case "TEXT" -> "txt";
case "IMAGE" -> "png";
case "VIDEO" -> "mp4";
default -> "bin";
};
}
private <T> PageResult<T> paginate(List<T> records, Integer pageNo, Integer pageSize) {
int actualPageNo = pageNo == null || pageNo < 1 ? 1 : pageNo;
int actualPageSize = pageSize == null || pageSize < 1 ? 10 : pageSize;
int fromIndex = Math.min((actualPageNo - 1) * actualPageSize, records.size());
int toIndex = Math.min(fromIndex + actualPageSize, records.size());
return new PageResult<>(records.subList(fromIndex, toIndex), (long) records.size(), actualPageNo, actualPageSize);
}
}

View File

@@ -0,0 +1,258 @@
package com.labelsys.backend.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.labelsys.backend.common.ResultCode;
import com.labelsys.backend.common.exception.BusinessException;
import com.labelsys.backend.context.LoginUser;
import com.labelsys.backend.dto.common.PageResult;
import com.labelsys.backend.dto.request.ManualModelConfigRequest;
import com.labelsys.backend.dto.request.PromptConfigOptionRequest;
import com.labelsys.backend.dto.request.SaveSysConfigRequest;
import com.labelsys.backend.dto.request.SysConfigPageQuery;
import com.labelsys.backend.dto.request.TaskModelConfigRequest;
import com.labelsys.backend.dto.response.SysConfigResponse;
import com.labelsys.backend.dto.response.TaskModelConfigResponse;
import com.labelsys.backend.dto.response.TaskPromptConfigResponse;
import com.labelsys.backend.entity.SysConfig;
import com.labelsys.backend.enums.ConfigType;
import com.labelsys.backend.mapper.SysConfigMapper;
import com.labelsys.backend.util.IdGenerator;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
@Slf4j
@Service
@RequiredArgsConstructor
public class SysConfigService {
private final SysConfigMapper sysConfigMapper;
private final ObjectMapper objectMapper;
@Transactional
public SysConfig saveConfig(LoginUser currentUser, SaveSysConfigRequest request) {
validateConfigType(request.configType());
SysConfig existing = sysConfigMapper.findByCompanyIdAndConfigName(currentUser.companyId(), request.configName());
if (existing != null) {
throw new BusinessException(ResultCode.CONFLICT, "配置名称已存在");
}
SysConfig config = SysConfig.builder()
.id(IdGenerator.nextId())
.companyId(currentUser.companyId())
.configType(request.configType())
.configName(request.configName())
.configValue(request.configValue())
.status(request.status())
.creatorId(currentUser.userId())
.build();
sysConfigMapper.insert(config);
log.info("saved sys config, companyId={}, userId={}, configName={}, configType={}",
currentUser.companyId(), currentUser.userId(), request.configName(), request.configType());
return config;
}
@Transactional
public SysConfig updateConfig(LoginUser currentUser, Long configId, SaveSysConfigRequest request) {
validateConfigType(request.configType());
SysConfig existing = getConfigEntity(currentUser, configId);
SysConfig duplicate = sysConfigMapper.findByCompanyIdAndConfigName(currentUser.companyId(), request.configName());
if (duplicate != null && !duplicate.getId().equals(configId)) {
throw new BusinessException(ResultCode.CONFLICT, "配置名称已存在");
}
existing.setConfigType(request.configType());
existing.setConfigName(request.configName());
existing.setConfigValue(request.configValue());
existing.setStatus(request.status());
sysConfigMapper.updateById(existing);
log.info("updated sys config, companyId={}, userId={}, configId={}",
currentUser.companyId(), currentUser.userId(), configId);
return existing;
}
public SysConfigResponse getConfig(LoginUser currentUser, Long configId) {
return toResponse(getConfigEntity(currentUser, configId));
}
public PageResult<SysConfigResponse> pageConfigs(LoginUser currentUser, SysConfigPageQuery query) {
LambdaQueryWrapper<SysConfig> wrapper = new LambdaQueryWrapper<SysConfig>()
.eq(SysConfig::getCompanyId, currentUser.companyId())
.eq(StringUtils.hasText(query.configType()), SysConfig::getConfigType, query.configType())
.eq(StringUtils.hasText(query.status()), SysConfig::getStatus, query.status())
.like(StringUtils.hasText(query.configName()), SysConfig::getConfigName, query.configName())
.orderByDesc(SysConfig::getCreatedAt);
List<SysConfigResponse> records = sysConfigMapper.selectList(wrapper).stream()
.sorted(Comparator.comparing(SysConfig::getCreatedAt, Comparator.nullsLast(Comparator.naturalOrder())).reversed())
.map(this::toResponse)
.toList();
return paginate(records, query.pageNo(), query.pageSize());
}
@Transactional
public ResolvedModelConfig resolveModelConfig(LoginUser currentUser, TaskModelConfigRequest request) {
if (request == null || !StringUtils.hasText(request.mode())) {
throw new BusinessException(ResultCode.BAD_REQUEST, "模型配置不能为空");
}
if ("SELECT".equalsIgnoreCase(request.mode())) {
return resolveSelectedModel(currentUser, request.selectedConfigName());
}
if ("MANUAL".equalsIgnoreCase(request.mode())) {
return resolveManualModel(currentUser, request.manualConfig());
}
throw new BusinessException(ResultCode.BAD_REQUEST, "不支持的模型配置模式");
}
public ResolvedPromptConfig resolvePromptConfig(LoginUser currentUser, PromptConfigOptionRequest request) {
if (request == null) {
throw new BusinessException(ResultCode.BAD_REQUEST, "提示词配置不能为空");
}
if (StringUtils.hasText(request.selectedConfigName())) {
SysConfig config = sysConfigMapper.findByCompanyIdAndConfigNameAndType(
currentUser.companyId(), request.selectedConfigName(), ConfigType.PROMPT.name());
if (config == null) {
throw new BusinessException(ResultCode.NOT_FOUND, "提示词配置不存在");
}
return new ResolvedPromptConfig(config.getId(), config.getConfigName(), config.getConfigValue());
}
if (!StringUtils.hasText(request.promptText())) {
throw new BusinessException(ResultCode.BAD_REQUEST, "提示词内容不能为空");
}
return new ResolvedPromptConfig(null, null, request.promptText());
}
public TaskModelConfigResponse toResponse(ResolvedModelConfig config) {
return new TaskModelConfigResponse(
config.configId(),
config.configName(),
config.modelName(),
config.modelUrl(),
maskSecret(config.apiKey()));
}
public TaskPromptConfigResponse toResponse(ResolvedPromptConfig config) {
return new TaskPromptConfigResponse(config.configId(), config.configName(), config.promptText());
}
public SysConfigResponse toResponse(SysConfig config) {
return new SysConfigResponse(
config.getId(),
config.getConfigType(),
config.getConfigName(),
config.getConfigValue(),
config.getStatus(),
config.getCreatorId(),
config.getCreatedAt(),
config.getUpdatedAt());
}
private ResolvedModelConfig resolveSelectedModel(LoginUser currentUser, String configName) {
if (!StringUtils.hasText(configName)) {
throw new BusinessException(ResultCode.BAD_REQUEST, "模型配置名称不能为空");
}
SysConfig config = sysConfigMapper.findByCompanyIdAndConfigNameAndType(
currentUser.companyId(), configName, ConfigType.MODEL.name());
if (config == null) {
throw new BusinessException(ResultCode.NOT_FOUND, "模型配置不存在");
}
ModelConfigValue configValue = parseModelConfig(config.getConfigValue());
return new ResolvedModelConfig(config.getId(), config.getConfigName(),
configValue.modelName(), configValue.modelUrl(), configValue.apiKey());
}
private ResolvedModelConfig resolveManualModel(LoginUser currentUser, ManualModelConfigRequest request) {
if (request == null || !StringUtils.hasText(request.modelName())
|| !StringUtils.hasText(request.modelUrl()) || !StringUtils.hasText(request.apiKey())) {
throw new BusinessException(ResultCode.BAD_REQUEST, "手动模型配置不完整");
}
SysConfig existing = sysConfigMapper.findByCompanyIdAndConfigName(currentUser.companyId(), request.modelName());
if (existing == null) {
String configValue = writeModelConfig(request);
SysConfig config = SysConfig.builder()
.id(IdGenerator.nextId())
.companyId(currentUser.companyId())
.configType(ConfigType.MODEL.name())
.configName(request.modelName())
.configValue(configValue)
.status("ENABLED")
.creatorId(currentUser.userId())
.build();
sysConfigMapper.insert(config);
log.info("auto created model config, companyId={}, userId={}, configName={}",
currentUser.companyId(), currentUser.userId(), request.modelName());
return new ResolvedModelConfig(config.getId(), config.getConfigName(),
request.modelName(), request.modelUrl(), request.apiKey());
}
if (!ConfigType.MODEL.name().equals(existing.getConfigType())) {
throw new BusinessException(ResultCode.CONFLICT, "同名配置已被其他类型占用");
}
ModelConfigValue configValue = parseModelConfig(existing.getConfigValue());
return new ResolvedModelConfig(existing.getId(), existing.getConfigName(),
configValue.modelName(), configValue.modelUrl(), configValue.apiKey());
}
private SysConfig getConfigEntity(LoginUser currentUser, Long configId) {
SysConfig config = sysConfigMapper.selectById(configId);
if (config == null || !currentUser.companyId().equals(config.getCompanyId())) {
throw new BusinessException(ResultCode.NOT_FOUND, "配置不存在");
}
return config;
}
private void validateConfigType(String configType) {
if (!ConfigType.isValid(configType)) {
throw new BusinessException(ResultCode.BAD_REQUEST, "配置类型非法");
}
}
private ModelConfigValue parseModelConfig(String value) {
try {
return objectMapper.readValue(value, ModelConfigValue.class);
} catch (JsonProcessingException ex) {
throw new BusinessException(ResultCode.BAD_REQUEST, "模型配置值格式非法");
}
}
private String writeModelConfig(ManualModelConfigRequest request) {
try {
return objectMapper.writeValueAsString(Map.of(
"modelName", request.modelName(),
"modelUrl", request.modelUrl(),
"apiKey", request.apiKey()));
} catch (JsonProcessingException ex) {
throw new BusinessException(ResultCode.BAD_REQUEST, "模型配置值生成失败");
}
}
private String maskSecret(String secret) {
if (!StringUtils.hasText(secret)) {
return null;
}
if (secret.length() <= 4) {
return "****";
}
return "****" + secret.substring(secret.length() - 4);
}
private <T> PageResult<T> paginate(List<T> records, Integer pageNo, Integer pageSize) {
int actualPageNo = pageNo == null || pageNo < 1 ? 1 : pageNo;
int actualPageSize = pageSize == null || pageSize < 1 ? 10 : pageSize;
int fromIndex = Math.min((actualPageNo - 1) * actualPageSize, records.size());
int toIndex = Math.min(fromIndex + actualPageSize, records.size());
return new PageResult<>(records.subList(fromIndex, toIndex), (long) records.size(), actualPageNo, actualPageSize);
}
private record ModelConfigValue(String modelName, String modelUrl, String apiKey) {
}
public record ResolvedModelConfig(Long configId, String configName, String modelName, String modelUrl, String apiKey) {
}
public record ResolvedPromptConfig(Long configId, String configName, String promptText) {
}
}