Browse Source

Merge branch 'dev-yhq'

tsurumure 1 tháng trước cách đây
mục cha
commit
7cf5c1e93b
76 tập tin đã thay đổi với 2111 bổ sung377 xóa
  1. 0 0
      configuration/docker/docker-drone.md
  2. 3 5
      configuration/docker/docker-rabbitmq.md
  3. 21 0
      db/__comfyui_task.sql
  4. 42 0
      db/ai_material.sql
  5. 25 0
      db/ai_material_category.sql
  6. 53 0
      db/ai_material_tag.sql
  7. 19 19
      db/ai_media_ttv_timbre.sql
  8. 12 3
      db/comfyui_task.sql
  9. 0 26
      db/comfyui_task_execute.sql
  10. 3 2
      db/sys_user_role.sql
  11. 3 5
      db/sys_user_role_menu.sql
  12. 14 17
      db/sys_user_role_permission.sql
  13. 23 9
      db/sys_user_role_permission_relation.sql
  14. 1 1
      db/sys_user_role_relation.sql
  15. 5 4
      src/main/java/com/backendsys/modules/TestController.java
  16. 4 1
      src/main/java/com/backendsys/modules/common/Filter/WebClientFilter.java
  17. 88 0
      src/main/java/com/backendsys/modules/common/config/rabbitmq/DemoRabbitListener.java
  18. 45 0
      src/main/java/com/backendsys/modules/common/config/rabbitmq/DemoRabbitListenerRunner.java
  19. 0 62
      src/main/java/com/backendsys/modules/common/config/rabbitmq/RabbitListener.java
  20. 0 41
      src/main/java/com/backendsys/modules/common/config/rabbitmq/RabbitListenerRunner.java
  21. 30 0
      src/main/java/com/backendsys/modules/common/config/rabbitmq/queue/DemoDlxQueueConfig.java
  22. 8 8
      src/main/java/com/backendsys/modules/common/config/rabbitmq/queue/DemoQueueConfig.java
  23. 0 30
      src/main/java/com/backendsys/modules/common/config/rabbitmq/queue/QueueDlxConfig.java
  24. 7 2
      src/main/java/com/backendsys/modules/common/config/security/utils/JwtUtil.java
  25. 1 1
      src/main/java/com/backendsys/modules/crt/controller/CrtGenerateController.java
  26. 1 1
      src/main/java/com/backendsys/modules/crt/service/CrtGenerateService.java
  27. 1 2
      src/main/java/com/backendsys/modules/crt/service/impl/CrtDramaProjectStoryboardServiceImpl.java
  28. 23 12
      src/main/java/com/backendsys/modules/crt/service/impl/CrtGenerateServiceImpl.java
  29. 35 0
      src/main/java/com/backendsys/modules/material/controller/MaterialCategoryController.java
  30. 62 0
      src/main/java/com/backendsys/modules/material/controller/MaterialController.java
  31. 36 0
      src/main/java/com/backendsys/modules/material/controller/MaterialTagController.java
  32. 15 0
      src/main/java/com/backendsys/modules/material/dao/MaterialCategoryDao.java
  33. 24 0
      src/main/java/com/backendsys/modules/material/dao/MaterialDao.java
  34. 16 0
      src/main/java/com/backendsys/modules/material/dao/MaterialTagDao.java
  35. 70 0
      src/main/java/com/backendsys/modules/material/entity/Material.java
  36. 37 0
      src/main/java/com/backendsys/modules/material/entity/MaterialCategory.java
  37. 43 0
      src/main/java/com/backendsys/modules/material/entity/MaterialTag.java
  38. 15 0
      src/main/java/com/backendsys/modules/material/service/MaterialCategoryService.java
  39. 25 0
      src/main/java/com/backendsys/modules/material/service/MaterialService.java
  40. 15 0
      src/main/java/com/backendsys/modules/material/service/MaterialTagService.java
  41. 38 0
      src/main/java/com/backendsys/modules/material/service/impl/MaterialCategoryImpl.java
  42. 173 0
      src/main/java/com/backendsys/modules/material/service/impl/MaterialServiceImpl.java
  43. 49 0
      src/main/java/com/backendsys/modules/material/service/impl/MaterialTagImpl.java
  44. 13 5
      src/main/java/com/backendsys/modules/sdk/comfyui/controller/ComfyuiDemoController.java
  45. 2 2
      src/main/java/com/backendsys/modules/sdk/comfyui/dao/ComfyuiTaskExecuteDao.java
  46. 9 0
      src/main/java/com/backendsys/modules/sdk/comfyui/entity/ComfyuiQueueItem.java
  47. 3 1
      src/main/java/com/backendsys/modules/sdk/comfyui/entity/ComfyuiResponse.java
  48. 10 1
      src/main/java/com/backendsys/modules/sdk/comfyui/entity/ComfyuiTask.java
  49. 0 29
      src/main/java/com/backendsys/modules/sdk/comfyui/entity/ComfyuiTaskExecute.java
  50. 23 0
      src/main/java/com/backendsys/modules/sdk/comfyui/entity/__ComfyuiTask.java
  51. 46 0
      src/main/java/com/backendsys/modules/sdk/comfyui/rabbitmq/ComfyuiQueueConfig.java
  52. 30 0
      src/main/java/com/backendsys/modules/sdk/comfyui/rabbitmq/ComfyuiQueueDlxConfig.java
  53. 137 0
      src/main/java/com/backendsys/modules/sdk/comfyui/rabbitmq/ComfyuiRabbitListener.java
  54. 21 0
      src/main/java/com/backendsys/modules/sdk/comfyui/rabbitmq/DeliveryTagHolder.java
  55. 10 0
      src/main/java/com/backendsys/modules/sdk/comfyui/rabbitmq/TagCtx.java
  56. 5 1
      src/main/java/com/backendsys/modules/sdk/comfyui/service/ComfyuiService.java
  57. 3 0
      src/main/java/com/backendsys/modules/sdk/comfyui/service/ComfyuiSocketService.java
  58. 9 0
      src/main/java/com/backendsys/modules/sdk/comfyui/service/ComfyuiTaskService.java
  59. 126 7
      src/main/java/com/backendsys/modules/sdk/comfyui/service/impl/ComfyuiServiceImpl.java
  60. 106 12
      src/main/java/com/backendsys/modules/sdk/comfyui/service/impl/ComfyuiSocketServiceImpl.java
  61. 110 0
      src/main/java/com/backendsys/modules/sdk/comfyui/service/impl/ComfyuiTaskServiceImpl.java
  62. 8 12
      src/main/java/com/backendsys/modules/sdk/comfyui/service/impl/ComfyuiText2ImageServiceImpl.java
  63. 9 10
      src/main/java/com/backendsys/modules/sdk/comfyui/utils/ComfyuiUtil.java
  64. 4 0
      src/main/java/com/backendsys/modules/upload/service/SysFileService.java
  65. 27 2
      src/main/java/com/backendsys/modules/upload/service/impl/SysFileServiceImpl.java
  66. 11 0
      src/main/java/com/backendsys/modules/upload/utils/ObjectKey/ObjectKeyEntity.java
  67. 38 1
      src/main/java/com/backendsys/modules/upload/utils/ObjectKey/ObjectKeyUtil.java
  68. 12 5
      src/main/java/com/backendsys/modules/upload/utils/UploadUtil.java
  69. 0 5
      src/main/java/com/backendsys/service/SDKService/SDKTencent/SDKTencentCOSServiceImpl.java
  70. 11 9
      src/main/resources/application-dev.yml
  71. 12 15
      src/main/resources/application-local.yml
  72. 11 9
      src/main/resources/application-prod.yml
  73. 1 0
      src/main/resources/application.yml
  74. 32 0
      src/main/resources/mapper/ai/material/MaterialCategoryDao.xml
  75. 148 0
      src/main/resources/mapper/ai/material/MaterialDao.xml
  76. 39 0
      src/main/resources/mapper/ai/material/MaterialTagDao.xml

+ 0 - 0
configuration/drone/docker.md → configuration/docker/docker-drone.md


+ 3 - 5
configuration/docker.md → configuration/docker/docker-rabbitmq.md

@@ -1,12 +1,11 @@
 ## RabbitMQ
 
 #### Prod
+> 默认端口映射:--network host
 ```bash
 docker pull rabbitmq:4.1.2-management
 docker run -id \
     --name=rabbitmq \
-    -p 15672:15672 \
-    -p 5672:5672 \
     --network host \
     --restart=unless-stopped \
     -v /app/rabbitmq:/var/lib/rabbitmq \
@@ -21,9 +20,8 @@ docker run -id \
 docker pull rabbitmq:4.1.2-management
 docker run -id \
     --name=rabbitmqdev \
-    -p 15672:15670 \
-    -p 5672:5670 \
-    --network host \
+    -p 15670:15672 \
+    -p 5670:5672 \
     --restart=unless-stopped \
     -v /app/rabbitmqdev:/var/lib/rabbitmq \
     -v /app/rabbitmqdev/log:/var/log/rabbitmq \

+ 21 - 0
db/__comfyui_task.sql

@@ -0,0 +1,21 @@
+# /**
+# Source Server Version: 8.0.31
+# Source Database: backendsys
+# Date: 2025/06/03 10:09:22
+# */
+#
+# DROP TABLE IF EXISTS `comfyui_task`;
+# CREATE TABLE `comfyui_task` (
+#     PRIMARY KEY (`id`),
+#     `id` BIGINT AUTO_INCREMENT COMMENT 'ID',
+#     `user_id` BIGINT NOT NULL COMMENT '用户ID',
+#     `client_id` VARCHAR(255) COMMENT 'Client ID',
+#     `task_type` VARCHAR(255) NOT NULL COMMENT '任务类型 (Text2Image, ..)',
+#     `task_status` TINYINT DEFAULT '-1' COMMENT '任务状态 (-1:未提交, 1:已提交)',
+#     `generate_request` TEXT COMMENT '任务请求原始参数 (JSONString)',
+#     `in_master` TINYINT DEFAULT '-1' COMMENT '队列状态 (-1:未进主队列, 1:已进主队列)',
+#     `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+#     `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+#     INDEX `idx_user_id` (`user_id`),
+#     INDEX `idx_in_master` (`in_master`)
+# ) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='任务表';

+ 42 - 0
db/ai_material.sql

@@ -0,0 +1,42 @@
+/**
+Source Server Version: 8.0.31
+Source Database: backendsys
+Date: 2025/07/28 15:57:12
+*/
+
+DROP TABLE IF EXISTS `ai_material`;
+CREATE TABLE `ai_material` (
+    PRIMARY KEY (`id`),
+    `id` BIGINT AUTO_INCREMENT COMMENT 'ID',
+    `user_id` BIGINT NOT NULL COMMENT '上传用户ID',
+    `category_id` BIGINT NOT NULL COMMENT '素材分类ID',
+    `tag_ids` VARCHAR(255) COMMENT '素材标签ID(多个以逗号间隔)',
+    `material_name` VARCHAR(100) NOT NULL COMMENT '素材名称',
+    `image_thumb_url` VARCHAR(1000) COMMENT '缩略图',
+    `image_url` VARCHAR(1000) COMMENT '高清图',
+    `fla_url` VARCHAR(1000) COMMENT 'FLA文件',
+    `psd_url` VARCHAR(1000) COMMENT 'PSD文件',
+    `is_copyright` TINYINT DEFAULT '-1' COMMENT '是否有版权 (-1否, 1是)',
+    `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+    `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+    INDEX `idx_user_id` (`user_id`),
+    INDEX `idx_category_id` (`category_id`),
+    INDEX `idx_tag_ids` (`tag_ids`),
+    INDEX `idx_material_name` (`material_name`)
+) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='素材表';
+
+INSERT INTO ai_material(user_id, category_id, tag_ids, material_name, image_thumb_url, image_url, fla_url, psd_url, is_copyright, create_time) VALUES
+    (1, 1, '1,2', '御姐魔女-1', 'https://www.xxx.com/xx.png', 'https://www.xxx.com/xx.png', 'https://www.xxx.com/xx.fla', 'https://www.xxx.com/xx.psd', 1, '2025-07-28 08:30:01'),
+    (1, 1, '1,2', '御姐魔女-2', 'https://www.xxx.com/xx.png', 'https://www.xxx.com/xx.png', 'https://www.xxx.com/xx.fla', 'https://www.xxx.com/xx.psd', 1, '2025-07-28 08:30:02'),
+    (1, 1, '1,2', '御姐魔女-3', 'https://www.xxx.com/xx.png', 'https://www.xxx.com/xx.png', 'https://www.xxx.com/xx.fla', 'https://www.xxx.com/xx.psd', 1, '2025-07-28 08:30:03'),
+    (1, 1, '1,2', '御姐魔女-4', 'https://www.xxx.com/xx.png', 'https://www.xxx.com/xx.png', 'https://www.xxx.com/xx.fla', 'https://www.xxx.com/xx.psd', 1, '2025-07-28 08:30:04'),
+    (1, 1, '1,2', '御姐魔女-5', 'https://www.xxx.com/xx.png', 'https://www.xxx.com/xx.png', 'https://www.xxx.com/xx.fla', 'https://www.xxx.com/xx.psd', 1, '2025-07-28 08:30:05'),
+    (1, 1, '1,2', '御姐魔女-6', 'https://www.xxx.com/xx.png', 'https://www.xxx.com/xx.png', 'https://www.xxx.com/xx.fla', 'https://www.xxx.com/xx.psd', 1, '2025-07-28 08:30:06'),
+    (1, 1, '1,2', '御姐魔女-7', 'https://www.xxx.com/xx.png', 'https://www.xxx.com/xx.png', 'https://www.xxx.com/xx.fla', 'https://www.xxx.com/xx.psd', 1, '2025-07-28 08:30:07'),
+    (1, 1, '4,5', '御姐魔女-8', 'https://www.xxx.com/xx.png', 'https://www.xxx.com/xx.png', 'https://www.xxx.com/xx.fla', 'https://www.xxx.com/xx.psd', 1, '2025-07-28 08:30:08'),
+    (1, 1, '3,4,5', '御姐魔女-9', 'https://www.xxx.com/xx.png', 'https://www.xxx.com/xx.png', 'https://www.xxx.com/xx.fla', 'https://www.xxx.com/xx.psd', 1, '2025-07-28 08:30:09'),
+    (1, 1, '3', '御姐魔女-10', 'https://www.xxx.com/xx.png', 'https://www.xxx.com/xx.png', 'https://www.xxx.com/xx.fla', 'https://www.xxx.com/xx.psd', 1, '2025-07-28 08:30:10'),
+    (1, 2, '1,2', '御姐魔女-11', 'https://www.xxx.com/xx.png', 'https://www.xxx.com/xx.png', 'https://www.xxx.com/xx.fla', 'https://www.xxx.com/xx.psd', 1, '2025-07-28 08:30:11'),
+    (1, 1, '1,2', '竹屋内', 'https://cos.daogu.ai/temp/d2361395-6e39-4a01-8c3b-1c79095f4391.png?imageMogr2/thumbnail/315x180/pad/1/color/I2Y4ZjhmOA==', 'https://cos.daogu.ai/temp/d2361395-6e39-4a01-8c3b-1c79095f4391.png', 'https://cos.daogu.ai/temp/2d0e951d-d894-4f88-8904-bf9f1d4797f5.fla', 'https://cos.daogu.ai/temp/aba04bf3-fa42-48d7-923f-b223b7aee22c.psd', 1, '2025-07-30 08:30:00')
+;
+

+ 25 - 0
db/ai_material_category.sql

@@ -0,0 +1,25 @@
+/**
+Source Server Version: 8.0.31
+Source Database: backendsys
+Date: 2025/07/28 15:57:12
+*/
+
+DROP TABLE IF EXISTS `ai_material_category`;
+CREATE TABLE `ai_material_category` (
+    PRIMARY KEY (`id`),
+    `id` BIGINT AUTO_INCREMENT COMMENT 'ID',
+    `category_name` VARCHAR(255) NOT NULL COMMENT '素材分类名称',
+    `sort` INT DEFAULT '1' COMMENT '排序',
+    INDEX `idx_category_name` (`category_name`)
+) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='素材分类表';
+
+INSERT INTO ai_material_category(category_name, sort) VALUES
+    ('人物素材', 7),
+    ('场景素材', 6),
+    ('表情素材', 5),
+    ('兽类素材', 4),
+    ('道具素材', 3),
+    ('特效素材', 2),
+    ('AI素材', 1)
+;
+

+ 53 - 0
db/ai_material_tag.sql

@@ -0,0 +1,53 @@
+/**
+Source Server Version: 8.0.31
+Source Database: backendsys
+Date: 2025/07/28 15:57:12
+*/
+
+DROP TABLE IF EXISTS `ai_material_tag`;
+CREATE TABLE `ai_material_tag` (
+    PRIMARY KEY (`id`),
+    `id` BIGINT AUTO_INCREMENT COMMENT 'ID',
+    `category_id` BIGINT NOT NULL COMMENT '素材分类ID',
+    `tag_name` VARCHAR(255) NOT NULL COMMENT '素材标签名称',
+    `sort` INT DEFAULT '1' COMMENT '排序',
+    INDEX `idx_tag_name` (`tag_name`),
+    INDEX `idx_category_id` (`category_id`)
+) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='素材标签表';
+
+INSERT INTO ai_material_tag(category_id, tag_name, sort) VALUES
+    # 1.人物素材
+    (1, '都市人物', 10),
+    (1, '历史人物', 9),
+    (1, '修仙人物', 8),
+    (1, '灵异人物', 7),
+    (1, '主题人物', 6),
+    (1, '火影人物', 5),
+    (1, '海贼人物', 4),
+    (1, '兽世人物', 3),
+    (1, '精选人物', 2),
+    (1, 'Q版人物', 1),
+    # 2.场景素材
+    (2, '都市场景', 7),
+    (2, '修仙古风', 6),
+    (2, '主题场景', 5),
+    (2, '通用场景', 4),
+    (2, '末日悬疑', 3),
+    (2, '兽世古代', 2),
+    (2, '星际宇宙', 1),
+    # 3.表情素材
+    (3, '身体部件', 2),
+    (3, '人物表情', 1),
+    # 4.兽类素材
+    (4, '异兽素材', 2),
+    (4, '动物素材', 1),
+    # 5.道具素材
+    (5, '通用道具', 3),
+    (5, '交通工具', 2),
+    (5, '武器装备', 1),
+    # 6.特效素材
+    (6, '特效素材', 1),
+    # 7.AI素材
+    (7, 'AI素材', 1)
+;
+

+ 19 - 19
db/ai_media_ttv_timbre.sql

@@ -19,23 +19,23 @@ CREATE TABLE `ai_media_ttv_timbre` (
 
 
 INSERT INTO ai_media_ttv_timbre(timbre_key, category_id, name, tag, description, avatar, audio_url) VALUES
-    ('0', '2', '小美-女', '成熟女声', '可使用于金融、零售、广告场景', 'http://cos.daogu.ai/materials/baidu/ai-generate-video/avatar/female-1.png', 'http://cos.daogu.ai/materials/baidu/ai-generate-video/audio/0:小美-女.mp3'),
-    ('1', '1', '小宇-男', '温润男声', '可使用于小说、故事场景', 'http://cos.daogu.ai/materials/baidu/ai-generate-video/avatar/male-1.png', 'http://cos.daogu.ai/materials/baidu/ai-generate-video/audio/1:小宇-男.mp3'),
-    ('3', '1', '小云-男', '磁性男声', '可使用于通用场景', 'http://cos.daogu.ai/materials/baidu/ai-generate-video/avatar/male-1.png', 'http://cos.daogu.ai/materials/baidu/ai-generate-video/audio/3:小云-男.mp3'),
-    ('4', '4', '小丫-女童', '乖巧童声', '可使用于小说、故事场景', 'http://cos.daogu.ai/materials/baidu/ai-generate-video/avatar/female-child-1.png', 'http://cos.daogu.ai/materials/baidu/ai-generate-video/audio/4:小丫-女童.mp3'),
-    ('5', '2', '小娇-女', '情感女声', '可使用于小说、故事场景', 'http://cos.daogu.ai/materials/baidu/ai-generate-video/avatar/female-1.png', 'http://cos.daogu.ai/materials/baidu/ai-generate-video/audio/5:小娇-女.mp3'),
-    ('103', '4', '小朵-女童', '可爱童声', '可使用于通用场景', 'http://cos.daogu.ai/materials/baidu/ai-generate-video/avatar/female-child-1.png', 'http://cos.daogu.ai/materials/baidu/ai-generate-video/audio/103:小朵-女童.mp3'),
-    ('106', '1', '小博-男', '成熟男声', '可使用于新闻、金融、零售场景', 'http://cos.daogu.ai/materials/baidu/ai-generate-video/avatar/male-1.png', 'http://cos.daogu.ai/materials/baidu/ai-generate-video/audio/106:小博-男.mp3'),
-    ('110', '3', '小童-男童', '活泼童声', '可使用于通用场景', 'http://cos.daogu.ai/materials/baidu/ai-generate-video/avatar/male-child-1.png', 'http://cos.daogu.ai/materials/baidu/ai-generate-video/audio/110:小童-男童.mp3'),
-    ('111', '2', '小萌-女', '萝莉女声', '可使用于社交、游戏场景', 'http://cos.daogu.ai/materials/baidu/ai-generate-video/avatar/female-child-1.png', 'http://cos.daogu.ai/materials/baidu/ai-generate-video/audio/111:小萌-女.mp3'),
-    ('4003', '1', '小耀-男', '磁性男声', '可使用于金融、零售、广告场景', 'http://cos.daogu.ai/materials/baidu/ai-generate-video/avatar/male-1.png', 'http://cos.daogu.ai/materials/baidu/ai-generate-video/audio/4003:小耀-男.mp3'),
-    ('4100', '2', '小雯-女', '成熟女声', '可使用于新闻、金融、零售场景', 'http://cos.daogu.ai/materials/baidu/ai-generate-video/avatar/female-1.png', 'http://cos.daogu.ai/materials/baidu/ai-generate-video/audio/4100:小雯-女.mp3'),
-    ('4103', '3', '小米-男童', '可爱童声', '可使用于通用场景', 'http://cos.daogu.ai/materials/baidu/ai-generate-video/avatar/male-child-1.png', 'http://cos.daogu.ai/materials/baidu/ai-generate-video/audio/4103:小米-男童.mp3'),
-    ('4105', '2', '小灵-女', '清澈女声', '可使用于通用场景', 'http://cos.daogu.ai/materials/baidu/ai-generate-video/avatar/female-1.png', 'http://cos.daogu.ai/materials/baidu/ai-generate-video/audio/4105:小灵-女.mp3'),
-    ('4106', '1', '小文-男', '情感男声', '可使用于社交、游戏场景', 'http://cos.daogu.ai/materials/baidu/ai-generate-video/avatar/male-1.png', 'http://cos.daogu.ai/materials/baidu/ai-generate-video/audio/4106:小文-男.mp3'),
-    ('4115', '1', '小贤-男', '情感男声', '可使用于社交、故事场景', 'http://cos.daogu.ai/materials/baidu/ai-generate-video/avatar/male-1.png', 'http://cos.daogu.ai/materials/baidu/ai-generate-video/audio/4115:小贤-男.mp3'),
-    ('4117', '2', '小乔-女', '情感女声', '可使用于社交、故事场景', 'http://cos.daogu.ai/materials/baidu/ai-generate-video/avatar/female-1.png', 'http://cos.daogu.ai/materials/baidu/ai-generate-video/audio/4117:小乔-女.mp3'),
-    ('4119', '2', '小鹿-女', '甜美女声', '可使用于社交、游戏场景', 'http://cos.daogu.ai/materials/baidu/ai-generate-video/avatar/female-1.png', 'http://cos.daogu.ai/materials/baidu/ai-generate-video/audio/4119:小鹿-女.mp3'),
-    ('5003', '1', '小遥-男', '甜美男声', '可使用于社交、故事场景', 'http://cos.daogu.ai/materials/baidu/ai-generate-video/avatar/male-1.png', 'http://cos.daogu.ai/materials/baidu/ai-generate-video/audio/5003:小遥-男.mp3'),
-    ('5118', '2', '小婷-女', '甜美女声', '可使用于社交、游戏场景', 'http://cos.daogu.ai/materials/baidu/ai-generate-video/avatar/female-1.png', 'http://cos.daogu.ai/materials/baidu/ai-generate-video/audio/5118:小婷-女.mp3')
+    ('0', '2', '小美-女', '成熟女声', '可使用于金融、零售、广告场景', 'https://cos.daogu.ai/materials/baidu/ai-generate-video/avatar/female-1.png', 'https://cos.daogu.ai/materials/baidu/ai-generate-video/audio/0:小美-女.mp3'),
+    ('1', '1', '小宇-男', '温润男声', '可使用于小说、故事场景', 'https://cos.daogu.ai/materials/baidu/ai-generate-video/avatar/male-1.png', 'https://cos.daogu.ai/materials/baidu/ai-generate-video/audio/1:小宇-男.mp3'),
+    ('3', '1', '小云-男', '磁性男声', '可使用于通用场景', 'https://cos.daogu.ai/materials/baidu/ai-generate-video/avatar/male-1.png', 'https://cos.daogu.ai/materials/baidu/ai-generate-video/audio/3:小云-男.mp3'),
+    ('4', '4', '小丫-女童', '乖巧童声', '可使用于小说、故事场景', 'https://cos.daogu.ai/materials/baidu/ai-generate-video/avatar/female-child-1.png', 'https://cos.daogu.ai/materials/baidu/ai-generate-video/audio/4:小丫-女童.mp3'),
+    ('5', '2', '小娇-女', '情感女声', '可使用于小说、故事场景', 'https://cos.daogu.ai/materials/baidu/ai-generate-video/avatar/female-1.png', 'https://cos.daogu.ai/materials/baidu/ai-generate-video/audio/5:小娇-女.mp3'),
+    ('103', '4', '小朵-女童', '可爱童声', '可使用于通用场景', 'https://cos.daogu.ai/materials/baidu/ai-generate-video/avatar/female-child-1.png', 'https://cos.daogu.ai/materials/baidu/ai-generate-video/audio/103:小朵-女童.mp3'),
+    ('106', '1', '小博-男', '成熟男声', '可使用于新闻、金融、零售场景', 'https://cos.daogu.ai/materials/baidu/ai-generate-video/avatar/male-1.png', 'https://cos.daogu.ai/materials/baidu/ai-generate-video/audio/106:小博-男.mp3'),
+    ('110', '3', '小童-男童', '活泼童声', '可使用于通用场景', 'https://cos.daogu.ai/materials/baidu/ai-generate-video/avatar/male-child-1.png', 'https://cos.daogu.ai/materials/baidu/ai-generate-video/audio/110:小童-男童.mp3'),
+    ('111', '2', '小萌-女', '萝莉女声', '可使用于社交、游戏场景', 'https://cos.daogu.ai/materials/baidu/ai-generate-video/avatar/female-child-1.png', 'https://cos.daogu.ai/materials/baidu/ai-generate-video/audio/111:小萌-女.mp3'),
+    ('4003', '1', '小耀-男', '磁性男声', '可使用于金融、零售、广告场景', 'https://cos.daogu.ai/materials/baidu/ai-generate-video/avatar/male-1.png', 'https://cos.daogu.ai/materials/baidu/ai-generate-video/audio/4003:小耀-男.mp3'),
+    ('4100', '2', '小雯-女', '成熟女声', '可使用于新闻、金融、零售场景', 'https://cos.daogu.ai/materials/baidu/ai-generate-video/avatar/female-1.png', 'https://cos.daogu.ai/materials/baidu/ai-generate-video/audio/4100:小雯-女.mp3'),
+    ('4103', '3', '小米-男童', '可爱童声', '可使用于通用场景', 'https://cos.daogu.ai/materials/baidu/ai-generate-video/avatar/male-child-1.png', 'https://cos.daogu.ai/materials/baidu/ai-generate-video/audio/4103:小米-男童.mp3'),
+    ('4105', '2', '小灵-女', '清澈女声', '可使用于通用场景', 'https://cos.daogu.ai/materials/baidu/ai-generate-video/avatar/female-1.png', 'https://cos.daogu.ai/materials/baidu/ai-generate-video/audio/4105:小灵-女.mp3'),
+    ('4106', '1', '小文-男', '情感男声', '可使用于社交、游戏场景', 'https://cos.daogu.ai/materials/baidu/ai-generate-video/avatar/male-1.png', 'https://cos.daogu.ai/materials/baidu/ai-generate-video/audio/4106:小文-男.mp3'),
+    ('4115', '1', '小贤-男', '情感男声', '可使用于社交、故事场景', 'https://cos.daogu.ai/materials/baidu/ai-generate-video/avatar/male-1.png', 'https://cos.daogu.ai/materials/baidu/ai-generate-video/audio/4115:小贤-男.mp3'),
+    ('4117', '2', '小乔-女', '情感女声', '可使用于社交、故事场景', 'https://cos.daogu.ai/materials/baidu/ai-generate-video/avatar/female-1.png', 'https://cos.daogu.ai/materials/baidu/ai-generate-video/audio/4117:小乔-女.mp3'),
+    ('4119', '2', '小鹿-女', '甜美女声', '可使用于社交、游戏场景', 'https://cos.daogu.ai/materials/baidu/ai-generate-video/avatar/female-1.png', 'https://cos.daogu.ai/materials/baidu/ai-generate-video/audio/4119:小鹿-女.mp3'),
+    ('5003', '1', '小遥-男', '甜美男声', '可使用于社交、故事场景', 'https://cos.daogu.ai/materials/baidu/ai-generate-video/avatar/male-1.png', 'https://cos.daogu.ai/materials/baidu/ai-generate-video/audio/5003:小遥-男.mp3'),
+    ('5118', '2', '小婷-女', '甜美女声', '可使用于社交、游戏场景', 'https://cos.daogu.ai/materials/baidu/ai-generate-video/avatar/female-1.png', 'https://cos.daogu.ai/materials/baidu/ai-generate-video/audio/5118:小婷-女.mp3')
 ;

+ 12 - 3
db/comfyui_task.sql

@@ -11,9 +11,18 @@ CREATE TABLE `comfyui_task` (
     `user_id` BIGINT NOT NULL COMMENT '用户ID',
     `client_id` VARCHAR(255) COMMENT 'Client ID',
     `task_type` VARCHAR(255) NOT NULL COMMENT '任务类型 (Text2Image, ..)',
-    `task_status` TINYINT DEFAULT '-1' COMMENT '任务状态 (-1:未提交, 1:已提交)',
+    `execute_prompt_id` VARCHAR(255) COMMENT '任务提交ID',
+    `execute_status` TINYINT DEFAULT '-1' COMMENT '任务执行状态 (-1:未开始, 1:进行中, 2:成功, 3:失败)',
+    `execute_url` VARCHAR(2000) COMMENT '任务执行URL',
+    `execute_url_port` INT COMMENT '任务执行URL端口',
     `generate_request` TEXT COMMENT '任务请求原始参数 (JSONString)',
+    `generate_response` TEXT COMMENT '任务生成原始结果 (JSONString)',
+    `reason` TEXT COMMENT '当任务失败时展示失败原因(如触发平台的内容风控等)',
+    `in_master` TINYINT DEFAULT '-1' COMMENT '队列状态 (-1:未进主队列, 1:已进主队列)',
+    `custom_params` VARCHAR(255) COMMENT '自定义参数',
     `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
     `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
-    INDEX `idx_user_id` (`user_id`)
-) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='任务表';
+    INDEX `idx_execute_prompt_id` (`execute_prompt_id`),
+    INDEX `idx_user_id` (`user_id`),
+    INDEX `idx_in_master` (`in_master`)
+) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='任务执行表';

+ 0 - 26
db/comfyui_task_execute.sql

@@ -1,26 +0,0 @@
-/**
-Source Server Version: 8.0.31
-Source Database: backendsys
-Date: 2025/06/03 10:09:22
-*/
-
-DROP TABLE IF EXISTS `comfyui_task_execute`;
-CREATE TABLE `comfyui_task_execute` (
-    PRIMARY KEY (`id`),
-    `id` BIGINT AUTO_INCREMENT COMMENT 'ID',
-    `task_id` BIGINT NOT NULL COMMENT '任务ID',
-    `user_id` BIGINT NOT NULL COMMENT '用户ID',
-    `client_id` VARCHAR(255) COMMENT 'Client ID',
-    `task_type` VARCHAR(255) NOT NULL COMMENT '任务类型 (Text2Image, ..)',
-    `execute_prompt_id` VARCHAR(255) NOT NULL COMMENT '任务提交ID',
-    `execute_status` TINYINT DEFAULT '-1' COMMENT '任务执行状态 (-1:未开始, 1:进行中, 2:成功, 3:失败)',
-    `execute_url` VARCHAR(2000) COMMENT '任务执行URL',
-    `execute_url_port` INT COMMENT '任务执行URL端口',
-    `generate_request` TEXT COMMENT '任务请求原始参数 (JSONString)',
-    `generate_response` TEXT COMMENT '任务生成原始结果 (JSONString)',
-    `reason` TEXT COMMENT '当任务失败时展示失败原因(如触发平台的内容风控等)',
-    `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
-    `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
-    INDEX `idx_execute_prompt_id` (`execute_prompt_id`),
-    INDEX `idx_user_id` (`user_id`)
-) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='任务执行表';

+ 3 - 2
db/sys_user_role.sql

@@ -14,7 +14,6 @@ CREATE TABLE `sys_user_role` (
     `status` TINYINT(1) DEFAULT '1' COMMENT '角色状态(1正常 2停用)',
     UNIQUE KEY (`role_name`),
     INDEX `idx_nickname` (`role_name`)
-#     FOREIGN KEY (`id`) REFERENCES `sys_user_role_module_relation`(`role_id`) ON DELETE CASCADE
 ) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='系统用户角色表';
 
 # ALTER TABLE sys_user_role_module_relation DROP FOREIGN KEY sys_user_role_module_relation_ibfk_1;
@@ -22,5 +21,7 @@ CREATE TABLE `sys_user_role` (
 INSERT INTO sys_user_role(role_name, role_description, sort) VALUES
     ('开发者', '全部权限', 1),
     ('管理员', '全部功能权限', 2),
-    ('普通用户', '基础权限', 3)
+    ('普通用户', '基础权限', 3),
+    ('运营-素材-管理员', '素材管理', 4),
+    ('运营-素材-普通用户', '素材管理', 5)
 ;

+ 3 - 5
db/sys_user_role_menu.sql

@@ -43,13 +43,11 @@ INSERT INTO sys_user_role_menu(id, parent_id, menu_name, menu_name_en, type, pat
     (11, -1, 'AI成片', 'AI Video', 1, '/ai/generate/video/broadcast/my/broadcast', '', '{}', 'VideoCamera', '34', 909),
 
 
-#     (-1, '素材管理', 'Material', 1, '/material', '', '{}', 'MessageBox', '20', 10),
-#     (4, '素材中心', 'Material', 1, '/material/center', '/src/views/material/materialCenter.vue', '{}', null, '20.3', 10),
-#     (4, '素材列表', 'Material List', 1, '/material/list', '/src/views/material/materialList.vue', '{}', null, '20.1', 10),
-#     (4, '素材分类', 'Material Category', 1, '/material/category', '/src/views/material/materialCategory.vue', '{}', null, '20.2', 10),
+    (20, -1, '素材管理', 'Material', 1, '/material', '', '{}', 'MessageBox', '20', 13),
+    (21, 20, '素材列表', 'Material List', 1, '/material/materialList', '/src/views/material/materialList.vue', '{}', null, '20.1', 13),
 #
 #     (-1, '我的', 'Account', 1, '/account', '', '{}', 'User', '21', 11),
-#     (8, '我的素材', 'My Material', 1, '/account/myMaterial', '', '{}', null, '21.1', 11),
+#     (8, '我的素材', 'My MaterialService', 1, '/account/myMaterial', '', '{}', null, '21.1', 11),
 #     (8, '我的作品', 'My Work', 1, '/account/myWork', '', '{}', null, '21.2', 11),
 #     (8, '我的草稿', 'My Draft', 1, '/account/myDraft', '', '{}', null, '21.3', 11),
 

+ 14 - 17
db/sys_user_role_permission.sql

@@ -176,23 +176,20 @@ INSERT INTO sys_user_role_permission(id, parent_id, permission_name, sort) VALUE
             ('11.6.1', '11.6', '商品规格SKU详情', null),
             ('11.6.2', '11.6', '创建商品规格SKU', null),
             ('11.6.3', '11.6', '编辑商品规格SKU', null),
-            ('11.6.4', '11.6', '删除商品规格SKU', null)
-
-
-
-#     ('20', -1, '素材管理', null),
-#         ('20.1', '20', '素材列表', null),
-#             ('20.1.1', '20.1', '素材中心', null),
-#             ('20.1.2', '20.1', '素材列表详情', null),
-#             ('20.1.3', '20.1', '创建素材', null),
-#             ('20.1.4', '20.1', '编辑素材', null),
-#             ('20.1.5', '20.1', '审核素材', null),
-#             ('20.1.6', '20.1', '删除素材', null),
-#         ('20.2', '20', '素材分类列表', null),
-#             ('20.2.1', '20.2', '素材分类详情', null),
-#             ('20.2.2', '20.2', '创建素材分类', null),
-#             ('20.2.3', '20.2', '编辑素材分类', null),
-#             ('20.2.4', '20.2', '删除素材分类', null),
+            ('11.6.4', '11.6', '删除商品规格SKU', null),
+
+
+    ('20', -1, '素材管理', null),
+        ('20.1', '20', '素材列表', null),
+            ('20.1.1', '20.1', '素材详情', null),
+            ('20.1.2', '20.1', '创建素材', null),
+            ('20.1.3', '20.1', '编辑素材', null),
+            ('20.1.4', '20.1', '删除素材', null),
+        ('20.2', '20', '素材分类列表', null),
+        ('20.3', '20', '素材标签列表', null)
+
+
+
 #
 #     ('21', -1, '个人中心', null),
 #         ('21.1', '21', '我的素材', null),

+ 23 - 9
db/sys_user_role_permission_relation.sql

@@ -76,13 +76,12 @@ INSERT INTO sys_user_role_permission_relation(role_id, permission_id) VALUES
         (1, '11.6'),
             (1, '11.6.1'), (1, '11.6.2'), (1, '11.6.3'), (1, '11.6.4'),
 
+    (1, '20'),
+        (1, '20.1'),
+            (1, '20.1.1'), (1, '20.1.2'), (1, '20.1.3'), (1, '20.1.4'),
+        (1, '20.2'),
+        (1, '20.3'),
 
-#     (1, '20'),
-#         (1, '20.1'),
-#             (1, '20.1.1'), (1, '20.1.2'), (1, '20.1.3'), (1, '20.1.4'), (1, '20.1.5'), (1, '20.1.6'),
-#         (1, '20.2'),
-#             (1, '20.2.1'), (1, '20.2.2'), (1, '20.2.3'), (1, '20.2.4'),
-#
 #     (1, '21'),
 #         (1, '21.1'),
 #             (1, '21.1.1'), (1, '21.1.2'), (1, '21.1.3'), (1, '21.1.4'), (1, '21.1.5'), (1, '21.1.6'),
@@ -165,9 +164,24 @@ INSERT INTO sys_user_role_permission_relation(role_id, permission_id) VALUES
 
     (3, '1'),
         (3, '1.1'),
-            (2, '1.1.2'), (3, '1.1.3'), (3, '1.1.4'), (3, '1.1.6'),
+            (3, '1.1.2'), (3, '1.1.3'), (3, '1.1.4'), (3, '1.1.6'),
 
     (3, '2'),
-    (3, '31')
-
+    (3, '31'),
+
+    # 运营-素材管理者
+    (4, '1.1.2'), (4, '1.1.3'), (4, '1.1.6'),
+    (4, '20'),
+        (4, '20.1'),
+            (4, '20.1.1'), (4, '20.1.2'), (4, '20.1.3'), (4, '20.1.4'),
+        (4, '20.2'),
+        (4, '20.3'),
+
+    # 运营-素材使用者
+    (5, '1.1.2'), (5, '1.1.3'), (5, '1.1.6'),
+    (5, '20.1'),
+        (5, '20.1'),
+            (5, '20.1.1'),
+        (5, '20.2'),
+        (5, '20.3')
 ;

+ 1 - 1
db/sys_user_role_relation.sql

@@ -15,7 +15,7 @@ CREATE TABLE `sys_user_role_relation` (
 ) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='系统用户角色关系表';
 
 INSERT INTO sys_user_role_relation(user_id, role_id) VALUES
-    (1, 1), (2, 2), (3, 3), (4, 2), (5, 2), (6, 2),
+    (1, 1), (2, 4), (3, 3), (4, 2), (5, 2), (6, 2),
     (7, 2), (8, 2), (9, 2), (10, 2), (11, 2), (12, 2),
     (13, 1), (14, 1), (15, 1), (16, 1)
 ;

+ 5 - 4
src/main/java/com/backendsys/modules/TestController.java

@@ -112,9 +112,9 @@ public class TestController {
 
         SysUser sysUser = new SysUser();
         sysUser.setUsername(UUID.randomUUID().toString());
-        System.out.println("【RabbitMQ-发送】:" + sysUser);
+        System.out.println("[Demo][RabbitMQ-发送]:" + sysUser);
 //        rabbitTemplate.convertAndSend("", "demo.queue", sysUser);
-        rabbitTemplate.convertAndSend("order.exchange", "order.create", sysUser);
+        rabbitTemplate.convertAndSend("demo.exchange", "demo.create", sysUser);
 
         return "ok";
     }
@@ -127,7 +127,8 @@ public class TestController {
         Channel channel = connection.createChannel(false);
 
         // 2. 手动拉消息,第二个参数传 false 表示“不要自动 ack”
-        GetResponse resp = channel.basicGet("order.queue", false);
+//        GetResponse resp = channel.basicGet("demo.queue", false);
+        GetResponse resp = channel.basicGet("demo.queue", false);
         if (resp == null) {
             channel.close();
             connection.close();
@@ -135,7 +136,7 @@ public class TestController {
         }
 
         String body = new String(resp.getBody(), StandardCharsets.UTF_8);
-        System.out.println("【RabbitMQ-手动处理】:" + body);
+        System.out.println("[Demo][RabbitMQ-手动处理]:" + body);
 
         // 3. 用同一个 channel 去 ack
         channel.basicAck(resp.getEnvelope().getDeliveryTag(), false);

+ 4 - 1
src/main/java/com/backendsys/modules/common/Filter/WebClientFilter.java

@@ -1,5 +1,7 @@
 package com.backendsys.modules.common.Filter;
 
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.json.JSONUtil;
 import org.springframework.core.io.buffer.DataBuffer;
 import org.springframework.core.io.buffer.DataBufferUtils;
 import org.springframework.core.io.buffer.DefaultDataBufferFactory;
@@ -26,7 +28,8 @@ public class WebClientFilter {
 
                     // 记录响应体
                     String bodyStr = new String(bodyBytes, StandardCharsets.UTF_8);
-                    System.out.println("[logRequestFilter] Response Body: " + bodyStr);
+//                    System.out.println("[logRequestFilter] Response Body: " + bodyStr);
+                    System.out.println("[logRequestFilter] Response Body: " + StrUtil.sub(bodyStr, 0, 1000));
 
                     // 重新构建完整的响应
                     return Mono.just(ClientResponse.create(response.statusCode())

+ 88 - 0
src/main/java/com/backendsys/modules/common/config/rabbitmq/DemoRabbitListener.java

@@ -0,0 +1,88 @@
+package com.backendsys.modules.common.config.rabbitmq;
+
+import com.rabbitmq.client.Channel;
+import org.springframework.amqp.core.*;
+import org.springframework.amqp.rabbit.annotation.RabbitListener;
+import org.springframework.amqp.rabbit.core.RabbitTemplate;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Lazy;
+import org.springframework.stereotype.Component;
+
+import java.io.IOException;
+
+import org.springframework.amqp.rabbit.connection.Connection;
+import com.rabbitmq.client.Channel;
+import com.rabbitmq.client.GetResponse;
+
+/**
+ * 自定义监听器
+ */
+@Component
+@Lazy(false)
+public class DemoRabbitListener {
+
+    @Autowired
+    private RabbitTemplate rabbitTemplate;
+
+//    // [监听] 自动 ACK
+//    @ComfyuiRabbitListener(id = "demoContainer", queues = "demo.queue", autoStartup = "false" )
+//    public void receive(SysUser sysUser) {
+//        System.out.println("收到消息: " + sysUser);
+//    }
+
+
+//    // [监听] 手动 ACK
+//    @RabbitListener(id = "demoContainer", queues = "demo.queue", ackMode = "MANUAL")
+//    public void receive(Message message, Channel channel) throws IOException, InterruptedException {
+//        try {
+//            // 1. 模拟耗时业务
+//            Thread.sleep(8000);
+//
+//            // 2. 业务处理
+//            String body = new String(message.getBody(), StandardCharsets.UTF_8);
+//            System.out.println("处理消息 (8s): " + body);
+
+
+//            // 外部调用ACK (消耗队列):
+//            try {
+//                Connection connection = rabbitTemplate.getConnectionFactory().createConnection();
+//                Channel channel = connection.createChannel(false);
+//                GetResponse resp = channel.basicGet("demo.queue", false);
+//                if (resp != null) {
+//                    channel.basicAck(resp.getEnvelope().getDeliveryTag(), false);
+//                }
+//                channel.close();
+//                connection.close();
+//            } catch (Exception e) {
+//                System.out.println(e.getMessage());
+//            }
+
+
+//            // 3. 手动确认(deliveryTag + 是否批量)
+//            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
+//        } catch (Exception e) {
+//            System.err.println("(RabbitListener) 处理失败: " + e.getMessage());
+//            // 拒绝并重新入队
+//            channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);
+//        }
+//    }
+
+    // [监听] 死信队列
+    @RabbitListener(id = "demoDlxContainer", queues = "demo.dlx.queue", ackMode = "MANUAL")
+    public void handleDemoDlx(Message message, Channel channel) throws IOException {
+        System.out.println("[Demo][RabbitMQ-死信队列]收到:" + new String(message.getBody()));
+
+        // 业务:记录日志 / 重发 / 报警 / 人工补偿
+        channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
+    }
+
+
+    // 4. 并发消费(一条队列多线程)
+    /*
+    @ComfyuiRabbitListener(queues = "demo.queue", concurrency = "5-10")
+    public void receive(String msg) { ... }
+     */
+
+
+
+}

+ 45 - 0
src/main/java/com/backendsys/modules/common/config/rabbitmq/DemoRabbitListenerRunner.java

@@ -0,0 +1,45 @@
+//package com.backendsys.modules.common.config.rabbitmq;
+//
+//import lombok.RequiredArgsConstructor;
+//import lombok.extern.slf4j.Slf4j;
+//import org.springframework.amqp.rabbit.listener.MessageListenerContainer;
+//import org.springframework.amqp.rabbit.listener.RabbitListenerEndpointRegistry;
+//import org.springframework.boot.CommandLineRunner;
+//import org.springframework.stereotype.Component;
+//
+///**
+// * 手动启动监听器
+// * 由于 RabbitMQ 的监听器默认是懒加载,所以需要手动启动监听器
+// */
+//@Slf4j
+//@Component
+//@RequiredArgsConstructor
+//public class DemoRabbitListenerRunner implements CommandLineRunner {
+//
+//    private final RabbitListenerEndpointRegistry registry;
+//
+////    @Override
+////    public void run(String... args) {
+////        List<String> MANUAL_START_IDS = List.of("demoContainer", "demoDlxContainer");
+////        MANUAL_START_IDS.forEach(id -> {
+////            MessageListenerContainer c = registry.getListenerContainer(id);
+////            if (c != null && !c.isRunning()) {
+////                c.start();
+////                System.out.printf("-- ComfyuiRabbitListener '{}' 已手动启动 --", id);
+////            }
+////        });
+////    }
+//
+//    public void run(String... args) {
+//        MessageListenerContainer container = registry.getListenerContainer("demoDlxContainer");
+//
+//        System.out.println("-- DemoRabbitListenerRunner container = " + container);
+//        System.out.println("container.isRunning() = " + container.isRunning());
+//
+//        if (container != null && !container.isRunning()) {
+//            container.start();     // 关键:真正启动监听
+//            System.out.println("-- demoDlxContainer Listener 已手动启动 --");
+//        }
+//    }
+//
+//}

+ 0 - 62
src/main/java/com/backendsys/modules/common/config/rabbitmq/RabbitListener.java

@@ -1,62 +0,0 @@
-package com.backendsys.modules.common.config.rabbitmq;
-
-import com.rabbitmq.client.Channel;
-import org.springframework.amqp.core.*;
-import org.springframework.context.annotation.Lazy;
-import org.springframework.stereotype.Component;
-
-import java.io.IOException;
-
-/**
- * 自定义监听器
- */
-@Component
-@Lazy(false)
-public class RabbitListener {
-
-//    // [监听] 自动 ACK
-//    @RabbitListener(id = "demoContainer", queues = "demo.queue", autoStartup = "false" )
-//    public void receive(SysUser sysUser) {
-//        System.out.println("收到消息: " + sysUser);
-//    }
-
-
-//    // [监听] 手动 ACK
-//    @RabbitListener(id = "demoContainer", queues = "demo.queue", ackMode = "MANUAL")
-//    public void receive(Message message, Channel channel) throws IOException, InterruptedException {
-//        try {
-//            // 1. 模拟耗时业务
-//            Thread.sleep(8000);
-//
-//            // 2. 业务处理
-//            String body = new String(message.getBody(), StandardCharsets.UTF_8);
-//            System.out.println("处理消息 (8s): " + body);
-//
-//            // 3. 手动确认(deliveryTag + 是否批量)
-//            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
-//        } catch (Exception e) {
-//            System.err.println("处理失败 (10s): " + e.getMessage());
-//            // 拒绝并重新入队
-//            channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);
-//        }
-//    }
-
-    // [监听] 死信队列
-    @org.springframework.amqp.rabbit.annotation.RabbitListener(id = "dlxContainer", queues = "dlx.queue", ackMode = "MANUAL")
-    public void handleDlx(Message message, Channel channel) throws IOException {
-        System.out.println("【RabbitMQ-死信】收到:" + new String(message.getBody()));
-
-        // 业务:记录日志 / 重发 / 报警 / 人工补偿
-        channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
-    }
-
-
-    // 4. 并发消费(一条队列多线程)
-    /*
-    @RabbitListener(queues = "demo.queue", concurrency = "5-10")
-    public void receive(String msg) { ... }
-     */
-
-
-
-}

+ 0 - 41
src/main/java/com/backendsys/modules/common/config/rabbitmq/RabbitListenerRunner.java

@@ -1,41 +0,0 @@
-package com.backendsys.modules.common.config.rabbitmq;
-
-import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.amqp.rabbit.listener.MessageListenerContainer;
-import org.springframework.amqp.rabbit.listener.RabbitListenerEndpointRegistry;
-import org.springframework.boot.CommandLineRunner;
-import org.springframework.stereotype.Component;
-
-/**
- * 手动启动监听器
- * 由于 RabbitMQ 的监听器默认是懒加载,所以需要手动启动监听器
- */
-@Slf4j
-@Component
-@RequiredArgsConstructor
-public class RabbitListenerRunner implements CommandLineRunner {
-
-    private final RabbitListenerEndpointRegistry registry;
-
-//    @Override
-//    public void run(String... args) {
-//        List<String> MANUAL_START_IDS = List.of("demoContainer", "dlxContainer");
-//        MANUAL_START_IDS.forEach(id -> {
-//            MessageListenerContainer c = registry.getListenerContainer(id);
-//            if (c != null && !c.isRunning()) {
-//                c.start();
-//                System.out.printf("-- RabbitListener '{}' 已手动启动 --", id);
-//            }
-//        });
-//    }
-
-    public void run(String... args) {
-        MessageListenerContainer container = registry.getListenerContainer("dlxContainer");
-        if (container != null && !container.isRunning()) {
-            container.start();     // 关键:真正启动监听
-            System.out.println("-- dlxContainer Listener 已手动启动 --");
-        }
-    }
-
-}

+ 30 - 0
src/main/java/com/backendsys/modules/common/config/rabbitmq/queue/DemoDlxQueueConfig.java

@@ -0,0 +1,30 @@
+package com.backendsys.modules.common.config.rabbitmq.queue;
+
+import org.springframework.amqp.core.*;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Lazy;
+
+/**
+ * 死信队列
+ * 在项目启动时创建 (RabbitMQ) 队列、交换机、绑定关系
+ */
+@Configuration
+@Lazy(false)
+public class DemoDlxQueueConfig {
+
+    /* === 死信交换机/队列 === */
+    public static final String EXCHANGE = "demo.dlx";
+    public static final String QUEUE = "demo.dlx.queue";
+    public static final String ROUTING_KEY = "demo.dlx.routekey";
+
+    @Bean
+    public DirectExchange demoDlxExchange() { return ExchangeBuilder.directExchange(EXCHANGE).durable(true).build(); }
+
+    @Bean
+    public Queue demoDlxQueue() { return QueueBuilder.durable(QUEUE).build(); }
+
+    @Bean
+    public Binding demoDlxBinding() { return BindingBuilder.bind(demoDlxQueue()).to(demoDlxExchange()).with(ROUTING_KEY); }
+
+}

+ 8 - 8
src/main/java/com/backendsys/modules/common/config/rabbitmq/queue/QueueDemoConfig.java → src/main/java/com/backendsys/modules/common/config/rabbitmq/queue/DemoQueueConfig.java

@@ -11,14 +11,14 @@ import org.springframework.context.annotation.Lazy;
  */
 @Configuration
 @Lazy(false)
-public class QueueDemoConfig {
+public class DemoQueueConfig {
 
     @Autowired
-    private QueueDlxConfig queueDlxConfig;
+    private DemoDlxQueueConfig config;
 
-    public static final String EXCHANGE = "order.exchange";
-    public static final String QUEUE = "order.queue";
-    public static final String ROUTING_KEY = "order.create";
+    public static final String EXCHANGE = "demo.exchange";
+    public static final String QUEUE = "demo.queue";
+    public static final String ROUTING_KEY = "demo.create";
 
     // 交换机
     @Bean
@@ -30,10 +30,10 @@ public class QueueDemoConfig {
     @Bean
     public Queue demoQueue() {
         return QueueBuilder.durable(QUEUE)
-                .ttl(5000)                    // 5s
+                .ttl(5000)                    // 5s`
                 .maxLength(1000)        // 1000条
-                .deadLetterExchange(queueDlxConfig.EXCHANGE)
-                .deadLetterRoutingKey(queueDlxConfig.ROUTING_KEY)
+                .deadLetterExchange(config.EXCHANGE)
+                .deadLetterRoutingKey(config.ROUTING_KEY)
                 .build();
     }
 

+ 0 - 30
src/main/java/com/backendsys/modules/common/config/rabbitmq/queue/QueueDlxConfig.java

@@ -1,30 +0,0 @@
-package com.backendsys.modules.common.config.rabbitmq.queue;
-
-import org.springframework.amqp.core.*;
-import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.Configuration;
-import org.springframework.context.annotation.Lazy;
-
-/**
- * 死信队列
- * 在项目启动时创建 (RabbitMQ) 队列、交换机、绑定关系
- */
-@Configuration
-@Lazy(false)
-public class QueueDlxConfig {
-
-    /* === 死信交换机/队列 === */
-    public static final String EXCHANGE = "dlx";
-    public static final String QUEUE = "dlx.queue";
-    public static final String ROUTING_KEY = "dlx.routekey";
-
-    @Bean
-    public DirectExchange dlxExchange() { return ExchangeBuilder.directExchange(EXCHANGE).durable(true).build(); }
-
-    @Bean
-    public Queue dlxQueue() { return QueueBuilder.durable(QUEUE).build(); }
-
-    @Bean
-    public Binding dlxBinding() { return BindingBuilder.bind(dlxQueue()).to(dlxExchange()).with(ROUTING_KEY); }
-
-}

+ 7 - 2
src/main/java/com/backendsys/modules/common/config/security/utils/JwtUtil.java

@@ -5,6 +5,7 @@ import cn.hutool.json.JSONUtil;
 import com.backendsys.modules.common.config.security.entity.SecurityAppUserInfo;
 import com.backendsys.modules.common.config.security.entity.SecurityUserInfo;
 import io.jsonwebtoken.Claims;
+import io.jsonwebtoken.JwtException;
 import io.jsonwebtoken.Jwts;
 import org.springframework.security.core.userdetails.UserDetails;
 import org.springframework.stereotype.Service;
@@ -126,8 +127,12 @@ public class JwtUtil {
      * @return
      */
     public Claims extractAllClaims(String token){
-//        return Jwts.parser().setSigningKey(getSignInKey()).build().parseClaimsJws(token).getBody();
-        return Jwts.parser().verifyWith(getSignInKey()).build().parseSignedClaims(token).getPayload();
+        try {
+            return Jwts.parser().verifyWith(getSignInKey()).build().parseSignedClaims(token).getPayload();
+        } catch (JwtException e) {
+            System.out.println("Token 解析失败: " + e.getMessage());
+        }
+        return null;
     }
 
     /**

+ 1 - 1
src/main/java/com/backendsys/modules/crt/controller/CrtGenerateController.java

@@ -26,7 +26,7 @@ public class CrtGenerateController {
     @Operation(summary = "查询任务队列")
     @GetMapping("/api/crt/generate/getQueue")
     public Result getQueue() {
-        return Result.success().put("data", crtGenerateService.getQueue());
+        return Result.success().put("data", crtGenerateService.getQueue(8000));
     }
 
     @PreAuthorize("@sr.hasPermission('36.3')")

+ 1 - 1
src/main/java/com/backendsys/modules/crt/service/CrtGenerateService.java

@@ -8,7 +8,7 @@ import java.util.Map;
 public interface CrtGenerateService {
 
     // [ComfyUI] 查询任务队列
-    Map<String, Object> getQueue();
+    Map<String, Object> getQueue(Integer port);
 
     // 短剧创作-生成图片
     Map<String, Object> generateImage(CrtDramaProjectStoryboard crtDramaProjectStoryboard);

+ 1 - 2
src/main/java/com/backendsys/modules/crt/service/impl/CrtDramaProjectStoryboardServiceImpl.java

@@ -1,6 +1,5 @@
 package com.backendsys.modules.crt.service.impl;
 
-import cn.hutool.core.convert.Convert;
 import com.backendsys.exception.CustException;
 import com.backendsys.modules.common.config.security.enums.SecurityEnum;
 import com.backendsys.modules.common.config.security.utils.SecurityUtil;
@@ -13,7 +12,7 @@ import com.backendsys.modules.crt.entity.*;
 import com.backendsys.modules.crt.service.CrtDramaProjectStoryboardService;
 import com.backendsys.modules.upload.entity.ObjectKey;
 import com.backendsys.modules.upload.enums.StyleEnums;
-import com.backendsys.modules.upload.utils.ObjectKeyUtil;
+import com.backendsys.modules.upload.utils.ObjectKey.ObjectKeyUtil;
 import com.backendsys.modules.upload.utils.UploadUtil;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import org.springframework.beans.factory.annotation.Autowired;

+ 23 - 12
src/main/java/com/backendsys/modules/crt/service/impl/CrtGenerateServiceImpl.java

@@ -2,7 +2,10 @@ package com.backendsys.modules.crt.service.impl;
 
 import cn.hutool.core.convert.Convert;
 import cn.hutool.core.date.DateUtil;
+import cn.hutool.core.map.MapUtil;
+import cn.hutool.json.JSONUtil;
 import com.backendsys.exception.CustException;
+import com.backendsys.modules.common.config.security.utils.SecurityUtil;
 import com.backendsys.modules.crt.dao.CrtDramaProjectSettingsDao;
 import com.backendsys.modules.crt.dao.CrtDramaProjectStoryboardDao;
 import com.backendsys.modules.crt.dao.CrtGenerateImageDao;
@@ -19,6 +22,7 @@ import com.backendsys.modules.sdk.comfyui.entity.ComfyuiQueue;
 import com.backendsys.modules.sdk.comfyui.entity.ComfyuiText2Image;
 import com.backendsys.modules.sdk.comfyui.service.ComfyuiService;
 import com.backendsys.modules.sdk.comfyui.service.ComfyuiSocketService;
+import com.backendsys.modules.sdk.comfyui.service.ComfyuiTaskService;
 import com.backendsys.modules.sdk.comfyui.service.ComfyuiText2ImageService;
 import com.backendsys.modules.upload.enums.StyleEnums;
 import com.backendsys.modules.upload.utils.UploadUtil;
@@ -31,10 +35,7 @@ import org.springframework.beans.factory.annotation.Value;
 import org.springframework.stereotype.Service;
 import reactor.core.publisher.Mono;
 
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.UUID;
+import java.util.*;
 import java.util.stream.Collectors;
 
 @Service
@@ -44,9 +45,11 @@ public class CrtGenerateServiceImpl implements CrtGenerateService {
     private Boolean IS_SAVE;
 
     @Autowired
-    private ComfyuiService comfyUIService;
+    private ComfyuiService comfyuiService;
     @Autowired
-    private ComfyuiSocketService comfyUISocketService;
+    private ComfyuiSocketService comfyuiSocketService;
+    @Autowired
+    private ComfyuiTaskService comfyuiTaskService;
 
     @Autowired
     private ComfyuiText2ImageService comfyuiText2ImageService;
@@ -64,9 +67,9 @@ public class CrtGenerateServiceImpl implements CrtGenerateService {
      * [ComfyUI] 查询任务队列
      */
     @Override
-    public Map<String, Object> getQueue() {
+    public Map<String, Object> getQueue(Integer port) {
         // [ComfyUI] 执行任务
-        Mono<ComfyuiQueue> cfQueueMono = comfyUIService.getQueue();
+        Mono<ComfyuiQueue> cfQueueMono = comfyuiService.getQueue(port);
         ComfyuiQueue response = cfQueueMono.block();
         System.out.println("结果: " + response);
 
@@ -153,7 +156,15 @@ public class CrtGenerateServiceImpl implements CrtGenerateService {
 
         // -- [ComfyUI] 发起生图任务 -----------------------------------------------
         Long task_id = comfyuiText2ImageService.generateText2Image(client_id, comfyuiText2Image);
+        System.out.println("[创建生图任务] task_id = " + task_id);
 
+        // -- [Rabbitmq] 推送到生成队列 --------------------------------------------
+        Long user_id = SecurityUtil.getUserId();
+        Map<String, Object> map = new HashMap<>();
+        map.put("drama_project_storyboard_id", drama_project_storyboard_id);
+        map.put("user_id", user_id);
+        String custom_params = JSONUtil.toJsonStr(map);
+        comfyuiTaskService.tryPushNext(user_id, custom_params);
 
 
 
@@ -161,17 +172,17 @@ public class CrtGenerateServiceImpl implements CrtGenerateService {
 //        Map<String, Object> params = new LinkedHashMap<>();
 //        params.put("drama_project_storyboard_id", drama_project_storyboard_id);
 //        // - is_save: 是否转存到 cos/tos
-//        comfyUISocketService.connectToSse(client_id, 8000, IS_SAVE, params).subscribe();
+//        comfyuiSocketService.connectToSse(client_id, 8000, IS_SAVE, params).subscribe();
 
         // -- [ComfyUI] 执行/排队生图任务 --------------------------------------------
 
+//        ComfyuiResponse response = comfyuiText2ImageService.generateText2Image(client_id, comfyuiText2Image);
+//        System.out.println("结果: " + response);
+//        // 结果: ComfyuiResponse(client_id=1a8a2d01-5500-437f-bb11-7a986130da48, prompt_id=c74501ed-6755-48f8-a440-aef3474b523c, number=47, node_errors={}, error=null)
 
 
 
 
-//        ComfyuiResponse response = null; // comfyuiText2ImageService.generateText2Image(client_id, comfyuiText2Image);
-//        System.out.println("结果: " + response);
-//        // 结果: ComfyuiResponse(client_id=1a8a2d01-5500-437f-bb11-7a986130da48, prompt_id=c74501ed-6755-48f8-a440-aef3474b523c, number=47, node_errors={}, error=null)
 
         Map<String, Object> resp = new LinkedHashMap<>();
         resp.put("drama_project_storyboard_id", drama_project_storyboard_id);

+ 35 - 0
src/main/java/com/backendsys/modules/material/controller/MaterialCategoryController.java

@@ -0,0 +1,35 @@
+package com.backendsys.modules.material.controller;
+
+import com.backendsys.modules.material.entity.MaterialCategory;
+import com.backendsys.modules.material.service.MaterialCategoryService;
+import com.backendsys.modules.common.utils.Result;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@Validated
+@RestController
+@Tag(name = "素材分类管理")
+public class MaterialCategoryController {
+
+    @Autowired
+    private MaterialCategoryService materialCategoryService;
+
+    @Operation(summary = "获取素材分类列表")
+//    @PreAuthorize("@sr.hasPermission('20.2')")
+    @GetMapping("/api/material/getMaterialCategoryList")
+    public Result getMaterialCategoryList(MaterialCategory materialCategory) {
+        return Result.success().put("data", materialCategoryService.selectMaterialCategoryList(materialCategory));
+    }
+
+    @Operation(summary = "获取素材分类列表(下拉)")
+//    @PreAuthorize("@sr.hasPermission('20.2')")
+    @GetMapping("/api/material/getMaterialCategoryPopover")
+    public Result getMaterialCategoryPopover() {
+        return Result.success().put("data", materialCategoryService.selectMaterialCategoryPopover());
+    }
+
+}

+ 62 - 0
src/main/java/com/backendsys/modules/material/controller/MaterialController.java

@@ -0,0 +1,62 @@
+package com.backendsys.modules.material.controller;
+
+import com.backendsys.modules.material.entity.Material;
+import com.backendsys.modules.material.service.MaterialService;
+import com.backendsys.modules.common.aspect.SysLog;
+import com.backendsys.modules.common.config.security.utils.SecurityUtil;
+import com.backendsys.modules.common.utils.Result;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+@Validated
+@RestController
+@Tag(name = "素材管理")
+public class MaterialController {
+
+    @Autowired
+    private MaterialService materialService;
+
+    @Operation(summary = "获取素材列表")
+    @PreAuthorize("@sr.hasPermission('20.1')")
+    @GetMapping("/api/material/getMaterialList")
+    public Result getMaterialList(Material material) {
+        return Result.success().put("data", materialService.selectMaterialList(material));
+    }
+
+    @Operation(summary = "获取素材列表")
+//    @PreAuthorize("@sr.hasPermission('20.1.1')")
+    @GetMapping("/api/material/getMaterialDetail")
+    public Result getMaterialDetail(@Validated(Material.Detail.class) Material material) {
+        return Result.success().put("data", materialService.selectMaterialDetail(material));
+    }
+
+    @Operation(summary = "创建素材")
+    @PreAuthorize("@sr.hasPermission('20.1.2')")
+    @PostMapping("/api/material/createMaterial")
+    public Result createMaterial(@Validated(Material.Create.class) @RequestBody Material material) {
+        material.setUser_id(SecurityUtil.getUserId());
+        return Result.success().put("data", materialService.insertMaterial(material));
+    }
+
+    @SysLog("编辑素材")
+    @Operation(summary = "编辑素材")
+    @PreAuthorize("@sr.hasPermission('20.1.3')")
+    @PutMapping("/api/material/updateMaterial")
+    public Result updateMaterial(@Validated(Material.Update.class) @RequestBody Material material) {
+        material.setUser_id(SecurityUtil.getUserId());
+        return Result.success().put("data", materialService.updateMaterial(material));
+    }
+
+    @SysLog("删除素材")
+    @Operation(summary = "删除素材")
+    @PreAuthorize("@sr.hasPermission('20.1.4')")
+    @DeleteMapping("/api/material/deleteMaterial")
+    public Result deleteMaterial(@Validated(Material.Delete.class) @RequestBody Material material) {
+        return Result.success().put("data", materialService.deleteMaterial(material));
+    }
+
+}

+ 36 - 0
src/main/java/com/backendsys/modules/material/controller/MaterialTagController.java

@@ -0,0 +1,36 @@
+package com.backendsys.modules.material.controller;
+
+import com.backendsys.modules.material.entity.MaterialTag;
+import com.backendsys.modules.material.service.MaterialTagService;
+import com.backendsys.modules.common.utils.Result;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@Validated
+@RestController
+@Tag(name = "素材标签管理")
+public class MaterialTagController {
+
+    @Autowired
+    private MaterialTagService materialTagService;
+
+    @Operation(summary = "获取素材标签列表")
+    @PreAuthorize("@sr.hasPermission('20.3')")
+    @GetMapping("/api/material/getMaterialTagList")
+    public Result getMaterialTagList(@Validated(MaterialTag.TagList.class) MaterialTag materialTag) {
+        return Result.success().put("data", materialTagService.selectMaterialTagList(materialTag));
+    }
+
+    @Operation(summary = "获取素材标签列表(下拉)")
+    @PreAuthorize("@sr.hasPermission('20.3')")
+    @GetMapping("/api/material/getMaterialTagPopover")
+    public Result getMaterialTagPopover(@Validated(MaterialTag.TagList.class) MaterialTag materialTag) {
+        return Result.success().put("data", materialTagService.selectMaterialTagPopover(materialTag));
+    }
+
+}

+ 15 - 0
src/main/java/com/backendsys/modules/material/dao/MaterialCategoryDao.java

@@ -0,0 +1,15 @@
+package com.backendsys.modules.material.dao;
+
+import com.backendsys.modules.material.entity.MaterialCategory;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+
+import java.util.List;
+
+@Mapper
+public interface MaterialCategoryDao extends BaseMapper<MaterialCategory> {
+
+    // 获取素材分类列表
+    List<MaterialCategory> selectMaterialCategoryList(MaterialCategory materialCategory);
+
+}

+ 24 - 0
src/main/java/com/backendsys/modules/material/dao/MaterialDao.java

@@ -0,0 +1,24 @@
+package com.backendsys.modules.material.dao;
+
+import com.backendsys.modules.material.entity.Material;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+
+import java.util.List;
+import java.util.Map;
+
+@Mapper
+public interface MaterialDao extends BaseMapper<Material> {
+
+    // 获取素材列表
+    List<Map<String, Object>> selectMaterialList(Material material);
+    // 获取素材详情
+    Material selectMaterialDetail(Material material);
+    // 创建素材
+    int insertMaterial(Material material);
+    // 编辑素材
+    int updateMaterial(Material material);
+    // 删除素材
+    int deleteMaterial(Material material);
+
+}

+ 16 - 0
src/main/java/com/backendsys/modules/material/dao/MaterialTagDao.java

@@ -0,0 +1,16 @@
+package com.backendsys.modules.material.dao;
+
+import com.backendsys.modules.material.entity.MaterialTag;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+
+import java.util.List;
+
+@Mapper
+public interface MaterialTagDao extends BaseMapper<MaterialTag> {
+
+    List<MaterialTag> selectMaterialTagList(MaterialTag materialTag);
+
+
+
+}

+ 70 - 0
src/main/java/com/backendsys/modules/material/entity/Material.java

@@ -0,0 +1,70 @@
+package com.backendsys.modules.material.entity;
+
+import com.backendsys.config.Mybatis.handler.timezone.LocalDateTimeAdapter;
+import com.backendsys.entity.validator.RangeArray;
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.google.gson.annotations.JsonAdapter;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Size;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+@Data
+@TableName("ai_material")
+public class Material {
+
+    public static interface Detail{}
+    public static interface Create{}
+    public static interface Update{}
+    public static interface Delete{}
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    @TableField("id")
+    @NotNull(message="素材ID不能为空", groups = { Detail.class, Update.class, Delete.class })
+    private Long material_id;
+
+    private Long user_id;
+    @TableField(exist = false)
+    private String user_nickname;
+
+    @NotNull(message="素材分类ID不能为空", groups = { Create.class, Update.class })
+    private Long category_id;
+    @TableField(exist = false)
+    private String category_name;
+
+    private String tag_ids;
+    @TableField(exist = false)
+    private Long tag_id;
+    @TableField(exist = false)
+    private List<MaterialTag> tag_list;
+
+    @Size(max = 100, message = "素材名称长度不超过 {max} 个字符", groups = { Create.class, Update.class })
+    @NotEmpty(message="素材名称不能为空", groups = { Create.class, Update.class })
+    private String material_name;
+
+    @Size(max = 1000, message = "缩略图路径长度不超过 {max} 个字符", groups = { Create.class, Update.class })
+    private String image_thumb_url;
+    @Size(max = 1000, message = "高清图路径长度不超过 {max} 个字符", groups = { Create.class, Update.class })
+    private String image_url;
+    @Size(max = 1000, message = "FLA路径长度不超过 {max} 个字符", groups = { Create.class, Update.class })
+    private String fla_url;
+    @Size(max = 1000, message = "PSD路径长度不超过 {max} 个字符", groups = { Create.class, Update.class })
+    private String psd_url;
+
+    @RangeArray(message="是否有版权,范围应是(-1否, 1是)", value = {"-1", "1"}, groups = { Create.class, Update.class })
+    private Integer is_copyright;
+
+    @JsonAdapter(LocalDateTimeAdapter.class)
+    private LocalDateTime create_time;
+    @JsonAdapter(LocalDateTimeAdapter.class)
+    private LocalDateTime update_time;
+
+}

+ 37 - 0
src/main/java/com/backendsys/modules/material/entity/MaterialCategory.java

@@ -0,0 +1,37 @@
+package com.backendsys.modules.material.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Size;
+import lombok.Data;
+import org.hibernate.validator.constraints.Range;
+
+@Data
+@TableName("ai_material_category")
+public class MaterialCategory {
+
+    public static interface CategoryList{}
+    public static interface Detail{}
+    public static interface Create{}
+    public static interface Update{}
+    public static interface Delete{}
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    @TableField("id")
+    @NotNull(message="分类ID不能为空", groups = { Detail.class, Update.class, Delete.class })
+    private Long category_id;
+
+    @Size(max = 100, message = "分类名称长度不超过 {max} 个字符", groups = { Create.class, Update.class })
+    @NotEmpty(message="分类名称不能为空", groups = { Create.class, Update.class })
+    private String category_name;
+
+    @Range(min = 1, max = 9999, message = "排序必须在 {min} 到 {max} 之间")
+    private Integer sort;
+
+}

+ 43 - 0
src/main/java/com/backendsys/modules/material/entity/MaterialTag.java

@@ -0,0 +1,43 @@
+package com.backendsys.modules.material.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Size;
+import lombok.Data;
+import org.hibernate.validator.constraints.Range;
+
+@Data
+@TableName("ai_material_tag")
+public class MaterialTag {
+
+    public static interface TagList{}
+    public static interface Detail{}
+    public static interface Create{}
+    public static interface Update{}
+    public static interface Delete{}
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    @TableField("id")
+    @NotNull(message="标签ID不能为空", groups = { Detail.class, Update.class, Delete.class })
+    private Long tag_id;
+
+    @NotNull(message="分类ID不能为空", groups = { TagList.class, Detail.class })
+    private Long category_id;
+
+    @Size(max = 100, message = "标签名称长度不超过 {max} 个字符", groups = { Create.class, Update.class })
+    @NotEmpty(message="标签名称不能为空", groups = { Create.class, Update.class })
+    private String tag_name;
+
+    @TableField(exist = false)
+    private Integer material_count;
+
+    @Range(min = 1, max = 9999, message = "排序必须在 {min} 到 {max} 之间")
+    private Integer sort;
+
+}

+ 15 - 0
src/main/java/com/backendsys/modules/material/service/MaterialCategoryService.java

@@ -0,0 +1,15 @@
+package com.backendsys.modules.material.service;
+
+import com.backendsys.modules.material.entity.MaterialCategory;
+import com.backendsys.utils.response.PageEntity;
+
+import java.util.List;
+
+public interface MaterialCategoryService {
+
+    // 获取素材分类列表
+    PageEntity selectMaterialCategoryList(MaterialCategory materialCategory);
+    // 获取素材分类列表 (下拉)
+    List<MaterialCategory> selectMaterialCategoryPopover();
+
+}

+ 25 - 0
src/main/java/com/backendsys/modules/material/service/MaterialService.java

@@ -0,0 +1,25 @@
+package com.backendsys.modules.material.service;
+
+import com.backendsys.modules.material.entity.Material;
+import com.backendsys.utils.response.PageEntity;
+
+import java.util.Map;
+
+public interface MaterialService {
+
+    // 获取素材列表
+    PageEntity selectMaterialList(Material material);
+
+    // 获取素材详情
+    Material selectMaterialDetail(Material material);
+
+    // 创建素材
+    Map<String, Object> insertMaterial(Material material);
+
+    // 编辑素材
+    Map<String, Object> updateMaterial(Material material);
+
+    // 删除素材
+    Map<String, Object> deleteMaterial(Material material);
+
+}

+ 15 - 0
src/main/java/com/backendsys/modules/material/service/MaterialTagService.java

@@ -0,0 +1,15 @@
+package com.backendsys.modules.material.service;
+
+import com.backendsys.modules.material.entity.MaterialTag;
+import com.backendsys.utils.response.PageEntity;
+
+import java.util.List;
+
+public interface MaterialTagService {
+
+    // 获取素材标签列表
+    PageEntity selectMaterialTagList(MaterialTag materialTag);
+    // 获取素材标签列表 (下拉)
+    List<MaterialTag> selectMaterialTagPopover(MaterialTag materialTag);
+
+}

+ 38 - 0
src/main/java/com/backendsys/modules/material/service/impl/MaterialCategoryImpl.java

@@ -0,0 +1,38 @@
+package com.backendsys.modules.material.service.impl;
+
+import com.backendsys.modules.material.dao.MaterialCategoryDao;
+import com.backendsys.modules.material.entity.MaterialCategory;
+import com.backendsys.modules.material.service.MaterialCategoryService;
+import com.backendsys.utils.response.PageEntity;
+import com.backendsys.utils.response.PageInfoResult;
+import com.backendsys.utils.v2.PageUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+@Service
+public class MaterialCategoryImpl implements MaterialCategoryService {
+
+    @Autowired
+    private MaterialCategoryDao materialCategoryDao;
+
+    /**
+     * 获取素材分类列表
+     */
+    @Override
+    public PageEntity selectMaterialCategoryList(MaterialCategory materialCategory) {
+        PageUtils.startPage();  // 分页
+        List<MaterialCategory> list = materialCategoryDao.selectMaterialCategoryList(materialCategory);
+        return new PageInfoResult(list).toEntity();
+    }
+
+    /**
+     * 获取素材分类列表 (下拉)
+     */
+    @Override
+    public List<MaterialCategory> selectMaterialCategoryPopover() {
+        return materialCategoryDao.selectMaterialCategoryList(new MaterialCategory());
+    }
+
+}

+ 173 - 0
src/main/java/com/backendsys/modules/material/service/impl/MaterialServiceImpl.java

@@ -0,0 +1,173 @@
+package com.backendsys.modules.material.service.impl;
+
+import cn.hutool.core.convert.Convert;
+import cn.hutool.core.util.StrUtil;
+import com.backendsys.exception.CustException;
+import com.backendsys.modules.material.dao.MaterialCategoryDao;
+import com.backendsys.modules.material.dao.MaterialDao;
+import com.backendsys.modules.material.dao.MaterialTagDao;
+import com.backendsys.modules.material.entity.Material;
+import com.backendsys.modules.material.entity.MaterialCategory;
+import com.backendsys.modules.material.entity.MaterialTag;
+import com.backendsys.modules.material.service.MaterialService;
+import com.backendsys.modules.upload.service.SysFileService;
+import com.backendsys.modules.upload.utils.ObjectKey.ObjectKeyEntity;
+import com.backendsys.modules.upload.utils.ObjectKey.ObjectKeyUtil;
+import com.backendsys.modules.upload.utils.UploadUtil;
+import com.backendsys.utils.response.PageEntity;
+import com.backendsys.utils.response.PageInfoResult;
+import com.backendsys.utils.v2.PageUtils;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+import java.util.stream.Collectors;
+
+@Service
+public class MaterialServiceImpl implements MaterialService {
+
+    @Autowired
+    private ObjectKeyUtil objectKeyUtil;
+
+    @Autowired
+    private MaterialDao materialDao;
+    @Autowired
+    private MaterialTagDao materialTagDao;
+    @Autowired
+    private MaterialCategoryDao materialCategoryDao;
+    @Autowired
+    private SysFileService sysFileService;
+
+    private List<MaterialTag> getMaterialTagByIds(String tag_ids) {
+        if (StrUtil.isEmpty(tag_ids)) return null;
+        LambdaQueryWrapper<MaterialTag> wrapper = new LambdaQueryWrapper<>();
+        wrapper.in(MaterialTag::getId, tag_ids.split(","));
+        wrapper.orderByDesc(MaterialTag::getSort);
+        return materialTagDao.selectList(wrapper);
+    }
+
+    /**
+     * 获取素材列表
+     */
+    @Override
+    public PageEntity selectMaterialList(Material material) {
+        PageUtils.startPage();  // 分页
+
+        List<Map<String, Object>> list = materialDao.selectMaterialList(material);
+
+        // 1) 完成分页实体渲染
+        PageEntity pageEntity = new PageInfoResult(list).toEntity();
+
+        if (!list.isEmpty()) {
+            // 2) 分页列表格式化
+            list = list.stream().map(item -> {
+                // 新增字段:标签列表
+                String tag_ids = Convert.toStr(item.get("tag_ids"));
+                List<MaterialTag> materialTagList = getMaterialTagByIds(tag_ids);
+                item.put("tag_list", materialTagList);
+                return item;
+            }).collect(Collectors.toList());
+
+            // 3) 分页实体重新赋值
+            List<Object> objectList = list.stream().map(item -> (Object) item).collect(Collectors.toList());
+            pageEntity.setList(objectList);
+        }
+
+        return pageEntity;
+    }
+
+    /**
+     * 获取素材详情
+     */
+    @Override
+    public Material selectMaterialDetail(Material material) {
+
+        Material detail = materialDao.selectMaterialDetail(material);
+        if (detail == null) throw new CustException("素材不存在");
+
+        // 新增字段:标签列表
+        List<MaterialTag> materialTagList = getMaterialTagByIds(detail.getTag_ids());
+        detail.setTag_list(materialTagList);
+
+        return detail;
+    }
+
+    /**
+     * 创建素材
+     */
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Map<String, Object> insertMaterial(Material material) {
+
+        MaterialCategory materialCategory = materialCategoryDao.selectById(material.getCategory_id());
+        if (materialCategory == null) throw new CustException("素材分类不存在");
+
+        // 生成缩略图,并填充缩略图地址
+        if (StrUtil.isNotEmpty(material.getImage_url())) {
+            ObjectKeyEntity objectKeyEntity = objectKeyUtil.urlToObjectKey(material.getImage_url());
+            String image_thumb_url = UploadUtil.getImageThumbUrl(material.getImage_url(), objectKeyEntity.getTarget(), 315, 180);
+            material.setImage_thumb_url(image_thumb_url);
+        }
+
+        materialDao.insertMaterial(material);
+
+        return Map.of("material_id", material.getMaterial_id());
+    }
+
+    /**
+     * 编辑素材
+     */
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Map<String, Object> updateMaterial(Material material) {
+
+        Material detail = materialDao.selectById(material.getMaterial_id());
+        if (detail == null) throw new CustException("素材不存在");
+
+        if (material.getCategory_id() != null) {
+            MaterialCategory materialCategory = materialCategoryDao.selectById(material.getCategory_id());
+            if (materialCategory == null) throw new CustException("素材分类不存在");
+        }
+
+        // 编辑的时候,如果素材图片有修改,需要删除之前的图片
+
+        // 生成缩略图,并填充缩略图地址
+        if (StrUtil.isNotEmpty(material.getImage_url())) {
+            ObjectKeyEntity objectKeyEntity = objectKeyUtil.urlToObjectKey(material.getImage_url());
+            String image_thumb_url = UploadUtil.getImageThumbUrl(material.getImage_url(), objectKeyEntity.getTarget(), 315, 180);
+            material.setImage_thumb_url(image_thumb_url);
+        }
+
+        materialDao.updateMaterial(material);
+        return Map.of("material_id", material.getMaterial_id());
+    };
+
+    /**
+     * 删除素材
+     */
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Map<String, Object> deleteMaterial(Material material) {
+
+        Material detail = materialDao.selectById(material.getMaterial_id());
+        if (detail == null) throw new CustException("素材不存在");
+
+        // 删除的时候,同时删除对象存储中的素材图片 (异步)
+        ObjectKeyEntity image_url_object = objectKeyUtil.urlToObjectKey(detail.getImage_url());
+        ObjectKeyEntity fla_url_object = objectKeyUtil.urlToObjectKey(detail.getFla_url());
+        ObjectKeyEntity psd_url_object = objectKeyUtil.urlToObjectKey(detail.getPsd_url());
+        CompletableFuture.runAsync(() -> {
+            sysFileService.deleteObject(image_url_object.getObject_key(), image_url_object.getTarget());
+            sysFileService.deleteObject(fla_url_object.getObject_key(), fla_url_object.getTarget());
+            sysFileService.deleteObject(psd_url_object.getObject_key(), psd_url_object.getTarget());
+        });
+
+        materialDao.deleteMaterial(material);
+        return Map.of("material_id", material.getMaterial_id());
+    }
+
+}

+ 49 - 0
src/main/java/com/backendsys/modules/material/service/impl/MaterialTagImpl.java

@@ -0,0 +1,49 @@
+package com.backendsys.modules.material.service.impl;
+
+import com.backendsys.modules.material.dao.MaterialTagDao;
+import com.backendsys.modules.material.entity.MaterialTag;
+import com.backendsys.modules.material.service.MaterialTagService;
+import com.backendsys.utils.response.PageEntity;
+import com.backendsys.utils.response.PageInfoResult;
+import com.backendsys.utils.v2.PageUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+@Service
+public class MaterialTagImpl implements MaterialTagService {
+
+    @Autowired
+    private MaterialTagDao materialTagDao;
+
+    /**
+     * 获取素材标签列表
+     */
+    @Override
+    public PageEntity selectMaterialTagList(MaterialTag materialTag) {
+        PageUtils.startPage();  // 分页
+        List<MaterialTag> list = materialTagDao.selectMaterialTagList(materialTag);
+        return new PageInfoResult(list).toEntity();
+    }
+
+    /**
+     * 获取素材标签列表 (下拉)
+     */
+    @Override
+    public List<MaterialTag> selectMaterialTagPopover(MaterialTag materialTag) {
+        List<MaterialTag> list = materialTagDao.selectMaterialTagList(materialTag);
+
+        // 筛选出列表中,所有 material_count 的总和
+        Integer all_material_count = list.stream().mapToInt(MaterialTag::getMaterial_count).sum();
+
+        MaterialTag all = new MaterialTag();
+        all.setTag_name("全部");
+        all.setTag_id(null);
+        all.setMaterial_count(all_material_count);
+        list.add(0, all);
+
+        return list;
+    }
+
+}

+ 13 - 5
src/main/java/com/backendsys/modules/sdk/comfyui/controller/ComfyuiDemoController.java

@@ -4,7 +4,6 @@ import com.backendsys.modules.common.config.security.annotations.Anonymous;
 import com.backendsys.modules.sdk.comfyui.service.ComfyuiService;
 import com.backendsys.modules.sdk.comfyui.service.ComfyuiSocketService;
 import com.backendsys.modules.sdk.tencentcloud.cos.service.TencentCosService;
-import com.backendsys.modules.upload.entity.SysFileResult;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.PostMapping;
@@ -17,9 +16,9 @@ public class ComfyuiDemoController {
     private TencentCosService tencentCosService;
 
     @Autowired
-    private ComfyuiService comfyUIService;
+    private ComfyuiService comfyuiService;
     @Autowired
-    private ComfyuiSocketService comfyUISocketService;
+    private ComfyuiSocketService comfyuiSocketService;
 
     /**
      * [ComfyUI] 创建 WebSocket 监听连接
@@ -27,7 +26,7 @@ public class ComfyuiDemoController {
     @Anonymous
     @PostMapping("/api/comfyui/ws/connect")
     public String connect(String clientId) {
-        comfyUISocketService.connect(clientId, 8000).subscribe();
+        comfyuiSocketService.connect(clientId, 8000).subscribe();
         return "Connection initiated for client_id: " + clientId;
     }
 
@@ -37,7 +36,7 @@ public class ComfyuiDemoController {
     @Anonymous
     @PostMapping("/api/comfyui/ws/disconnect")
     public String disconnect(String clientId) {
-        comfyUISocketService.disconnect(clientId).subscribe();
+        comfyuiSocketService.disconnect(clientId).subscribe();
         return "Disconnected: " + clientId;
     }
 
@@ -49,4 +48,13 @@ public class ComfyuiDemoController {
 //        return tencentCosService.urlToCOS(url, "png");
 //    }
 
+    /**
+     * [ComfyUI] 获取最佳端口
+     */
+    @Anonymous
+    @GetMapping("/api/comfyui/getBestPort")
+    public Integer getBestPort() {
+        return comfyuiService.getBestPort();
+    }
+
 }

+ 2 - 2
src/main/java/com/backendsys/modules/sdk/comfyui/dao/ComfyuiTaskExecuteDao.java

@@ -1,9 +1,9 @@
 package com.backendsys.modules.sdk.comfyui.dao;
 
-import com.backendsys.modules.sdk.comfyui.entity.ComfyuiTaskExecute;
+import com.backendsys.modules.sdk.comfyui.entity.ComfyuiTask;
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import org.apache.ibatis.annotations.Mapper;
 
 @Mapper
-public interface ComfyuiTaskExecuteDao extends BaseMapper<ComfyuiTaskExecute> {
+public interface ComfyuiTaskExecuteDao extends BaseMapper<ComfyuiTask> {
 }

+ 9 - 0
src/main/java/com/backendsys/modules/sdk/comfyui/entity/ComfyuiQueueItem.java

@@ -0,0 +1,9 @@
+package com.backendsys.modules.sdk.comfyui.entity;
+
+import lombok.Data;
+
+@Data
+public class ComfyuiQueueItem {
+    private Integer port;
+    private Integer count;
+}

+ 3 - 1
src/main/java/com/backendsys/modules/sdk/comfyui/entity/ComfyuiResponse.java

@@ -4,7 +4,9 @@ import lombok.Data;
 
 @Data
 public class ComfyuiResponse {
-    private String client_id;       // 任务ID
+    private String url;             // 任务链接
+    private Integer url_port;        // 任务链接
+    private String client_id;       // 任务Client ID
     private String prompt_id;       // 任务ID
     private Integer number;         // 当前任务序号,可用于后续获取需要等待任务数的计算
     private Object node_errors;     // 错误信息

+ 10 - 1
src/main/java/com/backendsys/modules/sdk/comfyui/entity/ComfyuiTask.java

@@ -14,8 +14,17 @@ public class ComfyuiTask {
     private Long user_id;                 // 用户ID
     private String client_id;             // Client ID
     private String task_type;             // 任务类型 (Text2Image, ..)
-    private Integer task_status;          // 任务状态 (-1:未开始, 1:进行中, 2:成功, 3:失败)
+    private String execute_prompt_id;     // 任务ID
+    private Integer execute_status;       // 任务状态 (-1:未开始, 1:进行中, 2:成功, 3:失败)
+    private String execute_url;           // 任务执行URL
+    private Integer execute_url_port;     // 任务执行URL端口
+
     private String generate_request;      // 任务请求原始参数 (JSONString)
+    private String generate_response;     // 任务生成原始结果 (JSONString)
+    private String reason;                // 任务状态信息,当任务失败时展示失败原因(如触发平台的内容风控等)
+
+    private Integer in_master;            // 队列状态 (-1:未进主队列, 1:已进主队列)
+    private String custom_params;         // 自定义参数
     private String create_time;
     private String update_time;
 

+ 0 - 29
src/main/java/com/backendsys/modules/sdk/comfyui/entity/ComfyuiTaskExecute.java

@@ -1,29 +0,0 @@
-package com.backendsys.modules.sdk.comfyui.entity;
-
-import com.baomidou.mybatisplus.annotation.IdType;
-import com.baomidou.mybatisplus.annotation.TableId;
-import com.baomidou.mybatisplus.annotation.TableName;
-import lombok.Data;
-
-@Data
-@TableName("comfyui_task_execute")
-public class ComfyuiTaskExecute {
-
-    @TableId(type = IdType.AUTO)
-    private Long id;
-    private Long task_id;
-    private Long user_id;                 // 用户ID
-    private String client_id;             // Client ID
-    private String task_type;             // 任务类型 (Text2Image, ..)
-    private String execute_prompt_id;     // 任务ID
-    private Integer execute_status;       // 任务状态 (-1:未开始, 1:进行中, 2:成功, 3:失败)
-    private String execute_url;           // 任务执行URL
-    private Integer execute_url_port;     // 任务执行URL端口
-
-    private String generate_request;      // 任务请求原始参数 (JSONString)
-    private String generate_response;     // 任务生成原始结果 (JSONString)
-    private String reason;                // 任务状态信息,当任务失败时展示失败原因(如触发平台的内容风控等)
-    private String create_time;
-    private String update_time;
-
-}

+ 23 - 0
src/main/java/com/backendsys/modules/sdk/comfyui/entity/__ComfyuiTask.java

@@ -0,0 +1,23 @@
+//package com.backendsys.modules.sdk.comfyui.entity;
+//
+//import com.baomidou.mybatisplus.annotation.IdType;
+//import com.baomidou.mybatisplus.annotation.TableId;
+//import com.baomidou.mybatisplus.annotation.TableName;
+//import lombok.Data;
+//
+//@Data
+//@TableName("comfyui_task")
+//public class ComfyuiTask {
+//
+//    @TableId(type = IdType.AUTO)
+//    private Long id;
+//    private Long user_id;                 // 用户ID
+//    private String client_id;             // Client ID
+//    private String task_type;             // 任务类型 (Text2Image, ..)
+//    private Integer task_status;          // 任务状态 (-1:未开始, 1:进行中, 2:成功, 3:失败)
+//    private String generate_request;      // 任务请求原始参数 (JSONString)
+//    private Integer in_master;            // 队列状态 (-1:未进主队列, 1:已进主队列)
+//    private String create_time;
+//    private String update_time;
+//
+//}

+ 46 - 0
src/main/java/com/backendsys/modules/sdk/comfyui/rabbitmq/ComfyuiQueueConfig.java

@@ -0,0 +1,46 @@
+package com.backendsys.modules.sdk.comfyui.rabbitmq;
+
+import com.backendsys.modules.common.config.rabbitmq.queue.DemoDlxQueueConfig;
+import org.springframework.amqp.core.*;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Lazy;
+
+/**
+ * 非懒加载,在项目启动时创建 RabbitMQ 队列、交换机、绑定关系
+ */
+@Configuration
+@Lazy(false)
+public class ComfyuiQueueConfig {
+
+    @Autowired
+    private ComfyuiQueueDlxConfig config;
+
+    public static final String EXCHANGE = "comfyui.exchange";
+    public static final String QUEUE = "comfyui.queue";
+    public static final String ROUTING_KEY = "comfyui.create";
+
+    // 交换机
+    @Bean
+    public DirectExchange comfyuiExchange() {
+        return ExchangeBuilder.directExchange(EXCHANGE).durable(true).build();
+    }
+
+    // 队列
+    @Bean
+    public Queue comfyuiQueue() {
+        return QueueBuilder.durable(QUEUE)
+            .deadLetterExchange(config.EXCHANGE)
+            .deadLetterRoutingKey(config.ROUTING_KEY)
+            .build();
+    }
+
+    // 队列绑定交换机
+    @Bean
+    public Binding comfyuiBinding() {
+        return BindingBuilder.bind(comfyuiQueue()).to(comfyuiExchange()).with(ROUTING_KEY);
+    }
+
+
+}

+ 30 - 0
src/main/java/com/backendsys/modules/sdk/comfyui/rabbitmq/ComfyuiQueueDlxConfig.java

@@ -0,0 +1,30 @@
+package com.backendsys.modules.sdk.comfyui.rabbitmq;
+
+import org.springframework.amqp.core.*;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Lazy;
+
+/**
+ * 死信队列
+ * 在项目启动时创建 (RabbitMQ) 队列、交换机、绑定关系
+ */
+@Configuration
+@Lazy(false)
+public class ComfyuiQueueDlxConfig {
+
+    /* === 死信交换机/队列 === */
+    public static final String EXCHANGE = "comfyui.dlx.exchange";
+    public static final String QUEUE = "comfyui.dlx.queue";
+    public static final String ROUTING_KEY = "comfyui.dlx.routekey";
+
+    @Bean
+    public DirectExchange comfyuiDlxExchange() { return ExchangeBuilder.directExchange(EXCHANGE).durable(true).build(); }
+
+    @Bean
+    public Queue comfyuiDlxQueue() { return QueueBuilder.durable(QUEUE).build(); }
+
+    @Bean
+    public Binding comfyuiDlxBinding() { return BindingBuilder.bind(comfyuiDlxQueue()).to(comfyuiDlxExchange()).with(ROUTING_KEY); }
+
+}

+ 137 - 0
src/main/java/com/backendsys/modules/sdk/comfyui/rabbitmq/ComfyuiRabbitListener.java

@@ -0,0 +1,137 @@
+package com.backendsys.modules.sdk.comfyui.rabbitmq;
+
+import cn.hutool.core.convert.Convert;
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.json.JSONObject;
+import cn.hutool.json.JSONUtil;
+import com.backendsys.modules.sdk.comfyui.dao.ComfyuiTaskDao;
+import com.backendsys.modules.sdk.comfyui.entity.ComfyuiResponse;
+import com.backendsys.modules.sdk.comfyui.entity.ComfyuiTask;
+import com.backendsys.modules.sdk.comfyui.service.ComfyuiService;
+import com.backendsys.modules.sdk.comfyui.service.ComfyuiSocketService;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.rabbitmq.client.Channel;
+import com.rabbitmq.client.GetResponse;
+import org.springframework.amqp.core.Message;
+import org.springframework.amqp.rabbit.annotation.RabbitListener;
+import org.springframework.amqp.support.AmqpHeaders;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Lazy;
+import org.springframework.messaging.handler.annotation.Header;
+import org.springframework.stereotype.Component;
+import reactor.core.publisher.Mono;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * 自定义监听器
+ */
+@Component
+@Lazy(false)
+public class ComfyuiRabbitListener {
+
+    @Autowired
+    private ComfyuiQueueConfig config;
+    @Autowired
+    private DeliveryTagHolder deliveryTagHolder;
+
+    @Autowired
+    private ComfyuiService comfyuiService;
+    @Autowired
+    private ComfyuiTaskDao comfyuiTaskDao;
+
+
+    // [监听] 手动 ACK
+    // (从 ComfyuiTaskService.tryPushNext 过来)
+    @RabbitListener(id = "ComfyuiContainer", queues = "comfyui.queue", ackMode = "MANUAL")
+    public void receive(Message message, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag) throws IOException {
+        try {
+
+            System.out.println("[Rabbitmq] 开始监听队列");
+
+            // 2. 业务处理 (提交Comfyui任务)
+            String body = new String(message.getBody(), StandardCharsets.UTF_8);
+            // {"id": 24, "user_id": 1, "client_id": "..", "generate_request" :"..", ..}
+
+            // 收到的参数:
+            JSONObject body_object = JSONUtil.parseObj(body);
+            String prompt = body_object.getStr("generate_request");
+            String client_id = body_object.getStr("client_id");
+            String custom_params = body_object.getStr("custom_params");
+
+            JSONObject prompt_object = JSONUtil.parseObj(prompt);
+
+            // [ComfyUI] 发起任务
+            Mono<ComfyuiResponse> cfPromptResponseMono = comfyuiService.prompt(client_id, prompt_object, custom_params);
+            ComfyuiResponse response = cfPromptResponseMono.block();
+            System.out.println("[Comfyui][发起任务] response = " + response);
+
+            // -- [DB] 更新 任务ID、任务状态、任务URL、任务URL端口 --------------------------
+            ComfyuiTask entity = new ComfyuiTask();
+            entity.setExecute_prompt_id(response.getPrompt_id());
+            entity.setExecute_url(response.getUrl());
+            entity.setExecute_url_port(response.getUrl_port());
+            entity.setExecute_status(1);    // 1: 正在生成中
+
+            LambdaQueryWrapper<ComfyuiTask> wrapperTask = new LambdaQueryWrapper<>();
+            wrapperTask.eq(ComfyuiTask::getClient_id, client_id);
+            comfyuiTaskDao.update(entity, wrapperTask);
+            // ------------------------------------------------------------------------
+
+            // 2. 暂存 deliveryTag
+            System.out.println("put client_id = " + client_id);
+            deliveryTagHolder.put(client_id, deliveryTag, channel);
+
+            /*
+                外部调用ACK (确认消息):
+                try {
+                    Connection connection = rabbitTemplate.getConnectionFactory().createConnection();
+                    Channel channel = connection.createChannel(false);
+                    GetResponse resp = channel.basicGet("demo.queue", false);
+                    if (resp != null) {
+                        channel.basicAck(resp.getEnvelope().getDeliveryTag(), false);
+                    }
+                    channel.close();
+                    connection.close();
+                } catch (Exception e) {
+                    System.err.println("(RabbitListener) 处理失败: " + e.getMessage());
+                }
+             */
+
+            // 3. 手动确认(deliveryTag + 是否批量)
+            // channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
+
+        } catch (Exception e) {
+            System.err.println("(RabbitListener) 处理失败: " + e.getMessage());
+            // 拒绝并重新入队
+            channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);
+        }
+    }
+
+
+    // [监听] 死信队列
+    @RabbitListener(id = "ComfyuiDlxContainer", queues = "comfyui.dlx.queue", ackMode = "MANUAL")
+    public void handleComfyuiDlx(Message message, Channel channel) throws IOException {
+
+        String body = new String(message.getBody());
+        System.out.println("[Comfyui][RabbitMQ-死信] 收到:" + StrUtil.sub(body, 0, 1000));
+
+        // 业务:记录日志 / 重发 / 报警 / 人工补偿
+        channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
+    }
+
+
+    // 4. 并发消费(一条队列多线程)
+    /*
+    @ComfyuiRabbitListener(queues = "demo.queue", concurrency = "5-10")
+    public void receive(String msg) { ... }
+     */
+
+
+
+}

+ 21 - 0
src/main/java/com/backendsys/modules/sdk/comfyui/rabbitmq/DeliveryTagHolder.java

@@ -0,0 +1,21 @@
+package com.backendsys.modules.sdk.comfyui.rabbitmq;
+
+import com.rabbitmq.client.Channel;
+import org.springframework.stereotype.Component;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+@Component
+public class DeliveryTagHolder {
+    private final Map<String, TagCtx> map = new ConcurrentHashMap<>();
+
+    public void put(String key, long tag, Channel ch) {
+        map.put(key, new TagCtx(tag, ch));
+    }
+
+    public TagCtx take(String key) {
+        return map.remove(key);
+    }
+}
+

+ 10 - 0
src/main/java/com/backendsys/modules/sdk/comfyui/rabbitmq/TagCtx.java

@@ -0,0 +1,10 @@
+package com.backendsys.modules.sdk.comfyui.rabbitmq;
+
+import com.rabbitmq.client.Channel;
+import lombok.Data;
+
+@Data
+public class TagCtx {
+    private final long deliveryTag;
+    private final Channel channel;
+}

+ 5 - 1
src/main/java/com/backendsys/modules/sdk/comfyui/service/ComfyuiService.java

@@ -8,9 +8,13 @@ import reactor.core.publisher.Mono;
 public interface ComfyuiService {
 
     // [ComfyUI] 查询任务队列
-    Mono<ComfyuiQueue> getQueue();
+    Mono<ComfyuiQueue> getQueue(Integer port);
+
+    // [ComfyUI] 获取最佳可用端口
+    Integer getBestPort();
 
     // [ComfyUI] 执行任务 (通用)
     Mono<ComfyuiResponse> prompt(String client_id, JSONObject prompt);
+    Mono<ComfyuiResponse> prompt(String client_id, JSONObject prompt, String custom_params);
 
 }

+ 3 - 0
src/main/java/com/backendsys/modules/sdk/comfyui/service/ComfyuiSocketService.java

@@ -14,6 +14,9 @@ public interface ComfyuiSocketService {
     Mono<Void> connectToSse(String clientId, Integer port, Boolean is_save);
     Mono<Void> connectToSse(String clientId, Integer port, Boolean is_save, Map<String, Object> params);
 
+    // 如果是在 websocket 周期,则需要手动输入 user_id
+    Mono<Void> connectToSse(String clientId, Integer port, Boolean is_save, Map<String, Object> params, Long input_user_id);
+
     // [ComfyUI] 断开 WebSocket 监听连接
     Mono<Void> disconnect(String clientId);
 

+ 9 - 0
src/main/java/com/backendsys/modules/sdk/comfyui/service/ComfyuiTaskService.java

@@ -0,0 +1,9 @@
+package com.backendsys.modules.sdk.comfyui.service;
+
+public interface ComfyuiTaskService {
+
+    // 推送主队列任务 (以用户为队列单位)
+    void tryPushNext(long user_id);
+    void tryPushNext(long user_id, String custom_params);
+
+}

+ 126 - 7
src/main/java/com/backendsys/modules/sdk/comfyui/service/impl/ComfyuiServiceImpl.java

@@ -1,11 +1,17 @@
 package com.backendsys.modules.sdk.comfyui.service.impl;
 
+import cn.hutool.core.convert.Convert;
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.json.JSONArray;
 import cn.hutool.json.JSONObject;
+import cn.hutool.json.JSONUtil;
 import com.backendsys.modules.common.Filter.WebClientFilter;
-import com.backendsys.modules.sdk.comfyui.entity.ComfyuiRequest;
-import com.backendsys.modules.sdk.comfyui.entity.ComfyuiResponse;
-import com.backendsys.modules.sdk.comfyui.entity.ComfyuiQueue;
+import com.backendsys.modules.sdk.comfyui.dao.ComfyuiTaskDao;
+import com.backendsys.modules.sdk.comfyui.entity.*;
 import com.backendsys.modules.sdk.comfyui.service.ComfyuiService;
+import com.backendsys.modules.sdk.comfyui.service.ComfyuiSocketService;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.http.HttpHeaders;
 import org.springframework.http.MediaType;
@@ -13,6 +19,7 @@ import org.springframework.stereotype.Service;
 import org.springframework.web.reactive.function.client.WebClient;
 import reactor.core.publisher.Mono;
 
+import java.util.*;
 import java.util.function.Consumer;
 
 @Service
@@ -22,6 +29,15 @@ public class ComfyuiServiceImpl implements ComfyuiService {
     private String COMFYUI_HOST;
     @Value("${comfyui.token}")
     private String COMFYUI_TOKEN;
+    @Value("${comfyui.ports}")
+    private String COMFYUI_PORTS_STR;
+    @Value("${comfyui.is-save}")
+    private Boolean IS_SAVE;
+
+    @Autowired
+    private ComfyuiTaskDao comfyuiTaskDao;
+    @Autowired
+    private ComfyuiSocketService comfyuiSocketService;
 
     private WebClient webClient;
     public WebClient getWebClient() {
@@ -45,8 +61,8 @@ public class ComfyuiServiceImpl implements ComfyuiService {
     /**
      * [ComfyUI] 查询任务队列
      */
-    public Mono<ComfyuiQueue> getQueue() {
-        String url = "http://" + COMFYUI_HOST + ":8000/queue";
+    public Mono<ComfyuiQueue> getQueue(Integer port) {
+        String url = "http://" + COMFYUI_HOST + ":" + port + "/queue";
         WebClient webClient = getWebClient();
         return webClient.get()
                 .uri(url)
@@ -55,18 +71,106 @@ public class ComfyuiServiceImpl implements ComfyuiService {
                 .exchangeToMono(response -> response.bodyToMono(ComfyuiQueue.class));
     }
 
+    /**
+     * 获取最佳可用端口
+     */
+    @Override
+    public Integer getBestPort() {
+
+//        List<Integer> PORTS = Arrays.asList(8000, 8001, 8002, 8003, 8004, 8005, 8006, 8007);
+        List<Integer> PORTS = Convert.toList(Integer.class, StrUtil.split(COMFYUI_PORTS_STR, ","));
+
+        List<ComfyuiQueueItem> queueCountsList = new ArrayList<>();
+
+        // 查询所有端口的队列情况
+        Boolean is_empty_queue = false;
+        for (int i = 0; i < PORTS.size(); i++) {
+
+            Integer port = PORTS.get(i);
+
+            ComfyuiQueueItem queueCountsItem = new ComfyuiQueueItem();
+            queueCountsItem.setPort(port);
+
+            System.out.println("-- 请求端口: " + port + " ---------------");
+
+            // [WebClient] 远程查询 ComfyuiAPI 队列情况
+            Mono<ComfyuiQueue> cfQueueMono = getQueue(port);
+            ComfyuiQueue response = cfQueueMono.block();
+            if (response != null) {
+                JSONArray queue_pending = response.getQueue_pending();
+                JSONArray queue_running = response.getQueue_running();
+                Integer count = queue_pending.size() + queue_running.size();
+                System.out.println("port: " + port + ", queueCount: " + count);
+
+                queueCountsItem.setCount(count);
+                queueCountsList.add(0, queueCountsItem);  // 从前面添加记录
+
+                if (queue_pending.size() == 0 && queue_running.size() == 0) {
+                    System.out.println("发现空的队列,停止遍历: " + port);
+                    is_empty_queue = true;
+                    break;  // 中断循环
+                }
+
+            } else {
+                System.out.println("port: " + port + ", response is null. ");
+            }
+
+        }
+
+        // 没有空队列时
+        // - 再次遍历,找出 count 最小值的行数据;
+        // - 将最小值行数据排序到最前面;
+        if (!is_empty_queue) {
+            queueCountsList.sort(Comparator.comparing(ComfyuiQueueItem::getCount));
+            System.out.println("-------------------------------");
+            System.out.println("没有空队列,将队列数量最小值的行数据排序到最前面: ");
+            System.out.println("port = " + queueCountsList.get(0).getPort() + ", count = " + queueCountsList.get(0).getCount());
+        }
+
+        System.out.println("-- 最终选择的端口: ----------------");
+        System.out.println("port = " + queueCountsList.get(0).getPort() + ", count = " + queueCountsList.get(0).getCount());
+
+        return queueCountsList.isEmpty() ? null : queueCountsList.get(0).getPort();
+    }
+
+
     /**
      * [ComfyUI] 执行任务
      */
     @Override
     public Mono<ComfyuiResponse> prompt(String client_id, JSONObject prompt) {
+        return prompt(client_id, prompt, null);
+    }
+    @Override
+    public Mono<ComfyuiResponse> prompt(String client_id, JSONObject prompt, String custom_params) {
+
+        // 获取最优端口
+        Integer port = getBestPort();
+
+        System.out.println("自定义参数 (custom_params) = " + custom_params);
+
+        System.out.println("[ComfyUI] 开始监听 WecSocket 事件");
+        JSONObject custom_params_object = (StrUtil.isNotEmpty(custom_params)) ? JSONUtil.parseObj(custom_params) : new JSONObject();
+        // -- [ComfyUI] 创建 WebSocket 监听进度 --------------------------
+        Map<String, Object> params = new LinkedHashMap<>();
+        Long drama_project_storyboard_id = Convert.toLong(custom_params_object.get("drama_project_storyboard_id"));
+        Long user_id = Convert.toLong(custom_params_object.get("user_id"));
+        if (drama_project_storyboard_id != null) {
+            params.put("drama_project_storyboard_id", drama_project_storyboard_id);
+        }
+        // - is_save: 是否转存到 cos/tos
+        comfyuiSocketService.connectToSse(client_id, port, IS_SAVE, params, user_id).subscribe();
+        // -------------------------------------------------------------
+        // 在任务结束后(SSE)(ComfyuiSocketService.connectToSse):
+        // 1) 手动确认消息(ACK)
+        // 2) 清除队列锁 (QUEUE_LOCK_KEY)
+        // 3) 尝试把下一条任务推进主队列
 
         ComfyuiRequest bodyValue = new ComfyuiRequest();
         bodyValue.setClient_id(client_id);
         bodyValue.setPrompt(prompt);
 
-        String url = "http://" + COMFYUI_HOST + ":8000/prompt";
-
+        String url = "http://" + COMFYUI_HOST + ":" + port + "/prompt";
         WebClient webClient = getWebClient();
         return webClient.post()
                 .uri(url)
@@ -74,10 +178,25 @@ public class ComfyuiServiceImpl implements ComfyuiService {
                 .accept(MediaType.APPLICATION_JSON)
                 .bodyValue(bodyValue)
                 .exchangeToMono(response -> response.bodyToMono(ComfyuiResponse.class))
+                .map(r -> {
+                    r.setUrl(url);
+                    r.setUrl_port(port);
+                    return r;
+                })
                 .onErrorResume(e -> {
                     // 捕获所有异常(包括上面抛出的 RuntimeException)
                     ComfyuiResponse response = new ComfyuiResponse();
                     response.setNode_errors(e.getMessage());
+
+                    // -- [DB] 更新任务错误信息 --------------------------------------------------
+                    ComfyuiTask entity = new ComfyuiTask();
+                    entity.setReason(e.getMessage());
+                    entity.setExecute_status(3);
+                    LambdaQueryWrapper<ComfyuiTask> wrapperTask = new LambdaQueryWrapper<>();
+                    wrapperTask.eq(ComfyuiTask::getClient_id, client_id);
+                    comfyuiTaskDao.update(entity, wrapperTask);
+                    // ------------------------------------------------------------------------
+
                     return Mono.just(response);
                 });
 

+ 106 - 12
src/main/java/com/backendsys/modules/sdk/comfyui/service/impl/ComfyuiSocketServiceImpl.java

@@ -1,20 +1,30 @@
 package com.backendsys.modules.sdk.comfyui.service.impl;
 
 import cn.hutool.core.convert.Convert;
+import cn.hutool.core.map.MapUtil;
 import cn.hutool.json.JSONArray;
 import cn.hutool.json.JSONObject;
 import cn.hutool.json.JSONUtil;
+import com.backendsys.modules.common.config.redis.utils.RedisUtil;
 import com.backendsys.modules.common.config.security.utils.SecurityUtil;
 import com.backendsys.modules.crt.dao.CrtGenerateImageDao;
 import com.backendsys.modules.crt.entity.CrtGenerateImage;
 import com.backendsys.modules.sdk.comfyui.enums.TaskStatusEnums;
+import com.backendsys.modules.sdk.comfyui.rabbitmq.ComfyuiQueueConfig;
+import com.backendsys.modules.sdk.comfyui.rabbitmq.ComfyuiRabbitListener;
+import com.backendsys.modules.sdk.comfyui.rabbitmq.DeliveryTagHolder;
+import com.backendsys.modules.sdk.comfyui.rabbitmq.TagCtx;
 import com.backendsys.modules.sdk.comfyui.service.ComfyuiSocketService;
-import com.backendsys.modules.sdk.comfyui.utils.ComfyUtil;
+import com.backendsys.modules.sdk.comfyui.service.ComfyuiTaskService;
+import com.backendsys.modules.sdk.comfyui.utils.ComfyuiUtil;
 import com.backendsys.modules.sse.entity.SseResponse;
 import com.backendsys.modules.sse.entity.SseResponseEnum;
 import com.backendsys.modules.sse.utils.SseUtil;
 import com.backendsys.modules.upload.entity.SysFileResult;
 import com.backendsys.modules.upload.service.SysFileService;
+import com.rabbitmq.client.Channel;
+import com.rabbitmq.client.GetResponse;
+import org.springframework.amqp.rabbit.core.RabbitTemplate;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.stereotype.Service;
@@ -29,11 +39,14 @@ import reactor.netty.http.client.HttpClient;
 import java.net.URI;
 import java.time.Duration;
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ConcurrentHashMap;
 
+import org.springframework.amqp.rabbit.connection.Connection;
+
 @Service
 public class ComfyuiSocketServiceImpl implements ComfyuiSocketService {
 
@@ -41,12 +54,27 @@ public class ComfyuiSocketServiceImpl implements ComfyuiSocketService {
     private SseUtil sseUtil;
 
     @Autowired
-    private ComfyUtil comfyUtil;
+    private RedisUtil redisUtil;
+    @Autowired
+    private RabbitTemplate rabbitTemplate;
+
+
+    @Autowired
+    private ComfyuiQueueConfig config;
+    @Autowired
+    private ComfyuiUtil comfyuiUtil;
+    @Autowired
+    private ComfyuiTaskService comfyuiTaskService;
     @Autowired
     private SysFileService sysFileService;
     @Autowired
     private CrtGenerateImageDao crtGenerateImageDao;
+    @Autowired
+    private DeliveryTagHolder deliveryTagHolder;
+
 
+    @Value("${spring.application.name}")
+    private String APPLICATION_NAME;
     @Value("${comfyui.host}")
     private String COMFYUI_HOST;
     @Value("${comfyui.token}")
@@ -121,9 +149,14 @@ public class ComfyuiSocketServiceImpl implements ComfyuiSocketService {
     }
     @Override
     public Mono<Void> connectToSse(String client_id, Integer port, Boolean is_save, Map<String, Object> params) {
+        return connectToSse(client_id, port, is_save, params, null);
+    }
+    // 如果是在 websocket 周期,则需要手动输入 user_id
+    @Override
+    public Mono<Void> connectToSse(String client_id, Integer port, Boolean is_save, Map<String, Object> params, Long input_user_id) {
 
         // 由于 Websocket 获取不到上下文信息,所以 user_id 不能在 socket 周期获取
-        Long user_id = SecurityUtil.getUserId();
+        Long user_id = input_user_id != null ? input_user_id : SecurityUtil.getUserId();
 
         String wsUrl =  "ws://" + COMFYUI_HOST + ":" + port + "/ws";
         return Mono.defer(() -> {
@@ -132,12 +165,11 @@ public class ComfyuiSocketServiceImpl implements ComfyuiSocketService {
             }
             // 动态创建带有认证头的客户端
             WebSocketClient clientWithAuth = createWebSocketClientWithToken(COMFYUI_TOKEN);
-            return clientWithAuth.execute(URI.create(wsUrl + "?clientId=" + client_id), session -> {
+            String wsUrlFull = wsUrl + "?clientId=" + client_id + "&userId=" + user_id;
+            System.out.println("Listen: " + wsUrlFull);
+            return clientWithAuth.execute(URI.create(wsUrlFull), session -> {
                 // 保存会话
                 sessions.put(client_id, session);
-                // 接收消息
-                System.out.println("------ wsUrl: " + wsUrl + " ------");
-                System.out.println("------ connectToSse client_id: " + client_id + ", user_id: " + user_id + " ------");
 
                 Flux<String> incomingMessages = session.receive()
                         .map(WebSocketMessage::getPayloadAsText)
@@ -151,21 +183,83 @@ public class ComfyuiSocketServiceImpl implements ComfyuiSocketService {
                             dataChildren.put("drama_project_storyboard_id", drama_project_storyboard_id);
                             data.put("data", dataChildren);
 
-                            // == [任务执行完成] =======================================================
+                            // == [任务执行完成] =======================================================================
                             // { "type": "executed", .. }
                             String type = Convert.toStr(data.get("type"));
                             if (TaskStatusEnums.EXECUTED.getValue().equals(type)) {
 
+                                // 在任务结束后(SSE)(ComfyuiSocketService.connectToSse):
+                                // 1) 手动确认消息(ACK)
+                                // 2) 清除队列锁 (QUEUE_LOCK_KEY)
+                                // 3) 尝试把下一条任务推进主队列
+
+
+
+
+
+
+
+                                // -- [RabbitMQ][完成当前队列任务,放行下一个队列] ----------------------------------------
+                                try {
+                                    System.out.println("-- 准备放行队列 --");
+//                                    Connection connection = rabbitTemplate.getConnectionFactory().createConnection();
+//                                    Channel channel = connection.createChannel(false);
+//                                    GetResponse resp = channel.basicGet(config.QUEUE, false);
+
+                                    // 已经被 Listener 的这里拿不到?
+                                    // 这里是空的?
+                                    TagCtx tag = deliveryTagHolder.take(client_id);
+                                    Channel channel = tag.getChannel();
+
+                                    // 1) 手动确认消息 (ACK)
+                                    System.out.println("-- 手动确认消息 (ACK) ----------");
+//                                    channel.basicAck(resp.getEnvelope().getDeliveryTag(), false);
+                                    System.out.println("client_id = " + client_id + ", tag = " + tag);
+                                    channel.basicAck(tag.getDeliveryTag(), false);
+                                    channel.close();
+
+
+                                    // 2) 清除队列锁 (QUEUE_LOCK_KEY)
+                                    String QUEUE_LOCK_KEY = APPLICATION_NAME + ":comfyui:queue:lock:user:" + Convert.toStr(user_id);
+                                    System.out.println("-- 清除队列锁 (QUEUE_LOCK_KEY): " + QUEUE_LOCK_KEY);
+                                    redisUtil.delete(QUEUE_LOCK_KEY);
+
+
+
+                                } catch (Exception e) {
+                                    System.out.println(e.getMessage());
+                                }
+                                // ----------------------------------------------------------------------------------
+
+
+
+
+
+
+
+
                                 JSONObject output = JSONUtil.parseObj(dataChildren.get("output"));
 
-                                // -- [生成图片 -> 转存图片 -> 新增记录] ------------------------------------------------------
+                                // -- [生成图片 -> 转存图片 -> 新增记录] ------------------------------------------------
                                 // 由于图片地址不是公开的,需要加 Token 访问,因此不能公开返回原始图片地址,比如:
                                 // http://43.128.1.201:8000/api/view?filename=fenjing_1_00012_.png&token=$2b$12$.MR4qGaFetN1FPQzbfyIrehsyjnPJ12xAZhR/l7KZpLkUPQTCG4gy
                                 // 因此,直接在 ComfyUI 的物理目录上构建 nginx 静态资源访问目录
                                 Object imagesObj = output.get("images");
                                 if (imagesObj != null) {
-                                    String prompt_id = Convert.toStr(dataChildren.get("prompt_id"));
 
+                                    // 3) 尝试把下一条任务推进主队列 (生图) ----------------------------------------------
+                                    System.out.println("-- 尝试把下一条任务推进主队列 (生图) --");
+                                    Map<String, Object> map = new HashMap<>();
+                                    map.put("drama_project_storyboard_id", drama_project_storyboard_id);
+                                    map.put("user_id", user_id);
+                                    String custom_params = JSONUtil.toJsonStr(map);
+
+                                    System.out.println("custom_params = " + custom_params);
+
+                                    comfyuiTaskService.tryPushNext(user_id, custom_params);
+                                    // -----------------------------------------------------------------------------
+
+                                    String prompt_id = Convert.toStr(dataChildren.get("prompt_id"));
 
                                     // 临时图片相对路径
                                     JSONArray images = JSONUtil.parseArray(imagesObj);
@@ -186,9 +280,9 @@ public class ComfyuiSocketServiceImpl implements ComfyuiSocketService {
                                     dataChildren.put("output", output);
                                     data.put("data", dataChildren);
 
-                                    // [DB] 执行任务
+                                    // [DB] 更新任务状态
                                     CompletableFuture.runAsync(() -> {
-                                        comfyUtil.executeComfyuiTask(prompt_id, JSONUtil.toJsonStr(dataChildren), 2);
+                                        comfyuiUtil.updateComfyuiTask(prompt_id, JSONUtil.toJsonStr(dataChildren), 2);
                                     });
 
                                 }

+ 110 - 0
src/main/java/com/backendsys/modules/sdk/comfyui/service/impl/ComfyuiTaskServiceImpl.java

@@ -0,0 +1,110 @@
+package com.backendsys.modules.sdk.comfyui.service.impl;
+
+import cn.hutool.core.convert.Convert;
+import cn.hutool.core.util.StrUtil;
+import com.backendsys.modules.common.config.redis.utils.RedisUtil;
+import com.backendsys.modules.sdk.comfyui.dao.ComfyuiTaskDao;
+import com.backendsys.modules.sdk.comfyui.entity.ComfyuiTask;
+import com.backendsys.modules.sdk.comfyui.rabbitmq.ComfyuiQueueConfig;
+import com.backendsys.modules.sdk.comfyui.service.ComfyuiTaskService;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import org.springframework.amqp.rabbit.core.RabbitTemplate;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.time.Duration;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@Service
+public class ComfyuiTaskServiceImpl implements ComfyuiTaskService {
+
+    @Value("${spring.application.name}")
+    private String APPLICATION_NAME;
+
+    @Autowired
+    private RedisTemplate redisTemplate;
+    @Autowired
+    private RabbitTemplate rabbitTemplate;
+    @Autowired
+    private ComfyuiQueueConfig config;
+
+    @Autowired
+    private ComfyuiTaskDao comfyuiTaskDao;
+
+    /**
+     * 推送主队列任务 (以用户为队列单位)
+     * 当用户提交新任务、或某任务执行结束后,都调用它
+     */
+    @Override
+    @Transactional
+    public void tryPushNext(long user_id) {
+        tryPushNext(user_id, null);
+    }
+    @Override
+    @Transactional
+    public void tryPushNext(long user_id, String custom_params) {
+
+        System.out.println("[推送主队列任务]");
+        // 1. 如果该用户已在主队列里,直接返回
+        String QUEUE_LOCK_KEY = APPLICATION_NAME + ":comfyui:queue:lock:user:" + Convert.toStr(user_id);
+        System.out.println("-- (Redis) QUEUE_LOCK_KEY = " + QUEUE_LOCK_KEY);
+        if (Boolean.TRUE.equals(redisTemplate.hasKey(QUEUE_LOCK_KEY))) {
+            return;
+        }
+
+        // 2. 抢令牌(原子)
+        Boolean ok = redisTemplate.opsForValue().setIfAbsent(QUEUE_LOCK_KEY, "1", Duration.ofMinutes(10));
+        if (!Boolean.TRUE.equals(ok)) {
+            return; // 抢失败(并发极小概率)
+        }
+
+        // 3. 该用户下一条未进主队列的任务
+        LambdaQueryWrapper<ComfyuiTask> wrapperTask = new LambdaQueryWrapper<>();
+        wrapperTask.eq(ComfyuiTask::getUser_id, user_id);
+        wrapperTask.eq(ComfyuiTask::getIn_master, -1);
+        List<ComfyuiTask> taskListByUser = comfyuiTaskDao.selectList(wrapperTask);
+        if (taskListByUser == null || taskListByUser.isEmpty()) {
+            System.out.println("用户没有任务了");
+            redisTemplate.delete(String.format(QUEUE_LOCK_KEY, user_id));
+            return;
+        } else {
+            System.out.println("用户队列数量: " + taskListByUser.size());
+        }
+
+        ComfyuiTask taskByUser = taskListByUser.get(0);
+        wrapperTask.eq(ComfyuiTask::getId, taskByUser.getId());
+
+        // 4. 打标 (进主队列)
+        ComfyuiTask entity = new ComfyuiTask();
+        entity.setIn_master(1);
+        taskByUser.setIn_master(1);
+        taskByUser.setCustom_params(custom_params);
+        comfyuiTaskDao.update(entity, wrapperTask);
+
+//        System.out.println("-- 在这里开始执行任务吗? --");
+//
+//        String client_id = taskByUser.getClient_id();
+//        System.out.println("task_id: " + taskByUser.getId());
+//        System.out.println("client_id: " + client_id);
+//        String prompt_object = taskByUser.getGenerate_request();
+//        System.out.println("prompt_object: " + StrUtil.sub(prompt_object, 0, 200));
+//
+////        // [ComfyUI] 执行任务
+////        JSONObject prompt_object = JSONUtil.parseObj(prompt);
+////        Mono<ComfyuiResponse> cfPromptResponseMono = comfyuiService.prompt(client_id, prompt_object);
+////        ComfyuiResponse response = cfPromptResponseMono.block();
+////        response.setClient_id(client_id);
+
+        // 5. 发送消息 (rabbitmq)
+        System.out.println("-- 推送成功: (rabbitmq) task_id: " + taskByUser.getId());
+        rabbitTemplate.convertAndSend(config.EXCHANGE, config.ROUTING_KEY, taskByUser);
+        System.out.println("--------------------------------------------------------");
+
+    }
+
+}

+ 8 - 12
src/main/java/com/backendsys/modules/sdk/comfyui/service/impl/ComfyuiText2ImageServiceImpl.java

@@ -2,27 +2,24 @@ package com.backendsys.modules.sdk.comfyui.service.impl;
 
 import cn.hutool.json.JSONObject;
 import cn.hutool.json.JSONUtil;
-import com.backendsys.modules.sdk.comfyui.dao.ComfyuiTaskDao;
 import com.backendsys.modules.sdk.comfyui.entity.ComfyuiResponse;
 import com.backendsys.modules.sdk.comfyui.entity.ComfyuiText2Image;
 import com.backendsys.modules.sdk.comfyui.enums.TaskTypeEnums;
 import com.backendsys.modules.sdk.comfyui.service.ComfyuiService;
 import com.backendsys.modules.sdk.comfyui.service.ComfyuiText2ImageService;
-import com.backendsys.modules.sdk.comfyui.utils.ComfyUtil;
+import com.backendsys.modules.sdk.comfyui.utils.ComfyuiUtil;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 import reactor.core.publisher.Mono;
 
-import java.util.concurrent.CompletableFuture;
-
 @Service
 public class ComfyuiText2ImageServiceImpl implements ComfyuiText2ImageService {
 
     @Autowired
-    private ComfyuiService comfyUIService;
+    private ComfyuiUtil comfyuiUtil;
 
     @Autowired
-    private ComfyUtil comfyUtil;
+    private ComfyuiService comfyuiService;
 
     /**
      * [ComfyUI] 文生图 (7.16生图.json)
@@ -338,17 +335,16 @@ public class ComfyuiText2ImageServiceImpl implements ComfyuiText2ImageService {
                 "}"
         ;
 
-
         // [DB] 初始化任务
-        Long task_id = comfyUtil.initComfyuiTask(client_id, prompt, TaskTypeEnums.TEXT_2_IMAGE.getValue());
+        Long task_id = comfyuiUtil.initComfyuiTask(client_id, prompt, TaskTypeEnums.TEXT_2_IMAGE.getValue());
         return task_id;
 
 //        // [ComfyUI] 执行任务
 //        JSONObject prompt_object = JSONUtil.parseObj(prompt);
-//        Mono<ComfyuiResponse> cfPromptResponseMono = comfyUIService.prompt(client_id, prompt_object);
+//        Mono<ComfyuiResponse> cfPromptResponseMono = comfyuiService.prompt(client_id, prompt_object);
 //        ComfyuiResponse response = cfPromptResponseMono.block();
 //        response.setClient_id(client_id);
-
+//
 //        return response;
     }
 
@@ -431,12 +427,12 @@ public class ComfyuiText2ImageServiceImpl implements ComfyuiText2ImageService {
 
 
         // [DB] 初始化任务
-        Long task_id = comfyUtil.initComfyuiTask(client_id, prompt, TaskTypeEnums.TEXT_2_IMAGE.getValue());
+        Long task_id = comfyuiUtil.initComfyuiTask(client_id, prompt, TaskTypeEnums.TEXT_2_IMAGE.getValue());
         return task_id;
 
 //        // [ComfyUI] 执行任务
 //        JSONObject prompt_object = JSONUtil.parseObj(prompt);
-//        Mono<ComfyuiResponse> cfPromptResponseMono = comfyUIService.prompt(client_id, prompt_object);
+//        Mono<ComfyuiResponse> cfPromptResponseMono = comfyuiService.prompt(client_id, prompt_object);
 //        ComfyuiResponse response = cfPromptResponseMono.block();
 //        response.setClient_id(client_id);
 

+ 9 - 10
src/main/java/com/backendsys/modules/sdk/comfyui/utils/ComfyUtil.java → src/main/java/com/backendsys/modules/sdk/comfyui/utils/ComfyuiUtil.java

@@ -4,13 +4,12 @@ import com.backendsys.modules.common.config.security.utils.SecurityUtil;
 import com.backendsys.modules.sdk.comfyui.dao.ComfyuiTaskDao;
 import com.backendsys.modules.sdk.comfyui.dao.ComfyuiTaskExecuteDao;
 import com.backendsys.modules.sdk.comfyui.entity.ComfyuiTask;
-import com.backendsys.modules.sdk.comfyui.entity.ComfyuiTaskExecute;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Component;
 
 @Component
-public class ComfyUtil {
+public class ComfyuiUtil {
 
     @Autowired
     private ComfyuiTaskDao comfyuiTaskDao;
@@ -40,17 +39,17 @@ public class ComfyUtil {
      * [更新] 执行任务
      * - status: 任务状态 (-1:未开始, 1:进行中, 2:成功, 3:失败)
      */
-    public void executeComfyuiTask(String prompt_id, String response, Integer status) {
+    public void updateComfyuiTask(String prompt_id, String response, Integer status) {
         try {
-            LambdaQueryWrapper<ComfyuiTaskExecute> wrapper = new LambdaQueryWrapper<>();
-            wrapper.eq(ComfyuiTaskExecute::getExecute_prompt_id, prompt_id);
+            LambdaQueryWrapper<ComfyuiTask> wrapper = new LambdaQueryWrapper<>();
+            wrapper.eq(ComfyuiTask::getExecute_prompt_id, prompt_id);
 
-            ComfyuiTaskExecute comfyuiTaskExecute = new ComfyuiTaskExecute();
-            comfyuiTaskExecute.setExecute_status(status);
-            comfyuiTaskExecute.setExecute_prompt_id(prompt_id);
-            comfyuiTaskExecute.setGenerate_response(response);
+            ComfyuiTask comfyuiTask = new ComfyuiTask();
+            comfyuiTask.setExecute_status(status);
+            comfyuiTask.setExecute_prompt_id(prompt_id);
+            comfyuiTask.setGenerate_response(response);
 
-            comfyuiTaskExecuteDao.update(comfyuiTaskExecute, wrapper);
+            comfyuiTaskExecuteDao.update(comfyuiTask, wrapper);
         } catch (Exception e) {
             System.out.println(e.getMessage());
         }

+ 4 - 0
src/main/java/com/backendsys/modules/upload/service/SysFileService.java

@@ -3,6 +3,7 @@ package com.backendsys.modules.upload.service;
 import com.backendsys.modules.upload.entity.SysFile;
 import com.backendsys.modules.upload.entity.SysFileMergeByMd5;
 import com.backendsys.modules.upload.entity.SysFileResult;
+import com.backendsys.modules.upload.utils.ObjectKey.ObjectKeyEntity;
 import com.backendsys.utils.response.PageEntity;
 import com.baomidou.mybatisplus.extension.service.IService;
 import org.springframework.web.multipart.MultipartFile;
@@ -39,5 +40,8 @@ public interface SysFileService extends IService<SysFile> {
     // 合并重复 MD5 文件
     Map<String, Object> mergeFileByMd5(SysFileMergeByMd5 sysFileMergeByMd5);
 
+    // 删除存储桶对象 (单个)
+    void deleteObject(String object_key, Integer target);
+    void deleteObjects(List<ObjectKeyEntity> list);
 
 }

+ 27 - 2
src/main/java/com/backendsys/modules/upload/service/impl/SysFileServiceImpl.java

@@ -25,6 +25,7 @@ import com.backendsys.modules.upload.entity.SysFileResult;
 import com.backendsys.modules.upload.enums.StyleEnums;
 import com.backendsys.modules.upload.enums.TargetEnums;
 import com.backendsys.modules.upload.service.SysFileService;
+import com.backendsys.modules.upload.utils.ObjectKey.ObjectKeyEntity;
 import com.backendsys.modules.upload.utils.UploadUtil;
 import com.backendsys.utils.response.PageEntity;
 import com.backendsys.utils.response.PageInfoResult;
@@ -324,8 +325,12 @@ public class SysFileServiceImpl extends ServiceImpl<SysFileDao, SysFile> impleme
     }
 
 
-    // target: 上传目标 (-1:本地, 1:腾讯云, 2:阿里云, 3.抖音云)
-    private void deleteObject(String object_key, Integer target) {
+    /**
+     * 删除存储桶对象 (单个)
+     * target: 上传目标 (-1:本地, 1:腾讯云, 2:阿里云, 3.抖音云)
+     */
+    @Override
+    public void deleteObject(String object_key, Integer target) {
         if (target == -1) {
             // [本地] 删除文件
             uploadUtil.delete(object_key);
@@ -341,6 +346,26 @@ public class SysFileServiceImpl extends ServiceImpl<SysFileDao, SysFile> impleme
             System.out.println("Delete douyin tos object: " + object_key);
         }
     }
+    /**
+     * 删除存储桶对象 (批量)
+     */
+    @Override
+    public void deleteObjects(List<ObjectKeyEntity> list) {
+        list.stream().forEach(item -> {
+            if (item.getTarget() == 1) {
+                // [腾讯云] 删除对象
+                List<String> object_keys = list.stream().map(it -> it.getObject_key()).collect(Collectors.toList());
+                tencentCosService.deleteObjects(object_keys);
+                System.out.println("Delete tencent cos object (batch): " + object_keys);
+            }
+            if (item.getTarget() == 3) {
+                // [抖音云] 删除对象
+                List<String> object_keys = list.stream().map(it -> it.getObject_key()).collect(Collectors.toList());
+                douyinTosService.deleteObjects(object_keys);
+                System.out.println("Delete douyin tos object (batch): " + object_keys);
+            }
+        });
+    }
 
     /**
      * 删除文件 (包括缩略图,如果有的话)

+ 11 - 0
src/main/java/com/backendsys/modules/upload/utils/ObjectKey/ObjectKeyEntity.java

@@ -0,0 +1,11 @@
+package com.backendsys.modules.upload.utils.ObjectKey;
+
+import lombok.Data;
+
+@Data
+public class ObjectKeyEntity {
+
+    private String object_key;
+    private Integer target;
+
+}

+ 38 - 1
src/main/java/com/backendsys/modules/upload/utils/ObjectKeyUtil.java → src/main/java/com/backendsys/modules/upload/utils/ObjectKey/ObjectKeyUtil.java

@@ -1,11 +1,13 @@
-package com.backendsys.modules.upload.utils;
+package com.backendsys.modules.upload.utils.ObjectKey;
 
 import com.backendsys.modules.sdk.douyincloud.tos.service.DouyinTosService;
 import com.backendsys.modules.sdk.tencentcloud.cos.service.TencentCosService;
 import com.backendsys.modules.upload.entity.ObjectKey;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
 import org.springframework.stereotype.Component;
 
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.stream.Collectors;
@@ -13,8 +15,13 @@ import java.util.stream.Collectors;
 @Component
 public class ObjectKeyUtil {
 
+    @Value("${tencent.cos.accessible-domain}")
+    private String TENCENT_DOMAIN;
     @Autowired
     private TencentCosService tencentCosService;
+
+    @Value("${douyin.tos.domain}")
+    private String DOUYIN_DOMAIN;
     @Autowired
     private DouyinTosService douyinTosService;
 
@@ -25,6 +32,36 @@ public class ObjectKeyUtil {
         return objectKeys.stream().collect(Collectors.groupingBy(ObjectKey::getTarget));
     }
 
+    /**
+     * 根据 url 获取 object_key
+     */
+    public ObjectKeyEntity urlToObjectKey(String url) {
+        String prefix = null;
+        Integer target = -1;
+        if (url.startsWith(TENCENT_DOMAIN)) {
+            prefix = TENCENT_DOMAIN;
+            target = 1;
+        } else if (url.startsWith(DOUYIN_DOMAIN)) {
+            prefix = DOUYIN_DOMAIN;
+            target = 3;
+        }
+        // 去掉协议头(http://、https://)后,再取前缀
+        int domainStart = url.indexOf(prefix);
+        if (domainStart == -1) {
+            return null;
+        }
+        // 去掉域名前缀,取后面的路径
+        String object_key = url.substring(domainStart + prefix.length());
+        // 去掉可能的前导斜杠
+        if (object_key.startsWith("/")) {
+            object_key = object_key.substring(1);
+        }
+        ObjectKeyEntity result = new ObjectKeyEntity();
+        result.setObject_key(object_key);
+        result.setTarget(target);
+        return result;
+    }
+
     /**
      * 批量删除 ObjectKeys (By target)
      */

+ 12 - 5
src/main/java/com/backendsys/modules/upload/utils/UploadUtil.java

@@ -5,6 +5,7 @@ import cn.hutool.core.convert.Convert;
 import com.backendsys.exception.CustException;
 import com.backendsys.modules.common.utils.CommonUtil;
 import com.backendsys.modules.upload.entity.SysFileResult;
+import com.backendsys.modules.upload.enums.StyleEnums;
 import com.backendsys.utils.response.Result;
 import com.backendsys.utils.response.ResultEnum;
 import net.coobird.thumbnailator.Thumbnails;
@@ -108,11 +109,17 @@ public class UploadUtil {
         }
     }
 
-    // 不同的云环境 (target),缩略图配置也不一样
-    //   -1:本地:
-    //   1:腾讯云: https://cloud.tencent.com/document/product/436/113295
-    //   2:阿里云:
-    //   3:抖音云: https://www.volcengine.com/docs/6349/153626
+    /**
+        获取缩略图 URL
+        不同的云环境 (target),缩略图配置也不一样
+        -1:本地:
+        1:腾讯云: https://cloud.tencent.com/document/product/436/113295
+        2:阿里云:
+        3:抖音云: https://www.volcengine.com/docs/6349/153626
+     */
+    public static String getImageThumbUrl(String url, Integer target, Integer width, Integer height) {
+        return getImageThumbUrl(url, target, width, height, StyleEnums.THUMB_BACKGROUND.getValue());
+    }
     public static String getImageThumbUrl(String url, Integer target, Integer width, Integer height, String backgroundColor) {
         if (target == -1) {
             UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(url);

+ 0 - 5
src/main/java/com/backendsys/service/SDKService/SDKTencent/SDKTencentCOSServiceImpl.java

@@ -61,11 +61,6 @@ public class SDKTencentCOSServiceImpl implements SDKTencentCOSService {
     @Autowired
     private HttpRequestAspect httpRequestAspect;
 
-    @Value("${tencent.cos.secret-id-temp}")
-    private String secretIdTemp;
-    @Value("${tencent.cos.secret-key}-temp")
-    private String secretKeyTemp;
-
     @Value("${tencent.cos.secret-id}")
     private String secretId;
     @Value("${tencent.cos.secret-key}")

+ 11 - 9
src/main/resources/application-dev.yml

@@ -140,16 +140,17 @@ tencent:
     # 上传的最大大小限制 (5MB = 5242880) (1GB = 1073741824)
     max-size: 5242880 # (即将删除)
     # ------------------------------------------------------
-    secret-id: AKID3zlNxRjstjnohWFnDUfeVBj3CJH7mFaK
-    secret-key: IXgzFKB71rXOCxlS4BTdtCYuJbP8h7Xr
+    secret-id: AKIDhMtEWrCNK9yb2NWRBiBTajjW5SddR2aA
+    secret-key: c4mUFqFRSHsELEsHRPBD6rRmsjLjsulO
     # ------------------------------------------------------
-    secret-id-temp: AKIDVuCIAoKiuzyqV1hIo8HEJtKcm8NWXkx4
-    secret-key-temp: 763nQwV0JPQDsLYeeaWL4atMhvynaGjy
-    # ------------------------------------------------------
-    region: ap-hongkong
-    bucket-name: storage-1320301544
-    # accessible-domain: https://storage-1320301544.cos.ap-hongkong.myqcloud.com
-    accessible-domain: http://cos.daogu.ai
+    #    # 香港
+    #    region: ap-hongkong
+    #    bucket-name: storage-1320301544
+    #    accessible-domain: https://cos.daogu.ai
+    # 广州
+    region: ap-guangzhou
+    bucket-name: daoguai-1320301544
+    accessible-domain: https://cos.daoguyujiamcn.com
   ivh:
     empty-app-key: 283ca6dc9d4147debc60bf9fc3fbbe03         # 空数据账号 (我自己的子账号)
     empty-access-token: eea44503a2f64c119fb0acd2006dacb0
@@ -211,6 +212,7 @@ klingai:
 
 comfyui:
   host: 127.0.0.1
+  ports: 8000, 8001, 8002, 8003, 8004, 8005, 8006, 8007
   token: $2b$12$.MR4qGaFetN1FPQzbfyIrehsyjnPJ12xAZhR/l7KZpLkUPQTCG4gy
   is-save: true
   # queue-key: comfyui:queue

+ 12 - 15
src/main/resources/application-local.yml

@@ -144,21 +144,17 @@ tencent:
     # 上传的最大大小限制 (5MB = 5242880) (1GB = 1073741824)
     max-size: 5242880 # (即将删除)
     # ------------------------------------------------------
-    secret-id: AKID3zlNxRjstjnohWFnDUfeVBj3CJH7mFaK
-    secret-key: IXgzFKB71rXOCxlS4BTdtCYuJbP8h7Xr
-    # ------------------------------------------------------
-    secret-id-temp: AKIDVuCIAoKiuzyqV1hIo8HEJtKcm8NWXkx4
-    secret-key-temp: 763nQwV0JPQDsLYeeaWL4atMhvynaGjy
+    secret-id: AKIDhMtEWrCNK9yb2NWRBiBTajjW5SddR2aA
+    secret-key: c4mUFqFRSHsELEsHRPBD6rRmsjLjsulO
     # ------------------------------------------------------
-    # 香港
-    region: ap-hongkong
-    bucket-name: storage-1320301544
-    accessible-domain: http://cos.daogu.ai
-#    # 广州
-#    region: ap-guangzhou
-#    bucket-name: duanju3-1320301544
-#    accessible-domain: https://duanju3-1320301544.cos.ap-hongkong.myqcloud.com
-
+#    # 香港
+#    region: ap-hongkong
+#    bucket-name: storage-1320301544
+#    accessible-domain: https://cos.daogu.ai
+    # 广州
+    region: ap-guangzhou
+    bucket-name: daoguai-1320301544
+    accessible-domain: https://cos.daoguyujiamcn.com
   ivh:
     empty-app-key: 283ca6dc9d4147debc60bf9fc3fbbe03         # 空数据账号 (我自己的子账号)
     empty-access-token: eea44503a2f64c119fb0acd2006dacb0
@@ -226,6 +222,7 @@ klingai:
 
 comfyui:
   host: 43.128.1.201
+  ports: 8000, 8001, 8002, 8003, 8004, 8005, 8006, 8007
   token: $2b$12$.MR4qGaFetN1FPQzbfyIrehsyjnPJ12xAZhR/l7KZpLkUPQTCG4gy
-  is-save: true
+  is-save: false
   # queue-key: comfyui:queue

+ 11 - 9
src/main/resources/application-prod.yml

@@ -141,16 +141,17 @@ tencent:
     # 上传的最大大小限制 (5MB = 5242880) (1GB = 1073741824)
     max-size: 5242880 # (即将删除)
     # ------------------------------------------------------
-    secret-id: AKID3zlNxRjstjnohWFnDUfeVBj3CJH7mFaK
-    secret-key: IXgzFKB71rXOCxlS4BTdtCYuJbP8h7Xr
+    secret-id: AKIDhMtEWrCNK9yb2NWRBiBTajjW5SddR2aA
+    secret-key: c4mUFqFRSHsELEsHRPBD6rRmsjLjsulO
     # ------------------------------------------------------
-    secret-id-temp: AKIDVuCIAoKiuzyqV1hIo8HEJtKcm8NWXkx4
-    secret-key-temp: 763nQwV0JPQDsLYeeaWL4atMhvynaGjy
-    # ------------------------------------------------------
-    region: ap-hongkong
-    bucket-name: storage-1320301544
-    # accessible-domain: https://storage-1320301544.cos.ap-hongkong.myqcloud.com
-    accessible-domain: http://cos.daogu.ai
+    #    # 香港
+    #    region: ap-hongkong
+    #    bucket-name: storage-1320301544
+    #    accessible-domain: https://cos.daogu.ai
+    # 广州
+    region: ap-guangzhou
+    bucket-name: daoguai-1320301544
+    accessible-domain: https://cos.daoguyujiamcn.com
   ivh:
     empty-app-key: 283ca6dc9d4147debc60bf9fc3fbbe03         # 空数据账号 (我自己的子账号)
     empty-access-token: eea44503a2f64c119fb0acd2006dacb0
@@ -212,6 +213,7 @@ klingai:
 
 comfyui:
   host: 127.0.0.1
+  ports: 8000, 8001, 8002, 8003, 8004, 8005, 8006, 8007
   token: $2b$12$.MR4qGaFetN1FPQzbfyIrehsyjnPJ12xAZhR/l7KZpLkUPQTCG4gy
   is-save: true
   # queue-key: comfyui:queue

+ 1 - 0
src/main/resources/application.yml

@@ -128,6 +128,7 @@ whitelist:
     /docs/**,
     /v3/**
   jwt:
+    /api/comfyui,
     /api/webhook,
     /api/public/**,
     /api/v2/public/**,

+ 32 - 0
src/main/resources/mapper/ai/material/MaterialCategoryDao.xml

@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
+<mapper namespace="com.backendsys.modules.material.dao.MaterialCategoryDao">
+
+    <sql id="includeMaterialCategory">
+        id,
+        id category_id,
+        category_name,
+        sort
+    </sql>
+    <!-- COALESCE(content_type, '') content_type, -->
+    <resultMap id="resultMapMaterialCategory" type="com.backendsys.modules.material.entity.MaterialCategory">
+        <id property="id" column="id" jdbcType="BIGINT" />
+        <result property="category_id" column="id" javaType="java.lang.Long" />
+        <result property="category_name" column="category_name" />
+        <result property="sort" column="sort" javaType="java.lang.Integer" />
+    </resultMap>
+
+    <select id="selectMaterialCategoryList" resultMap="resultMapMaterialCategory">
+        SELECT
+            <include refid="includeMaterialCategory" />
+        FROM ai_material_category
+        <where>
+            <if test="category_name != null and category_name != ''">
+                AND category_name like concat('%', #{category_name}, '%')
+            </if>
+        </where>
+        ORDER BY sort DESC
+    </select>
+
+
+</mapper>

+ 148 - 0
src/main/resources/mapper/ai/material/MaterialDao.xml

@@ -0,0 +1,148 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
+<mapper namespace="com.backendsys.modules.material.dao.MaterialDao">
+
+    <sql id="includeMaterial">
+        m.id,
+        m.id material_id,
+        m.user_id,
+        m.category_id,
+        COALESCE(mc.category_name, '') category_name,
+        COALESCE(m.tag_ids, '') tag_ids,
+        m.material_name,
+        COALESCE(m.image_thumb_url, '') image_thumb_url,
+        m.is_copyright,
+        m.create_time,
+        m.update_time
+    </sql>
+
+    <sql id="includeMaterialDetail">
+        m.id,
+        m.id material_id,
+        m.user_id,
+        m.category_id,
+        COALESCE(mc.category_name, '') category_name,
+        COALESCE(m.tag_ids, '') tag_ids,
+        m.material_name,
+        COALESCE(m.image_thumb_url, '') image_thumb_url,
+        COALESCE(m.image_url, '') image_url,
+        COALESCE(m.fla_url, '') fla_url,
+        COALESCE(m.psd_url, '') psd_url,
+        m.is_copyright,
+        m.create_time,
+        m.update_time
+    </sql>
+
+    <resultMap id="resultMapMaterial" type="java.util.LinkedHashMap">
+        <id property="id" column="id" jdbcType="BIGINT" />
+        <result property="material_id" column="id" javaType="java.lang.Long" />
+        <result property="user_id" column="user_id" javaType="java.lang.Long" />
+        <result property="user_nickname" column="user_nickname" />
+        <result property="category_id" column="category_id" javaType="java.lang.Long" />
+        <result property="category_name" column="category_name" />
+        <result property="tag_ids" column="tag_ids" />
+        <result property="material_name" column="material_name" />
+        <result property="image_thumb_url" column="image_thumb_url" />
+        <result property="is_copyright" column="is_copyright" javaType="java.lang.Integer" />
+        <result property="create_time" column="create_time"
+                typeHandler="com.backendsys.config.Mybatis.handler.timezone.LocalDateTimeHandler" />
+        <result property="update_time" column="update_time"
+                typeHandler="com.backendsys.config.Mybatis.handler.timezone.LocalDateTimeHandler" />
+    </resultMap>
+
+    <resultMap id="resultMapMaterialDetail" type="com.backendsys.modules.material.entity.Material">
+        <id property="id" column="id" jdbcType="BIGINT" />
+        <result property="material_id" column="id" javaType="java.lang.Long" />
+        <result property="user_id" column="user_id" javaType="java.lang.Long" />
+        <result property="user_nickname" column="user_nickname" />
+        <result property="category_id" column="category_id" javaType="java.lang.Long" />
+        <result property="category_name" column="category_name" />
+        <result property="tag_ids" column="tag_ids" />
+        <result property="material_name" column="material_name" />
+        <result property="image_thumb_url" column="image_thumb_url" />
+        <result property="image_url" column="image_url" />
+        <result property="fla_url" column="fla_url" />
+        <result property="psd_url" column="psd_url" />
+        <result property="is_copyright" column="is_copyright" javaType="java.lang.Integer" />
+        <result property="create_time" column="create_time"
+                typeHandler="com.backendsys.config.Mybatis.handler.timezone.LocalDateTimeHandler" />
+        <result property="update_time" column="update_time"
+                typeHandler="com.backendsys.config.Mybatis.handler.timezone.LocalDateTimeHandler" />
+    </resultMap>
+
+    <!-- 查 -->
+    <select id="selectMaterialList" resultMap="resultMapMaterial">
+        SELECT
+            <include refid="includeMaterial" />
+        FROM ai_material m
+        LEFT JOIN ai_material_category mc ON m.category_id = mc.id
+        <where>
+            <if test="category_id != null and category_id != ''">
+                AND m.category_id = #{category_id}
+            </if>
+            <if test="tag_id != null and tag_id != ''">
+                AND FIND_IN_SET(#{tag_id}, m.tag_ids) > 0
+            </if>
+            <if test="material_name != null and material_name != ''">
+                AND m.material_name like concat('%', #{material_name}, '%')
+            </if>
+            <if test="is_copyright != null and is_copyright != ''">
+                AND m.is_copyright = #{is_copyright}
+            </if>
+        </where>
+        ORDER BY m.create_time DESC
+    </select>
+
+    <select id="selectMaterialDetail" resultMap="resultMapMaterialDetail">
+        SELECT
+            <include refid="includeMaterialDetail" />, uf.nickname AS user_nickname
+        FROM ai_material m
+        LEFT JOIN ai_material_category mc ON m.category_id = mc.id
+        LEFT JOIN sys_user_info uf ON m.user_id = uf.user_id
+        WHERE m.id = #{material_id}
+    </select>
+
+    <!-- 增 -->
+    <insert id="insertMaterial" parameterType="com.backendsys.modules.material.entity.Material"
+        useGeneratedKeys="true" keyProperty="material_id">
+        INSERT INTO ai_material (
+            user_id, category_id, material_name
+            <if test="tag_ids != null and tag_ids != ''">, tag_ids</if>
+            <if test="image_thumb_url != null and image_thumb_url != ''">, image_thumb_url</if>
+            <if test="image_url != null and image_url != ''">, image_url</if>
+            <if test="fla_url != null and fla_url != ''">, fla_url</if>
+            <if test="psd_url != null and psd_url != ''">, psd_url</if>
+            <if test="is_copyright != null and is_copyright != ''">, is_copyright</if>
+        )
+        VALUES (
+           #{user_id}, #{category_id}, #{material_name}
+            <if test="tag_ids != null and tag_ids != ''">, #{tag_ids}</if>
+            <if test="image_thumb_url != null and image_thumb_url != ''">, #{image_thumb_url}</if>
+            <if test="image_url != null and image_url != ''">, #{image_url}</if>
+            <if test="fla_url != null and fla_url != ''">, #{fla_url}</if>
+            <if test="psd_url != null and psd_url != ''">, #{psd_url}</if>
+            <if test="is_copyright != null and is_copyright != ''">, #{is_copyright}</if>
+       )
+    </insert>
+
+    <!-- 改 -->
+    <update id="updateMaterial" parameterType="com.backendsys.modules.material.entity.Material"
+        useGeneratedKeys="true" keyProperty="material_id">
+        UPDATE ai_material SET
+        user_id = #{user_id}
+        <if test="category_id != null and category_id != ''">, category_id = #{category_id}</if>
+        <if test="material_name != null and material_name != ''">, material_name = #{material_name}</if>
+        <if test="tag_ids != null and tag_ids != ''">, tag_ids = #{tag_ids}</if>
+        <if test="image_thumb_url != null and image_thumb_url != ''">, image_thumb_url = #{image_thumb_url}</if>
+        <if test="image_url != null and image_url != ''">, image_url = #{image_url}</if>
+        <if test="fla_url != null and fla_url != ''">, fla_url = #{fla_url}</if>
+        <if test="psd_url != null and psd_url != ''">, psd_url = #{psd_url}</if>
+        <if test="is_copyright != null and is_copyright != ''">, is_copyright = #{is_copyright}</if>
+        WHERE id = #{material_id}
+    </update>
+
+    <delete id="deleteMaterial">
+        DELETE FROM ai_material WHERE id = #{material_id}
+    </delete>
+
+</mapper>

+ 39 - 0
src/main/resources/mapper/ai/material/MaterialTagDao.xml

@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
+<mapper namespace="com.backendsys.modules.material.dao.MaterialTagDao">
+
+    <sql id="includeMaterialTag">
+        mt.id,
+        mt.id tag_id,
+        mt.category_id,
+        mt.tag_name,
+        mt.sort
+    </sql>
+    <!-- COALESCE(content_type, '') content_type, -->
+    <resultMap id="resultMapMaterialTag" type="com.backendsys.modules.material.entity.MaterialTag">
+        <id property="id" column="id" jdbcType="BIGINT" />
+        <result property="tag_id" column="id" javaType="java.lang.Long" />
+        <result property="category_id" column="category_id" javaType="java.lang.Long" />
+        <result property="tag_name" column="tag_name" />
+        <result property="material_count" column="material_count" javaType="java.lang.Integer" />
+        <result property="sort" column="sort" javaType="java.lang.Integer" />
+    </resultMap>
+
+    <select id="selectMaterialTagList" resultMap="resultMapMaterialTag">
+        SELECT
+            <include refid="includeMaterialTag" />,
+            COUNT(m.id) material_count
+        FROM ai_material_tag mt
+        LEFT JOIN ai_material m ON FIND_IN_SET(mt.id, m.tag_ids) > 0
+        <where>
+            mt.category_id = #{category_id}
+            <if test="tag_name != null and tag_name != ''">
+                AND mt.tag_name like concat('%', #{tag_name}, '%')
+            </if>
+        </where>
+        GROUP BY mt.id
+        ORDER BY mt.sort DESC
+    </select>
+
+
+</mapper>