7 Commits

Author SHA1 Message Date
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
39 changed files with 652 additions and 73 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

@@ -1,41 +1,29 @@
/*
* 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;
}
@@ -44,11 +32,9 @@ public class ImgJsonSerializer extends JsonSerializer<String> {
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();
}
resourceUrl = fileServer;
for (String img : imgs) {
Matcher matcher = pattern.matcher(img);
//若图片以http或https开头直接返回
@@ -59,6 +45,6 @@ public class ImgJsonSerializer extends JsonSerializer<String> {
}
}
sb.deleteCharAt(sb.length() - 1);
gen.writeString(sb.toString());*/
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

@@ -11,10 +11,12 @@ 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 java.io.IOException;
import java.util.List;
import static com.tashow.cloud.common.pojo.CommonResult.success;
@@ -91,4 +93,13 @@ public class AiSampleController {
PageResult<AiSampleDO> pageResult = aiSampleService.getAiSamplePage(pageReqVO);
return success(BeanUtils.toBean(pageResult, AiSampleRespVO.class));
}
@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);
}
}

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')")

View File

@@ -1,6 +1,9 @@
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,6 +13,7 @@ import lombok.Data;
public class AiSampleFileRespVO {
@Schema(description = "文件地址")
@JsonSerialize(using = ImgJsonSerializer.class)
private String fileUrl;
@Schema(description = "文件名称")

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

@@ -1,8 +1,9 @@
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.Data;
@@ -21,6 +22,7 @@ public class AiSampleRespVO {
private List<AiSampleTagDO> tags;
@Schema(description = "样本文件地址")
@JsonSerialize(using = ImgJsonSerializer.class)
private String sampleFilePath;
@Schema(description = "样本名称", example = "张三")
@@ -45,4 +47,10 @@ public class AiSampleRespVO {
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private LocalDateTime updateTime;
@Schema(description = "文件后缀")
private String suffix;
}

View File

@@ -0,0 +1,73 @@
package com.tashow.cloud.ai.controller.admin.model;
import com.tashow.cloud.ai.controller.admin.model.vo.AiModelPageReqVO;
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.model.AiModelDO;
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.*;
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;
@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));
}
}

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模型分页 Request VO")
@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-启用 2-测试中 3-已废弃)", example = "1")
private Integer status;
}

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模型 Response VO")
@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模型新增/修改 Request VO")
@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

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

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

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

@@ -28,6 +28,7 @@ public interface AiSampleTagMapper extends BaseMapperX<AiSampleTagDO> {
"FROM tz_ai_sample_tag t " +
"INNER JOIN tz_ai_sample_tag_group_relate r ON t.id = r.sample_tag_id " +
"<where>" +
"AND t.deleted =0"+
" <if test=\"pageReqVO.groupId != null\">" +
" AND r.sample_tag_group_id = #{pageReqVO.groupId}" +
" </if>" +

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

@@ -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,12 @@ public interface AiSampleService {
*/
PageResult<AiSampleDO> getAiSamplePage(AiSamplePageReqVO pageReqVO);
/**
* 批量下载样本
*
* @param ids 样本ID列表逗号分隔
* @param response HTTP响应
*/
void downloadSamples(String ids, HttpServletResponse response) throws IOException;
}

View File

@@ -3,6 +3,7 @@ package com.tashow.cloud.ai.service.aisample;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ObjectUtil;
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.*;
@@ -14,18 +15,23 @@ import com.tashow.cloud.ai.dal.mysql.aisample.AiSampleTagMapper;
import com.tashow.cloud.ai.dal.mysql.aisample.AiSampleTagRelateMapper;
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.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.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
/**
* 样本库 Service 实现类
@@ -44,7 +50,7 @@ public class AiSampleServiceImpl implements AiSampleService {
private AiSampleTagRelateMapper aiSampleTagRelateMapper;
@Resource
private FileApi fileApi;
@Value("file-server")
@Value("${file-server}")
private String fileServer;
@Override
@@ -55,15 +61,18 @@ public class AiSampleServiceImpl implements AiSampleService {
/* 调用文件上传服务*/
for (MultipartFile file : uploadReqVO.getFiles()) {
//返回上传结果
String file1 = fileServer + fileApi.createFile(file.getBytes());
String filePath=fileApi.createFile(file.getOriginalFilename(), "", file.getBytes());
//保存样本信息
AiSampleDO aiSampleDO = new AiSampleDO();
aiSampleDO.setSampleFilePath(file1);
aiSampleDO.setSampleName(file.getOriginalFilename());
aiSampleDO.setSampleFilePath(filePath);
String filename = file.getOriginalFilename();
String displayName = filename.substring(0, filename.lastIndexOf('.'));
aiSampleDO.setSampleName(displayName);
aiSampleDO.setSuffix(filename.substring(filename.lastIndexOf('.') + 1));
aiSampleDO.setSampleMineType(file.getContentType());
aiSampleDO.setSampleSize(file.getSize());
aiSampleMapper.insert(aiSampleDO);
urls.add(new AiSampleFileRespVO().setFileUrl(file1).setFileName(file.getOriginalFilename()));
urls.add(new AiSampleFileRespVO().setFileUrl(filePath).setFileName(filename).setDisplayName(displayName));
}
// 返回
return urls;
@@ -172,9 +181,34 @@ public class AiSampleServiceImpl implements AiSampleService {
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());
}
return new PageResult<>(aiSampleDOPageResult.getRecords(), aiSampleDOPageResult.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);
} else {
// 多个文件打包成ZIP
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();
}
}
}
}
}

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

@@ -5,7 +5,9 @@ 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.AiSampleTagGroupDO;
import com.tashow.cloud.ai.dal.dataobject.aisample.AiSampleTagGroupRelateDO;
import com.tashow.cloud.ai.dal.mysql.aisample.AiSampleTagGroupMapper;
import com.tashow.cloud.ai.dal.mysql.aisample.AiSampleTagGroupRelateMapper;
import com.tashow.cloud.ai.dal.mysql.aisample.AiSampleTagMapper;
import com.tashow.cloud.common.pojo.PageResult;
@@ -17,6 +19,7 @@ import org.springframework.validation.annotation.Validated;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
/**
* 样本标签库 Service 实现类
@@ -30,6 +33,8 @@ public class AiSampleTagServiceImpl implements AiSampleTagService {
@Resource
private AiSampleTagMapper aiSampleTagMapper;
@Resource
private AiSampleTagGroupMapper aiSampleTagGroupMapper;
@Resource
private AiSampleTagGroupRelateMapper tagGroupRelateMapper;
@Override
@@ -41,7 +46,6 @@ public class AiSampleTagServiceImpl implements AiSampleTagService {
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);
@@ -58,11 +62,13 @@ public class AiSampleTagServiceImpl implements AiSampleTagService {
// 更新
AiSampleTagDO updateObj = BeanUtils.toBean(updateReqVO, AiSampleTagDO.class);
aiSampleTagMapper.updateById(updateObj);
//删除所有关联
tagGroupRelateMapper.delete(new LambdaQueryWrapper<AiSampleTagGroupRelateDO>().eq(AiSampleTagGroupRelateDO::getSampleTagId, updateReqVO.getId()));
List<Long> groupIds = updateReqVO.getGroupIds();
// 删除该标签的所有旧关联
tagGroupRelateMapper.delete(new LambdaQueryWrapper<AiSampleTagGroupRelateDO>()
.eq(AiSampleTagGroupRelateDO::getSampleTagId, updateReqVO.getId()));
//插入关联
List<AiSampleTagGroupRelateDO> tagGroupRelateDOS = new ArrayList<>();
for (Long groupId : updateReqVO.getGroupIds()) {
for (Long groupId : groupIds) {
AiSampleTagGroupRelateDO tagGroupRelateDO = new AiSampleTagGroupRelateDO();
tagGroupRelateDO.setSampleTagId(updateReqVO.getId());
tagGroupRelateDO.setSampleTagGroupId(groupId);
@@ -100,4 +106,17 @@ public class AiSampleTagServiceImpl implements AiSampleTagService {
public List<AiSampleTagDO> getAiSampleTagList() {
return aiSampleTagMapper.selectList(new LambdaQueryWrapper<AiSampleTagDO>().orderByDesc(AiSampleTagDO::getId));
}
@Override
public List<AiSampleTagGroupDO> getGroupListByTagId(Long tagId) {
List<AiSampleTagGroupRelateDO> relateList = tagGroupRelateMapper.selectList(
new LambdaQueryWrapper<AiSampleTagGroupRelateDO>()
.eq(AiSampleTagGroupRelateDO::getSampleTagId, tagId));
List<Long> groupIds = relateList.stream()
.map(AiSampleTagGroupRelateDO::getSampleTagGroupId)
.distinct()
.collect(Collectors.toList());
return aiSampleTagGroupMapper.selectList(
new LambdaQueryWrapper<AiSampleTagGroupDO>().in(AiSampleTagGroupDO::getId, groupIds));
}
}

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,55 @@
package com.tashow.cloud.ai.service.model;
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.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);
}

View File

@@ -0,0 +1,60 @@
package com.tashow.cloud.ai.service.model;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
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.model.AiModelDO;
import com.tashow.cloud.ai.dal.mysql.model.AiModelMapper;
import com.tashow.cloud.common.pojo.PageResult;
import com.tashow.cloud.common.util.object.BeanUtils;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import java.util.List;
/**
* AI模型管理 Service 实现类
*
* @author tashow
*/
@Service
@Validated
public class AiModelServiceImpl implements AiModelService {
@Resource
private AiModelMapper modelMapper;
@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) {
AiModelDO updateObj = BeanUtils.toBean(updateReqVO, AiModelDO.class);
modelMapper.updateById(updateObj);
}
@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);
}
}

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