Browse Source

【功能新增】AI:音乐管理 50%

YunaiV 10 months ago
parent
commit
9d6b615f10
20 changed files with 338 additions and 55 deletions
  1. 8 5
      yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/ErrorCodeConstants.java
  2. 11 1
      yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/music/AiMusicGenerateModeEnum.java
  3. 11 1
      yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/music/AiMusicStatusEnum.java
  4. 7 13
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/AiImageController.java
  5. 1 1
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/AiImagePageReqVO.java
  6. 0 13
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/AiImageController.http
  7. 26 0
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/AiMusicController.http
  8. 36 4
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/AiMusicController.java
  9. 44 0
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/AiMusicPageReqVO.java
  10. 67 0
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/AiMusicRespVO.java
  11. 18 0
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/AiMusicUpdatePublicStatusReqVO.java
  12. 8 1
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/AiSunoGenerateReqVO.java
  13. 5 0
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/music/AiMusicDO.java
  14. 14 0
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/music/AiMusicMapper.java
  15. 5 5
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatMessageServiceImpl.java
  16. 10 6
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageServiceImpl.java
  17. 27 0
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/music/AiMusicService.java
  18. 36 2
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/music/AiMusicServiceImpl.java
  19. 2 2
      yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/music/SunoTests.java
  20. 2 1
      yudao-server/src/main/resources/application.yaml

+ 8 - 5
yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/ErrorCodeConstants.java

@@ -29,13 +29,16 @@ public interface ErrorCodeConstants {
 
     // ========== API 聊天消息 1-040-004-000 ==========
 
-    ErrorCode AI_CHAT_MESSAGE_NOT_EXIST = new ErrorCode(1_040_004_000, "消息不存在!");
-    ErrorCode AI_CHAT_STREAM_ERROR = new ErrorCode(1_040_004_001, "Stream 对话异常!");
+    ErrorCode CHAT_MESSAGE_NOT_EXIST = new ErrorCode(1_040_004_000, "消息不存在!");
+    ErrorCode CHAT_STREAM_ERROR = new ErrorCode(1_040_004_001, "Stream 对话异常!");
 
     // ========== API 绘画 1-040-005-000 ==========
 
-    ErrorCode AI_IMAGE_NOT_EXISTS = new ErrorCode(1_022_005_000, "图片不存在!");
-    ErrorCode AI_IMAGE_MIDJOURNEY_SUBMIT_FAIL = new ErrorCode(1_022_005_001, "Midjourney 提交失败!原因:{}");
-    ErrorCode AI_IMAGE_CUSTOM_ID_NOT_EXISTS = new ErrorCode(1_022_005_002, "Midjourney 按钮 customId 不存在! {}");
+    ErrorCode IMAGE_NOT_EXISTS = new ErrorCode(1_022_005_000, "图片不存在!");
+    ErrorCode IMAGE_MIDJOURNEY_SUBMIT_FAIL = new ErrorCode(1_022_005_001, "Midjourney 提交失败!原因:{}");
+    ErrorCode IMAGE_CUSTOM_ID_NOT_EXISTS = new ErrorCode(1_022_005_002, "Midjourney 按钮 customId 不存在! {}");
+
+    // ========== API 音乐 1-040-006-000 ==========
+    ErrorCode MUSIC_NOT_EXISTS = new ErrorCode(1_022_006_000, "音乐不存在!");
 
 }

+ 11 - 1
yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/music/AiMusicGenerateModeEnum.java

@@ -1,8 +1,11 @@
 package cn.iocoder.yudao.module.ai.enums.music;
 
+import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
 import lombok.AllArgsConstructor;
 import lombok.Getter;
 
+import java.util.Arrays;
+
 /**
  * AI 音乐状态的枚举
  *
@@ -10,7 +13,7 @@ import lombok.Getter;
  */
 @AllArgsConstructor
 @Getter
-public enum AiMusicGenerateModeEnum {
+public enum AiMusicGenerateModeEnum implements IntArrayValuable {
 
     LYRIC(1, "歌词模式"),
     DESCRIPTION(2, "描述模式");
@@ -24,4 +27,11 @@ public enum AiMusicGenerateModeEnum {
      */
     private final String name;
 
+    public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(AiMusicGenerateModeEnum::getMode).toArray();
+
+    @Override
+    public int[] array() {
+        return ARRAYS;
+    }
+
 }

+ 11 - 1
yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/music/AiMusicStatusEnum.java

@@ -1,8 +1,11 @@
 package cn.iocoder.yudao.module.ai.enums.music;
 
+import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
 import lombok.AllArgsConstructor;
 import lombok.Getter;
 
+import java.util.Arrays;
+
 /**
  * AI 音乐状态的枚举
  *
@@ -10,7 +13,7 @@ import lombok.Getter;
  */
 @AllArgsConstructor
 @Getter
-public enum AiMusicStatusEnum {
+public enum AiMusicStatusEnum implements IntArrayValuable {
 
     IN_PROGRESS(10, "进行中"),
     SUCCESS(20, "已完成");
@@ -25,4 +28,11 @@ public enum AiMusicStatusEnum {
      */
     private final String name;
 
+    public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(AiMusicStatusEnum::getStatus).toArray();
+
+    @Override
+    public int[] array() {
+        return ARRAYS;
+    }
+
 }

+ 7 - 13
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/AiImageController.java

@@ -1,6 +1,5 @@
 package cn.iocoder.yudao.module.ai.controller.admin.image;
 
-import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.util.ObjUtil;
 import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi;
 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
@@ -26,9 +25,7 @@ import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.*;
 
-import java.util.Collections;
 import java.util.List;
-import java.util.Objects;
 
 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
 import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
@@ -42,15 +39,16 @@ public class AiImageController {
     @Resource
     private AiImageService imageService;
 
-    @Operation(summary = "获取【我的】绘图分页")
     @GetMapping("/my-page")
+    @Operation(summary = "获取【我的】绘图分页")
     public CommonResult<PageResult<AiImageRespVO>> getImagePageMy(@Validated PageParam pageReqVO) {
         PageResult<AiImageDO> pageResult = imageService.getImagePageMy(getLoginUserId(), pageReqVO);
         return success(BeanUtils.toBean(pageResult, AiImageRespVO.class));
     }
 
-    @Operation(summary = "获取【我的】绘图记录")
     @GetMapping("/get-my")
+    @Operation(summary = "获取【我的】绘图记录")
+    @Parameter(name = "id", required = true, description = "绘画编号", example = "1024")
     public CommonResult<AiImageRespVO> getImageMy(@RequestParam("id") Long id) {
         AiImageDO image = imageService.getImage(id);
         if (image == null || ObjUtil.notEqual(getLoginUserId(), image.getUserId())) {
@@ -59,17 +57,13 @@ public class AiImageController {
         return success(BeanUtils.toBean(image, AiImageRespVO.class));
     }
 
-    @Operation(summary = "获取【我的】绘图记录 - ids")
     @GetMapping("/get-my-ids")
+    @Operation(summary = "获取【我的】绘图记录列表")
+    @Parameter(name = "ids", required = true, description = "绘画编号数组", example = "1024,2048")
     public CommonResult<List<AiImageRespVO>> getImageMyIds(@RequestParam("ids") List<Long> ids) {
         List<AiImageDO> imageList = imageService.getImageByIds(ids);
-        if (CollUtil.isEmpty(imageList)) {
-            return success(Collections.emptyList());
-        }
-        List<AiImageDO> userImageList = imageList.stream()
-                .map(item -> ObjUtil.equal(getLoginUserId(), item.getUserId()) ? item : null)
-                .filter(Objects::nonNull).toList();
-        return success(BeanUtils.toBean(userImageList, AiImageRespVO.class));
+        imageList.removeIf(item -> !ObjUtil.equal(getLoginUserId(), item.getUserId()));
+        return success(BeanUtils.toBean(imageList, AiImageRespVO.class));
     }
 
     @Operation(summary = "生成图片")

+ 1 - 1
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/AiImagePageReqVO.java

@@ -20,7 +20,7 @@ public class AiImagePageReqVO extends PageParam {
     @Schema(description = "用户编号", example = "28987")
     private Long userId;
 
-    @Schema(description = "平台")
+    @Schema(description = "平台", example = "OpenAI")
     private String platform;
 
     @Schema(description = "绘画状态", example = "1")

+ 0 - 13
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/AiImageController.http

@@ -1,13 +0,0 @@
-### 生成音乐:Suno +
-POST {{baseUrl}}/ai/music/generate
-Content-Type: application/json
-Authorization: {{token}}
-
-{
-  "platform": "Suno",
-  "generateMode": 1,
-  "prompt": "来一首快乐的歌曲",
-  "modelVersion": "chirp-v3.5",
-  "tags": ["Happy"],
-  "title": "Happy Song"
-}

+ 26 - 0
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/AiMusicController.http

@@ -0,0 +1,26 @@
+### 生成音乐:Suno + 歌词模式
+POST {{baseUrl}}/ai/music/generate
+Content-Type: application/json
+Authorization: {{token}}
+
+{
+  "platform": "Suno",
+  "generateMode": 1,
+  "prompt": "来一首快乐的歌曲",
+  "modelVersion": "chirp-v3.5",
+  "tags": ["Happy"],
+  "title": "Happy Song"
+}
+
+### 生成音乐:Suno + 描述模式
+POST {{baseUrl}}/ai/music/generate
+Content-Type: application/json
+Authorization: {{token}}
+
+{
+  "platform": "Suno",
+  "generateMode": 2,
+  "prompt": "来一首快乐的歌曲",
+  "makeInstrumental": false,
+  "title": "Happy Song"
+}

+ 36 - 4
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/AiMusicController.java

@@ -1,16 +1,21 @@
 package cn.iocoder.yudao.module.ai.controller.admin.music;
 
 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.module.ai.controller.admin.music.vo.AiMusicPageReqVO;
+import cn.iocoder.yudao.module.ai.controller.admin.music.vo.AiMusicRespVO;
+import cn.iocoder.yudao.module.ai.controller.admin.music.vo.AiMusicUpdatePublicStatusReqVO;
 import cn.iocoder.yudao.module.ai.controller.admin.music.vo.AiSunoGenerateReqVO;
+import cn.iocoder.yudao.module.ai.dal.dataobject.music.AiMusicDO;
 import cn.iocoder.yudao.module.ai.service.music.AiMusicService;
 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.validation.Valid;
-import org.springframework.web.bind.annotation.PostMapping;
-import org.springframework.web.bind.annotation.RequestBody;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
 
 import java.util.List;
 
@@ -31,4 +36,31 @@ public class AiMusicController {
         return success(musicService.generateMusic(getLoginUserId(), reqVO));
     }
 
+    // ================ 绘图管理 ================
+
+    @GetMapping("/page")
+    @Operation(summary = "获得音乐分页")
+    @PreAuthorize("@ss.hasPermission('ai:music:query')")
+    public CommonResult<PageResult<AiMusicRespVO>> getMusicPage(@Valid AiMusicPageReqVO pageReqVO) {
+        PageResult<AiMusicDO> pageResult = musicService.getMusicPage(pageReqVO);
+        return success(BeanUtils.toBean(pageResult, AiMusicRespVO.class));
+    }
+
+    @DeleteMapping("/delete")
+    @Operation(summary = "删除音乐")
+    @Parameter(name = "id", description = "编号", required = true)
+    @PreAuthorize("@ss.hasPermission('ai:music:delete')")
+    public CommonResult<Boolean> deleteMusic(@RequestParam("id") Long id) {
+        musicService.deleteMusic(id);
+        return success(true);
+    }
+
+    @PutMapping("/update")
+    @Operation(summary = "更新音乐发布状态")
+    @PreAuthorize("@ss.hasPermission('ai:music:update')")
+    public CommonResult<Boolean> updateMusicPublicStatus(@Valid @RequestBody AiMusicUpdatePublicStatusReqVO updateReqVO) {
+        musicService.updateMusicPublicStatus(updateReqVO);
+        return success(true);
+    }
+
 }

+ 44 - 0
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/AiMusicPageReqVO.java

@@ -0,0 +1,44 @@
+package cn.iocoder.yudao.module.ai.controller.admin.music.vo;
+
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import cn.iocoder.yudao.framework.common.validation.InEnum;
+import cn.iocoder.yudao.module.ai.enums.music.AiMusicGenerateModeEnum;
+import cn.iocoder.yudao.module.ai.enums.music.AiMusicStatusEnum;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.time.LocalDateTime;
+
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+@Schema(description = "管理后台 - AI 音乐分页 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class AiMusicPageReqVO extends PageParam {
+
+    @Schema(description = "用户编号", example = "12212")
+    private Long userId;
+
+    @Schema(description = "音乐名称", example = "夜空中最亮的星")
+    private String title;
+
+    @Schema(description = "音乐状态", example = "20")
+    @InEnum(AiMusicStatusEnum.class)
+    private Integer status;
+
+    @Schema(description = "生成模式", example = "1")
+    @InEnum(AiMusicGenerateModeEnum.class)
+    private Integer generateMode;
+
+    @Schema(description = "是否发布", example = "true")
+    private Boolean publicStatus;
+
+    @Schema(description = "创建时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime[] createTime;
+
+}

+ 67 - 0
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/AiMusicRespVO.java

@@ -0,0 +1,67 @@
+package cn.iocoder.yudao.module.ai.controller.admin.music.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+@Schema(description = "管理后台 - AI 音乐 Response VO")
+@Data
+public class AiMusicRespVO {
+
+    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "24790")
+    private Long id;
+
+    @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "12212")
+    private Long userId;
+
+    @Schema(description = "音乐名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "夜空中最亮的星")
+    private String title;
+
+    @Schema(description = "歌词", example = "oh~卖糕的")
+    private String lyric;
+
+    @Schema(description = "图片地址", example = "https://www.iocoder.cn")
+    private String imageUrl;
+
+    @Schema(description = "音频地址", example = "https://www.iocoder.cn")
+    private String audioUrl;
+
+    @Schema(description = "视频地址", example = "https://www.iocoder.cn")
+    private String videoUrl;
+
+    @Schema(description = "音乐状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "20")
+    private Integer status;
+
+    @Schema(description = "描述词", example = "一首轻快的歌曲")
+    private String gptDescriptionPrompt;
+
+    @Schema(description = "提示词", example = "创作一首带有轻松吉他旋律的流行歌曲,[verse] 描述夏日海滩的宁静,[chorus] 节奏加快,表达对自由的向往。")
+    private String prompt;
+
+    @Schema(description = "模型平台", requiredMode = Schema.RequiredMode.REQUIRED, example = "Suno")
+    private String platform;
+
+    @Schema(description = "模型", requiredMode = Schema.RequiredMode.REQUIRED, example = "chirp-v3.5")
+    private String model;
+
+    @Schema(description = "生成模式", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    private Integer generateMode;
+
+    @Schema(description = "音乐风格标签")
+    private List<String> tags;
+
+    @Schema(description = "是否发布", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
+    private Boolean publicStatus;
+
+    @Schema(description = "任务编号", example = "11369")
+    private String taskId;
+
+    @Schema(description = "错误信息")
+    private String errorMessage;
+
+    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    private LocalDateTime createTime;
+
+}

+ 18 - 0
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/AiMusicUpdatePublicStatusReqVO.java

@@ -0,0 +1,18 @@
+package cn.iocoder.yudao.module.ai.controller.admin.music.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+@Schema(description = "管理后台 - AI 音乐修改发布状态 Request VO")
+@Data
+public class AiMusicUpdatePublicStatusReqVO {
+
+    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "15583")
+    private Long id;
+
+    @Schema(description = "是否发布", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
+    @NotNull(message = "是否发布不能为空")
+    private Boolean publicStatus;
+
+}

+ 8 - 1
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/AiSunoGenerateReqVO.java

@@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.ai.controller.admin.music.vo;
 
 import io.swagger.v3.oas.annotations.media.Schema;
 import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotEmpty;
 import jakarta.validation.constraints.NotNull;
 import lombok.Data;
 
@@ -15,6 +16,10 @@ public class AiSunoGenerateReqVO {
     @NotBlank(message = "平台不能为空")
     private String platform; // 参见 AiPlatformEnum 枚举
 
+    /**
+     * 1. 描述模式:描述词 + 是否纯音乐 + 模型 TODO @xin:目前貌似描述词没弄对?看着不是 prompt 字段(也可能我弄错了)。可以微信再沟通下哈
+     * 2. 歌词模式:歌词 + 音乐风格 + 标题 + 模型 TODO @xin:目前这块少传递了标题;
+     */
     @Schema(description = "生成模式", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
     @NotNull(message = "生成模式不能为空")
     private Integer generateMode; // 参见 AiMusicGenerateModeEnum 枚举
@@ -26,7 +31,9 @@ public class AiSunoGenerateReqVO {
     @Schema(description = "是否纯音乐", requiredMode = Schema.RequiredMode.NOT_REQUIRED, example = "true")
     private Boolean makeInstrumental;
 
-    @Schema(description = "模型版本", requiredMode = Schema.RequiredMode.NOT_REQUIRED, example = "chirp-v3.5")
+    // TODO @xin:看了下这个字段,发现最终还是 model 合适点;因为它其实是模型
+    @Schema(description = "模型版本", requiredMode = Schema.RequiredMode.REQUIRED, example = "chirp-v3.5")
+    @NotEmpty(message = "模型不能为空")
     private String modelVersion; // 参见 AiModelEnum 枚举
 
     @Schema(description = "音乐风格", requiredMode = Schema.RequiredMode.NOT_REQUIRED, example = "[\"pop\",\"jazz\",\"punk\"]")

+ 5 - 0
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/music/AiMusicDO.java

@@ -98,6 +98,11 @@ public class AiMusicDO extends BaseDO {
     @TableField(typeHandler = JacksonTypeHandler.class)
     private List<String> tags;
 
+    /**
+     * 是否公开
+     */
+    private Boolean publicStatus;
+
     /**
      * 任务编号
      */

+ 14 - 0
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/music/AiMusicMapper.java

@@ -1,6 +1,9 @@
 package cn.iocoder.yudao.module.ai.dal.mysql.music;
 
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
+import cn.iocoder.yudao.module.ai.controller.admin.music.vo.AiMusicPageReqVO;
 import cn.iocoder.yudao.module.ai.dal.dataobject.music.AiMusicDO;
 import org.apache.ibatis.annotations.Mapper;
 
@@ -18,4 +21,15 @@ public interface AiMusicMapper extends BaseMapperX<AiMusicDO> {
         return selectList(AiMusicDO::getStatus, status);
     }
 
+    default PageResult<AiMusicDO> selectPage(AiMusicPageReqVO reqVO) {
+        return selectPage(reqVO, new LambdaQueryWrapperX<AiMusicDO>()
+                .eqIfPresent(AiMusicDO::getUserId, reqVO.getUserId())
+                .eqIfPresent(AiMusicDO::getTitle, reqVO.getTitle())
+                .eqIfPresent(AiMusicDO::getStatus, reqVO.getStatus())
+                .eqIfPresent(AiMusicDO::getGenerateMode, reqVO.getGenerateMode())
+                .betweenIfPresent(AiMusicDO::getCreateTime, reqVO.getCreateTime())
+                .eqIfPresent(AiMusicDO::getPublicStatus, reqVO.getPublicStatus())
+                .orderByDesc(AiMusicDO::getId));
+    }
+
 }

+ 5 - 5
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatMessageServiceImpl.java

@@ -44,7 +44,7 @@ import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionU
 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.error;
 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
-import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.AI_CHAT_MESSAGE_NOT_EXIST;
+import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.CHAT_MESSAGE_NOT_EXIST;
 import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.CHAT_CONVERSATION_NOT_EXISTS;
 
 /**
@@ -150,7 +150,7 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
             log.error("[sendChatMessageStream][userId({}) sendReqVO({}) 发生异常]", userId, sendReqVO, throwable);
             chatMessageMapper.updateById(new AiChatMessageDO().setId(assistantMessage.getId()).setContent(throwable.getMessage()));
         }).onErrorResume(error -> {
-            return Flux.just(error(ErrorCodeConstants.AI_CHAT_STREAM_ERROR));
+            return Flux.just(error(ErrorCodeConstants.CHAT_STREAM_ERROR));
         });
     }
 
@@ -257,7 +257,7 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
         // 1. 校验消息存在
         AiChatMessageDO message = chatMessageMapper.selectById(id);
         if (message == null || ObjUtil.notEqual(message.getUserId(), userId)) {
-            throw exception(AI_CHAT_MESSAGE_NOT_EXIST);
+            throw exception(CHAT_MESSAGE_NOT_EXIST);
         }
         // 2. 执行删除
         chatMessageMapper.deleteById(id);
@@ -268,7 +268,7 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
         // 1. 校验消息存在
         List<AiChatMessageDO> messages = chatMessageMapper.selectListByConversationId(conversationId);
         if (CollUtil.isEmpty(messages) || ObjUtil.notEqual(messages.get(0).getUserId(), userId)) {
-            throw exception(AI_CHAT_MESSAGE_NOT_EXIST);
+            throw exception(CHAT_MESSAGE_NOT_EXIST);
         }
         // 2. 执行删除
         chatMessageMapper.deleteBatchIds(convertList(messages, AiChatMessageDO::getId));
@@ -279,7 +279,7 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
         // 1. 校验消息存在
         AiChatMessageDO message = chatMessageMapper.selectById(id);
         if (message == null) {
-            throw exception(AI_CHAT_MESSAGE_NOT_EXIST);
+            throw exception(CHAT_MESSAGE_NOT_EXIST);
         }
         // 2. 执行删除
         chatMessageMapper.deleteById(id);

+ 10 - 6
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageServiceImpl.java

@@ -35,6 +35,7 @@ import org.springframework.scheduling.annotation.Async;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
+import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 
@@ -76,6 +77,9 @@ public class AiImageServiceImpl implements AiImageService {
 
     @Override
     public List<AiImageDO> getImageByIds(List<Long> ids) {
+        if (CollUtil.isEmpty(ids)) {
+            return Collections.emptyList();
+        }
         return imageMapper.selectBatchIds(ids);
     }
 
@@ -135,7 +139,7 @@ public class AiImageServiceImpl implements AiImageService {
         // 1. 校验是否存在
         AiImageDO image = validateImageExists(id);
         if (ObjUtil.notEqual(image.getUserId(), userId)) {
-            throw exception(AI_IMAGE_NOT_EXISTS);
+            throw exception(IMAGE_NOT_EXISTS);
         }
         // 2. 删除记录
         imageMapper.deleteById(id);
@@ -165,7 +169,7 @@ public class AiImageServiceImpl implements AiImageService {
     private AiImageDO validateImageExists(Long id) {
         AiImageDO image = imageMapper.selectById(id);
         if (image == null) {
-            throw exception(AI_IMAGE_NOT_EXISTS);
+            throw exception(IMAGE_NOT_EXISTS);
         }
         return image;
     }
@@ -191,7 +195,7 @@ public class AiImageServiceImpl implements AiImageService {
         if (!MidjourneyApi.SubmitCodeEnum.SUCCESS_CODES.contains(imagineResponse.code())) {
             String description = imagineResponse.description().contains("quota_not_enough") ?
                     "账户余额不足" : imagineResponse.description();
-            throw exception(AI_IMAGE_MIDJOURNEY_SUBMIT_FAIL, description);
+            throw exception(IMAGE_MIDJOURNEY_SUBMIT_FAIL, description);
         }
 
         // 4. 情况二【成功】:更新 taskId 和参数
@@ -271,13 +275,13 @@ public class AiImageServiceImpl implements AiImageService {
         // 1.1 检查 image
         AiImageDO image = validateImageExists(reqVO.getId());
         if (ObjUtil.notEqual(userId, image.getUserId())) {
-            throw exception(AI_IMAGE_NOT_EXISTS);
+            throw exception(IMAGE_NOT_EXISTS);
         }
         // 1.2 检查 customId
         MidjourneyApi.Button button = CollUtil.findOne(image.getButtons(),
                 buttonX -> buttonX.customId().equals(reqVO.getCustomId()));
         if (button == null) {
-            throw exception(AI_IMAGE_CUSTOM_ID_NOT_EXISTS);
+            throw exception(IMAGE_CUSTOM_ID_NOT_EXISTS);
         }
 
         // 2. 调用 Midjourney Proxy 提交任务
@@ -286,7 +290,7 @@ public class AiImageServiceImpl implements AiImageService {
         if (!MidjourneyApi.SubmitCodeEnum.SUCCESS_CODES.contains(actionResponse.code())) {
             String description = actionResponse.description().contains("quota_not_enough") ?
                     "账户余额不足" : actionResponse.description();
-            throw exception(AI_IMAGE_MIDJOURNEY_SUBMIT_FAIL, description);
+            throw exception(IMAGE_MIDJOURNEY_SUBMIT_FAIL, description);
         }
 
         // 3. 新增 image 记录

+ 27 - 0
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/music/AiMusicService.java

@@ -1,6 +1,11 @@
 package cn.iocoder.yudao.module.ai.service.music;
 
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.ai.controller.admin.music.vo.AiMusicPageReqVO;
+import cn.iocoder.yudao.module.ai.controller.admin.music.vo.AiMusicUpdatePublicStatusReqVO;
 import cn.iocoder.yudao.module.ai.controller.admin.music.vo.AiSunoGenerateReqVO;
+import cn.iocoder.yudao.module.ai.dal.dataobject.music.AiMusicDO;
+import jakarta.validation.Valid;
 
 import java.util.List;
 
@@ -27,4 +32,26 @@ public interface AiMusicService {
      */
     Integer syncMusic();
 
+    /**
+     * 更新音乐发布状态
+     *
+     * @param updateReqVO 更新信息
+     */
+    void updateMusicPublicStatus(@Valid AiMusicUpdatePublicStatusReqVO updateReqVO);
+
+    /**
+     * 删除AI 音乐
+     *
+     * @param id 编号
+     */
+    void deleteMusic(Long id);
+
+    /**
+     * 获得音乐分页
+     *
+     * @param pageReqVO 分页查询
+     * @return 音乐分页
+     */
+    PageResult<AiMusicDO> getMusicPage(AiMusicPageReqVO pageReqVO);
+
 }

+ 36 - 2
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/music/AiMusicServiceImpl.java

@@ -4,6 +4,9 @@ import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.text.StrPool;
 import cn.hutool.core.util.StrUtil;
 import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.ai.controller.admin.music.vo.AiMusicPageReqVO;
+import cn.iocoder.yudao.module.ai.controller.admin.music.vo.AiMusicUpdatePublicStatusReqVO;
 import cn.iocoder.yudao.module.ai.controller.admin.music.vo.AiSunoGenerateReqVO;
 import cn.iocoder.yudao.module.ai.dal.dataobject.music.AiMusicDO;
 import cn.iocoder.yudao.module.ai.dal.mysql.music.AiMusicMapper;
@@ -15,8 +18,10 @@ import org.springframework.stereotype.Service;
 
 import java.util.*;
 
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
+import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.MUSIC_NOT_EXISTS;
 
 /**
  * AI 音乐 Service 实现类
@@ -56,7 +61,7 @@ public class AiMusicServiceImpl implements AiMusicService {
             return Collections.emptyList();
         }
         List<AiMusicDO> musicList = buildMusicDOList(musicDataList);
-        musicList.forEach(music -> music.setUserId(userId).setPlatform(music.getPlatform()).setGenerateMode(reqVO.getGenerateMode()));
+        musicList.forEach(music -> music.setUserId(userId).setPlatform(reqVO.getPlatform()).setGenerateMode(reqVO.getGenerateMode()));
         musicMapper.insertBatch(musicList);
         return convertList(musicList, AiMusicDO::getId);
     }
@@ -92,12 +97,41 @@ public class AiMusicServiceImpl implements AiMusicService {
      * @return AiMusicDO 集合
      */
     private static List<AiMusicDO> buildMusicDOList(List<SunoApi.MusicData> musicList) {
+        // TODO @xin:成功的情况下,需要下载到自己的文件服务器。参考图片的处理
         return convertList(musicList, musicData -> new AiMusicDO()
                 .setTaskId(musicData.id()).setModel(musicData.modelName())
                 .setPrompt(musicData.prompt()).setGptDescriptionPrompt(musicData.gptDescriptionPrompt())
                 .setAudioUrl(musicData.audioUrl()).setVideoUrl(musicData.videoUrl()).setImageUrl(musicData.imageUrl())
                 .setTitle(musicData.title()).setLyric(musicData.lyric()).setTags(StrUtil.split(musicData.tags(), StrPool.COMMA))
-                .setStatus(Objects.equals("complete", musicData.status()) ? AiMusicStatusEnum.SUCCESS.getStatus() : AiMusicStatusEnum.IN_PROGRESS.getStatus()));
+                .setStatus(Objects.equals("complete", musicData.status()) ?
+                        AiMusicStatusEnum.SUCCESS.getStatus() : AiMusicStatusEnum.IN_PROGRESS.getStatus()));
+    }
+
+    @Override
+    public void updateMusicPublicStatus(AiMusicUpdatePublicStatusReqVO updateReqVO) {
+        // 校验存在
+        validateMusicExists(updateReqVO.getId());
+        // 更新
+        musicMapper.updateBatch(new AiMusicDO().setPublicStatus(updateReqVO.getPublicStatus()));
+    }
+
+    @Override
+    public void deleteMusic(Long id) {
+        // 校验存在
+        validateMusicExists(id);
+        // 删除
+        musicMapper.deleteById(id);
+    }
+
+    private void validateMusicExists(Long id) {
+        if (musicMapper.selectById(id) == null) {
+            throw exception(MUSIC_NOT_EXISTS);
+        }
+    }
 
+    @Override
+    public PageResult<AiMusicDO> getMusicPage(AiMusicPageReqVO pageReqVO) {
+        return musicMapper.selectPage(pageReqVO);
     }
+
 }

+ 2 - 2
yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/music/SunoTests.java

@@ -17,7 +17,8 @@ public class SunoTests {
 
     @Before
     public void setup() {
-        String url = "https://suno-om0w1cy6e-status2xxs-projects.vercel.app";
+//        String url = "https://suno-om0w1cy6e-status2xxs-projects.vercel.app";
+        String url = "http://127.0.0.1:3001";
         this.sunoApi = new SunoApi(url);
     }
 
@@ -53,5 +54,4 @@ public class SunoTests {
         System.out.println(limitUsageData);
     }
 
-
 }

+ 2 - 1
yudao-server/src/main/resources/application.yaml

@@ -201,7 +201,8 @@ yudao.ai:
     notify-url: http://java.nat300.top/admin-api/ai/image/midjourney/notify
   suno:
     enable: true
-    base-url: https://suno-om0w1cy6e-status2xxs-projects.vercel.app
+#    base-url: https://suno-om0w1cy6e-status2xxs-projects.vercel.app
+    base-url: http://127.0.0.1:3001
 
 --- #################### 芋道相关配置 ####################