Compare commits

..

16 Commits

Author SHA1 Message Date
wh
942c68989c Merge commit 'ca81514d4373039356d1684f93ae24c9b2182c79' 2026-04-28 20:14:39 +08:00
wh
ca81514d43 任务管理优化 2026-04-28 20:14:14 +08:00
wh
7e35cf146e Merge commit '343df65c698d579335d9c7c47160b79ae0c14f8a' 2026-04-28 12:15:39 +08:00
wh
343df65c69 系统资源管理优化 2026-04-28 12:15:10 +08:00
wh
a01a06d4c1 提交readme 2026-04-27 23:26:25 +08:00
wh
1314e290d5 Merge commit 'd404c7d1879e1dc8854731e4c8d8578d92fccb1b' 2026-04-27 23:23:06 +08:00
wh
d404c7d187 资源管理模块优化 2026-04-27 23:22:14 +08:00
wh
79812b2c77 新增系统管理员岗位 2026-04-27 16:25:39 +08:00
wh
5662c1fda9 提交代码实现v1.0 2026-04-27 10:27:57 +08:00
wh
849e78658d 去掉演示数据bizdata 2026-04-27 00:05:59 +08:00
wh
ebe8b6c7ed 提交数据库设计 2026-04-26 23:41:58 +08:00
wh
b3c9fdfedd datapermissve优化 2026-04-23 17:38:39 +08:00
wh
298e8eb35c Merge commit '2beebbe4697356016295740ea01df1c99b26adc7' 2026-04-23 17:00:34 +08:00
wh
2beebbe469 优化mybatis方法去掉冗余方法 2026-04-23 17:00:02 +08:00
wh
f087687bfa Merge commit '9483bea005d27391f2d2cc3b394c7a6a460283c7' 2026-04-23 12:31:45 +08:00
wh
9483bea005 取消uplaoder 岗位 2026-04-23 12:27:44 +08:00
110 changed files with 4763 additions and 734 deletions

1723
README.md

File diff suppressed because it is too large Load Diff

10
pom.xml
View File

@@ -82,6 +82,14 @@
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>s3</artifactId>
</dependency>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>sts</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
@@ -126,4 +134,4 @@
</plugin>
</plugins>
</build>
</project>
</project>

View File

@@ -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) {

View File

@@ -11,13 +11,13 @@ import lombok.NoArgsConstructor;
@Schema(description = "统一返回结果")
public class Result<T> {
@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 <T> Result<T> success() {

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}

View File

@@ -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 = "岗位枚举值UPLOADER上传者、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;
}
}

View File

@@ -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<PageResult<AnnotationResultResponse>> page(@ParameterObject AnnotationResultPageQuery query) {
return Result.success(annotationResultService.pageResults(UserContext.requireUser(), query));
}
@Operation(summary = "查询标注结果详情")
@GetMapping("/{id}")
public Result<AnnotationResultResponse> 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<AnnotationResultCompareResponse> 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<MergeReviewResultResponse> mergeReview(
@Parameter(description = "结果ID", example = "191000000000000401")
@PathVariable Long id,
@Valid @RequestBody MergeReviewResultRequest request
) {
return Result.success(annotationResultArchiveService.mergeReview(UserContext.requireUser(), id, request));
}
}

View File

@@ -0,0 +1,68 @@
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<AnnotationTaskResponse> create(@Valid @RequestBody CreateAnnotationTaskRequest request) {
return Result.success(annotationTaskService.createTask(UserContext.requireUser(), request));
}
@Operation(summary = "更新标注任务")
@PutMapping("/{id}")
public Result<AnnotationTaskResponse> 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<PageResult<AnnotationTaskResponse>> page(@ParameterObject AnnotationTaskPageQuery query) {
return Result.success(annotationTaskService.pageTasks(UserContext.requireUser(), query));
}
@Operation(summary = "查询标注任务详情")
@GetMapping("/{id}")
public Result<AnnotationTaskResponse> detail(
@Parameter(description = "任务ID", example = "191000000000000301") @PathVariable Long id) {
return Result.success(annotationTaskService.getTask(UserContext.requireUser(), id));
}
@Operation(summary = "删除标注任务")
@DeleteMapping("/{id}")
public Result<Void> delete(
@Parameter(description = "任务ID", example = "191000000000000301") @PathVariable Long id) {
annotationTaskService.deleteTask(UserContext.requireUser(), id);
return Result.success();
}
}

View File

