8 Commits

Author SHA1 Message Date
daf84cd6fe feat(ai): 增加AI模型样本管理功能
- 实现模型引用样本和获取关联样本功能
- 添加模型准确度分析和差异筛查功能
- 实现模型迭代复制机制
- 重构样本控制器支持批量更新和模型关联
- 优化样本响应VO结构增加标签和模型信息
- 完善模型和样本的数据访问层实现
- 集成翻译服务器进行模型分析验证
- 优化文件上传和下载功能实现
2026-03-05 18:32:03 +08:00
a8aa2a60fe feat(ai): 添加样本文件后缀支持和标签分组功能
- 在AiSampleDO中新增suffix字段存储文件后缀
- 在AiSampleRespVO中添加suffix响应字段
- 修改文件上传逻辑分离文件名和后缀并分别存储
- 新增根据标签ID获取分组列表的API接口
- 在标签服务中实现分组关联查询功能
- 修复标签分组关联关系创建和更新逻辑
- 优化标签分页查询添加软删除过滤条件
2026-03-04 12:50:10 +08:00
56656693c3 feat(ai-sample): 添加样本批量下载功能
- 在AiSampleController中新增/download接口支持批量下载
- 实现downloadSamples方法处理单个和多个样本下载逻辑
- 集成HttpUtil下载文件并使用ZipOutputStream打包多文件
- 添加文件服务器地址配置注入
2026-02-28 11:48:10 +08:00
2bd20404ba refactor(ai): 重构AI模块文件处理和模型管理功能
- 修改AbstractFileClient移除域名拼接逻辑
- 将getModel接口改为getEnabledModels批量获取已启用模型
- 更新AiSampleMapper查询语句改用LEFT JOIN并添加筛选条件
- 移除application-local.yaml中的file-server配置项
- 添加ImgJsonSerializer组件处理图片URL序列化
- 优化AiSampleServiceImpl文件上传逻辑
- 调整分页参数验证注解格式
2026-02-28 10:19:04 +08:00
4fbe69062c feat(ai): 更新文件服务依赖配置
- 将文件服务API依赖从 infraapi 模块迁移到 fileapi 模块
- 在 pom.xml 中添加 tashow-file-api 依赖
- 更新 application-local.yaml 配置文件中的文件服务配置
- 修改 RpcConfiguration 配置类中的文件API引用路径
- 添加外部服务配置项包括文件服务URL和AI翻译服务URL
2026-02-27 15:33:44 +08:00
5b5b827bb7 feat(ai): 添加AI模型管理和分段上传功能
- 创建AI模型版本管理表tz_ai_model并添加相关索引
- 添加AiModelController、AiModelService等完整的模型管理接口
- 实现模型创建、更新、删除、分页查询等功能
- 添加模型状态更新功能支持启用禁用等状态变更
- 在S3FileClient中实现分段上传uploadMultipart方法
- 扩展FileApi接口增加createFileMultipart分段上传接口
- 修改Nacos配置将命名空间从dev改为具体ID值
- 在SecurityConfiguration中开放AI模型管理接口权限
2026-02-27 15:17:11 +08:00
3438ea40a4 Merge branch 'refs/heads/feature/order' into feature/zijie 2026-02-27 11:02:30 +08:00
29cdf6c581 feat(ai): 添加AI模型管理和分段上传功能
- 创建AI模型版本管理表tz_ai_model并添加相关索引
- 添加AiModelController、AiModelService等完整的模型管理接口
- 实现模型创建、更新、删除、分页查询等功能
- 添加模型状态更新功能支持启用禁用等状态变更
- 在S3FileClient中实现分段上传uploadMultipart方法
- 扩展FileApi接口增加createFileMultipart分段上传接口
- 修改Nacos配置将命名空间从dev改为具体ID值
- 在SecurityConfiguration中开放AI模型管理接口权限
2026-02-27 11:01:44 +08:00
55 changed files with 1786 additions and 253 deletions

View File

@@ -111,3 +111,26 @@ CREATE TABLE `tz_ai_dialog_message`
) ENGINE = InnoDB COMMENT ='ai-对话消息表';
DROP TABLE IF EXISTS `tz_ai_model`;
CREATE TABLE `tz_ai_model`
(
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
`model_name` varchar(128) NOT NULL COMMENT '模型名称',
`version` varchar(32) NOT NULL COMMENT '版本号',
`load_percentage` decimal(5,4) NULL DEFAULT 0.0000 COMMENT '负载百分比',
`status` tinyint NOT NULL DEFAULT 0 COMMENT '状态0-禁用 1-启用 2-测试中 3-已废弃)',
`description` varchar(500) NULL DEFAULT '' COMMENT '版本描述',
`creator` varchar(64) NULL DEFAULT '' COMMENT '创建者',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updater` varchar(64) NULL DEFAULT '' COMMENT '更新者',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
`tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
PRIMARY KEY (`id`) USING BTREE,
INDEX `idx_model_name` (`model_name` ASC) USING BTREE,
INDEX `idx_version` (`version` ASC) USING BTREE,
INDEX `idx_status` (`status` ASC) USING BTREE,
INDEX `idx_create_time` (`create_time` ASC) USING BTREE
) ENGINE = InnoDB COMMENT = 'AI模型版本管理表';

View File

@@ -59,4 +59,10 @@ public interface FileApi {
*/
@PostMapping(PREFIX + "/create")
CommonResult<String> createFile(@Valid @RequestBody FileCreateReqDTO createReqDTO);
/**
* 分段上传文件
*/
@PostMapping(PREFIX + "/create-multipart")
CommonResult<String> createFileMultipart(@Valid @RequestBody FileCreateReqDTO createReqDTO);
}

View File

