diff --git a/pom.xml b/pom.xml index 3cf8ba9..e95862b 100644 --- a/pom.xml +++ b/pom.xml @@ -82,6 +82,14 @@ lombok true + + software.amazon.awssdk + s3 + + + software.amazon.awssdk + sts + com.h2database h2 @@ -126,4 +134,4 @@ - \ No newline at end of file + diff --git a/src/main/java/com/labelsys/backend/LabelsysBackendApplication.java b/src/main/java/com/labelsys/backend/LabelsysBackendApplication.java index bbf8c6c..e502d34 100644 --- a/src/main/java/com/labelsys/backend/LabelsysBackendApplication.java +++ b/src/main/java/com/labelsys/backend/LabelsysBackendApplication.java @@ -3,9 +3,11 @@ package com.labelsys.backend; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.scheduling.annotation.EnableScheduling; @SpringBootApplication @MapperScan("com.labelsys.backend.mapper") +@EnableScheduling public class LabelsysBackendApplication { public static void main(String[] args) { diff --git a/src/main/java/com/labelsys/backend/common/Result.java b/src/main/java/com/labelsys/backend/common/Result.java index b54991f..783a10f 100644 --- a/src/main/java/com/labelsys/backend/common/Result.java +++ b/src/main/java/com/labelsys/backend/common/Result.java @@ -11,13 +11,13 @@ import lombok.NoArgsConstructor; @Schema(description = "统一返回结果") public class Result { - @Schema(description = "业务状态码") + @Schema(description = "业务状态码", example = "0") private Integer code; - @Schema(description = "返回消息") + @Schema(description = "返回消息", example = "success") private String message; - @Schema(description = "返回数据") + @Schema(description = "返回数据", example = "{}") private T data; public static Result success() { diff --git a/src/main/java/com/labelsys/backend/config/MybatisPlusConfig.java b/src/main/java/com/labelsys/backend/config/MybatisPlusConfig.java new file mode 100644 index 0000000..d9dbce0 --- /dev/null +++ b/src/main/java/com/labelsys/backend/config/MybatisPlusConfig.java @@ -0,0 +1,23 @@ +package com.labelsys.backend.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import com.baomidou.mybatisplus.annotation.DbType; +import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; + +@Configuration +public class MybatisPlusConfig { + + @Bean + public MybatisPlusInterceptor mybatisPlusInterceptor() { + MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); + PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor(DbType.POSTGRE_SQL); + paginationInnerInterceptor.setOverflow(false); + paginationInnerInterceptor.setMaxLimit(200L); + interceptor.addInnerInterceptor(paginationInnerInterceptor); + + return interceptor; + } +} diff --git a/src/main/java/com/labelsys/backend/config/ObjectStorageConfig.java b/src/main/java/com/labelsys/backend/config/ObjectStorageConfig.java new file mode 100644 index 0000000..161698b --- /dev/null +++ b/src/main/java/com/labelsys/backend/config/ObjectStorageConfig.java @@ -0,0 +1,31 @@ +package com.labelsys.backend.config; + +import java.net.URI; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.S3Configuration; + +@Configuration +@RequiredArgsConstructor +@EnableConfigurationProperties(ObjectStorageProperties.class) +public class ObjectStorageConfig { + + private final ObjectStorageProperties properties; + + @Bean + public S3Client s3Client() { + return S3Client.builder() + .endpointOverride(URI.create(properties.getEndpoint())) + .region(Region.of(properties.getRegion())) + .credentialsProvider(StaticCredentialsProvider.create( + AwsBasicCredentials.create(properties.getAccessKey(), properties.getSecretKey()))) + .serviceConfiguration(S3Configuration.builder().pathStyleAccessEnabled(properties.isPathStyleAccess()).build()) + .build(); + } +} diff --git a/src/main/java/com/labelsys/backend/config/ObjectStorageProperties.java b/src/main/java/com/labelsys/backend/config/ObjectStorageProperties.java new file mode 100644 index 0000000..b3ca457 --- /dev/null +++ b/src/main/java/com/labelsys/backend/config/ObjectStorageProperties.java @@ -0,0 +1,17 @@ +package com.labelsys.backend.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +@Data +@ConfigurationProperties(prefix = "labelsys.object-storage") +public class ObjectStorageProperties { + private String endpoint; + private String region; + private String accessKey; + private String secretKey; + private boolean pathStyleAccess = true; + private String sourceBucket; + private String artifactBucket; + private String exportBucket; +} diff --git a/src/main/java/com/labelsys/backend/context/LoginUser.java b/src/main/java/com/labelsys/backend/context/LoginUser.java index 7d1d124..c302bff 100644 --- a/src/main/java/com/labelsys/backend/context/LoginUser.java +++ b/src/main/java/com/labelsys/backend/context/LoginUser.java @@ -7,37 +7,28 @@ import com.labelsys.backend.enums.UserRole; import io.swagger.v3.oas.annotations.media.Schema; @Schema(description = "当前登录用户上下文") -public record LoginUser( - @Schema(description = "用户ID") Long userId, - @Schema(description = "公司ID") Long companyId, - @Schema(description = "公司编码") String companyCode, - @Schema(description = "公司名称") String companyName, +public record LoginUser(@Schema(description = "用户ID") Long userId, @Schema(description = "公司ID") Long companyId, + @Schema(description = "公司编码") String companyCode, @Schema(description = "公司名称") String companyName, @Schema(description = "手机号") String phone, @Schema(description = "用户名,可为空", example = "alpha-reviewer") String username, @Schema(description = "真实姓名") String realName, @Schema(description = "角色,枚举值:EMPLOYEE员工、MANAGER部门经理、ENGINEER总工程师") UserRole role, - @Schema(description = "岗位,枚举值:ANNOTATOR标注员、DATA_TRAINER数据训练师、REVIEWER审核员、ADMIN超级管理员") UserPosition position, + @Schema( + description = "岗位,枚举值:ANNOTATOR标注员、DATA_TRAINER数据训练师、REVIEWER审核员、ADMIN超级管理员、SUPER_ADMIN系统管理员") UserPosition position, @Schema(description = "是否必须修改密码") boolean mustChangePassword, - @Schema(description = "会话版本") Integer sessionVersion -) { + @Schema(description = "会话版本") Integer sessionVersion) { public static LoginUser from(SysUser user, SysCompany company) { - return new LoginUser( - user.getId(), - company.getId(), - company.getCompanyCode(), - company.getCompanyName(), - user.getPhone(), - user.getUsername(), - user.getRealName(), - user.getRole(), - user.getPosition(), - Boolean.TRUE.equals(user.getMustChangePassword()), - user.getSessionVersion() - ); + return new LoginUser(user.getId(), company.getId(), company.getCompanyCode(), company.getCompanyName(), + user.getPhone(), user.getUsername(), user.getRealName(), user.getRole(), user.getPosition(), + Boolean.TRUE.equals(user.getMustChangePassword()), user.getSessionVersion()); } - public boolean isPlatformAdmin() { - return "PLATFORM".equals(companyCode) && position == UserPosition.ADMIN; + // public boolean isPlatformAdmin() { + // return "PLATFORM".equals(companyCode) && position == UserPosition.ADMIN; + // } + + public boolean isSuperAdmin() { + return position == UserPosition.SUPER_ADMIN; } } diff --git a/src/main/java/com/labelsys/backend/controller/AnnotationResultController.java b/src/main/java/com/labelsys/backend/controller/AnnotationResultController.java new file mode 100644 index 0000000..412b1dd --- /dev/null +++ b/src/main/java/com/labelsys/backend/controller/AnnotationResultController.java @@ -0,0 +1,72 @@ +package com.labelsys.backend.controller; + +import com.labelsys.backend.annotation.RequirePosition; +import com.labelsys.backend.common.Result; +import com.labelsys.backend.context.UserContext; +import com.labelsys.backend.dto.common.PageResult; +import com.labelsys.backend.dto.request.AnnotationResultPageQuery; +import com.labelsys.backend.dto.request.MergeReviewResultRequest; +import com.labelsys.backend.dto.response.AnnotationResultCompareResponse; +import com.labelsys.backend.dto.response.AnnotationResultResponse; +import com.labelsys.backend.dto.response.MergeReviewResultResponse; +import com.labelsys.backend.enums.UserPosition; +import com.labelsys.backend.service.AnnotationResultArchiveService; +import com.labelsys.backend.service.AnnotationResultService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springdoc.core.annotations.ParameterObject; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@Tag(name = "标注结果管理") +@RestController +@RequestMapping("/api/annotation-results") +@RequiredArgsConstructor +public class AnnotationResultController { + + private final AnnotationResultService annotationResultService; + private final AnnotationResultArchiveService annotationResultArchiveService; + + @Operation(summary = "分页查询标注结果") + @GetMapping + public Result> page(@ParameterObject AnnotationResultPageQuery query) { + return Result.success(annotationResultService.pageResults(UserContext.requireUser(), query)); + } + + @Operation(summary = "查询标注结果详情") + @GetMapping("/{id}") + public Result detail( + @Parameter(description = "结果ID", example = "191000000000000401") + @PathVariable Long id + ) { + return Result.success(annotationResultService.getResult(UserContext.requireUser(), id)); + } + + @Operation(summary = "查询标注结果比对信息") + @RequirePosition(UserPosition.REVIEWER) + @GetMapping("/{id}/compare") + public Result compare( + @Parameter(description = "结果ID", example = "191000000000000401") + @PathVariable Long id + ) { + return Result.success(annotationResultService.compareResult(UserContext.requireUser(), id)); + } + + @Operation(summary = "提交合并审核结果") + @RequirePosition(UserPosition.REVIEWER) + @PostMapping("/{id}/merge-review") + public Result mergeReview( + @Parameter(description = "结果ID", example = "191000000000000401") + @PathVariable Long id, + @Valid @RequestBody MergeReviewResultRequest request + ) { + return Result.success(annotationResultArchiveService.mergeReview(UserContext.requireUser(), id, request)); + } +} diff --git a/src/main/java/com/labelsys/backend/controller/AnnotationTaskController.java b/src/main/java/com/labelsys/backend/controller/AnnotationTaskController.java new file mode 100644 index 0000000..1723173 --- /dev/null +++ b/src/main/java/com/labelsys/backend/controller/AnnotationTaskController.java @@ -0,0 +1,74 @@ +package com.labelsys.backend.controller; + +import com.labelsys.backend.common.Result; +import com.labelsys.backend.context.UserContext; +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.service.AnnotationTaskService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springdoc.core.annotations.ParameterObject; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@Tag(name = "标注任务管理") +@RestController +@RequestMapping("/api/annotation-tasks") +@RequiredArgsConstructor +public class AnnotationTaskController { + + private final AnnotationTaskService annotationTaskService; + + @Operation(summary = "创建标注任务") + @PostMapping + public Result create(@Valid @RequestBody CreateAnnotationTaskRequest request) { + return Result.success(annotationTaskService.createTask(UserContext.requireUser(), request)); + } + + @Operation(summary = "更新标注任务") + @PutMapping("/{id}") + public Result update( + @Parameter(description = "任务ID", example = "191000000000000301") + @PathVariable Long id, + @Valid @RequestBody UpdateAnnotationTaskRequest request + ) { + return Result.success(annotationTaskService.updateTask(UserContext.requireUser(), id, request)); + } + + @Operation(summary = "分页查询标注任务") + @GetMapping + public Result> page(@ParameterObject AnnotationTaskPageQuery query) { + return Result.success(annotationTaskService.pageTasks(UserContext.requireUser(), query)); + } + + @Operation(summary = "查询标注任务详情") + @GetMapping("/{id}") + public Result detail( + @Parameter(description = "任务ID", example = "191000000000000301") + @PathVariable Long id + ) { + return Result.success(annotationTaskService.getTask(UserContext.requireUser(), id)); + } + + @Operation(summary = "删除标注任务") + @DeleteMapping("/{id}") + public Result delete( + @Parameter(description = "任务ID", example = "191000000000000301") + @PathVariable Long id + ) { + annotationTaskService.deleteTask(UserContext.requireUser(), id); + return Result.success(); + } +} diff --git a/src/main/java/com/labelsys/backend/controller/AuthController.java b/src/main/java/com/labelsys/backend/controller/AuthController.java index 34bcc6e..e0dc11c 100644 --- a/src/main/java/com/labelsys/backend/controller/AuthController.java +++ b/src/main/java/com/labelsys/backend/controller/AuthController.java @@ -10,6 +10,7 @@ import com.labelsys.backend.dto.response.CurrentUserResponse; import com.labelsys.backend.dto.response.LoginResponse; import com.labelsys.backend.service.AuthService; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import java.util.List; @@ -33,7 +34,10 @@ public class AuthController { @Operation(summary = "根据手机号查询可登录公司") @GetMapping("/companies") - public Result> listCompanies(@RequestParam String phone) { + public Result> listCompanies( + @Parameter(description = "手机号", example = "13800138000") + @RequestParam String phone + ) { return Result.success(authService.listAvailableCompanies(phone)); } @@ -52,7 +56,10 @@ public class AuthController { @Operation(summary = "退出登录") @PostMapping("/logout") - public Result logout(@RequestHeader(HttpHeaders.AUTHORIZATION) String authorization) { + public Result logout( + @Parameter(description = "Bearer 访问令牌", example = "Bearer eyJhbGciOiJIUzI1NiJ9.demo.token") + @RequestHeader(HttpHeaders.AUTHORIZATION) String authorization + ) { authService.logout(extractToken(authorization)); return Result.success(); } diff --git a/src/main/java/com/labelsys/backend/controller/CompanyUserController.java b/src/main/java/com/labelsys/backend/controller/CompanyUserController.java index 078c727..5885b91 100644 --- a/src/main/java/com/labelsys/backend/controller/CompanyUserController.java +++ b/src/main/java/com/labelsys/backend/controller/CompanyUserController.java @@ -10,6 +10,7 @@ import com.labelsys.backend.dto.response.UserResponse; import com.labelsys.backend.enums.UserPosition; import com.labelsys.backend.service.UserService; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import java.util.List; @@ -45,14 +46,22 @@ public class CompanyUserController { @Operation(summary = "修改员工角色和岗位") @PutMapping("/{userId}/assignment") - public Result updateAssignment(@PathVariable Long userId, @Valid @RequestBody UpdateUserAssignmentRequest request) { + public Result updateAssignment( + @Parameter(description = "用户ID", example = "191000000000000021") + @PathVariable Long userId, + @Valid @RequestBody UpdateUserAssignmentRequest request + ) { userService.updateAssignment(UserContext.requireUser(), userId, request); return Result.success(); } @Operation(summary = "修改员工状态") @PutMapping("/{userId}/status") - public Result updateStatus(@PathVariable Long userId, @Valid @RequestBody UpdateUserStatusRequest request) { + public Result updateStatus( + @Parameter(description = "用户ID", example = "191000000000000021") + @PathVariable Long userId, + @Valid @RequestBody UpdateUserStatusRequest request + ) { userService.updateStatus(UserContext.requireUser(), userId, request); return Result.success(); } diff --git a/src/main/java/com/labelsys/backend/controller/PlatformCompanyAdminController.java b/src/main/java/com/labelsys/backend/controller/PlatformCompanyAdminController.java index 51c98d8..b7d33db 100644 --- a/src/main/java/com/labelsys/backend/controller/PlatformCompanyAdminController.java +++ b/src/main/java/com/labelsys/backend/controller/PlatformCompanyAdminController.java @@ -4,11 +4,13 @@ import com.labelsys.backend.annotation.RequirePosition; import com.labelsys.backend.common.Result; import com.labelsys.backend.context.UserContext; import com.labelsys.backend.dto.request.CreateCompanyAdminRequest; +import com.labelsys.backend.dto.request.CreateSystemEngineerAdminRequest; import com.labelsys.backend.dto.request.UpdateUserStatusRequest; import com.labelsys.backend.dto.response.UserResponse; import com.labelsys.backend.enums.UserPosition; import com.labelsys.backend.service.UserService; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import java.util.List; @@ -25,7 +27,7 @@ import org.springframework.web.bind.annotation.RestController; @Tag(name = "平台公司管理员管理") @RestController @RequestMapping("/api/platform/company-admins") -@RequirePosition(UserPosition.ADMIN) +@RequirePosition(UserPosition.SUPER_ADMIN) @RequiredArgsConstructor public class PlatformCompanyAdminController { @@ -33,20 +35,37 @@ public class PlatformCompanyAdminController { @Operation(summary = "查询指定公司管理员列表") @GetMapping - public Result> listCompanyAdmins(@RequestParam Long companyId) { + public Result> listCompanyAdmins( + @Parameter(description = "公司ID", example = "191000000000000001") + @RequestParam Long companyId + ) { return Result.success(userService.listCompanyAdmins(UserContext.requireUser(), companyId).stream().map(UserResponse::from).toList()); } + @Operation(summary = "查询所有公司用户列表") + @GetMapping("/all") + public Result> listAllUsers() { + return Result.success(userService.listAllUsers(UserContext.requireUser()).stream().map(UserResponse::from).toList()); + } + @Operation(summary = "创建公司管理员") @PostMapping public Result createCompanyAdmin(@Valid @RequestBody CreateCompanyAdminRequest request) { return Result.success(UserResponse.from(userService.createCompanyAdmin(UserContext.requireUser(), request))); } + @Operation(summary = "创建系统管理员") + @PostMapping("/system-engineer") + public Result createSystemEngineerAdmin(@Valid @RequestBody CreateSystemEngineerAdminRequest request) { + return Result.success(UserResponse.from(userService.createSystemEngineerAdmin(UserContext.requireUser(), request))); + } + @Operation(summary = "修改公司管理员状态") @PutMapping("/{companyId}/{userId}/status") public Result updateCompanyAdminStatus( + @Parameter(description = "公司ID", example = "191000000000000001") @PathVariable Long companyId, + @Parameter(description = "用户ID", example = "191000000000000021") @PathVariable Long userId, @Valid @RequestBody UpdateUserStatusRequest request ) { diff --git a/src/main/java/com/labelsys/backend/controller/PlatformCompanyController.java b/src/main/java/com/labelsys/backend/controller/PlatformCompanyController.java index 2a6e7ad..ed3c27e 100644 --- a/src/main/java/com/labelsys/backend/controller/PlatformCompanyController.java +++ b/src/main/java/com/labelsys/backend/controller/PlatformCompanyController.java @@ -9,6 +9,7 @@ import com.labelsys.backend.dto.response.CompanyResponse; import com.labelsys.backend.enums.UserPosition; import com.labelsys.backend.service.CompanyService; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import java.util.List; @@ -24,7 +25,7 @@ import org.springframework.web.bind.annotation.RestController; @Tag(name = "平台公司管理") @RestController @RequestMapping("/api/platform/companies") -@RequirePosition(UserPosition.ADMIN) +@RequirePosition(UserPosition.SUPER_ADMIN) @RequiredArgsConstructor public class PlatformCompanyController { @@ -44,7 +45,11 @@ public class PlatformCompanyController { @Operation(summary = "修改公司状态") @PutMapping("/{companyId}/status") - public Result updateCompanyStatus(@PathVariable Long companyId, @Valid @RequestBody UpdateCompanyStatusRequest request) { + public Result updateCompanyStatus( + @Parameter(description = "公司ID", example = "191000000000000001") + @PathVariable Long companyId, + @Valid @RequestBody UpdateCompanyStatusRequest request + ) { companyService.updateStatus(UserContext.requireUser(), companyId, request); return Result.success(); } diff --git a/src/main/java/com/labelsys/backend/controller/SourceResourceController.java b/src/main/java/com/labelsys/backend/controller/SourceResourceController.java new file mode 100644 index 0000000..2891642 --- /dev/null +++ b/src/main/java/com/labelsys/backend/controller/SourceResourceController.java @@ -0,0 +1,62 @@ +package com.labelsys.backend.controller; + +import com.labelsys.backend.common.Result; +import com.labelsys.backend.context.UserContext; +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.service.SourceResourceService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springdoc.core.annotations.ParameterObject; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@Tag(name = "资源管理") +@RestController +@RequestMapping("/api/source-resources") +@RequiredArgsConstructor +public class SourceResourceController { + + private final SourceResourceService sourceResourceService; + + @Operation(summary = "上传资源") + @PostMapping("/upload") + public Result upload(@ParameterObject @ModelAttribute SourceUploadRequest request) { + return Result.success(sourceResourceService.upload(UserContext.requireUser(), request)); + } + + @Operation(summary = "分页查询资源") + @GetMapping + public Result> page(@ParameterObject @ModelAttribute SourceResourcePageQuery query) { + return Result.success(sourceResourceService.pageResources(UserContext.requireUser(), query)); + } + + @Operation(summary = "查询资源详情") + @GetMapping("/{id}") + public Result detail( + @Parameter(description = "资源ID", example = "191000000000000101") + @PathVariable Long id + ) { + return Result.success(sourceResourceService.getResource(UserContext.requireUser(), id)); + } + + @Operation(summary = "删除资源") + @DeleteMapping("/{id}") + public Result delete( + @Parameter(description = "资源ID", example = "191000000000000101") + @PathVariable Long id + ) { + sourceResourceService.deleteResource(UserContext.requireUser(), id); + return Result.success(); + } +} diff --git a/src/main/java/com/labelsys/backend/controller/SysConfigController.java b/src/main/java/com/labelsys/backend/controller/SysConfigController.java new file mode 100644 index 0000000..56fceee --- /dev/null +++ b/src/main/java/com/labelsys/backend/controller/SysConfigController.java @@ -0,0 +1,66 @@ +package com.labelsys.backend.controller; + +import com.labelsys.backend.annotation.RequirePosition; +import com.labelsys.backend.common.Result; +import com.labelsys.backend.context.UserContext; +import com.labelsys.backend.dto.common.PageResult; +import com.labelsys.backend.dto.request.SaveSysConfigRequest; +import com.labelsys.backend.dto.request.SysConfigPageQuery; +import com.labelsys.backend.dto.response.SysConfigResponse; +import com.labelsys.backend.enums.UserPosition; +import com.labelsys.backend.service.SysConfigService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springdoc.core.annotations.ParameterObject; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@Tag(name = "系统配置管理") +@RestController +@RequestMapping("/api/sys-configs") +@RequiredArgsConstructor +public class SysConfigController { + + private final SysConfigService sysConfigService; + + @Operation(summary = "创建系统配置") + @RequirePosition(UserPosition.ADMIN) + @PostMapping + public Result create(@Valid @RequestBody SaveSysConfigRequest request) { + return Result.success(sysConfigService.toResponse(sysConfigService.saveConfig(UserContext.requireUser(), request))); + } + + @Operation(summary = "更新系统配置") + @RequirePosition(UserPosition.ADMIN) + @PutMapping("/{id}") + public Result update( + @Parameter(description = "配置ID", example = "191000000000000501") + @PathVariable Long id, + @Valid @RequestBody SaveSysConfigRequest request + ) { + return Result.success(sysConfigService.toResponse(sysConfigService.updateConfig(UserContext.requireUser(), id, request))); + } + + @Operation(summary = "分页查询系统配置") + @GetMapping + public Result> page(@ParameterObject SysConfigPageQuery query) { + return Result.success(sysConfigService.pageConfigs(UserContext.requireUser(), query)); + } + + @Operation(summary = "查询系统配置详情") + @GetMapping("/{id}") + public Result detail( + @Parameter(description = "配置ID", example = "191000000000000501") + @PathVariable Long id + ) { + return Result.success(sysConfigService.getConfig(UserContext.requireUser(), id)); + } +} diff --git a/src/main/java/com/labelsys/backend/dto/common/PageResult.java b/src/main/java/com/labelsys/backend/dto/common/PageResult.java new file mode 100644 index 0000000..39011f1 --- /dev/null +++ b/src/main/java/com/labelsys/backend/dto/common/PageResult.java @@ -0,0 +1,17 @@ +package com.labelsys.backend.dto.common; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.List; + +@Schema(description = "分页结果") +public record PageResult( + @Schema(description = "当前页记录", example = "[]") List records, + @Schema(description = "总记录数", example = "2") Long total, + @Schema(description = "页码", example = "1") Integer pageNo, + @Schema(description = "每页数量", example = "10") Integer pageSize +) { + public static PageResult from(IPage page) { + return new PageResult<>(page.getRecords(), page.getTotal(), (int) page.getCurrent(), (int) page.getSize()); + } +} diff --git a/src/main/java/com/labelsys/backend/dto/request/AnnotationResultPageQuery.java b/src/main/java/com/labelsys/backend/dto/request/AnnotationResultPageQuery.java new file mode 100644 index 0000000..21e5ff6 --- /dev/null +++ b/src/main/java/com/labelsys/backend/dto/request/AnnotationResultPageQuery.java @@ -0,0 +1,14 @@ +package com.labelsys.backend.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "标注结果分页查询请求") +public record AnnotationResultPageQuery( + @Schema(description = "任务ID", example = "191000000000000301") Long taskId, + @Schema(description = "资源ID", example = "191000000000000101") Long resourceId, + @Schema(description = "是否需要人工审核", example = "true") Boolean requiresManualReview, + @Schema(description = "运行态状态", example = "MANUAL_REVIEW_PENDING") String runtimeStatus, + @Schema(description = "页码", example = "1") Integer pageNo, + @Schema(description = "每页数量", example = "10") Integer pageSize +) { +} diff --git a/src/main/java/com/labelsys/backend/dto/request/AnnotationTaskPageQuery.java b/src/main/java/com/labelsys/backend/dto/request/AnnotationTaskPageQuery.java new file mode 100644 index 0000000..a15b67e --- /dev/null +++ b/src/main/java/com/labelsys/backend/dto/request/AnnotationTaskPageQuery.java @@ -0,0 +1,15 @@ +package com.labelsys.backend.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "标注任务分页查询请求") +public record AnnotationTaskPageQuery( + @Schema(description = "关键字", example = "运输") String keyword, + @Schema(description = "任务类型", example = "EXTRACT_QA") String taskType, + @Schema(description = "任务状态", example = "PENDING") String taskStatus, + @Schema(description = "资源ID", example = "191000000000000101") Long resourceId, + @Schema(description = "是否已删除", example = "false") Boolean isDeleted, + @Schema(description = "页码", example = "1") Integer pageNo, + @Schema(description = "每页数量", example = "10") Integer pageSize +) { +} diff --git a/src/main/java/com/labelsys/backend/dto/request/ChangePasswordRequest.java b/src/main/java/com/labelsys/backend/dto/request/ChangePasswordRequest.java index 14e9f53..fa55b90 100644 --- a/src/main/java/com/labelsys/backend/dto/request/ChangePasswordRequest.java +++ b/src/main/java/com/labelsys/backend/dto/request/ChangePasswordRequest.java @@ -5,8 +5,8 @@ import jakarta.validation.constraints.NotBlank; @Schema(description = "修改密码请求") public record ChangePasswordRequest( - @Schema(description = "旧密码") @NotBlank(message = "不能为空") String oldPassword, - @Schema(description = "新密码") @NotBlank(message = "不能为空") String newPassword, - @Schema(description = "确认新密码") @NotBlank(message = "不能为空") String confirmPassword + @Schema(description = "旧密码", example = "P@ssw0rd!") @NotBlank(message = "不能为空") String oldPassword, + @Schema(description = "新密码", example = "N3wP@ssw0rd!") @NotBlank(message = "不能为空") String newPassword, + @Schema(description = "确认新密码", example = "N3wP@ssw0rd!") @NotBlank(message = "不能为空") String confirmPassword ) { } diff --git a/src/main/java/com/labelsys/backend/dto/request/CreateAnnotationTaskRequest.java b/src/main/java/com/labelsys/backend/dto/request/CreateAnnotationTaskRequest.java new file mode 100644 index 0000000..be1b837 --- /dev/null +++ b/src/main/java/com/labelsys/backend/dto/request/CreateAnnotationTaskRequest.java @@ -0,0 +1,20 @@ +package com.labelsys.backend.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import java.util.List; + +@Schema(description = "创建标注任务请求") +public record CreateAnnotationTaskRequest( + @Schema(description = "任务名称", example = "运输文档问答抽取任务") @NotBlank(message = "任务名称不能为空") String taskName, + @Schema(description = "行业类型", example = "transport") String industryType, + @Schema(description = "任务类型", example = "EXTRACT_QA") String taskType, + @Schema(description = "资源ID列表", example = "[191000000000000101,191000000000000102]") @NotEmpty(message = "资源列表不能为空") List resourceIds, + @Schema(description = "抽取模型配置", example = "{\"mode\":\"SELECT\",\"selectedConfigName\":\"qwen-plus-extract\"}") @Valid TaskModelConfigRequest extractModel, + @Schema(description = "校验模型配置", example = "{\"mode\":\"MANUAL\",\"manualConfig\":{\"modelName\":\"qwen-max\",\"modelUrl\":\"https://dashscope.aliyuncs.com/compatible-mode/v1\",\"apiKey\":\"sk-demo5678\"}}") @Valid TaskModelConfigRequest verifyModel, + @Schema(description = "抽取提示词配置", example = "{\"selectedConfigName\":\"qa-extract-v1\"}") @Valid PromptConfigOptionRequest extractPrompt, + @Schema(description = "校验提示词配置", example = "{\"promptText\":\"请对抽取结果进行逐项校验,并输出差异说明。\"}") @Valid PromptConfigOptionRequest verifyPrompt +) { +} diff --git a/src/main/java/com/labelsys/backend/dto/request/CreateCompanyAdminRequest.java b/src/main/java/com/labelsys/backend/dto/request/CreateCompanyAdminRequest.java index cc6f906..707068d 100644 --- a/src/main/java/com/labelsys/backend/dto/request/CreateCompanyAdminRequest.java +++ b/src/main/java/com/labelsys/backend/dto/request/CreateCompanyAdminRequest.java @@ -6,9 +6,9 @@ import jakarta.validation.constraints.NotNull; @Schema(description = "创建公司管理员请求") public record CreateCompanyAdminRequest( - @Schema(description = "公司ID") @NotNull(message = "不能为空") Long companyId, - @Schema(description = "手机号") @NotBlank(message = "不能为空") String phone, + @Schema(description = "公司ID", example = "191000000000000001") @NotNull(message = "不能为空") Long companyId, + @Schema(description = "手机号", example = "13800138001") @NotBlank(message = "不能为空") String phone, @Schema(description = "用户名,前端展示用,可为空", example = "platform-ops") String username, - @Schema(description = "真实姓名") @NotBlank(message = "不能为空") String realName + @Schema(description = "真实姓名", example = "平台运维") @NotBlank(message = "不能为空") String realName ) { } diff --git a/src/main/java/com/labelsys/backend/dto/request/CreateCompanyRequest.java b/src/main/java/com/labelsys/backend/dto/request/CreateCompanyRequest.java index 5dbd65f..11979fd 100644 --- a/src/main/java/com/labelsys/backend/dto/request/CreateCompanyRequest.java +++ b/src/main/java/com/labelsys/backend/dto/request/CreateCompanyRequest.java @@ -5,7 +5,7 @@ import jakarta.validation.constraints.NotBlank; @Schema(description = "创建公司请求") public record CreateCompanyRequest( - @Schema(description = "公司编码") @NotBlank(message = "不能为空") String companyCode, - @Schema(description = "公司名称") @NotBlank(message = "不能为空") String companyName + @Schema(description = "公司编码", example = "ALPHA") @NotBlank(message = "不能为空") String companyCode, + @Schema(description = "公司名称", example = "阿尔法标注有限公司") @NotBlank(message = "不能为空") String companyName ) { } diff --git a/src/main/java/com/labelsys/backend/dto/request/CreateSystemEngineerAdminRequest.java b/src/main/java/com/labelsys/backend/dto/request/CreateSystemEngineerAdminRequest.java new file mode 100644 index 0000000..e3a50a2 --- /dev/null +++ b/src/main/java/com/labelsys/backend/dto/request/CreateSystemEngineerAdminRequest.java @@ -0,0 +1,10 @@ +package com.labelsys.backend.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; + +@Schema(description = "创建系统工程师管理员请求") +public record CreateSystemEngineerAdminRequest( + @Schema(description = "手机号", example = "13800138002") @NotBlank(message = "不能为空") String phone, + @Schema(description = "用户名,前端展示用,可为空", example = "system-engineer") String username, + @Schema(description = "真实姓名", example = "系统工程师") @NotBlank(message = "不能为空") String realName) {} \ No newline at end of file diff --git a/src/main/java/com/labelsys/backend/dto/request/CreateUserRequest.java b/src/main/java/com/labelsys/backend/dto/request/CreateUserRequest.java index 42bc05e..e0cbf52 100644 --- a/src/main/java/com/labelsys/backend/dto/request/CreateUserRequest.java +++ b/src/main/java/com/labelsys/backend/dto/request/CreateUserRequest.java @@ -8,10 +8,10 @@ import jakarta.validation.constraints.NotNull; @Schema(description = "创建员工请求") public record CreateUserRequest( - @Schema(description = "手机号") @NotBlank(message = "不能为空") String phone, + @Schema(description = "手机号", example = "13800138002") @NotBlank(message = "不能为空") String phone, @Schema(description = "用户名,前端展示用,可为空", example = "alpha-admin") String username, - @Schema(description = "真实姓名") @NotBlank(message = "不能为空") String realName, - @Schema(description = "角色,枚举值:EMPLOYEE员工、MANAGER部门经理、ENGINEER总工程师") @NotNull(message = "不能为空") UserRole role, - @Schema(description = "岗位,枚举值:ANNOTATOR标注员、DATA_TRAINER数据训练师、REVIEWER审核员、ADMIN超级管理员") @NotNull(message = "不能为空") UserPosition position + @Schema(description = "真实姓名", example = "张审核") @NotBlank(message = "不能为空") String realName, + @Schema(description = "角色,枚举值:EMPLOYEE员工、MANAGER部门经理、ENGINEER总工程师", example = "EMPLOYEE") @NotNull(message = "不能为空") UserRole role, + @Schema(description = "岗位,枚举值:ANNOTATOR标注员、DATA_TRAINER数据训练师、REVIEWER审核员、ADMIN超级管理员", example = "ANNOTATOR") @NotNull(message = "不能为空") UserPosition position ) { } diff --git a/src/main/java/com/labelsys/backend/dto/request/LoginRequest.java b/src/main/java/com/labelsys/backend/dto/request/LoginRequest.java index a47f8f9..ccddbcf 100644 --- a/src/main/java/com/labelsys/backend/dto/request/LoginRequest.java +++ b/src/main/java/com/labelsys/backend/dto/request/LoginRequest.java @@ -5,8 +5,8 @@ import jakarta.validation.constraints.NotBlank; @Schema(description = "登录请求") public record LoginRequest( - @Schema(description = "手机号") @NotBlank(message = "不能为空") String phone, - @Schema(description = "公司编码") @NotBlank(message = "不能为空") String companyCode, - @Schema(description = "登录密码") @NotBlank(message = "不能为空") String password + @Schema(description = "手机号", example = "平台管理员:13900000000") @NotBlank(message = "不能为空") String phone, + @Schema(description = "公司编码", example = "平台编号:PLATFORM") @NotBlank(message = "不能为空") String companyCode, + @Schema(description = "登录密码", example = "平台密码:admin@123") @NotBlank(message = "不能为空") String password ) { } diff --git a/src/main/java/com/labelsys/backend/dto/request/ManualModelConfigRequest.java b/src/main/java/com/labelsys/backend/dto/request/ManualModelConfigRequest.java new file mode 100644 index 0000000..85d2eeb --- /dev/null +++ b/src/main/java/com/labelsys/backend/dto/request/ManualModelConfigRequest.java @@ -0,0 +1,12 @@ +package com.labelsys.backend.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; + +@Schema(description = "手动模型配置请求") +public record ManualModelConfigRequest( + @Schema(description = "模型名称", example = "qwen-plus") @NotBlank(message = "模型名称不能为空") String modelName, + @Schema(description = "模型地址", example = "https://dashscope.aliyuncs.com/compatible-mode/v1") @NotBlank(message = "模型地址不能为空") String modelUrl, + @Schema(description = "模型密钥", example = "sk-demo1234") @NotBlank(message = "模型密钥不能为空") String apiKey +) { +} diff --git a/src/main/java/com/labelsys/backend/dto/request/MergeReviewResultRequest.java b/src/main/java/com/labelsys/backend/dto/request/MergeReviewResultRequest.java new file mode 100644 index 0000000..d8bc6ab --- /dev/null +++ b/src/main/java/com/labelsys/backend/dto/request/MergeReviewResultRequest.java @@ -0,0 +1,12 @@ +package com.labelsys.backend.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; + +@Schema(description = "合并审核结果请求") +public record MergeReviewResultRequest( + @Schema(description = "差异摘要 JSON", example = "{\"changed\":[{\"field\":\"answer\",\"from\":\"3天\",\"to\":\"72小时\"}],\"summary\":\"统一时间表达\"}") @NotBlank(message = "差异摘要不能为空") String diffSummary, + @Schema(description = "最终问答内容 JSON", example = "[{\"question\":\"运输时效是多久?\",\"answer\":\"72小时\"}]") @NotBlank(message = "问答内容不能为空") String qaContentJson, + @Schema(description = "审核备注", example = "已按审核意见合并,统一为小时口径。") String reviewComment +) { +} diff --git a/src/main/java/com/labelsys/backend/dto/request/PromptConfigOptionRequest.java b/src/main/java/com/labelsys/backend/dto/request/PromptConfigOptionRequest.java new file mode 100644 index 0000000..e8a1195 --- /dev/null +++ b/src/main/java/com/labelsys/backend/dto/request/PromptConfigOptionRequest.java @@ -0,0 +1,10 @@ +package com.labelsys.backend.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "任务提示词配置请求") +public record PromptConfigOptionRequest( + @Schema(description = "已选择的提示词配置名称", example = "qa-extract-v1") String selectedConfigName, + @Schema(description = "手动输入的提示词内容", example = "请抽取文档中的问答对,并按 JSON 数组输出。") String promptText +) { +} diff --git a/src/main/java/com/labelsys/backend/dto/request/SaveSysConfigRequest.java b/src/main/java/com/labelsys/backend/dto/request/SaveSysConfigRequest.java new file mode 100644 index 0000000..e03e773 --- /dev/null +++ b/src/main/java/com/labelsys/backend/dto/request/SaveSysConfigRequest.java @@ -0,0 +1,13 @@ +package com.labelsys.backend.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; + +@Schema(description = "保存系统配置请求") +public record SaveSysConfigRequest( + @Schema(description = "配置类型", example = "MODEL") @NotBlank(message = "配置类型不能为空") String configType, + @Schema(description = "配置名称", example = "qwen-plus-extract") @NotBlank(message = "配置名称不能为空") String configName, + @Schema(description = "配置值", example = "{\"modelName\":\"qwen-plus\",\"modelUrl\":\"https://dashscope.aliyuncs.com/compatible-mode/v1\",\"apiKey\":\"sk-demo1234\"}") @NotBlank(message = "配置值不能为空") String configValue, + @Schema(description = "配置状态", example = "ENABLED") @NotBlank(message = "配置状态不能为空") String status +) { +} diff --git a/src/main/java/com/labelsys/backend/dto/request/SourceResourcePageQuery.java b/src/main/java/com/labelsys/backend/dto/request/SourceResourcePageQuery.java new file mode 100644 index 0000000..ee2c21e --- /dev/null +++ b/src/main/java/com/labelsys/backend/dto/request/SourceResourcePageQuery.java @@ -0,0 +1,13 @@ +package com.labelsys.backend.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "资源分页查询请求") +public record SourceResourcePageQuery( + @Schema(description = "关键字", example = "运输") String keyword, + @Schema(description = "资源类型", example = "TEXT") String resourceType, + @Schema(description = "资源状态", example = "READY") String sourceStatus, + @Schema(description = "页码", example = "1") Integer pageNo, + @Schema(description = "每页数量", example = "10") Integer pageSize +) { +} diff --git a/src/main/java/com/labelsys/backend/dto/request/SourceUploadRequest.java b/src/main/java/com/labelsys/backend/dto/request/SourceUploadRequest.java new file mode 100644 index 0000000..38eb0ea --- /dev/null +++ b/src/main/java/com/labelsys/backend/dto/request/SourceUploadRequest.java @@ -0,0 +1,22 @@ +package com.labelsys.backend.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.springframework.web.multipart.MultipartFile; + +@Data +@Schema(description = "资源上传请求") +public class SourceUploadRequest { + + @Schema(description = "资源名称", example = "2026年4月运输巡检记录") + private String resourceName; + + @Schema(description = "资源类型:TEXT、IMAGE、VIDEO", example = "TEXT") + private String resourceType; + + @Schema(description = "备注", example = "第一批导入样本") + private String remark; + + @Schema(description = "上传文件", example = "inspection-record.txt") + private MultipartFile file; +} diff --git a/src/main/java/com/labelsys/backend/dto/request/SysConfigPageQuery.java b/src/main/java/com/labelsys/backend/dto/request/SysConfigPageQuery.java new file mode 100644 index 0000000..e2af1be --- /dev/null +++ b/src/main/java/com/labelsys/backend/dto/request/SysConfigPageQuery.java @@ -0,0 +1,13 @@ +package com.labelsys.backend.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "系统配置分页查询请求") +public record SysConfigPageQuery( + @Schema(description = "配置类型", example = "MODEL") String configType, + @Schema(description = "配置名称", example = "qwen-plus-extract") String configName, + @Schema(description = "配置状态", example = "ENABLED") String status, + @Schema(description = "页码", example = "1") Integer pageNo, + @Schema(description = "每页数量", example = "10") Integer pageSize +) { +} diff --git a/src/main/java/com/labelsys/backend/dto/request/TaskModelConfigRequest.java b/src/main/java/com/labelsys/backend/dto/request/TaskModelConfigRequest.java new file mode 100644 index 0000000..c5d59fc --- /dev/null +++ b/src/main/java/com/labelsys/backend/dto/request/TaskModelConfigRequest.java @@ -0,0 +1,13 @@ +package com.labelsys.backend.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; + +@Schema(description = "任务模型配置请求") +public record TaskModelConfigRequest( + @Schema(description = "配置模式:SELECT 或 MANUAL", example = "SELECT") @NotBlank(message = "配置模式不能为空") String mode, + @Schema(description = "已选择的配置名称", example = "qwen-plus-extract") String selectedConfigName, + @Schema(description = "手动录入的模型配置", example = "{\"modelName\":\"qwen-plus\",\"modelUrl\":\"https://dashscope.aliyuncs.com/compatible-mode/v1\",\"apiKey\":\"sk-demo1234\"}") @Valid ManualModelConfigRequest manualConfig +) { +} diff --git a/src/main/java/com/labelsys/backend/dto/request/UpdateAnnotationTaskRequest.java b/src/main/java/com/labelsys/backend/dto/request/UpdateAnnotationTaskRequest.java new file mode 100644 index 0000000..556d67c --- /dev/null +++ b/src/main/java/com/labelsys/backend/dto/request/UpdateAnnotationTaskRequest.java @@ -0,0 +1,18 @@ +package com.labelsys.backend.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotEmpty; +import java.util.List; + +@Schema(description = "更新标注任务请求") +public record UpdateAnnotationTaskRequest( + @Schema(description = "行业类型", example = "transport") String industryType, + @Schema(description = "任务类型", example = "EXTRACT_QA") String taskType, + @Schema(description = "资源ID列表", example = "[191000000000000101,191000000000000102]") @NotEmpty(message = "资源列表不能为空") List resourceIds, + @Schema(description = "抽取模型配置", example = "{\"mode\":\"SELECT\",\"selectedConfigName\":\"qwen-plus-extract\"}") @Valid TaskModelConfigRequest extractModel, + @Schema(description = "校验模型配置", example = "{\"mode\":\"MANUAL\",\"manualConfig\":{\"modelName\":\"qwen-max\",\"modelUrl\":\"https://dashscope.aliyuncs.com/compatible-mode/v1\",\"apiKey\":\"sk-demo5678\"}}") @Valid TaskModelConfigRequest verifyModel, + @Schema(description = "抽取提示词配置", example = "{\"selectedConfigName\":\"qa-extract-v1\"}") @Valid PromptConfigOptionRequest extractPrompt, + @Schema(description = "校验提示词配置", example = "{\"promptText\":\"请对抽取结果进行逐项校验,并输出差异说明。\"}") @Valid PromptConfigOptionRequest verifyPrompt +) { +} diff --git a/src/main/java/com/labelsys/backend/dto/request/UpdateCompanyStatusRequest.java b/src/main/java/com/labelsys/backend/dto/request/UpdateCompanyStatusRequest.java index 6b42560..fcb7544 100644 --- a/src/main/java/com/labelsys/backend/dto/request/UpdateCompanyStatusRequest.java +++ b/src/main/java/com/labelsys/backend/dto/request/UpdateCompanyStatusRequest.java @@ -6,6 +6,6 @@ import jakarta.validation.constraints.NotNull; @Schema(description = "修改公司状态请求") public record UpdateCompanyStatusRequest( - @Schema(description = "公司状态,枚举值:ENABLED启用、DISABLED禁用") @NotNull(message = "不能为空") CompanyStatus status + @Schema(description = "公司状态,枚举值:ENABLED启用、DISABLED禁用", example = "ENABLED") @NotNull(message = "不能为空") CompanyStatus status ) { } diff --git a/src/main/java/com/labelsys/backend/dto/request/UpdateUserAssignmentRequest.java b/src/main/java/com/labelsys/backend/dto/request/UpdateUserAssignmentRequest.java index 6e050dd..d057c7d 100644 --- a/src/main/java/com/labelsys/backend/dto/request/UpdateUserAssignmentRequest.java +++ b/src/main/java/com/labelsys/backend/dto/request/UpdateUserAssignmentRequest.java @@ -7,7 +7,7 @@ import jakarta.validation.constraints.NotNull; @Schema(description = "修改员工角色岗位请求") public record UpdateUserAssignmentRequest( - @Schema(description = "角色,枚举值:EMPLOYEE员工、MANAGER部门经理、ENGINEER总工程师") @NotNull(message = "不能为空") UserRole role, - @Schema(description = "岗位,枚举值:ANNOTATOR标注员、DATA_TRAINER数据训练师、REVIEWER审核员、ADMIN超级管理员") @NotNull(message = "不能为空") UserPosition position + @Schema(description = "角色,枚举值:EMPLOYEE员工、MANAGER部门经理、ENGINEER总工程师", example = "EMPLOYEE") @NotNull(message = "不能为空") UserRole role, + @Schema(description = "岗位,枚举值:ANNOTATOR标注员、DATA_TRAINER数据训练师、REVIEWER审核员、ADMIN超级管理员", example = "REVIEWER") @NotNull(message = "不能为空") UserPosition position ) { } diff --git a/src/main/java/com/labelsys/backend/dto/request/UpdateUserStatusRequest.java b/src/main/java/com/labelsys/backend/dto/request/UpdateUserStatusRequest.java index 9548dd4..b9d220c 100644 --- a/src/main/java/com/labelsys/backend/dto/request/UpdateUserStatusRequest.java +++ b/src/main/java/com/labelsys/backend/dto/request/UpdateUserStatusRequest.java @@ -6,6 +6,6 @@ import jakarta.validation.constraints.NotNull; @Schema(description = "修改员工状态请求") public record UpdateUserStatusRequest( - @Schema(description = "用户状态,枚举值:ENABLED启用、DISABLED禁用") @NotNull(message = "不能为空") UserStatus status + @Schema(description = "用户状态,枚举值:ENABLED启用、DISABLED禁用", example = "DISABLED") @NotNull(message = "不能为空") UserStatus status ) { } diff --git a/src/main/java/com/labelsys/backend/dto/response/AnnotationResultCompareResponse.java b/src/main/java/com/labelsys/backend/dto/response/AnnotationResultCompareResponse.java new file mode 100644 index 0000000..87ee883 --- /dev/null +++ b/src/main/java/com/labelsys/backend/dto/response/AnnotationResultCompareResponse.java @@ -0,0 +1,16 @@ +package com.labelsys.backend.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "标注结果比对响应") +public record AnnotationResultCompareResponse( + @Schema(description = "结果ID", example = "191000000000000401") Long id, + @Schema(description = "任务ID", example = "191000000000000301") Long taskId, + @Schema(description = "资源ID", example = "191000000000000101") Long resourceId, + @Schema(description = "问答内容 JSON", example = "[{\"question\":\"运输时效是多久?\",\"answer\":\"3天\"}]") String qaContentJson, + @Schema(description = "差异摘要 JSON", example = "{\"changed\":[{\"field\":\"answer\",\"from\":\"3天\",\"to\":\"72小时\"}],\"summary\":\"统一时间表达\"}") String diffSummary, + @Schema(description = "问答存储模式", example = "EXTERNAL") String qaContentStorageMode, + @Schema(description = "外置问答文件路径", example = "review/191000000000000401/qa-content.json") String qaContentFilePath, + @Schema(description = "资源预览路径", example = "preview/191000000000000101/index.html") String sourcePreviewPath +) { +} diff --git a/src/main/java/com/labelsys/backend/dto/response/AnnotationResultResponse.java b/src/main/java/com/labelsys/backend/dto/response/AnnotationResultResponse.java new file mode 100644 index 0000000..34d0135 --- /dev/null +++ b/src/main/java/com/labelsys/backend/dto/response/AnnotationResultResponse.java @@ -0,0 +1,19 @@ +package com.labelsys.backend.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.time.LocalDateTime; + +@Schema(description = "标注结果响应") +public record AnnotationResultResponse( + @Schema(description = "结果ID", example = "191000000000000401") Long id, + @Schema(description = "任务ID", example = "191000000000000301") Long taskId, + @Schema(description = "资源ID", example = "191000000000000101") Long resourceId, + @Schema(description = "运行态状态", example = "MANUAL_REVIEW_PENDING") String runtimeStatus, + @Schema(description = "是否需要人工审核", example = "true") Boolean requiresManualReview, + @Schema(description = "是否已删除", example = "false") Boolean isDeleted, + @Schema(description = "问答存储模式", example = "INLINE") String qaContentStorageMode, + @Schema(description = "审核备注", example = "需统一时间字段口径。") String reviewComment, + @Schema(description = "审核时间", example = "2026-04-27T11:00:00") LocalDateTime reviewedAt, + @Schema(description = "创建时间", example = "2026-04-27T10:40:00") LocalDateTime createdAt +) { +} diff --git a/src/main/java/com/labelsys/backend/dto/response/AnnotationTaskResponse.java b/src/main/java/com/labelsys/backend/dto/response/AnnotationTaskResponse.java new file mode 100644 index 0000000..0769434 --- /dev/null +++ b/src/main/java/com/labelsys/backend/dto/response/AnnotationTaskResponse.java @@ -0,0 +1,22 @@ +package com.labelsys.backend.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.time.LocalDateTime; +import java.util.List; + +@Schema(description = "标注任务响应") +public record AnnotationTaskResponse( + @Schema(description = "任务ID", example = "191000000000000301") Long id, + @Schema(description = "任务名称", example = "运输文档问答抽取任务") String taskName, + @Schema(description = "行业类型:默认值transport,暂不显示", example = "transport") String industryType, + @Schema(description = "任务类型:暂不显示", example = "EXTRACT_QA") String taskType, + @Schema(description = "任务状态", example = "PENDING") String taskStatus, + @Schema(description = "资源ID列表", example = "[191000000000000101,191000000000000102]") List resourceIds, + @Schema(description = "抽取模型配置", example = "{\"configId\":191000000000000511,\"configName\":\"qwen-plus-extract\",\"modelName\":\"qwen-plus\",\"modelUrl\":\"https://dashscope.aliyuncs.com/compatible-mode/v1\",\"maskedApiKey\":\"****1234\"}") TaskModelConfigResponse extractModel, + @Schema(description = "校验模型配置", example = "{\"configId\":191000000000000512,\"configName\":\"qwen-max-verify\",\"modelName\":\"qwen-max\",\"modelUrl\":\"https://dashscope.aliyuncs.com/compatible-mode/v1\",\"maskedApiKey\":\"****5678\"}") TaskModelConfigResponse verifyModel, + @Schema(description = "抽取提示词配置", example = "{\"configId\":191000000000000521,\"configName\":\"qa-extract-v1\",\"promptText\":\"请抽取文档中的问答对,并按 JSON 数组输出。\"}") TaskPromptConfigResponse extractPrompt, + @Schema(description = "校验提示词配置", example = "{\"configId\":191000000000000522,\"configName\":\"qa-verify-v1\",\"promptText\":\"请对抽取结果进行逐项校验,并输出差异说明。\"}") TaskPromptConfigResponse verifyPrompt, + @Schema(description = "创建时间", example = "2026-04-27T10:20:00") LocalDateTime createdAt, + @Schema(description = "更新时间", example = "2026-04-27T10:30:00") LocalDateTime updatedAt +) { +} diff --git a/src/main/java/com/labelsys/backend/dto/response/CompanyOptionResponse.java b/src/main/java/com/labelsys/backend/dto/response/CompanyOptionResponse.java index 4123641..cacc555 100644 --- a/src/main/java/com/labelsys/backend/dto/response/CompanyOptionResponse.java +++ b/src/main/java/com/labelsys/backend/dto/response/CompanyOptionResponse.java @@ -5,9 +5,9 @@ import io.swagger.v3.oas.annotations.media.Schema; @Schema(description = "可登录公司选项") public record CompanyOptionResponse( - @Schema(description = "公司ID") Long companyId, - @Schema(description = "公司编码") String companyCode, - @Schema(description = "公司名称") String companyName + @Schema(description = "公司ID", example = "191000000000000001") Long companyId, + @Schema(description = "公司编码", example = "ALPHA") String companyCode, + @Schema(description = "公司名称", example = "阿尔法标注有限公司") String companyName ) { public static CompanyOptionResponse from(SysCompany company) { return new CompanyOptionResponse(company.getId(), company.getCompanyCode(), company.getCompanyName()); diff --git a/src/main/java/com/labelsys/backend/dto/response/CompanyResponse.java b/src/main/java/com/labelsys/backend/dto/response/CompanyResponse.java index 2ed7439..c7416fe 100644 --- a/src/main/java/com/labelsys/backend/dto/response/CompanyResponse.java +++ b/src/main/java/com/labelsys/backend/dto/response/CompanyResponse.java @@ -6,10 +6,10 @@ import io.swagger.v3.oas.annotations.media.Schema; @Schema(description = "公司响应") public record CompanyResponse( - @Schema(description = "公司ID") Long companyId, - @Schema(description = "公司编码") String companyCode, - @Schema(description = "公司名称") String companyName, - @Schema(description = "公司状态,枚举值:ENABLED启用、DISABLED禁用") CompanyStatus status + @Schema(description = "公司ID", example = "191000000000000001") Long companyId, + @Schema(description = "公司编码", example = "ALPHA") String companyCode, + @Schema(description = "公司名称", example = "阿尔法标注有限公司") String companyName, + @Schema(description = "公司状态,枚举值:ENABLED启用、DISABLED禁用", example = "ENABLED") CompanyStatus status ) { public static CompanyResponse from(SysCompany company) { return new CompanyResponse(company.getId(), company.getCompanyCode(), company.getCompanyName(), company.getStatus()); diff --git a/src/main/java/com/labelsys/backend/dto/response/CurrentUserResponse.java b/src/main/java/com/labelsys/backend/dto/response/CurrentUserResponse.java index 5f5514f..844a9d9 100644 --- a/src/main/java/com/labelsys/backend/dto/response/CurrentUserResponse.java +++ b/src/main/java/com/labelsys/backend/dto/response/CurrentUserResponse.java @@ -7,16 +7,16 @@ import io.swagger.v3.oas.annotations.media.Schema; @Schema(description = "当前登录用户响应") public record CurrentUserResponse( - @Schema(description = "用户ID") Long userId, - @Schema(description = "公司ID") Long companyId, - @Schema(description = "公司编码") String companyCode, - @Schema(description = "公司名称") String companyName, - @Schema(description = "手机号") String phone, + @Schema(description = "用户ID", example = "191000000000000021") Long userId, + @Schema(description = "公司ID", example = "191000000000000001") Long companyId, + @Schema(description = "公司编码", example = "ALPHA") String companyCode, + @Schema(description = "公司名称", example = "阿尔法标注有限公司") String companyName, + @Schema(description = "手机号", example = "13800138002") String phone, @Schema(description = "用户名,可为空", example = "alpha-admin") String username, - @Schema(description = "真实姓名") String realName, - @Schema(description = "角色,枚举值:EMPLOYEE员工、MANAGER部门经理、ENGINEER总工程师") UserRole role, - @Schema(description = "岗位,枚举值:ANNOTATOR标注员、DATA_TRAINER数据训练师、REVIEWER审核员、ADMIN超级管理员") UserPosition position, - @Schema(description = "是否必须修改密码") boolean mustChangePassword + @Schema(description = "真实姓名", example = "张审核") String realName, + @Schema(description = "角色,枚举值:EMPLOYEE员工、MANAGER部门经理、ENGINEER总工程师", example = "EMPLOYEE") UserRole role, + @Schema(description = "岗位,枚举值:ANNOTATOR标注员、DATA_TRAINER数据训练师、REVIEWER审核员、ADMIN超级管理员", example = "REVIEWER") UserPosition position, + @Schema(description = "是否必须修改密码", example = "false") boolean mustChangePassword ) { public static CurrentUserResponse from(LoginUser loginUser) { return new CurrentUserResponse( diff --git a/src/main/java/com/labelsys/backend/dto/response/DataRecordResponse.java b/src/main/java/com/labelsys/backend/dto/response/DataRecordResponse.java deleted file mode 100644 index fbdcdb4..0000000 --- a/src/main/java/com/labelsys/backend/dto/response/DataRecordResponse.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.labelsys.backend.dto.response; - -import com.labelsys.backend.entity.BizDataRecord; -import com.labelsys.backend.enums.UserRole; -import io.swagger.v3.oas.annotations.media.Schema; - -@Schema(description = "数据记录响应") -public record DataRecordResponse( - @Schema(description = "记录ID") Long id, - @Schema(description = "公司ID") Long companyId, - @Schema(description = "创建人ID") Long creatorId, - @Schema(description = "创建人角色,枚举值:EMPLOYEE员工、MANAGER部门经理、ENGINEER总工程师") UserRole creatorRole, - @Schema(description = "记录名称") String recordName -) { - public static DataRecordResponse from(BizDataRecord record) { - return new DataRecordResponse( - record.getId(), - record.getCompanyId(), - record.getCreatorId(), - record.getCreatorRole(), - record.getRecordName() - ); - } -} diff --git a/src/main/java/com/labelsys/backend/dto/response/LoginResponse.java b/src/main/java/com/labelsys/backend/dto/response/LoginResponse.java index 01d4617..ce5aeb1 100644 --- a/src/main/java/com/labelsys/backend/dto/response/LoginResponse.java +++ b/src/main/java/com/labelsys/backend/dto/response/LoginResponse.java @@ -8,14 +8,14 @@ import io.swagger.v3.oas.annotations.media.Schema; @Schema(description = "登录响应") public record LoginResponse( - @Schema(description = "访问令牌") String token, - @Schema(description = "当前公司信息") CompanyOptionResponse company, - @Schema(description = "手机号") String phone, + @Schema(description = "访问令牌", example = "eyJhbGciOiJIUzI1NiJ9.demo.token") String token, + @Schema(description = "当前公司信息", example = "{\"companyId\":191000000000000001,\"companyCode\":\"ALPHA\",\"companyName\":\"阿尔法标注有限公司\"}") CompanyOptionResponse company, + @Schema(description = "手机号", example = "13800138002") String phone, @Schema(description = "用户名,可为空", example = "alpha-admin") String username, - @Schema(description = "真实姓名") String realName, - @Schema(description = "角色,枚举值:EMPLOYEE员工、MANAGER部门经理、ENGINEER总工程师") UserRole role, - @Schema(description = "岗位,枚举值:ANNOTATOR标注员、DATA_TRAINER数据训练师、REVIEWER审核员、ADMIN超级管理员") UserPosition position, - @Schema(description = "是否必须修改密码") boolean mustChangePassword + @Schema(description = "真实姓名", example = "张审核") String realName, + @Schema(description = "角色,枚举值:EMPLOYEE员工、MANAGER部门经理、ENGINEER总工程师", example = "EMPLOYEE") UserRole role, + @Schema(description = "岗位,枚举值:ANNOTATOR标注员、DATA_TRAINER数据训练师、REVIEWER审核员、ADMIN超级管理员", example = "REVIEWER") UserPosition position, + @Schema(description = "是否必须修改密码", example = "false") boolean mustChangePassword ) { public static LoginResponse from(String token, LoginUser loginUser, SysCompany company) { return new LoginResponse( diff --git a/src/main/java/com/labelsys/backend/dto/response/MenuResponse.java b/src/main/java/com/labelsys/backend/dto/response/MenuResponse.java index 66c67a9..eb28005 100644 --- a/src/main/java/com/labelsys/backend/dto/response/MenuResponse.java +++ b/src/main/java/com/labelsys/backend/dto/response/MenuResponse.java @@ -5,10 +5,10 @@ import io.swagger.v3.oas.annotations.media.Schema; @Schema(description = "菜单响应") public record MenuResponse( - @Schema(description = "菜单编码") String menuCode, - @Schema(description = "菜单名称") String menuName, - @Schema(description = "菜单路径") String path, - @Schema(description = "排序") Integer sortOrder + @Schema(description = "菜单编码", example = "annotation.task.list") String menuCode, + @Schema(description = "菜单名称", example = "标注任务") String menuName, + @Schema(description = "菜单路径", example = "/annotation/tasks") String path, + @Schema(description = "排序", example = "10") Integer sortOrder ) { public static MenuResponse from(SysMenu menu) { return new MenuResponse(menu.getMenuCode(), menu.getMenuName(), menu.getPath(), menu.getSortOrder()); diff --git a/src/main/java/com/labelsys/backend/dto/response/MergeReviewResultResponse.java b/src/main/java/com/labelsys/backend/dto/response/MergeReviewResultResponse.java new file mode 100644 index 0000000..7dda5fd --- /dev/null +++ b/src/main/java/com/labelsys/backend/dto/response/MergeReviewResultResponse.java @@ -0,0 +1,13 @@ +package com.labelsys.backend.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.time.LocalDateTime; + +@Schema(description = "合并审核结果响应") +public record MergeReviewResultResponse( + @Schema(description = "运行态结果ID", example = "191000000000000401") Long resultId, + @Schema(description = "历史结果ID", example = "191000000000000451") Long historyId, + @Schema(description = "归档原因", example = "MANUAL_REVIEW_MERGED") String archiveReason, + @Schema(description = "归档时间", example = "2026-04-27T11:05:00") LocalDateTime archivedAt +) { +} diff --git a/src/main/java/com/labelsys/backend/dto/response/SourceResourceResponse.java b/src/main/java/com/labelsys/backend/dto/response/SourceResourceResponse.java new file mode 100644 index 0000000..7488aa8 --- /dev/null +++ b/src/main/java/com/labelsys/backend/dto/response/SourceResourceResponse.java @@ -0,0 +1,21 @@ +package com.labelsys.backend.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.time.LocalDateTime; + +@Schema(description = "资源响应") +public record SourceResourceResponse( + @Schema(description = "资源ID", example = "191000000000000101") Long id, + @Schema(description = "资源名称", example = "2026年4月运输巡检记录") String resourceName, + @Schema(description = "资源类型", example = "TEXT") String resourceType, + @Schema(description = "桶名称", example = "annotation-source") String bucketName, + @Schema(description = "文件路径", example = "company/191000000000000001/text/191000000000000101.txt") String filePath, + @Schema(description = "文件大小", example = "20480") Long fileSize, + @Schema(description = "资源状态", example = "READY") String sourceStatus, + @Schema(description = "存储提供方", example = "rustfs") String storageProvider, + @Schema(description = "备注", example = "第一批导入样本") String remark, + @Schema(description = "创建人名称", example = "张审核") String creatorName, + @Schema(description = "创建时间", example = "2026-04-27T10:00:00") LocalDateTime createdAt, + @Schema(description = "更新时间", example = "2026-04-27T10:05:00") LocalDateTime updatedAt +) { +} diff --git a/src/main/java/com/labelsys/backend/dto/response/SourceUploadResponse.java b/src/main/java/com/labelsys/backend/dto/response/SourceUploadResponse.java new file mode 100644 index 0000000..6145246 --- /dev/null +++ b/src/main/java/com/labelsys/backend/dto/response/SourceUploadResponse.java @@ -0,0 +1,17 @@ +package com.labelsys.backend.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.time.LocalDateTime; + +@Schema(description = "资源上传响应") +public record SourceUploadResponse( + @Schema(description = "资源ID", example = "191000000000000101") Long id, + @Schema(description = "资源名称", example = "2026年4月运输巡检记录") String resourceName, + @Schema(description = "资源类型", example = "TEXT") String resourceType, + @Schema(description = "桶名称", example = "annotation-source") String bucketName, + @Schema(description = "文件路径", example = "company/191000000000000001/text/191000000000000101.txt") String filePath, + @Schema(description = "文件大小", example = "20480") Long fileSize, + @Schema(description = "资源状态", example = "READY") String sourceStatus, + @Schema(description = "创建时间", example = "2026-04-27T10:00:00") LocalDateTime createdAt +) { +} diff --git a/src/main/java/com/labelsys/backend/dto/response/SysConfigResponse.java b/src/main/java/com/labelsys/backend/dto/response/SysConfigResponse.java new file mode 100644 index 0000000..f0ff9c8 --- /dev/null +++ b/src/main/java/com/labelsys/backend/dto/response/SysConfigResponse.java @@ -0,0 +1,17 @@ +package com.labelsys.backend.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.time.LocalDateTime; + +@Schema(description = "系统配置响应") +public record SysConfigResponse( + @Schema(description = "配置ID", example = "191000000000000501") Long id, + @Schema(description = "配置类型", example = "MODEL") String configType, + @Schema(description = "配置名称", example = "qwen-plus-extract") String configName, + @Schema(description = "配置值", example = "{\"modelName\":\"qwen-plus\",\"modelUrl\":\"https://dashscope.aliyuncs.com/compatible-mode/v1\",\"apiKey\":\"sk-demo1234\"}") String configValue, + @Schema(description = "配置状态", example = "ENABLED") String status, + @Schema(description = "创建人ID", example = "191000000000000021") Long creatorId, + @Schema(description = "创建时间", example = "2026-04-27T09:50:00") LocalDateTime createdAt, + @Schema(description = "更新时间", example = "2026-04-27T10:15:00") LocalDateTime updatedAt +) { +} diff --git a/src/main/java/com/labelsys/backend/dto/response/TaskModelConfigResponse.java b/src/main/java/com/labelsys/backend/dto/response/TaskModelConfigResponse.java new file mode 100644 index 0000000..fdb608b --- /dev/null +++ b/src/main/java/com/labelsys/backend/dto/response/TaskModelConfigResponse.java @@ -0,0 +1,13 @@ +package com.labelsys.backend.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "任务模型配置响应") +public record TaskModelConfigResponse( + @Schema(description = "配置ID", example = "191000000000000511") Long configId, + @Schema(description = "配置名称", example = "qwen-plus-extract") String configName, + @Schema(description = "模型名称", example = "qwen-plus") String modelName, + @Schema(description = "模型地址", example = "https://dashscope.aliyuncs.com/compatible-mode/v1") String modelUrl, + @Schema(description = "脱敏后的模型密钥", example = "****1234") String maskedApiKey +) { +} diff --git a/src/main/java/com/labelsys/backend/dto/response/TaskPromptConfigResponse.java b/src/main/java/com/labelsys/backend/dto/response/TaskPromptConfigResponse.java new file mode 100644 index 0000000..ad34b8a --- /dev/null +++ b/src/main/java/com/labelsys/backend/dto/response/TaskPromptConfigResponse.java @@ -0,0 +1,11 @@ +package com.labelsys.backend.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "任务提示词配置响应") +public record TaskPromptConfigResponse( + @Schema(description = "配置ID", example = "191000000000000521") Long configId, + @Schema(description = "配置名称", example = "qa-extract-v1") String configName, + @Schema(description = "提示词内容", example = "请抽取文档中的问答对,并按 JSON 数组输出。") String promptText +) { +} diff --git a/src/main/java/com/labelsys/backend/dto/response/UserResponse.java b/src/main/java/com/labelsys/backend/dto/response/UserResponse.java index 6cc9148..7201efa 100644 --- a/src/main/java/com/labelsys/backend/dto/response/UserResponse.java +++ b/src/main/java/com/labelsys/backend/dto/response/UserResponse.java @@ -8,15 +8,15 @@ import io.swagger.v3.oas.annotations.media.Schema; @Schema(description = "用户响应") public record UserResponse( - @Schema(description = "用户ID") Long userId, - @Schema(description = "公司ID") Long companyId, - @Schema(description = "手机号") String phone, + @Schema(description = "用户ID", example = "191000000000000021") Long userId, + @Schema(description = "公司ID", example = "191000000000000001") Long companyId, + @Schema(description = "手机号", example = "13800138002") String phone, @Schema(description = "用户名,可为空", example = "alpha-admin") String username, - @Schema(description = "真实姓名") String realName, - @Schema(description = "角色,枚举值:EMPLOYEE员工、MANAGER部门经理、ENGINEER总工程师") UserRole role, - @Schema(description = "岗位,枚举值:ANNOTATOR标注员、DATA_TRAINER数据训练师、REVIEWER审核员、ADMIN超级管理员") UserPosition position, - @Schema(description = "用户状态,枚举值:ENABLED启用、DISABLED禁用") UserStatus status, - @Schema(description = "是否必须修改密码") boolean mustChangePassword + @Schema(description = "真实姓名", example = "张审核") String realName, + @Schema(description = "角色,枚举值:EMPLOYEE员工、MANAGER部门经理、ENGINEER总工程师", example = "EMPLOYEE") UserRole role, + @Schema(description = "岗位,枚举值:ANNOTATOR标注员、DATA_TRAINER数据训练师、REVIEWER审核员、ADMIN超级管理员", example = "REVIEWER") UserPosition position, + @Schema(description = "用户状态,枚举值:ENABLED启用、DISABLED禁用", example = "ENABLED") UserStatus status, + @Schema(description = "是否必须修改密码", example = "false") boolean mustChangePassword ) { public static UserResponse from(SysUser user) { return new UserResponse( diff --git a/src/main/java/com/labelsys/backend/entity/AnnotationResult.java b/src/main/java/com/labelsys/backend/entity/AnnotationResult.java new file mode 100644 index 0000000..9a5921c --- /dev/null +++ b/src/main/java/com/labelsys/backend/entity/AnnotationResult.java @@ -0,0 +1,37 @@ +package com.labelsys.backend.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.labelsys.backend.enums.UserRole; +import java.time.LocalDateTime; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@TableName("annotation_result") +public class AnnotationResult { + @TableId(type = IdType.INPUT) + private Long id; + private Long companyId; + private Long creatorId; + private UserRole creatorRole; + private Long taskId; + private Long resourceId; + private String qaContentJson; + private String qaContentStorageMode; + private String qaContentFilePath; + private String diffSummary; + private Boolean requiresManualReview; + private Boolean isDeleted; + private Long reviewerId; + private String reviewComment; + private LocalDateTime reviewedAt; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; +} diff --git a/src/main/java/com/labelsys/backend/entity/AnnotationResultHistory.java b/src/main/java/com/labelsys/backend/entity/AnnotationResultHistory.java new file mode 100644 index 0000000..d180683 --- /dev/null +++ b/src/main/java/com/labelsys/backend/entity/AnnotationResultHistory.java @@ -0,0 +1,34 @@ +package com.labelsys.backend.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.labelsys.backend.enums.UserRole; +import java.time.LocalDateTime; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@TableName("annotation_result_history") +public class AnnotationResultHistory { + @TableId(type = IdType.INPUT) + private Long id; + private Long companyId; + private Long creatorId; + private UserRole creatorRole; + private Long sourceResultId; + private Long taskId; + private Long resourceId; + private String qaContentJson; + private String qaContentStorageMode; + private String qaContentFilePath; + private String archiveReason; + private Long archivedBy; + private LocalDateTime archivedAt; + private LocalDateTime createdAt; +} diff --git a/src/main/java/com/labelsys/backend/entity/AnnotationTask.java b/src/main/java/com/labelsys/backend/entity/AnnotationTask.java new file mode 100644 index 0000000..31b3898 --- /dev/null +++ b/src/main/java/com/labelsys/backend/entity/AnnotationTask.java @@ -0,0 +1,46 @@ +package com.labelsys.backend.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.labelsys.backend.enums.UserRole; +import java.time.LocalDateTime; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@TableName("annotation_task") +public class AnnotationTask { + @TableId(type = IdType.INPUT) + private Long id; + private Long companyId; + private Long creatorId; + private UserRole creatorRole; + private String taskName; + private String industryType; + private String taskType; + private Long extractModelConfigId; + private String extractModelName; + private String extractModelUrl; + private String extractModelApiKey; + private Long verifyModelConfigId; + private String verifyModelName; + private String verifyModelUrl; + private String verifyModelApiKey; + private Long extractPromptConfigId; + private String extractPrompt; + private Long verifyPromptConfigId; + private String verifyPrompt; + private String taskStatus; + private Boolean isDeleted; + private LocalDateTime startedAt; + private LocalDateTime finishedAt; + private String errorMessage; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; +} diff --git a/src/main/java/com/labelsys/backend/entity/AnnotationTaskResource.java b/src/main/java/com/labelsys/backend/entity/AnnotationTaskResource.java new file mode 100644 index 0000000..c5c18ce --- /dev/null +++ b/src/main/java/com/labelsys/backend/entity/AnnotationTaskResource.java @@ -0,0 +1,24 @@ +package com.labelsys.backend.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import java.time.LocalDateTime; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@TableName("annotation_task_resource") +public class AnnotationTaskResource { + @TableId(type = IdType.INPUT) + private Long id; + private Long companyId; + private Long taskId; + private Long resourceId; + private LocalDateTime createdAt; +} diff --git a/src/main/java/com/labelsys/backend/entity/BizDataRecord.java b/src/main/java/com/labelsys/backend/entity/BizDataRecord.java index 7d2a7ee..9acc68c 100644 --- a/src/main/java/com/labelsys/backend/entity/BizDataRecord.java +++ b/src/main/java/com/labelsys/backend/entity/BizDataRecord.java @@ -1,8 +1,5 @@ package com.labelsys.backend.entity; -import com.baomidou.mybatisplus.annotation.IdType; -import com.baomidou.mybatisplus.annotation.TableId; -import com.baomidou.mybatisplus.annotation.TableName; import com.labelsys.backend.enums.UserRole; import java.time.LocalDateTime; import lombok.AllArgsConstructor; @@ -14,9 +11,7 @@ import lombok.NoArgsConstructor; @Builder @NoArgsConstructor @AllArgsConstructor -@TableName("biz_data_record") public class BizDataRecord { - @TableId(type = IdType.INPUT) private Long id; private Long companyId; private Long creatorId; diff --git a/src/main/java/com/labelsys/backend/entity/SourceResource.java b/src/main/java/com/labelsys/backend/entity/SourceResource.java new file mode 100644 index 0000000..ef469ab --- /dev/null +++ b/src/main/java/com/labelsys/backend/entity/SourceResource.java @@ -0,0 +1,34 @@ +package com.labelsys.backend.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.labelsys.backend.enums.UserRole; +import java.time.LocalDateTime; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@TableName("source_resource") +public class SourceResource { + @TableId(type = IdType.INPUT) + private Long id; + private Long companyId; + private Long creatorId; + private UserRole creatorRole; + private String resourceName; + private String resourceType; + private String bucketName; + private String filePath; + private Long fileSize; + private String sourceStatus; + private String storageProvider; + private String remark; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; +} diff --git a/src/main/java/com/labelsys/backend/entity/SysConfig.java b/src/main/java/com/labelsys/backend/entity/SysConfig.java new file mode 100644 index 0000000..26b5559 --- /dev/null +++ b/src/main/java/com/labelsys/backend/entity/SysConfig.java @@ -0,0 +1,28 @@ +package com.labelsys.backend.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import java.time.LocalDateTime; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@TableName("sys_config") +public class SysConfig { + @TableId(type = IdType.INPUT) + private Long id; + private Long companyId; + private String configType; + private String configName; + private String configValue; + private String status; + private Long creatorId; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; +} diff --git a/src/main/java/com/labelsys/backend/entity/SysMenu.java b/src/main/java/com/labelsys/backend/entity/SysMenu.java index e1a124b..cd6395c 100644 --- a/src/main/java/com/labelsys/backend/entity/SysMenu.java +++ b/src/main/java/com/labelsys/backend/entity/SysMenu.java @@ -18,10 +18,10 @@ public class SysMenu { @TableId(type = IdType.INPUT) private Long id; private Long companyId; - private String permissionCode; private String menuCode; private String menuName; private String path; + private String visiblePositions; private Integer sortOrder; private LocalDateTime createdAt; private LocalDateTime updatedAt; diff --git a/src/main/java/com/labelsys/backend/enums/ConfigType.java b/src/main/java/com/labelsys/backend/enums/ConfigType.java new file mode 100644 index 0000000..6a52f31 --- /dev/null +++ b/src/main/java/com/labelsys/backend/enums/ConfigType.java @@ -0,0 +1,13 @@ +package com.labelsys.backend.enums; + +import java.util.Arrays; + +public enum ConfigType { + MODEL, + PROMPT, + SYSTEM; + + public static boolean isValid(String value) { + return Arrays.stream(values()).anyMatch(type -> type.name().equals(value)); + } +} diff --git a/src/main/java/com/labelsys/backend/enums/QaContentStorageMode.java b/src/main/java/com/labelsys/backend/enums/QaContentStorageMode.java new file mode 100644 index 0000000..20e6c11 --- /dev/null +++ b/src/main/java/com/labelsys/backend/enums/QaContentStorageMode.java @@ -0,0 +1,12 @@ +package com.labelsys.backend.enums; + +import java.util.Arrays; + +public enum QaContentStorageMode { + INLINE, + EXTERNAL; + + public static boolean isValid(String value) { + return Arrays.stream(values()).anyMatch(mode -> mode.name().equals(value)); + } +} diff --git a/src/main/java/com/labelsys/backend/enums/ResourceType.java b/src/main/java/com/labelsys/backend/enums/ResourceType.java new file mode 100644 index 0000000..f3e4883 --- /dev/null +++ b/src/main/java/com/labelsys/backend/enums/ResourceType.java @@ -0,0 +1,13 @@ +package com.labelsys.backend.enums; + +import java.util.Arrays; + +public enum ResourceType { + TEXT, + IMAGE, + VIDEO; + + public static boolean isValid(String value) { + return Arrays.stream(values()).anyMatch(type -> type.name().equals(value)); + } +} diff --git a/src/main/java/com/labelsys/backend/enums/RuntimeResultStatus.java b/src/main/java/com/labelsys/backend/enums/RuntimeResultStatus.java new file mode 100644 index 0000000..8f3878d --- /dev/null +++ b/src/main/java/com/labelsys/backend/enums/RuntimeResultStatus.java @@ -0,0 +1,7 @@ +package com.labelsys.backend.enums; + +public enum RuntimeResultStatus { + MANUAL_REVIEW_PENDING, + AUTO_ARCHIVE_PENDING, + ARCHIVED +} diff --git a/src/main/java/com/labelsys/backend/enums/SourceStatus.java b/src/main/java/com/labelsys/backend/enums/SourceStatus.java new file mode 100644 index 0000000..9dfede0 --- /dev/null +++ b/src/main/java/com/labelsys/backend/enums/SourceStatus.java @@ -0,0 +1,14 @@ +package com.labelsys.backend.enums; + +import java.util.Arrays; + +public enum SourceStatus { + UPLOADED, + PROCESSING, + READY, + ARCHIVED; + + public static boolean isValid(String value) { + return Arrays.stream(values()).anyMatch(status -> status.name().equals(value)); + } +} diff --git a/src/main/java/com/labelsys/backend/enums/TaskStatus.java b/src/main/java/com/labelsys/backend/enums/TaskStatus.java new file mode 100644 index 0000000..1ab8ab9 --- /dev/null +++ b/src/main/java/com/labelsys/backend/enums/TaskStatus.java @@ -0,0 +1,14 @@ +package com.labelsys.backend.enums; + +import java.util.Arrays; + +public enum TaskStatus { + PENDING, + RUNNING, + COMPLETED, + FAILED; + + public static boolean isValid(String value) { + return Arrays.stream(values()).anyMatch(status -> status.name().equals(value)); + } +} diff --git a/src/main/java/com/labelsys/backend/enums/UserPosition.java b/src/main/java/com/labelsys/backend/enums/UserPosition.java index 3681151..55f3795 100644 --- a/src/main/java/com/labelsys/backend/enums/UserPosition.java +++ b/src/main/java/com/labelsys/backend/enums/UserPosition.java @@ -6,13 +6,13 @@ import lombok.RequiredArgsConstructor; @Getter @RequiredArgsConstructor -@Schema(description = "岗位枚举,枚举值:ANNOTATOR标注员、DATA_TRAINER数据训练师、REVIEWER审核员、ADMIN超级管理员") +@Schema(description = "岗位枚举,枚举值:ANNOTATOR标注员、DATA_TRAINER数据训练师、REVIEWER审核员、ADMIN公司管理员、SUPER_ADMIN超级管理员") public enum UserPosition { - ANNOTATOR(1), DATA_TRAINER(2), REVIEWER(3), ADMIN(4); + ANNOTATOR(1), DATA_TRAINER(2), REVIEWER(3), ADMIN(4), SUPER_ADMIN(5); private final int level; public boolean canAccess(UserPosition required) { return this.level >= required.level; } -} +} \ No newline at end of file diff --git a/src/main/java/com/labelsys/backend/interceptor/AuthInterceptor.java b/src/main/java/com/labelsys/backend/interceptor/AuthInterceptor.java index 37acd88..8dadd0d 100644 --- a/src/main/java/com/labelsys/backend/interceptor/AuthInterceptor.java +++ b/src/main/java/com/labelsys/backend/interceptor/AuthInterceptor.java @@ -1,5 +1,13 @@ package com.labelsys.backend.interceptor; +import java.time.Duration; +import java.util.Set; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.HandlerInterceptor; + import com.labelsys.backend.annotation.RequirePosition; import com.labelsys.backend.common.exception.ForbiddenException; import com.labelsys.backend.common.exception.UnauthorizedException; @@ -8,48 +16,36 @@ import com.labelsys.backend.context.UserContext; import com.labelsys.backend.entity.SysCompany; import com.labelsys.backend.entity.SysUser; import com.labelsys.backend.enums.CompanyStatus; -import com.labelsys.backend.enums.UserPosition; import com.labelsys.backend.enums.UserStatus; import com.labelsys.backend.mapper.SysCompanyMapper; import com.labelsys.backend.mapper.SysUserMapper; import com.labelsys.backend.service.session.TokenSessionRepository; + import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import java.time.Duration; -import java.util.Set; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Component; -import org.springframework.web.method.HandlerMethod; -import org.springframework.web.servlet.HandlerInterceptor; @Component public class AuthInterceptor implements HandlerInterceptor { private static final Set OPEN_PATHS = Set.of( - "/label/api/auth/companies", - "/label/api/auth/login", - "/label/swagger-ui.html", - "/label/v3/api-docs", - "/label/v3/api-docs/swagger-config" - ); + "/api/auth/companies", "/label/api/auth/companies", + "/api/auth/login", "/label/api/auth/login", + "/swagger-ui.html", "/label/swagger-ui.html", + "/v3/api-docs", "/label/v3/api-docs", + "/v3/api-docs/swagger-config", "/label/v3/api-docs/swagger-config"); private static final Set ALLOWED_WHEN_MUST_CHANGE_PASSWORD = Set.of( - "/label/api/auth/change-password", - "/label/api/auth/logout", - "/label/api/auth/me" - ); + "/api/auth/change-password", "/label/api/auth/change-password", + "/api/auth/logout", "/label/api/auth/logout", + "/api/auth/me", "/label/api/auth/me"); private final TokenSessionRepository tokenSessionRepository; private final SysUserMapper sysUserMapper; private final SysCompanyMapper sysCompanyMapper; private final Duration sessionTtl; - public AuthInterceptor( - TokenSessionRepository tokenSessionRepository, - SysUserMapper sysUserMapper, - SysCompanyMapper sysCompanyMapper, - @Value("${labelsys.session.ttl:PT2H}") Duration sessionTtl - ) { + public AuthInterceptor(TokenSessionRepository tokenSessionRepository, SysUserMapper sysUserMapper, + SysCompanyMapper sysCompanyMapper, @Value("${labelsys.session.ttl:PT2H}") Duration sessionTtl) { this.tokenSessionRepository = tokenSessionRepository; this.sysUserMapper = sysUserMapper; this.sysCompanyMapper = sysCompanyMapper; @@ -69,12 +65,13 @@ public class AuthInterceptor implements HandlerInterceptor { return true; } String token = extractToken(request.getHeader("Authorization")); - LoginUser loginUser = tokenSessionRepository.find(token) - .orElseThrow(() -> new UnauthorizedException("未登录或登录已过期")); + LoginUser loginUser = + tokenSessionRepository.find(token).orElseThrow(() -> new UnauthorizedException("未登录或登录已过期")); SysUser user = sysUserMapper.findByIdAndCompanyId(loginUser.userId(), loginUser.companyId()); SysCompany company = sysCompanyMapper.selectById(loginUser.companyId()); - if (user == null || company == null || user.getStatus() != UserStatus.ENABLED || company.getStatus() != CompanyStatus.ENABLED) { + if (user == null || company == null || user.getStatus() != UserStatus.ENABLED + || company.getStatus() != CompanyStatus.ENABLED) { throw new UnauthorizedException("未登录或登录已过期"); } if (!user.getSessionVersion().equals(loginUser.sessionVersion())) { @@ -96,7 +93,8 @@ public class AuthInterceptor implements HandlerInterceptor { } @Override - public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { + public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, + Exception ex) { UserContext.clear(); } diff --git a/src/main/java/com/labelsys/backend/mapper/AnnotationResultHistoryMapper.java b/src/main/java/com/labelsys/backend/mapper/AnnotationResultHistoryMapper.java new file mode 100644 index 0000000..203a3d1 --- /dev/null +++ b/src/main/java/com/labelsys/backend/mapper/AnnotationResultHistoryMapper.java @@ -0,0 +1,7 @@ +package com.labelsys.backend.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.labelsys.backend.entity.AnnotationResultHistory; + +public interface AnnotationResultHistoryMapper extends BaseMapper { +} diff --git a/src/main/java/com/labelsys/backend/mapper/AnnotationResultMapper.java b/src/main/java/com/labelsys/backend/mapper/AnnotationResultMapper.java new file mode 100644 index 0000000..bdda150 --- /dev/null +++ b/src/main/java/com/labelsys/backend/mapper/AnnotationResultMapper.java @@ -0,0 +1,17 @@ +package com.labelsys.backend.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.labelsys.backend.entity.AnnotationResult; +import java.time.LocalDateTime; +import org.apache.ibatis.annotations.Param; + +public interface AnnotationResultMapper extends BaseMapper { + + AnnotationResult findActiveByIdAndCompanyId(@Param("id") Long id, @Param("companyId") Long companyId); + + int markArchived(@Param("id") Long id, + @Param("companyId") Long companyId, + @Param("reviewerId") Long reviewerId, + @Param("reviewComment") String reviewComment, + @Param("reviewedAt") LocalDateTime reviewedAt); +} \ No newline at end of file diff --git a/src/main/java/com/labelsys/backend/mapper/AnnotationTaskMapper.java b/src/main/java/com/labelsys/backend/mapper/AnnotationTaskMapper.java new file mode 100644 index 0000000..35bd601 --- /dev/null +++ b/src/main/java/com/labelsys/backend/mapper/AnnotationTaskMapper.java @@ -0,0 +1,11 @@ +package com.labelsys.backend.mapper; + +import org.apache.ibatis.annotations.Param; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.labelsys.backend.entity.AnnotationTask; + +public interface AnnotationTaskMapper extends BaseMapper { + + AnnotationTask findByIdAndCompanyId(@Param("id") Long id, @Param("companyId") Long companyId); +} \ No newline at end of file diff --git a/src/main/java/com/labelsys/backend/mapper/AnnotationTaskResourceMapper.java b/src/main/java/com/labelsys/backend/mapper/AnnotationTaskResourceMapper.java new file mode 100644 index 0000000..07c05e8 --- /dev/null +++ b/src/main/java/com/labelsys/backend/mapper/AnnotationTaskResourceMapper.java @@ -0,0 +1,15 @@ +package com.labelsys.backend.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.labelsys.backend.entity.AnnotationTaskResource; +import java.util.List; +import org.apache.ibatis.annotations.Param; + +public interface AnnotationTaskResourceMapper extends BaseMapper { + + List listResourceIdsByTaskId(@Param("taskId") Long taskId); + + int deleteByTaskId(@Param("taskId") Long taskId); + + int countByResourceId(@Param("resourceId") Long resourceId); +} diff --git a/src/main/java/com/labelsys/backend/mapper/BizDataRecordMapper.java b/src/main/java/com/labelsys/backend/mapper/BizDataRecordMapper.java deleted file mode 100644 index 7f68e16..0000000 --- a/src/main/java/com/labelsys/backend/mapper/BizDataRecordMapper.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.labelsys.backend.mapper; - -import com.baomidou.mybatisplus.core.mapper.BaseMapper; -import com.labelsys.backend.entity.BizDataRecord; -import java.util.List; -import org.apache.ibatis.annotations.Param; - -public interface BizDataRecordMapper extends BaseMapper { - - List listVisibleByEmployee(@Param("companyId") Long companyId, @Param("creatorId") Long creatorId); - - List listVisibleByManager(@Param("companyId") Long companyId); - - List listVisibleByEngineer(@Param("companyId") Long companyId); -} diff --git a/src/main/java/com/labelsys/backend/mapper/SourceResourceMapper.java b/src/main/java/com/labelsys/backend/mapper/SourceResourceMapper.java new file mode 100644 index 0000000..b6eefd7 --- /dev/null +++ b/src/main/java/com/labelsys/backend/mapper/SourceResourceMapper.java @@ -0,0 +1,13 @@ +package com.labelsys.backend.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.labelsys.backend.entity.SourceResource; +import java.util.List; +import org.apache.ibatis.annotations.Param; + +public interface SourceResourceMapper extends BaseMapper { + + List selectByCompanyIdAndIds(@Param("companyId") Long companyId, @Param("resourceIds") List resourceIds); + + SourceResource selectByCompanyIdAndResourceName(@Param("companyId") Long companyId, @Param("resourceName") String resourceName); +} \ No newline at end of file diff --git a/src/main/java/com/labelsys/backend/mapper/SysConfigMapper.java b/src/main/java/com/labelsys/backend/mapper/SysConfigMapper.java new file mode 100644 index 0000000..2a81c2b --- /dev/null +++ b/src/main/java/com/labelsys/backend/mapper/SysConfigMapper.java @@ -0,0 +1,14 @@ +package com.labelsys.backend.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.labelsys.backend.entity.SysConfig; +import org.apache.ibatis.annotations.Param; + +public interface SysConfigMapper extends BaseMapper { + + SysConfig findByCompanyIdAndConfigName(@Param("companyId") Long companyId, @Param("configName") String configName); + + SysConfig findByCompanyIdAndConfigNameAndType(@Param("companyId") Long companyId, + @Param("configName") String configName, + @Param("configType") String configType); +} diff --git a/src/main/java/com/labelsys/backend/mapper/SysMenuMapper.java b/src/main/java/com/labelsys/backend/mapper/SysMenuMapper.java index 5a42212..823e073 100644 --- a/src/main/java/com/labelsys/backend/mapper/SysMenuMapper.java +++ b/src/main/java/com/labelsys/backend/mapper/SysMenuMapper.java @@ -7,5 +7,5 @@ import org.apache.ibatis.annotations.Param; public interface SysMenuMapper extends BaseMapper { - List listCurrentMenus(@Param("companyId") Long companyId, @Param("positionCodes") List positionCodes); + List listCurrentMenus(@Param("companyId") Long companyId, @Param("visiblePositions") List visiblePositions); } diff --git a/src/main/java/com/labelsys/backend/scheduled/AutoArchiveAnnotationResultJob.java b/src/main/java/com/labelsys/backend/scheduled/AutoArchiveAnnotationResultJob.java new file mode 100644 index 0000000..f18bbd1 --- /dev/null +++ b/src/main/java/com/labelsys/backend/scheduled/AutoArchiveAnnotationResultJob.java @@ -0,0 +1,23 @@ +package com.labelsys.backend.scheduled; + +import com.labelsys.backend.service.AnnotationResultArchiveService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +@RequiredArgsConstructor +public class AutoArchiveAnnotationResultJob { + + private final AnnotationResultArchiveService annotationResultArchiveService; + + @Scheduled(fixedDelayString = "${labelsys.annotation.auto-archive-fixed-delay:300000}") + public void autoArchiveEligibleResults() { + int archivedCount = annotationResultArchiveService.autoArchiveEligibleResults(); + if (archivedCount > 0) { + log.info("auto archived annotation results, count={}", archivedCount); + } + } +} diff --git a/src/main/java/com/labelsys/backend/service/AnnotationResultArchiveService.java b/src/main/java/com/labelsys/backend/service/AnnotationResultArchiveService.java new file mode 100644 index 0000000..d1a568d --- /dev/null +++ b/src/main/java/com/labelsys/backend/service/AnnotationResultArchiveService.java @@ -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 results = annotationResultMapper.selectList(new LambdaQueryWrapper() + .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); + } +} diff --git a/src/main/java/com/labelsys/backend/service/AnnotationResultService.java b/src/main/java/com/labelsys/backend/service/AnnotationResultService.java new file mode 100644 index 0000000..a5f66e8 --- /dev/null +++ b/src/main/java/com/labelsys/backend/service/AnnotationResultService.java @@ -0,0 +1,113 @@ +package com.labelsys.backend.service; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +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 com.labelsys.backend.service.DataPermissionService; +import java.util.List; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +@RequiredArgsConstructor +public class AnnotationResultService { + + private final AnnotationResultMapper annotationResultMapper; + private final SourceResourceMapper sourceResourceMapper; + private final DataPermissionService dataPermissionService; + + public PageResult pageResults(LoginUser currentUser, AnnotationResultPageQuery query) { + List allowedRoles = dataPermissionService.getAllowedRoles(currentUser); + boolean shouldFilterByUserId = dataPermissionService.shouldFilterByUserId(currentUser); + + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .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()); + + if (shouldFilterByUserId) { + wrapper.eq(AnnotationResult::getCreatorId, currentUser.userId()); + } else if (!allowedRoles.isEmpty()) { + wrapper.in(AnnotationResult::getCreatorRole, allowedRoles); + } + + wrapper.orderByDesc(AnnotationResult::getCreatedAt); + + Page page = new Page<>(query.pageNo(), query.pageSize()); + Page resultPage = annotationResultMapper.selectPage(page, wrapper); + + List records = resultPage.getRecords().stream() + .map(this::toResponse) + .filter(response -> query.runtimeStatus() == null || query.runtimeStatus().equals(response.runtimeStatus())) + .toList(); + + return new PageResult<>(records, resultPage.getTotal(), (int) resultPage.getCurrent(), (int) resultPage.getSize()); + } + + public AnnotationResultResponse getResult(LoginUser currentUser, Long resultId) { + AnnotationResult result = annotationResultMapper.findActiveByIdAndCompanyId(resultId, currentUser.companyId()); + if (result == null) { + log.warn("Result not found or cross-tenant access attempt: resultId={}, companyId={}, userId={}", + resultId, currentUser.companyId(), currentUser.userId()); + throw new BusinessException(ResultCode.NOT_FOUND, "结果不存在"); + } + return toResponse(result); + } + + public AnnotationResultCompareResponse compareResult(LoginUser currentUser, Long resultId) { + AnnotationResult result = annotationResultMapper.findActiveByIdAndCompanyId(resultId, currentUser.companyId()); + if (result == null) { + log.warn("Result not found or cross-tenant access attempt: resultId={}, companyId={}, userId={}", + resultId, currentUser.companyId(), currentUser.userId()); + 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(); + } +} \ No newline at end of file diff --git a/src/main/java/com/labelsys/backend/service/AnnotationTaskService.java b/src/main/java/com/labelsys/backend/service/AnnotationTaskService.java new file mode 100644 index 0000000..ca4364d --- /dev/null +++ b/src/main/java/com/labelsys/backend/service/AnnotationTaskService.java @@ -0,0 +1,295 @@ +package com.labelsys.backend.service; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +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 lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@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 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 currentResourceIds = normalizeIds(annotationTaskResourceMapper.listResourceIdsByTaskId(taskId)); + List targetResourceIds = normalizeIds(request.resourceIds()); + boolean resourcesChanged = !currentResourceIds.equals(targetResourceIds); + if (TaskStatus.RUNNING.name().equals(task.getTaskStatus()) && resourcesChanged) { + throw new BusinessException(ResultCode.CONFLICT, "运行中的任务不允许修改资源"); + } + + List 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 pageTasks(LoginUser currentUser, AnnotationTaskPageQuery query) { + List allowedRoles = dataPermissionService.getAllowedRoles(currentUser); + boolean shouldFilterByUserId = dataPermissionService.shouldFilterByUserId(currentUser); + + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .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()); + + if (shouldFilterByUserId) { + wrapper.eq(AnnotationTask::getCreatorId, currentUser.userId()); + } else if (!allowedRoles.isEmpty()) { + wrapper.in(AnnotationTask::getCreatorRole, allowedRoles); + } + + wrapper.orderByDesc(AnnotationTask::getCreatedAt); + + Page page = new Page<>(query.pageNo(), query.pageSize()); + Page resultPage = annotationTaskMapper.selectPage(page, wrapper); + + List records = resultPage.getRecords().stream() + .filter(task -> query.resourceId() == null || + annotationTaskResourceMapper.listResourceIdsByTaskId(task.getId()).contains(query.resourceId())) + .map(task -> buildTaskResponse(task, + normalizeIds(annotationTaskResourceMapper.listResourceIdsByTaskId(task.getId())))) + .toList(); + + return new PageResult<>(records, resultPage.getTotal(), (int) resultPage.getCurrent(), (int) resultPage.getSize()); + } + + @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 loadAndValidateResources(LoginUser currentUser, List resourceIds) { + if (resourceIds == null || resourceIds.isEmpty()) { + throw new BusinessException(ResultCode.BAD_REQUEST, "任务资源不能为空"); + } + List normalizedIds = normalizeIds(resourceIds); + List 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 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 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 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 resourceIds(List resources) { + return resources.stream().map(SourceResource::getId).sorted().toList(); + } + + private List normalizeIds(List resourceIds) { + Set uniqueIds = new HashSet<>(resourceIds); + List 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); + } +} diff --git a/src/main/java/com/labelsys/backend/service/CompanyService.java b/src/main/java/com/labelsys/backend/service/CompanyService.java index 4bffbde..e73ad1f 100644 --- a/src/main/java/com/labelsys/backend/service/CompanyService.java +++ b/src/main/java/com/labelsys/backend/service/CompanyService.java @@ -47,8 +47,8 @@ public class CompanyService { } private void assertPlatformAdmin(LoginUser currentUser) { - if (!currentUser.isPlatformAdmin()) { - throw new ForbiddenException("仅平台管理员可操作"); + if (!currentUser.isSuperAdmin()) { + throw new ForbiddenException("仅系统管理员可操作"); } } } diff --git a/src/main/java/com/labelsys/backend/service/DataPermissionService.java b/src/main/java/com/labelsys/backend/service/DataPermissionService.java index db04686..43c3c3a 100644 --- a/src/main/java/com/labelsys/backend/service/DataPermissionService.java +++ b/src/main/java/com/labelsys/backend/service/DataPermissionService.java @@ -3,27 +3,18 @@ package com.labelsys.backend.service; import com.labelsys.backend.context.LoginUser; import com.labelsys.backend.entity.BizDataRecord; import com.labelsys.backend.enums.UserRole; -import com.labelsys.backend.mapper.BizDataRecordMapper; 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 BizDataRecordMapper bizDataRecordMapper; - - public List listVisibleRecords(LoginUser currentUser) { - return switch (currentUser.role()) { - case EMPLOYEE -> bizDataRecordMapper.listVisibleByEmployee(currentUser.companyId(), currentUser.userId()); - case MANAGER -> bizDataRecordMapper.listVisibleByManager(currentUser.companyId()); - case ENGINEER -> bizDataRecordMapper.listVisibleByEngineer(currentUser.companyId()); - }; - } + private final JdbcTemplate jdbcTemplate; public boolean canAccessCreator(LoginUser currentUser, Long creatorId, UserRole creatorRole) { return switch (currentUser.role()) { @@ -33,22 +24,15 @@ public class DataPermissionService { }; } - /** - * 通用数据过滤方法 - * - * @param currentUser 当前登录用户 - * @param allRecords 待过滤的全量数据列表 - * @param roleExtractor 从数据对象中提取“关联角色”或“创建者角色”的函数 - * @param ownerIdExtractor 从数据对象中提取“所有者ID”的函数(用于员工只能看自己的情况) - * @param 数据类型 - * @return 过滤后的数据列表 + /** + * Generic in-memory role-based data filter for records already loaded in memory. */ public List filterByRole( LoginUser currentUser, List allRecords, Function roleExtractor, Function ownerIdExtractor) { - + if (allRecords == null || allRecords.isEmpty()) { return List.of(); } @@ -62,32 +46,50 @@ public class DataPermissionService { Long recordOwnerId = ownerIdExtractor.apply(record); return switch (currentRole) { - case EMPLOYEE -> - // 员工只能查看自己创建/拥有的数据 - currentUserId.equals(recordOwnerId); - - case MANAGER -> - // 经理可以查看员工和经理的数据,不能查看总工程师的数据 - recordRole == UserRole.EMPLOYEE || recordRole == UserRole.MANAGER; - - case ENGINEER -> - // 总工程师可以查看所有数据 - true; + case EMPLOYEE -> currentUserId.equals(recordOwnerId); + case MANAGER -> recordRole == UserRole.EMPLOYEE || recordRole == UserRole.MANAGER; + case ENGINEER -> true; }; }) .collect(Collectors.toList()); } /** - * 针对 BizDataRecord 的便捷调用方法 + * Returns the creator roles visible to the current user for SQL-side filtering. */ - // public List listVisibleRecordsGeneric(LoginUser currentUser, List allRecords) { - // return filterByRole( - // currentUser, - // allRecords, - // BizDataRecord:: - // BizDataRecord::getCreatorRole, // 提取创建者角色 - // BizDataRecord::getCreatorId // 提取创建者ID - // ); - // } + public List getAllowedRoles(LoginUser currentUser) { + return switch (currentUser.role()) { + case EMPLOYEE -> List.of(); + case MANAGER -> List.of("EMPLOYEE", "MANAGER"); + case ENGINEER -> List.of("EMPLOYEE", "MANAGER", "ENGINEER"); + }; + } + + /** + * Whether SQL queries should additionally restrict by creator/user id. + */ + public boolean shouldFilterByUserId(LoginUser currentUser) { + return currentUser.role() == UserRole.EMPLOYEE; + } + + public List listVisibleRecords(LoginUser currentUser) { + List 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); + } } diff --git a/src/main/java/com/labelsys/backend/service/MenuService.java b/src/main/java/com/labelsys/backend/service/MenuService.java index 31af6d6..fdae154 100644 --- a/src/main/java/com/labelsys/backend/service/MenuService.java +++ b/src/main/java/com/labelsys/backend/service/MenuService.java @@ -14,11 +14,11 @@ public class MenuService { private final SysMenuMapper sysMenuMapper; public List listCurrentMenus(LoginUser currentUser) { - List positionCodes = java.util.Arrays.stream(com.labelsys.backend.enums.UserPosition.values()) + List visiblePositions = java.util.Arrays.stream(com.labelsys.backend.enums.UserPosition.values()) .filter(position -> currentUser.position().canAccess(position)) .map(Enum::name) .toList(); - return sysMenuMapper.listCurrentMenus(currentUser.companyId(), positionCodes) + return sysMenuMapper.listCurrentMenus(currentUser.companyId(), visiblePositions) .stream() .map(MenuResponse::from) .toList(); diff --git a/src/main/java/com/labelsys/backend/service/ObjectStorageInitializer.java b/src/main/java/com/labelsys/backend/service/ObjectStorageInitializer.java new file mode 100644 index 0000000..ead66ce --- /dev/null +++ b/src/main/java/com/labelsys/backend/service/ObjectStorageInitializer.java @@ -0,0 +1,62 @@ +package com.labelsys.backend.service; + +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Component; + +import com.labelsys.backend.config.ObjectStorageProperties; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.CreateBucketRequest; +import software.amazon.awssdk.services.s3.model.HeadBucketRequest; +import software.amazon.awssdk.services.s3.model.NoSuchBucketException; + +@Slf4j +@Component +@RequiredArgsConstructor +public class ObjectStorageInitializer { + + private final S3Client s3Client; + private final ObjectStorageProperties properties; + + @EventListener(ApplicationReadyEvent.class) + public void initializeBuckets() { + log.info("开始初始化对象存储桶..."); + + String[] buckets = { + properties.getSourceBucket(), + properties.getArtifactBucket(), + properties.getExportBucket() + }; + + for (String bucketName : buckets) { + try { + if (!bucketExists(bucketName)) { + createBucket(bucketName); + log.info("成功创建存储桶: {}", bucketName); + } else { + log.info("存储桶已存在: {}", bucketName); + } + } catch (Exception e) { + log.error("初始化存储桶 {} 失败: {}", bucketName, e.getMessage()); + } + } + + log.info("对象存储桶初始化完成"); + } + + private boolean bucketExists(String bucketName) { + try { + s3Client.headBucket(HeadBucketRequest.builder().bucket(bucketName).build()); + return true; + } catch (NoSuchBucketException e) { + return false; + } + } + + private void createBucket(String bucketName) { + s3Client.createBucket(CreateBucketRequest.builder().bucket(bucketName).build()); + } +} \ No newline at end of file diff --git a/src/main/java/com/labelsys/backend/service/ObjectStorageService.java b/src/main/java/com/labelsys/backend/service/ObjectStorageService.java new file mode 100644 index 0000000..b043112 --- /dev/null +++ b/src/main/java/com/labelsys/backend/service/ObjectStorageService.java @@ -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); +} diff --git a/src/main/java/com/labelsys/backend/service/RustfsObjectStorageService.java b/src/main/java/com/labelsys/backend/service/RustfsObjectStorageService.java new file mode 100644 index 0000000..0dd9365 --- /dev/null +++ b/src/main/java/com/labelsys/backend/service/RustfsObjectStorageService.java @@ -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, "对象存储删除失败"); + } + } +} diff --git a/src/main/java/com/labelsys/backend/service/SourceResourceService.java b/src/main/java/com/labelsys/backend/service/SourceResourceService.java new file mode 100644 index 0000000..447dd20 --- /dev/null +++ b/src/main/java/com/labelsys/backend/service/SourceResourceService.java @@ -0,0 +1,168 @@ +package com.labelsys.backend.service; + +import java.io.IOException; +import java.util.List; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; +import org.springframework.web.multipart.MultipartFile; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +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 lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@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, "资源类型非法"); + } + String resourceName = + StringUtils.hasText(request.getResourceName()) ? request.getResourceName() : file.getOriginalFilename(); + SourceResource existingResource = + sourceResourceMapper.selectByCompanyIdAndResourceName(currentUser.companyId(), resourceName); + if (existingResource != null) { + throw new BusinessException(ResultCode.BAD_REQUEST, "资源名称已存在:" + resourceName); + } + + 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 pageResources(LoginUser currentUser, SourceResourcePageQuery query) { + List allowedRoles = dataPermissionService.getAllowedRoles(currentUser); + boolean shouldFilterByUserId = dataPermissionService.shouldFilterByUserId(currentUser); + + LambdaQueryWrapper wrapper = + new LambdaQueryWrapper().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()); + + if (shouldFilterByUserId) { + wrapper.eq(SourceResource::getCreatorId, currentUser.userId()); + } else if (!allowedRoles.isEmpty()) { + wrapper.in(SourceResource::getCreatorRole, allowedRoles); + } + + wrapper.orderByDesc(SourceResource::getCreatedAt); + + Page page = new Page<>(query.pageNo(), query.pageSize()); + Page resultPage = sourceResourceMapper.selectPage(page, wrapper); + + List records = resultPage.getRecords().stream().map(this::toResponse).toList(); + + return new PageResult<>(records, resultPage.getTotal(), (int)resultPage.getCurrent(), + (int)resultPage.getSize()); + } + + public SourceResourceResponse getResource(LoginUser currentUser, Long resourceId) { + SourceResource resource = sourceResourceMapper.selectById(resourceId); + if (resource == null || !currentUser.companyId().equals(resource.getCompanyId())) { + log.warn("Resource not found or cross-tenant access attempt: resourceId={}, companyId={}, userId={}", + resourceId, currentUser.companyId(), currentUser.userId()); + throw new BusinessException(ResultCode.NOT_FOUND, "资源不存在"); + } + 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"; + }; + } +} diff --git a/src/main/java/com/labelsys/backend/service/SysConfigService.java b/src/main/java/com/labelsys/backend/service/SysConfigService.java new file mode 100644 index 0000000..0979969 --- /dev/null +++ b/src/main/java/com/labelsys/backend/service/SysConfigService.java @@ -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 pageConfigs(LoginUser currentUser, SysConfigPageQuery query) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .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 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 PageResult paginate(List 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) { + } +} diff --git a/src/main/java/com/labelsys/backend/service/UserService.java b/src/main/java/com/labelsys/backend/service/UserService.java index 776e632..59e2b5b 100644 --- a/src/main/java/com/labelsys/backend/service/UserService.java +++ b/src/main/java/com/labelsys/backend/service/UserService.java @@ -12,6 +12,7 @@ import com.labelsys.backend.common.exception.BusinessException; import com.labelsys.backend.common.exception.ForbiddenException; import com.labelsys.backend.context.LoginUser; import com.labelsys.backend.dto.request.CreateCompanyAdminRequest; +import com.labelsys.backend.dto.request.CreateSystemEngineerAdminRequest; import com.labelsys.backend.dto.request.CreateUserRequest; import com.labelsys.backend.dto.request.UpdateUserAssignmentRequest; import com.labelsys.backend.dto.request.UpdateUserStatusRequest; @@ -39,6 +40,12 @@ public class UserService { private final PasswordEncoder passwordEncoder; private final TokenSessionRepository tokenSessionRepository; + public List listAllUsers(LoginUser currentUser) { + assertSystemAdmin(currentUser); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper().orderByAsc(SysUser::getId); + return sysUserMapper.selectList(wrapper); + } + public List listCompanyUsers(LoginUser currentUser) { assertCompanyAdmin(currentUser); LambdaQueryWrapper wrapper = new LambdaQueryWrapper() @@ -47,17 +54,32 @@ public class UserService { } public List listCompanyAdmins(LoginUser currentUser, Long companyId) { - assertPlatformAdmin(currentUser); + assertSystemAdmin(currentUser); return sysUserMapper.listCompanyAdmins(companyId); } public SysUser createCompanyAdmin(LoginUser currentUser, CreateCompanyAdminRequest request) { - assertPlatformAdmin(currentUser); + assertSystemAdmin(currentUser); ensureEnabledCompany(request.companyId()); - return createUser( - request.companyId(), - new CreateUserRequest(request.phone(), request.username(), request.realName(), UserRole.EMPLOYEE, UserPosition.ADMIN) - ); + return createUser(request.companyId(), new CreateUserRequest(request.phone(), request.username(), + request.realName(), UserRole.EMPLOYEE, UserPosition.ADMIN)); + } + + public SysUser createSystemEngineerAdmin(LoginUser currentUser, CreateSystemEngineerAdminRequest request) { + assertSystemAdmin(currentUser); + Long systemCompanyId = 1L; + ensureEnabledCompany(systemCompanyId); + + if (sysUserMapper.findByCompanyIdAndPhone(systemCompanyId, request.phone()) != null) { + throw new BusinessException(ResultCode.CONFLICT, "同一公司内手机号已存在"); + } + + SysUser user = SysUser.builder().id(IdGenerator.nextId()).companyId(systemCompanyId).phone(request.phone()) + .username(request.username()).realName(request.realName()).role(UserRole.ENGINEER) + .position(UserPosition.SUPER_ADMIN).passwordHash(passwordEncoder.encode(DEFAULT_PASSWORD)) + .mustChangePassword(true).status(UserStatus.ENABLED).sessionVersion(1).build(); + sysUserMapper.insert(user); + return user; } public SysUser createCompanyUser(LoginUser currentUser, CreateUserRequest request) { @@ -66,7 +88,7 @@ public class UserService { } public SysUser createCompanyUser(LoginUser currentUser, Long companyId, CreateUserRequest request) { - assertPlatformAdmin(currentUser); + assertSystemAdmin(currentUser); ensureEnabledCompany(companyId); return createUser(companyId, request); } @@ -90,8 +112,9 @@ public class UserService { } @Transactional - public void updateCompanyAdminStatus(LoginUser currentUser, Long companyId, Long userId, UpdateUserStatusRequest request) { - assertPlatformAdmin(currentUser); + public void updateCompanyAdminStatus(LoginUser currentUser, Long companyId, Long userId, + UpdateUserStatusRequest request) { + assertSystemAdmin(currentUser); if (sysUserMapper.updateStatus(userId, companyId, request.status()) == 0) { throw new BusinessException(ResultCode.NOT_FOUND, "用户不存在"); } @@ -102,19 +125,10 @@ public class UserService { if (sysUserMapper.findByCompanyIdAndPhone(companyId, request.phone()) != null) { throw new BusinessException(ResultCode.CONFLICT, "同一公司内手机号已存在"); } - SysUser user = SysUser.builder() - .id(IdGenerator.nextId()) - .companyId(companyId) - .phone(request.phone()) - .username(request.username()) - .realName(request.realName()) - .role(request.role()) - .position(request.position()) - .passwordHash(passwordEncoder.encode(DEFAULT_PASSWORD)) - .mustChangePassword(true) - .status(UserStatus.ENABLED) - .sessionVersion(1) - .build(); + SysUser user = SysUser.builder().id(IdGenerator.nextId()).companyId(companyId).phone(request.phone()) + .username(request.username()).realName(request.realName()).role(request.role()).position(request.position()) + .passwordHash(passwordEncoder.encode(DEFAULT_PASSWORD)).mustChangePassword(true).status(UserStatus.ENABLED) + .sessionVersion(1).build(); sysUserMapper.insert(user); return user; } @@ -126,14 +140,20 @@ public class UserService { } } - private void assertPlatformAdmin(LoginUser currentUser) { - if (!currentUser.isPlatformAdmin()) { - throw new ForbiddenException("仅平台管理员可操作"); + // private void assertPlatformAdmin(LoginUser currentUser) { + // if (!currentUser.isPlatformAdmin()) { + // throw new ForbiddenException("仅平台管理员可操作"); + // } + // } + + private void assertSystemAdmin(LoginUser currentUser) { + if (!currentUser.isSuperAdmin()) { + throw new ForbiddenException("仅超级管理员可操作"); } } private void assertCompanyAdmin(LoginUser currentUser) { - if (currentUser.isPlatformAdmin() || currentUser.position() != UserPosition.ADMIN) { + if (currentUser.isSuperAdmin() || currentUser.position() != UserPosition.ADMIN) { throw new ForbiddenException("仅公司管理员可操作"); } } diff --git a/src/main/java/com/labelsys/backend/util/ObjectStoragePathBuilder.java b/src/main/java/com/labelsys/backend/util/ObjectStoragePathBuilder.java new file mode 100644 index 0000000..30574a1 --- /dev/null +++ b/src/main/java/com/labelsys/backend/util/ObjectStoragePathBuilder.java @@ -0,0 +1,38 @@ +package com.labelsys.backend.util; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +public final class ObjectStoragePathBuilder { + + private static final DateTimeFormatter YEAR_MONTH = DateTimeFormatter.ofPattern("yyyyMM"); + + private ObjectStoragePathBuilder() { + } + + public static String sourceObjectKey(Long companyId, String resourceType, Long resourceId, String extension) { + String category = resourceType.toLowerCase(); + return "source/%d/%s/%s/%d/original.%s".formatted( + companyId, + category, + YEAR_MONTH.format(LocalDateTime.now()), + resourceId, + extension.toLowerCase()); + } + + public static String resultQaObjectKey(Long companyId, Long taskId, Long resultId) { + return "result/%d/%s/%d/%d/qa.json".formatted( + companyId, + YEAR_MONTH.format(LocalDateTime.now()), + taskId, + resultId); + } + + public static String resultDiffObjectKey(Long companyId, Long taskId, Long resultId) { + return "result/%d/%s/%d/%d/diff.json".formatted( + companyId, + YEAR_MONTH.format(LocalDateTime.now()), + taskId, + resultId); + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index a175856..5d2b770 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -27,13 +27,28 @@ mybatis-plus: map-underscore-to-camel-case: true springdoc: + api-docs: + enabled: true + path: /v3/api-docs swagger-ui: + enabled: true path: /swagger-ui.html labelsys: session: ttl: PT2H store-type: redis + annotation: + auto-archive-timeout: PT2H + object-storage: + endpoint: ${OBJECT_STORAGE_ENDPOINT:http://39.107.112.174:9000} + region: ${OBJECT_STORAGE_REGION:cn-east-1} + access-key: ${OBJECT_STORAGE_ACCESS_KEY:admin} + secret-key: ${OBJECT_STORAGE_SECRET_KEY:your_strong_password} + path-style-access: ${OBJECT_STORAGE_PATH_STYLE:true} + source-bucket: ${OBJECT_STORAGE_SOURCE_BUCKET:source-data} + artifact-bucket: ${OBJECT_STORAGE_ARTIFACT_BUCKET:annotation-artifacts} + export-bucket: ${OBJECT_STORAGE_EXPORT_BUCKET:finetune-export} logging: level: diff --git a/src/main/resources/mapper/AnnotationResultMapper.xml b/src/main/resources/mapper/AnnotationResultMapper.xml new file mode 100644 index 0000000..77fb503 --- /dev/null +++ b/src/main/resources/mapper/AnnotationResultMapper.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + id, company_id, creator_id, creator_role, task_id, resource_id, qa_content_json, + qa_content_storage_mode, qa_content_file_path, diff_summary, requires_manual_review, + is_deleted, reviewer_id, review_comment, reviewed_at, created_at, updated_at + + + + + + update annotation_result + set is_deleted = true, + reviewer_id = #{reviewerId}, + review_comment = #{reviewComment}, + reviewed_at = #{reviewedAt}, + updated_at = #{reviewedAt} + where id = #{id} + and company_id = #{companyId} + and is_deleted = false + + \ No newline at end of file diff --git a/src/main/resources/mapper/AnnotationTaskMapper.xml b/src/main/resources/mapper/AnnotationTaskMapper.xml new file mode 100644 index 0000000..f5b378c --- /dev/null +++ b/src/main/resources/mapper/AnnotationTaskMapper.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + id, company_id, creator_id, creator_role, task_name, industry_type, task_type, + extract_model_config_id, extract_model_name, extract_model_url, extract_model_api_key, + verify_model_config_id, verify_model_name, verify_model_url, verify_model_api_key, + extract_prompt_config_id, extract_prompt, verify_prompt_config_id, verify_prompt, + task_status, is_deleted, started_at, finished_at, error_message, created_at, updated_at + + + + \ No newline at end of file diff --git a/src/main/resources/mapper/AnnotationTaskResourceMapper.xml b/src/main/resources/mapper/AnnotationTaskResourceMapper.xml new file mode 100644 index 0000000..407be46 --- /dev/null +++ b/src/main/resources/mapper/AnnotationTaskResourceMapper.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + delete from annotation_task_resource where task_id = #{taskId} + + + + diff --git a/src/main/resources/mapper/BizDataRecordMapper.xml b/src/main/resources/mapper/BizDataRecordMapper.xml deleted file mode 100644 index bd3a0bd..0000000 --- a/src/main/resources/mapper/BizDataRecordMapper.xml +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - - - - - - - - - - id, company_id, creator_id, creator_role, record_name, created_at, updated_at - - - - - - - - diff --git a/src/main/resources/mapper/SourceResourceMapper.xml b/src/main/resources/mapper/SourceResourceMapper.xml new file mode 100644 index 0000000..c496633 --- /dev/null +++ b/src/main/resources/mapper/SourceResourceMapper.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + id, company_id, creator_id, creator_role, resource_name, + resource_type, bucket_name, file_path, file_size, source_status, storage_provider, remark, + created_at, updated_at + + + + + \ No newline at end of file diff --git a/src/main/resources/mapper/SysConfigMapper.xml b/src/main/resources/mapper/SysConfigMapper.xml new file mode 100644 index 0000000..4040d56 --- /dev/null +++ b/src/main/resources/mapper/SysConfigMapper.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + id, company_id, config_type, config_name, config_value, status, creator_id, created_at, updated_at + + + + + + diff --git a/src/main/resources/mapper/SysMenuMapper.xml b/src/main/resources/mapper/SysMenuMapper.xml index 9aedbdf..dde48e6 100644 --- a/src/main/resources/mapper/SysMenuMapper.xml +++ b/src/main/resources/mapper/SysMenuMapper.xml @@ -4,26 +4,24 @@ - + diff --git a/src/main/resources/sql/data.sql b/src/main/resources/sql/data.sql index 568c728..c67b8ae 100644 --- a/src/main/resources/sql/data.sql +++ b/src/main/resources/sql/data.sql @@ -1,23 +1,150 @@ -insert into sys_company (id, company_code, company_name, status) values +BEGIN; + +INSERT INTO sys_company (id, company_code, company_name, status) VALUES (1, 'PLATFORM', '平台公司', 'ENABLED'), (2, 'ALPHA', '甲公司', 'ENABLED') -on conflict do nothing; +ON CONFLICT DO NOTHING; -insert into sys_user (id, company_id, phone, username, role, position, real_name, password_hash, must_change_password, status, session_version) values - (1, 1, '13900000000', 'platform-admin', 'ENGINEER', 'ADMIN', '平台管理员', '$2a$10$TGPk5rNNhKNJQvTWImw5J.LVzw9HDFWR6hyNJCkLDcp0GU8/vp0aS', false, 'ENABLED', 1), - (2, 2, '13800138000', 'alpha-admin', 'EMPLOYEE', 'ADMIN', '甲公司管理员', '$2a$10$/hSD8ch7A9lFWi/DOb8yJOHdlrhV57p95CBv9Uv93Yky7t6c4Rs/S', true, 'ENABLED', 1), - (3, 2, '13700000000', 'alpha-annotator', 'EMPLOYEE', 'ANNOTATOR', '甲公司标注员', '$2a$10$bRMZPcIaiB1BUx6HPw6FSODPSuph8kUi8/JZOM6lACwjjhkbBL5mq', false, 'ENABLED', 1), - (4, 2, '13600000000', 'alpha-manager', 'MANAGER', 'REVIEWER', '甲公司经理', '$2a$10$bRMZPcIaiB1BUx6HPw6FSODPSuph8kUi8/JZOM6lACwjjhkbBL5mq', false, 'ENABLED', 1), - (5, 2, '13500000000', 'alpha-engineer', 'ENGINEER', 'ADMIN', '甲公司工程师', '$2a$10$bRMZPcIaiB1BUx6HPw6FSODPSuph8kUi8/JZOM6lACwjjhkbBL5mq', false, 'ENABLED', 1) -on conflict do nothing; +INSERT INTO sys_user ( + id, company_id, phone, username, role, position, real_name, + password_hash, must_change_password, status, session_version +) VALUES + (1, 1, '13900000000', 'platform-admin', 'ENGINEER', 'SUPER_ADMIN', '平台管理员', + '$2a$10$TGPk5rNNhKNJQvTWImw5J.LVzw9HDFWR6hyNJCkLDcp0GU8/vp0aS', FALSE, 'ENABLED', 1), + (2, 2, '13800138000', 'alpha-admin', 'ENGINEER', 'ADMIN', '甲公司管理员', + '$2a$10$/hSD8ch7A9lFWi/DOb8yJOHdlrhV57p95CBv9Uv93Yky7t6c4Rs/S', TRUE, 'ENABLED', 1), + (3, 2, '13700000000', 'alpha-annotator', 'EMPLOYEE', 'ANNOTATOR', '甲公司标注员', + '$2a$10$bRMZPcIaiB1BUx6HPw6FSODPSuph8kUi8/JZOM6lACwjjhkbBL5mq', TRUE, 'ENABLED', 1), + (4, 2, '13600000000', 'alpha-trainer', 'EMPLOYEE', 'DATA_TRAINER', '甲公司数据训练师', + '$2a$10$bRMZPcIaiB1BUx6HPw6FSODPSuph8kUi8/JZOM6lACwjjhkbBL5mq', TRUE, 'ENABLED', 1), + (5, 2, '13500000000', 'alpha-reviewer', 'MANAGER', 'REVIEWER', '甲公司审核员', + '$2a$10$bRMZPcIaiB1BUx6HPw6FSODPSuph8kUi8/JZOM6lACwjjhkbBL5mq', TRUE, 'ENABLED', 1), + (6, 2, '13400000000', 'alpha-chief-engineer', 'ENGINEER', 'REVIEWER', '甲公司总工程师', + '$2a$10$bRMZPcIaiB1BUx6HPw6FSODPSuph8kUi8/JZOM6lACwjjhkbBL5mq', TRUE, 'ENABLED', 1) +ON CONFLICT DO NOTHING; -insert into sys_menu (id, company_id, permission_code, menu_code, menu_name, path, sort_order) values - (201, 2, 'USER_MANAGE', 'USER_MANAGE', '员工管理', '/users', 1), - (202, 2, 'DATA_RECORD_VIEW', 'DATA_RECORDS', '数据记录', '/data-records', 2) -on conflict do nothing; +INSERT INTO sys_menu (id, company_id, menu_code, menu_name, path, visible_positions, sort_order) VALUES + (201, 2, 'USER_MANAGE', '用户管理', '/users', 'ADMIN', 1), + (202, 2, 'RESOURCE_MANAGE', '资源管理', '/resources', 'ANNOTATOR,DATA_TRAINER,REVIEWER,ADMIN', 2), + (203, 2, 'TASK_MANAGE', '任务管理', '/tasks', 'ANNOTATOR,DATA_TRAINER,REVIEWER,ADMIN', 3), + (204, 2, 'RESULT_REVIEW', '结果审核', '/results', 'REVIEWER,ADMIN', 4), + (205, 2, 'DATASET_EXPORT', '训练集导出', '/datasets', 'DATA_TRAINER,ADMIN', 5), + (206, 2, 'SYSTEM_CONFIG', '系统配置', '/configs', 'ADMIN', 6) +ON CONFLICT DO NOTHING; -insert into biz_data_record (id, company_id, creator_id, creator_role, record_name) values - (401, 2, 3, 'EMPLOYEE', '员工创建的数据'), - (402, 2, 4, 'MANAGER', '经理创建的数据'), - (403, 2, 5, 'ENGINEER', '工程师创建的数据') -on conflict do nothing; +INSERT INTO sys_config ( + id, company_id, config_type, config_name, config_value, status, creator_id +) VALUES + (401, 2, 'MODEL', 'qwen-max', + '{"modelUrl":"https://api.example.com/extract","apiKey":"extract-api-key-demo"}', 'ENABLED', 2), + (402, 2, 'MODEL', 'glm-4.5', + '{"modelUrl":"https://api.example.com/verify","apiKey":"verify-api-key-demo"}', 'ENABLED', 2), + (403, 2, 'PROMPT', 'extractPrompt', + '请根据输入内容提取结构化问答对。', 'ENABLED', 2), + (404, 2, 'PROMPT', 'verifyPrompt', + '请核验抽取结果是否准确,并给出修正答案。', 'ENABLED', 2), + (406, 2, 'MODEL', 'qwen-vl-max', + '{"modelUrl":"https://api.example.com/extract-vl","apiKey":"extract-vl-api-key-demo"}', 'ENABLED', 2), + (407, 2, 'MODEL', 'glm-4.5v', + '{"modelUrl":"https://api.example.com/verify-vl","apiKey":"verify-vl-api-key-demo"}', 'ENABLED', 2), + (408, 2, 'PROMPT', 'imageExtractPrompt', + '请根据输入图片内容提取结构化问答对。', 'ENABLED', 2), + (409, 2, 'PROMPT', 'imageVerifyPrompt', + '请核验图片问答结果是否准确。', 'ENABLED', 2), + (405, 2, 'SYSTEM', 'storageProvider', + '{"provider":"rustfs","defaultBucket":"source-data"}', 'ENABLED', 2) +ON CONFLICT DO NOTHING; + +INSERT INTO source_resource ( + id, company_id, creator_id, creator_role, resource_name, resource_type, + bucket_name, file_path, file_size, source_status, storage_provider, remark +) VALUES + (601, 2, 3, 'EMPLOYEE', '设备巡检规范.txt', 'TEXT', + 'source-data', 'text/202604/601.txt', 20480, 'READY', 'rustfs', '文本资源示例'), + (602, 2, 3, 'EMPLOYEE', '控制柜照片.jpg', 'IMAGE', + 'source-data', 'image/202604/602.jpg', 532480, 'READY', 'rustfs', '图片资源示例') +ON CONFLICT DO NOTHING; + +INSERT INTO annotation_task ( + id, company_id, creator_id, creator_role, task_name, industry_type, task_type, + extract_model_config_id, + extract_model_name, extract_model_url, extract_model_api_key, + verify_model_config_id, + verify_model_name, verify_model_url, verify_model_api_key, + extract_prompt_config_id, extract_prompt, + verify_prompt_config_id, verify_prompt, + task_status, is_deleted, started_at, finished_at, error_message +) VALUES + (701, 2, 4, 'EMPLOYEE', '多资源问答抽取任务', 'electricity', 'EXTRACT_QA', + 401, + 'qwen-max', 'https://api.example.com/extract', 'extract-api-key-demo', + 402, + 'glm-4.5', 'https://api.example.com/verify', 'verify-api-key-demo', + 403, '请根据输入文本提取结构化问答对。', + 404, '请核验生成答案是否与原始内容一致。', + 'PENDING', FALSE, NULL, NULL, NULL), + (702, 2, 4, 'EMPLOYEE', '图片问答抽取任务', 'transport', 'EXTRACT_QA', + 406, + 'qwen-vl-max', 'https://api.example.com/extract-vl', 'extract-vl-api-key-demo', + 407, + 'glm-4.5v', 'https://api.example.com/verify-vl', 'verify-vl-api-key-demo', + 408, '请根据输入图片内容提取结构化问答对。', + 409, '请核验图片问答结果是否准确。', + 'COMPLETED', FALSE, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, NULL) +ON CONFLICT DO NOTHING; + +INSERT INTO annotation_task_resource ( + id, company_id, task_id, resource_id +) VALUES + (711, 2, 701, 601), + (712, 2, 701, 602), + (713, 2, 702, 602) +ON CONFLICT DO NOTHING; + +INSERT INTO annotation_result ( + id, company_id, creator_id, creator_role, task_id, resource_id, + qa_content_json, qa_content_storage_mode, qa_content_file_path, diff_summary, + requires_manual_review, is_deleted, reviewer_id, review_comment, reviewed_at +) VALUES + (801, 2, 3, 'EMPLOYEE', 701, 601, + '{"question":"巡检开始前需要做什么?","answer":"详见外置结果文件,包含完整步骤与注意事项。"}', + 'EXTERNAL', 'annotation-results/202604/801-qa.json', + '{"extract_question":"巡检开始前需要做什么?","extract_answer":"开始前检查设备状态和作业环境。","verify_answer":"开始前应确认设备状态、防护用品和现场环境安全。","mismatch_fields":["answer"],"reason":"抽取答案遗漏了安全检查要点。"}', + TRUE, FALSE, NULL, NULL, NULL), + (802, 2, 3, 'EMPLOYEE', 702, 602, + '{"question":"图片中的控制柜当前状态如何?","answer":"控制柜处于运行状态,绿色指示灯亮起。"}', + 'INLINE', NULL, + '{"extract_question":"图片中的控制柜当前状态如何?","extract_answer":"控制柜处于运行状态,绿色指示灯亮起。","verify_answer":"控制柜正在运行,指示灯显示正常。","mismatch_fields":[],"reason":"校验结果与抽取结果基本一致。"}', + FALSE, FALSE, 5, '结果可通过。', CURRENT_TIMESTAMP) +ON CONFLICT DO NOTHING; + +INSERT INTO annotation_result_history ( + id, company_id, creator_id, creator_role, source_result_id, task_id, resource_id, + qa_content_json, qa_content_storage_mode, qa_content_file_path, archive_reason, archived_by, archived_at +) VALUES + (901, 2, 3, 'EMPLOYEE', 802, 702, 602, + '{"question":"图片中的控制柜当前状态如何?","answer":"控制柜处于运行状态,绿色指示灯亮起。"}', + 'INLINE', + NULL, + '审核通过后归档', 5, CURRENT_TIMESTAMP) +ON CONFLICT DO NOTHING; + +INSERT INTO training_dataset ( + id, company_id, creator_id, creator_role, result_history_id, sample_type, glm_format_json, dataset_status +) VALUES + (1001, 2, 4, 'EMPLOYEE', 901, 'TEXT', + '{"messages":[{"role":"system","content":"你是专业知识问答助手"},{"role":"user","content":"图片中的控制柜当前状态如何?"},{"role":"assistant","content":"控制柜处于运行状态,绿色指示灯亮起。"}]}', + 'DRAFT') +ON CONFLICT DO NOTHING; + +INSERT INTO export_batch ( + id, company_id, creator_id, creator_role, batch_no, dataset_file_path, sample_count, finetune_job_id, finetune_status +) VALUES + (1101, 2, 4, 'EMPLOYEE', 'BATCH-20260424-001', 'export/BATCH-20260424-001.jsonl', 1, NULL, 'NOT_STARTED') +ON CONFLICT DO NOTHING; + +INSERT INTO export_batch_item (id, batch_id, dataset_id) VALUES + (1201, 1101, 1001) +ON CONFLICT DO NOTHING; + +COMMIT; diff --git a/src/main/resources/sql/schema.sql b/src/main/resources/sql/schema.sql index b598e1e..e9e97e1 100644 --- a/src/main/resources/sql/schema.sql +++ b/src/main/resources/sql/schema.sql @@ -1,47 +1,414 @@ -create table if not exists sys_company ( - id bigint primary key, - company_code varchar(64) not null unique, - company_name varchar(128) not null, - status varchar(32) not null, - created_at timestamp default current_timestamp, - updated_at timestamp default current_timestamp +-- Active: 1775801470429@@39.107.112.174@5432@lablesystem +begin; + +-- Drop Tables (按依赖关系倒序删除) +DROP TABLE IF EXISTS export_batch_item CASCADE; +DROP TABLE IF EXISTS export_batch CASCADE; +DROP TABLE IF EXISTS training_dataset CASCADE; +DROP TABLE IF EXISTS annotation_result_history CASCADE; +DROP TABLE IF EXISTS annotation_result CASCADE; +DROP TABLE IF EXISTS annotation_task_resource CASCADE; +DROP TABLE IF EXISTS annotation_task CASCADE; +DROP TABLE IF EXISTS source_resource CASCADE; +DROP TABLE IF EXISTS sys_config CASCADE; +DROP TABLE IF EXISTS sys_menu CASCADE; +DROP TABLE IF EXISTS sys_user CASCADE; +DROP TABLE IF EXISTS sys_company CASCADE; + + +CREATE TABLE IF NOT EXISTS sys_company ( + id BIGINT PRIMARY KEY, + company_code VARCHAR(64) NOT NULL UNIQUE, + company_name VARCHAR(128) NOT NULL, + status VARCHAR(32) NOT NULL DEFAULT 'ENABLED', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -create table if not exists sys_user ( - id bigint primary key, - company_id bigint not null, - phone varchar(32) not null, - username varchar(64), - role varchar(32) not null, - position varchar(32) not null, - real_name varchar(64) not null, - password_hash varchar(255) not null, - must_change_password boolean not null default true, - status varchar(32) not null, - session_version integer not null default 1, - created_at timestamp default current_timestamp, - updated_at timestamp default current_timestamp, - constraint uq_sys_user_company_phone unique (company_id, phone) +COMMENT ON TABLE sys_company IS '公司表。'; +COMMENT ON COLUMN sys_company.id IS '公司主键ID。'; +COMMENT ON COLUMN sys_company.company_code IS '公司编码,登录时通过 companyCode 使用。'; +COMMENT ON COLUMN sys_company.company_name IS '公司名称。'; +COMMENT ON COLUMN sys_company.status IS '公司状态,默认 ENABLED,可按需改为 DISABLED 等状态。'; +COMMENT ON COLUMN sys_company.created_at IS '创建时间。'; +COMMENT ON COLUMN sys_company.updated_at IS '更新时间。'; + +CREATE TABLE IF NOT EXISTS sys_user ( + id BIGINT PRIMARY KEY, + company_id BIGINT NOT NULL, + phone VARCHAR(32) NOT NULL, + username VARCHAR(64), + role VARCHAR(32) NOT NULL DEFAULT 'EMPLOYEE', + position VARCHAR(32) NOT NULL DEFAULT 'ANNOTATOR', + real_name VARCHAR(64) NOT NULL, + password_hash VARCHAR(255) NOT NULL, + must_change_password BOOLEAN NOT NULL DEFAULT TRUE, + status VARCHAR(32) NOT NULL DEFAULT 'ENABLED', + session_version INTEGER NOT NULL DEFAULT 1, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT uq_sys_user_company_phone UNIQUE (company_id, phone), + CONSTRAINT fk_sys_user_company FOREIGN KEY (company_id) REFERENCES sys_company(id) ); -create table if not exists sys_menu ( - id bigint primary key, - company_id bigint not null, - permission_code varchar(64) not null, - menu_code varchar(64) not null, - menu_name varchar(128) not null, - path varchar(255) not null, - sort_order integer not null default 0, - created_at timestamp default current_timestamp, - updated_at timestamp default current_timestamp +COMMENT ON TABLE sys_user IS '用户表。role 表示数据权限角色,position 表示岗位。'; +COMMENT ON COLUMN sys_user.id IS '用户主键ID。'; +COMMENT ON COLUMN sys_user.company_id IS '所属公司ID,关联 sys_company.id。'; +COMMENT ON COLUMN sys_user.phone IS '登录手机号,同公司内唯一。'; +COMMENT ON COLUMN sys_user.username IS '用户名或账号别名,用于展示。'; +COMMENT ON COLUMN sys_user.role IS '数据权限角色,默认 EMPLOYEE,可选 EMPLOYEE、MANAGER、ENGINEER。'; +COMMENT ON COLUMN sys_user.position IS '岗位,默认 ANNOTATOR,可选 ANNOTATOR、DATA_TRAINER、REVIEWER、ADMIN、SUPER_ADMIN。'; +COMMENT ON COLUMN sys_user.real_name IS '用户真实姓名。'; +COMMENT ON COLUMN sys_user.password_hash IS '密码哈希值。'; +COMMENT ON COLUMN sys_user.must_change_password IS '是否首次登录强制改密。'; +COMMENT ON COLUMN sys_user.status IS '用户状态,默认 ENABLED。'; +COMMENT ON COLUMN sys_user.session_version IS '会话版本号,用于强制旧 Token 失效。'; +COMMENT ON COLUMN sys_user.created_at IS '创建时间。'; +COMMENT ON COLUMN sys_user.updated_at IS '更新时间。'; + +CREATE TABLE IF NOT EXISTS sys_menu ( + id BIGINT PRIMARY KEY, + company_id BIGINT NOT NULL, + menu_code VARCHAR(64) NOT NULL, + menu_name VARCHAR(128) NOT NULL, + path VARCHAR(255) NOT NULL, + visible_positions VARCHAR(255) NOT NULL DEFAULT 'ADMIN', + sort_order INTEGER NOT NULL DEFAULT 0, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT fk_sys_menu_company FOREIGN KEY (company_id) REFERENCES sys_company(id) ); -create table if not exists biz_data_record ( - id bigint primary key, - company_id bigint not null, - creator_id bigint not null, - creator_role varchar(32) not null, - record_name varchar(255) not null, - created_at timestamp default current_timestamp, - updated_at timestamp default current_timestamp +COMMENT ON TABLE sys_menu IS '菜单表。'; +COMMENT ON COLUMN sys_menu.id IS '菜单主键ID。'; +COMMENT ON COLUMN sys_menu.company_id IS '所属公司ID。'; +COMMENT ON COLUMN sys_menu.menu_code IS '菜单编码。'; +COMMENT ON COLUMN sys_menu.menu_name IS '菜单名称。'; +COMMENT ON COLUMN sys_menu.path IS '前端路由路径。'; +COMMENT ON COLUMN sys_menu.visible_positions IS '可见岗位列表,使用逗号分隔存储,如 ANNOTATOR,DATA_TRAINER,REVIEWER,ADMIN。'; +COMMENT ON COLUMN sys_menu.sort_order IS '菜单排序号,默认 0。'; +COMMENT ON COLUMN sys_menu.created_at IS '创建时间。'; +COMMENT ON COLUMN sys_menu.updated_at IS '更新时间。'; + +CREATE TABLE IF NOT EXISTS sys_config ( + id BIGINT PRIMARY KEY, + company_id BIGINT NOT NULL, + config_type VARCHAR(32) NOT NULL DEFAULT 'SYSTEM', + config_name VARCHAR(128) NOT NULL, + config_value TEXT NOT NULL, + status VARCHAR(32) NOT NULL DEFAULT 'ENABLED', + creator_id BIGINT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT uq_sys_config_company_name UNIQUE (company_id, config_name), + CONSTRAINT fk_sys_config_company FOREIGN KEY (company_id) REFERENCES sys_company(id), + CONSTRAINT fk_sys_config_creator FOREIGN KEY (creator_id) REFERENCES sys_user(id) ); + +COMMENT ON TABLE sys_config IS '系统配置表,保存模型配置、Prompt 配置和系统参数配置。'; +COMMENT ON COLUMN sys_config.id IS '配置主键ID。'; +COMMENT ON COLUMN sys_config.company_id IS '所属公司ID。'; +COMMENT ON COLUMN sys_config.config_type IS '配置类型,默认 SYSTEM,可选 MODEL、PROMPT、SYSTEM。'; +COMMENT ON COLUMN sys_config.config_name IS '配置名称。MODEL 类型时存模型名如 qwen-max;PROMPT 类型时存名称如 extractPrompt、verifyPrompt;SYSTEM 类型时存参数名。'; +COMMENT ON COLUMN sys_config.config_value IS '配置值。MODEL 类型建议保存 JSON,至少包含 modelName、modelUrl、apiKey;PROMPT 类型保存提示词文本;SYSTEM 类型预留后续扩展。'; +COMMENT ON COLUMN sys_config.status IS '配置状态,默认 ENABLED。'; +COMMENT ON COLUMN sys_config.creator_id IS '创建人用户ID。'; +COMMENT ON COLUMN sys_config.created_at IS '创建时间。'; +COMMENT ON COLUMN sys_config.updated_at IS '更新时间。'; + +CREATE TABLE IF NOT EXISTS source_resource ( + id BIGINT PRIMARY KEY, + company_id BIGINT NOT NULL, + creator_id BIGINT NOT NULL, + creator_role VARCHAR(32) NOT NULL DEFAULT 'EMPLOYEE', + resource_name VARCHAR(255) NOT NULL, + resource_type VARCHAR(32) NOT NULL DEFAULT 'TEXT', + bucket_name VARCHAR(128) NOT NULL, + file_path VARCHAR(512) NOT NULL, + file_size BIGINT NOT NULL DEFAULT 0, + source_status VARCHAR(32) NOT NULL DEFAULT 'UPLOADED', + storage_provider VARCHAR(64) NOT NULL DEFAULT 'rustfs', + remark VARCHAR(255), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT fk_source_resource_company FOREIGN KEY (company_id) REFERENCES sys_company(id), + CONSTRAINT fk_source_resource_creator FOREIGN KEY (creator_id) REFERENCES sys_user(id) +); + +COMMENT ON TABLE source_resource IS '资源表,保存文本、图片、视频资源元数据。'; +COMMENT ON COLUMN source_resource.id IS '资源主键ID。'; +COMMENT ON COLUMN source_resource.company_id IS '所属公司ID。'; +COMMENT ON COLUMN source_resource.creator_id IS '上传人或创建人用户ID。'; +COMMENT ON COLUMN source_resource.creator_role IS '创建人数据权限角色,默认 EMPLOYEE。'; +COMMENT ON COLUMN source_resource.resource_name IS '资源名称。'; +COMMENT ON COLUMN source_resource.resource_type IS '资源类型,默认 TEXT,可选 TEXT、IMAGE、VIDEO。'; +COMMENT ON COLUMN source_resource.bucket_name IS '对象存储桶名称。'; +COMMENT ON COLUMN source_resource.file_path IS '文件存储路径,表示对象在存储系统中的实际路径。'; +COMMENT ON COLUMN source_resource.file_size IS '文件大小,单位字节,默认 0。'; +COMMENT ON COLUMN source_resource.source_status IS '资源状态,默认 UPLOADED,可选 PROCESSING、READY、ARCHIVED。'; +COMMENT ON COLUMN source_resource.storage_provider IS '存储提供方,默认 rustfs。'; +COMMENT ON COLUMN source_resource.remark IS '备注说明。'; +COMMENT ON COLUMN source_resource.created_at IS '创建时间。'; +COMMENT ON COLUMN source_resource.updated_at IS '更新时间。'; + +CREATE TABLE IF NOT EXISTS annotation_task ( + id BIGINT PRIMARY KEY, + company_id BIGINT NOT NULL, + creator_id BIGINT NOT NULL, + creator_role VARCHAR(32) NOT NULL DEFAULT 'EMPLOYEE', + task_name VARCHAR(255) NOT NULL, + industry_type VARCHAR(32) NOT NULL DEFAULT 'transport', + task_type VARCHAR(32) NOT NULL DEFAULT 'EXTRACT_QA', + extract_model_config_id BIGINT, + extract_model_name VARCHAR(128), + extract_model_url VARCHAR(255), + extract_model_api_key VARCHAR(255), + verify_model_config_id BIGINT, + verify_model_name VARCHAR(128), + verify_model_url VARCHAR(255), + verify_model_api_key VARCHAR(255), + extract_prompt_config_id BIGINT, + extract_prompt TEXT, + verify_prompt_config_id BIGINT, + verify_prompt TEXT, + task_status VARCHAR(32) NOT NULL DEFAULT 'PENDING', + is_deleted BOOLEAN NOT NULL DEFAULT FALSE, + started_at TIMESTAMP, + finished_at TIMESTAMP, + error_message TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT fk_annotation_task_company FOREIGN KEY (company_id) REFERENCES sys_company(id), + CONSTRAINT fk_annotation_task_creator FOREIGN KEY (creator_id) REFERENCES sys_user(id), + CONSTRAINT fk_annotation_task_extract_model_config FOREIGN KEY (extract_model_config_id) REFERENCES sys_config(id), + CONSTRAINT fk_annotation_task_verify_model_config FOREIGN KEY (verify_model_config_id) REFERENCES sys_config(id), + CONSTRAINT fk_annotation_task_extract_prompt_config FOREIGN KEY (extract_prompt_config_id) REFERENCES sys_config(id), + CONSTRAINT fk_annotation_task_verify_prompt_config FOREIGN KEY (verify_prompt_config_id) REFERENCES sys_config(id) +); + +COMMENT ON TABLE annotation_task IS '任务表,保存任务、配置引用与执行快照。'; +COMMENT ON COLUMN annotation_task.id IS '任务主键ID。'; +COMMENT ON COLUMN annotation_task.company_id IS '所属公司ID。'; +COMMENT ON COLUMN annotation_task.creator_id IS '任务创建人用户ID。'; +COMMENT ON COLUMN annotation_task.creator_role IS '任务创建人数据权限角色,默认 EMPLOYEE。'; +COMMENT ON COLUMN annotation_task.task_name IS '任务名称。'; +COMMENT ON COLUMN annotation_task.industry_type IS '行业类型简写,默认 transport,可选值按业务扩展,例如 electricity。'; +COMMENT ON COLUMN annotation_task.task_type IS '任务类型,默认 EXTRACT_QA。'; +COMMENT ON COLUMN annotation_task.extract_model_config_id IS '抽取模型配置ID,关联 sys_config.id。'; +COMMENT ON COLUMN annotation_task.extract_model_name IS '抽取模型名称。'; +COMMENT ON COLUMN annotation_task.extract_model_url IS '抽取模型调用地址。'; +COMMENT ON COLUMN annotation_task.extract_model_api_key IS '抽取模型调用密钥。'; +COMMENT ON COLUMN annotation_task.verify_model_config_id IS '校验模型配置ID,关联 sys_config.id。'; +COMMENT ON COLUMN annotation_task.verify_model_name IS '校验模型名称。'; +COMMENT ON COLUMN annotation_task.verify_model_url IS '校验模型调用地址。'; +COMMENT ON COLUMN annotation_task.verify_model_api_key IS '校验模型调用密钥。'; +COMMENT ON COLUMN annotation_task.extract_prompt_config_id IS '抽取Prompt配置ID,关联 sys_config.id。'; +COMMENT ON COLUMN annotation_task.extract_prompt IS '抽取 Prompt 文本。'; +COMMENT ON COLUMN annotation_task.verify_prompt_config_id IS '校验Prompt配置ID,关联 sys_config.id。'; +COMMENT ON COLUMN annotation_task.verify_prompt IS '校验 Prompt 文本。'; +COMMENT ON COLUMN annotation_task.task_status IS '任务状态,默认 PENDING,可选 RUNNING、COMPLETED、FAILED。'; +COMMENT ON COLUMN annotation_task.is_deleted IS '任务软删除标记,默认 FALSE。'; +COMMENT ON COLUMN annotation_task.started_at IS '任务开始时间。'; +COMMENT ON COLUMN annotation_task.finished_at IS '任务结束时间。'; +COMMENT ON COLUMN annotation_task.error_message IS '任务失败错误信息。'; +COMMENT ON COLUMN annotation_task.created_at IS '创建时间。'; +COMMENT ON COLUMN annotation_task.updated_at IS '更新时间。'; + +CREATE TABLE IF NOT EXISTS annotation_task_resource ( + id BIGINT PRIMARY KEY, + company_id BIGINT NOT NULL, + task_id BIGINT NOT NULL, + resource_id BIGINT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT uq_annotation_task_resource UNIQUE (task_id, resource_id), + CONSTRAINT fk_annotation_task_resource_company FOREIGN KEY (company_id) REFERENCES sys_company(id), + CONSTRAINT fk_annotation_task_resource_task FOREIGN KEY (task_id) REFERENCES annotation_task(id), + CONSTRAINT fk_annotation_task_resource_resource FOREIGN KEY (resource_id) REFERENCES source_resource(id) +); + +COMMENT ON TABLE annotation_task_resource IS '任务与资源关联表,一个任务可绑定多个资源。'; +COMMENT ON COLUMN annotation_task_resource.id IS '任务资源关联主键ID。'; +COMMENT ON COLUMN annotation_task_resource.company_id IS '所属公司ID。'; +COMMENT ON COLUMN annotation_task_resource.task_id IS '任务ID。'; +COMMENT ON COLUMN annotation_task_resource.resource_id IS '资源ID。'; +COMMENT ON COLUMN annotation_task_resource.created_at IS '创建时间。'; + +CREATE TABLE IF NOT EXISTS annotation_result ( + id BIGINT PRIMARY KEY, + company_id BIGINT NOT NULL, + creator_id BIGINT NOT NULL, + creator_role VARCHAR(32) NOT NULL DEFAULT 'EMPLOYEE', + task_id BIGINT NOT NULL, + resource_id BIGINT NOT NULL, + qa_content_json TEXT NOT NULL DEFAULT '{}', + qa_content_storage_mode VARCHAR(32) NOT NULL DEFAULT 'INLINE', + qa_content_file_path VARCHAR(512), + diff_summary TEXT NOT NULL DEFAULT '{}', + requires_manual_review BOOLEAN NOT NULL DEFAULT FALSE, + is_deleted BOOLEAN NOT NULL DEFAULT FALSE, + reviewer_id BIGINT, + review_comment TEXT, + reviewed_at TIMESTAMP, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT fk_annotation_result_company FOREIGN KEY (company_id) REFERENCES sys_company(id), + CONSTRAINT fk_annotation_result_creator FOREIGN KEY (creator_id) REFERENCES sys_user(id), + CONSTRAINT fk_annotation_result_task FOREIGN KEY (task_id) REFERENCES annotation_task(id), + CONSTRAINT fk_annotation_result_resource FOREIGN KEY (resource_id) REFERENCES source_resource(id), + CONSTRAINT fk_annotation_result_reviewer FOREIGN KEY (reviewer_id) REFERENCES sys_user(id) +); + +COMMENT ON TABLE annotation_result IS '当前标注结果表。'; +COMMENT ON COLUMN annotation_result.id IS '标注结果主键ID。'; +COMMENT ON COLUMN annotation_result.company_id IS '所属公司ID。'; +COMMENT ON COLUMN annotation_result.creator_id IS '结果创建人用户ID。'; +COMMENT ON COLUMN annotation_result.creator_role IS '结果创建人数据权限角色,默认 EMPLOYEE。'; +COMMENT ON COLUMN annotation_result.task_id IS '关联任务ID。'; +COMMENT ON COLUMN annotation_result.resource_id IS '关联资源ID。'; +COMMENT ON COLUMN annotation_result.qa_content_json IS '问答内容 JSON 字符串。字段类型为 TEXT,建议结构为 {\"question\":\"...\",\"answer\":\"...\"}。中小体积内容默认直接入库。'; +COMMENT ON COLUMN annotation_result.qa_content_storage_mode IS '问答内容存储模式,默认 INLINE,可选 INLINE、EXTERNAL。当完整问答内容较大时,可设为 EXTERNAL,仅在表内保留摘要或索引信息。'; +COMMENT ON COLUMN annotation_result.qa_content_file_path IS '当 qa_content_storage_mode = EXTERNAL 时,记录外置问答内容文件路径。'; +COMMENT ON COLUMN annotation_result.diff_summary IS '差异摘要 JSON 字符串。字段类型为 TEXT,建议结构为 {\"extract_question\":\"...\",\"extract_answer\":\"...\",\"verify_answer\":\"...\",\"mismatch_fields\":[\"question\",\"answer\"],\"reason\":\"...\"}。'; +COMMENT ON COLUMN annotation_result.requires_manual_review IS '是否需要人工审核,默认 FALSE。'; +COMMENT ON COLUMN annotation_result.is_deleted IS '软删除标记,默认 FALSE。'; +COMMENT ON COLUMN annotation_result.reviewer_id IS '审核人用户ID。'; +COMMENT ON COLUMN annotation_result.review_comment IS '审核意见。'; +COMMENT ON COLUMN annotation_result.reviewed_at IS '审核时间。'; +COMMENT ON COLUMN annotation_result.created_at IS '创建时间。'; +COMMENT ON COLUMN annotation_result.updated_at IS '更新时间。'; + +CREATE TABLE IF NOT EXISTS annotation_result_history ( + id BIGINT PRIMARY KEY, + company_id BIGINT NOT NULL, + creator_id BIGINT NOT NULL, + creator_role VARCHAR(32) NOT NULL DEFAULT 'EMPLOYEE', + source_result_id BIGINT, + task_id BIGINT NOT NULL, + resource_id BIGINT NOT NULL, + qa_content_json TEXT NOT NULL DEFAULT '{}', + qa_content_storage_mode VARCHAR(32) NOT NULL DEFAULT 'INLINE', + qa_content_file_path VARCHAR(512), + archive_reason VARCHAR(255), + archived_by BIGINT, + archived_at TIMESTAMP, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT fk_annotation_result_history_company FOREIGN KEY (company_id) REFERENCES sys_company(id), + CONSTRAINT fk_annotation_result_history_creator FOREIGN KEY (creator_id) REFERENCES sys_user(id), + CONSTRAINT fk_annotation_result_history_result FOREIGN KEY (source_result_id) REFERENCES annotation_result(id), + CONSTRAINT fk_annotation_result_history_task FOREIGN KEY (task_id) REFERENCES annotation_task(id), + CONSTRAINT fk_annotation_result_history_resource FOREIGN KEY (resource_id) REFERENCES source_resource(id), + CONSTRAINT fk_annotation_result_history_archived_by FOREIGN KEY (archived_by) REFERENCES sys_user(id) +); + +COMMENT ON TABLE annotation_result_history IS '历史归档结果表。'; +COMMENT ON COLUMN annotation_result_history.id IS '历史结果主键ID。'; +COMMENT ON COLUMN annotation_result_history.company_id IS '所属公司ID。'; +COMMENT ON COLUMN annotation_result_history.creator_id IS '历史记录创建人用户ID。'; +COMMENT ON COLUMN annotation_result_history.creator_role IS '历史记录创建人数据权限角色,默认 EMPLOYEE。'; +COMMENT ON COLUMN annotation_result_history.source_result_id IS '来源运行态结果ID。'; +COMMENT ON COLUMN annotation_result_history.task_id IS '关联任务ID。'; +COMMENT ON COLUMN annotation_result_history.resource_id IS '关联资源ID。'; +COMMENT ON COLUMN annotation_result_history.qa_content_json IS '归档后的问答内容 JSON 字符串。字段类型为 TEXT,建议结构为 {"question":"...","answer":"..."}。'; +COMMENT ON COLUMN annotation_result_history.qa_content_storage_mode IS '归档后的问答内容存储模式,默认 INLINE,可选 INLINE、EXTERNAL。'; +COMMENT ON COLUMN annotation_result_history.qa_content_file_path IS '当 qa_content_storage_mode = EXTERNAL 时,记录归档后的外置问答内容文件路径。'; +COMMENT ON COLUMN annotation_result_history.archive_reason IS '归档原因说明。'; +COMMENT ON COLUMN annotation_result_history.archived_by IS '归档操作人用户ID。'; +COMMENT ON COLUMN annotation_result_history.archived_at IS '归档时间。'; +COMMENT ON COLUMN annotation_result_history.created_at IS '创建时间。'; + +CREATE TABLE IF NOT EXISTS training_dataset ( + id BIGINT PRIMARY KEY, + company_id BIGINT NOT NULL, + creator_id BIGINT NOT NULL, + creator_role VARCHAR(32) NOT NULL DEFAULT 'EMPLOYEE', + result_history_id BIGINT NOT NULL, + sample_type VARCHAR(32) NOT NULL DEFAULT 'TEXT', + glm_format_json TEXT NOT NULL, + dataset_status VARCHAR(32) NOT NULL DEFAULT 'DRAFT', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT fk_training_dataset_company FOREIGN KEY (company_id) REFERENCES sys_company(id), + CONSTRAINT fk_training_dataset_creator FOREIGN KEY (creator_id) REFERENCES sys_user(id), + CONSTRAINT fk_training_dataset_result_history FOREIGN KEY (result_history_id) REFERENCES annotation_result_history(id) +); + +COMMENT ON TABLE training_dataset IS '训练样本表。'; +COMMENT ON COLUMN training_dataset.id IS '训练样本主键ID。'; +COMMENT ON COLUMN training_dataset.company_id IS '所属公司ID。'; +COMMENT ON COLUMN training_dataset.creator_id IS '样本创建人用户ID。'; +COMMENT ON COLUMN training_dataset.creator_role IS '样本创建人数据权限角色,默认 EMPLOYEE。'; +COMMENT ON COLUMN training_dataset.result_history_id IS '来源历史结果ID。'; +COMMENT ON COLUMN training_dataset.sample_type IS '样本类型,默认 TEXT,可按需扩展 IMAGE、VIDEO。'; +COMMENT ON COLUMN training_dataset.glm_format_json IS 'GLM微调格式 JSON 字符串。'; +COMMENT ON COLUMN training_dataset.dataset_status IS '样本状态,默认 DRAFT,可选 EXPORTED。'; +COMMENT ON COLUMN training_dataset.created_at IS '创建时间。'; +COMMENT ON COLUMN training_dataset.updated_at IS '更新时间。'; + +CREATE TABLE IF NOT EXISTS export_batch ( + id BIGINT PRIMARY KEY, + company_id BIGINT NOT NULL, + creator_id BIGINT NOT NULL, + creator_role VARCHAR(32) NOT NULL DEFAULT 'EMPLOYEE', + batch_no VARCHAR(64) NOT NULL UNIQUE, + dataset_file_path VARCHAR(512), + sample_count INTEGER NOT NULL DEFAULT 0, + finetune_job_id VARCHAR(128), + finetune_status VARCHAR(32) NOT NULL DEFAULT 'NOT_STARTED', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT fk_export_batch_company FOREIGN KEY (company_id) REFERENCES sys_company(id), + CONSTRAINT fk_export_batch_creator FOREIGN KEY (creator_id) REFERENCES sys_user(id) +); + +COMMENT ON TABLE export_batch IS '导出批次表。'; +COMMENT ON COLUMN export_batch.id IS '导出批次主键ID。'; +COMMENT ON COLUMN export_batch.company_id IS '所属公司ID。'; +COMMENT ON COLUMN export_batch.creator_id IS '批次创建人用户ID。'; +COMMENT ON COLUMN export_batch.creator_role IS '批次创建人数据权限角色,默认 EMPLOYEE。'; +COMMENT ON COLUMN export_batch.batch_no IS '批次编号,全局唯一。'; +COMMENT ON COLUMN export_batch.dataset_file_path IS '导出的数据文件路径。'; +COMMENT ON COLUMN export_batch.sample_count IS '批次包含的样本数量,默认 0。'; +COMMENT ON COLUMN export_batch.finetune_job_id IS '微调任务ID。'; +COMMENT ON COLUMN export_batch.finetune_status IS '微调状态,默认 NOT_STARTED,可选 RUNNING、SUCCESS、FAILED。'; +COMMENT ON COLUMN export_batch.created_at IS '创建时间。'; +COMMENT ON COLUMN export_batch.updated_at IS '更新时间。'; + +CREATE TABLE IF NOT EXISTS export_batch_item ( + id BIGINT PRIMARY KEY, + batch_id BIGINT NOT NULL, + dataset_id BIGINT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT fk_export_batch_item_batch FOREIGN KEY (batch_id) REFERENCES export_batch(id), + CONSTRAINT fk_export_batch_item_dataset FOREIGN KEY (dataset_id) REFERENCES training_dataset(id), + CONSTRAINT uq_export_batch_item UNIQUE (batch_id, dataset_id) +); + +COMMENT ON TABLE export_batch_item IS '导出批次与训练样本关系表。'; +COMMENT ON COLUMN export_batch_item.id IS '批次样本关系主键ID。'; +COMMENT ON COLUMN export_batch_item.batch_id IS '关联导出批次ID。'; +COMMENT ON COLUMN export_batch_item.dataset_id IS '关联训练样本ID。'; +COMMENT ON COLUMN export_batch_item.created_at IS '创建时间。'; + +CREATE INDEX IF NOT EXISTS idx_sys_user_company ON sys_user(company_id); +CREATE INDEX IF NOT EXISTS idx_sys_user_role ON sys_user(company_id, role); +CREATE INDEX IF NOT EXISTS idx_sys_user_position ON sys_user(company_id, position); +CREATE INDEX IF NOT EXISTS idx_sys_menu_company_sort ON sys_menu(company_id, sort_order); +CREATE INDEX IF NOT EXISTS idx_sys_config_company_type ON sys_config(company_id, config_type); +CREATE INDEX IF NOT EXISTS idx_source_resource_company_type ON source_resource(company_id, resource_type); +CREATE INDEX IF NOT EXISTS idx_source_resource_company_status ON source_resource(company_id, source_status); +CREATE INDEX IF NOT EXISTS idx_source_resource_creator ON source_resource(company_id, creator_id); +CREATE INDEX IF NOT EXISTS idx_annotation_task_company_status ON annotation_task(company_id, task_status); +CREATE INDEX IF NOT EXISTS idx_annotation_task_company_deleted ON annotation_task(company_id, is_deleted); +CREATE INDEX IF NOT EXISTS idx_annotation_task_creator ON annotation_task(company_id, creator_id); +CREATE INDEX IF NOT EXISTS idx_annotation_task_resource_company_task ON annotation_task_resource(company_id, task_id); +CREATE INDEX IF NOT EXISTS idx_annotation_task_resource_company_resource ON annotation_task_resource(company_id, resource_id); +CREATE INDEX IF NOT EXISTS idx_annotation_result_company_deleted ON annotation_result(company_id, is_deleted); +CREATE INDEX IF NOT EXISTS idx_annotation_result_company_manual ON annotation_result(company_id, requires_manual_review); +CREATE INDEX IF NOT EXISTS idx_annotation_result_task ON annotation_result(company_id, task_id); +CREATE INDEX IF NOT EXISTS idx_annotation_result_history_company ON annotation_result_history(company_id); +CREATE INDEX IF NOT EXISTS idx_annotation_result_history_task ON annotation_result_history(company_id, task_id); +CREATE INDEX IF NOT EXISTS idx_annotation_result_history_resource ON annotation_result_history(company_id, resource_id); +CREATE INDEX IF NOT EXISTS idx_training_dataset_company_status ON training_dataset(company_id, dataset_status); +CREATE INDEX IF NOT EXISTS idx_export_batch_company_status ON export_batch(company_id, finetune_status); + +COMMIT;