@@ -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<List<CompanyOptionResponse>> listCompanies(@RequestParam String phone) {
public Result<List<CompanyOptionResponse>> 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<Void> logout(@RequestHeader(HttpHeaders.AUTHORIZATION) String authorization) {
public Result<Void> logout(
@Parameter(description = "Bearer 访问令牌", example = "Bearer eyJhbGciOiJIUzI1NiJ9.demo.token")
@RequestHeader(HttpHeaders.AUTHORIZATION) String authorization
) {
authService.logout(extractToken(authorization));
return Result.success();
}

View File

@@ -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<Void> updateAssignment(@PathVariable Long userId, @Valid @RequestBody UpdateUserAssignmentRequest request) {
public Result<Void> 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<Void> updateStatus(@PathVariable Long userId, @Valid @RequestBody UpdateUserStatusRequest request) {
public Result<Void> updateStatus(
@Parameter(description = "用户ID", example = "191000000000000021")
@PathVariable Long userId,
@Valid @RequestBody UpdateUserStatusRequest request
) {
userService.updateStatus(UserContext.requireUser(), userId, request);
return Result.success();
}

View File

@@ -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<List<UserResponse>> listCompanyAdmins(@RequestParam Long companyId) {
public Result<List<UserResponse>> 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<List<UserResponse>> listAllUsers() {
return Result.success(userService.listAllUsers(UserContext.requireUser()).stream().map(UserResponse::from).toList());
}
@Operation(summary = "创建公司管理员")
@PostMapping
public Result<UserResponse> createCompanyAdmin(@Valid @RequestBody CreateCompanyAdminRequest request) {
return Result.success(UserResponse.from(userService.createCompanyAdmin(UserContext.requireUser(), request)));
}
@Operation(summary = "创建系统管理员")
@PostMapping("/system-engineer")
public Result<UserResponse> createSystemEngineerAdmin(@Valid @RequestBody CreateSystemEngineerAdminRequest request) {
return Result.success(UserResponse.from(userService.createSystemEngineerAdmin(UserContext.requireUser(), request)));
}
@Operation(summary = "修改公司管理员状态")
@PutMapping("/{companyId}/{userId}/status")
public Result<Void> updateCompanyAdminStatus(
@Parameter(description = "公司ID", example = "191000000000000001")
@PathVariable Long companyId,
@Parameter(description = "用户ID", example = "191000000000000021")
@PathVariable Long userId,
@Valid @RequestBody UpdateUserStatusRequest request
) {

View File

@@ -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<Void> updateCompanyStatus(@PathVariable Long companyId, @Valid @RequestBody UpdateCompanyStatusRequest request) {
public Result<Void> updateCompanyStatus(
@Parameter(description = "公司ID", example = "191000000000000001")
@PathVariable Long companyId,
@Valid @RequestBody UpdateCompanyStatusRequest request
) {
companyService.updateStatus(UserContext.requireUser(), companyId, request);
return Result.success();
}

View File

@@ -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<SourceUploadResponse> upload(@ParameterObject @ModelAttribute SourceUploadRequest request) {
return Result.success(sourceResourceService.upload(UserContext.requireUser(), request));
}
@Operation(summary = "分页查询资源")
@GetMapping
public Result<PageResult<SourceResourceResponse>> page(@ParameterObject @ModelAttribute SourceResourcePageQuery query) {
return Result.success(sourceResourceService.pageResources(UserContext.requireUser(), query));
}
@Operation(summary = "查询资源详情")
@GetMapping("/{id}")
public Result<SourceResourceResponse> detail(
@Parameter(description = "资源ID", example = "191000000000000101")
@PathVariable Long id
) {
return Result.success(sourceResourceService.getResource(UserContext.requireUser(), id));
}
@Operation(summary = "删除资源")
@DeleteMapping("/{id}")
public Result<Void> delete(
@Parameter(description = "资源ID", example = "191000000000000101")
@PathVariable Long id
) {
sourceResourceService.deleteResource(UserContext.requireUser(), id);
return Result.success();
}
}

View File

@@ -0,0 +1,65 @@
package com.labelsys.backend.controller;
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;
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.request.UpdateSysConfigRequest;
import com.labelsys.backend.dto.response.SysConfigResponse;
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;
@Tag(name = "系统配置管理")
@RestController
@RequestMapping("/api/sys-configs")
@RequiredArgsConstructor
public class SysConfigController {
private final SysConfigService sysConfigService;
@Operation(summary = "创建系统配置")
// @RequirePosition(UserPosition.ADMIN)
@PostMapping
public Result<SysConfigResponse> create(@Valid @RequestBody SaveSysConfigRequest request) {
return Result
.success(sysConfigService.toResponse(sysConfigService.saveConfig(UserContext.requireUser(), request)));
}
@Operation(summary = "更新系统配置")
// @RequirePosition(UserPosition.ADMIN)
@PutMapping("/{id}")
public Result<SysConfigResponse> update(
@Parameter(description = "配置ID", example = "191000000000000501") @PathVariable Long id,
@Valid @RequestBody UpdateSysConfigRequest request) {
return Result.success(
sysConfigService.toResponse(sysConfigService.updateConfig(UserContext.requireUser(), id, request)));
}
@Operation(summary = "分页查询系统配置")
@GetMapping
public Result<PageResult<SysConfigResponse>> page(@ParameterObject SysConfigPageQuery query) {
return Result.success(sysConfigService.pageConfigs(UserContext.requireUser(), query));
}
@Operation(summary = "查询系统配置详情")
@GetMapping("/{id}")
public Result<SysConfigResponse>
detail(@Parameter(description = "配置ID", example = "191000000000000501") @PathVariable Long id) {
return Result.success(sysConfigService.getConfig(UserContext.requireUser(), id));
}
}

View File

@@ -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<T>(
@Schema(description = "当前页记录", example = "[]") List<T> records,
@Schema(description = "总记录数", example = "2") Long total,
@Schema(description = "页码", example = "1") Integer pageNo,
@Schema(description = "每页数量", example = "10") Integer pageSize
) {
public static <T> PageResult<T> from(IPage<T> page) {
return new PageResult<>(page.getRecords(), page.getTotal(), (int) page.getCurrent(), (int) page.getSize());
}
}

View File

@@ -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
) {
}

View File

@@ -0,0 +1,17 @@
package com.labelsys.backend.dto.request;
import com.labelsys.backend.enums.TaskType;
import io.swagger.v3.oas.annotations.media.Schema;
@Schema(description = "标注任务分页查询请求")
public record AnnotationTaskPageQuery(
@Schema(description = "关键字", example = "运输") String keyword,
@Schema(description = "任务类型", example = "EXTRACT_QA") TaskType 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
) {
}

View File

@@ -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
) {
}

View File

@@ -0,0 +1,24 @@
package com.labelsys.backend.dto.request;
import java.util.List;
import com.labelsys.backend.enums.IndustryType;
import com.labelsys.backend.enums.TaskType;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
@Schema(description = "创建标注任务请求")
public record CreateAnnotationTaskRequest(
@Schema(description = "任务名称", example = "运输文档问答抽取任务") @NotBlank(message = "任务名称不能为空") String taskName,
@Schema(description = "行业类型", defaultValue = "TRANSPORT", example = "TRANSPORT") @NotNull(message = "行业类型不能为空") IndustryType industryType,
@Schema(description = "任务类型", defaultValue = "EXTRACT_QA", example = "EXTRACT_QA") @NotNull(message = "任务类型不能为空") TaskType taskType,
@Schema(description = "资源ID列表", example = "[191000000000000101,191000000000000102]") @NotEmpty(message = "资源列表不能为空") List<Long> 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) {
}

View File

@@ -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
) {
}

View File

@@ -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
) {
}

View File

@@ -0,0 +1,11 @@
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) {
}

View File

@@ -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 = "岗位,枚举值:UPLOADER上传者、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
) {
}

View File

@@ -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
) {
}

View File

@@ -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
) {
}

View File

@@ -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
) {
}

View File

@@ -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
) {
}

View File

@@ -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") String status) {}

View File

@@ -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
) {
}

View File

@@ -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;
}

View File

@@ -0,0 +1,10 @@
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) {}

View File

@@ -0,0 +1,16 @@
package com.labelsys.backend.dto.request;
import com.labelsys.backend.enums.ConfigMode;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
@Schema(description = "任务模型配置请求")
public record TaskModelConfigRequest(
@Schema(description = "配置模式SELECT 或 MANUAL", example = "SELECT") @NotNull(message = "配置模式不能为空") ConfigMode 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) {
}

View File

@@ -0,0 +1,20 @@
package com.labelsys.backend.dto.request;
import java.util.List;
import com.labelsys.backend.enums.IndustryType;
import com.labelsys.backend.enums.TaskType;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
@Schema(description = "更新标注任务请求")
public record UpdateAnnotationTaskRequest(
@Schema(description = "行业类型", example = "TRANSPORT") IndustryType industryType,
@Schema(description = "任务类型", example = "EXTRACT_QA") TaskType taskType,
@Schema(description = "资源ID列表", example = "[191000000000000101,191000000000000102]") List<Long> 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) {
}

View File

@@ -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
) {
}

View File