@@ -2,9 +2,11 @@ package com.tashow.cloud.fileapi.api.file.dto;
import jakarta.validation.constraints.NotEmpty;
import lombok.Data;
import lombok.experimental.Accessors;
/** RPC 服务 - 文件创建 Request DTO */
@Data
@Accessors(chain = true)
public class FileCreateReqDTO {
/** 原文件名称 */
@@ -13,9 +15,10 @@ public class FileCreateReqDTO {
/** 文件路径 */
private String path;
/**
* 文件内容
*/
/** 文件访问地址 */
private String url;
/** 文件内容 */
@NotEmpty(message = "文件内容不能为空")
private byte[] content;
}

View File

@@ -26,7 +26,7 @@ public class PageParam implements Serializable {
/**
* 页码,从 1 开始", example = "1
*/
@NotNull(message = "页码不能为空")
@NotNull(message = "页码不能为空" )
@Min(value = 1, message = "页码最小值为 1")
private Integer pageNo = 1;

View File

@@ -1,64 +1,50 @@
/*
* Copyright (c) 2018-2999 广州市蓝海创新科技有限公司 All rights reserved.
*
* https://www.mall4j.com/
*
* 未经允许不可做商业用途
*
* 版权所有侵权必究
*/
package com.tashow.cloud.common.util.serializer;
package com.tashow.cloud.common.serializer;
import cn.hutool.core.util.StrUtil;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* @author lanhai
* 图片URL序列化器
* 自动为相对路径的图片添加文件服务器前缀
*/
@Component
public class ImgJsonSerializer extends JsonSerializer<String> {
/* @Autowired
private Qiniu qiniu;
@Autowired
private ImgUploadUtil imgUploadUtil;*/
@Value("${file-server}")
private String fileServer;
@Override
public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
/*if (StrUtil.isBlank(value)) {
if (StrUtil.isBlank(value)) {
gen.writeString(StrUtil.EMPTY);
return;
}
String[] imgs = value.split(StrUtil.COMMA);
StringBuilder sb = new StringBuilder();
String resourceUrl = "";
String rule="^((http[s]{0,1})://)";
Pattern pattern= Pattern.compile(rule);
if (Objects.equals(imgUploadUtil.getUploadType(), 2)) {
resourceUrl = qiniu.getResourcesUrl();
} else if (Objects.equals(imgUploadUtil.getUploadType(), 1)) {
resourceUrl = imgUploadUtil.getResourceUrl();
}
String rule = "^((http[s]{0,1})://)";
Pattern pattern = Pattern.compile(rule);
resourceUrl = fileServer;
for (String img : imgs) {
Matcher matcher = pattern.matcher(img);
//若图片以http或https开头直接返回
if (matcher.find()){
if (matcher.find()) {
sb.append(img).append(StrUtil.COMMA);
}else {
} else {
sb.append(resourceUrl).append(img).append(StrUtil.COMMA);
}
}
sb.deleteCharAt(sb.length()-1);
gen.writeString(sb.toString());*/
sb.deleteCharAt(sb.length() - 1);
gen.writeString(sb.toString());
}
}

View File

@@ -7,12 +7,16 @@ spring:
username: nacos # Nacos 账号
password: nacos # Nacos 密码
discovery: # 【配置中心】配置项
namespace: dev # 命名空间。这里使用 dev 开发环境
namespace: 63caf548-313d-44bb-929c-531bf2f3b1a2 # 命名空间
group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP
metadata:
version: 1.0.0 # 服务实例的版本号,可用于灰度发布
config: # 【注册中心】配置项
namespace: dev # 命名空间。这里使用 dev 开发环境
namespace: 63caf548-313d-44bb-929c-531bf2f3b1a2 # 命名空间
group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP
# 日志文件配置
logging:
level:

View File

@@ -28,6 +28,11 @@
<groupId>com.tashow.cloud</groupId>
<artifactId>tashow-infra-api</artifactId>
</dependency>
<!--文件管理 - file-server-->
<dependency>
<groupId>com.tashow.cloud</groupId>
<artifactId>tashow-file-api</artifactId>
</dependency>
<!-- RPC 远程调用相关 -->
<dependency>
<groupId>com.tashow.cloud</groupId>

View File

@@ -1,20 +1,34 @@
package com.tashow.cloud.ai.controller.admin.aisample;
import com.tashow.cloud.ai.controller.admin.aisample.vo.*;
import com.tashow.cloud.ai.controller.admin.aisample.vo.AiSampleFileRespVO;
import com.tashow.cloud.ai.controller.admin.aisample.vo.AiSamplePageReqVO;
import com.tashow.cloud.ai.controller.admin.aisample.vo.AiSampleRelateModelVO;
import com.tashow.cloud.ai.controller.admin.aisample.vo.AiSampleRelateTagVO;
import com.tashow.cloud.ai.controller.admin.aisample.vo.AiSampleRespVO;
import com.tashow.cloud.ai.controller.admin.aisample.vo.AiSampleSaveReqVO;
import com.tashow.cloud.ai.controller.admin.aisample.vo.FileUploadReqVO;
import com.tashow.cloud.ai.dal.dataobject.aisample.AiSampleDO;
import com.tashow.cloud.ai.service.aisample.AiSampleService;
import com.tashow.cloud.common.pojo.CommonResult;
import com.tashow.cloud.common.pojo.PageResult;
import com.tashow.cloud.common.util.object.BeanUtils;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.annotation.security.PermitAll;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
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.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.io.IOException;
import java.util.List;
import static com.tashow.cloud.common.pojo.CommonResult.success;
@@ -31,14 +45,12 @@ public class AiSampleController {
@PostMapping("/create")
@Operation(summary = "创建样本库-上传文件")
@PermitAll
// @PreAuthorize("@ss.hasPermission('ai:sample:create')")
public CommonResult<List<AiSampleFileRespVO>> createAiSample(FileUploadReqVO uploadReqVO) {
return success(aiSampleService.createAiSample(uploadReqVO));
}
@PutMapping("/updates")
@Operation(summary = "更新样本")
// @PreAuthorize("@ss.hasPermission('ai:sample:update')")
@Operation(summary = "批量更新样本")
@PermitAll
public CommonResult<Boolean> updateAiSample(@Valid @RequestBody List<AiSampleSaveReqVO> updateReqVO) {
aiSampleService.updateAiSamples(updateReqVO);
@@ -47,7 +59,6 @@ public class AiSampleController {
@PutMapping("/relate")
@Operation(summary = "添加关联标签")
// @PreAuthorize("@ss.hasPermission('ai:sample:update')")
@PermitAll
public CommonResult<Boolean> relate(@Valid @RequestBody AiSampleRelateTagVO relateTagVO) {
aiSampleService.relate(relateTagVO);
@@ -56,7 +67,6 @@ public class AiSampleController {
@DeleteMapping("/deleteRelate")
@Operation(summary = "删除关联标签")
// @PreAuthorize("@ss.hasPermission('ai:sample:delete')")
@PermitAll
public CommonResult<Boolean> deleteRelate(@Valid @RequestBody AiSampleRelateTagVO relateTagVO) {
aiSampleService.deleteRelate(relateTagVO);
@@ -64,9 +74,8 @@ public class AiSampleController {
}
@DeleteMapping("/delete")
@Operation(summary = "删除样本")
@Operation(summary = "删除样本")
@Parameter(name = "id", description = "编号", required = true)
// @PreAuthorize("@ss.hasPermission('ai:sample:delete')")
@PermitAll
public CommonResult<Boolean> deleteAiSample(@RequestParam("id") String ids) {
aiSampleService.deleteAiSample(ids);
@@ -74,21 +83,36 @@ public class AiSampleController {
}
@GetMapping("/get")
@Operation(summary = "获得样本")
@Operation(summary = "获得样本")
@Parameter(name = "id", description = "编号", required = true, example = "1024")
// @PreAuthorize("@ss.hasPermission('ai:sample:query')")
@PermitAll
public CommonResult<AiSampleRespVO> getAiSample(@RequestParam("id") Long id) {
AiSampleDO aiSample = aiSampleService.getAiSample(id);
return success(BeanUtils.toBean(aiSample, AiSampleRespVO.class));
return success(aiSampleService.buildSampleRespVO(aiSampleService.getAiSample(id)));
}
@GetMapping("/page")
@Operation(summary = "获得样本分页")
// @PreAuthorize("@ss.hasPermission('ai:sample:query')")
@Operation(summary = "获得样本分页")
@PermitAll
public CommonResult<PageResult<AiSampleRespVO>> getAiSamplePage(@Valid AiSamplePageReqVO pageReqVO) {
PageResult<AiSampleDO> pageResult = aiSampleService.getAiSamplePage(pageReqVO);
return success(BeanUtils.toBean(pageResult, AiSampleRespVO.class));
List<AiSampleRespVO> list = pageResult.getList().stream().map(aiSampleService::buildSampleRespVO).toList();
return success(new PageResult<>(list, pageResult.getTotal()));
}
}
@GetMapping("/download")
@Operation(summary = "批量下载样本")
@Parameter(name = "ids", description = "样本ID列表逗号分隔", required = true, example = "1,2,3")
@PermitAll
public void downloadSamples(@RequestParam("ids") String ids, HttpServletResponse response) throws IOException {
aiSampleService.downloadSamples(ids, response);
}
@PutMapping("/relateModel")
@Operation(summary = "样本关联模型")
@PermitAll
public CommonResult<Boolean> relateModel(@Valid @RequestBody AiSampleRelateModelVO relateModelVO) {
aiSampleService.relateSamples(relateModelVO.getSampleIds(), relateModelVO.getModelIds());
return success(true);
}
}

View File

@@ -2,6 +2,7 @@ package com.tashow.cloud.ai.controller.admin.aisample;
import com.tashow.cloud.ai.controller.admin.aisample.vo.AiSampleTagListRespVO;
import com.tashow.cloud.ai.controller.admin.aisample.vo.AiSampleTagPageReqVO;
import com.tashow.cloud.ai.controller.admin.aisample.vo.AiSampleTagGroupRespVO;
import com.tashow.cloud.ai.controller.admin.aisample.vo.AiSampleTagSaveReqVO;
import com.tashow.cloud.ai.dal.dataobject.aisample.AiSampleTagDO;
import com.tashow.cloud.ai.service.aisample.AiSampleTagService;
@@ -66,6 +67,14 @@ public class AiSampleTagController {
return success(BeanUtils.toBean(aiSampleTagService.getAiSampleTagList(), AiSampleTagListRespVO.class));
}
@GetMapping("/group-list-by-tag-id")
@Operation(summary = "根据标签 id 获取分组")
@Parameter(name = "tagId", description = "标签 id", required = true)
@PermitAll
public CommonResult<List<AiSampleTagGroupRespVO>> getGroupListByTagId(@RequestParam("tagId") Long tagId) {
return success(BeanUtils.toBean(aiSampleTagService.getGroupListByTagId(tagId), AiSampleTagGroupRespVO.class));
}
@GetMapping("/page")
@Operation(summary = "获得样本标签库分页")
// @PreAuthorize("@ss.hasPermission('ai:sampleTag:query')")
@@ -75,4 +84,4 @@ public class AiSampleTagController {
return success(pageResult);
}
}
}

View File

@@ -1,6 +1,8 @@
package com.tashow.cloud.ai.controller.admin.aisample.vo;
import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.tashow.cloud.common.serializer.ImgJsonSerializer;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@@ -10,8 +12,10 @@ import lombok.Data;
public class AiSampleFileRespVO {
@Schema(description = "文件地址")
@JsonSerialize(using = ImgJsonSerializer.class)
private String fileUrl;
@Schema(description = "文件名称")
private String fileName;
}

View File

@@ -12,10 +12,10 @@ import lombok.ToString;
@ToString(callSuper = true)
public class AiSamplePageReqVO extends PageParam {
@Schema(description = "标签ids", example = "25839")
@Schema(description = "标签ids")
private String tagIds;
@Schema(description = "样本名称", example = "张三")
@Schema(description = "样本名称")
private String sampleName;
@Schema(description = "样本格式", example = "1")

View File

@@ -0,0 +1,18 @@
package com.tashow.cloud.ai.controller.admin.aisample.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
@Schema(description = "管理后台 - 样本关联模型 Request VO")
@Data
public class AiSampleRelateModelVO {
@Schema(description = "样本ID列表", requiredMode = Schema.RequiredMode.REQUIRED)
private List<Long> sampleIds;
@Schema(description = "模型ID列表", requiredMode = Schema.RequiredMode.REQUIRED)
private List<Long> modelIds;
}

View File

@@ -1,26 +1,44 @@
package com.tashow.cloud.ai.controller.admin.aisample.vo;
import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.tashow.cloud.ai.dal.dataobject.aisample.AiSampleTagDO;
import com.tashow.cloud.common.serializer.ImgJsonSerializer;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
import java.util.List;
@Schema(description = "管理后台 - 样本库 Response VO")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@ExcelIgnoreUnannotated
public class AiSampleRespVO {
@Schema(description = "主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "7701")
private Long id;
@Schema(description = "标签列表")
@Schema(description = "标签列表已废弃使用enumTags和customTags代替")
@Deprecated
private List<AiSampleTagDO> tags;
@Schema(description = "枚举标签列表(包含分组信息)")
private List<TagInfo> enumTags;
// @Schema(description = "个性标签列表")
// private List<TagInfo> customTags;
@Schema(description = "关联模型列表")
private List<ModelInfo> relatedModels;
@Schema(description = "样本文件地址")
@JsonSerialize(using = ImgJsonSerializer.class)
private String sampleFilePath;
@Schema(description = "样本名称", example = "张三")
@@ -45,4 +63,46 @@ public class AiSampleRespVO {
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private LocalDateTime updateTime;
@Schema(description = "文件后缀")
private String suffix;
@Schema(description = "标签信息")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class TagInfo {
@Schema(description = "标签ID")
private Long id;
@Schema(description = "标签名称")
private String tagName;
@Schema(description = "枚举值")
private String enumValue;
@Schema(description = "分组ID")
private Long groupId;
@Schema(description = "分组名称")
private String groupName;
}
@Schema(description = "模型信息")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class ModelInfo {
@Schema(description = "模型ID")
private Long id;
@Schema(description = "模型名称")
private String modelName;
@Schema(description = "版本号")
private String version;
}
}

View File

@@ -6,7 +6,7 @@ import lombok.*;
import java.time.LocalDateTime;
import com.alibaba.excel.annotation.*;
@Schema(description = "管理后台 - 样本标签分组库 Response VO")
@Schema(description = "管理后台 - 样本标签")
@Data
@ExcelIgnoreUnannotated
public class AiSampleTagGroupRespVO {
@@ -19,6 +19,10 @@ public class AiSampleTagGroupRespVO {
@ExcelProperty("分组名称")
private String groupName;
@Schema(description = "1-枚举型, 2-个性型", example = "1")
@ExcelProperty("分组类型")
private Integer groupType;
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
@ExcelProperty("创建时间")
private LocalDateTime createTime;

View File

@@ -3,7 +3,7 @@ package com.tashow.cloud.ai.controller.admin.aisample.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.*;
@Schema(description = "管理后台 - 样本标签分组库新增/修改 Request VO")
@Schema(description = "管理后台 - 样本标签分组库")
@Data
public class AiSampleTagGroupSaveReqVO {
@@ -13,4 +13,10 @@ public class AiSampleTagGroupSaveReqVO {
@Schema(description = "分组名称", example = "张三")
private String groupName;
@Schema(description = "分组类型1-枚举型, 2-个性型", example = "1")
private Integer groupType;
@Schema(description = "枚举值(枚举分组专用)", example = "")
private String enumValue;
}

View File

@@ -13,4 +13,8 @@ public class AiSampleTagListRespVO {
private Long id;
@Schema(description = "标签名称")
private String tagName;
@Schema(description = "标签类型1-枚举标签, 2-个性标签")
private Integer tagType;
@Schema(description = "枚举值")
private String enumValue;
}

View File

@@ -18,4 +18,13 @@ public class AiSampleTagSaveReqVO {
@Schema(description = "标签名称", example = "张三")
private String tagName;
@Schema(description = "标签类型1-枚举标签, 2-个性标签", example = "1")
private Integer tagType;
@Schema(description = "枚举值", example = "")
private String enumValue;
@Schema(description = "关联分组ID单个ID或逗号分隔的多个ID", example = "1")
private String groupId;
}

View File

@@ -0,0 +1,128 @@
package com.tashow.cloud.ai.controller.admin.model;
import com.tashow.cloud.ai.controller.admin.aisample.vo.AiSampleRespVO;
import com.tashow.cloud.ai.controller.admin.model.vo.AiModelAccuracyAnalysisReqVO;
import com.tashow.cloud.ai.controller.admin.model.vo.AiModelAnalysisResultVO;
import com.tashow.cloud.ai.controller.admin.model.vo.AiModelDifferenceVO;
import com.tashow.cloud.ai.controller.admin.model.vo.AiModelIterateReqVO;
import com.tashow.cloud.ai.controller.admin.model.vo.AiModelPageReqVO;
import com.tashow.cloud.ai.controller.admin.model.vo.AiModelReferenceSamplesReqVO;
import com.tashow.cloud.ai.controller.admin.model.vo.AiModelRespVO;
import com.tashow.cloud.ai.controller.admin.model.vo.AiModelSaveReqVO;
import com.tashow.cloud.ai.dal.dataobject.aisample.AiSampleDO;
import com.tashow.cloud.ai.dal.dataobject.model.AiModelDO;
import com.tashow.cloud.ai.service.aisample.AiSampleService;
import com.tashow.cloud.ai.service.model.AiModelService;
import com.tashow.cloud.common.pojo.CommonResult;
import com.tashow.cloud.common.pojo.PageResult;
import com.tashow.cloud.common.util.object.BeanUtils;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.annotation.security.PermitAll;
import jakarta.validation.Valid;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
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.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import static com.tashow.cloud.common.pojo.CommonResult.success;
@Tag(name = "管理后台 - AI模型管理")
@RestController
@RequestMapping("/ai/model")
@Validated
public class AiModelController {
@Resource
private AiModelService modelService;
@Resource
private AiSampleService aiSampleService;
@PostMapping("/create")
@Operation(summary = "创建模型")
@PermitAll
public CommonResult<Long> createModel(@Valid @RequestBody AiModelSaveReqVO createReqVO) {
return success(modelService.createModel(createReqVO));
}
@PutMapping("/update")
@Operation(summary = "更新模型")
@PermitAll
public CommonResult<Boolean> updateModel(@Valid @RequestBody AiModelSaveReqVO updateReqVO) {
modelService.updateModel(updateReqVO);
return success(true);
}
@DeleteMapping("/delete")
@Operation(summary = "删除模型")
@Parameter(name = "id", description = "编号", required = true)
@PermitAll
public CommonResult<Boolean> deleteModel(@RequestParam("id") Long id) {
modelService.deleteModel(id);
return success(true);
}
@GetMapping("/get")
@Operation(summary = "获取所有已启用的模型")
@PermitAll
public CommonResult<List<AiModelRespVO>> getEnabledModels() {
List<AiModelDO> models = modelService.getEnabledModels();
return success(BeanUtils.toBean(models, AiModelRespVO.class));
}
@GetMapping("/page")
@Operation(summary = "获得模型分页")
@PermitAll
public CommonResult<PageResult<AiModelRespVO>> getModelPage(@Valid AiModelPageReqVO pageReqVO) {
PageResult<AiModelDO> pageResult = modelService.getModelPage(pageReqVO);
return success(BeanUtils.toBean(pageResult, AiModelRespVO.class));
}
@PostMapping("/reference-samples")
@Operation(summary = "模型引用样本")
@PermitAll
public CommonResult<Boolean> referenceSamples(@Valid @RequestBody AiModelReferenceSamplesReqVO reqVO) {
modelService.referenceSamples(reqVO.getModelId(), reqVO.getSampleIds());
return success(true);
}
@GetMapping("/samples")
@Operation(summary = "获取模型关联样本")
@Parameter(name = "modelId", description = "模型ID", required = true)
@PermitAll
public CommonResult<List<AiSampleRespVO>> getModelSamples(@RequestParam Long modelId) {
List<AiSampleDO> samples = modelService.getModelSamples(modelId);
return success(samples.stream().map(aiSampleService::buildSampleRespVO).toList());
}
@PostMapping("/accuracy-analysis")
@Operation(summary = "模型准确度分析")
@PermitAll
public CommonResult<AiModelAnalysisResultVO> analyzeModel(@Valid @RequestBody AiModelAccuracyAnalysisReqVO reqVO) {
return success(modelService.analyzeModel(reqVO.getSampleIds(), reqVO.getModelIds()));
}
@GetMapping("/difference")
@Operation(summary = "模型样本差异筛查")
@PermitAll
public CommonResult<AiModelDifferenceVO> checkDifference(@RequestParam List<Long> modelIds) {
return success(modelService.checkDifference(modelIds));
}
@PostMapping("/iterate")
@Operation(summary = "模型迭代")
@PermitAll
public CommonResult<Long> iterateModel(@Valid @RequestBody AiModelIterateReqVO reqVO) {
return success(modelService.iterateModel(reqVO.getModelId(), reqVO.getNewVersion(), reqVO.getDescription()));
}
}

View File

@@ -0,0 +1,21 @@
package com.tashow.cloud.ai.controller.admin.model.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import lombok.Data;
import java.util.List;
@Schema(description = "管理后台 - 模型准确度分析 Request VO")
@Data
public class AiModelAccuracyAnalysisReqVO {
@Schema(description = "样本ID列表", requiredMode = Schema.RequiredMode.REQUIRED)
@NotEmpty(message = "样本ID列表不能为空")
private List<Long> sampleIds;
@Schema(description = "模型ID列表", requiredMode = Schema.RequiredMode.REQUIRED)
@NotEmpty(message = "模型ID列表不能为空")
private List<Long> modelIds;
}

View File

@@ -0,0 +1,68 @@
package com.tashow.cloud.ai.controller.admin.model.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
import java.util.List;
@Schema(description = "管理后台 - 模型准确度分析")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class AiModelAnalysisResultVO {
@Schema(description = "样本分析列表")
private List<SampleAnalysis> samples;
@Schema(description = "样本分析")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class SampleAnalysis {
@Schema(description = "样本ID")
private Long sampleId;
@Schema(description = "样本名称", example = "")
private String sampleName;
@Schema(description = "枚举标签", example = "")
private String enumTag;
@Schema(description = "模型分析结果列表")
private List<ModelAnalysisResult> modelResults;
}
@Schema(description = "模型分析结果")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class ModelAnalysisResult {
@Schema(description = "模型ID")
private Long modelId;
@Schema(description = "模型名称", example = "A01 模型")
private String modelName;
@Schema(description = "模型版本", example = "V1.0.0")
private String modelVersion;
@Schema(description = "物种识别结果", example = "")
private String speciesResult;
@Schema(description = "物种匹配度", example = "1.00")
private BigDecimal speciesMatchRate;
@Schema(description = "情绪识别结果", example = "兴奋")
private String emotionResult;
@Schema(description = "情绪匹配度", example = "0.95")
private BigDecimal emotionMatchRate;
}
}

View File

@@ -0,0 +1,24 @@
package com.tashow.cloud.ai.controller.admin.model.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@Schema(description = "管理后台 - 模型样本差异")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class AiModelDifferenceVO {
@Schema(description = "重复引用样本列表")
private List<AiSampleDetailVO> duplicatedSamples;
@Schema(description = "未重复引用样本列表")
private List<AiSampleDetailVO> uniqueSamples;
}

View File

@@ -0,0 +1,23 @@
package com.tashow.cloud.ai.controller.admin.model.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
@Schema(description = "管理后台 - 模型迭代 ")
@Data
public class AiModelIterateReqVO {
@Schema(description = "模型ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotNull(message = "模型ID不能为空")
private Long modelId;
@Schema(description = "新版本号", requiredMode = Schema.RequiredMode.REQUIRED, example = "v2.0")
@NotBlank(message = "新版本号不能为空")
private String newVersion;
@Schema(description = "版本描述", example = "优化算法性能")
private String description;
}

View File

@@ -0,0 +1,24 @@
package com.tashow.cloud.ai.controller.admin.model.vo;
import com.tashow.cloud.common.pojo.PageParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
@Schema(description = "管理后台 - AI模型分")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class AiModelPageReqVO extends PageParam {
@Schema(description = "模型名称", example = "宠物声音翻译")
private String modelName;
@Schema(description = "版本号", example = "v1.0.0")
private String version;
@Schema(description = "状态0-禁用 1-启用)", example = "1")
private Integer status;
}

View File

@@ -0,0 +1,22 @@
package com.tashow.cloud.ai.controller.admin.model.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.util.List;
@Schema(description = "管理后台 - 模型引用样")
@Data
public class AiModelReferenceSamplesReqVO {
@Schema(description = "模型ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotNull(message = "模型ID不能为空")
private Long modelId;
@Schema(description = "样本ID列表", requiredMode = Schema.RequiredMode.REQUIRED)
@NotEmpty(message = "样本ID列表不能为空")
private List<Long> sampleIds;
}

View File

@@ -0,0 +1,40 @@
package com.tashow.cloud.ai.controller.admin.model.vo;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - AI模型")
@Data
public class AiModelRespVO {
@Schema(description = "主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Long id;
@Schema(description = "模型名称", example = "模型")
private String modelName;
@Schema(description = "版本号", example = "v1.0.0")
private String version;
@Schema(description = "负载", example = "0.75")
private BigDecimal loadPercentage;
@Schema(description = "状态0-禁用 1-启用 2-测试中 3-已废弃)", example = "1")
private Integer status;
@Schema(description = "版本描述", example = "描述")
private String description;
@Schema(description = "创建时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private LocalDateTime createTime;
@Schema(description = "更新时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private LocalDateTime updateTime;
}

View File

@@ -0,0 +1,35 @@
package com.tashow.cloud.ai.controller.admin.model.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.math.BigDecimal;
@Schema(description = "管理后台 - AI模型")
@Data
public class AiModelSaveReqVO {
@Schema(description = "主键", example = "1")
private Long id;
@Schema(description = "模型名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "模型")
@NotBlank(message = "模型名称不能为空")
private String modelName;
@Schema(description = "版本号", requiredMode = Schema.RequiredMode.REQUIRED, example = "v1.0.0")
@NotBlank(message = "版本号不能为空")
private String version;
@Schema(description = "负载", example = "0.75")
private BigDecimal loadPercentage;
@Schema(description = "状态0-禁用 1-启用)", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotNull(message = "状态不能为空")
private Integer status;
@Schema(description = "版本描述", example = "描述")
private String description;
}

View File

@@ -0,0 +1,80 @@
package com.tashow.cloud.ai.controller.admin.model.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
/**
* 样本详情VO - 用于模型分析和差异筛查
*/
@Schema(description = "样本详情VO")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class AiSampleDetailVO {
@Schema(description = "样本ID")
private Long id;
@Schema(description = "样本名称")
private String sampleName;
@Schema(description = "样本文件地址")
private String sampleFilePath;
@Schema(description = "枚举标签列表")
private List<TagInfo> enumTags;
@Schema(description = "个性标签列表")
private List<TagInfo> customTags;
@Schema(description = "关联模型列表")
private List<ModelInfo> relatedModels;
/**
* 标签信息
*/
@Schema(description = "标签信息")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class TagInfo {
@Schema(description = "标签ID")
private Long id;
@Schema(description = "标签名称")
private String tagName;
@Schema(description = "枚举值")
private String enumValue;
@Schema(description = "分组名称")
private String groupName;
}
/**
* 模型信息
*/
@Schema(description = "模型信息")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class ModelInfo {
@Schema(description = "模型ID")
private Long id;
@Schema(description = "模型名称")
private String modelName;
@Schema(description = "版本号")
private String version;
}
}

View File

@@ -4,6 +4,8 @@ import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.tashow.cloud.common.serializer.ImgJsonSerializer;
import com.tashow.cloud.mybatis.mybatis.core.dataobject.BaseDO;
import lombok.*;
@@ -57,5 +59,9 @@ public class AiSampleDO extends BaseDO {
* 样本注释
*/
private String remark;
/**
*后缀)
*/
private String suffix;
}

View File

@@ -1,8 +1,8 @@
package com.tashow.cloud.ai.dal.dataobject.aisample;
import lombok.*;
import com.baomidou.mybatisplus.annotation.*;
import com.tashow.cloud.mybatis.mybatis.core.dataobject.BaseDO;
import lombok.*;
/**
* 样本标签库 DO
@@ -30,5 +30,19 @@ public class AiSampleTagDO extends BaseDO {
*/
private String tagName;
/**
* 枚举值
*/
private String enumValue;
/**
* 标签类型1-枚举标签, 2-个性标签
*/
private Integer tagType;
/**
* 关联分组ID单个ID或逗号分隔的多个ID
*/
private String groupId;
}

View File

@@ -31,4 +31,14 @@ public class AiSampleTagGroupDO extends BaseDO {
*/
private String groupName;
/**
* 分组类型1-枚举型, 2-个性型
*/
private Integer groupType;
/**
* 枚举值
*/
private String enumValue;
}

View File

@@ -0,0 +1,59 @@
package com.tashow.cloud.ai.dal.dataobject.model;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.tashow.cloud.mybatis.mybatis.core.dataobject.BaseDO;
import lombok.*;
import java.math.BigDecimal;
/**
* AI模型管理 DO
*
* @author tashow
*/
@TableName("tz_ai_model")
@KeySequence("tz_ai_model_seq")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class AiModelDO extends BaseDO {
/**
* 主键
*/
@TableId
private Long id;
/**
* 模型名称
*/
private String modelName;
/**
* 版本号
*/
private String version;
/**
* 负载百分比0.0-1.0
*/
private BigDecimal loadPercentage;
/**
* 状态0-禁用 1-启用 2-测试中 3-已废弃)
*/
private Integer status;
/**
* 版本描述
*/
private String description;
}

View File

@@ -0,0 +1,40 @@
package com.tashow.cloud.ai.dal.dataobject.model;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.tashow.cloud.mybatis.mybatis.core.dataobject.BaseDO;
import lombok.*;
/**
* 模型样本关联 DO
*
* @author tashow
*/
@TableName("tz_ai_model_sample_relate")
@KeySequence("tz_ai_model_sample_relate_seq")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class AiModelSampleRelateDO extends BaseDO {
/**
* 主键
*/
@TableId
private Long id;
/**
* 模型ID
*/
private Long modelId;
/**
* 样本ID
*/
private Long sampleId;
}

View File

@@ -28,14 +28,18 @@ public interface AiSampleMapper extends BaseMapperX<AiSampleDO> {
",sample_mine_type sampleMineType ,sample_size sampleSize, remark,creator,updater,create_time createTime" +
",update_time updateTime,deleted,tenant_id tenantId " +
"FROM tz_ai_sample t " +
"INNER JOIN tz_ai_sample_tag_relate r ON t.id = r.sample_id " +
"LEFt JOIN tz_ai_sample_tag_relate r ON t.id = r.sample_id " +
"<where>" +
"AND t.deleted =0"+
" <if test=\"pageReqVO.tagIds != null and pageReqVO.tagIds != ''\">" +
" AND FIND_IN_SET(r.sample_tag_id,${pageReqVO.tagIds}) " +
" AND FIND_IN_SET(r.sample_tag_id, '${pageReqVO.tagIds}') " +
" </if>" +
" <if test=\"pageReqVO.sampleName != null and pageReqVO.sampleName != ''\">" +
" AND t.sample_name LIKE CONCAT('%',#{pageReqVO.sampleName},'%')" +
" </if>" +
" <if test=\"pageReqVO.sampleMineType != null and pageReqVO.sampleMineType != ''\">" +
" AND t.sample_mine_type LIKE CONCAT('%',#{pageReqVO.sampleMineType},'%')" +
" </if>" +
"</where>" +
"GROUP BY t.id ORDER BY t.id DESC" +
"</script>")

View File

@@ -24,12 +24,12 @@ public interface AiSampleTagMapper extends BaseMapperX<AiSampleTagDO> {
}
@Select("<script>" +
"SELECT t.id, t.tag_name tagName " +
"SELECT t.id, t.tag_name tagName, t.tag_type tagType, t.enum_value enumValue, t.group_id groupId " +
"FROM tz_ai_sample_tag t " +
"INNER JOIN tz_ai_sample_tag_group_relate r ON t.id = r.sample_tag_id " +
"<where>" +
" t.deleted = 0" +
" <if test=\"pageReqVO.groupId != null\">" +
" AND r.sample_tag_group_id = #{pageReqVO.groupId}" +
" AND (t.group_id IS NULL OR FIND_IN_SET(#{pageReqVO.groupId}, t.group_id))" +
" </if>" +
" <if test=\"pageReqVO.tagName != null and pageReqVO.tagName != ''\">" +
" AND t.tag_name LIKE CONCAT('%',#{pageReqVO.tagName},'%')" +

View File

@@ -0,0 +1,26 @@
package com.tashow.cloud.ai.dal.mysql.model;
import com.tashow.cloud.ai.controller.admin.model.vo.AiModelPageReqVO;
import com.tashow.cloud.ai.dal.dataobject.model.AiModelDO;
import com.tashow.cloud.common.pojo.PageResult;
import com.tashow.cloud.mybatis.mybatis.core.mapper.BaseMapperX;
import com.tashow.cloud.mybatis.mybatis.core.query.LambdaQueryWrapperX;
import org.apache.ibatis.annotations.Mapper;
/**
* AI模型管理 Mapper
*
* @author tashow
*/
@Mapper
public interface AiModelMapper extends BaseMapperX<AiModelDO> {
default PageResult<AiModelDO> selectPage(AiModelPageReqVO reqVO) {
return selectPage(reqVO, new LambdaQueryWrapperX<AiModelDO>()
.likeIfPresent(AiModelDO::getModelName, reqVO.getModelName())
.eqIfPresent(AiModelDO::getVersion, reqVO.getVersion())
.eqIfPresent(AiModelDO::getStatus, reqVO.getStatus())
.orderByDesc(AiModelDO::getCreateTime));
}
}

View File

@@ -0,0 +1,15 @@
package com.tashow.cloud.ai.dal.mysql.model;
import com.tashow.cloud.ai.dal.dataobject.model.AiModelSampleRelateDO;
import com.tashow.cloud.mybatis.mybatis.core.mapper.BaseMapperX;
import org.apache.ibatis.annotations.Mapper;
/**
* 模型样本关联 Mapper
*
* @author tashow
*/
@Mapper
public interface AiModelSampleRelateMapper extends BaseMapperX<AiModelSampleRelateDO> {
}

View File

@@ -1,6 +1,6 @@
package com.tashow.cloud.ai.framework.rpc.config;
import com.tashow.cloud.infraapi.api.file.FileApi;
import com.tashow.cloud.fileapi.api.file.FileApi;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Configuration;

View File

@@ -3,8 +3,10 @@ package com.tashow.cloud.ai.service.aisample;
import com.tashow.cloud.ai.controller.admin.aisample.vo.*;
import com.tashow.cloud.ai.dal.dataobject.aisample.AiSampleDO;
import com.tashow.cloud.common.pojo.PageResult;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import java.io.IOException;
import java.util.List;
/**
@@ -69,5 +71,28 @@ public interface AiSampleService {
*/
PageResult<AiSampleDO> getAiSamplePage(AiSamplePageReqVO pageReqVO);
/**
* 批量下载样本
*
* @param ids 样本ID列表逗号分隔
* @param response HTTP响应
*/
void downloadSamples(String ids, HttpServletResponse response) throws IOException;
}
/**
* 样本关联模型(覆写逻辑)
*
* @param sampleIds 样本ID列表
* @param modelIds 模型ID列表
*/
void relateSamples(List<Long> sampleIds, List<Long> modelIds);
/**
* 构建样本响应VO包含枚举标签、个性标签、关联模型
*
* @param sample 样本DO
* @return 样本响应VO
*/
AiSampleRespVO buildSampleRespVO(AiSampleDO sample);
}

View File

@@ -1,37 +1,52 @@
package com.tashow.cloud.ai.service.aisample;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.tashow.cloud.ai.controller.admin.aisample.vo.*;
import com.tashow.cloud.ai.controller.admin.aisample.vo.AiSampleFileRespVO;
import com.tashow.cloud.ai.controller.admin.aisample.vo.AiSamplePageReqVO;
import com.tashow.cloud.ai.controller.admin.aisample.vo.AiSampleRelateTagVO;
import com.tashow.cloud.ai.controller.admin.aisample.vo.AiSampleRespVO;
import com.tashow.cloud.ai.controller.admin.aisample.vo.AiSampleSaveReqVO;
import com.tashow.cloud.ai.controller.admin.aisample.vo.FileUploadReqVO;
import com.tashow.cloud.ai.dal.dataobject.aisample.AiSampleDO;
import com.tashow.cloud.ai.dal.dataobject.aisample.AiSampleTagDO;
import com.tashow.cloud.ai.dal.dataobject.aisample.AiSampleTagGroupDO;
import com.tashow.cloud.ai.dal.dataobject.aisample.AiSampleTagRelateDO;
import com.tashow.cloud.ai.dal.dataobject.model.AiModelSampleRelateDO;
import com.tashow.cloud.ai.dal.mysql.aisample.AiSampleMapper;
import com.tashow.cloud.ai.dal.mysql.aisample.AiSampleTagGroupMapper;
import com.tashow.cloud.ai.dal.mysql.aisample.AiSampleTagMapper;
import com.tashow.cloud.ai.dal.mysql.aisample.AiSampleTagRelateMapper;
import com.tashow.cloud.ai.dal.mysql.model.AiModelMapper;
import com.tashow.cloud.ai.dal.mysql.model.AiModelSampleRelateMapper;
import com.tashow.cloud.common.pojo.PageResult;
import com.tashow.cloud.common.util.object.BeanUtils;
import com.tashow.cloud.infraapi.api.file.FileApi;
import com.tashow.cloud.fileapi.api.file.FileApi;
import com.tashow.cloud.mybatis.mybatis.core.util.MyBatisUtils;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
import lombok.SneakyThrows;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
/**
* 样本库 Service 实现类
*
* @author 芋道源码
*/
@Service
@Validated
public class AiSampleServiceImpl implements AiSampleService {
@@ -43,39 +58,41 @@ public class AiSampleServiceImpl implements AiSampleService {
@Resource
private AiSampleTagRelateMapper aiSampleTagRelateMapper;
@Resource
private AiSampleTagGroupMapper aiSampleTagGroupMapper;
@Resource
private AiModelSampleRelateMapper modelSampleRelateMapper;
@Resource
private AiModelMapper modelMapper;
@Resource
private FileApi fileApi;
@Value("file-server")
@Value("${file-server}")
private String fileServer;
@Override
@SneakyThrows
public List<AiSampleFileRespVO> createAiSample(FileUploadReqVO uploadReqVO) {
//返回图片路径
List<AiSampleFileRespVO> urls = new ArrayList<>();
/* 调用文件上传服务*/
List<AiSampleFileRespVO> result = new ArrayList<>();
for (MultipartFile file : uploadReqVO.getFiles()) {
//返回上传结果
String file1 = fileServer + fileApi.createFile(file.getBytes());
//保存样本信息
AiSampleDO aiSampleDO = new AiSampleDO();
aiSampleDO.setSampleFilePath(file1);
aiSampleDO.setSampleName(file.getOriginalFilename());
aiSampleDO.setSampleMineType(file.getContentType());
aiSampleDO.setSampleSize(file.getSize());
aiSampleMapper.insert(aiSampleDO);
urls.add(new AiSampleFileRespVO().setFileUrl(file1).setFileName(file.getOriginalFilename()));
String filename = file.getOriginalFilename();
String filePath = fileApi.createFile(filename, "", file.getBytes());
AiSampleDO sample = new AiSampleDO();
sample.setSampleFilePath(filePath);
sample.setSampleName(filename.substring(0, filename.lastIndexOf('.')));
sample.setSuffix(filename.substring(filename.lastIndexOf('.') + 1));
sample.setSampleMineType(file.getContentType());
sample.setSampleSize(file.getSize());
aiSampleMapper.insert(sample);
result.add(new AiSampleFileRespVO().setFileUrl(filePath).setFileName(filename));
}
// 返回
return urls;
return result;
}
@Override
public void updateAiSample(AiSampleSaveReqVO updateReqVO) {
// 校验存在
validateAiSampleExists(updateReqVO.getId());
// 更新
AiSampleDO updateObj = BeanUtils.toBean(updateReqVO, AiSampleDO.class);
aiSampleMapper.updateById(updateObj);
aiSampleMapper.updateById(BeanUtils.toBean(updateReqVO, AiSampleDO.class));
}
@Override
@@ -85,96 +102,232 @@ public class AiSampleServiceImpl implements AiSampleService {
@Override
public void relate(AiSampleRelateTagVO relateTagVO) {
List<AiSampleTagRelateDO> tagRelateDOS = new ArrayList<>();
List<AiSampleTagRelateDO> relates = new ArrayList<>();
for (Long sampleId : relateTagVO.getSampleIds()) {
for (Long tagId : relateTagVO.getTagId()) {
AiSampleTagRelateDO relateDO = aiSampleTagRelateMapper.selectOne(
new LambdaQueryWrapper<AiSampleTagRelateDO>()
.eq(AiSampleTagRelateDO::getSampleId, sampleId)
.eq(AiSampleTagRelateDO::getSampleTagId, tagId) );
if (relateDO== null){
relateDO = new AiSampleTagRelateDO();
relateDO.setSampleId(sampleId);
relateDO.setSampleTagId(tagId);
tagRelateDOS.add(relateDO);
validateTagGroupRule(sampleId, tagId);
AiSampleTagRelateDO exist = aiSampleTagRelateMapper.selectOne(new LambdaQueryWrapper<AiSampleTagRelateDO>()
.eq(AiSampleTagRelateDO::getSampleId, sampleId)
.eq(AiSampleTagRelateDO::getSampleTagId, tagId));
if (exist == null) {
relates.add(new AiSampleTagRelateDO().setSampleId(sampleId).setSampleTagId(tagId));
}
}
}
if (!tagRelateDOS.isEmpty()){
aiSampleTagRelateMapper.insertBatch(tagRelateDOS);
if (CollUtil.isNotEmpty(relates)) {
aiSampleTagRelateMapper.insertBatch(relates);
}
}
@Override
public void deleteRelate(AiSampleRelateTagVO relateTagVO) {
List<Long> tagRelateIds = new ArrayList<>();
List<Long> ids = new ArrayList<>();
for (Long sampleId : relateTagVO.getSampleIds()) {
for (Long tagId : relateTagVO.getTagId()) {
AiSampleTagRelateDO relateDO = aiSampleTagRelateMapper.selectOne(
new LambdaQueryWrapper<AiSampleTagRelateDO>()
.eq(AiSampleTagRelateDO::getSampleId, sampleId)
.eq(AiSampleTagRelateDO::getSampleTagId, tagId)
);
if (relateDO != null) {
tagRelateIds.add(relateDO.getId());
AiSampleTagRelateDO exist = aiSampleTagRelateMapper.selectOne(new LambdaQueryWrapper<AiSampleTagRelateDO>()
.eq(AiSampleTagRelateDO::getSampleId, sampleId)
.eq(AiSampleTagRelateDO::getSampleTagId, tagId));
if (exist != null) {
ids.add(exist.getId());
}
}
}
if (!tagRelateIds.isEmpty()) {
aiSampleTagRelateMapper.deleteBatchIds(tagRelateIds);
if (CollUtil.isNotEmpty(ids)) {
aiSampleTagRelateMapper.deleteBatchIds(ids);
}
}
@Override
public void deleteAiSample(String ids) {
// 删除
aiSampleMapper.deleteByIds(Arrays.asList(ids.split(StrUtil.COMMA)));
}
private void validateAiSampleExists(Long id) {
if (aiSampleMapper.selectById(id) == null) {
// throw exception(AI_SAMPLE_NOT_EXISTS);
}
}
@Override
public AiSampleDO getAiSample(Long id) {
AiSampleDO aiSampleDO = aiSampleMapper.selectById(id);
aiSampleDO.setSampleFilePath(aiSampleDO.getSampleFilePath());
//先获取关联的标签id
List<AiSampleTagRelateDO> tagRelateDOS = aiSampleTagRelateMapper.selectList(new LambdaQueryWrapper<AiSampleTagRelateDO>().eq(AiSampleTagRelateDO::getSampleId, id));
List<Long> tagIds = tagRelateDOS.stream().map(AiSampleTagRelateDO::getSampleTagId).toList();
aiSampleDO.setTags(aiSampleTagMapper.selectList(new LambdaQueryWrapper<AiSampleTagDO>().in(AiSampleTagDO::getId, tagIds)));
return aiSampleDO;
AiSampleDO sample = aiSampleMapper.selectById(id);
if (sample == null) {
return null;
}
List<Long> tagIds = aiSampleTagRelateMapper.selectList(new LambdaQueryWrapper<AiSampleTagRelateDO>()
.eq(AiSampleTagRelateDO::getSampleId, id))
.stream().map(AiSampleTagRelateDO::getSampleTagId).toList();
sample.setTags(CollUtil.isEmpty(tagIds)
? List.of()
: aiSampleTagMapper.selectList(new LambdaQueryWrapper<AiSampleTagDO>().in(AiSampleTagDO::getId, tagIds)));
return sample;
}
@Override
public PageResult<AiSampleDO> getAiSamplePage(AiSamplePageReqVO pageReqVO) {
// PageResult<AiSampleDO> aiSampleDOPageResult = aiSampleMapper.selectPage(pageReqVO);
IPage<AiSampleDO> aiSampleDOPageResult = aiSampleMapper.getAiSamplePage(MyBatisUtils.buildPage(pageReqVO),pageReqVO);
//根据样本id获取关联的标签id
List<Long> sampleIds = aiSampleDOPageResult.getRecords().stream().map(AiSampleDO::getId).toList();
List<AiSampleTagRelateDO> tagRelateDOS = aiSampleTagRelateMapper.selectList(
new LambdaQueryWrapper<AiSampleTagRelateDO>()
.in(!sampleIds.isEmpty(), AiSampleTagRelateDO::getSampleId, sampleIds));
List<Long> tagIds = tagRelateDOS.stream().map(AiSampleTagRelateDO::getSampleTagId).toList();
//获取标签信息
List<AiSampleTagDO> aiSampleTagDOS = aiSampleTagMapper.selectList(
new LambdaQueryWrapper<AiSampleTagDO>()
.in(!tagIds.isEmpty(), AiSampleTagDO::getId, tagIds));
//封装标签信息
for (AiSampleDO aiSampleDO : aiSampleDOPageResult.getRecords()) {
List<AiSampleTagRelateDO> list = tagRelateDOS.stream()
.filter(a -> ObjectUtil.equals(aiSampleDO.getId(), a.getSampleId()))
.toList();
Object[] tagsId = list.stream().map(AiSampleTagRelateDO::getSampleTagId).toArray();
List<AiSampleTagDO> list1 = aiSampleTagDOS.stream().filter(a -> ArrayUtil.containsAny(tagsId, a.getId())).toList();
aiSampleDO.setTags(list1);
aiSampleDO.setSampleFilePath(aiSampleDO.getSampleFilePath());
IPage<AiSampleDO> page = aiSampleMapper.getAiSamplePage(MyBatisUtils.buildPage(pageReqVO), pageReqVO);
List<AiSampleDO> samples = page.getRecords();
List<Long> sampleIds = samples.stream().map(AiSampleDO::getId).toList();
if (CollUtil.isEmpty(sampleIds)) {
return new PageResult<>(samples, page.getTotal());
}
return new PageResult<>(aiSampleDOPageResult.getRecords(), aiSampleDOPageResult.getTotal());
List<AiSampleTagRelateDO> relates = aiSampleTagRelateMapper.selectList(new LambdaQueryWrapper<AiSampleTagRelateDO>()
.in(AiSampleTagRelateDO::getSampleId, sampleIds));
List<Long> tagIds = relates.stream().map(AiSampleTagRelateDO::getSampleTagId).distinct().toList();
List<AiSampleTagDO> tags = CollUtil.isEmpty(tagIds)
? List.of()
: aiSampleTagMapper.selectList(new LambdaQueryWrapper<AiSampleTagDO>().in(AiSampleTagDO::getId, tagIds));
Map<Long, AiSampleTagDO> tagMap = new HashMap<>();
for (AiSampleTagDO tag : tags) {
tagMap.put(tag.getId(), tag);
}
Map<Long, List<AiSampleTagDO>> sampleTagMap = new HashMap<>();
for (AiSampleTagRelateDO relate : relates) {
AiSampleTagDO tag = tagMap.get(relate.getSampleTagId());
if (tag != null) {
sampleTagMap.computeIfAbsent(relate.getSampleId(), k -> new ArrayList<>()).add(tag);
}
}
for (AiSampleDO sample : samples) {
sample.setTags(sampleTagMap.getOrDefault(sample.getId(), List.of()));
}
return new PageResult<>(samples, page.getTotal());
}
}
@Override
public void downloadSamples(String ids, HttpServletResponse response) throws IOException {
List<AiSampleDO> samples = aiSampleMapper.selectBatchIds(Arrays.asList(ids.split(StrUtil.COMMA)));
if (samples.size() == 1) {
AiSampleDO sample = samples.get(0);
byte[] bytes = HttpUtil.downloadBytes(fileServer + sample.getSampleFilePath());
response.setContentType(sample.getSampleMineType());
response.setHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode(sample.getSampleName(), "UTF-8"));
response.getOutputStream().write(bytes);
return;
}
response.setContentType("application/zip");
response.setHeader("Content-Disposition", "attachment; filename=samples.zip");
try (ZipOutputStream zos = new ZipOutputStream(response.getOutputStream())) {
for (AiSampleDO sample : samples) {
byte[] bytes = HttpUtil.downloadBytes(fileServer + sample.getSampleFilePath());
zos.putNextEntry(new ZipEntry(sample.getSampleName()));
zos.write(bytes);
zos.closeEntry();
}
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public void relateSamples(List<Long> sampleIds, List<Long> modelIds) {
if (CollUtil.isEmpty(sampleIds)) {
return;
}
boolean noModels = CollUtil.isEmpty(modelIds);
for (Long sampleId : sampleIds) {
modelSampleRelateMapper.delete(new LambdaQueryWrapper<AiModelSampleRelateDO>()
.eq(AiModelSampleRelateDO::getSampleId, sampleId));
if (noModels) {
continue;
}
for (Long modelId : modelIds) {
modelSampleRelateMapper.insert(new AiModelSampleRelateDO().setSampleId(sampleId).setModelId(modelId));
}
}
}
private void validateTagGroupRule(Long sampleId, Long tagId) {
AiSampleTagDO currentTag = aiSampleTagMapper.selectById(tagId);
if (currentTag == null || !Integer.valueOf(1).equals(currentTag.getTagType())) {
return;
}
Long groupId = getFirstGroupId(currentTag.getGroupId());
if (groupId == null) {
return;
}
AiSampleTagGroupDO group = aiSampleTagGroupMapper.selectById(groupId);
if (group != null && Integer.valueOf(2).equals(group.getGroupType())) {
return;
}
List<AiSampleTagRelateDO> existingRelates = aiSampleTagRelateMapper.selectList(new LambdaQueryWrapper<AiSampleTagRelateDO>()
.eq(AiSampleTagRelateDO::getSampleId, sampleId));
for (AiSampleTagRelateDO relate : existingRelates) {
if (tagId.equals(relate.getSampleTagId())) {
continue;
}
AiSampleTagDO existingTag = aiSampleTagMapper.selectById(relate.getSampleTagId());
if (existingTag != null && parseGroupIds(existingTag.getGroupId()).contains(groupId)) {
aiSampleTagRelateMapper.deleteById(relate.getId());
}
}
}
@Override
public AiSampleRespVO buildSampleRespVO(AiSampleDO sample) {
AiSampleRespVO respVO = BeanUtils.toBean(sample, AiSampleRespVO.class);
respVO.setEnumTags(buildEnumTags(sample.getId()));
respVO.setRelatedModels(buildRelatedModels(sample.getId()));
return respVO;
}
private List<AiSampleRespVO.TagInfo> buildEnumTags(Long sampleId) {
List<Long> tagIds = aiSampleTagRelateMapper.selectList(new LambdaQueryWrapper<AiSampleTagRelateDO>()
.eq(AiSampleTagRelateDO::getSampleId, sampleId))
.stream().map(AiSampleTagRelateDO::getSampleTagId).toList();
if (CollUtil.isEmpty(tagIds)) {
return List.of();
}
List<AiSampleTagDO> tags = aiSampleTagMapper.selectBatchIds(tagIds).stream()
.filter(tag -> Integer.valueOf(1).equals(tag.getTagType())).toList();
Map<Long, String> groupNames = new HashMap<>();
Set<Long> groupIds = new HashSet<>();
for (AiSampleTagDO tag : tags) {
Long groupId = getFirstGroupId(tag.getGroupId());
if (groupId != null) {
groupIds.add(groupId);
}
}
for (AiSampleTagGroupDO group : aiSampleTagGroupMapper.selectBatchIds(groupIds)) {
groupNames.put(group.getId(), group.getGroupName());
}
return tags.stream().map(tag -> {
Long groupId = getFirstGroupId(tag.getGroupId());
return AiSampleRespVO.TagInfo.builder()
.id(tag.getId())
.tagName(tag.getTagName())
.enumValue(tag.getEnumValue())
.groupId(groupId)
.groupName(groupId == null ? null : groupNames.get(groupId))
.build();
}).toList();
}
private List<AiSampleRespVO.ModelInfo> buildRelatedModels(Long sampleId) {
List<Long> modelIds = modelSampleRelateMapper.selectList(new LambdaQueryWrapper<AiModelSampleRelateDO>()
.eq(AiModelSampleRelateDO::getSampleId, sampleId))
.stream().map(AiModelSampleRelateDO::getModelId).toList();
if (CollUtil.isEmpty(modelIds)) {
return List.of();
}
return modelMapper.selectBatchIds(modelIds).stream()
.map(model -> AiSampleRespVO.ModelInfo.builder()
.id(model.getId())
.modelName(model.getModelName())
.version(model.getVersion())
.build())
.toList();
}
private Long getFirstGroupId(String groupIds) {
return Long.parseLong(groupIds.split(StrUtil.COMMA)[0].trim());
}
private List<Long> parseGroupIds(String groupIds) {
return Arrays.stream(groupIds.split(StrUtil.COMMA))
.map(String::trim)
.map(Long::parseLong)
.toList();
}
}

View File

@@ -13,48 +13,40 @@ import org.springframework.validation.annotation.Validated;
import java.util.List;
/**
* 样本标签分组库 Service 实现类
*
* @author 芋道源码
*/
@Service
@Validated
public class AiSampleTagGroupServiceImpl implements AiSampleTagGroupService {
private static final Integer ENUM_TYPE = 1;
@Resource
private AiSampleTagGroupMapper aiSampleTagGroupMapper;
@Override
public Long createAiSampleTagGroup(AiSampleTagGroupSaveReqVO createReqVO) {
// 插入
AiSampleTagGroupDO aiSampleTagGroup = BeanUtils.toBean(createReqVO, AiSampleTagGroupDO.class);
aiSampleTagGroupMapper.insert(aiSampleTagGroup);
// 返回
return aiSampleTagGroup.getId();
AiSampleTagGroupDO group = BeanUtils.toBean(createReqVO, AiSampleTagGroupDO.class);
aiSampleTagGroupMapper.insert(group);
return group.getId();
}
@Override
public void updateAiSampleTagGroup(AiSampleTagGroupSaveReqVO updateReqVO) {
// 校验存在
validateAiSampleTagGroupExists(updateReqVO.getId());
// 更新
AiSampleTagGroupDO updateObj = BeanUtils.toBean(updateReqVO, AiSampleTagGroupDO.class);
aiSampleTagGroupMapper.updateById(updateObj);
AiSampleTagGroupDO existingGroup = aiSampleTagGroupMapper.selectById(updateReqVO.getId());
if (isEnumType(existingGroup.getGroupType())
&& !existingGroup.getGroupType().equals(updateReqVO.getGroupType())) {
throw new IllegalArgumentException("Enum group type cannot be changed");
}
AiSampleTagGroupDO group = BeanUtils.toBean(updateReqVO, AiSampleTagGroupDO.class);
aiSampleTagGroupMapper.updateById(group);
}
@Override
public void deleteAiSampleTagGroup(Long id) {
// 校验存在
validateAiSampleTagGroupExists(id);
// 删除
aiSampleTagGroupMapper.deleteById(id);
}
private void validateAiSampleTagGroupExists(Long id) {
if (aiSampleTagGroupMapper.selectById(id) == null) {
// throw exception(AI_SAMPLE_TAG_GROUP_NOT_EXISTS);
AiSampleTagGroupDO group = aiSampleTagGroupMapper.selectById(id);
if (isEnumType(group.getGroupType())) {
throw new IllegalArgumentException("Enum group cannot be deleted");
}
aiSampleTagGroupMapper.deleteById(id);
}
@Override
@@ -69,6 +61,11 @@ public class AiSampleTagGroupServiceImpl implements AiSampleTagGroupService {
@Override
public List<AiSampleTagGroupDO> getAiSampleTagGroupList() {
return aiSampleTagGroupMapper.selectList(new LambdaQueryWrapper<AiSampleTagGroupDO>().orderByDesc(AiSampleTagGroupDO::getId));
return aiSampleTagGroupMapper.selectList(new LambdaQueryWrapper<AiSampleTagGroupDO>()
.orderByDesc(AiSampleTagGroupDO::getId));
}
}
private boolean isEnumType(Integer type) {
return ENUM_TYPE.equals(type);
}
}

View File

@@ -3,6 +3,7 @@ package com.tashow.cloud.ai.service.aisample;
import com.tashow.cloud.ai.controller.admin.aisample.vo.AiSampleTagPageReqVO;
import com.tashow.cloud.ai.controller.admin.aisample.vo.AiSampleTagSaveReqVO;
import com.tashow.cloud.ai.dal.dataobject.aisample.AiSampleTagDO;
import com.tashow.cloud.ai.dal.dataobject.aisample.AiSampleTagGroupDO;
import com.tashow.cloud.common.pojo.PageResult;
import jakarta.validation.Valid;
@@ -58,4 +59,12 @@ public interface AiSampleTagService {
* @return
*/
List<AiSampleTagDO> getAiSampleTagList();
}
/**
* 根据标签 id 获取关联分组列表
*
* @param tagId 标签 id
* @return 分组列表
*/
List<AiSampleTagGroupDO> getGroupListByTagId(Long tagId);
}

View File

@@ -1,12 +1,13 @@
package com.tashow.cloud.ai.service.aisample;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.tashow.cloud.ai.controller.admin.aisample.vo.AiSampleTagPageReqVO;
import com.tashow.cloud.ai.controller.admin.aisample.vo.AiSampleTagSaveReqVO;
import com.tashow.cloud.ai.dal.dataobject.aisample.AiSampleTagDO;
import com.tashow.cloud.ai.dal.dataobject.aisample.AiSampleTagGroupRelateDO;
import com.tashow.cloud.ai.dal.mysql.aisample.AiSampleTagGroupRelateMapper;
import com.tashow.cloud.ai.dal.dataobject.aisample.AiSampleTagGroupDO;
import com.tashow.cloud.ai.dal.mysql.aisample.AiSampleTagGroupMapper;
import com.tashow.cloud.ai.dal.mysql.aisample.AiSampleTagMapper;
import com.tashow.cloud.common.pojo.PageResult;
import com.tashow.cloud.common.util.object.BeanUtils;
@@ -15,74 +16,56 @@ import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* 样本标签库 Service 实现类
*
* @author 芋道源码
*/
@Service
@Validated
public class AiSampleTagServiceImpl implements AiSampleTagService {
private static final Integer ENUM_TYPE = 1;
private static final Integer DEFAULT_TAG_TYPE = 2;
@Resource
private AiSampleTagMapper aiSampleTagMapper;
@Resource
private AiSampleTagGroupRelateMapper tagGroupRelateMapper;
private AiSampleTagGroupMapper aiSampleTagGroupMapper;
@Override
public Long createAiSampleTag(AiSampleTagSaveReqVO createReqVO) {
// 插入
AiSampleTagDO aiSampleTag = BeanUtils.toBean(createReqVO, AiSampleTagDO.class);
aiSampleTagMapper.insert(aiSampleTag);
//插入关联
List<AiSampleTagGroupRelateDO> tagGroupRelateDOS = new ArrayList<>();
for (Long groupId : createReqVO.getGroupIds()) {
AiSampleTagGroupRelateDO tagGroupRelateDO = new AiSampleTagGroupRelateDO();
tagGroupRelateDO.setSampleTagId(createReqVO.getId());
tagGroupRelateDO.setSampleTagGroupId(groupId);
tagGroupRelateDO.setSampleTagId(aiSampleTag.getId());
tagGroupRelateDOS.add(tagGroupRelateDO);
Integer tagType = createReqVO.getTagType() == null ? DEFAULT_TAG_TYPE : createReqVO.getTagType();
createReqVO.setTagType(tagType);
if (isEnumType(tagType)) {
validateEnumTagGroup(createReqVO.getGroupId());
}
tagGroupRelateMapper.insertBatch(tagGroupRelateDOS);
// 返回
return aiSampleTag.getId();
AiSampleTagDO tag = BeanUtils.toBean(createReqVO, AiSampleTagDO.class);
aiSampleTagMapper.insert(tag);
return tag.getId();
}
@Override
public void updateAiSampleTag(AiSampleTagSaveReqVO updateReqVO) {
// 校验存在
validateAiSampleTagExists(updateReqVO.getId());
// 更新
AiSampleTagDO updateObj = BeanUtils.toBean(updateReqVO, AiSampleTagDO.class);
aiSampleTagMapper.updateById(updateObj);
//删除所有关联
tagGroupRelateMapper.delete(new LambdaQueryWrapper<AiSampleTagGroupRelateDO>().eq(AiSampleTagGroupRelateDO::getSampleTagId, updateReqVO.getId()));
//插入关联
List<AiSampleTagGroupRelateDO> tagGroupRelateDOS = new ArrayList<>();
for (Long groupId : updateReqVO.getGroupIds()) {
AiSampleTagGroupRelateDO tagGroupRelateDO = new AiSampleTagGroupRelateDO();
tagGroupRelateDO.setSampleTagId(updateReqVO.getId());
tagGroupRelateDO.setSampleTagGroupId(groupId);
tagGroupRelateDOS.add(tagGroupRelateDO);
AiSampleTagDO existingTag = aiSampleTagMapper.selectById(updateReqVO.getId());
if (isEnumType(existingTag.getTagType())) {
if (!existingTag.getTagType().equals(updateReqVO.getTagType())) {
throw new IllegalArgumentException("Enum tag type cannot be changed");
}
if (!existingTag.getGroupId().equals(updateReqVO.getGroupId())) {
throw new IllegalArgumentException("Enum tag group cannot be changed");
}
}
tagGroupRelateMapper.insertBatch(tagGroupRelateDOS);
AiSampleTagDO tag = BeanUtils.toBean(updateReqVO, AiSampleTagDO.class);
aiSampleTagMapper.updateById(tag);
}
@Override
public void deleteAiSampleTag(Long id) {
// 校验存在
validateAiSampleTagExists(id);
// 删除
aiSampleTagMapper.deleteById(id);
}
private void validateAiSampleTagExists(Long id) {
if (aiSampleTagMapper.selectById(id) == null) {
// throw exception(AI_SAMPLE_TAG_NOT_EXISTS);
AiSampleTagDO tag = aiSampleTagMapper.selectById(id);
if (isEnumType(tag.getTagType())) {
throw new IllegalArgumentException("Enum tag cannot be deleted");
}
aiSampleTagMapper.deleteById(id);
}
@Override
@@ -92,12 +75,41 @@ public class AiSampleTagServiceImpl implements AiSampleTagService {
@Override
public PageResult<AiSampleTagDO> getAiSampleTagPage(AiSampleTagPageReqVO pageReqVO) {
IPage<AiSampleTagDO> prodPageList = aiSampleTagMapper.getAiSampleTagPage(MyBatisUtils.buildPage(pageReqVO),pageReqVO);
return new PageResult<>(prodPageList.getRecords(), prodPageList.getTotal());
IPage<AiSampleTagDO> page = aiSampleTagMapper.getAiSampleTagPage(MyBatisUtils.buildPage(pageReqVO), pageReqVO);
return new PageResult<>(page.getRecords(), page.getTotal());
}
@Override
public List<AiSampleTagDO> getAiSampleTagList() {
return aiSampleTagMapper.selectList(new LambdaQueryWrapper<AiSampleTagDO>().orderByDesc(AiSampleTagDO::getId));
}
}
@Override
public List<AiSampleTagGroupDO> getGroupListByTagId(Long tagId) {
AiSampleTagDO tag = aiSampleTagMapper.selectById(tagId);
return aiSampleTagGroupMapper.selectBatchIds(parseGroupIds(tag.getGroupId()));
}
private void validateEnumTagGroup(String groupIdStr) {
Long groupId = getFirstGroupId(groupIdStr);
AiSampleTagGroupDO group = aiSampleTagGroupMapper.selectById(groupId);
if (group == null || !isEnumType(group.getGroupType())) {
throw new IllegalArgumentException("Enum tag can only bind enum group");
}
}
private boolean isEnumType(Integer type) {
return ENUM_TYPE.equals(type);
}
private Long getFirstGroupId(String groupIds) {
return parseGroupIds(groupIds).get(0);
}
private List<Long> parseGroupIds(String groupIds) {
return Arrays.stream(groupIds.split(StrUtil.COMMA))
.map(String::trim)
.map(Long::parseLong)
.toList();
}
}

View File

@@ -20,7 +20,7 @@ import com.tashow.cloud.ai.dal.mysql.dialog.AiDialogMessageMapper;
import com.tashow.cloud.common.pojo.PageParam;
import com.tashow.cloud.common.pojo.PageResult;
import com.tashow.cloud.common.util.object.BeanUtils;
import com.tashow.cloud.infraapi.api.file.FileApi;
import com.tashow.cloud.fileapi.api.file.FileApi;
import jakarta.annotation.Resource;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;

View File

@@ -0,0 +1,100 @@
package com.tashow.cloud.ai.service.model;
import com.tashow.cloud.ai.controller.admin.model.vo.AiModelAnalysisResultVO;
import com.tashow.cloud.ai.controller.admin.model.vo.AiModelDifferenceVO;
import com.tashow.cloud.ai.controller.admin.model.vo.AiModelPageReqVO;
import com.tashow.cloud.ai.controller.admin.model.vo.AiModelSaveReqVO;
import com.tashow.cloud.ai.dal.dataobject.aisample.AiSampleDO;
import com.tashow.cloud.ai.dal.dataobject.model.AiModelDO;
import com.tashow.cloud.common.pojo.PageResult;
import jakarta.validation.Valid;
import java.util.List;
/**
* AI模型管理 Service 接口
*
* @author tashow
*/
public interface AiModelService {
/**
* 创建模型
*
* @param createReqVO 创建信息
* @return 编号
*/
Long createModel(@Valid AiModelSaveReqVO createReqVO);
/**
* 更新模型
*
* @param updateReqVO 更新信息
*/
void updateModel(@Valid AiModelSaveReqVO updateReqVO);
/**
* 删除模型
*
* @param id 编号
*/
void deleteModel(Long id);
/**
* 获取所有已启用的模型
*
* @return 已启用的模型列表
*/
List<AiModelDO> getEnabledModels();
/**
* 获得模型分页
*
* @param pageReqVO 分页查询
* @return 模型分页
*/
PageResult<AiModelDO> getModelPage(AiModelPageReqVO pageReqVO);
/**
* 模型引用样本(覆写式)
*
* @param modelId 模型ID
* @param sampleIds 样本ID列表
*/
void referenceSamples(Long modelId, List<Long> sampleIds);
/**
* 获取模型关联的样本
*
* @param modelId 模型ID
* @return 样本列表
*/
List<AiSampleDO> getModelSamples(Long modelId);
/**
* 模型准确度分析(通过翻译接口分析样本匹配度)
*
* @param modelId 模型ID
* @return 分析结果
*/
AiModelAnalysisResultVO analyzeModel(List<Long> sampleIds, List<Long> modelIds);
/**
* 模型样本差异筛查(支持多模型)
*
* @param modelIds 模型ID列表
* @return 差异结果(重复引用样本和未重复引用样本)
*/
AiModelDifferenceVO checkDifference(List<Long> modelIds);
/**
* 模型迭代(复制模型和样本关联)
*
* @param modelId 原模型ID
* @param newVersion 新版本号
* @param description 版本描述
* @return 新模型ID
*/
Long iterateModel(Long modelId, String newVersion, String description);
}

View File

@@ -0,0 +1,367 @@
package com.tashow.cloud.ai.service.model;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.tashow.cloud.ai.controller.admin.model.vo.AiModelAnalysisResultVO;
import com.tashow.cloud.ai.controller.admin.model.vo.AiModelDifferenceVO;
import com.tashow.cloud.ai.controller.admin.model.vo.AiModelPageReqVO;
import com.tashow.cloud.ai.controller.admin.model.vo.AiModelSaveReqVO;
import com.tashow.cloud.ai.controller.admin.model.vo.AiSampleDetailVO;
import com.tashow.cloud.ai.dal.dataobject.aisample.AiSampleDO;
import com.tashow.cloud.ai.dal.dataobject.aisample.AiSampleTagDO;
import com.tashow.cloud.ai.dal.dataobject.aisample.AiSampleTagGroupDO;
import com.tashow.cloud.ai.dal.dataobject.aisample.AiSampleTagRelateDO;
import com.tashow.cloud.ai.dal.dataobject.model.AiModelDO;
import com.tashow.cloud.ai.dal.dataobject.model.AiModelSampleRelateDO;
import com.tashow.cloud.ai.dal.mysql.aisample.AiSampleMapper;
import com.tashow.cloud.ai.dal.mysql.aisample.AiSampleTagGroupMapper;
import com.tashow.cloud.ai.dal.mysql.aisample.AiSampleTagMapper;
import com.tashow.cloud.ai.dal.mysql.aisample.AiSampleTagRelateMapper;
import com.tashow.cloud.ai.dal.mysql.model.AiModelMapper;
import com.tashow.cloud.ai.dal.mysql.model.AiModelSampleRelateMapper;
import com.tashow.cloud.common.pojo.PageResult;
import com.tashow.cloud.common.util.object.BeanUtils;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
@Service
@Validated
@Slf4j
public class AiModelServiceImpl implements AiModelService {
@Resource
private AiModelMapper modelMapper;
@Resource
private AiModelSampleRelateMapper modelSampleRelateMapper;
@Resource
private AiSampleMapper sampleMapper;
@Resource
private AiSampleTagRelateMapper sampleTagRelateMapper;
@Resource
private AiSampleTagMapper sampleTagMapper;
@Resource
private AiSampleTagGroupMapper sampleTagGroupMapper;
@Value("${translate-server}")
private String translateServer;
@Value("${file-server}")
private String fileServer;
@Override
public Long createModel(AiModelSaveReqVO createReqVO) {
AiModelDO model = BeanUtils.toBean(createReqVO, AiModelDO.class);
modelMapper.insert(model);
return model.getId();
}
@Override
public void updateModel(AiModelSaveReqVO updateReqVO) {
modelMapper.updateById(BeanUtils.toBean(updateReqVO, AiModelDO.class));
}
@Override
public void deleteModel(Long id) {
modelMapper.deleteById(id);
}
@Override
public List<AiModelDO> getEnabledModels() {
return modelMapper.selectList(new LambdaQueryWrapper<AiModelDO>().eq(AiModelDO::getStatus, 1));
}
@Override
public PageResult<AiModelDO> getModelPage(AiModelPageReqVO pageReqVO) {
return modelMapper.selectPage(pageReqVO);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void referenceSamples(Long modelId, List<Long> sampleIds) {
modelSampleRelateMapper.delete(new LambdaQueryWrapper<AiModelSampleRelateDO>()
.eq(AiModelSampleRelateDO::getModelId, modelId));
if (CollUtil.isEmpty(sampleIds)) {
return;
}
List<AiModelSampleRelateDO> relates = sampleIds.stream()
.map(sampleId -> AiModelSampleRelateDO.builder().modelId(modelId).sampleId(sampleId).build())
.toList();
modelSampleRelateMapper.insertBatch(relates);
}
@Override
public List<AiSampleDO> getModelSamples(Long modelId) {
List<Long> sampleIds = modelSampleRelateMapper.selectList(new LambdaQueryWrapper<AiModelSampleRelateDO>()
.eq(AiModelSampleRelateDO::getModelId, modelId))
.stream().map(AiModelSampleRelateDO::getSampleId).toList();
return sampleMapper.selectBatchIds(sampleIds);
}
@Override
public AiModelAnalysisResultVO analyzeModel(List<Long> sampleIds, List<Long> modelIds) {
if (CollUtil.isEmpty(sampleIds) || CollUtil.isEmpty(modelIds)) {
return AiModelAnalysisResultVO.builder().samples(List.of()).build();
}
List<AiModelAnalysisResultVO.SampleAnalysis> samples = new ArrayList<>();
for (Long sampleId : sampleIds) {
AiSampleDO sample = sampleMapper.selectById(sampleId);
if (sample == null) {
continue;
}
List<AiSampleTagDO> enumTags = getEnumTagsForSample(sampleId);
String enumTagStr = enumTags.stream().map(AiSampleTagDO::getTagName).collect(Collectors.joining(","));
List<AiModelAnalysisResultVO.ModelAnalysisResult> modelResults = modelIds.stream()
.map(modelRefId -> buildModelAnalysis(sample, enumTags, modelRefId))
.filter(Objects::nonNull)
.toList();
samples.add(AiModelAnalysisResultVO.SampleAnalysis.builder()
.sampleId(sample.getId())
.sampleName(sample.getSampleName())
.enumTag(enumTagStr)
.modelResults(modelResults)
.build());
}
return AiModelAnalysisResultVO.builder().samples(samples).build();
}
@Override
public AiModelDifferenceVO checkDifference(List<Long> modelIds) {
Map<Long, Integer> sampleRefCount = new HashMap<>();
for (Long modelId : modelIds) {
for (Long sampleId : getModelSampleIds(modelId)) {
sampleRefCount.merge(sampleId, 1, Integer::sum);
}
}
List<Long> duplicatedIds = new ArrayList<>();
List<Long> uniqueIds = new ArrayList<>();
sampleRefCount.forEach((sampleId, count) -> {
if (count >= 2) {
duplicatedIds.add(sampleId);
} else {
uniqueIds.add(sampleId);
}
});
return AiModelDifferenceVO.builder()
.duplicatedSamples(buildSampleDetailVOList(duplicatedIds))
.uniqueSamples(buildSampleDetailVOList(uniqueIds))
.build();
}
@Override
@Transactional(rollbackFor = Exception.class)
public Long iterateModel(Long modelId, String newVersion, String description) {
AiModelDO original = modelMapper.selectById(modelId);
AiModelDO newModel = BeanUtils.toBean(original, AiModelDO.class);
newModel.setId(null);
newModel.setVersion(newVersion);
newModel.setDescription(description);
newModel.setStatus(2);
modelMapper.insert(newModel);
List<AiModelSampleRelateDO> newRelates = modelSampleRelateMapper.selectList(new LambdaQueryWrapper<AiModelSampleRelateDO>()
.eq(AiModelSampleRelateDO::getModelId, modelId))
.stream()
.map(relate -> AiModelSampleRelateDO.builder()
.modelId(newModel.getId())
.sampleId(relate.getSampleId())
.build())
.toList();
if (CollUtil.isNotEmpty(newRelates)) {
modelSampleRelateMapper.insertBatch(newRelates);
}
return newModel.getId();
}
private AiModelAnalysisResultVO.ModelAnalysisResult buildModelAnalysis(AiSampleDO sample, List<AiSampleTagDO> enumTags,
Long modelId) {
AiModelDO model = modelMapper.selectById(modelId);
if (model == null) {
return null;
}
String speciesResult = null;
BigDecimal speciesMatchRate = null;
String emotionResult = null;
BigDecimal emotionMatchRate = null;
JSONObject translateResult = requestTranslate(sample, model);
if (translateResult != null) {
speciesResult = translateResult.getString("species_labels");
AiSampleTagGroupDO speciesGroup = getGroupByName("物种识别");
if (speciesGroup != null) {
speciesMatchRate = calculateMatchRate(speciesResult, enumTags, speciesGroup.getId());
}
JSONObject intentResult = translateResult.getJSONObject("intent_result");
JSONObject probabilities = intentResult == null ? null : intentResult.getJSONObject("probabilities");
if (probabilities != null && !probabilities.isEmpty()) {
String emotionKey = probabilities.entrySet().stream()
.max(Comparator.comparingDouble(entry -> ((Number) entry.getValue()).doubleValue()))
.map(Map.Entry::getKey)
.orElse(null);
if (StrUtil.isNotBlank(emotionKey) && emotionKey.contains("_")) {
emotionResult = emotionKey.split("_")[1];
AiSampleTagGroupDO emotionGroup = getGroupByName("情绪识别");
if (emotionGroup != null) {
emotionMatchRate = calculateMatchRate(emotionResult, enumTags, emotionGroup.getId());
}
}
}
}
return AiModelAnalysisResultVO.ModelAnalysisResult.builder()
.modelId(model.getId())
.modelName(model.getModelName())
.modelVersion(model.getVersion())
.speciesResult(speciesResult)
.speciesMatchRate(speciesMatchRate)
.emotionResult(emotionResult)
.emotionMatchRate(emotionMatchRate)
.build();
}
private JSONObject requestTranslate(AiSampleDO sample, AiModelDO model) {
try {
String fileUrl = fileServer + sample.getSampleFilePath();
String response = HttpRequest.post(translateServer)
.form("audio_data", HttpUtil.downloadBytes(fileUrl), sample.getSampleName())
.form("model_name", model.getModelName())
.form("model_version", model.getVersion())
.timeout(20000)
.execute().body();
JSONObject result = JSON.parseObject(response);
if (result == null || result.isEmpty() || !result.containsKey("intent_result")) {
return null;
}
if (!Boolean.TRUE.equals(result.getBoolean("is_species_sound")) || result.getDoubleValue("confidence") < 0.7) {
return null;
}
JSONObject intent = result.getJSONObject("intent_result");
return intent != null && intent.getDoubleValue("confidence") >= 0.7 ? result : null;
} catch (Exception ex) {
log.error("调用翻译API失败样本ID: {}", sample.getId(), ex);
return null;
}
}
private List<Long> getSampleModelIds(Long sampleId) {
return modelSampleRelateMapper.selectList(new LambdaQueryWrapper<AiModelSampleRelateDO>()
.eq(AiModelSampleRelateDO::getSampleId, sampleId))
.stream().map(AiModelSampleRelateDO::getModelId).toList();
}
private Set<Long> getModelSampleIds(Long modelId) {
return modelSampleRelateMapper.selectList(new LambdaQueryWrapper<AiModelSampleRelateDO>()
.eq(AiModelSampleRelateDO::getModelId, modelId))
.stream().map(AiModelSampleRelateDO::getSampleId).collect(Collectors.toSet());
}
private List<AiSampleDetailVO> buildSampleDetailVOList(List<Long> sampleIds) {
return sampleIds.stream().map(this::buildSampleDetailVO).toList();
}
private AiSampleDetailVO buildSampleDetailVO(Long sampleId) {
AiSampleDO sample = sampleMapper.selectById(sampleId);
List<Long> tagIds = sampleTagRelateMapper.selectList(new LambdaQueryWrapper<AiSampleTagRelateDO>()
.eq(AiSampleTagRelateDO::getSampleId, sampleId))
.stream().map(AiSampleTagRelateDO::getSampleTagId).toList();
List<AiSampleDetailVO.TagInfo> enumTags = new ArrayList<>();
List<AiSampleDetailVO.TagInfo> customTags = new ArrayList<>();
for (AiSampleTagDO tag : sampleTagMapper.selectBatchIds(tagIds)) {
AiSampleDetailVO.TagInfo tagInfo = AiSampleDetailVO.TagInfo.builder()
.id(tag.getId())
.tagName(tag.getTagName())
.enumValue(tag.getEnumValue())
.groupName(getTagGroupName(tag.getGroupId()))
.build();
if (Integer.valueOf(1).equals(tag.getTagType())) {
enumTags.add(tagInfo);
} else {
customTags.add(tagInfo);
}
}
List<Long> modelIds = getSampleModelIds(sampleId);
List<AiSampleDetailVO.ModelInfo> relatedModels = modelMapper.selectBatchIds(modelIds).stream()
.map(model -> AiSampleDetailVO.ModelInfo.builder()
.id(model.getId())
.modelName(model.getModelName())
.version(model.getVersion())
.build())
.toList();
return AiSampleDetailVO.builder()
.id(sample.getId())
.sampleName(sample.getSampleName())
.sampleFilePath(sample.getSampleFilePath())
.enumTags(enumTags)
.customTags(customTags)
.relatedModels(relatedModels)
.build();
}
private String getTagGroupName(String groupIds) {
if (StrUtil.isBlank(groupIds)) {
return "";
}
AiSampleTagGroupDO group = sampleTagGroupMapper.selectById(Long.parseLong(groupIds.split(StrUtil.COMMA)[0].trim()));
return group == null ? "" : group.getGroupName();
}
private List<AiSampleTagDO> getEnumTagsForSample(Long sampleId) {
List<Long> tagIds = sampleTagRelateMapper.selectList(new LambdaQueryWrapper<AiSampleTagRelateDO>()
.eq(AiSampleTagRelateDO::getSampleId, sampleId))
.stream().map(AiSampleTagRelateDO::getSampleTagId).toList();
return sampleTagMapper.selectBatchIds(tagIds).stream()
.filter(tag -> Integer.valueOf(1).equals(tag.getTagType()))
.toList();
}
private AiSampleTagGroupDO getGroupByName(String groupName) {
return sampleTagGroupMapper.selectOne(new LambdaQueryWrapper<AiSampleTagGroupDO>()
.eq(AiSampleTagGroupDO::getGroupName, groupName));
}
private BigDecimal calculateMatchRate(String recognitionValue, List<AiSampleTagDO> enumTags, Long groupId) {
if (StrUtil.isBlank(recognitionValue) || CollUtil.isEmpty(enumTags)) {
return BigDecimal.ZERO;
}
for (AiSampleTagDO tag : enumTags) {
if (tag.getGroupId().contains(groupId.toString())) {
if (recognitionValue.equalsIgnoreCase(tag.getTagName())
|| recognitionValue.contains(tag.getTagName())
|| tag.getTagName().contains(recognitionValue)) {
return BigDecimal.ONE;
}
}
}
return BigDecimal.ZERO;
}
}

View File

@@ -7,12 +7,15 @@ spring:
username: nacos # Nacos 账号
password: nacos # Nacos 密码
discovery: # 【配置中心】配置项
namespace: dev # 命名空间。这里使用 dev 开发环境
namespace: 63caf548-313d-44bb-929c-531bf2f3b1a2 # 命名空间
group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP
metadata:
version: 1.0.0 # 服务实例的版本号,可用于灰度发布
config: # 【注册中心】配置项
namespace: dev # 命名空间。这里使用 dev 开发环境
namespace: 63caf548-313d-44bb-929c-531bf2f3b1a2 # 命名空间
group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP
# 添加字符编码配置解决YAML解析时的字符编码问题
encode: UTF-8
--- #################### 外部服务配置 ####################
# AI翻译服务URL
translate-server: http://43.139.42.137:8000/analyze/audio

View File

@@ -11,6 +11,7 @@ import org.springframework.web.bind.annotation.RestController;
import static com.tashow.cloud.common.pojo.CommonResult.success;
@RestController // 提供 RESTful API 接口,给 Feign 调用
@Validated
public class FileApiImpl implements FileApi {
@@ -24,4 +25,11 @@ public class FileApiImpl implements FileApi {
createReqDTO.getContent()));
}
@Override
public CommonResult<String> createFileMultipart(FileCreateReqDTO createReqDTO) {
return success(fileService.createFileMultipart(createReqDTO.getName(), createReqDTO.getPath(),
createReqDTO.getContent()));
}
}

View File

@@ -63,7 +63,7 @@ public abstract class AbstractFileClient<Config extends FileClientConfig> implem
* @return URL 访问地址
*/
protected String formatFileUrl(String domain, String path) {
return StrUtil.format("{}/admin-api/file/{}/get/{}", domain, getId(), path);
return StrUtil.format("/admin-api/file/{}/get/{}", getId(), path);
}
}

View File

@@ -11,10 +11,13 @@ import com.amazonaws.services.s3.AmazonS3Client;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
import com.amazonaws.services.s3.model.ObjectMetadata;
import com.amazonaws.services.s3.model.S3Object;
import com.amazonaws.services.s3.model.*;
import com.tashow.cloud.file.framework.file.core.client.AbstractFileClient;
import java.io.ByteArrayInputStream;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
@@ -92,7 +95,32 @@ public class S3FileClient extends AbstractFileClient<S3FileClientConfig> {
objectMetadata);
// 拼接返回路径
return config.getDomain() + "/" + path;
return config.getDomain() + path;
}
/** 分段上传 */
public String uploadMultipart(byte[] content, String path, String type) throws Exception {
int partSize = 5 * 1024 * 1024;
ObjectMetadata metadata = new ObjectMetadata();
metadata.setContentType(type);
String uploadId = client.initiateMultipartUpload(new InitiateMultipartUploadRequest(config.getBucket(), path, metadata)).getUploadId();
try {
List<PartETag> partETags = new ArrayList<>();
for (int i = 0, partNum = 1; i < content.length; i += partSize, partNum++) {
int size = Math.min(partSize, content.length - i);
byte[] part = new byte[size];
System.arraycopy(content, i, part, 0, size);
UploadPartResult result = client.uploadPart(new UploadPartRequest()
.withBucketName(config.getBucket()).withKey(path).withUploadId(uploadId)
.withPartNumber(partNum).withInputStream(new ByteArrayInputStream(part)).withPartSize(size));
partETags.add(result.getPartETag());
}
client.completeMultipartUpload(new CompleteMultipartUploadRequest(config.getBucket(), path, uploadId, partETags));
return config.getDomain() + path;
} catch (Exception e) {
client.abortMultipartUpload(new AbortMultipartUploadRequest(config.getBucket(), path, uploadId));
throw e;
}
}
@Override

View File

@@ -31,6 +31,9 @@ public interface FileService {
*/
String createFile(String name, String path, byte[] content);
/** 分段上传文件 */
String createFileMultipart(String name, String path, byte[] content);
/**
* 创建文件
*

View File

@@ -12,6 +12,7 @@ import com.tashow.cloud.file.dal.dataobject.file.FileDO;
import com.tashow.cloud.file.dal.mysql.file.FileMapper;
import com.tashow.cloud.file.framework.file.core.client.FileClient;
import com.tashow.cloud.file.framework.file.core.client.s3.FilePresignedUrlRespDTO;
import com.tashow.cloud.file.framework.file.core.client.s3.S3FileClient;
import com.tashow.cloud.file.framework.file.core.utils.FileTypeUtils;
import jakarta.annotation.Resource;
import lombok.SneakyThrows;
@@ -70,6 +71,30 @@ public class FileServiceImpl implements FileService {
return url;
}
@Override
@SneakyThrows
public String createFileMultipart(String name, String path, byte[] content) {
String type = FileTypeUtils.getMineType(content, name);
if (StrUtil.isEmpty(path)) {
path = FileUtils.generatePath(content, name);
}
if (StrUtil.isEmpty(name)) {
name = path;
}
FileClient client = fileConfigService.getMasterFileClient();
Assert.notNull(client, "客户端(master) 不能为空");
String url = ((S3FileClient) client).uploadMultipart(content, path, type);
FileDO file = new FileDO();
file.setConfigId(client.getId());
file.setName(name);
file.setPath(path);
file.setUrl(url);
file.setType(type);
file.setSize(content.length);
fileMapper.insert(file);
return url;
}
@Override
public Long createFile(FileCreateReqVO createReqVO) {
FileDO file = BeanUtils.toBean(createReqVO, FileDO.class);

View File

@@ -7,13 +7,10 @@ spring:
username: nacos # Nacos 账号
password: nacos # Nacos 密码
discovery: # 【配置中心】配置项
namespace: dev # 命名空间。这里使用 dev 开发环境
namespace: 63caf548-313d-44bb-929c-531bf2f3b1a2 # 命名空间
group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP
metadata:
version: 1.0.0 # 服务实例的版本号,可用于灰度发布
config: # 【注册中心】配置项
namespace: dev # 命名空间。这里使用 dev 开发环境
namespace: 63caf548-313d-44bb-929c-531bf2f3b1a2 # 命名空间
group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP

View File

@@ -7,11 +7,11 @@ spring:
username: nacos # Nacos 账号
password: nacos # Nacos 密码
discovery: # 【配置中心】配置项
namespace: dev # 命名空间。这里使用 dev 开发环境
namespace: 63caf548-313d-44bb-929c-531bf2f3b1a2 # 命名空间
group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP
metadata:
version: 1.0.0 # 服务实例的版本号,可用于灰度发布
config: # 【注册中心】配置项
namespace: dev # 命名空间。这里使用 dev 开发环境
namespace: 63caf548-313d-44bb-929c-531bf2f3b1a2 # 命名空间
group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP

View File

@@ -10,6 +10,8 @@ import com.tashow.cloud.common.pojo.PageResult;
import com.tashow.cloud.common.util.collection.CollectionUtils;
import com.tashow.cloud.common.util.object.BeanUtils;
import com.tashow.cloud.common.util.validation.ValidationUtils;
import com.tashow.cloud.infraapi.api.config.ConfigApi;
import com.tashow.cloud.infraapi.api.file.FileApi;
import com.tashow.cloud.permission.core.util.DataPermissionUtils;

View File

@@ -5,11 +5,11 @@ spring:
username: nacos # Nacos 账号
password: nacos # Nacos 密码
discovery: # 【配置中心】配置项
namespace: dev # 命名空间。这里使用 dev 开发环境
namespace: 63caf548-313d-44bb-929c-531bf2f3b1a2 # 命名空间
group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP
metadata:
version: 1.0.0 # 服务实例的版本号,可用于灰度发布
config: # 【注册中心】配置项
namespace: dev # 命名空间。这里使用 dev 开发环境
namespace: 63caf548-313d-44bb-929c-531bf2f3b1a2 # 命名空间
group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP