chenbiao 2 vuotta sitten
vanhempi
sitoutus
81e5c54f1a
30 muutettua tiedostoa jossa 2819 lisäystä ja 0 poistoa
  1. 78 0
      midjourney/pom.xml
  2. 52 0
      midjourney/src/main/java/com/yhlxj/Application.java
  3. 40 0
      midjourney/src/main/java/com/yhlxj/dao/MyMetaObjectHandler.java
  4. 37 0
      midjourney/src/main/java/com/yhlxj/dao/MybatisPlusConfig.java
  5. 11 0
      midjourney/src/main/java/com/yhlxj/dao/mapper/midjourney/MidjourneyAccountMapper.java
  6. 11 0
      midjourney/src/main/java/com/yhlxj/dao/mapper/midjourney/MidjourneyUserConversationMapper.java
  7. 11 0
      midjourney/src/main/java/com/yhlxj/dao/mapper/midjourney/MidjourneyUserMapper.java
  8. 36 0
      midjourney/src/main/java/com/yhlxj/dao/model/BaseEntity.java
  9. 21 0
      midjourney/src/main/java/com/yhlxj/dao/model/dto/BlendDimensions.java
  10. 35 0
      midjourney/src/main/java/com/yhlxj/dao/model/dto/MessageButton.java
  11. 33 0
      midjourney/src/main/java/com/yhlxj/dao/model/dto/SubmitActionDTO.java
  12. 33 0
      midjourney/src/main/java/com/yhlxj/dao/model/dto/SubmitBlendDTO.java
  13. 27 0
      midjourney/src/main/java/com/yhlxj/dao/model/dto/SubmitDescribeDTO.java
  14. 34 0
      midjourney/src/main/java/com/yhlxj/dao/model/dto/SubmitImagineDTO.java
  15. 30 0
      midjourney/src/main/java/com/yhlxj/dao/model/dto/SubmitModalDTO.java
  16. 27 0
      midjourney/src/main/java/com/yhlxj/dao/model/dto/SubmitShortenDTO.java
  17. 55 0
      midjourney/src/main/java/com/yhlxj/dao/model/entity/MidjourneyAccount.java
  18. 57 0
      midjourney/src/main/java/com/yhlxj/dao/model/entity/MidjourneyUser.java
  19. 107 0
      midjourney/src/main/java/com/yhlxj/dao/model/entity/MidjourneyUserConversation.java
  20. 29 0
      midjourney/src/main/java/com/yhlxj/dao/model/response/SubmitResult.java
  21. 772 0
      midjourney/src/main/java/com/yhlxj/redis/RedisService.java
  22. 24 0
      midjourney/src/main/java/com/yhlxj/service/midjourney/MidjourneyAccountService.java
  23. 33 0
      midjourney/src/main/java/com/yhlxj/service/midjourney/MidjourneyService.java
  24. 284 0
      midjourney/src/main/java/com/yhlxj/service/midjourney/impl/MidJourneyAccountServiceImpl.java
  25. 485 0
      midjourney/src/main/java/com/yhlxj/service/midjourney/impl/MidjourneyServiceImpl.java
  26. 363 0
      midjourney/src/main/java/com/yhlxj/web/mirror/MidjourneyController.java
  27. 93 0
      midjourney/src/main/java/com/yhlxj/web/mirror/MirrorController.java
  28. 0 0
      midjourney/src/main/resources/application-dev.yml
  29. 0 0
      midjourney/src/main/resources/application-prd.yml
  30. 1 0
      pom.xml

+ 78 - 0
midjourney/pom.xml

@@ -0,0 +1,78 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>com.netflix</groupId>
+        <artifactId>netflix</artifactId>
+        <version>1.0</version>
+    </parent>
+
+    <groupId>com.sxfoundation</groupId>
+    <artifactId>midjourney</artifactId>
+
+    <dependencies>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-web</artifactId>
+            <!--
+            <exclusions>
+                <exclusion>
+                    <groupId>org.springframework.boot</groupId>
+                    <artifactId>spring-boot-starter-tomcat</artifactId>
+                </exclusion>
+            </exclusions>
+             -->
+        </dependency>
+
+        <dependency>
+            <groupId>com.netflix</groupId>
+            <artifactId>netflix-common</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>mysql</groupId>
+            <artifactId>mysql-connector-java</artifactId>
+            <scope>runtime</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-configuration-processor</artifactId>
+            <optional>true</optional>
+        </dependency>
+
+
+    </dependencies>
+
+    <properties>
+        <maven.compiler.source>11</maven.compiler.source>
+        <maven.compiler.target>11</maven.compiler.target>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+    </properties>
+
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+                <version>${spring.boot.version}</version>
+                <configuration>
+                    <mainClass>${start-class}</mainClass>
+                    <layout>ZIP</layout>
+                </configuration>
+                <executions>
+                    <execution>
+                        <goals>
+                            <goal>repackage</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
+    </build>
+
+</project>

+ 52 - 0
midjourney/src/main/java/com/yhlxj/Application.java

@@ -0,0 +1,52 @@
+package com.yhlxj;
+
+import com.fasterxml.jackson.annotation.JsonAutoDetect;
+import com.fasterxml.jackson.annotation.JsonTypeInfo;
+import com.fasterxml.jackson.annotation.PropertyAccessor;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.context.annotation.Bean;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.core.StringRedisTemplate;
+import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
+import org.springframework.scheduling.annotation.EnableScheduling;
+import org.springframework.validation.beanvalidation.MethodValidationPostProcessor;
+
+@SpringBootApplication
+@EnableScheduling
+public class Application {
+
+	public static void main(String[] args) {
+		SpringApplication.run(Application.class, args);
+	}
+
+
+	@Bean
+	public MethodValidationPostProcessor methodValidationPostProcessor (){
+		return new MethodValidationPostProcessor();
+	}
+
+	@Bean
+	public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory factory) {
+		StringRedisTemplate template = new StringRedisTemplate(factory);
+		//定义key序列化方式
+		//RedisSerializer<String> redisSerializer = new StringRedisSerializer();//Long类型会出现异常信息;需要我们上面的自定义key生成策略,一般没必要
+		//定义value的序列化方式
+		Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
+		ObjectMapper om = new ObjectMapper();
+		om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
+		om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance,
+				ObjectMapper.DefaultTyping.NON_FINAL,
+				JsonTypeInfo.As.WRAPPER_ARRAY);
+		jackson2JsonRedisSerializer.setObjectMapper(om);
+
+		// template.setKeySerializer(redisSerializer);
+		template.setValueSerializer(jackson2JsonRedisSerializer);
+		template.setHashValueSerializer(jackson2JsonRedisSerializer);
+		template.afterPropertiesSet();
+		return template;
+	}
+}

+ 40 - 0
midjourney/src/main/java/com/yhlxj/dao/MyMetaObjectHandler.java

@@ -0,0 +1,40 @@
+package com.yhlxj.dao;
+
+import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
+import org.apache.ibatis.reflection.MetaObject;
+import org.springframework.stereotype.Component;
+
+import java.util.Date;
+
+/**
+ * @author chan
+ * @className MyMetaObjectHandler
+ * @description
+ * @date 2020/7/29 2:44 下午
+ */
+@Component
+public class MyMetaObjectHandler implements MetaObjectHandler {
+    @Override
+    public void insertFill(MetaObject metaObject) {
+        //根据属性名称设置值
+        this.setFieldValByName("createdTime",new Date(),metaObject);
+        this.setFieldValByName("updateTime",new Date(),metaObject);
+        //this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now()); // 起始版本 3.3.0(推荐使用)
+        //this.fillStrategy(metaObject, "createTime", LocalDateTime.now()); // 也可以使用(3.3.0 该方法有bug请升级到之后的版本如`3.3.1.8-SNAPSHOT`)
+        /* 上面选其一使用,下面的已过时(注意 strictInsertFill 有多个方法,详细查看源码) */
+        //this.setFieldValByName("operator", "Jerry", metaObject);
+        //this.setInsertFieldValByName("operator", "Jerry", metaObject);
+
+    }
+
+    @Override
+    public void updateFill(MetaObject metaObject) {
+
+        this.setFieldValByName("updateTime", new Date(), metaObject);
+        //this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now()); // 起始版本 3.3.0(推荐使用)
+        //this.fillStrategy(metaObject, "updateTime", LocalDateTime.now()); // 也可以使用(3.3.0 该方法有bug请升级到之后的版本如`3.3.1.8-SNAPSHOT`)
+        /* 上面选其一使用,下面的已过时(注意 strictUpdateFill 有多个方法,详细查看源码) */
+        //this.setFieldValByName("operator", "Tom", metaObject);
+        //this.setUpdateFieldValByName("operator", "Tom", metaObject);
+    }
+}

+ 37 - 0
midjourney/src/main/java/com/yhlxj/dao/MybatisPlusConfig.java

@@ -0,0 +1,37 @@
+package com.yhlxj.dao;
+
+import com.baomidou.mybatisplus.annotation.DbType;
+import com.baomidou.mybatisplus.autoconfigure.ConfigurationCustomizer;
+import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
+import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
+import org.mybatis.spring.annotation.MapperScan;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.transaction.annotation.EnableTransactionManagement;
+
+/**
+ * @author chan
+ * @className MybatisConfig
+ * @date 2020/7/15 3:05 下午
+ */
+
+@EnableTransactionManagement
+@Configuration
+@MapperScan(basePackages = {"com.yhlxj.dao.mapper"})
+public class MybatisPlusConfig {
+
+    /**
+     * 新的分页插件,一缓和二缓遵循mybatis的规则,需要设置 MybatisConfiguration#useDeprecatedExecutor = false 避免缓存出现问题(该属性会在旧插件移除后一同移除)
+     */
+    @Bean
+    public MybatisPlusInterceptor mybatisPlusInterceptor() {
+        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
+        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.H2));
+        return interceptor;
+    }
+
+    @Bean
+    public ConfigurationCustomizer configurationCustomizer() {
+        return configuration -> configuration.setUseDeprecatedExecutor(false);
+    }
+}

+ 11 - 0
midjourney/src/main/java/com/yhlxj/dao/mapper/midjourney/MidjourneyAccountMapper.java

@@ -0,0 +1,11 @@
+package com.yhlxj.dao.mapper.midjourney;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.yhlxj.dao.model.entity.MidjourneyAccount;
+
+/**
+ * @author zwhui
+ * @date 2024/4/12 15:51
+ */
+public interface MidjourneyAccountMapper extends BaseMapper<MidjourneyAccount> {
+}

+ 11 - 0
midjourney/src/main/java/com/yhlxj/dao/mapper/midjourney/MidjourneyUserConversationMapper.java

@@ -0,0 +1,11 @@
+package com.yhlxj.dao.mapper.midjourney;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.yhlxj.dao.model.entity.MidjourneyUserConversation;
+
+/**
+ * @author zwhui
+ * @date 2024/4/23 16:03
+ */
+public interface MidjourneyUserConversationMapper extends BaseMapper<MidjourneyUserConversation> {
+}

+ 11 - 0
midjourney/src/main/java/com/yhlxj/dao/mapper/midjourney/MidjourneyUserMapper.java

@@ -0,0 +1,11 @@
+package com.yhlxj.dao.mapper.midjourney;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.yhlxj.dao.model.entity.MidjourneyUser;
+
+/**
+ * @author zwhui
+ * @date 2024/4/16 17:46
+ */
+public interface MidjourneyUserMapper extends BaseMapper<MidjourneyUser> {
+}

+ 36 - 0
midjourney/src/main/java/com/yhlxj/dao/model/BaseEntity.java

@@ -0,0 +1,36 @@
+package com.yhlxj.dao.model;
+
+import com.baomidou.mybatisplus.annotation.FieldFill;
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+import java.util.Date;
+
+/**
+ * @author chan
+ * @className BaseEntity
+ * @description
+ * @date 2020/7/29 2:28 下午
+ */
+@Data
+@Accessors(chain = true)
+public class BaseEntity {
+
+    @TableId(value = "id",type =IdType.AUTO)
+    private Long id;
+
+    /**
+     * 创建时间
+     */
+    @TableField(fill = FieldFill.INSERT)
+    private Date createdTime;
+
+    /**
+     * 修改时间
+     */
+    @TableField(fill = FieldFill.INSERT_UPDATE)
+    private Date updateTime;
+}

+ 21 - 0
midjourney/src/main/java/com/yhlxj/dao/model/dto/BlendDimensions.java

@@ -0,0 +1,21 @@
+package com.yhlxj.dao.model.dto;
+
+
+import lombok.Getter;
+
+@Getter
+public enum BlendDimensions {
+
+	PORTRAIT("2:3"),
+
+	SQUARE("1:1"),
+
+	LANDSCAPE("3:2");
+
+	private final String value;
+
+	BlendDimensions(String value) {
+		this.value = value;
+	}
+
+}

+ 35 - 0
midjourney/src/main/java/com/yhlxj/dao/model/dto/MessageButton.java

@@ -0,0 +1,35 @@
+package com.yhlxj.dao.model.dto;
+
+import lombok.Data;
+
+/**
+ * 可执行按钮
+ */
+@Data
+public class MessageButton {
+    /**
+     * 动作标识
+     */
+    private String customId;
+
+    /**
+     * 图标
+     */
+    private String emoji = "";
+
+    /**
+     * 文本
+     */
+    private String label;
+
+    /**
+     * 类型,系统内部使用
+     */
+    private int type = 2;
+
+    /**
+     * 样式: 2(Primary)、3(Green)
+     */
+    private int style = 2;
+
+}

+ 33 - 0
midjourney/src/main/java/com/yhlxj/dao/model/dto/SubmitActionDTO.java

@@ -0,0 +1,33 @@
+package com.yhlxj.dao.model.dto;
+
+import lombok.Data;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+
+/**
+ * @author zwhui
+ * @date 2024/4/24 10:56
+ */
+@Data
+public class SubmitActionDTO {
+    /**
+     * 任务对应按钮的customId
+     */
+    @NotBlank(message = "customId参数不能为空")
+    private String customId;
+
+    /**
+     * 执行Imagine任务生成的ID
+     */
+    @NotNull(message = "任务ID不能为空")
+    private Long taskId;
+
+    /**
+     * 机器人类型
+     * bot类型,mj(默认)或niji,可用值:MID_JOURNEY,NIJI_JOURNEY,示例值(MID_JOURNEY)
+     * MID_JOURNEY
+     * NIJI_JOURNEY
+     */
+    private String botType = "MID_JOURNEY";
+}

+ 33 - 0
midjourney/src/main/java/com/yhlxj/dao/model/dto/SubmitBlendDTO.java

@@ -0,0 +1,33 @@
+package com.yhlxj.dao.model.dto;
+
+import lombok.Data;
+
+import javax.validation.constraints.NotBlank;
+import java.util.List;
+
+/**
+ * @author zwhui
+ * @date 2024/4/23 13:49
+ */
+@Data
+public class SubmitBlendDTO {
+
+	/**
+	 * base64
+	 */
+	@NotBlank(message = "请上传至少一张图片")
+	private List<String> base64Array;
+
+	/**
+	 * 比例: PORTRAIT(2:3); SQUARE(1:1); LANDSCAPE(3:2)
+	 */
+	private BlendDimensions dimensions = BlendDimensions.SQUARE;
+
+	/**
+	 * 机器人类型
+	 * bot类型,mj(默认)或niji,可用值:MID_JOURNEY,NIJI_JOURNEY,示例值(MID_JOURNEY)
+	 * MID_JOURNEY
+	 * NIJI_JOURNEY
+	 */
+	private String botType = "MID_JOURNEY";
+}

+ 27 - 0
midjourney/src/main/java/com/yhlxj/dao/model/dto/SubmitDescribeDTO.java

@@ -0,0 +1,27 @@
+package com.yhlxj.dao.model.dto;
+
+import lombok.Data;
+
+import javax.validation.constraints.NotBlank;
+
+/**
+ * @author zwhui
+ * @date 2024/4/23 13:49
+ */
+@Data
+public class SubmitDescribeDTO {
+
+	/**
+	 * base64
+	 */
+	@NotBlank(message = "请上传图片")
+	private String base64;
+
+	/**
+	 * 机器人类型
+	 * bot类型,mj(默认)或niji,可用值:MID_JOURNEY,NIJI_JOURNEY,示例值(MID_JOURNEY)
+	 * MID_JOURNEY
+	 * NIJI_JOURNEY
+	 */
+	private String botType = "MID_JOURNEY";
+}

+ 34 - 0
midjourney/src/main/java/com/yhlxj/dao/model/dto/SubmitImagineDTO.java

@@ -0,0 +1,34 @@
+package com.yhlxj.dao.model.dto;
+
+import lombok.Data;
+
+import javax.validation.constraints.NotBlank;
+import java.util.List;
+
+/**
+ * @author zwhui
+ * @date 2024/4/23 13:49
+ */
+@Data
+public class SubmitImagineDTO {
+
+    /**
+     * 提示词
+     */
+    @NotBlank(message = "提示词不能为空")
+    private String prompt;
+
+    /**
+     * 垫图base64数组
+     */
+    private List<String> base64Array;
+
+    /**
+     * 机器人类型
+     * bot类型,mj(默认)或niji,可用值:MID_JOURNEY,NIJI_JOURNEY,示例值(MID_JOURNEY)
+     * MID_JOURNEY
+     * NIJI_JOURNEY
+     */
+    private String botType = "MID_JOURNEY";
+
+}

+ 30 - 0
midjourney/src/main/java/com/yhlxj/dao/model/dto/SubmitModalDTO.java

@@ -0,0 +1,30 @@
+package com.yhlxj.dao.model.dto;
+
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+
+/**
+ * @author zwhui
+ * @date 2024/4/23 18:04
+ */
+@Data
+public class SubmitModalDTO {
+
+    /**
+     * 局部重绘base64
+     */
+    private String maskBase64;
+
+    /**
+     * 提示词
+     */
+    private String prompt;
+
+    /**
+     * 任务id
+     */
+    @NotNull(message = "任务ID不能为空")
+    private Long taskId;
+
+}

+ 27 - 0
midjourney/src/main/java/com/yhlxj/dao/model/dto/SubmitShortenDTO.java

@@ -0,0 +1,27 @@
+package com.yhlxj.dao.model.dto;
+
+import lombok.Data;
+
+import javax.validation.constraints.NotBlank;
+
+/**
+ * @author zwhui
+ * @date 2024/4/23 18:07
+ */
+@Data
+public class SubmitShortenDTO {
+
+    /**
+     * 提示词
+     */
+    @NotBlank(message = "提示词不能为空")
+    private String prompt;
+
+    /**
+     * 机器人类型
+     * bot类型,mj(默认)或niji,可用值:MID_JOURNEY,NIJI_JOURNEY,示例值(MID_JOURNEY)
+     * MID_JOURNEY
+     * NIJI_JOURNEY
+     */
+    private String botType = "MID_JOURNEY";
+}

+ 55 - 0
midjourney/src/main/java/com/yhlxj/dao/model/entity/MidjourneyAccount.java

@@ -0,0 +1,55 @@
+package com.yhlxj.dao.model.entity;
+
+import com.ejlchina.searcher.bean.SearchBean;
+import com.yhlxj.dao.model.BaseEntity;
+import lombok.Data;
+
+/**
+ * @author zwhui
+ * @date 2024/4/12 10:52
+ */
+@Data
+@SearchBean(tables = "midjourney_account")
+public class MidjourneyAccount extends BaseEntity {
+
+    /**
+     * 账号id
+     */
+    private Long accountId;
+
+    /**
+     * 状态 true有效 false无效
+     */
+    private Boolean status;
+
+    /**
+     * 服务器id
+     */
+    private Long guildId;
+
+    /**
+     * 频道id
+     */
+    private Long channelId;
+
+    /**
+     * user_token
+     */
+    private String userToken;
+
+    /**
+     * plus服务实例id
+     */
+    private Long instanceId;
+
+    /**
+     * fast剩余时间
+     */
+    private String fastRemainTime;
+
+    /**
+     * 备注
+     */
+    private String remark;
+
+}

+ 57 - 0
midjourney/src/main/java/com/yhlxj/dao/model/entity/MidjourneyUser.java

@@ -0,0 +1,57 @@
+package com.yhlxj.dao.model.entity;
+
+/**
+ * @author zwhui
+ * @date 2024/4/16 17:01
+ */
+
+import com.ejlchina.searcher.bean.SearchBean;
+import com.yhlxj.dao.model.BaseEntity;
+import lombok.Data;
+
+import java.util.Date;
+
+@Data
+@SearchBean(tables = "midjourney_user")
+public class MidjourneyUser extends BaseEntity {
+
+    /**
+     * 名称
+     */
+    private String name;
+
+    /**
+     * 头像
+     */
+    private String img;
+
+    /**
+     * 用户凭证
+     */
+    private String userToken;
+
+    /**
+     * 过期时间
+     */
+    private Date expireTime;
+
+    /**
+     * 车票id
+     */
+    private Long relationId;
+
+    /**
+     * mj fast次数
+     */
+    private Integer mjFastNum;
+
+    /**
+     * mj relax次数
+     */
+    private Integer mjRelaxNum;
+
+    /**
+     * 当前模式 1:fast 2:relax
+     */
+    private Integer mode;
+}

+ 107 - 0
midjourney/src/main/java/com/yhlxj/dao/model/entity/MidjourneyUserConversation.java

@@ -0,0 +1,107 @@
+package com.yhlxj.dao.model.entity;
+
+import com.ejlchina.searcher.bean.SearchBean;
+import com.yhlxj.dao.model.BaseEntity;
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+import java.io.Serializable;
+
+/**
+ * @author zwhui
+ * @date 2024/4/23 11:10
+ */
+@Data
+@Accessors(chain = true)
+@SearchBean(tables = "midjourney_user_conversation")
+public class MidjourneyUserConversation extends BaseEntity implements Serializable {
+
+    private Long userId;
+
+    /**
+     * plus服务任务id
+     */
+    private Long taskId;
+
+    /**
+     * IMAGINE,UPSCALE,VARIATION,ZOOM,PAN,DESCRIBE,BLEND,SHORTEN
+     */
+    private String action;
+
+    /**
+     * 频道id
+     */
+    private Long channelId;
+
+    /**
+     * plus服务实例id
+     */
+    private Long instanceId;
+
+    /**
+     * 模式 1:fast 2:relax
+     */
+    private Integer mode;
+
+    /**
+     *  MID_JOURNEY
+     *  NIJI_JOURNEY
+     */
+    private String BotType;
+
+    /**
+     * 开始时间
+     */
+    private Long startTime;
+
+    /**
+     * 完成时间
+     */
+    private Long finishTime;
+
+    /**
+     * 提示词
+     */
+    private String prompt;
+
+    /**
+     * 描述
+     */
+    private String description;
+
+    /**
+     * 任务状态,可用值:IN_PROGRESS,FAILURE,SUCCESS,CANCEL
+     */
+    private String status;
+
+    /**
+     * 失败原因
+     */
+    private String failReason;
+
+    /**
+     * 进度
+     */
+    private String progress;
+
+    /**
+     * 图片地址
+     */
+    private String imageUrl;
+
+    /**
+     * 按钮
+     */
+    private String buttons;
+
+    /**
+     * 属性
+     */
+    private String properties;
+
+    /**
+     * 书签
+     */
+    private Boolean bookmark;
+
+}

+ 29 - 0
midjourney/src/main/java/com/yhlxj/dao/model/response/SubmitResult.java

@@ -0,0 +1,29 @@
+package com.yhlxj.dao.model.response;
+
+import lombok.Data;
+
+import java.util.HashMap;
+import java.util.Map;
+
+@Data
+public class SubmitResult {
+    /**
+     * 状态码: 1(提交成功), 22(排队中), other(错误)
+     */
+    private int code;
+
+    /**
+     * 描述
+     */
+    private String description;
+
+    /**
+     * 任务ID
+     */
+    private String result;
+
+    /**
+     * 扩展字段
+     */
+    private Map<String, Object> properties = new HashMap<>();
+}

+ 772 - 0
midjourney/src/main/java/com/yhlxj/redis/RedisService.java

@@ -0,0 +1,772 @@
+package com.yhlxj.redis;
+
+import com.cyksj.common.util.StringUtil;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.core.ZSetOperations;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.PostConstruct;
+import javax.annotation.Resource;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+
+/**
+ * @description 定义常用的 Redis操作
+ * @author chan
+ * @date 2021-03-31 5:34 下午
+ */
+@Service
+@SuppressWarnings("all")
+public class RedisService {
+
+    @Resource
+    private RedisTemplate<String, Object> redisTemplate;
+
+    public static String env = "dev";
+
+    @Value("${spring.profiles.active}")
+    private String active;
+
+    @PostConstruct
+    public void init(){
+        env = active + ":";
+    }
+
+
+    /**
+     * 指定缓存失效时间
+     *
+     * @param key  键
+     * @param time 时间(秒)
+     * @return Boolean
+     */
+    public Boolean expire(String key, Long time) {
+        try {
+            if (time > 0) {
+                redisTemplate.expire(key, time, TimeUnit.SECONDS);
+            }
+            return true;
+        } catch (Exception e) {
+            e.printStackTrace();
+            return false;
+        }
+    }
+
+    /**
+     * 根据key获取过期时间
+     *
+     * @param key 键 不能为 null
+     * @return 时间(秒) 返回 0代表为永久有效
+     */
+    public Long getExpire(String key) {
+        return redisTemplate.getExpire(key, TimeUnit.SECONDS);
+    }
+
+    /**
+     * 判断 key是否存在
+     *
+     * @param key 键
+     * @return true 存在 false不存在
+     */
+    public Boolean hasKey(String key) {
+        try {
+            return redisTemplate.hasKey(key);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return false;
+        }
+    }
+
+    /**
+     * 删除缓存
+     *
+     * @param key 可以传一个值 或多个
+     */
+    public void del(String... key) {
+        if (key != null && key.length > 0) {
+            if (key.length == 1) {
+                redisTemplate.delete(key[0]);
+            } else {
+                redisTemplate.delete(Arrays.asList(key));
+            }
+        }
+    }
+
+    /**
+     * 普通缓存获取
+     *
+     * @param key 键
+     * @return 值
+     */
+    public Object get(String key) {
+        return key == null ? null : redisTemplate.opsForValue().get(key);
+    }
+
+    /**
+     * String缓存获取
+     * @param key 键
+     * @return 值
+     */
+    public String getStr(String key) {
+            return key == null ? null : StringUtil.getString(redisTemplate.opsForValue().get(key));
+    }
+
+    /**
+     * 普通缓存放入
+     *
+     * @param key   键
+     * @param value 值
+     * @return true成功 false失败
+     */
+    public Boolean set(String key, Object value) {
+        try {
+            redisTemplate.opsForValue().set(key, value);
+            return true;
+        } catch (Exception e) {
+            e.printStackTrace();
+            return false;
+        }
+    }
+
+    /**
+     * 普通缓存放入并设置时间
+     *
+     * @param key   键
+     * @param value 值
+     * @param time  时间(秒) time要大于0 如果time小于等于0 将设置无限期
+     * @return true成功 false 失败
+     */
+    public Boolean set(String key, Object value, Long time) {
+        try {
+            if (time > 0) {
+                redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
+            } else {
+                set(key, value);
+            }
+            return true;
+        } catch (Exception e) {
+            e.printStackTrace();
+            return false;
+        }
+    }
+
+    /**
+     * 递增
+     *
+     * @param key   键
+     * @param delta 要增加几(大于0)
+     * @return Long
+     */
+    public Long incr(String key, Long delta) {
+        if (delta < 0) {
+            throw new RuntimeException("递增因子必须大于0");
+        }
+        return redisTemplate.opsForValue().increment(key, delta);
+    }
+
+    /**
+     * 递减
+     *
+     * @param key   键
+     * @param delta 要减少几
+     * @return Long
+     */
+    public Long decr(String key, Long delta) {
+        if (delta < 0) {
+            throw new RuntimeException("递减因子必须大于0");
+        }
+        return redisTemplate.opsForValue().increment(key, -delta);
+    }
+
+    /**
+     * HashGet
+     *
+     * @param key  键 不能为 null
+     * @param item 项 不能为 null
+     * @return 值
+     */
+    public Object hget(String key, String item) {
+        return redisTemplate.opsForHash().get(key, item);
+    }
+
+    /**
+     * 获取 hashKey对应的所有键值
+     *
+     * @param key 键
+     * @return 对应的多个键值
+     */
+    public Map<Object, Object> hmget(String key) {
+        return redisTemplate.opsForHash().entries(key);
+    }
+
+    /**
+     * HashSet
+     *
+     * @param key 键
+     * @param map 对应多个键值
+     * @return true 成功 false 失败
+     */
+    public Boolean hmset(String key, Map<Object, Object> map) {
+        try {
+            redisTemplate.opsForHash().putAll(key, map);
+            return true;
+        } catch (Exception e) {
+            e.printStackTrace();
+            return false;
+        }
+    }
+
+    /**
+     * HashSet 并设置时间
+     *
+     * @param key  键
+     * @param map  对应多个键值
+     * @param time 时间(秒)
+     * @return true成功 false失败
+     */
+    public Boolean hmset(String key, Map<String, Object> map, Long time) {
+        try {
+            redisTemplate.opsForHash().putAll(key, map);
+            if (time > 0) {
+                expire(key, time);
+            }
+            return true;
+        } catch (Exception e) {
+            e.printStackTrace();
+            return false;
+        }
+    }
+
+    /**
+     * 向一张hash表中放入数据,如果不存在将创建
+     *
+     * @param key   键
+     * @param item  项
+     * @param value 值
+     * @return true 成功 false失败
+     */
+    public Boolean hset(String key, String item, Object value) {
+        try {
+            redisTemplate.opsForHash().put(key, item, value);
+            return true;
+        } catch (Exception e) {
+            e.printStackTrace();
+            return false;
+        }
+    }
+
+    /**
+     * 向一张hash表中放入数据,如果不存在将创建
+     *
+     * @param key   键
+     * @param item  项
+     * @param value 值
+     * @param time  时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间
+     * @return true 成功 false失败
+     */
+    public Boolean hset(String key, String item, Object value, Long time) {
+        try {
+            redisTemplate.opsForHash().put(key, item, value);
+            if (time > 0) {
+                expire(key, time);
+            }
+            return true;
+        } catch (Exception e) {
+            e.printStackTrace();
+            return false;
+        }
+    }
+
+    /**
+     * 删除hash表中的值
+     *
+     * @param key  键 不能为 null
+     * @param item 项 可以使多个不能为 null
+     */
+    public void hdel(String key, Object... item) {
+        redisTemplate.opsForHash().delete(key, item);
+    }
+
+    /**
+     * 判断hash表中是否有该项的值
+     *
+     * @param key  键 不能为 null
+     * @param item 项 不能为 null
+     * @return true 存在 false不存在
+     */
+    public Boolean hHasKey(String key, String item) {
+        return redisTemplate.opsForHash().hasKey(key, item);
+    }
+
+    /**
+     * hash递增 如果不存在,就会创建一个 并把新增后的值返回
+     *
+     * @param key  键
+     * @param item 项
+     * @param by   要增加几(大于0)
+     * @return Double
+     */
+    public Double hincr(String key, String item, Double by) {
+        return redisTemplate.opsForHash().increment(key, item, by);
+    }
+
+    /**
+     * hash递减
+     *
+     * @param key  键
+     * @param item 项
+     * @param by   要减少记(小于0)
+     * @return Double
+     */
+    public Double hdecr(String key, String item, Double by) {
+        return redisTemplate.opsForHash().increment(key, item, -by);
+    }
+
+    /**
+     * 根据 key获取 Set中的所有值
+     *
+     * @param key 键
+     * @return Set
+     */
+    public Set<Object> sGet(String key) {
+        try {
+            return redisTemplate.opsForSet().members(key);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return null;
+        }
+    }
+
+    /**
+     * 根据value从一个set中查询,是否存在
+     *
+     * @param key   键
+     * @param value 值
+     * @return true 存在 false不存在
+     */
+    public Boolean sHasKey(String key, Object value) {
+        try {
+            return redisTemplate.opsForSet().isMember(key, value);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return false;
+        }
+    }
+
+    /**
+     * 将数据放入set缓存
+     *
+     * @param key    键
+     * @param values 值 可以是多个
+     * @return 成功个数
+     */
+    public Long sSet(String key, Object... values) {
+        try {
+            return redisTemplate.opsForSet().add(key, values);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return 0L;
+        }
+    }
+
+    /**
+     * 将set数据放入缓存
+     *
+     * @param key    键
+     * @param time   时间(秒)
+     * @param values 值 可以是多个
+     * @return 成功个数
+     */
+    public Long sSetAndTime(String key, Long time, Object... values) {
+        try {
+            Long count = redisTemplate.opsForSet().add(key, values);
+            if (time > 0) {
+                expire(key, time);
+            }
+            return count;
+        } catch (Exception e) {
+            e.printStackTrace();
+            return 0L;
+        }
+    }
+
+    /**
+     * 获取set缓存的长度
+     *
+     * @param key 键
+     * @return Long
+     */
+    public Long sGetSetSize(String key) {
+        try {
+            return redisTemplate.opsForSet().size(key);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return 0L;
+        }
+    }
+
+    /**
+     * 移除值为value的
+     *
+     * @param key    键
+     * @param values 值 可以是多个
+     * @return 移除的个数
+     */
+    public Long setRemove(String key, Object... values) {
+        try {
+            return redisTemplate.opsForSet().remove(key, values);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return 0L;
+        }
+    }
+
+    /**
+     * 获取list缓存的内容
+     *
+     * @param key   键
+     * @param start 开始
+     * @param end   结束 0 到 -1代表所有值
+     * @return List
+     */
+    public List<Object> lGet(String key, Long start, Long end) {
+        try {
+            return redisTemplate.opsForList().range(key, start, end);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return null;
+        }
+    }
+
+    /**
+     * 获取list缓存的长度
+     *
+     * @param key 键
+     * @return Long
+     */
+    public Long lGetListSize(String key) {
+        try {
+            return redisTemplate.opsForList().size(key);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return 0L;
+        }
+    }
+
+    /**
+     * 通过索引 获取list中的值
+     *
+     * @param key   键
+     * @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;
+     *              index<0时,-1,表尾,-2倒数第二个元素,依次类推
+     * @return Object
+     */
+    public Object lGetIndex(String key, Long index) {
+        try {
+            return redisTemplate.opsForList().index(key, index);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return null;
+        }
+    }
+
+    /**
+     * 将list放入缓存
+     *
+     * @param key   键
+     * @param value 值
+     * @return Boolean
+     */
+    public Boolean lSet(String key, Object value) {
+        try {
+            redisTemplate.opsForList().rightPush(key, value);
+            return true;
+        } catch (Exception e) {
+            e.printStackTrace();
+            return false;
+        }
+    }
+
+    /**
+     * 将list放入缓存
+     *
+     * @param key   键
+     * @param value 值
+     * @param time  时间(秒)
+     * @return Boolean
+     */
+    public Boolean lSet(String key, Object value, Long time) {
+        try {
+            redisTemplate.opsForList().rightPush(key, value);
+            if (time > 0) {
+                expire(key, time);
+            }
+            return true;
+        } catch (Exception e) {
+            e.printStackTrace();
+            return false;
+        }
+    }
+
+    /**
+     * 将list放入缓存
+     *
+     * @param key   键
+     * @param value 值
+     * @return Boolean
+     */
+    public Boolean lSet(String key, List<Object> value) {
+        try {
+            redisTemplate.opsForList().rightPushAll(key, value);
+            return true;
+        } catch (Exception e) {
+            e.printStackTrace();
+            return false;
+        }
+    }
+
+    /**
+     * 将list放入缓存
+     *
+     * @param key   键
+     * @param value 值
+     * @param time  时间(秒)
+     * @return Boolean
+     */
+    public Boolean lSet(String key, List<Object> value, Long time) {
+        try {
+            redisTemplate.opsForList().rightPushAll(key, value);
+            if (time > 0) {
+                expire(key, time);
+            }
+            return true;
+        } catch (Exception e) {
+            e.printStackTrace();
+            return false;
+        }
+    }
+
+    /**
+     * 根据索引修改list中的某条数据
+     *
+     * @param key   键
+     * @param index 索引
+     * @param value 值
+     * @return Boolean
+     */
+    public Boolean lUpdateIndex(String key, Long index, Object value) {
+        try {
+            redisTemplate.opsForList().set(key, index, value);
+            return true;
+        } catch (Exception e) {
+            e.printStackTrace();
+            return false;
+        }
+    }
+
+    /**
+     * 移除N个值为value
+     *
+     * @param key   键
+     * @param count 移除多少个
+     * @param value 值
+     * @return 移除的个数
+     */
+    public Long lRemove(String key, Long count, Object value) {
+        try {
+            return redisTemplate.opsForList().remove(key, count, value);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return 0L;
+        }
+    }
+
+    public boolean setNx(String key, Object value, Long time) {
+        if (time > 0) {
+            return redisTemplate.opsForValue().setIfAbsent(key, value, time, TimeUnit.SECONDS);
+        }
+        return false;
+    }
+
+
+    /**
+     * 添加元素到ZSet
+     */
+    public Boolean zAdd(String key, double score, Object value) {
+        try {
+            return redisTemplate.opsForZSet().add(key, value, score);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return false;
+        }
+    }
+
+    // 增加ZSet元素的分数
+    public Double zIncrementScore(String key, String value, double delta) {
+        try {
+            return redisTemplate.opsForZSet().incrementScore(key, value, delta);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return null;
+        }
+    }
+
+
+    // 封装获取ZSet分数操作
+    public Double zScore(String key, Object value) {
+        try {
+            return redisTemplate.opsForZSet().score(key, value);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return null;
+        }
+    }
+
+    // 封装ZSet添加或更新分数操作
+    public Boolean zAddOrUpdateScore(String key, Object value, double score) {
+        try {
+            return redisTemplate.opsForZSet().add(key, value, score);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return false;
+        }
+    }
+
+    /**
+     * 获取ZSet的大小
+     */
+    public Long zCard(String key) {
+        try {
+            return redisTemplate.opsForZSet().zCard(key);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return null;
+        }
+    }
+    /**
+     * 获取ZSet的大小
+     */
+    public Long zCount(String key, long start, long end) {
+        try {
+            return redisTemplate.opsForZSet().count(key, start, end);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return null;
+        }
+    }
+
+    // 封装获取ZSet范围内成员的方法
+    public Set<Object> zRange(String key, long start, long end) {
+        try {
+            ZSetOperations<String, Object> zSetOperations = redisTemplate.opsForZSet();
+            return zSetOperations.range(key, start, end);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return null;
+        }
+    }
+    /**
+     * 删除ZSet中分数范围内的元素
+     */
+    public Long zRemoveRangeByScore(String key, double min, double max) {
+        try {
+            return redisTemplate.opsForZSet().removeRangeByScore(key, min, max);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return 0L;
+        }
+    }
+
+    /**
+     * 返回有序集 key 中,所有分数介于 min 和 max 之间(包括等于 min 或 max)的成员。
+     * 有序集成员按分数值递增(从小到大)次序排列。
+     * 此方法还支持限制返回的元素数量。
+     *
+     * @param key 有序集合的key
+     * @param min 分数范围的最小值
+     * @param max 分数范围的最大值
+     * @param offset 开始返回的索引位置
+     * @param count 返回的最大元素数量
+     * @return 介于最小值和最大值之间的成员的集合
+     */
+    public Set<Object> zRangeByScore(String key, double min, double max, long offset, long count) {
+        return redisTemplate.opsForZSet().rangeByScore(key, min, max, offset, count);
+    }
+
+    /**
+     * 从ZSet获取所有成员及其分数
+     * @param key ZSet的键
+     * @return ZSet中所有成员及其分数的集合
+     */
+    public Set<ZSetOperations.TypedTuple<Object>> zRangeWithScores(String key, long start, long end){
+        return redisTemplate.opsForZSet().rangeWithScores(key, start, end);
+    }
+
+    /**
+     * 检查指定值是否存在于有序集合中
+     * @param key
+     * @param value
+     * @return
+     */
+    public boolean checkValueExistsInZSet(String key, String value) {
+        // 检查指定值是否存在于有序集合中
+        return redisTemplate.opsForZSet().score(key, value) != null;
+    }
+
+    /**
+     * 从ZSet中删除指定的成员
+     *
+     * @param key ZSet的键
+     * @param value 要删除的成员值
+     * @return 被成功移除的成员数量
+     */
+    public Long zRemove(String key, Object value) {
+        try {
+            return redisTemplate.opsForZSet().remove(key, value);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return 0L;
+        }
+    }
+
+
+
+
+    public Set<String> keys(String name) {
+        return redisTemplate.keys(name);
+    }
+
+    @AllArgsConstructor
+    @Getter
+    public enum key {
+        MIDJOURNEY_FAST_LIMIT("midjourney:fast:limit:", "midjourney fast次数", 60 * 60 * 48L),
+        MIDJOURNEY_RELAX_LIMIT("midjourney:relax:limit:", "midjourney relax次数", 60 * 60 * 48L),
+        MIDJOURNEY_EXPIRE_TIME("midjourney:expire:time:", "midjourney expire time", 60 * 60 * 48L),
+        MIDJOURNEY_USER("midjourney:user:", "midjourney user", 60 * 60 * 2L),
+        MIDJOURNEY_CONVERSATION("midjourney:conversation:", "midjourney conversation", 60 * 60 * 2L),
+        MIDJOURNEY_ACCOUNT("midjourney:account:", "midjourney account", 60 * 60 * 48L),
+        MIDJOURNEY_QUERY("midjourney:query:", "midjourney query", 60 * 60 * 2L),
+        ;
+
+        private String name;
+
+        private String desc;
+
+        private long timeout;
+
+        public String getEnvName() {
+            return this.name + env;
+        }
+        public String getNameFormat(Object ... str) {
+            return String.format(this.getEnvName(), str);
+        }
+
+    }
+}

+ 24 - 0
midjourney/src/main/java/com/yhlxj/service/midjourney/MidjourneyAccountService.java

@@ -0,0 +1,24 @@
+package com.yhlxj.service.midjourney;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.yhlxj.dao.model.entity.MidjourneyAccount;
+import com.yhlxj.dao.model.entity.MidjourneyUser;
+
+/**
+ * @author zwhui
+ * @date 2024/4/12 15:54
+ */
+public interface MidjourneyAccountService extends IService<MidjourneyAccount> {
+    void addAccount(MidjourneyAccount midjourneyAccount) throws Exception;
+
+    void removeAccount(Long id);
+
+    void updateAccount(MidjourneyAccount midjourneyAccount);
+
+    void updateStatus(Long id);
+
+    void syncAccount(Long id);
+
+    MidjourneyUser getMidjourneyUserToken(Long userId, Long relationId);
+
+}

+ 33 - 0
midjourney/src/main/java/com/yhlxj/service/midjourney/MidjourneyService.java

@@ -0,0 +1,33 @@
+package com.yhlxj.service.midjourney;
+
+
+import com.yhlxj.dao.model.dto.BlendDimensions;
+import com.yhlxj.dao.model.entity.MidjourneyUser;
+import com.yhlxj.dao.model.entity.MidjourneyUserConversation;
+import com.yhlxj.dao.model.response.SubmitResult;
+
+import java.util.List;
+
+/**
+ * @author zwhui
+ * @date 2024/4/23 15:44
+ */
+public interface MidjourneyService {
+    MidjourneyUserConversation submitImagine(MidjourneyUser user, String prompt, String botType, List<String> base64Array) throws Exception;
+
+    MidjourneyUserConversation submitDescribe(MidjourneyUser user, String botType, String base64) throws Exception;
+
+    MidjourneyUserConversation submitBlend(MidjourneyUser user, BlendDimensions dimensions, String botType, List<String> base64Array) throws Exception;
+
+    MidjourneyUserConversation submitModal(MidjourneyUser user, Long taskId, String prompt, String maskBase64) throws Exception;
+
+    MidjourneyUserConversation submitShorten(MidjourneyUser user, String botType, String prompt) throws Exception;
+
+    List<MidjourneyUserConversation> listConversationByIds(Integer mode, List<Long> ids) throws Exception;
+
+    SubmitResult submitAction(MidjourneyUser user, Long taskId, String customId, Long num, String botType) throws Exception;
+
+    MidjourneyUserConversation cancelConversation(MidjourneyUser user, Long id);
+
+    void notifyHook(String conversation) throws Exception;
+}

+ 284 - 0
midjourney/src/main/java/com/yhlxj/service/midjourney/impl/MidJourneyAccountServiceImpl.java

@@ -0,0 +1,284 @@
+package com.yhlxj.service.midjourney.impl;
+
+import cn.hutool.core.lang.UUID;
+import cn.hutool.core.map.MapUtil;
+import cn.hutool.http.HttpRequest;
+import cn.hutool.http.HttpUtil;
+import cn.hutool.json.JSONObject;
+import cn.hutool.json.JSONUtil;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.cyksj.common.exception.BusinessRuntimeException;
+import com.cyksj.common.util.Jsons;
+import com.cyksj.common.util.StringUtil;
+import com.yhlxj.dao.mapper.midjourney.MidjourneyAccountMapper;
+import com.yhlxj.dao.mapper.midjourney.MidjourneyUserMapper;
+import com.yhlxj.dao.model.entity.MidjourneyAccount;
+import com.yhlxj.dao.model.entity.MidjourneyUser;
+import com.yhlxj.redis.RedisService;
+import com.yhlxj.service.midjourney.MidjourneyAccountService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.imageio.stream.FileImageOutputStream;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @author zwhui
+ * @date 2024/4/12 15:54
+ */
+@Service
+@Slf4j
+@RequiredArgsConstructor
+public class MidJourneyAccountServiceImpl extends ServiceImpl<MidjourneyAccountMapper, MidjourneyAccount> implements MidjourneyAccountService {
+
+    private static final String HOST = "http://43.154.230.104:8080";
+
+    private final GroupsRelationMapper groupsRelationMapper;
+
+    private final GroupsMapper groupsMapper;
+
+    private final GoodsDonSkuMapper skuMapper;
+
+    private final UserMapper userMapper;
+
+    private final UserBindRelationService userBindRelationService;
+
+    private final MidjourneyUserMapper midjourneyUserMapper;
+
+    private final RedisService redisService;
+
+    @Override
+    @Transactional(rollbackFor = Throwable.class)
+    public void addAccount(MidjourneyAccount midjourneyAccount) throws Exception {
+        baseMapper.insert(midjourneyAccount);
+        addAcount(midjourneyAccount);
+        //拉取plus服务账号实例id
+        Long instanceId = getAccount(midjourneyAccount.getId());
+        midjourneyAccount.setInstanceId(instanceId);
+        baseMapper.updateById(midjourneyAccount);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Throwable.class)
+    public void removeAccount(Long id) {
+        MidjourneyAccount midjourneyAccount = baseMapper.selectById(id);
+        if (midjourneyAccount == null) {
+            throw BusinessRuntimeException.getInstance("账号不存在");
+        }
+        baseMapper.deleteById(id);
+        //删除plus服务账号信息
+        delAcount(midjourneyAccount.getInstanceId());
+    }
+
+    @Override
+    @Transactional(rollbackFor = Throwable.class)
+    public void updateAccount(MidjourneyAccount midjourneyAccount) {
+        MidjourneyAccount account = baseMapper.selectById(midjourneyAccount.getId());
+        if (account == null) {
+            throw BusinessRuntimeException.getInstance("账号不存在");
+        }
+        baseMapper.updateById(midjourneyAccount);
+        midjourneyAccount.setInstanceId(account.getInstanceId());
+        updAccount(midjourneyAccount);
+    }
+
+    @Override
+    public void updateStatus(Long id) {
+        MidjourneyAccount midjourneyAccount = baseMapper.selectById(id);
+        if (midjourneyAccount == null) {
+            throw BusinessRuntimeException.getInstance("账号不存在");
+        }
+        midjourneyAccount.setStatus(!midjourneyAccount.getStatus());
+        baseMapper.updateById(midjourneyAccount);
+        updAccount(midjourneyAccount);
+    }
+
+    @Override
+    public void syncAccount(Long id) {
+        MidjourneyAccount midjourneyAccount = baseMapper.selectById(id);
+        if (midjourneyAccount == null) {
+            throw BusinessRuntimeException.getInstance("账号不存在");
+        }
+        String fastRemainTime = syncInfo(midjourneyAccount.getInstanceId());
+        midjourneyAccount.setFastRemainTime(fastRemainTime);
+        baseMapper.updateById(midjourneyAccount);
+    }
+
+
+    private void addAcount(MidjourneyAccount midjourneyAccount) throws Exception {
+        Map<String, Object> accountParam = createAccountParam(midjourneyAccount);
+        String body = HttpRequest.post(HOST + "/mj/account/create").body(Jsons.toJson(accountParam)).execute().body();
+        log.info("addAccount body:{}", body);
+        JSONObject jsonObject = JSONUtil.parseObj(body);
+        if (jsonObject.getInt("code") != 1) {
+            throw BusinessRuntimeException.getInstance("addAccount error message:" + jsonObject.getStr("description"));
+        }
+    }
+
+    private Map<String, Object> createAccountParam(MidjourneyAccount midjourneyAccount) {
+        Map<String, Object> param = new HashMap<>();
+        param.put("channelId", midjourneyAccount.getChannelId());
+        param.put("guildId", midjourneyAccount.getGuildId());
+        param.put("userToken", midjourneyAccount.getUserToken());
+        param.put("remark", midjourneyAccount.getId());
+        return param;
+    }
+
+    private void delAcount(Long id) {
+        String body = HttpRequest.delete(HOST + "/mj/account/"+id+"/delete").execute().body();
+        log.info("delAcount body:{}", body);
+        JSONObject jsonObject = JSONUtil.parseObj(body);
+        if (jsonObject.getInt("code") != 1) {
+            throw BusinessRuntimeException.getInstance("delAcount error message:" + jsonObject.getStr("description"));
+        }
+    }
+
+    private Long getAccount(Long id) throws Exception {
+        String body = HttpRequest.post(HOST + "/mj/account/query").body(Jsons.toJson(MapUtil.of("remark", id))).execute().body();
+        log.info("getAccount body:{}", body);
+        JSONObject jsonObject = JSONUtil.parseObj(body);
+        return JSONUtil.parseObj(JSONUtil.parseArray(jsonObject.getStr("content")).get(0)).get("id", Long.class);
+    }
+
+    private void updAccount(MidjourneyAccount midjourneyAccount){
+        Map<String, Object> accountParam = createAccountParam(midjourneyAccount);
+        String body = HttpRequest.put(HOST + "/mj/account/"+midjourneyAccount.getInstanceId()+"/update-reconnect")
+                .body(JSONUtil.toJsonStr(accountParam)).execute().body();
+        log.info("updAccount body:{}", body);
+        JSONObject jsonObject = JSONUtil.parseObj(body);
+        if (jsonObject.getInt("code") != 1) {
+            throw BusinessRuntimeException.getInstance("updAccount error message:" + jsonObject.getStr("description"));
+        }
+    }
+
+    private String syncInfo(Long id){
+        String body = HttpRequest.post(HOST + "/mj/account/"+id+"/sync-info").execute().body();
+        log.info("syncInfo body:{}", body);
+        JSONObject jsonObject = JSONUtil.parseObj(body);
+        if (jsonObject.getInt("code") != 1) {
+            throw BusinessRuntimeException.getInstance("syncInfo error message:" + jsonObject.getStr("description"));
+        }
+        return JSONUtil.parseObj(jsonObject.getStr("result")).getStr("fastTimeRemaining");
+    }
+
+
+    @Override
+    public MidjourneyUser getMidjourneyUserToken(Long userId, Long relationId) {
+        GroupsRelation groupsRelation = getGroupsRelation(userId, relationId);
+        GroupsTrips groupsTrips = getGroupsTrips(groupsRelation);
+        GoodsDonSku goodsDonSku = skuMapper.selectById(groupsTrips.getSkuId());
+
+        if (goodsDonSku != null && goodsDonSku.getIsMirror()) {
+            return getMidjourneyUser(userId, relationId, groupsRelation, goodsDonSku);
+        } else {
+            throw BusinessRuntimeException.getInstance("服务器出了点问题");
+        }
+    }
+
+    /**
+     * 获取车位信息
+     *
+     * @param userId
+     * @param relationId
+     * @return
+     */
+    private GroupsRelation getGroupsRelation(Long userId, Long relationId) {
+        List<Long> userIdList = userBindRelationService.getRelationUserIdList(userId, null);
+        GroupsRelation groupsRelation = groupsRelationMapper.selectOne(Wrappers.lambdaQuery(GroupsRelation.class).in(GroupsRelation::getUserId, userIdList).eq(GroupsRelation::getId, relationId));
+        if (groupsRelation == null) {
+            throw BusinessRuntimeException.getInstance("车票不存在");
+        }
+        return groupsRelation;
+    }
+
+    /**
+     * 获取车队信息
+     *
+     * @param groupsRelation
+     * @return
+     */
+    private GroupsTrips getGroupsTrips(GroupsRelation groupsRelation) {
+        GroupsTrips groupsTrips = groupsMapper.selectById(groupsRelation.getGroupsId());
+        if (groupsTrips == null) {
+            throw BusinessRuntimeException.getInstance("车队异常");
+        }
+        return groupsTrips;
+    }
+
+    private MidjourneyUser getMidjourneyUser(Long userId, Long relationId, GroupsRelation groupsRelation, GoodsDonSku goodsDonSku) {
+        MidjourneyUser midjourneyUser = midjourneyUserMapper.selectOne(Wrappers.lambdaQuery(MidjourneyUser.class).eq(MidjourneyUser::getRelationId, relationId));
+        User user = userMapper.selectById(userId);
+        if (midjourneyUser == null) {
+            midjourneyUser = createMidjourneyUser(groupsRelation, user, goodsDonSku);
+        } else {
+            updateMidjourneyUser(groupsRelation, user, midjourneyUser);
+        }
+        return midjourneyUser;
+    }
+
+    /**
+     * 创建MidjourneyUser
+     */
+    private synchronized MidjourneyUser createMidjourneyUser(GroupsRelation groupsRelation, User user, GoodsDonSku goodsDonSku) {
+        MidjourneyUser midjourneyUser = new MidjourneyUser();
+        midjourneyUser.setExpireTime(groupsRelation.getExpiryTime());
+        midjourneyUser.setName(user.getNickname());
+        midjourneyUser.setImg(getWxImg(user.getHeadimgurl(), user));
+        midjourneyUser.setRelationId(groupsRelation.getId());
+        midjourneyUser.setUserToken(UUID.randomUUID().toString());
+        midjourneyUser.setMjFastNum(goodsDonSku.getMjFastNum());
+        midjourneyUser.setMjRelaxNum((goodsDonSku.getMjRelaxNum() == null || goodsDonSku.getMjRelaxNum() == 0) ? null : goodsDonSku.getMjRelaxNum());
+        midjourneyUserMapper.insert(midjourneyUser);
+        redisService.set(RedisService.key.MIDJOURNEY_FAST_LIMIT.getName() + midjourneyUser.getId(), midjourneyUser.getMjFastNum(),RedisService.key.MIDJOURNEY_FAST_LIMIT.getTimeout());
+        redisService.set(RedisService.key.MIDJOURNEY_USER.getName() + midjourneyUser.getUserToken(), midjourneyUser,RedisService.key.MIDJOURNEY_USER.getTimeout());
+        if (midjourneyUser.getMjRelaxNum() != null) {
+            redisService.set(RedisService.key.MIDJOURNEY_RELAX_LIMIT.getName() + midjourneyUser.getId(), midjourneyUser.getMjRelaxNum(),RedisService.key.MIDJOURNEY_RELAX_LIMIT.getTimeout());
+        }
+        return midjourneyUser;
+    }
+
+    private void updateMidjourneyUser(GroupsRelation groupsRelation, User user, MidjourneyUser midjourneyUser) {
+        midjourneyUser.setName(user.getNickname());
+        midjourneyUser.setImg(getWxImg(user.getHeadimgurl(), user));
+        midjourneyUser.setExpireTime(groupsRelation.getExpiryTime());
+        midjourneyUserMapper.updateById(midjourneyUser);
+        redisService.set(RedisService.key.MIDJOURNEY_USER.getName() + midjourneyUser.getUserToken(), midjourneyUser,RedisService.key.MIDJOURNEY_USER.getTimeout());
+    }
+
+    /**
+     * 更换wx头像至oss
+     */
+    private String getWxImg(String headimgurl, User user) {
+        if (headimgurl.contains("thirdwx.qlogo.cn")) {
+            try {
+                return uploadPic(headimgurl, "wxheadimg-" + user.getId());
+            } catch (Exception ex) {
+                log.error("上传微信头像错误!msg:{}", StringUtil.getErrorText(ex));
+                return "./avatars.png";
+            }
+        } else {
+            return user.getHeadimgurl();
+        }
+    }
+
+    private static String uploadPic(String url, String prefix) throws IOException {
+        byte[] body = HttpUtil.downloadBytes(url);
+        Path tempFile = Files.createTempFile(prefix, ".jpeg");
+        try (FileImageOutputStream imageOutput = new FileImageOutputStream(tempFile.toFile())) {
+            imageOutput.write(body, 0, body.length);
+        }
+        Map<String, Object> paramMap = new HashMap<>();
+        paramMap.put("file", tempFile.toFile());
+        JSONObject result = JSONUtil.parseObj(HttpUtil.post("https://files.liuliangbang.vip/pic/ups", paramMap));
+        return result.getJSONObject("value").getJSONArray("saved").getJSONObject(0).getJSONObject("info").getStr("cdnUrl");
+    }
+
+}

+ 485 - 0
midjourney/src/main/java/com/yhlxj/service/midjourney/impl/MidjourneyServiceImpl.java

@@ -0,0 +1,485 @@
+package com.yhlxj.service.midjourney.impl;
+
+import cn.hutool.core.collection.CollectionUtil;
+import cn.hutool.core.map.MapUtil;
+import cn.hutool.http.HttpRequest;
+import cn.hutool.http.HttpUtil;
+import cn.hutool.json.JSONArray;
+import cn.hutool.json.JSONObject;
+import cn.hutool.json.JSONUtil;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.cyksj.common.exception.BusinessRuntimeException;
+import com.cyksj.common.task.GlobalThreadPoolTaskExecutor;
+import com.cyksj.common.util.Jsons;
+import com.cyksj.common.util.StringUtil;
+import com.yhlxj.dao.mapper.midjourney.MidjourneyUserConversationMapper;
+import com.yhlxj.dao.mapper.midjourney.MidjourneyUserMapper;
+import com.yhlxj.dao.model.dto.BlendDimensions;
+import com.yhlxj.dao.model.dto.MessageButton;
+import com.yhlxj.dao.model.entity.MidjourneyAccount;
+import com.yhlxj.dao.model.entity.MidjourneyUser;
+import com.yhlxj.dao.model.entity.MidjourneyUserConversation;
+import com.yhlxj.dao.model.response.SubmitResult;
+import com.yhlxj.redis.RedisService;
+import com.yhlxj.service.midjourney.MidjourneyAccountService;
+import com.yhlxj.service.midjourney.MidjourneyService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+
+import javax.imageio.stream.FileImageOutputStream;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * @author zwhui
+ * @date 2024/4/23 15:44
+ */
+@Service
+@Slf4j
+@RequiredArgsConstructor
+public class MidjourneyServiceImpl implements MidjourneyService {
+    private static final String RELAX_HOST = "http://43.154.230.104:8080";
+    private static final String FAST_HOST = "https://aigc.api4midjourney.com/api";
+    private static final String FAST_TOKEN = "14a6b469-7cb2-4b99-9238-676c7c5cffef";
+
+    private final MidjourneyUserConversationMapper conversationMapper;
+
+    private static final List<String> progress = List.of("100%");
+
+    private static final List<String> status = List.of("MODAL","CANCEL","FAILURE");
+
+    private final RedisService redisService;
+
+    private final MidjourneyUserMapper midjourneyUserMapper;
+
+    @Value("${midjourney.url}")
+    private String midjourneyHost;
+
+    private final EnvCommonService envCommonService;
+
+    private static final GlobalThreadPoolTaskExecutor TASK_EXECUTOR = GlobalThreadPoolTaskExecutor.getInstance();
+
+    private final MidjourneyAccountService midjourneyAccountService;
+
+
+    @Override
+    public MidjourneyUserConversation submitImagine(MidjourneyUser user, String prompt, String botType, List<String> base64Array) throws Exception {
+        Map<String, Object> imagineParam = MapUtil.builder(new HashMap<String,Object>())
+                .put("prompt", prompt)
+                .put("state", user.getId())
+                .build();
+        if (CollectionUtil.isNotEmpty(base64Array)) {
+            imagineParam.put("base64Array", base64Array);
+        }
+        SubmitResult result = submit(user.getMode(),"imagine", null,imagineParam);
+        return saveConversation(user.getId(), user.getMode(),result,"IMAGINE", StringUtils.EMPTY, botType);
+    }
+
+    public MidjourneyUserConversation saveConversation(Long userId,Integer mode,SubmitResult result, String action,String prompt, String botType) throws Exception {
+        Long taskId = Long.parseLong(result.getResult());
+        Map<String, Object> properties = result.getProperties();
+        MidjourneyUserConversation conversation = new MidjourneyUserConversation()
+                .setUserId(userId).setTaskId(taskId).setAction(action).setPrompt(prompt)
+                .setMode(mode).setStartTime(System.currentTimeMillis()).setProgress("0%")
+                .setStatus(result.getCode() == 21 ? "NOT_START" : "IN_PROGRESS").setTaskId(taskId);
+        if (MapUtil.isNotEmpty(properties)) {
+            conversation.setProperties(Jsons.toJson(properties));
+            conversation.setChannelId(properties.get("discordChannelId") == null ? null : Long.parseLong(properties.get("discordChannelId").toString()))
+                    .setInstanceId(properties.get("discordInstanceId") == null ? null : Long.parseLong(properties.get("discordInstanceId").toString()));
+        }
+        conversationMapper.insert(conversation);
+        return conversation;
+    }
+
+
+    @Override
+    public MidjourneyUserConversation submitDescribe(MidjourneyUser user, String botType, String base64) throws Exception {
+        Map<String, Object> param = MapUtil.builder(new HashMap<String,Object>())
+                .put("state", user.getId())
+                .put("base64", base64)
+                .build();
+        SubmitResult result = submit(user.getMode(),"describe", null, param);
+        return saveConversation(user.getId(), user.getMode(),result,"DESCRIBE", StringUtils.EMPTY, botType);
+    }
+
+    @Override
+    public MidjourneyUserConversation submitBlend(MidjourneyUser user, BlendDimensions dimensions, String botType, List<String> base64Array) throws Exception {
+        Map<String, Object> param = MapUtil.builder(new HashMap<String,Object>())
+                .put("base64Array", base64Array)
+                .put("state", user.getId())
+                .build();
+        if (dimensions != null) {
+            param.put("dimensions", dimensions);
+        }
+        SubmitResult result = submit(user.getMode(),"blend", null, param);
+        return saveConversation(user.getId(), user.getMode(),result,"BLEND", StringUtils.EMPTY, botType);
+    }
+
+    @Override
+    public MidjourneyUserConversation submitModal(MidjourneyUser user, Long taskId, String prompt, String maskBase64) throws Exception {
+        Map<String, Object> param = MapUtil.builder(new HashMap<String,Object>())
+                .put("taskId", taskId)
+                .put("state", user.getId())
+                .build();
+        if (StringUtils.isNotBlank(prompt)) {
+            param.put("prompt", prompt);
+        }
+        if (StringUtils.isNotBlank(maskBase64)) {
+            param.put("maskBase64", maskBase64);
+        }
+        SubmitResult result = submit(user.getMode(),"modal", null, param);
+        return saveConversation(user.getId(),  user.getMode(),result,"MODAL", StringUtils.EMPTY, StringUtils.EMPTY);
+    }
+
+    @Override
+    public MidjourneyUserConversation submitShorten(MidjourneyUser user, String prompt, String botType) throws Exception {
+        Map<String, Object> param = MapUtil.builder(new HashMap<String,Object>())
+                .put("prompt", prompt)
+                .put("state", user.getId())
+                .build();
+        SubmitResult result = submit(user.getMode(),"shorten", null, param);
+        return saveConversation(user.getId(), user.getMode(), result,"SHORTEN", StringUtils.EMPTY, botType);
+    }
+
+    /**
+     * 恢复次数
+     */
+    public Long recoverUserLimit(Long id,Integer mode,Long num){
+        if (mode == 1){
+            num =  redisService.incr(RedisService.key.MIDJOURNEY_FAST_LIMIT.getName() + id, 1L);
+        }
+        if (mode == 2){
+            if (num != null) {
+                num = redisService.incr(RedisService.key.MIDJOURNEY_RELAX_LIMIT.getName() + id, 1L);
+            }
+        }
+        return num;
+    }
+    /**
+     * 同步数据库
+     */
+    public void syncUser(Long id,Integer mode,Long num){
+        log.info("同步次数 id:{},mode:{},num:{}",id,mode,num);
+        if (num == null){
+            return;
+        }
+        LambdaUpdateWrapper<MidjourneyUser> wrapper = Wrappers.lambdaUpdate(MidjourneyUser.class)
+                .eq(MidjourneyUser::getId, id);
+        if (mode == 1){
+            wrapper.set(MidjourneyUser::getMjFastNum, num);
+        }
+        if (mode == 2){
+            wrapper.set(MidjourneyUser::getMjRelaxNum, num);
+        }
+        midjourneyUserMapper.update(null, wrapper);
+    }
+
+    @Override
+    public SubmitResult submitAction(MidjourneyUser user, Long taskId, String customId, Long num, String botType) throws Exception {
+        MidjourneyUserConversation conversation = conversationMapper.selectOne(Wrappers.lambdaQuery(MidjourneyUserConversation.class).eq(MidjourneyUserConversation::getTaskId, taskId).last("limit 1"));
+        if (conversation == null) {
+            throw BusinessRuntimeException.getInstance("关联任务不存在或已失效");
+        }
+        if (!user.getMode().equals(conversation.getMode())) {
+            throw BusinessRuntimeException.getInstance("当前出图模式与关联任务出图模式不符");
+        }
+        if (StringUtils.isNotBlank(conversation.getButtons())){
+            List<MessageButton> messageButtons = Jsons.parseList(conversation.getButtons(), MessageButton.class);
+            messageButtons.forEach(button -> {
+                if (button.getCustomId().equals(customId)) {
+                    log.info("action customId:{}", customId);
+                    button.setStyle(3);
+                }
+            });
+            conversation.setButtons(Jsons.toJson(messageButtons));
+        }
+        if (customId.contains("BOOKMARK")) {
+            conversation.setBookmark(true);
+            conversationMapper.updateById(conversation);
+            return null;
+        }
+        Map<String, Object> param = MapUtil.builder(new HashMap<String,Object>())
+                .put("taskId", taskId)
+                .put("state", user.getId())
+                .put("customId", customId)
+                .build();
+        SubmitResult result = submit(user.getMode(),"action", conversation.getInstanceId(),param);
+        if (result.getCode() == 21) {
+            // 以上操作有弹窗确认,恢复次数
+            recoverUserLimit(user.getId(), user.getMode(),num);
+        }else {
+            syncUser(user.getId(), user.getMode(),num);
+        }
+        saveConversation(user.getId(), user.getMode(), result,"ACTION", StringUtils.EMPTY, botType);
+        return result;
+    }
+
+
+    public SubmitResult submit(Integer mode,String action,Long instanceId, Map<String, Object> param) throws Exception {
+        String url = "";
+        String accountWithMinUsage = "";
+        if (mode == 1) {
+            param.put("mode", "FAST");
+            url = FAST_HOST;
+        } else if (mode == 2) {
+            url = RELAX_HOST;
+            //慢速查询在使用次数最少的账号
+            accountWithMinUsage = getAccountWithMinUsage(instanceId);
+            param.put("accountFilter",MapUtil.builder(new HashMap<String,Object>())
+                    .put("instanceId",accountWithMinUsage).build());
+        }
+        url = url + getActionUrl(action);
+        param.put("notifyHook",midjourneyHost +(EnvCommonService.active.equals(envCommonService.getEnv()) ? "/8081":"/8082") + "/api/applets/midjourney/notifyHook");
+        String body = HttpRequest.post(url).body(Jsons.toJson(param)).header("Authorization", FAST_TOKEN).execute().body();
+        log.info("action body:{}", body);
+        SubmitResult submitResult = Jsons.parseObject(body, SubmitResult.class);
+        int code = submitResult.getCode();
+        if (code != 1 && code != 21 && code != 22) {
+            if (code == 3) {
+                if(mode == 2 && StringUtils.isNotBlank(accountWithMinUsage)){
+                    redisService.hdel(RedisService.key.MIDJOURNEY_ACCOUNT.getName(),accountWithMinUsage);
+                }
+                throw BusinessRuntimeException.getInstance("账号不存在");
+            }
+            if (code == 4) {
+                throw BusinessRuntimeException.getInstance(submitResult.getDescription());
+            }
+            if (code == 24) {
+                throw BusinessRuntimeException.getInstance("prompt包含敏感词");
+            }
+            log.error("action:" + action + " error message:" + submitResult.getDescription());
+            throw BusinessRuntimeException.getInstance("队列已满,请稍后尝试");
+        }
+        return submitResult;
+    }
+
+    public String getAccountWithMinUsage(Long instanceId) {
+        String key = RedisService.key.MIDJOURNEY_ACCOUNT.getName();
+        try {
+            // 获取所有账号ID和使用次数
+            Map<Object, Object> accountsUsage = redisService.hmget(key);
+
+            if (accountsUsage == null || accountsUsage.isEmpty()) {
+                Map<Object, Object> map = midjourneyAccountService.list(Wrappers.lambdaQuery(MidjourneyAccount.class).eq(MidjourneyAccount::getStatus,Boolean.TRUE)).stream().collect(Collectors.toMap(k -> k.getInstanceId().toString(), v -> 0));
+                // 保存所有账号ID和使用次数
+                redisService.hmset(key, map);
+            }
+
+            // 找到使用次数最少的账号ID
+            String minAccountId = null;
+            if (instanceId != null) {
+                minAccountId = instanceId.toString();
+            }else {
+                int minUsage = Integer.MAX_VALUE;
+                for (Map.Entry<Object, Object> entry : accountsUsage.entrySet()) {
+                    int usage = Integer.parseInt(entry.getValue().toString());
+                    if (usage < minUsage) {
+                        minUsage = usage;
+                        minAccountId = entry.getKey().toString();
+                    }
+                }
+            }
+            // 增加使用次数
+            if (minAccountId != null) {
+                redisService.hincr(key, minAccountId, 1.0);
+            }
+            return minAccountId;
+        } catch (Exception e) {
+            throw BusinessRuntimeException.getInstance("获取账号失败");
+        }
+    }
+    private String getActionUrl(String action) {
+         switch (action) {
+            case "imagine":
+                return "/mj/submit/imagine";
+            case "action":
+                return "/mj/submit/action";
+            case "describe":
+                return "/mj/submit/describe";
+            case "blend":
+                return"/mj/submit/blend";
+            case "modal":
+                return "/mj/submit/modal";
+            case "shorten":
+                return "/mj/submit/shorten";
+            default:  throw new IllegalArgumentException("Unknown action: " + action);
+        }
+    }
+    @Override
+    public List<MidjourneyUserConversation> listConversationByIds(Integer mode, List<Long> ids) {
+        List<MidjourneyUserConversation> list = new ArrayList<>();
+        ids.forEach(id ->{
+            String queryKey = RedisService.key.MIDJOURNEY_QUERY.getName();
+            Long count = redisService.incr(queryKey + id, 1L);
+            if (count%5 == 0) {
+                try {
+                    listByIds(mode,List.of(id)).forEach(json ->{
+                        JSONObject jsons = JSONUtil.parseObj(json);
+                        MidjourneyUserConversation conversation = JSONUtil.toBean(jsons, MidjourneyUserConversation.class);
+                        if (StringUtil.isNotBlank(conversation.getImageUrl())) {
+                            conversation.setImageUrl(conversation.getImageUrl().replace("cdn.discordapp.com", "mj.galaxydvd.com"));
+                        }
+                        conversation.setUserId(jsons.getLong("state"));
+                        conversation.setTaskId(jsons.getLong("id"));
+                        list.add(conversation);
+                        TASK_EXECUTOR.execute(() -> {
+                            try {
+                                sync(conversation);
+                            } catch (IOException e) {
+                                throw new RuntimeException(e);
+                            }
+                        });
+                    });
+                } catch (Exception e) {
+                    throw new RuntimeException(e);
+                }
+            }else {
+                list.add((MidjourneyUserConversation) redisService.get(RedisService.key.MIDJOURNEY_CONVERSATION.getName() + id));
+            }
+        });
+        return list;
+    }
+
+
+    public void sync(MidjourneyUserConversation conversation) throws IOException {
+        if ((StringUtils.isNotBlank(conversation.getProgress()) && progress.contains(conversation.getProgress())) || status.contains(conversation.getStatus())) {
+            log.info("同步任务:{},进度:{}",conversation.getTaskId(),conversation.getProgress());
+            Long userId = conversation.getUserId();
+            MidjourneyUserConversation dbConversation = conversationMapper.selectOne(Wrappers.lambdaQuery(MidjourneyUserConversation.class).eq(MidjourneyUserConversation::getTaskId, conversation.getTaskId())
+                    .eq(MidjourneyUserConversation::getUserId, userId).orderByDesc(MidjourneyUserConversation::getId).last("limit 1"));
+            if (dbConversation != null) {
+                if ("SUCCESS".equals(dbConversation.getStatus())) {
+                    return;
+                }
+                conversation.setId(dbConversation.getId());
+                conversationMapper.updateById(conversation);
+                if (dbConversation.getMode() == 2){
+                    redisService.hdecr(RedisService.key.MIDJOURNEY_ACCOUNT.getName() , dbConversation.getInstanceId().toString(), 1.0);
+                }
+                TASK_EXECUTOR.execute(() -> {
+                    try {
+                        String imageUrl = conversation.getImageUrl();
+                        if (StringUtil.isNotBlank(imageUrl)) {
+                            conversation.setImageUrl(uploadPic(imageUrl, "conversation"+dbConversation.getId()));
+                            conversationMapper.updateById(conversation);
+                        }
+                    } catch (IOException e) {
+                        log.error("上传图片失败",e);
+                    }
+                });
+                //失败返还次数
+                if ("FAILURE".equals(conversation.getStatus())){
+                    Object num = redisService.get(RedisService.key.MIDJOURNEY_RELAX_LIMIT.getName() + userId);
+                    LambdaUpdateWrapper<MidjourneyUser> wrapper = Wrappers.lambdaUpdate(MidjourneyUser.class)
+                            .eq(MidjourneyUser::getId, userId);
+                    Integer mode = dbConversation.getMode();
+                    if (mode == 1){
+                        num =  redisService.incr(RedisService.key.MIDJOURNEY_FAST_LIMIT.getName() + userId, 1L);
+                        wrapper.set(MidjourneyUser::getMjFastNum, num);
+                    }
+                    if (mode == 2){
+                        if (num != null) {
+                            num = redisService.incr(RedisService.key.MIDJOURNEY_RELAX_LIMIT.getName() + userId, 1L);
+                            wrapper.set(MidjourneyUser::getMjRelaxNum, num);
+                        }
+                    }
+                    midjourneyUserMapper.update(null, wrapper);
+                }
+            }
+        }
+    }
+
+
+    private static String uploadPic(String url, String prefix) throws IOException {
+        byte[] body = HttpUtil.downloadBytes(url);
+        Path tempFile = Files.createTempFile(prefix, ".png");
+        try (FileImageOutputStream imageOutput = new FileImageOutputStream(tempFile.toFile())) {
+            imageOutput.write(body, 0, body.length);
+        }
+        Map<String, Object> paramMap = new HashMap<>();
+        paramMap.put("file", tempFile.toFile());
+        String json = HttpUtil.post("https://files.liuliangbang.vip/pic/ups", paramMap);
+        log.info("上传图片结果:url:{},json:{}",url, json);
+        return new JSONObject(json).getJSONObject("value").getJSONArray("saved").getJSONObject(0).getJSONObject("info").getStr("cdnUrl");
+    }
+
+    private static List<String> uploadBase64Pic(List<String> base64Array) throws IOException {
+        List<String> list = new ArrayList<>();
+        for (String base64 : base64Array) {
+            String fileExt = "jpeg";
+            if (base64.contains(";")){
+                fileExt = base64.split(";")[0].split("/")[1];
+                base64 = base64.split(",")[1];
+            }
+            byte[] body = Base64.getDecoder().decode(base64);
+            Path tempFile = Files.createTempFile(UUID.randomUUID().toString(), "." + fileExt);
+            try (FileImageOutputStream imageOutput = new FileImageOutputStream(tempFile.toFile())) {
+                imageOutput.write(body, 0, body.length);
+            }
+            Map<String, Object> paramMap = new HashMap<>();
+            paramMap.put("file", tempFile.toFile());
+            String json = HttpUtil.post("https://files.liuliangbang.vip/pic/ups", paramMap);
+            list.add(new JSONObject(json).getJSONObject("value").getJSONArray("saved").getJSONObject(0).getJSONObject("info").getStr("cdnUrl"));
+        }
+        return list;
+    }
+    public JSONArray listByIds(Integer mode,List<Long> ids) throws Exception {
+        Map<String, Object> param = MapUtil.builder(new HashMap<String, Object>()).put("ids", ids).build();
+        String body = HttpRequest.post((mode == 1 ? FAST_HOST : RELAX_HOST) + "/mj/task/list-by-condition").header("Authorization", FAST_TOKEN).body(Jsons.toJson(param)).execute().body();
+        log.info("listByIds body:{}", body);
+        return JSONUtil.parseArray(body);
+    }
+
+    @Override
+    public MidjourneyUserConversation cancelConversation(MidjourneyUser user, Long id) {
+        MidjourneyUserConversation conversation = conversationMapper.selectOne(Wrappers.lambdaQuery(MidjourneyUserConversation.class).eq(MidjourneyUserConversation::getId, id));
+        if (conversation == null) {
+            throw BusinessRuntimeException.getInstance("会话不存在");
+        }
+        if (!Objects.equals(conversation.getUserId(), user.getId())) {
+            throw BusinessRuntimeException.getInstance("不是您的会话");
+        }
+        conversation.setStatus("CANCEL");
+        conversationMapper.updateById(conversation);
+        TASK_EXECUTOR.execute(() -> {
+            cancel(user.getMode(),conversation.getTaskId());
+        });
+        return conversation;
+    }
+
+    public void cancel(Integer mode,Long taskId) {
+        String body = HttpUtil.post(mode == 1 ? FAST_HOST : RELAX_HOST + "/mj/task/"+taskId+"/cancel",MapUtil.empty());
+        log.info("cancel body:{}", body);
+        JSONObject jsonObject = JSONUtil.parseObj(body);
+        if (jsonObject.getInt("code") != 1) {
+            throw BusinessRuntimeException.getInstance("cancel error message:" + jsonObject.getStr("description"));
+        }
+    }
+
+    @Override
+    public void notifyHook(String json) {
+        log.info("notifyHook:{}", json);
+        JSONObject jsons = JSONUtil.parseObj(json);
+        MidjourneyUserConversation conversation = JSONUtil.toBean(jsons, MidjourneyUserConversation.class);
+        conversation.setUserId(jsons.getLong("state"));
+        conversation.setTaskId(jsons.getLong("id"));
+        if (StringUtil.isNotBlank(conversation.getImageUrl())) {
+            conversation.setImageUrl(conversation.getImageUrl().replace("cdn.discordapp.com", "mj.galaxydvd.com"));
+        }
+        redisService.set(RedisService.key.MIDJOURNEY_CONVERSATION.getName() + conversation.getTaskId(), conversation,RedisService.key.MIDJOURNEY_CONVERSATION.getTimeout());
+        TASK_EXECUTOR.execute(() -> {
+            try {
+                sync(conversation);
+            } catch (IOException e) {
+                throw new RuntimeException(e);
+            }
+        });
+    }
+}

+ 363 - 0
midjourney/src/main/java/com/yhlxj/web/mirror/MidjourneyController.java

@@ -0,0 +1,363 @@
+package com.yhlxj.web.mirror;
+
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.cyksj.common.annotation.NoSubmit;
+import com.cyksj.common.exception.BusinessRuntimeException;
+import com.cyksj.common.util.IoKit;
+import com.cyksj.dto.Result;
+import com.cyksj.enums.GatewayResponse;
+import com.ejlchina.searcher.BeanSearcher;
+import com.ejlchina.searcher.SearchResult;
+import com.ejlchina.searcher.util.MapBuilder;
+import com.ejlchina.searcher.util.MapUtils;
+import com.yhlxj.dao.mapper.midjourney.MidjourneyUserMapper;
+import com.yhlxj.dao.model.dto.*;
+import com.yhlxj.dao.model.entity.MidjourneyUser;
+import com.yhlxj.dao.model.entity.MidjourneyUserConversation;
+import com.yhlxj.dao.model.response.SubmitResult;
+import com.yhlxj.redis.RedisService;
+import com.yhlxj.service.midjourney.MidjourneyService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.*;
+
+import javax.servlet.http.HttpServletRequest;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * @author zwhui
+ * @date 2024/4/23 10:31
+ */
+@Slf4j
+@RequiredArgsConstructor
+@RestController
+@RequestMapping("/applets/midjourney")
+public class MidjourneyController {
+
+    private final MidjourneyUserMapper midjourneyUserMapper;
+
+    private final HttpServletRequest request;
+
+    private final MidjourneyService midjourneyService;
+
+    private final BeanSearcher beanSearcher;
+    
+    private final RedisService redisService;
+
+    public MidjourneyUser getUser(){
+        String userToken = request.getHeader("user-token");
+        MidjourneyUser midjourneyUser = (MidjourneyUser) redisService.get(RedisService.key.MIDJOURNEY_USER.getName() + userToken);
+        if (midjourneyUser == null){
+            midjourneyUser = midjourneyUserMapper.selectOne(Wrappers.lambdaQuery(MidjourneyUser.class)
+                    .eq(MidjourneyUser::getUserToken, userToken).last("limit 1"));
+            if (midjourneyUser == null){
+                throw new BusinessRuntimeException("账号不存在,请重新登录");
+            }
+            redisService.set(RedisService.key.MIDJOURNEY_USER.getName() + userToken, midjourneyUser, RedisService.key.MIDJOURNEY_USER.getTimeout());
+            Object fastNum = redisService.get(RedisService.key.MIDJOURNEY_FAST_LIMIT.getName() + midjourneyUser.getId());
+            if (fastNum == null){
+                redisService.set(RedisService.key.MIDJOURNEY_FAST_LIMIT.getName() + midjourneyUser.getId(), midjourneyUser.getMjFastNum(), RedisService.key.MIDJOURNEY_FAST_LIMIT.getTimeout());
+            }
+            Object relaxNum = redisService.get(RedisService.key.MIDJOURNEY_RELAX_LIMIT.getName() + midjourneyUser.getId());
+            if (relaxNum == null){
+                if (midjourneyUser.getMjRelaxNum() != null){
+                    redisService.set(RedisService.key.MIDJOURNEY_RELAX_LIMIT.getName() + midjourneyUser.getId(), midjourneyUser.getMjRelaxNum(), RedisService.key.MIDJOURNEY_RELAX_LIMIT.getTimeout());
+                }
+            }
+        }else {
+            Object fastNum = redisService.get(RedisService.key.MIDJOURNEY_FAST_LIMIT.getName() + midjourneyUser.getId());
+            if (fastNum == null){
+                redisService.set(RedisService.key.MIDJOURNEY_FAST_LIMIT.getName() + midjourneyUser.getId(), midjourneyUser.getMjFastNum(), RedisService.key.MIDJOURNEY_FAST_LIMIT.getTimeout());
+            }else {
+                midjourneyUser.setMjFastNum(Integer.parseInt(fastNum.toString()));
+            }
+            Object relaxNum = redisService.get(RedisService.key.MIDJOURNEY_RELAX_LIMIT.getName() + midjourneyUser.getId());
+            if (relaxNum == null){
+                if (midjourneyUser.getMjRelaxNum() != null){
+                    redisService.set(RedisService.key.MIDJOURNEY_RELAX_LIMIT.getName() + midjourneyUser.getId(), midjourneyUser.getMjRelaxNum(), RedisService.key.MIDJOURNEY_RELAX_LIMIT.getTimeout());
+                }
+            }else {
+                midjourneyUser.setMjRelaxNum(Integer.parseInt(relaxNum.toString()));
+            }
+        }
+        return midjourneyUser;
+    }
+
+
+    /**
+     * 检查用户次数
+     */
+    public Long checkUserLimit(MidjourneyUser user){
+        Long num = 0L;
+        if (user.getExpireTime().before(new Date())){
+            throw BusinessRuntimeException.getInstance("账号已过期");
+        }
+       if (user.getMode() == 1){
+           num = redisService.decr(RedisService.key.MIDJOURNEY_FAST_LIMIT.getName() + user.getId(), 1L);
+           if (num < 0) {
+               user.setMode(2);
+               redisService.incr(RedisService.key.MIDJOURNEY_FAST_LIMIT.getName() + user.getId(), 1L);
+           }
+       }
+       if (user.getMode() == 2){
+           Object relax = redisService.get(RedisService.key.MIDJOURNEY_RELAX_LIMIT.getName() + user.getId());
+           if (relax != null){
+               num = redisService.decr(RedisService.key.MIDJOURNEY_RELAX_LIMIT.getName() + user.getId(), 1L);
+               if (num < 0){
+                   redisService.incr(RedisService.key.MIDJOURNEY_RELAX_LIMIT.getName() + user.getId(), 1L);
+               }
+           }else {
+               num = null;
+           }
+       }
+       return num;
+    }
+
+    /**
+     * 恢复次数
+     */
+    public void recoverUserLimit(Long id,Integer mode,Long num){
+        if (mode == 1){
+            redisService.incr(RedisService.key.MIDJOURNEY_FAST_LIMIT.getName() + id, 1L);
+        }
+        if (mode == 2){
+            if (num != null){
+                redisService.incr(RedisService.key.MIDJOURNEY_RELAX_LIMIT.getName() + id, 1L);
+            }
+        }
+    }
+
+    /**
+     * 同步数据库
+     */
+    public void syncUser(Long id,Integer mode,Long num){
+        log.info("同步次数 id:{},mode:{},num:{}",id,mode,num);
+        if (num == null){
+            return;
+        }
+        LambdaUpdateWrapper<MidjourneyUser> wrapper = Wrappers.lambdaUpdate(MidjourneyUser.class)
+                .eq(MidjourneyUser::getId, id);
+        if (mode == 1){
+            wrapper.set(MidjourneyUser::getMjFastNum, num);
+        }
+        if (mode == 2){
+            wrapper.set(MidjourneyUser::getMjRelaxNum, num);
+        }
+        midjourneyUserMapper.update(null, wrapper);
+    }
+
+
+    /**
+     * 查询用户信息
+     */
+    @GetMapping("/whoami")
+    public Result<MidjourneyUser> queryUser(){
+        MidjourneyUser user = getUser();
+        return GatewayResponse.SUCCESS.newBuilder().toResult(user);
+    }
+
+
+    /**
+     * 提交Imagine任务
+     */
+    @PostMapping("/submit/imagine")
+    @NoSubmit
+    public Result<MidjourneyUserConversation> submitImagine(@RequestBody SubmitImagineDTO submitImagineDTO) {
+        log.info("提交Imagine任务,提示:{},base64数组长度:{}",submitImagineDTO.getPrompt(),submitImagineDTO.getBase64Array());
+        MidjourneyUser user = getUser();
+        Long num = checkUserLimit(user);
+        if (num != null && num < 0){
+            throw BusinessRuntimeException.getInstance("次数已用完");
+        }
+        MidjourneyUserConversation conversation;
+        try {
+            conversation = midjourneyService.submitImagine(user, submitImagineDTO.getPrompt(), submitImagineDTO.getBotType(), submitImagineDTO.getBase64Array());
+        } catch (Exception e) {
+            recoverUserLimit(user.getId(), user.getMode(),num);
+            throw BusinessRuntimeException.getInstance(e.getMessage());
+        }
+        syncUser(user.getId(), user.getMode(), num);
+        return GatewayResponse.SUCCESS.newBuilder().toResult(conversation);
+    }
+
+    /**
+     * 提交Describe任务
+     */
+    @PostMapping("/submit/describe")
+    @NoSubmit
+    public Result<MidjourneyUserConversation> submitDescribe(@RequestBody SubmitDescribeDTO submitDescribeDTO) {
+        log.info("提交Describe任务");
+        MidjourneyUser user = getUser();
+        Long num = checkUserLimit(user);
+        if (num != null && num < 0){
+            throw BusinessRuntimeException.getInstance("次数已用完");
+        }
+        MidjourneyUserConversation conversation;
+        try {
+            conversation = midjourneyService.submitDescribe(user, submitDescribeDTO.getBase64(), submitDescribeDTO.getBotType());
+        } catch (Exception e) {
+            recoverUserLimit(user.getId(), user.getMode(),num);
+            throw BusinessRuntimeException.getInstance(e.getMessage());
+        }
+        syncUser(user.getId(), user.getMode(), num);
+        return GatewayResponse.SUCCESS.newBuilder().toResult(conversation);
+    }
+
+    /**
+     * 提交Blend任务
+     */
+    @PostMapping("/submit/blend")
+    @NoSubmit
+    public Result<MidjourneyUserConversation> submitBlend(@RequestBody SubmitBlendDTO submitBlendDTO) {
+        log.info("提交Blend任务 dimensions:{},base64数组长度:{}", submitBlendDTO.getDimensions(), submitBlendDTO.getBase64Array().size());
+        MidjourneyUser user = getUser();
+        Long num = checkUserLimit(user);
+        if (num != null && num < 0){
+            throw BusinessRuntimeException.getInstance("次数已用完");
+        }
+        MidjourneyUserConversation conversation;
+        try {
+            conversation = midjourneyService.submitBlend(user, submitBlendDTO.getDimensions(), submitBlendDTO.getBotType(), submitBlendDTO.getBase64Array());
+        } catch (Exception e) {
+            recoverUserLimit(user.getId(), user.getMode(),num);
+            throw BusinessRuntimeException.getInstance(e.getMessage());
+        }
+        syncUser(user.getId(), user.getMode(), num);
+        return GatewayResponse.SUCCESS.newBuilder().toResult(conversation);
+    }
+
+    /**
+     * 提交Modal任务
+     */
+    @PostMapping("/submit/modal")
+    @NoSubmit
+    public Result<MidjourneyUserConversation> submitModal(@RequestBody SubmitModalDTO submitModalDTO) {
+        log.info("提交Modal任务,taskId:{},提示:{},base64数组长度:{}",submitModalDTO.getTaskId(),submitModalDTO.getPrompt(),submitModalDTO.getMaskBase64());
+        MidjourneyUser user = getUser();
+        Long num = checkUserLimit(user);
+        if (num != null && num < 0){
+            throw BusinessRuntimeException.getInstance("次数已用完");
+        }
+        MidjourneyUserConversation conversation;
+        try {
+            conversation = midjourneyService.submitModal(user, submitModalDTO.getTaskId(), submitModalDTO.getPrompt(), submitModalDTO.getMaskBase64());
+        } catch (Exception e) {
+            recoverUserLimit(user.getId(), user.getMode(),num);
+            throw BusinessRuntimeException.getInstance(e.getMessage());
+        }
+        syncUser(user.getId(), user.getMode(), num);
+        return GatewayResponse.SUCCESS.newBuilder().toResult(conversation);
+    }
+
+    /**
+     * 提交Shorten任务
+     */
+    @PostMapping("/submit/shorten")
+    @NoSubmit
+    public Result<MidjourneyUserConversation> submitShorten(@RequestBody SubmitShortenDTO submitShortenDTO) {
+        log.info("提交Shorten任务 提示词:{}",submitShortenDTO.getPrompt());
+        MidjourneyUser user = getUser();
+        Long num = checkUserLimit(user);
+        if (num != null && num < 0){
+            throw BusinessRuntimeException.getInstance("次数已用完");
+        }
+        MidjourneyUserConversation conversation;
+        try {
+            conversation = midjourneyService.submitShorten(user, submitShortenDTO.getBotType(), submitShortenDTO.getPrompt());
+        } catch (Exception e) {
+            recoverUserLimit(user.getId(), user.getMode(),num);
+            throw BusinessRuntimeException.getInstance(e.getMessage());
+        }
+        syncUser(user.getId(), user.getMode(), num);
+        return GatewayResponse.SUCCESS.newBuilder().toResult(conversation);
+    }
+
+    /**
+     * 执行动作
+     */
+    @PostMapping("/submit/action")
+    @NoSubmit
+    public Result<SubmitResult> action(@RequestBody SubmitActionDTO actionDTO) {
+        log.info("任务id:{},执行动作:{}",actionDTO.getTaskId(),actionDTO.getCustomId());
+        MidjourneyUser user = getUser();
+        Long num = checkUserLimit(user);
+        if (num != null && num < 0){
+            throw BusinessRuntimeException.getInstance("次数已用完");
+        }
+        SubmitResult conversation;
+        try {
+            conversation = midjourneyService.submitAction(user, actionDTO.getTaskId(), actionDTO.getCustomId(),num, actionDTO.getBotType());
+        } catch (Exception e) {
+            recoverUserLimit(user.getId(), user.getMode(),num);
+            throw BusinessRuntimeException.getInstance(e.getMessage());
+        }
+        return GatewayResponse.SUCCESS.newBuilder().toResult(conversation);
+    }
+
+
+    /**
+     * 查询会话列表
+     */
+    @GetMapping("/conversation/list")
+    public Result<SearchResult<MidjourneyUserConversation>> conversationList(){
+        MidjourneyUser user = getUser();
+        log.info("查询会话列表 userId:{}",user.getId());
+        MapBuilder builder = MapUtils.flatBuilder(request.getParameterMap());
+        return GatewayResponse.SUCCESS.newBuilder().toResult(beanSearcher.search(MidjourneyUserConversation.class,
+                builder.field(MidjourneyUserConversation::getUserId, user.getId())
+                        .orderBy(MidjourneyUserConversation::getId).desc().build()));
+    }
+
+    /**
+     * 根据ids查询会话
+     */
+    @GetMapping("/conversation/listByIds")
+    public Result<List<MidjourneyUserConversation>> conversationListByIds(Integer mode,@RequestParam(value = "ids" ) List<Long> ids) throws Exception {
+        MidjourneyUser user = getUser();
+        log.info("根据ids查询会话 userId:{}",user.getId());
+        return GatewayResponse.SUCCESS.newBuilder().toResult(midjourneyService.listConversationByIds(mode,ids));
+    }
+
+    /**
+     * 取消任务
+     */
+    @PostMapping("/conversation/{id}/cancel")
+    @NoSubmit
+    public Result<MidjourneyUserConversation> conversationCancel(@PathVariable("id") Long id){
+        MidjourneyUser user = getUser();
+        log.info("取消任务 id:{}",id);
+        return GatewayResponse.SUCCESS.newBuilder().toResult(midjourneyService.cancelConversation(user,id));
+    }
+
+
+    /**
+     * 修改midjourney账号模式
+     */
+    @PutMapping("/put/midjourney/user/{mode}")
+    public Result<String> putMidjourneyUserMode(@PathVariable Integer mode, Long userId) {
+        log.info("修改midjourney账号模式 userId:{},mode:{}", userId, mode);
+        MidjourneyUser midjourneyUser = midjourneyUserMapper.selectById(userId);
+        if (midjourneyUser == null) {
+            throw BusinessRuntimeException.getInstance("参数错误");
+        }
+        midjourneyUser.setMode(mode);
+        midjourneyUserMapper.updateById(midjourneyUser);
+        redisService.del(RedisService.key.MIDJOURNEY_USER.getName() + midjourneyUser.getUserToken());
+        return GatewayResponse.SUCCESS.newBuilder().toResult();
+    }
+
+    /**
+     * midjourney 回调
+     */
+    @PostMapping("/notifyHook")
+    public void notifyHook(HttpServletRequest request) throws Exception {
+        InputStream inputStream = request.getInputStream();
+        byte[] bytes = IoKit.toBytes(inputStream);
+        String json = new String(bytes, StandardCharsets.UTF_8);
+        midjourneyService.notifyHook(json);
+    }
+}

+ 93 - 0
midjourney/src/main/java/com/yhlxj/web/mirror/MirrorController.java

@@ -0,0 +1,93 @@
+package com.yhlxj.web.mirror;
+
+import cn.hutool.core.date.DateUtil;
+import com.cyksj.common.EnvCommonService;
+import com.cyksj.common.exception.BusinessRuntimeException;
+import com.cyksj.common.task.GlobalThreadPoolTaskExecutor;
+import com.cyksj.dto.Result;
+import com.cyksj.enums.GatewayResponse;
+import com.cyksj.model.entity.ChatgptSession;
+import com.cyksj.model.entity.ChatgptUser;
+import com.cyksj.model.entity.MidjourneyUser;
+import com.cyksj.model.request.gpt.ConversationRequest;
+import com.cyksj.model.response.ConversationLimitResponse;
+import com.cyksj.model.views.ChatGptUserConversationRecordHistoryView;
+import com.cyksj.model.views.ChatGptUserView;
+import com.cyksj.model.views.ChatgptCarInfoView;
+import com.cyksj.model.views.ChatgptConversionLimitView;
+import com.cyksj.service.chatgpt.ChatGptAccountService;
+import com.cyksj.service.midjourney.MidjourneyAccountService;
+import com.cyksj.web.util.StpUserUtil;
+import com.ejlchina.searcher.BeanSearcher;
+import com.ejlchina.searcher.SearchResult;
+import com.ejlchina.searcher.util.MapBuilder;
+import com.ejlchina.searcher.util.MapUtils;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.web.bind.annotation.*;
+
+import javax.servlet.http.Cookie;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.time.LocalDateTime;
+import java.util.Date;
+import java.util.Set;
+
+/**
+ * /server/镜像服务
+ *
+ * @author chan
+ * @date 2024/3/19 14:41
+ */
+@Slf4j
+@RequiredArgsConstructor
+@RestController
+@RequestMapping("/applets/mirror")
+public class MirrorController {
+
+    private static final GlobalThreadPoolTaskExecutor TASK_EXECUTOR = GlobalThreadPoolTaskExecutor.getInstance();
+
+
+    private final HttpServletResponse response;
+
+    private final HttpServletRequest request;
+
+    private final BeanSearcher beanSearcher;
+
+    private final MidjourneyAccountService midjourneyAccountService;
+
+    private final EnvCommonService envCommonService;
+
+    @Value("${midjourney.url}")
+    private String midjourneyHost;
+
+    @Value("${midjourney.drawUrl}")
+    private String midjourneyDrawUrl;
+
+    /**
+     * MJ车票跳转登录
+     *
+     * @param relationId 车票id
+     */
+    @GetMapping("/midjourneyMirror/{relationId}")
+    public Result<String> midjourneyMirror(@PathVariable Long relationId) {
+        Long userId = StpUserUtil.getLoginIdAsLong();
+        MidjourneyUser midjourneyUser = midjourneyAccountService.getMidjourneyUserToken(userId, relationId);
+        return GatewayResponse.SUCCESS.newBuilder().toResult(midjourneyHost + (EnvCommonService.active.equals(envCommonService.getEnv()) ? "/8081":"/8082") +"/api/applets/mirror/midjourneyMirrorWithToken/" + midjourneyUser.getUserToken());
+    }
+
+    @GetMapping("/midjourneyMirrorWithToken/{userToken}")
+    public void midjourneyMirrorWithToken(@PathVariable String userToken) throws IOException {
+        Cookie cookie = new Cookie("userToken", userToken);
+        cookie.setMaxAge(31536000);
+        cookie.setPath("/");
+        cookie.setMaxAge();
+        response.addCookie(cookie);
+        response.sendRedirect(midjourneyDrawUrl);
+    }
+}

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 0 - 0
midjourney/src/main/resources/application-dev.yml


Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 0 - 0
midjourney/src/main/resources/application-prd.yml


+ 1 - 0
pom.xml

@@ -207,6 +207,7 @@
         <module>netflix-dao</module>
         <module>netflix-service</module>
         <module>netflix-web</module>
+        <module>midjourney</module>
     </modules>
 
 

Kaikkia tiedostoja ei voida näyttää, sillä liian monta tiedostoa muuttui tässä diffissä