@@ -0,0 +1,14 @@
package com.labelsys.backend.dto.request;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
@Schema(description = "更新系统配置请求")
public record UpdateSysConfigRequest(
@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\"}") @NotBlank(
message = "配置值不能为空") String configValue,
@Schema(description = "配置状态", example = "ENABLED") String status) {}

View File

@@ -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 = "岗位,枚举值:UPLOADER上传者、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
) {
}

View File

@@ -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
) {
}

View File

@@ -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
) {
}

View File

@@ -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
) {
}

View File

@@ -0,0 +1,25 @@
package com.labelsys.backend.dto.response;
import io.swagger.v3.oas.annotations.media.Schema;
import java.time.LocalDateTime;
import java.util.List;
import com.labelsys.backend.enums.IndustryType;
import com.labelsys.backend.enums.TaskType;
@Schema(description = "标注任务响应")
public record AnnotationTaskResponse(
@Schema(description = "任务ID", example = "191000000000000301") Long id,
@Schema(description = "任务名称", example = "运输文档问答抽取任务") String taskName,
@Schema(description = "行业类型:默认值transport暂不显示", example = "transport") IndustryType industryType,
@Schema(description = "任务类型:暂不显示", example = "EXTRACT_QA") TaskType taskType,
@Schema(description = "任务状态", example = "PENDING") String taskStatus,
@Schema(description = "资源ID列表", example = "[191000000000000101,191000000000000102]") List<Long> 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
) {
}

View File

@@ -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());

View File

@@ -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());

View File

@@ -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 = "岗位,枚举值:UPLOADER上传者、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(

View File

@@ -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()
);
}
}

View File

@@ -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 = "岗位,枚举值:UPLOADER上传者、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(

View File

@@ -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());

View File

@@ -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
) {
}

View File

@@ -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
) {
}

View File

@@ -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
) {
}

View File

@@ -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
) {
}

View File

@@ -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
) {
}

View File

@@ -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
) {
}

View File

@@ -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 = "岗位,枚举值:UPLOADER上传者、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(

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -0,0 +1,48 @@
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.IndustryType;
import com.labelsys.backend.enums.TaskType;
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 IndustryType industryType;
private TaskType 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;
}

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -0,0 +1,29 @@
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 String creatorRole;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}

View File

@@ -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;

View File

@@ -0,0 +1,12 @@
package com.labelsys.backend.enums;
import io.swagger.v3.oas.annotations.media.Schema;
@Schema(description = "模型配置模式枚举值SELECT:选择已有模型配置、MANUAL手动配置新模型")
public enum ConfigMode {
@Schema(description = "从已有配置中选择")
SELECT,
@Schema(description = "手动录入配置")
MANUAL
}

View File

@@ -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));
}
}

View File

@@ -0,0 +1,21 @@
package com.labelsys.backend.enums;
import io.swagger.v3.oas.annotations.media.Schema;
@Schema(description = "行业类型")
public enum IndustryType {
@Schema(description = "交通运输")
TRANSPORT,
@Schema(description = "电力")
ELECTRICITY,
@Schema(description = "金融")
FINANCE,
@Schema(description = "医疗")
MEDICAL,
@Schema(description = "教育")
EDUCATION
}

View File

@@ -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));
}
}

View File

@@ -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));
}
}

View File

@@ -0,0 +1,7 @@
package com.labelsys.backend.enums;
public enum RuntimeResultStatus {
MANUAL_REVIEW_PENDING,
AUTO_ARCHIVE_PENDING,
ARCHIVED
}

View File

@@ -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));
}
}

View File

@@ -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));
}
}

View File

@@ -0,0 +1,12 @@
package com.labelsys.backend.enums;
import io.swagger.v3.oas.annotations.media.Schema;
@Schema(description = "标注任务类型")
public enum TaskType {
@Schema(description = "抽取问答对")
EXTRACT_QA,
@Schema(description = "大模型微调")
FINE_TUNE
}

View File

@@ -6,17 +6,13 @@ import lombok.RequiredArgsConstructor;
@Getter
@RequiredArgsConstructor
@Schema(description = "岗位枚举,枚举值:UPLOADER上传者、ANNOTATOR标注员、DATA_TRAINER数据训练师、REVIEWER审核员、ADMIN超级管理员")
@Schema(description = "岗位枚举枚举值ANNOTATOR标注员、DATA_TRAINER数据训练师、REVIEWER审核员、ADMIN公司管理员、SUPER_ADMIN超级管理员")
public enum UserPosition {
UPLOADER(1),
ANNOTATOR(2),
DATA_TRAINER(3),
REVIEWER(4),
ADMIN(5);
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;
}
}
}

View File

@@ -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<String> 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<String> 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.findById(loginUser.companyId());
if (user == null || company == null || user.getStatus() != UserStatus.ENABLED || company.getStatus() != CompanyStatus.ENABLED) {
SysCompany company = sysCompanyMapper.selectById(loginUser.companyId());
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();
}

View File

@@ -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<AnnotationResultHistory> {
}

View File

@@ -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> {
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);
}

View File

@@ -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> {
AnnotationTask findByIdAndCompanyId(@Param("id") Long id, @Param("companyId") Long companyId);
}

View File

@@ -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<AnnotationTaskResource> {
List<Long> listResourceIdsByTaskId(@Param("taskId") Long taskId);
int deleteByTaskId(@Param("taskId") Long taskId);
int countByResourceId(@Param("resourceId") Long resourceId);
}

View File

@@ -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<BizDataRecord> {
List<BizDataRecord> listVisibleByEmployee(@Param("companyId") Long companyId, @Param("creatorId") Long creatorId);
List<BizDataRecord> listVisibleByManager(@Param("companyId") Long companyId);
List<BizDataRecord> listVisibleByEngineer(@Param("companyId") Long companyId);
}

View File

@@ -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<SourceResource> {
List<SourceResource> selectByCompanyIdAndIds(@Param("companyId") Long companyId, @Param("resourceIds") List<Long> resourceIds);
SourceResource selectByCompanyIdAndResourceName(@Param("companyId") Long companyId, @Param("resourceName") String resourceName);
}

View File

@@ -8,17 +8,11 @@ import org.apache.ibatis.annotations.Param;
public interface SysCompanyMapper extends BaseMapper<SysCompany> {
int insert(SysCompany company);
SysCompany findById(@Param("id") Long id);
SysCompany findByCompanyCode(@Param("companyCode") String companyCode);
List<SysCompany> findEnabledCompaniesByPhone(@Param("phone") String phone);
List<SysCompany> listAll();
int updateStatus(@Param("id") Long id, @Param("status") CompanyStatus status);
void deleteAll();
}
}

View File

@@ -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> {
SysConfig findByCompanyIdAndConfigName(@Param("companyId") Long companyId, @Param("configName") String configName);
SysConfig findByCompanyIdAndConfigNameAndType(@Param("companyId") Long companyId,
@Param("configName") String configName,
@Param("configType") String configType);
}

View File

@@ -7,5 +7,5 @@ import org.apache.ibatis.annotations.Param;
public interface SysMenuMapper extends BaseMapper<SysMenu> {
List<SysMenu> listCurrentMenus(@Param("companyId") Long companyId, @Param("positionCodes") List<String> positionCodes);
List<SysMenu> listCurrentMenus(@Param("companyId") Long companyId, @Param("visiblePositions") List<String> visiblePositions);
}

View File

@@ -10,35 +10,21 @@ import org.apache.ibatis.annotations.Param;
public interface SysUserMapper extends BaseMapper<SysUser> {
int insert(SysUser user);
SysUser findById(@Param("id") Long id);
SysUser findByIdAndCompanyId(@Param("id") Long id, @Param("companyId") Long companyId);
SysUser findByCompanyIdAndPhone(@Param("companyId") Long companyId, @Param("phone") String phone);
List<SysUser> listByCompanyId(@Param("companyId") Long companyId);
List<SysUser> listCompanyAdmins(@Param("companyId") Long companyId);
int updatePassword(
@Param("id") Long id,
@Param("companyId") Long companyId,
@Param("passwordHash") String passwordHash,
@Param("mustChangePassword") boolean mustChangePassword
);
int updatePassword(@Param("id") Long id, @Param("companyId") Long companyId,
@Param("passwordHash") String passwordHash, @Param("mustChangePassword") boolean mustChangePassword);
int updateAssignment(
@Param("id") Long id,
@Param("companyId") Long companyId,
@Param("role") UserRole role,
@Param("position") UserPosition position
);
int updateAssignment(@Param("id") Long id, @Param("companyId") Long companyId, @Param("role") UserRole role,
@Param("position") UserPosition position);
int updateStatus(@Param("id") Long id, @Param("companyId") Long companyId, @Param("status") UserStatus status);
int bumpSessionVersion(@Param("id") Long id, @Param("companyId") Long companyId);
void deleteAll();
}
}

View File

@@ -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);
}
}
}

View File

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

View File

@@ -0,0 +1,102 @@
package com.labelsys.backend.service;
import java.util.List;
import org.springframework.stereotype.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 lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Service
@RequiredArgsConstructor
public class AnnotationResultService {
private final AnnotationResultMapper annotationResultMapper;
private final SourceResourceMapper sourceResourceMapper;
private final DataPermissionService dataPermissionService;
public PageResult<AnnotationResultResponse> pageResults(LoginUser currentUser, AnnotationResultPageQuery query) {
List<String> allowedRoles = dataPermissionService.getAllowedRoles(currentUser);
boolean shouldFilterByUserId = dataPermissionService.shouldFilterByUserId(currentUser);
LambdaQueryWrapper<AnnotationResult> wrapper =
new LambdaQueryWrapper<AnnotationResult>().eq(AnnotationResult::getCompanyId, currentUser.companyId())
.eq(query.taskId() != null, AnnotationResult::getTaskId, query.taskId())
.eq(query.resourceId() != null, AnnotationResult::getResourceId, query.resourceId())
.eq(query.requiresManualReview() != null, AnnotationResult::getRequiresManualReview,
query.requiresManualReview());
if (shouldFilterByUserId) {
wrapper.eq(AnnotationResult::getCreatorId, currentUser.userId());
} else if (!allowedRoles.isEmpty()) {
wrapper.in(AnnotationResult::getCreatorRole, allowedRoles);
}
wrapper.orderByDesc(AnnotationResult::getCreatedAt);
Page<AnnotationResult> page = new Page<>(query.pageNo(), query.pageSize());
Page<AnnotationResult> resultPage = annotationResultMapper.selectPage(page, wrapper);
List<AnnotationResultResponse> 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();
}
}

View File

@@ -0,0 +1,325 @@
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.entity.SysConfig;
import com.labelsys.backend.enums.IndustryType;
import com.labelsys.backend.enums.SourceStatus;
import com.labelsys.backend.enums.TaskStatus;
import com.labelsys.backend.enums.TaskType;
import com.labelsys.backend.mapper.AnnotationTaskMapper;
import com.labelsys.backend.mapper.AnnotationTaskResourceMapper;
import com.labelsys.backend.mapper.SourceResourceMapper;
import com.labelsys.backend.mapper.SysConfigMapper;
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 SysConfigMapper sysConfigMapper;
private final SysConfigService sysConfigService;
private final DataPermissionService dataPermissionService;
@Transactional
public AnnotationTaskResponse createTask(LoginUser currentUser, CreateAnnotationTaskRequest request) {
List<SourceResource> resources = loadAndValidateResources(currentUser, request.resourceIds());
SysConfigService.ResolvedModelConfig extractModel = sysConfigService.resolveModelConfig(currentUser,
request.extractModel());
SysConfigService.ResolvedModelConfig verifyModel = sysConfigService.resolveModelConfig(currentUser,
request.verifyModel());
SysConfigService.ResolvedPromptConfig extractPrompt = sysConfigService.resolvePromptConfig(currentUser,
request.extractPrompt());
SysConfigService.ResolvedPromptConfig verifyPrompt = sysConfigService.resolvePromptConfig(currentUser,
request.verifyPrompt());
AnnotationTask task = AnnotationTask.builder().id(IdGenerator.nextId()).companyId(currentUser.companyId())
.creatorId(currentUser.userId()).creatorRole(currentUser.role()).taskName(request.taskName())
.industryType(defaultIndustryType(request.industryType())).taskType(defaultTaskType(request.taskType()))
.extractModelConfigId(extractModel.configId()).extractModelName(extractModel.modelName())
.extractModelUrl(extractModel.modelUrl()).extractModelApiKey(extractModel.apiKey())
.verifyModelConfigId(verifyModel.configId()).verifyModelName(verifyModel.modelName())
.verifyModelUrl(verifyModel.modelUrl()).verifyModelApiKey(verifyModel.apiKey())
.extractPromptConfigId(extractPrompt.configId()).extractPrompt(extractPrompt.promptText())
.verifyPromptConfigId(verifyPrompt.configId()).verifyPrompt(verifyPrompt.promptText())
.taskStatus(TaskStatus.PENDING.name()).isDeleted(false).build();
annotationTaskMapper.insert(task);
saveTaskBindings(task.getId(), currentUser.companyId(), resources);
log.info("created annotation task, companyId={}, userId={}, taskId={}, resourceCount={}",
currentUser.companyId(), currentUser.userId(), task.getId(), resources.size());
return buildTaskResponse(task, resourceIds(resources), extractModel, verifyModel, extractPrompt, verifyPrompt);
}
@Transactional
public AnnotationTaskResponse updateTask(LoginUser currentUser, Long taskId, UpdateAnnotationTaskRequest request) {
AnnotationTask task = annotationTaskMapper.findByIdAndCompanyId(taskId, currentUser.companyId());
if (task == null) {
throw new BusinessException(ResultCode.NOT_FOUND, "任务不存在");
}
assertTaskPermission(currentUser, task);
boolean resourcesChanged = false;
List<SourceResource> resources = null;
if (request.resourceIds() != null && !request.resourceIds().isEmpty()) {
List<Long> currentResourceIds = normalizeIds(annotationTaskResourceMapper.listResourceIdsByTaskId(taskId));
List<Long> targetResourceIds = normalizeIds(request.resourceIds());
resourcesChanged = !currentResourceIds.equals(targetResourceIds);
if (TaskStatus.RUNNING.name().equals(task.getTaskStatus()) && resourcesChanged) {
throw new BusinessException(ResultCode.CONFLICT, "运行中的任务不允许修改资源");
}
resources = loadAndValidateResources(currentUser, request.resourceIds());
}
SysConfigService.ResolvedModelConfig extractModel = null;
SysConfigService.ResolvedModelConfig verifyModel = null;
SysConfigService.ResolvedPromptConfig extractPrompt = null;
SysConfigService.ResolvedPromptConfig verifyPrompt = null;
if (request.extractModel() != null) {
extractModel = sysConfigService.resolveModelConfig(currentUser, request.extractModel());
task.setExtractModelConfigId(extractModel.configId());
task.setExtractModelName(extractModel.modelName());
task.setExtractModelUrl(extractModel.modelUrl());
task.setExtractModelApiKey(extractModel.apiKey());
}
if (request.verifyModel() != null) {
verifyModel = sysConfigService.resolveModelConfig(currentUser, request.verifyModel());
task.setVerifyModelConfigId(verifyModel.configId());
task.setVerifyModelName(verifyModel.modelName());
task.setVerifyModelUrl(verifyModel.modelUrl());
task.setVerifyModelApiKey(verifyModel.apiKey());
}
if (request.extractPrompt() != null) {
extractPrompt = sysConfigService.resolvePromptConfig(currentUser, request.extractPrompt());
task.setExtractPromptConfigId(extractPrompt.configId());
task.setExtractPrompt(extractPrompt.promptText());
}
if (request.verifyPrompt() != null) {
verifyPrompt = sysConfigService.resolvePromptConfig(currentUser, request.verifyPrompt());
task.setVerifyPromptConfigId(verifyPrompt.configId());
task.setVerifyPrompt(verifyPrompt.promptText());
}
if (request.industryType() != null) {
task.setIndustryType(request.industryType());
}
if (request.taskType() != null) {
task.setTaskType(request.taskType());
}
annotationTaskMapper.updateById(task);
if (resourcesChanged && resources != null) {
annotationTaskResourceMapper.deleteByTaskId(taskId);
saveTaskBindings(taskId, currentUser.companyId(), resources);
}
log.info("updated annotation task, companyId={}, userId={}, taskId={}, resourcesChanged={}",
currentUser.companyId(), currentUser.userId(), taskId, resourcesChanged);
List<Long> finalResourceIds = resources != null ? resourceIds(resources)
: normalizeIds(annotationTaskResourceMapper.listResourceIdsByTaskId(taskId));
if (extractModel == null || verifyModel == null || extractPrompt == null || verifyPrompt == null) {
return buildTaskResponse(task, finalResourceIds);
}
return buildTaskResponse(task, finalResourceIds, extractModel, verifyModel, extractPrompt, verifyPrompt);
}
public AnnotationTaskResponse getTask(LoginUser currentUser, Long taskId) {
AnnotationTask task = annotationTaskMapper.findByIdAndCompanyId(taskId, currentUser.companyId());
if (task == null) {
throw new BusinessException(ResultCode.NOT_FOUND, "任务不存在");
}
assertTaskPermission(currentUser, task);
return buildTaskResponse(task, normalizeIds(annotationTaskResourceMapper.listResourceIdsByTaskId(taskId)));
}
public PageResult<AnnotationTaskResponse> pageTasks(LoginUser currentUser, AnnotationTaskPageQuery query) {
List<String> allowedRoles = dataPermissionService.getAllowedRoles(currentUser);
boolean shouldFilterByUserId = dataPermissionService.shouldFilterByUserId(currentUser);
LambdaQueryWrapper<AnnotationTask> wrapper = new LambdaQueryWrapper<AnnotationTask>()
.eq(AnnotationTask::getCompanyId, currentUser.companyId())
.eq(query.taskType() != null, 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<AnnotationTask> page = new Page<>(query.pageNo(), query.pageSize());
Page<AnnotationTask> resultPage = annotationTaskMapper.selectPage(page, wrapper);
List<AnnotationTaskResponse> 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<SourceResource> loadAndValidateResources(LoginUser currentUser, List<Long> resourceIds) {
if (resourceIds == null || resourceIds.isEmpty()) {
throw new BusinessException(ResultCode.BAD_REQUEST, "任务资源不能为空");
}
List<Long> normalizedIds = normalizeIds(resourceIds);
List<SourceResource> resources = sourceResourceMapper.selectByCompanyIdAndIds(currentUser.companyId(),
normalizedIds);
if (resources.size() != normalizedIds.size()) {
throw new BusinessException(ResultCode.BAD_REQUEST, "存在无效资源");
}
for (SourceResource resource : resources) {
if (!dataPermissionService.canAccessCreator(currentUser, resource.getCreatorId(),
resource.getCreatorRole())) {
throw new BusinessException(ResultCode.FORBIDDEN, "无权访问资源");
}
if (!SourceStatus.READY.name().equals(resource.getSourceStatus())) {
throw new BusinessException(ResultCode.BAD_REQUEST, "仅允许选择已就绪资源");
}
}
resources.sort(Comparator.comparing(SourceResource::getId));
return resources;
}
private void saveTaskBindings(Long taskId, Long companyId, List<SourceResource> resources) {
for (SourceResource resource : resources) {
annotationTaskResourceMapper.insert(AnnotationTaskResource.builder().id(IdGenerator.nextId())
.companyId(companyId).taskId(taskId).resourceId(resource.getId()).build());
}
}
private AnnotationTaskResponse buildTaskResponse(AnnotationTask task, List<Long> resourceIds,
SysConfigService.ResolvedModelConfig extractModel, SysConfigService.ResolvedModelConfig verifyModel,
SysConfigService.ResolvedPromptConfig extractPrompt, SysConfigService.ResolvedPromptConfig verifyPrompt) {
return new AnnotationTaskResponse(task.getId(), task.getTaskName(), task.getIndustryType(), task.getTaskType(),
task.getTaskStatus(), resourceIds, sysConfigService.toResponse(extractModel),
sysConfigService.toResponse(verifyModel), sysConfigService.toResponse(extractPrompt),
sysConfigService.toResponse(verifyPrompt), task.getCreatedAt(), task.getUpdatedAt());
}
private AnnotationTaskResponse buildTaskResponse(AnnotationTask task, List<Long> resourceIds) {
String extractModelConfigName = resolveConfigName(task.getExtractModelConfigId());
String verifyModelConfigName = resolveConfigName(task.getVerifyModelConfigId());
String extractPromptConfigName = resolveConfigName(task.getExtractPromptConfigId());
String verifyPromptConfigName = resolveConfigName(task.getVerifyPromptConfigId());
return new AnnotationTaskResponse(task.getId(), task.getTaskName(), task.getIndustryType(), task.getTaskType(),
task.getTaskStatus(), resourceIds,
new TaskModelConfigResponse(task.getExtractModelConfigId(), extractModelConfigName,
task.getExtractModelName(),
task.getExtractModelUrl(), maskSecret(task.getExtractModelApiKey())),
new TaskModelConfigResponse(task.getVerifyModelConfigId(), verifyModelConfigName,
task.getVerifyModelName(),
task.getVerifyModelUrl(), maskSecret(task.getVerifyModelApiKey())),
new TaskPromptConfigResponse(task.getExtractPromptConfigId(), extractPromptConfigName,
task.getExtractPrompt()),
new TaskPromptConfigResponse(task.getVerifyPromptConfigId(), verifyPromptConfigName,
task.getVerifyPrompt()),
task.getCreatedAt(), task.getUpdatedAt());
}
private List<Long> resourceIds(List<SourceResource> resources) {
return resources.stream().map(SourceResource::getId).sorted().toList();
}
private List<Long> normalizeIds(List<Long> resourceIds) {
Set<Long> uniqueIds = new HashSet<>(resourceIds);
List<Long> sortedIds = new ArrayList<>(uniqueIds);
sortedIds.sort(Long::compareTo);
return sortedIds;
}
private void assertTaskPermission(LoginUser currentUser, AnnotationTask task) {
if (!dataPermissionService.canAccessCreator(currentUser, task.getCreatorId(), task.getCreatorRole())) {
throw new BusinessException(ResultCode.FORBIDDEN, "无权操作任务");
}
}
private IndustryType defaultIndustryType(IndustryType industryType) {
return industryType != null ? industryType : IndustryType.TRANSPORT;
}
private TaskType defaultTaskType(TaskType taskType) {
return taskType != null ? taskType : TaskType.EXTRACT_QA;
}
private String maskSecret(String secret) {
if (!StringUtils.hasText(secret)) {
return null;
}
if (secret.length() <= 4) {
return "****";
}
return "****" + secret.substring(secret.length() - 4);
}
private String resolveConfigName(Long configId) {
if (configId == null) {
return null;
}
SysConfig config = sysConfigMapper.selectById(configId);
return config != null ? config.getConfigName() : null;
}
}

View File

@@ -21,7 +21,7 @@ public class CompanyService {
public List<SysCompany> listCompanies(LoginUser currentUser) {
assertPlatformAdmin(currentUser);
return sysCompanyMapper.listAll();
return sysCompanyMapper.selectList(null);
}
public SysCompany createCompany(LoginUser currentUser, CreateCompanyRequest request) {
@@ -47,8 +47,8 @@ public class CompanyService {
}
private void assertPlatformAdmin(LoginUser currentUser) {
if (!currentUser.isPlatformAdmin()) {
throw new ForbiddenException("平台管理员可操作");
if (!currentUser.isSuperAdmin()) {
throw new ForbiddenException("系统管理员可操作");
}
}
}

View File

@@ -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<BizDataRecord> 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 <T> 数据类型
* @return 过滤后的数据列表
/**
* Generic in-memory role-based data filter for records already loaded in memory.
*/
public <T> List<T> filterByRole(
LoginUser currentUser,
List<T> allRecords,
Function<T, UserRole> roleExtractor,
Function<T, Long> 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<BizDataRecord> listVisibleRecordsGeneric(LoginUser currentUser, List<BizDataRecord> allRecords) {
// return filterByRole(
// currentUser,
// allRecords,
// BizDataRecord::
// BizDataRecord::getCreatorRole, // 提取创建者角色
// BizDataRecord::getCreatorId // 提取创建者ID
// );
// }
public List<String> 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<BizDataRecord> listVisibleRecords(LoginUser currentUser) {
List<BizDataRecord> allRecords = jdbcTemplate.query("""
select id, company_id, creator_id, creator_role, record_name, created_at, updated_at
from biz_data_record
where company_id = ?
order by id
""",
(rs, rowNum) -> BizDataRecord.builder()
.id(rs.getLong("id"))
.companyId(rs.getLong("company_id"))
.creatorId(rs.getLong("creator_id"))
.creatorRole(UserRole.valueOf(rs.getString("creator_role")))
.recordName(rs.getString("record_name"))
.createdAt(rs.getTimestamp("created_at") == null ? null : rs.getTimestamp("created_at").toLocalDateTime())
.updatedAt(rs.getTimestamp("updated_at") == null ? null : rs.getTimestamp("updated_at").toLocalDateTime())
.build(),
currentUser.companyId());
return filterByRole(currentUser, allRecords, BizDataRecord::getCreatorRole, BizDataRecord::getCreatorId);
}
}

View File

@@ -14,11 +14,11 @@ public class MenuService {
private final SysMenuMapper sysMenuMapper;
public List<MenuResponse> listCurrentMenus(LoginUser currentUser) {
List<String> positionCodes = java.util.Arrays.stream(com.labelsys.backend.enums.UserPosition.values())
List<String> 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();

View File

@@ -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());
}
}

View File

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

View File

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

View File

@@ -0,0 +1,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<SourceResourceResponse> pageResources(LoginUser currentUser, SourceResourcePageQuery query) {
List<String> allowedRoles = dataPermissionService.getAllowedRoles(currentUser);
boolean shouldFilterByUserId = dataPermissionService.shouldFilterByUserId(currentUser);
LambdaQueryWrapper<SourceResource> wrapper =
new LambdaQueryWrapper<SourceResource>().eq(SourceResource::getCompanyId, currentUser.companyId())
.eq(StringUtils.hasText(query.resourceType()), SourceResource::getResourceType, query.resourceType())
.eq(StringUtils.hasText(query.sourceStatus()), SourceResource::getSourceStatus, query.sourceStatus())
.like(StringUtils.hasText(query.keyword()), SourceResource::getResourceName, query.keyword());
if (shouldFilterByUserId) {
wrapper.eq(SourceResource::getCreatorId, currentUser.userId());
} else if (!allowedRoles.isEmpty()) {
wrapper.in(SourceResource::getCreatorRole, allowedRoles);
}
wrapper.orderByDesc(SourceResource::getCreatedAt);
Page<SourceResource> page = new Page<>(query.pageNo(), query.pageSize());
Page<SourceResource> resultPage = sourceResourceMapper.selectPage(page, wrapper);
List<SourceResourceResponse> 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";
};
}
}

View File

@@ -0,0 +1,278 @@
package com.labelsys.backend.service;
import java.util.List;
import java.util.Map;
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.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.request.UpdateSysConfigRequest;
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.ConfigMode;
import com.labelsys.backend.enums.ConfigType;
import com.labelsys.backend.enums.UserRole;
import com.labelsys.backend.mapper.SysConfigMapper;
import com.labelsys.backend.util.IdGenerator;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Service
@RequiredArgsConstructor
public class SysConfigService {
private final SysConfigMapper sysConfigMapper;
private final ObjectMapper objectMapper;
private final DataPermissionService dataPermissionService;
@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()).creatorRole(currentUser.role().name())
.build();
sysConfigMapper.insert(config);
log.info("saved sys config, companyId={}, userId={}, userRole={}, configName={}, configType={}",
currentUser.companyId(), currentUser.userId(), currentUser.role().name(), request.configName(),
request.configType());
return config;
}
@Transactional
public SysConfig updateConfig(LoginUser currentUser, Long configId, UpdateSysConfigRequest 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, "配置名称已存在");
// }
if (StringUtils.hasText(request.configName())) {
existing.setConfigName(request.configName());
}
if (StringUtils.hasText(request.configType())) {
existing.setConfigType(request.configType());
}
if (StringUtils.hasText(request.configValue())) {
existing.setConfigValue(request.configValue());
}
if (StringUtils.hasText(request.status())) {
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) {
SysConfig config = getConfigEntity(currentUser, configId);
if (!dataPermissionService.canAccessCreator(currentUser, config.getCreatorId(),
UserRole.valueOf(config.getCreatorRole()))) {
throw new BusinessException(ResultCode.FORBIDDEN, "无权访问配置");
}
return toResponse(config);
}
public PageResult<SysConfigResponse> pageConfigs(LoginUser currentUser, SysConfigPageQuery query) {
List<String> allowedRoles = dataPermissionService.getAllowedRoles(currentUser);
boolean shouldFilterByUserId = dataPermissionService.shouldFilterByUserId(currentUser);
LambdaQueryWrapper<SysConfig> wrapper = new LambdaQueryWrapper<SysConfig>()
.eq(SysConfig::getCompanyId, currentUser.companyId())
.eq(StringUtils.hasText(query.configType()), SysConfig::getConfigType, query.configType())
.eq(StringUtils.hasText(query.status()), SysConfig::getStatus, query.status())
.like(StringUtils.hasText(query.configName()), SysConfig::getConfigName, query.configName());
if (shouldFilterByUserId) {
wrapper.eq(SysConfig::getCreatorId, currentUser.userId());
} else if (!allowedRoles.isEmpty()) {
wrapper.in(SysConfig::getCreatorRole, allowedRoles);
}
wrapper.orderByDesc(SysConfig::getCreatedAt);
Page<SysConfig> page = new Page<>(query.pageNo(), query.pageSize());
Page<SysConfig> resultPage = sysConfigMapper.selectPage(page, wrapper);
List<SysConfigResponse> records = resultPage.getRecords().stream().map(this::toResponse).toList();
return new PageResult<>(records, resultPage.getTotal(), (int) resultPage.getCurrent(),
(int) resultPage.getSize());
}
@Transactional
public ResolvedModelConfig resolveModelConfig(LoginUser currentUser, TaskModelConfigRequest request) {
if (request == null || request.mode() == null) {
throw new BusinessException(ResultCode.BAD_REQUEST, "模型配置不能为空");
}
if (request.mode() == ConfigMode.SELECT) {
return resolveSelectedModel(currentUser, request.selectedConfigName());
}
if (request.mode() == ConfigMode.MANUAL) {
return resolveManualModel(currentUser, request.manualConfig());
}
throw new BusinessException(ResultCode.BAD_REQUEST, "不支持的模型配置模式");
}
public ResolvedPromptConfig resolvePromptConfig(LoginUser currentUser, PromptConfigOptionRequest request) {
if (request == null) {
throw new BusinessException(ResultCode.BAD_REQUEST, "提示词配置不能为空");
}
if (StringUtils.hasText(request.selectedConfigName())) {
SysConfig config = sysConfigMapper.findByCompanyIdAndConfigNameAndType(currentUser.companyId(),
request.selectedConfigName(), ConfigType.PROMPT.name());
if (config == null) {
throw new BusinessException(ResultCode.NOT_FOUND, "提示词配置不存在");
}
return new ResolvedPromptConfig(config.getId(), config.getConfigName(), config.getConfigValue());
}
if (!StringUtils.hasText(request.promptText())) {
throw new BusinessException(ResultCode.BAD_REQUEST, "提示词内容不能为空");
}
return new ResolvedPromptConfig(null, null, request.promptText());
}
public TaskModelConfigResponse toResponse(ResolvedModelConfig config) {
return new TaskModelConfigResponse(config.configId(), config.configName(), config.modelName(),
config.modelUrl(), maskSecret(config.apiKey()));
}
public TaskPromptConfigResponse toResponse(ResolvedPromptConfig config) {
return new TaskPromptConfigResponse(config.configId(), config.configName(), config.promptText());
}
public SysConfigResponse toResponse(SysConfig config) {
return new SysConfigResponse(config.getId(), config.getConfigType(), config.getConfigName(),
config.getConfigValue(), config.getStatus(), config.getCreatorId(), config.getCreatedAt(),
config.getUpdatedAt());
}
private ResolvedModelConfig resolveSelectedModel(LoginUser currentUser, String configName) {
if (!StringUtils.hasText(configName)) {
throw new BusinessException(ResultCode.BAD_REQUEST, "模型配置名称不能为空");
}
SysConfig config = sysConfigMapper.findByCompanyIdAndConfigNameAndType(currentUser.companyId(), configName,
ConfigType.MODEL.name());
if (config == null) {
throw new BusinessException(ResultCode.NOT_FOUND, "模型配置不存在");
}
ModelConfigValue configValue = parseModelConfig(config.getConfigValue());
return new ResolvedModelConfig(config.getId(), config.getConfigName(), configValue.modelName(),
configValue.modelUrl(), configValue.apiKey());
}
private ResolvedModelConfig resolveManualModel(LoginUser currentUser, ManualModelConfigRequest request) {
if (request == null || !StringUtils.hasText(request.modelName()) || !StringUtils.hasText(request.modelUrl())
|| !StringUtils.hasText(request.apiKey())) {
throw new BusinessException(ResultCode.BAD_REQUEST, "手动模型配置不完整");
}
SysConfig existing = sysConfigMapper.findByCompanyIdAndConfigName(currentUser.companyId(), request.modelName());
if (existing == null) {
String configValue = writeModelConfig(request);
SysConfig config = SysConfig.builder().id(IdGenerator.nextId()).companyId(currentUser.companyId())
.configType(ConfigType.MODEL.name()).configName(request.modelName()).configValue(configValue)
.status("ENABLED").creatorId(currentUser.userId()).build();
sysConfigMapper.insert(config);
log.info("auto created model config, companyId={}, userId={}, configName={}", currentUser.companyId(),
currentUser.userId(), request.modelName());
return new ResolvedModelConfig(config.getId(), config.getConfigName(), request.modelName(),
request.modelUrl(), request.apiKey());
}
if (!ConfigType.MODEL.name().equals(existing.getConfigType())) {
throw new BusinessException(ResultCode.CONFLICT, "同名配置已被其他类型占用");
}
ModelConfigValue configValue = parseModelConfig(existing.getConfigValue());
return new ResolvedModelConfig(existing.getId(), existing.getConfigName(), configValue.modelName(),
configValue.modelUrl(), configValue.apiKey());
}
private SysConfig getConfigEntity(LoginUser currentUser, Long configId) {
SysConfig config = sysConfigMapper.selectById(configId);
if (config == null || !currentUser.companyId().equals(config.getCompanyId())) {
throw new BusinessException(ResultCode.NOT_FOUND, "配置不存在");
}
return config;
}
private void validateConfigType(String configType) {
if (!ConfigType.isValid(configType)) {
throw new BusinessException(ResultCode.BAD_REQUEST, "配置类型非法");
}
}
private ModelConfigValue parseModelConfig(String value) {
try {
return objectMapper.readValue(value, ModelConfigValue.class);
} catch (JsonProcessingException ex) {
throw new BusinessException(ResultCode.BAD_REQUEST, "模型配置值格式非法");
}
}
private String writeModelConfig(ManualModelConfigRequest request) {
try {
return objectMapper.writeValueAsString(
Map.of("modelName", request.modelName(), "modelUrl", request.modelUrl(), "apiKey",
request.apiKey()));
} catch (JsonProcessingException ex) {
throw new BusinessException(ResultCode.BAD_REQUEST, "模型配置值生成失败");
}
}
private String maskSecret(String secret) {
if (!StringUtils.hasText(secret)) {
return null;
}
if (secret.length() <= 4) {
return "****";
}
return "****" + secret.substring(secret.length() - 4);
}
// private <T> PageResult<T> paginate(List<T> records, Integer pageNo, Integer
// pageSize) {
// int actualPageNo = pageNo == null || pageNo < 1 ? 1 : pageNo;
// int actualPageSize = pageSize == null || pageSize < 1 ? 10 : pageSize;
// int fromIndex = Math.min((actualPageNo - 1) * actualPageSize,
// records.size());
// int toIndex = Math.min(fromIndex + actualPageSize, records.size());
// return new PageResult<>(records.subList(fromIndex, toIndex),
// (long)records.size(), actualPageNo,
// actualPageSize);
// }
private record ModelConfigValue(String modelName, String modelUrl, String apiKey) {
}
public record ResolvedModelConfig(Long configId, String configName, String modelName, String modelUrl,
String apiKey) {
}
public record ResolvedPromptConfig(Long configId, String configName, String promptText) {
}
}

View File

@@ -1,10 +1,18 @@
package com.labelsys.backend.service;
import java.util.List;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
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.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;
@@ -18,11 +26,8 @@ import com.labelsys.backend.mapper.SysCompanyMapper;
import com.labelsys.backend.mapper.SysUserMapper;
import com.labelsys.backend.service.session.TokenSessionRepository;
import com.labelsys.backend.util.IdGenerator;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
@@ -35,23 +40,46 @@ public class UserService {
private final PasswordEncoder passwordEncoder;
private final TokenSessionRepository tokenSessionRepository;
public List<SysUser> listAllUsers(LoginUser currentUser) {
assertSystemAdmin(currentUser);
LambdaQueryWrapper wrapper = new LambdaQueryWrapper<SysUser>().orderByAsc(SysUser::getId);
return sysUserMapper.selectList(wrapper);
}
public List<SysUser> listCompanyUsers(LoginUser currentUser) {
assertCompanyAdmin(currentUser);
return sysUserMapper.listByCompanyId(currentUser.companyId());
LambdaQueryWrapper wrapper = new LambdaQueryWrapper<SysUser>()
.eq(SysUser::getCompanyId, currentUser.companyId()).orderByAsc(SysUser::getId);
return sysUserMapper.selectList(wrapper);
}
public List<SysUser> 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) {
@@ -60,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);
}
@@ -84,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, "用户不存在");
}
@@ -96,38 +125,35 @@ 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;
}
private void ensureEnabledCompany(Long companyId) {
SysCompany company = sysCompanyMapper.findById(companyId);
SysCompany company = sysCompanyMapper.selectById(companyId);
if (company == null || company.getStatus() != CompanyStatus.ENABLED) {
throw new BusinessException(ResultCode.NOT_FOUND, "公司不存在或已禁用");
}
}
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("仅公司管理员可操作");
}
}

View File

@@ -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);
}
}

View File

@@ -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:

View File

@@ -0,0 +1,50 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.labelsys.backend.mapper.AnnotationResultMapper">
<resultMap id="AnnotationResultResultMap" type="com.labelsys.backend.entity.AnnotationResult">
<id column="id" property="id"/>
<result column="company_id" property="companyId"/>
<result column="creator_id" property="creatorId"/>
<result column="creator_role" property="creatorRole"/>
<result column="task_id" property="taskId"/>
<result column="resource_id" property="resourceId"/>
<result column="qa_content_json" property="qaContentJson"/>
<result column="qa_content_storage_mode" property="qaContentStorageMode"/>
<result column="qa_content_file_path" property="qaContentFilePath"/>
<result column="diff_summary" property="diffSummary"/>
<result column="requires_manual_review" property="requiresManualReview"/>
<result column="is_deleted" property="isDeleted"/>
<result column="reviewer_id" property="reviewerId"/>
<result column="review_comment" property="reviewComment"/>
<result column="reviewed_at" property="reviewedAt"/>
<result column="created_at" property="createdAt"/>
<result column="updated_at" property="updatedAt"/>
</resultMap>
<sql id="AnnotationResultColumns">
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
</sql>
<select id="findActiveByIdAndCompanyId" resultMap="AnnotationResultResultMap">
select <include refid="AnnotationResultColumns"/>
from annotation_result
where id = #{id}
and company_id = #{companyId}
and is_deleted = false
limit 1
</select>
<update id="markArchived">
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
</update>
</mapper>

Some files were not shown because too many files have changed in this diff Show More