tools;
+
+ /**
+ * 取值:String或者ToolChoiceObj
+ *
+ * @since 1.1.2
+ */
+ @JsonProperty("tool_choice")
+ private Object toolChoice;
+
+ /**
+ * 使用什么取样温度,0到2之间。较高的值(如0.8)将使输出更加随机,而较低的值(如0.2)将使输出更加集中和确定。
+ *
+ * We generally recommend altering this or but not both.top_p
+ */
+ @Builder.Default
+ private double temperature = 0.2;
+
+ /**
+ * 使用温度采样的替代方法称为核心采样,其中模型考虑具有top_p概率质量的令牌的结果。因此,0.1 意味着只考虑包含前 10% 概率质量的代币。
+ *
+ * 我们通常建议更改此设置,但不要同时更改两者。temperature
+ */
+ @JsonProperty("top_p")
+ @Builder.Default
+ private Double topP = 1d;
+
+
+ /**
+ * 为每个提示生成的完成次数。
+ */
+ @Builder.Default
+ private Integer n = 1;
+
+
+ /**
+ * 是否流式输出.
+ * default:false
+ */
+ @Builder.Default
+ private boolean stream = false;
+ /**
+ * 停止输出标识
+ */
+ private List stop;
+ /**
+ * 最大支持4096
+ */
+ @JsonProperty("max_tokens")
+ @Builder.Default
+ private Integer maxTokens = 2048;
+
+
+ @JsonProperty("presence_penalty")
+ @Builder.Default
+ private double presencePenalty = 0;
+
+ /**
+ * -2.0 ~~ 2.0
+ */
+ @JsonProperty("frequency_penalty")
+ @Builder.Default
+ private double frequencyPenalty = 0;
+
+ @JsonProperty("logit_bias")
+ private Map logitBias;
+ /**
+ * 用户唯一值,确保接口不被重复调用
+ */
+ private String user;
+
+ /**
+ * @since 1.1.2
+ */
+ private Integer seed;
+
+
+ /**
+ * 最新模型参考官方文档:
+ * 官方稳定模型列表
+ */
+ @Getter
+ @AllArgsConstructor
+ public enum Model {
+ /**
+ * gpt-3.5-turbo
+ */
+ GPT_3_5_TURBO("gpt-3.5-turbo"),
+ /**
+ * 临时模型,不建议使用,2023年9 月 13 日将被弃用
+ */
+ @Deprecated
+ GPT_3_5_TURBO_0301("gpt-3.5-turbo-0301"),
+ /**
+ * gpt-3.5-turbo-0613 支持函数
+ */
+ GPT_3_5_TURBO_1106("gpt-3.5-turbo-1106"),
+
+ GPT_3_5_TURBO_0613("gpt-3.5-turbo-0613"),
+ /**
+ * gpt-3.5-turbo-16k 超长上下文
+ */
+ GPT_3_5_TURBO_16K("gpt-3.5-turbo-16k"),
+ /**
+ * gpt-3.5-turbo-16k-0613 超长上下文 支持函数
+ */
+ GPT_3_5_TURBO_16K_0613("gpt-3.5-turbo-16k-0613"),
+ /**
+ * GPT4.0
+ */
+ GPT_4("gpt-4"),
+ /**
+ * 临时模型,不建议使用,2023年9 月 13 日将被弃用
+ */
+ @Deprecated
+ GPT_4_0314("gpt-4-0314"),
+ /**
+ * GPT4.0 超长上下文
+ */
+ GPT_4_32K("gpt-4-32k"),
+ /**
+ * 临时模型,不建议使用,2023年9 月 13 日将被弃用
+ */
+ @Deprecated
+ GPT_4_32K_0314("gpt-4-32k-0314"),
+
+ /**
+ * gpt-4-0613,支持函数
+ */
+ GPT_4_0613("gpt-4-0613"),
+ /**
+ * gpt-4-0613,支持函数
+ */
+ GPT_4_32K_0613("gpt-4-32k-0613"),
+ /**
+ * 支持数组模式,支持function call,支持可重复输出
+ */
+ GPT_4_1106_PREVIEW("gpt-4-1106-preview"),
+ /**
+ * 支持图片
+ */
+ GPT_4_VISION_PREVIEW("gpt-4-vision-preview"),
+ ;
+ private final String name;
+ }
+
+ @Getter
+ @AllArgsConstructor
+ public enum ChatType {
+ /**
+ * 对话类型 - 输入
+ */
+ CHAT_IN("in"),
+ /**
+ * 对话类型 - 输出
+ */
+ CHAT_OUT("out"),
+
+ ;
+ private final String name;
+ }
+
+ public static double getModelCost(String modelName) {
+ return switch (modelName) {
+ case "gpt-3.5-turbo-0613" -> OpenAIConst.GPT3_COST;
+ default -> OpenAIConst.GPT4_COST;
+ };
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/chat/BaseMessage.java b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/chat/BaseMessage.java
new file mode 100644
index 00000000..391b4c6a
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/chat/BaseMessage.java
@@ -0,0 +1,84 @@
+package com.xmzs.common.chat.entity.chat;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.unfbx.chatgpt.entity.chat.tool.ToolCalls;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.Getter;
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * 描述:
+ *
+ * @author https:www.unfbx.com
+ * @since 1.1.2
+ * 2023-03-02
+ */
+@Data
+@JsonInclude(JsonInclude.Include.NON_NULL)
+@JsonIgnoreProperties(ignoreUnknown = true)
+@AllArgsConstructor
+public class BaseMessage implements Serializable {
+
+ /**
+ * 目前支持四个中角色参考官网,进行情景输入:
+ * https://platform.openai.com/docs/guides/chat/introduction
+ */
+ private String role;
+
+
+ private String name;
+
+ /**
+ * The tool calls generated by the model, such as function calls.
+ * @since 1.1.2
+ */
+ @JsonProperty("tool_calls")
+ private List toolCalls;
+
+ /**
+ * @since 1.1.2
+ */
+ @JsonProperty("tool_call_id")
+ private String toolCallId;
+
+ @Deprecated
+ @JsonProperty("function_call")
+ private FunctionCall functionCall;
+
+
+ /**
+ * 构造函数
+ *
+ * @param role 角色
+ * @param name name
+ * @param functionCall functionCall
+ */
+ public BaseMessage(String role, String name, FunctionCall functionCall) {
+ this.role = role;
+ this.name = name;
+ this.functionCall = functionCall;
+ }
+
+ public BaseMessage() {
+ }
+
+
+ @Getter
+ @AllArgsConstructor
+ public enum Role {
+
+ SYSTEM("system"),
+ USER("user"),
+ ASSISTANT("assistant"),
+ FUNCTION("function"),
+ TOOL("tool"),
+ ;
+ private final String name;
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/chat/ChatChoice.java b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/chat/ChatChoice.java
new file mode 100644
index 00000000..b3a1644a
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/chat/ChatChoice.java
@@ -0,0 +1,31 @@
+package com.xmzs.common.chat.entity.chat;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 描述:
+ *
+ * @author https:www.unfbx.com
+ * @since 2023-03-02
+ */
+@Data
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class ChatChoice implements Serializable {
+ private long index;
+ /**
+ * 请求参数stream为true返回是delta
+ */
+ @JsonProperty("delta")
+ private Message delta;
+ /**
+ * 请求参数stream为false返回是message
+ */
+ @JsonProperty("message")
+ private Message message;
+ @JsonProperty("finish_reason")
+ private String finishReason;
+}
diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/chat/ChatCompletion.java b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/chat/ChatCompletion.java
new file mode 100644
index 00000000..0e626ebe
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/chat/ChatCompletion.java
@@ -0,0 +1,34 @@
+package com.xmzs.common.chat.entity.chat;
+
+import cn.hutool.core.collection.CollectionUtil;
+import cn.hutool.core.util.StrUtil;
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import lombok.*;
+import lombok.experimental.SuperBuilder;
+import lombok.extern.slf4j.Slf4j;
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * 描述: chat模型参数
+ *
+ * @author https:www.unfbx.com
+ * 2023-03-02
+ */
+@Data
+@SuperBuilder
+@Slf4j
+@JsonInclude(JsonInclude.Include.NON_NULL)
+@NoArgsConstructor
+@AllArgsConstructor
+public class ChatCompletion extends BaseChatCompletion implements Serializable {
+
+ /**
+ * 问题描述
+ */
+ @NonNull
+ private List messages;
+
+}
diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/chat/ChatCompletionResponse.java b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/chat/ChatCompletionResponse.java
new file mode 100644
index 00000000..170e74dd
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/chat/ChatCompletionResponse.java
@@ -0,0 +1,26 @@
+package com.xmzs.common.chat.entity.chat;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.xmzs.common.chat.entity.common.Usage;
+import lombok.Data;
+
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * 描述: chat答案类
+ *
+ * @author https:www.unfbx.com
+ * 2023-03-02
+ */
+@Data
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class ChatCompletionResponse implements Serializable {
+ private String id;
+ private String object;
+ private long created;
+ private String model;
+ private List choices;
+ private Usage usage;
+}
diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/chat/ChatCompletionWithPicture.java b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/chat/ChatCompletionWithPicture.java
new file mode 100644
index 00000000..a5c68018
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/chat/ChatCompletionWithPicture.java
@@ -0,0 +1,30 @@
+package com.xmzs.common.chat.entity.chat;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import lombok.*;
+import lombok.experimental.SuperBuilder;
+import lombok.extern.slf4j.Slf4j;
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * 描述: chat模型附带图片的参数
+ *
+ * @author https:www.unfbx.com
+ * @since 1.1.2
+ * 2023-11-10
+ */
+@Data
+@SuperBuilder
+@Slf4j
+@JsonInclude(JsonInclude.Include.NON_NULL)
+@NoArgsConstructor
+@AllArgsConstructor
+public class ChatCompletionWithPicture extends BaseChatCompletion implements Serializable {
+ /**
+ * 问题描述
+ */
+ private List messages;
+
+}
diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/chat/Content.java b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/chat/Content.java
new file mode 100644
index 00000000..f26fcb9a
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/chat/Content.java
@@ -0,0 +1,43 @@
+package com.xmzs.common.chat.entity.chat;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.*;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * 描述:
+ *
+ * @author https://www.unfbx.com
+ * @since 1.1.2
+ * 2023-11-10
+ */
+@Data
+@Builder
+@Slf4j
+@JsonInclude(JsonInclude.Include.NON_NULL)
+@NoArgsConstructor
+@AllArgsConstructor
+public class Content {
+ /**
+ * 输入类型:text、image_url
+ *
+ * @see Type
+ */
+ private String type;
+ private String text;
+ @JsonProperty("image_url")
+ private ImageUrl imageUrl;
+
+ /**
+ * 生成图片风格
+ */
+ @Getter
+ @AllArgsConstructor
+ public enum Type {
+ TEXT("text"),
+ IMAGE_URL("image_url"),
+ ;
+ private final String name;
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/chat/FunctionCall.java b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/chat/FunctionCall.java
new file mode 100644
index 00000000..cc19b3a5
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/chat/FunctionCall.java
@@ -0,0 +1,27 @@
+package com.xmzs.common.chat.entity.chat;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * 描述:函数调用返回值
+ *
+ * @author https://www.unfbx.com
+ * @since 2023-06-14
+ */
+@Data
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+public class FunctionCall {
+ /**
+ * 方法名
+ */
+ private String name;
+ /**
+ * 方法参数
+ */
+ private String arguments;
+}
diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/chat/Functions.java b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/chat/Functions.java
new file mode 100644
index 00000000..95c1b3c7
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/chat/Functions.java
@@ -0,0 +1,46 @@
+package com.xmzs.common.chat.entity.chat;
+
+import lombok.Builder;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 描述:方法参数实体类,实例数据如下
+ *
+ * {
+ * "name": "get_current_weather",
+ * "description": "Get the current weather in a given location",
+ * "parameters": {
+ * "type": "object",
+ * "properties": {
+ * "location": {
+ * "type": "string",
+ * "description": "The city and state, e.g. San Francisco, CA"
+ * },
+ * "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}
+ * },
+ * "required": ["location"]
+ * },
+ * }
+ *
+ * @author https:www.unfbx.com
+ * @since 2023-06-14
+ */
+@Data
+@Builder
+public class Functions implements Serializable {
+ /**
+ * 方法名称
+ */
+ private String name;
+ /**
+ * 方法描述
+ */
+ private String description;
+ /**
+ * 方法参数
+ * 扩展参数可以继承Parameters自己实现,json格式的数据
+ */
+ private Parameters parameters;
+}
diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/chat/ImageUrl.java b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/chat/ImageUrl.java
new file mode 100644
index 00000000..c2fa6b37
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/chat/ImageUrl.java
@@ -0,0 +1,28 @@
+package com.xmzs.common.chat.entity.chat;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * 描述:
+ *
+ * @author https://www.unfbx.com
+ * 2023-11-10
+ */
+@Data
+@Builder
+@Slf4j
+@JsonInclude(JsonInclude.Include.NON_NULL)
+@NoArgsConstructor
+@AllArgsConstructor
+public class ImageUrl {
+ /**
+ * 图片地址,支持base64. eg: data:image/jpeg;base64,{base64_image}
+ * https://platform.openai.com/docs/guides/vision
+ */
+ private String url;
+}
diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/chat/Message.java b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/chat/Message.java
new file mode 100644
index 00000000..3738ebba
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/chat/Message.java
@@ -0,0 +1,117 @@
+package com.xmzs.common.chat.entity.chat;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.Getter;
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * 描述:
+ *
+ * @author https:www.unfbx.com
+ * @since 2023-03-02
+ */
+@Data
+@JsonInclude(JsonInclude.Include.NON_NULL)
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class Message implements Serializable {
+
+ /**
+ * 目前支持四个中角色参考官网,进行情景输入:
+ * https://platform.openai.com/docs/guides/chat/introduction
+ */
+ private String role;
+
+ private String content;
+
+ private String name;
+
+ @JsonProperty("function_call")
+ private FunctionCall functionCall;
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ /**
+ * 构造函数
+ *
+ * @param role 角色
+ * @param content 描述主题信息
+ * @param name name
+ * @param functionCall functionCall
+ */
+ public Message(String role, String content, String name, FunctionCall functionCall) {
+ this.role = role;
+ this.content = content;
+ this.name = name;
+ this.functionCall = functionCall;
+ }
+
+ public Message() {
+ }
+
+ private Message(Builder builder) {
+ setRole(builder.role);
+ setContent(builder.content);
+ setName(builder.name);
+ setFunctionCall(builder.functionCall);
+ }
+
+
+ @Getter
+ @AllArgsConstructor
+ public enum Role {
+
+ SYSTEM("system"),
+ USER("user"),
+ ASSISTANT("assistant"),
+ FUNCTION("function"),
+ ;
+ private String name;
+ }
+
+ public static final class Builder {
+ private String role;
+ private String content;
+ private String name;
+ private FunctionCall functionCall;
+
+ public Builder() {
+ }
+
+ public Builder role(Role role) {
+ this.role = role.getName();
+ return this;
+ }
+
+ public Builder role(String role) {
+ this.role = role;
+ return this;
+ }
+
+ public Builder content(String content) {
+ this.content = content;
+ return this;
+ }
+
+ public Builder name(String name) {
+ this.name = name;
+ return this;
+ }
+
+ public Builder functionCall(FunctionCall functionCall) {
+ this.functionCall = functionCall;
+ return this;
+ }
+
+ public Message build() {
+ return new Message(this);
+ }
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/chat/MessagePicture.java b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/chat/MessagePicture.java
new file mode 100644
index 00000000..9ad7d632
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/chat/MessagePicture.java
@@ -0,0 +1,114 @@
+package com.xmzs.common.chat.entity.chat;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.unfbx.chatgpt.entity.chat.tool.ToolCalls;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * 描述:
+ *
+ * @author https:www.unfbx.com
+ * @since 2023-03-02
+ */
+@Data
+@JsonInclude(JsonInclude.Include.NON_NULL)
+@JsonIgnoreProperties(ignoreUnknown = true)
+@AllArgsConstructor
+public class MessagePicture extends BaseMessage implements Serializable {
+ /**
+ * Content数组支持多图片输入
+ * https://platform.openai.com/docs/guides/vision
+ */
+ private List content;
+
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ /**
+ * 构造函数
+ *
+ * @param role 角色
+ * @param name name
+ * @param content content
+ * @param functionCall functionCall
+ */
+ public MessagePicture(String role, String name, List content, List toolCalls, String toolCallId, FunctionCall functionCall) {
+ this.content = content;
+ super.setRole(role);
+ super.setName(name);
+ super.setToolCalls(toolCalls);
+ super.setToolCallId(toolCallId);
+ super.setFunctionCall(functionCall);
+ }
+
+ public MessagePicture() {
+ }
+
+ private MessagePicture(Builder builder) {
+ setContent(builder.content);
+ super.setRole(builder.role);
+ super.setName(builder.name);
+ super.setFunctionCall(builder.functionCall);
+ super.setToolCalls(builder.toolCalls);
+ super.setToolCallId(builder.toolCallId);
+ }
+
+ public static final class Builder {
+ private String role;
+ private List content;
+ private String name;
+ private String toolCallId;
+ private List toolCalls;
+ private FunctionCall functionCall;
+
+ public Builder() {
+ }
+
+ public Builder role(Role role) {
+ this.role = role.getName();
+ return this;
+ }
+
+ public Builder role(String role) {
+ this.role = role;
+ return this;
+ }
+
+ public Builder content(List content) {
+ this.content = content;
+ return this;
+ }
+
+ public Builder name(String name) {
+ this.name = name;
+ return this;
+ }
+
+ public Builder functionCall(FunctionCall functionCall) {
+ this.functionCall = functionCall;
+ return this;
+ }
+
+ public Builder toolCalls(List toolCalls) {
+ this.toolCalls = toolCalls;
+ return this;
+ }
+
+ public Builder toolCallId(String toolCallId) {
+ this.toolCallId = toolCallId;
+ return this;
+ }
+
+ public MessagePicture build() {
+ return new MessagePicture(this);
+ }
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/chat/Parameters.java b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/chat/Parameters.java
new file mode 100644
index 00000000..b4d5ad8a
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/chat/Parameters.java
@@ -0,0 +1,42 @@
+package com.xmzs.common.chat.entity.chat;
+
+import lombok.Builder;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.List;
+/**
+ * 描述:方法参数类,扩展参数可以继承Parameters自己实现
+ * 参考:
+ *
+ * {
+ * "type": "object",
+ * "properties": {
+ * "location": {
+ * "type": "string",
+ * "description": "The city and state, e.g. San Francisco, CA"
+ * },
+ * "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}
+ * },
+ * "required": ["location"]
+ * }
+ *
+ * @author https:www.unfbx.com
+ * @since 2023-06-14
+ */
+@Data
+@Builder
+public class Parameters implements Serializable {
+ /**
+ * 参数类型
+ */
+ private String type;
+ /**
+ * 参数属性、描述
+ */
+ private Object properties;
+ /**
+ * 方法必输字段
+ */
+ private List required;
+}
diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/chat/ResponseFormat.java b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/chat/ResponseFormat.java
new file mode 100644
index 00000000..06da355c
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/chat/ResponseFormat.java
@@ -0,0 +1,28 @@
+package com.xmzs.common.chat.entity.chat;
+
+import lombok.*;
+
+/**
+ * 指定模型必须输出的格式的对象。
+ */
+@Data
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+public class ResponseFormat {
+ /**
+ * 默认:text
+ *
+ * @see Type
+ */
+ private String type;
+
+ @Getter
+ @AllArgsConstructor
+ public enum Type {
+ JSON_OBJECT("json_object"),
+ TEXT("text"),
+ ;
+ private final String name;
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/chat/tool/ToolCallFunction.java b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/chat/tool/ToolCallFunction.java
new file mode 100644
index 00000000..293ac314
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/chat/tool/ToolCallFunction.java
@@ -0,0 +1,31 @@
+package com.xmzs.common.chat.entity.chat.tool;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+
+/**
+ * ToolCall 的 Function参数
+ * The function that the model called.
+ *
+ * @author https:www.unfbx.com
+ * @since 1.1.2
+ * 2023-11-09
+ */
+@Data
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+public class ToolCallFunction implements Serializable {
+ /**
+ * 方法名
+ */
+ private String name;
+ /**
+ * 方法参数
+ */
+ private String arguments;
+}
diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/chat/tool/ToolCalls.java b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/chat/tool/ToolCalls.java
new file mode 100644
index 00000000..9b304b57
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/chat/tool/ToolCalls.java
@@ -0,0 +1,38 @@
+package com.unfbx.chatgpt.entity.chat.tool;
+
+import com.xmzs.common.chat.entity.chat.tool.ToolCallFunction;
+import lombok.*;
+
+import java.io.Serializable;
+
+/**
+ * The tool calls generated by the model, such as function calls.
+ *
+ * @author unfbx
+ * @since 1.1.2
+ * 2023-11-09
+ */
+@Data
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+public class ToolCalls implements Serializable {
+ /**
+ * The ID of the tool call.
+ */
+ private String id;
+ /**
+ * The type of the tool. Currently, only function is supported.
+ */
+ private String type;
+
+ private ToolCallFunction function;
+
+ @Getter
+ @AllArgsConstructor
+ public enum Type {
+ FUNCTION("function"),
+ ;
+ private final String name;
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/chat/tool/ToolChoice.java b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/chat/tool/ToolChoice.java
new file mode 100644
index 00000000..d99b6577
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/chat/tool/ToolChoice.java
@@ -0,0 +1,25 @@
+package com.xmzs.common.chat.entity.chat.tool;
+
+import lombok.*;
+
+import java.io.Serializable;
+
+/**
+ * choice和object同时存在是以object为准
+ *
+ * @author unfbx
+ * @since 1.1.2
+ * 2023-11-09
+ */
+@Data
+public class ToolChoice implements Serializable {
+
+ @Getter
+ @AllArgsConstructor
+ public enum Choice {
+ NONE("none"),
+ AUTO("auto"),
+ ;
+ private final String name;
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/chat/tool/ToolChoiceObj.java b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/chat/tool/ToolChoiceObj.java
new file mode 100644
index 00000000..04dbfd55
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/chat/tool/ToolChoiceObj.java
@@ -0,0 +1,33 @@
+package com.xmzs.common.chat.entity.chat.tool;
+
+import lombok.*;
+
+/**
+ * @author unfbx
+ * @since 1.1.2
+ * 2023-11-09
+ */
+@Data
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+public class ToolChoiceObj {
+ /**
+ * 需要调用的方法名称
+ */
+ private ToolChoiceObjFunction function;
+ /**
+ * 工具的类型。目前仅支持函数。
+ *
+ * @see Type
+ */
+ private String type;
+
+ @Getter
+ @AllArgsConstructor
+ public enum Type {
+ FUNCTION("function"),
+ ;
+ private final String name;
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/chat/tool/ToolChoiceObjFunction.java b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/chat/tool/ToolChoiceObjFunction.java
new file mode 100644
index 00000000..b45ab9ab
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/chat/tool/ToolChoiceObjFunction.java
@@ -0,0 +1,21 @@
+package com.xmzs.common.chat.entity.chat.tool;
+
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * @author unfbx
+ * @since 1.1.2
+ * 2023-11-09
+ */
+@Data
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+public class ToolChoiceObjFunction {
+
+ private String name;
+}
diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/chat/tool/Tools.java b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/chat/tool/Tools.java
new file mode 100644
index 00000000..43f5be92
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/chat/tool/Tools.java
@@ -0,0 +1,35 @@
+package com.xmzs.common.chat.entity.chat.tool;
+
+
+import lombok.*;
+
+import java.io.Serializable;
+
+/**
+ * @author unfbx
+ * @since 1.1.2
+ * 2023-11-09
+ */
+@Data
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+public class Tools implements Serializable {
+
+ /**
+ * 目前只支持:function
+ *
+ * @see Type
+ */
+ private String type;
+
+ private ToolsFunction function;
+
+ @Getter
+ @AllArgsConstructor
+ public enum Type {
+ FUNCTION("function"),
+ ;
+ private final String name;
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/chat/tool/ToolsFunction.java b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/chat/tool/ToolsFunction.java
new file mode 100644
index 00000000..7465eb5e
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/chat/tool/ToolsFunction.java
@@ -0,0 +1,36 @@
+package com.xmzs.common.chat.entity.chat.tool;
+
+
+import com.xmzs.common.chat.entity.chat.Parameters;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+
+/**
+ * @author unfbx
+ * @since 1.1.2
+ * 2023-11-09
+ */
+@Data
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+public class ToolsFunction implements Serializable {
+
+ /**
+ * 要调用的函数的名称。必须是 a-z、A-Z、0-9,或包含下划线和破折号,最大长度为 64
+ */
+ private String name;
+ /**
+ * 对函数功能的描述,模型使用它来选择何时以及如何调用该函数。
+ */
+ private String description;
+ /**
+ * 函数接受的参数,描述为 JSON Schema 对象
+ * 扩展参数可以继承Parameters自己实现,json格式的数据
+ */
+ private Parameters parameters;
+}
diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/common/Choice.java b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/common/Choice.java
new file mode 100644
index 00000000..63360b87
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/common/Choice.java
@@ -0,0 +1,23 @@
+package com.xmzs.common.chat.entity.common;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 描述:
+ *
+ * @author https:www.unfbx.com
+ * 2023-02-15
+ */
+@Data
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class Choice implements Serializable {
+ private String text;
+ private long index;
+ private Object logprobs;
+ @JsonProperty("finish_reason")
+ private String finishReason;
+}
diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/common/DeleteResponse.java b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/common/DeleteResponse.java
new file mode 100644
index 00000000..4d35e0d2
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/common/DeleteResponse.java
@@ -0,0 +1,20 @@
+package com.xmzs.common.chat.entity.common;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 描述:
+ *
+ * @author https:www.unfbx.com
+ * 2023-02-15
+ */
+@Data
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class DeleteResponse implements Serializable {
+ private String id;
+ private String object;
+ private boolean deleted;
+}
diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/common/OpenAiResponse.java b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/common/OpenAiResponse.java
new file mode 100644
index 00000000..36b8b6e2
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/common/OpenAiResponse.java
@@ -0,0 +1,30 @@
+package com.xmzs.common.chat.entity.common;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.List;
+/**
+ * 描述:
+ *
+ * @author https:www.unfbx.com
+ * 2023-02-15
+ */
+@Data
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class OpenAiResponse implements Serializable {
+ private String object;
+ private List data;
+ private Error error;
+
+
+ @Data
+ @JsonIgnoreProperties(ignoreUnknown = true)
+ public class Error {
+ private String message;
+ private String type;
+ private String param;
+ private String code;
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/common/Usage.java b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/common/Usage.java
new file mode 100644
index 00000000..9802aa76
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/common/Usage.java
@@ -0,0 +1,24 @@
+package com.xmzs.common.chat.entity.common;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 描述:
+ *
+ * @author https:www.unfbx.com
+ * 2023-02-15
+ */
+@Data
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class Usage implements Serializable {
+ @JsonProperty("prompt_tokens")
+ private long promptTokens;
+ @JsonProperty("completion_tokens")
+ private long completionTokens;
+ @JsonProperty("total_tokens")
+ private long totalTokens;
+}
diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/completions/Completion.java b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/completions/Completion.java
new file mode 100644
index 00000000..f3ecc121
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/completions/Completion.java
@@ -0,0 +1,126 @@
+package com.xmzs.common.chat.entity.completions;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.*;
+import lombok.extern.slf4j.Slf4j;
+
+
+import java.io.Serializable;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 描述: 问题类
+ *
+ * @author https:www.unfbx.com
+ * 2023-02-11
+ */
+@Data
+@Builder
+@Slf4j
+@JsonInclude(JsonInclude.Include.NON_NULL)
+@NoArgsConstructor
+@AllArgsConstructor
+public class Completion implements Serializable {
+
+ @NonNull
+ @Builder.Default
+ private String model = Model.DAVINCI_003.getName();
+ /**
+ * 问题描述
+ */
+ @NonNull
+ private String prompt;
+ /**
+ * 完成输出后的后缀,用于格式化输出结果
+ */
+ private String suffix;
+
+ /**
+ * 最大支持4096
+ */
+ @JsonProperty("max_tokens")
+ @Builder.Default
+ private Integer maxTokens = 2048;
+ /**
+ * 使用什么取样温度,0到2之间。较高的值(如0.8)将使输出更加随机,而较低的值(如0.2)将使输出更加集中和确定。
+ *
+ * We generally recommend altering this or but not both.top_p
+ */
+ @Builder.Default
+ private double temperature = 0;
+
+ /**
+ * 使用温度采样的替代方法称为核心采样,其中模型考虑具有top_p概率质量的令牌的结果。因此,0.1 意味着只考虑包含前 10% 概率质量的代币。
+ *
+ * 我们通常建议更改此设置,但不要同时更改两者。temperature
+ */
+ @JsonProperty("top_p")
+ @Builder.Default
+ private Double topP = 1d;
+
+ /**
+ * 为每个提示生成的完成次数。
+ */
+ @Builder.Default
+ private Integer n = 1;
+
+ @Builder.Default
+ private boolean stream = false;
+ /**
+ * 最大值:5
+ */
+ private Integer logprobs;
+
+ @Builder.Default
+ private boolean echo = false;
+
+ private List stop;
+
+ @JsonProperty("presence_penalty")
+ @Builder.Default
+ private double presencePenalty = 0;
+
+ /**
+ * -2.0 ~~ 2.0
+ */
+ @JsonProperty("frequency_penalty")
+ @Builder.Default
+ private double frequencyPenalty = 0;
+
+ @JsonProperty("best_of")
+ @Builder.Default
+ private Integer bestOf = 1;
+
+ @JsonProperty("logit_bias")
+ private Map logitBias;
+ /**
+ * 用户唯一值,确保接口不被重复调用
+ */
+ private String user;
+
+ /**
+ * 获取当前参数的tokens数
+ * @return token数量
+ */
+// public long tokens() {
+// if (StrUtil.isBlank(this.prompt) || StrUtil.isBlank(this.model)) {
+// log.warn("参数异常model:{},prompt:{}", this.model, this.prompt);
+// return 0;
+// }
+// return TikTokensUtil.tokens(this.model, this.prompt);
+// }
+
+ @Getter
+ @AllArgsConstructor
+ public enum Model {
+ DAVINCI_003("text-davinci-003"),
+ DAVINCI_002("text-davinci-002"),
+ DAVINCI("davinci"),
+ ;
+ private String name;
+ }
+}
+
+
diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/completions/CompletionResponse.java b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/completions/CompletionResponse.java
new file mode 100644
index 00000000..891aa91d
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/completions/CompletionResponse.java
@@ -0,0 +1,27 @@
+package com.xmzs.common.chat.entity.completions;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.xmzs.common.chat.entity.common.Choice;
+import com.xmzs.common.chat.entity.common.OpenAiResponse;
+import com.xmzs.common.chat.entity.common.Usage;
+import lombok.Data;
+
+
+import java.io.Serializable;
+
+/**
+ * 描述: 答案类
+ *
+ * @author https:www.unfbx.com
+ * 2023-02-11
+ */
+@Data
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class CompletionResponse extends OpenAiResponse implements Serializable {
+ private String id;
+ private String object;
+ private long created;
+ private String model;
+ private Choice[] choices;
+ private Usage usage;
+}
diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/dto/WebSocketMessageDto.java b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/dto/WebSocketMessageDto.java
new file mode 100644
index 00000000..7a73aaef
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/dto/WebSocketMessageDto.java
@@ -0,0 +1,29 @@
+package com.xmzs.common.chat.entity.dto;
+
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * 消息的dto
+ *
+ * @author zendwang
+ */
+@Data
+public class WebSocketMessageDto implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 需要推送到的session key 列表
+ */
+ private List sessionKeys;
+
+ /**
+ * 需要发送的消息
+ */
+ private String message;
+}
diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/edits/Edit.java b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/edits/Edit.java
new file mode 100644
index 00000000..7e803362
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/edits/Edit.java
@@ -0,0 +1,104 @@
+package com.xmzs.common.chat.entity.edits;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.*;
+import lombok.extern.slf4j.Slf4j;
+
+import java.io.Serializable;
+
+/**
+ * 描述:
+ *
+ * @author https:www.unfbx.com
+ * 2023-02-15
+ */
+@Getter
+@Builder
+@Slf4j
+@NoArgsConstructor
+@AllArgsConstructor
+public class Edit implements Serializable {
+ /**
+ * 编辑模型,目前支持两种
+ */
+ @NonNull
+ private String model;
+
+ @NonNull
+ private String input;
+ /**
+ * 提示说明。告知模型如何修改。
+ */
+ @NonNull
+ private String instruction;
+
+
+ /**
+ * 使用什么取样温度,0到2之间。较高的值(如0.8)将使输出更加随机,而较低的值(如0.2)将使输出更加集中和确定。
+ *
+ * We generally recommend altering this or but not both.top_p
+ */
+ @Builder.Default
+ private double temperature = 0;
+
+ /**
+ * 使用温度采样的替代方法称为核心采样,其中模型考虑具有top_p概率质量的令牌的结果。因此,0.1 意味着只考虑包含前 10% 概率质量的代币。
+ *
+ * 我们通常建议更改此设置,但不要同时更改两者。temperature
+ */
+ @JsonProperty("top_p")
+ @Builder.Default
+ private Double topP = 1d;
+
+ /**
+ * 为每个提示生成的完成次数。
+ */
+ @Builder.Default
+ private Integer n = 1;
+
+ public void setModel(Model model) {
+ this.model = model.getName();
+ }
+
+ public void setTemperature(double temperature) {
+ if (temperature > 2 || temperature < 0) {
+ log.error("temperature参数异常,temperature属于[0,2]");
+ this.temperature = 2;
+ return;
+ }
+ if (temperature < 0) {
+ log.error("temperature参数异常,temperature属于[0,2]");
+ this.temperature = 0;
+ return;
+ }
+ this.temperature = temperature;
+ }
+
+
+ public void setTopP(Double topP) {
+ this.topP = topP;
+ }
+
+ public void setN(Integer n) {
+ this.n = n;
+ }
+
+ public void setInput(String input) {
+ this.input = input;
+ }
+
+ public void setInstruction(String instruction) {
+ this.instruction = instruction;
+ }
+ @Getter
+ @AllArgsConstructor
+ public enum Model {
+ TEXT_DAVINCI_EDIT_001("text-davinci-edit-001"),
+ CODE_DAVINCI_EDIT_001("code-davinci-edit-001"),
+ ;
+ private String name;
+ }
+}
+
+
+
diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/edits/EditResponse.java b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/edits/EditResponse.java
new file mode 100644
index 00000000..8f5c60b8
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/edits/EditResponse.java
@@ -0,0 +1,27 @@
+package com.xmzs.common.chat.entity.edits;
+
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.xmzs.common.chat.entity.common.Choice;
+import com.xmzs.common.chat.entity.common.Usage;
+import lombok.Data;
+
+
+import java.io.Serializable;
+
+/**
+ * 描述:
+ *
+ * @author https:www.unfbx.com
+ * 2023-02-15
+ */
+@Data
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class EditResponse implements Serializable {
+ private String id;
+ private String object;
+ private long created;
+ private String model;
+ private Choice[] choices;
+ private Usage usage;
+}
diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/embeddings/Embedding.java b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/embeddings/Embedding.java
new file mode 100644
index 00000000..79965902
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/embeddings/Embedding.java
@@ -0,0 +1,54 @@
+package com.xmzs.common.chat.entity.embeddings;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import lombok.*;
+import lombok.extern.slf4j.Slf4j;
+
+import java.io.Serializable;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * 描述:
+ *
+ * @author https:www.unfbx.com
+ * 2023-02-15
+ */
+@Getter
+@Slf4j
+@Builder
+@JsonInclude(JsonInclude.Include.NON_NULL)
+@NoArgsConstructor
+@AllArgsConstructor
+public class Embedding implements Serializable {
+ @NonNull
+ @Builder.Default
+ private String model = Model.TEXT_EMBEDDING_ADA_002.getName();
+ /**
+ * 必选项:长度不能超过:8192
+ */
+ @NonNull
+ private List input;
+
+ private String user;
+
+ public void setModel(Model model) {
+ if (Objects.isNull(model)) {
+ model = Model.TEXT_EMBEDDING_ADA_002;
+ }
+ this.model = model.getName();
+ }
+
+
+ public void setUser(String user) {
+ this.user = user;
+ }
+
+ @Getter
+ @AllArgsConstructor
+ public enum Model {
+ TEXT_EMBEDDING_ADA_002("text-embedding-ada-002"),
+ ;
+ private String name;
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/embeddings/EmbeddingResponse.java b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/embeddings/EmbeddingResponse.java
new file mode 100644
index 00000000..57bb1140
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/embeddings/EmbeddingResponse.java
@@ -0,0 +1,25 @@
+package com.xmzs.common.chat.entity.embeddings;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.xmzs.common.chat.entity.common.Usage;
+import lombok.Data;
+
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * 描述:
+ *
+ * @author https:www.unfbx.com
+ * 2023-02-15
+ */
+@Data
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class EmbeddingResponse implements Serializable {
+
+ private String object;
+ private List- data;
+ private String model;
+ private Usage usage;
+}
diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/embeddings/Item.java b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/embeddings/Item.java
new file mode 100644
index 00000000..2ceb1c89
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/embeddings/Item.java
@@ -0,0 +1,16 @@
+package com.xmzs.common.chat.entity.embeddings;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.util.List;
+
+@Data
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class Item implements Serializable {
+ private String object;
+ private List
embedding;
+ private Integer index;
+}
diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/engines/Engine.java b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/engines/Engine.java
new file mode 100644
index 00000000..1f8e552d
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/engines/Engine.java
@@ -0,0 +1,25 @@
+package com.xmzs.common.chat.entity.engines;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 描述:
+ *
+ * @author https:www.unfbx.com
+ * 2023-02-15
+ */
+@Data
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class Engine implements Serializable {
+
+ private String id;
+ private String object;
+ private String owner;
+ private boolean ready;
+ private Object permissions;
+ private long created;
+
+}
diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/files/File.java b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/files/File.java
new file mode 100644
index 00000000..581cd297
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/files/File.java
@@ -0,0 +1,28 @@
+package com.xmzs.common.chat.entity.files;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 描述:
+ *
+ * @author https:www.unfbx.com
+ * 2023-02-15
+ */
+@Data
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class File implements Serializable {
+
+ private String id;
+ private String object;
+ private long bytes;
+ private long created_at;
+ private String filename;
+ private String purpose;
+ private String status;
+ @JsonProperty("status_details")
+ private String statusDetails;
+}
diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/files/UploadFileResponse.java b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/files/UploadFileResponse.java
new file mode 100644
index 00000000..00276ade
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/files/UploadFileResponse.java
@@ -0,0 +1,17 @@
+package com.xmzs.common.chat.entity.files;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 描述:
+ *
+ * @author https:www.unfbx.com
+ * 2023-02-15
+ */
+@Data
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class UploadFileResponse extends File implements Serializable {
+}
diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/fineTune/Event.java b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/fineTune/Event.java
new file mode 100644
index 00000000..af4a0d50
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/fineTune/Event.java
@@ -0,0 +1,17 @@
+package com.xmzs.common.chat.entity.fineTune;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+
+@Data
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class Event implements Serializable {
+ private String object;
+ @JsonProperty("created_at")
+ private long createdAt;
+ private String level;
+ private String message;
+}
diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/fineTune/FineTune.java b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/fineTune/FineTune.java
new file mode 100644
index 00000000..29d4e5a5
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/fineTune/FineTune.java
@@ -0,0 +1,123 @@
+package com.xmzs.common.chat.entity.fineTune;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.xmzs.common.chat.openai.exception.CommonError;
+import lombok.*;
+import lombok.extern.slf4j.Slf4j;
+import com.xmzs.common.core.exception.base.BaseException;
+
+
+import java.io.Serializable;
+import java.util.List;
+import java.util.Objects;
+
+@Getter
+@Slf4j
+@Builder
+@JsonInclude(JsonInclude.Include.NON_NULL)
+@NoArgsConstructor
+@AllArgsConstructor
+public class FineTune implements Serializable {
+
+ /**
+ * 上传的文件ID
+ */
+ @NonNull
+ @JsonProperty("training_file")
+ private String trainingFile;
+
+ @JsonProperty("validation_file")
+ private String validationFile;
+ /**
+ * 参考
+ * @see Model
+ */
+ private String model;
+
+ @JsonProperty("n_epochs")
+ @Builder.Default
+ private Integer n_epochs = 4;
+
+ @JsonProperty("batch_size")
+ private Integer batchSize;
+
+ @JsonProperty("learning_rate_multiplier")
+ private Double learningRateMultiplier;
+
+ @JsonProperty("prompt_loss_weight")
+ @Builder.Default
+ private Double promptLossWeight = 0.01;
+
+ @JsonProperty("compute_classification_metrics")
+ @Builder.Default
+ private boolean computeClassificationMetrics = false;
+
+ @JsonProperty("classification_n_classes")
+ private Integer classificationNClasses;
+
+ @JsonProperty("classification_betas")
+ private List classificationBetas;
+
+ private String suffix;
+
+ public void setTrainingFile(String trainingFile) {
+ this.trainingFile = trainingFile;
+ }
+
+ public void setValidationFile(String validationFile) {
+ this.validationFile = validationFile;
+ }
+
+ public void setModel(String model) {
+ this.model = model;
+ }
+
+ public void setN_epochs(Integer n_epochs) {
+ this.n_epochs = n_epochs;
+ }
+
+ public void setBatchSize(Integer batchSize) {
+ this.batchSize = batchSize;
+ }
+
+ public void setLearningRateMultiplier(Double learningRateMultiplier) {
+ this.learningRateMultiplier = learningRateMultiplier;
+ }
+
+ public void setPromptLossWeight(Double promptLossWeight) {
+ this.promptLossWeight = promptLossWeight;
+ }
+
+ public void setComputeClassificationMetrics(boolean computeClassificationMetrics) {
+ this.computeClassificationMetrics = computeClassificationMetrics;
+ }
+
+ public void setClassificationNClasses(Integer classificationNClasses) {
+ this.classificationNClasses = classificationNClasses;
+ }
+
+ public void setClassificationBetas(List classificationBetas) {
+ this.classificationBetas = classificationBetas;
+ }
+
+ public void setSuffix(String suffix) {
+ if(Objects.nonNull(suffix) && !"".equals(suffix) && suffix.length() > 40){
+ log.error("后缀长度不能大于40");
+ throw new BaseException(CommonError.PARAM_ERROR.msg());
+ }
+ this.suffix = suffix;
+ }
+
+ @Getter
+ @AllArgsConstructor
+ public enum Model {
+ // or a fine-tuned model created after 2022-04-21.
+ ADA("ada"),
+ BABBAGE("babbage"),
+ CURIE("curie"),
+ DAVINCI("davinci"),
+ ;
+ private String name;
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/fineTune/FineTuneDeleteResponse.java b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/fineTune/FineTuneDeleteResponse.java
new file mode 100644
index 00000000..ddb787ac
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/fineTune/FineTuneDeleteResponse.java
@@ -0,0 +1,18 @@
+package com.xmzs.common.chat.entity.fineTune;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import lombok.Data;
+
+import java.io.Serializable;
+
+@Data
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class FineTuneDeleteResponse implements Serializable {
+
+ private String id;
+
+ private String object;
+
+ private boolean deleted;
+
+}
diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/fineTune/FineTuneResponse.java b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/fineTune/FineTuneResponse.java
new file mode 100644
index 00000000..2753fcaa
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/fineTune/FineTuneResponse.java
@@ -0,0 +1,49 @@
+package com.xmzs.common.chat.entity.fineTune;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.List;
+
+@Data
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class FineTuneResponse implements Serializable {
+
+ private String id;
+
+ private String object;
+
+ private String model;
+
+ @JsonProperty("created_at")
+ private long createdAt;
+
+ private List events;
+
+ @JsonProperty("fine_tuned_model")
+ private String fineTunedModel;
+
+ @JsonProperty("hyperparams")
+ private HyperParam hyperParams;
+
+ @JsonProperty("organization_id")
+ private String organizationId;
+
+ @JsonProperty("result_files")
+ private List resultFiles;
+
+ private String status;
+
+ @JsonProperty("validation_files")
+ private List validationFiles;
+
+ @JsonProperty("training_files")
+ private List trainingFiles;
+
+ @JsonProperty("updated_at")
+ private long updatedAt;
+
+
+}
diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/fineTune/HyperParam.java b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/fineTune/HyperParam.java
new file mode 100644
index 00000000..1aca22d8
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/fineTune/HyperParam.java
@@ -0,0 +1,21 @@
+package com.xmzs.common.chat.entity.fineTune;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+
+@Data
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class HyperParam implements Serializable {
+
+ @JsonProperty("batch_size")
+ private Integer batchSize;
+ @JsonProperty("learning_rate_multiplier")
+ private Double learningRateMultiplier;
+ @JsonProperty("n_epochs")
+ private Integer nEpochs;
+ @JsonProperty("prompt_loss_weight")
+ private Double promptLossWeight;
+}
diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/fineTune/TrainingFile.java b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/fineTune/TrainingFile.java
new file mode 100644
index 00000000..e0736b80
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/fineTune/TrainingFile.java
@@ -0,0 +1,23 @@
+package com.xmzs.common.chat.entity.fineTune;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+
+@Data
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class TrainingFile implements Serializable {
+
+ private String id;
+ private String object;
+ private long bytes;
+ @JsonProperty("created_at")
+ private long createdAt;
+ private String filename;
+ private String purpose;
+ private String status;
+ @JsonProperty("status_details")
+ private String statusDetails;
+}
diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/images/Image.java b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/images/Image.java
new file mode 100644
index 00000000..76b75d56
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/images/Image.java
@@ -0,0 +1,108 @@
+package com.xmzs.common.chat.entity.images;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.*;
+import lombok.extern.slf4j.Slf4j;
+
+import java.io.Serializable;
+
+/**
+ * 描述:
+ *
+ * @author https:www.unfbx.com
+ * 2023-02-15
+ */
+@Getter
+@Slf4j
+@Builder
+@JsonInclude(JsonInclude.Include.NON_NULL)
+@NoArgsConstructor
+@AllArgsConstructor
+public class Image implements Serializable {
+
+ /**
+ * 提示词:dall-e-2支持1000字符、dall-e-3支持4000字符
+ */
+ private String prompt;
+ /**
+ * 支持dall-e-2、dall-e-3
+ *
+ * @see Model
+ */
+ @Builder.Default
+ private String model = Model.DALL_E_3.getName();
+
+ /**
+ * 此参数仅仅dall-e-3,默认值:standard
+ *
+ * @see Quality
+ */
+ private String quality;
+
+ /**
+ * 为每个提示生成的个数,dall-e-3只能为1。
+ */
+ private Integer n;
+ /**
+ * 图片尺寸,默认值:1024x1024
+ * dall-e-2支持:256x256, 512x512, or 1024x1024
+ * dall-e-3支持:1024x1024, 1792x1024, or 1024x1792
+ *
+ * @see SizeEnum
+ */
+ private String size;
+ /**
+ * 此参数仅仅dall-e-3,取值范围:vivid、natural
+ * 默认值:vivid
+ *
+ * @see Style
+ */
+ private String style;
+
+ /**
+ * 生成图片格式:url、b64_json
+ *
+ * @see ResponseFormat
+ */
+ @JsonProperty("response_format")
+ private String responseFormat;
+
+ private String user;
+
+ /**
+ * 图片生成模型
+ */
+ @Getter
+ @AllArgsConstructor
+ public enum Model {
+ DALL_E_2("dall-e-2"),
+ DALL_E_3("dall-e-3"),
+ ;
+ private final String name;
+ }
+
+ /**
+ * 生成图片质量
+ */
+ @Getter
+ @AllArgsConstructor
+ public enum Quality {
+ STANDARD("standard"),
+ HD("hd"),
+ ;
+ private final String name;
+ }
+
+ /**
+ * 生成图片风格
+ */
+ @Getter
+ @AllArgsConstructor
+ public enum Style {
+ VIVID("vivid"),
+ NATURAL("natural"),
+ ;
+ private final String name;
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/images/ImageEdit.java b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/images/ImageEdit.java
new file mode 100644
index 00000000..d1e87681
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/images/ImageEdit.java
@@ -0,0 +1,99 @@
+package com.xmzs.common.chat.entity.images;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.xmzs.common.chat.openai.exception.CommonError;
+import lombok.*;
+import lombok.extern.slf4j.Slf4j;
+import com.xmzs.common.core.exception.base.BaseException;
+
+
+import java.io.Serializable;
+import java.util.Objects;
+
+/**
+ * 描述:
+ *
+ * @author https:www.unfbx.com
+ * 2023-02-15
+ */
+@Getter
+@Slf4j
+@Builder
+@JsonInclude(JsonInclude.Include.NON_NULL)
+@NoArgsConstructor
+@AllArgsConstructor
+public class ImageEdit implements Serializable {
+ /**
+ * 必选项:描述文字,最多1000字符
+ */
+ @NonNull
+ private String prompt;
+ /**
+ * 为每个提示生成的完成次数。
+ */
+ @Builder.Default
+ private Integer n = 1;
+ /**
+ * 256x256
+ * 512x512
+ * 1024x1024
+ */
+ @Builder.Default
+ private String size = SizeEnum.size_512.getName();
+
+ @JsonProperty("response_format")
+ @Builder.Default
+ private String responseFormat = ResponseFormat.URL.getName();
+
+ private String user;
+
+ public ImageEdit setN(Integer n) {
+ if(n < 1){
+ log.warn("n最小值1");
+ n = 1;
+ }
+ if(n > 10){
+ log.warn("n最大值10");
+ n = 10;
+ }
+ this.n = n;
+ return this;
+ }
+
+ public ImageEdit setPrompt(String prompt) {
+ if(Objects.isNull(prompt) || "".equals(prompt)){
+ log.error("参数异常");
+ throw new BaseException(CommonError.PARAM_ERROR.msg());
+ }
+ if(prompt.length() > 1000){
+ log.error("长度超过1000");
+ throw new BaseException(CommonError.PARAM_ERROR.msg());
+ }
+ this.prompt = prompt;
+ return this;
+ }
+
+ public ImageEdit setSize(SizeEnum size) {
+ if(Objects.isNull(size)){
+ size = SizeEnum.size_512;
+ }
+ this.size = size.getName();
+ return this;
+ }
+
+ public ImageEdit setResponseFormat(ResponseFormat responseFormat) {
+ if(Objects.isNull(responseFormat)){
+ responseFormat = ResponseFormat.URL;
+ }
+ this.responseFormat = responseFormat.getName();
+ return this;
+ }
+
+ public ImageEdit setUser(String user) {
+ this.user = user;
+ return this;
+ }
+
+
+}
diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/images/ImageResponse.java b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/images/ImageResponse.java
new file mode 100644
index 00000000..1d2b5a08
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/images/ImageResponse.java
@@ -0,0 +1,20 @@
+package com.xmzs.common.chat.entity.images;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * 描述:
+ *
+ * @author https:www.unfbx.com
+ * 2023-02-15
+ */
+@Data
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class ImageResponse implements Serializable {
+ private long created;
+ private List- data;
+}
diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/images/ImageVariations.java b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/images/ImageVariations.java
new file mode 100644
index 00000000..58018db7
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/images/ImageVariations.java
@@ -0,0 +1,81 @@
+package com.xmzs.common.chat.entity.images;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+import java.io.Serializable;
+import java.util.Objects;
+
+/**
+ * 描述:
+ *
+ * @author https:www.unfbx.com
+ * 2023-02-15
+ */
+@Getter
+@Slf4j
+@Builder
+@JsonInclude(JsonInclude.Include.NON_NULL)
+@NoArgsConstructor
+@AllArgsConstructor
+public class ImageVariations implements Serializable {
+ /**
+ * 为每个提示生成的完成次数。
+ */
+ @Builder.Default
+ private Integer n = 1;
+ /**
+ * 256x256
+ * 512x512
+ * 1024x1024
+ */
+ @Builder.Default
+ private String size = SizeEnum.size_512.getName();
+
+ @JsonProperty("response_format")
+ @Builder.Default
+ private String responseFormat = ResponseFormat.URL.getName();
+
+ private String user;
+
+
+ public void setN(Integer n) {
+ if (n < 1) {
+ log.warn("n最小值1");
+ this.n = 1;
+ return;
+ }
+ if (n > 10) {
+ log.warn("n最大值10");
+ this.n = 10;
+ return;
+ }
+ this.n = n;
+ }
+
+
+ public void setSize(SizeEnum size) {
+ if (Objects.isNull(size)) {
+ size = SizeEnum.size_512;
+ }
+ this.size = size.getName();
+ }
+
+ public void setResponseFormat(ResponseFormat responseFormat) {
+ if (Objects.isNull(responseFormat)) {
+ responseFormat = ResponseFormat.URL;
+ }
+ this.responseFormat = responseFormat.getName();
+ }
+
+ public void setUser(String user) {
+ this.user = user;
+ }
+
+
+}
diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/images/Item.java b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/images/Item.java
new file mode 100644
index 00000000..c22afa44
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/images/Item.java
@@ -0,0 +1,21 @@
+package com.xmzs.common.chat.entity.images;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 描述:
+ *
+ * @author https:www.unfbx.com
+ * 2023-02-15
+ */
+@Data
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class Item implements Serializable {
+ private String url;
+ @JsonProperty("b64_json")
+ private String b64Json;
+}
diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/images/ResponseFormat.java b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/images/ResponseFormat.java
new file mode 100644
index 00000000..b75c6912
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/images/ResponseFormat.java
@@ -0,0 +1,22 @@
+package com.xmzs.common.chat.entity.images;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.io.Serializable;
+
+/**
+ * 描述:
+ *
+ * @author https:www.unfbx.com
+ * 2023-02-15
+ */
+@AllArgsConstructor
+@Getter
+public enum ResponseFormat implements Serializable {
+ URL("url"),
+ B64_JSON("b64_json"),
+ ;
+
+ private String name;
+}
diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/images/SizeEnum.java b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/images/SizeEnum.java
new file mode 100644
index 00000000..a72ef384
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/images/SizeEnum.java
@@ -0,0 +1,26 @@
+package com.xmzs.common.chat.entity.images;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.io.Serializable;
+
+/**
+ * 描述:
+ *
+ * @author https:www.unfbx.com
+ * 2023-02-15
+ */
+@Getter
+@AllArgsConstructor
+public enum SizeEnum implements Serializable {
+ size_1024_1792("1024x1792"),
+ size_1792_1024("1792x1024"),
+ size_1024("1024x1024"),
+ size_512("512x512"),
+ size_256("256x256"),
+
+ ;
+ private String name;
+
+}
diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/models/Model.java b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/models/Model.java
new file mode 100644
index 00000000..0b45b6fc
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/models/Model.java
@@ -0,0 +1,29 @@
+package com.xmzs.common.chat.entity.models;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * 描述:
+ *
+ * @author https:www.unfbx.com
+ * 2023-02-15
+ */
+@Data
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class Model implements Serializable {
+
+ private String id;
+ private String object;
+ private long created;
+ @JsonProperty("owned_by")
+ private String ownedBy;
+ @JsonProperty("permission")
+ private List
permission;
+ private String root;
+ private Object parent;
+}
diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/models/ModelResponse.java b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/models/ModelResponse.java
new file mode 100644
index 00000000..33aee8d8
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/models/ModelResponse.java
@@ -0,0 +1,20 @@
+package com.xmzs.common.chat.entity.models;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * 描述:
+ *
+ * @author https:www.unfbx.com
+ * 2023-02-15
+ */
+@Data
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class ModelResponse implements Serializable {
+ private String object;
+ private List data;
+}
diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/models/Permission.java b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/models/Permission.java
new file mode 100644
index 00000000..ffdbd526
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/models/Permission.java
@@ -0,0 +1,45 @@
+package com.xmzs.common.chat.entity.models;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 描述:
+ *
+ * @author https:www.unfbx.com
+ * 2023-02-15
+ */
+@Data
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class Permission implements Serializable {
+
+ private String id;
+ @JsonProperty("object")
+ private String object;
+ @JsonProperty("created")
+ private long created;
+ @JsonProperty("allow_create_engine")
+ private boolean allowCreateEngine;
+ @JsonProperty("allow_sampling")
+ private boolean allowSampling;
+ @JsonProperty("allow_logprobs")
+ private boolean allowLogprobs;
+ @JsonProperty("allow_search_indices")
+ private boolean allowSearchIndices;
+ @JsonProperty("allow_view")
+ private boolean allowView;
+ @JsonProperty("allow_fine_tuning")
+ private boolean allowFineTuning;
+ @JsonProperty("organization")
+ private String organization;
+ /**
+ * 不知道是什么类型的数据
+ */
+ @JsonProperty("group")
+ private Object group;
+ @JsonProperty("is_blocking")
+ private boolean isBlocking;
+}
diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/moderations/Categories.java b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/moderations/Categories.java
new file mode 100644
index 00000000..140195d9
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/moderations/Categories.java
@@ -0,0 +1,50 @@
+package com.xmzs.common.chat.entity.moderations;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 描述:
+ *
+ * @author https:www.unfbx.com
+ * 2023-02-15
+ */
+@Data
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class Categories implements Serializable {
+ /**
+ * 表达、煽动或宣扬基于种族、性别、民族、宗教、国籍、性取向、残疾状况或种姓的仇恨的内容。
+ */
+ private boolean hate;
+ /**
+ * 仇恨内容,还包括对目标群体的暴力或严重伤害。
+ */
+ @JsonProperty("hate/threatening")
+ private boolean hateThreatening;
+ /**
+ * 宣扬、鼓励或描绘自残行为(例如自杀、割伤和饮食失调)的内容。
+ */
+ @JsonProperty("self-harm")
+ private boolean selfHarm;
+ /**
+ * 旨在引起性兴奋的内容,例如对性活动的描述,或宣传性服务(不包括性教育和健康)的内容。
+ */
+ private boolean sexual;
+ /**
+ * 包含未满 18 周岁的个人的色情内容。
+ */
+ @JsonProperty("sexual/minors")
+ private boolean sexualMinors;
+ /**
+ * 宣扬或美化暴力或歌颂他人遭受苦难或羞辱的内容。
+ */
+ private boolean violence;
+ /**
+ * 以极端血腥细节描绘死亡、暴力或严重身体伤害的暴力内容。
+ */
+ @JsonProperty("violence/graphic")
+ private boolean violenceGraphic;
+}
diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/moderations/CategoryScores.java b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/moderations/CategoryScores.java
new file mode 100644
index 00000000..9efcdd2d
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/moderations/CategoryScores.java
@@ -0,0 +1,31 @@
+package com.xmzs.common.chat.entity.moderations;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+
+/**
+ * 描述:
+ *
+ * @author https:www.unfbx.com
+ * 2023-02-15
+ */
+@Data
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class CategoryScores implements Serializable {
+ private BigDecimal hate;
+ @JsonProperty("hate/threatening")
+ private BigDecimal hateThreatening;
+ @JsonProperty("self-harm")
+ private BigDecimal selfHarm;
+ private BigDecimal sexual;
+ @JsonProperty("sexual/minors")
+ private BigDecimal sexualMinors;
+ private BigDecimal violence;
+ @JsonProperty("violence/graphic")
+ private BigDecimal violenceGraphic;
+
+}
diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/moderations/Moderation.java b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/moderations/Moderation.java
new file mode 100644
index 00000000..193659a8
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/moderations/Moderation.java
@@ -0,0 +1,55 @@
+package com.xmzs.common.chat.entity.moderations;
+
+import com.xmzs.common.chat.openai.exception.CommonError;
+import lombok.*;
+import lombok.extern.slf4j.Slf4j;
+import com.xmzs.common.core.exception.base.BaseException;
+
+
+import java.io.Serializable;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * 描述:文本审核,敏感词鉴别
+ *
+ * @author https:www.unfbx.com
+ * 2023-02-15
+ */
+@Getter
+@Builder
+@Slf4j
+@NoArgsConstructor
+@AllArgsConstructor
+public class Moderation implements Serializable {
+
+ @NonNull
+ private List input;
+ @Builder.Default
+ private String model = Model.TEXT_MODERATION_LATEST.getName();
+
+ public void setInput(List input) {
+ if (Objects.isNull(input) || input.size() == 0) {
+ log.error("input不能为空");
+ throw new BaseException(CommonError.PARAM_ERROR.msg());
+ }
+ this.input = input;
+ }
+
+ public void setModel(Model model) {
+ if (Objects.isNull(model)) {
+ model = Model.TEXT_MODERATION_LATEST;
+ }
+ this.model = model.getName();
+ }
+
+ @Getter
+ @AllArgsConstructor
+ public enum Model {
+ TEXT_MODERATION_STABLE("text-moderation-stable"),
+ TEXT_MODERATION_LATEST("text-moderation-latest"),
+ ;
+
+ private String name;
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/moderations/ModerationResponse.java b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/moderations/ModerationResponse.java
new file mode 100644
index 00000000..91e00a94
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/moderations/ModerationResponse.java
@@ -0,0 +1,21 @@
+package com.xmzs.common.chat.entity.moderations;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * 描述:
+ *
+ * @author https:www.unfbx.com
+ * 2023-02-15
+ */
+@Data
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class ModerationResponse implements Serializable {
+ private String id;
+ private String model;
+ private List results;
+}
diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/moderations/Result.java b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/moderations/Result.java
new file mode 100644
index 00000000..12ab9ac7
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/moderations/Result.java
@@ -0,0 +1,22 @@
+package com.xmzs.common.chat.entity.moderations;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 描述:
+ *
+ * @author https:www.unfbx.com
+ * 2023-02-15
+ */
+@Data
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class Result implements Serializable {
+ private Categories categories;
+ @JsonProperty("category_scores")
+ private CategoryScores categoryScores;
+ private boolean flagged;
+}
diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/whisper/Transcriptions.java b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/whisper/Transcriptions.java
new file mode 100644
index 00000000..58032619
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/whisper/Transcriptions.java
@@ -0,0 +1,49 @@
+package com.xmzs.common.chat.entity.whisper;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.experimental.FieldNameConstants;
+
+/**
+ * @author Admin
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+@FieldNameConstants
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class Transcriptions extends Whisper {
+ /**
+ * 模型目前只支持这一种:WHISPER_1
+ */
+ @Builder.Default
+ private String model = Whisper.Model.WHISPER_1.getName();
+ /**
+ * 提示语,需要与语音语言匹配
+ */
+ private String prompt;
+ /**
+ * 输出的格式,采用以下选项之一:json、text、srt、verbose_json 或 vtt。
+ * 默认值:json
+ */
+ @JsonProperty("response_format")
+ @Builder.Default
+ private String responseFormat = ResponseFormat.JSON.getName();
+ /**
+ * 温度控制随机效果:0-1,值越大输出更加随机
+ * 默认值:0
+ */
+ @Builder.Default
+ private Double temperature = 0d;
+ /**
+ * 输入音频的语言,以 ISO-639-1 格式提供输入语言将提高准确性和延迟。
+ * 参考:ISO-639-1
+ */
+ private String language;
+
+}
diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/whisper/Translations.java b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/whisper/Translations.java
new file mode 100644
index 00000000..fe68f9b6
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/whisper/Translations.java
@@ -0,0 +1,41 @@
+package com.xmzs.common.chat.entity.whisper;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.experimental.FieldNameConstants;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+@FieldNameConstants
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class Translations {
+ /**
+ * 模型目前只支持这一种:WHISPER_1
+ */
+ @Builder.Default
+ private String model = Whisper.Model.WHISPER_1.getName();
+ /**
+ * 提示语,需要与语音语言匹配
+ */
+ private String prompt;
+ /**
+ * 输出的格式,采用以下选项之一:json、text、srt、verbose_json 或 vtt。
+ * 默认值:json
+ */
+ @JsonProperty("response_format")
+ @Builder.Default
+ private String responseFormat = Whisper.ResponseFormat.JSON.getName();
+ /**
+ * 温度控制随机效果:0-1,值越大输出更加随机
+ * 默认值:0
+ */
+ @Builder.Default
+ private double temperature = 0;
+
+}
diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/whisper/Whisper.java b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/whisper/Whisper.java
new file mode 100644
index 00000000..4e4f681b
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/whisper/Whisper.java
@@ -0,0 +1,38 @@
+package com.xmzs.common.chat.entity.whisper;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.Getter;
+
+import java.io.Serializable;
+
+/**
+ * 描述:语音转文字
+ *
+ * @author https:www.unfbx.com
+ * @since 2023-03-02
+ */
+@Data
+public class Whisper implements Serializable {
+
+
+ @Getter
+ @AllArgsConstructor
+ public enum Model {
+ WHISPER_1("whisper-1"),
+ ;
+ private String name;
+ }
+
+ @Getter
+ @AllArgsConstructor
+ public enum ResponseFormat {
+ JSON("json"),
+ TEXT("text"),
+ SRT("srt"),
+ VERBOSE_JSON("verbose_json"),
+ VTT("vtt"),
+ ;
+ private String name;
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/whisper/WhisperResponse.java b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/whisper/WhisperResponse.java
new file mode 100644
index 00000000..183207f1
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/whisper/WhisperResponse.java
@@ -0,0 +1,19 @@
+package com.xmzs.common.chat.entity.whisper;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 描述:
+ *
+ * @author https:www.unfbx.com
+ * @since 2023-03-02
+ */
+@Data
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class WhisperResponse implements Serializable {
+
+ private String text;
+}
diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/handler/PlusWebSocketHandler.java b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/handler/PlusWebSocketHandler.java
new file mode 100644
index 00000000..96c17de0
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/handler/PlusWebSocketHandler.java
@@ -0,0 +1,162 @@
+package com.xmzs.common.chat.handler;
+
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.json.JSONUtil;
+import com.alibaba.fastjson2.JSONObject;
+import com.xmzs.common.core.utils.SpringUtils;
+import com.xmzs.common.chat.config.LocalCache;
+import com.xmzs.common.chat.entity.chat.ChatCompletion;
+import com.xmzs.common.chat.holder.WebSocketSessionHolder;
+import com.xmzs.common.chat.listener.WebSocketEventListener;
+import com.xmzs.common.chat.openai.OpenAiStreamClient;
+import com.xmzs.common.chat.entity.chat.Message;
+import com.xmzs.common.chat.utils.WebSocketUtils;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.web.socket.*;
+import org.springframework.web.socket.handler.AbstractWebSocketHandler;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+
+/**
+ * WebSocketHandler 实现类
+ *
+ * @author zendwang
+ */
+@Slf4j
+public class PlusWebSocketHandler extends AbstractWebSocketHandler {
+
+ /**
+ * 是否开启文本审核
+ */
+ @Value("${baidu.enabled}")
+ private Boolean enabled;
+
+ /**
+ * 连接成功后
+ */
+ @Override
+ public void afterConnectionEstablished(WebSocketSession session) {
+ WebSocketSessionHolder.addSession(session.getId(), session);
+ }
+
+ /**
+ * 处理发送来的文本消息
+ *
+ * @param session
+ * @param message
+ */
+ @Override
+ protected void handleTextMessage(WebSocketSession session, TextMessage message) {
+// if(enabled){
+// // 判断文本是否合规
+// TextReviewService textReviewService=(TextReviewService) SpringUtils.context().getBean("textReviewService");
+// String type = textReviewService.textReview(message.getPayload());
+// // 审核状态 1 代表合法
+// String conclusionType = "1";
+// if (!conclusionType.equals(type) && StringUtils.isNotEmpty(type)) {
+// HashMap msgMap = new HashMap<>(10);
+// msgMap.put("content", "文本不合规,请修改!");
+// String jsonStr = JSONUtil.toJsonStr(msgMap);
+// WebSocketUtils.sendMessage(session, jsonStr);
+// WebSocketUtils.sendMessage(session, "[DONE]");
+// return;
+// }
+// }
+ WebSocketEventListener eventSourceListener = new WebSocketEventListener(session);
+ String messageContext = (String) LocalCache.CACHE.get(session.getId());
+ List messages = new ArrayList<>();
+ if (StrUtil.isNotBlank(messageContext)) {
+ messages = JSONUtil.toList(messageContext, Message.class);
+ // 上下文长度
+ int contextSize=10;
+ if (messages.size() >= contextSize) {
+ messages = messages.subList(1, contextSize);
+ }
+ Message currentMessage = Message.builder().content(message.getPayload()).role(Message.Role.USER).build();
+ messages.add(currentMessage);
+ } else {
+ Message currentMessage = Message.builder().content(message.getPayload()).role(Message.Role.USER).build();
+ messages.add(currentMessage);
+ }
+ ChatCompletion chatCompletion = ChatCompletion
+ .builder()
+ .model(ChatCompletion.Model.GPT_3_5_TURBO.getName())
+ .messages(messages)
+ .temperature(0.2)
+ .stream(true)
+ .build();
+ OpenAiStreamClient openAiStreamClient=(OpenAiStreamClient) SpringUtils.context().getBean("openAiStreamClient");
+ openAiStreamClient.streamChatCompletion(chatCompletion, eventSourceListener);
+ LocalCache.CACHE.put(session.getId(), JSONUtil.toJsonStr(messages), LocalCache.TIMEOUT);
+ }
+
+ /**
+ * 根据key获取Value值
+ *
+ * @Date 2023/7/27
+ * @param jsonObject
+ * @param key
+ * @param defaultValue
+ * @return String
+ **/
+ public String getValue(JSONObject jsonObject,String key,String defaultValue){
+ String value = (String)jsonObject.get(key);
+ if(StrUtil.isEmpty(value)){
+ return defaultValue;
+ }
+ return value;
+ }
+
+ @Override
+ protected void handleBinaryMessage(WebSocketSession session, BinaryMessage message) throws Exception {
+ super.handleBinaryMessage(session, message);
+ }
+
+ /**
+ * 心跳监测的回复
+ *
+ * @param session
+ * @param message
+ * @throws Exception
+ */
+ @Override
+ protected void handlePongMessage(WebSocketSession session, PongMessage message) throws Exception {
+ WebSocketUtils.sendPongMessage(session);
+ }
+
+ /**
+ * 连接出错时
+ *
+ * @param session
+ * @param exception
+ * @throws Exception
+ */
+ @Override
+ public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
+ log.error("[transport error] sessionId: {} , exception:{}", session.getId(), exception.getMessage());
+ }
+
+ /**
+ * 连接关闭后
+ *
+ * @param session
+ * @param status
+ */
+ @Override
+ public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
+ WebSocketSessionHolder.removeSession(session.getId());
+ }
+
+ /**
+ * 是否支持分片消息
+ *
+ * @return
+ */
+ @Override
+ public boolean supportsPartialMessages() {
+ return false;
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/holder/WebSocketSessionHolder.java b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/holder/WebSocketSessionHolder.java
new file mode 100644
index 00000000..d04a0bb7
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/holder/WebSocketSessionHolder.java
@@ -0,0 +1,37 @@
+package com.xmzs.common.chat.holder;
+
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+import org.springframework.web.socket.WebSocketSession;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * WebSocketSession 用于保存当前所有在线的会话信息
+ *
+ * @author zendwang
+ */
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public class WebSocketSessionHolder {
+
+ private static final Map USER_SESSION_MAP = new ConcurrentHashMap<>();
+
+ public static void addSession(String sessionKey, WebSocketSession session) {
+ USER_SESSION_MAP.put(sessionKey, session);
+ }
+
+ public static void removeSession(String sessionKey) {
+ if (USER_SESSION_MAP.containsKey(sessionKey)) {
+ USER_SESSION_MAP.remove(sessionKey);
+ }
+ }
+
+ public static WebSocketSession getSessions(Long sessionKey) {
+ return USER_SESSION_MAP.get(sessionKey);
+ }
+
+ public static Boolean existSession(Long sessionKey) {
+ return USER_SESSION_MAP.containsKey(sessionKey);
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/interceptor/PlusWebSocketInterceptor.java b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/interceptor/PlusWebSocketInterceptor.java
new file mode 100644
index 00000000..49f24672
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/interceptor/PlusWebSocketInterceptor.java
@@ -0,0 +1,46 @@
+package com.xmzs.common.chat.interceptor;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.server.ServerHttpRequest;
+import org.springframework.http.server.ServerHttpResponse;
+import org.springframework.web.socket.WebSocketHandler;
+import org.springframework.web.socket.server.HandshakeInterceptor;
+
+import java.util.Map;
+
+/**
+ * WebSocket握手请求的拦截器
+ *
+ * @author zendwang
+ */
+@Slf4j
+public class PlusWebSocketInterceptor implements HandshakeInterceptor {
+
+ /**
+ * 握手前
+ *
+ * @param request request
+ * @param response response
+ * @param wsHandler wsHandler
+ * @param attributes attributes
+ * @return 是否握手成功
+ */
+ @Override
+ public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map attributes) {
+ return true;
+ }
+
+
+ /**
+ * 握手后
+ *
+ * @param request request
+ * @param response response
+ * @param wsHandler wsHandler
+ * @param exception 异常
+ */
+ @Override
+ public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) {
+
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/listener/SSEEventSourceListener.java b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/listener/SSEEventSourceListener.java
new file mode 100644
index 00000000..2ec6b966
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/listener/SSEEventSourceListener.java
@@ -0,0 +1,81 @@
+package com.xmzs.common.chat.listener;
+
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.xmzs.common.chat.config.LocalCache;
+import com.xmzs.common.chat.entity.chat.ChatChoice;
+import com.xmzs.common.chat.entity.chat.ChatCompletionResponse;
+import com.xmzs.common.core.utils.StringUtils;
+import lombok.AllArgsConstructor;
+import lombok.SneakyThrows;
+import lombok.extern.slf4j.Slf4j;
+import okhttp3.Response;
+import okhttp3.ResponseBody;
+import okhttp3.sse.EventSource;
+import okhttp3.sse.EventSourceListener;
+
+import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitter;
+
+import java.util.Objects;
+
+/**
+ * 描述:OpenAIEventSourceListener
+ *
+ * @author https:www.unfbx.com
+ * @date 2023-02-22
+ */
+@Slf4j
+@AllArgsConstructor
+public class SSEEventSourceListener extends EventSourceListener {
+
+ private static final String DONE_SIGNAL = "[DONE]";
+
+ private final ResponseBodyEmitter emitter;
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onOpen(EventSource eventSource, Response response) {
+ log.info("OpenAI建立sse连接...");
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @SneakyThrows
+ @Override
+ public void onEvent(EventSource eventSource, String id, String type, String data) {
+ try {
+ log.info("响应数据{}=========",data);
+ emitter.send(data);
+ if (data.equals(DONE_SIGNAL)) {
+ //成功响应
+ emitter.complete();
+ }
+ } catch (Exception e) {
+ log.error("sse信息推送失败!");
+ eventSource.cancel();
+ }
+ }
+
+ @Override
+ public void onClosed(EventSource eventSource) {
+ log.info("OpenAI关闭sse连接...");
+ }
+
+ @SneakyThrows
+ @Override
+ public void onFailure(EventSource eventSource, Throwable t, Response response) {
+ if (Objects.isNull(response)) {
+ return;
+ }
+ ResponseBody body = response.body();
+ if (Objects.nonNull(body)) {
+ log.error("OpenAI sse连接异常data:{},异常:{}", body.string(), t);
+ } else {
+ log.error("OpenAI sse连接异常data:{},异常:{}", response, t);
+ }
+ eventSource.cancel();
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/listener/WebSocketEventListener.java b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/listener/WebSocketEventListener.java
new file mode 100644
index 00000000..027cd0df
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/listener/WebSocketEventListener.java
@@ -0,0 +1,94 @@
+package com.xmzs.common.chat.listener;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import com.xmzs.common.chat.constant.OpenAIConst;
+import lombok.SneakyThrows;
+import lombok.extern.slf4j.Slf4j;
+import okhttp3.Response;
+import okhttp3.ResponseBody;
+import okhttp3.sse.EventSource;
+import okhttp3.sse.EventSourceListener;
+import com.xmzs.common.chat.entity.chat.ChatCompletionResponse;
+import org.springframework.web.socket.TextMessage;
+import org.springframework.web.socket.WebSocketSession;
+
+import java.util.Objects;
+
+/**
+ * 描述:OpenAI流式输出Socket接收
+ *
+ * @author https:www.unfbx.com
+ * @date 2023-03-23
+ */
+@Slf4j
+public class WebSocketEventListener extends EventSourceListener {
+
+ private WebSocketSession session;
+
+ /**
+ * 消息结束标识
+ */
+ private final String msgEnd = "[DONE]";
+
+ public WebSocketEventListener(WebSocketSession session) {
+ this.session = session;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onOpen(EventSource eventSource, Response response) {
+ log.info("OpenAI建立Socket连接...");
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @SneakyThrows
+ @Override
+ public void onEvent(EventSource eventSource, String id, String type, String data) {
+ log.info("OpenAI返回数据:{}", data);
+ if (data.equals(msgEnd)) {
+ log.info("OpenAI返回数据结束了");
+ session.sendMessage(new TextMessage(msgEnd));
+ return;
+ }
+ ObjectMapper mapper = new ObjectMapper();
+ // 读取Json
+ ChatCompletionResponse completionResponse = mapper.readValue(data, ChatCompletionResponse.class);
+ String delta = mapper.writeValueAsString(completionResponse.getChoices().get(0).getDelta());
+ session.sendMessage(new TextMessage(delta));
+ }
+
+
+ @Override
+ public void onClosed(EventSource eventSource) {
+ log.info("OpenAI关闭Socket连接...");
+ }
+
+
+ @SneakyThrows
+ @Override
+ public void onFailure(EventSource eventSource, Throwable t, Response response) {
+ if (Objects.isNull(response)) {
+ return;
+ }
+ ResponseBody body = response.body();
+ if (Objects.nonNull(body)) {
+ // 返回非流式回复内容
+ if(response.code() == OpenAIConst.SUCCEED_CODE){
+ ObjectMapper mapper = new ObjectMapper();
+ ChatCompletionResponse completionResponse = mapper.readValue(body.string(), ChatCompletionResponse.class);
+ String delta = mapper.writeValueAsString(completionResponse.getChoices().get(0).getMessage().getContent());
+ session.sendMessage(new TextMessage(delta));
+ }else {
+ log.error("Socket连接异常data:{},异常:{}", body.string(), t);
+ }
+ } else {
+ log.error("Socket连接异常data:{},异常:{}", response, t);
+ }
+ eventSource.cancel();
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/listener/WebSocketTopicListener.java b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/listener/WebSocketTopicListener.java
new file mode 100644
index 00000000..632dda03
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/listener/WebSocketTopicListener.java
@@ -0,0 +1,38 @@
+package com.xmzs.common.chat.listener;
+
+import cn.hutool.core.collection.CollUtil;
+import com.xmzs.common.chat.holder.WebSocketSessionHolder;
+import com.xmzs.common.chat.utils.WebSocketUtils;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.ApplicationArguments;
+import org.springframework.boot.ApplicationRunner;
+import org.springframework.core.Ordered;
+
+/**
+ * WebSocket 主题订阅监听器
+ *
+ * @author zendwang
+ */
+@Slf4j
+public class WebSocketTopicListener implements ApplicationRunner, Ordered {
+
+ @Override
+ public void run(ApplicationArguments args) throws Exception {
+ WebSocketUtils.subscribeMessage((message) -> {
+ log.info("WebSocket主题订阅收到消息session keys={} message={}!", message.getSessionKeys(), message.getMessage());
+ if (CollUtil.isNotEmpty(message.getSessionKeys())) {
+ message.getSessionKeys().forEach(key -> {
+ if (WebSocketSessionHolder.existSession(key)) {
+ WebSocketUtils.sendMessage(key, message.getMessage());
+ }
+ });
+ }
+ });
+ log.info("初始化WebSocket主题订阅监听器成功");
+ }
+
+ @Override
+ public int getOrder() {
+ return -1;
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/openai/OpenAiApi.java b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/openai/OpenAiApi.java
new file mode 100644
index 00000000..d3e77191
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/openai/OpenAiApi.java
@@ -0,0 +1,343 @@
+package com.xmzs.common.chat.openai;
+
+import com.xmzs.common.chat.entity.chat.ChatCompletionWithPicture;
+import io.reactivex.Single;
+import okhttp3.MultipartBody;
+import okhttp3.RequestBody;
+import okhttp3.ResponseBody;
+
+import com.xmzs.common.chat.entity.billing.BillingUsage;
+import com.xmzs.common.chat.entity.billing.CreditGrantsResponse;
+import com.xmzs.common.chat.entity.billing.Subscription;
+import com.xmzs.common.chat.entity.chat.ChatCompletion;
+import com.xmzs.common.chat.entity.chat.ChatCompletionResponse;
+import com.xmzs.common.chat.entity.common.DeleteResponse;
+import com.xmzs.common.chat.entity.common.OpenAiResponse;
+import com.xmzs.common.chat.entity.completions.Completion;
+import com.xmzs.common.chat.entity.completions.CompletionResponse;
+import com.xmzs.common.chat.entity.edits.Edit;
+import com.xmzs.common.chat.entity.edits.EditResponse;
+import com.xmzs.common.chat.entity.embeddings.Embedding;
+import com.xmzs.common.chat.entity.embeddings.EmbeddingResponse;
+import com.xmzs.common.chat.entity.engines.Engine;
+import com.xmzs.common.chat.entity.files.File;
+import com.xmzs.common.chat.entity.files.UploadFileResponse;
+import com.xmzs.common.chat.entity.fineTune.Event;
+import com.xmzs.common.chat.entity.fineTune.FineTune;
+import com.xmzs.common.chat.entity.fineTune.FineTuneDeleteResponse;
+import com.xmzs.common.chat.entity.fineTune.FineTuneResponse;
+import com.xmzs.common.chat.entity.images.Image;
+import com.xmzs.common.chat.entity.images.ImageResponse;
+import com.xmzs.common.chat.entity.models.Model;
+import com.xmzs.common.chat.entity.models.ModelResponse;
+import com.xmzs.common.chat.entity.moderations.Moderation;
+import com.xmzs.common.chat.entity.moderations.ModerationResponse;
+import com.xmzs.common.chat.entity.whisper.WhisperResponse;
+import retrofit2.http.*;
+
+import java.time.LocalDate;
+import java.util.Map;
+
+/**
+ * 描述: open ai官方api接口
+ *
+ * @author https:www.unfbx.com
+ * 2023-02-15
+ */
+public interface OpenAiApi {
+
+ /**
+ * 模型列表
+ *
+ * @return Single ModelResponse
+ */
+ @GET("v1/models")
+ Single models();
+
+ /**
+ * models 返回的数据id
+ *
+ * @param id 模型主键
+ * @return Single Model
+ */
+ @GET("v1/models/{id}")
+ Single model(@Path("id") String id);
+
+ /**
+ * 文本问答
+ * Given a prompt, the model will return one or more predicted completions, and can also return the probabilities of alternative tokens at each position.
+ *
+ * @param completion 问答参数
+ * @return Single CompletionResponse
+ */
+ @POST("v1/completions")
+ Single completions(@Body Completion completion);
+
+ /**
+ * Creates a new edit for the provided input, instruction, and parameters.
+ * 文本修复
+ *
+ * @param edit 编辑参数
+ * @return Single EditResponse
+ */
+ @POST("v1/edits")
+ Single edits(@Body Edit edit);
+
+ /**
+ * Creates an image given a prompt.
+ * 根据描述生成图片
+ *
+ * @param image 图片对象
+ * @return Single ImageResponse
+ */
+ @POST("v1/images/generations")
+ Single genImages(@Body Image image);
+
+ /**
+ * Creates an edited or extended image given an original image and a prompt.
+ * 根据描述修改图片
+ *
+ * @param image 图片对象
+ * @param mask 图片对象
+ * @param requestBodyMap 请求参数
+ * @return Single ImageResponse
+ */
+ @Multipart
+ @POST("v1/images/edits")
+ Single editImages(@Part() MultipartBody.Part image,
+ @Part() MultipartBody.Part mask,
+ @PartMap() Map requestBodyMap
+ );
+
+ /**
+ * Creates a variation of a given image.
+ *
+ * @param image 图片对象
+ * @param requestBodyMap 请求参数
+ * @return Single ImageResponse
+ */
+ @Multipart
+ @POST("v1/images/variations")
+ Single variationsImages(@Part() MultipartBody.Part image,
+ @PartMap() Map requestBodyMap
+ );
+
+ /**
+ * 文本向量计算
+ *
+ * @param embedding 向量参数
+ * @return Single EmbeddingResponse
+ */
+ @POST("v1/embeddings")
+ Single embeddings(@Body Embedding embedding);
+
+
+ /**
+ * Returns a list of files that belong to the user's organization.
+ *
+ * @return Single OpenAiResponse File
+ */
+ @GET("/v1/files")
+ Single> files();
+
+ /**
+ * 删除文件
+ *
+ * @param fileId 文件id
+ * @return Single DeleteResponse
+ */
+ @DELETE("v1/files/{file_id}")
+ Single deleteFile(@Path("file_id") String fileId);
+
+ /**
+ * 上传文件
+ *
+ * @param purpose purpose
+ * @param file 文件对象
+ * @return Single UploadFileResponse
+ */
+ @Multipart
+ @POST("v1/files")
+ Single uploadFile(@Part MultipartBody.Part file,
+ @Part("purpose") RequestBody purpose);
+
+
+ /**
+ * 检索文件
+ *
+ * @param fileId 文件id
+ * @return Single File
+ */
+ @GET("v1/files/{file_id}")
+ Single retrieveFile(@Path("file_id") String fileId);
+
+ /**
+ * 检索文件内容
+ * ###不对免费用户开放###
+ * ###不对免费用户开放###
+ * ###不对免费用户开放###
+ *
+ * @param fileId 文件id
+ * @return Single ResponseBody
+ */
+ @Streaming
+ @GET("v1/files/{file_id}/content")
+ Single retrieveFileContent(@Path("file_id") String fileId);
+
+
+ /**
+ * 文本审核
+ *
+ * @param moderation 文本审核参数
+ * @return Single ModerationResponse
+ */
+ @POST("v1/moderations")
+ Single moderations(@Body Moderation moderation);
+
+
+ /**
+ * 创建微调作业
+ *
+ * @param fineTune 微调
+ * @return Single FineTuneResponse
+ */
+ @POST("v1/fine-tunes")
+ Single fineTune(@Body FineTune fineTune);
+
+ /**
+ * 微调作业集合
+ *
+ * @return Single OpenAiResponse FineTuneResponse
+ */
+ @GET("v1/fine-tunes")
+ Single> fineTunes();
+
+
+ /**
+ * 检索微调作业
+ *
+ * @return Single FineTuneResponse
+ */
+ @GET("v1/fine-tunes/{fine_tune_id}")
+ Single retrieveFineTune(@Path("fine_tune_id") String fineTuneId);
+
+ /**
+ * 取消微调作业
+ *
+ * @return Single FineTuneResponse
+ */
+ @POST("v1/fine-tunes/{fine_tune_id}/cancel")
+ Single cancelFineTune(@Path("fine_tune_id") String fineTuneId);
+
+ /**
+ * 微调作业事件列表
+ *
+ * @return Single OpenAiResponse Event
+ */
+ @GET("v1/fine-tunes/{fine_tune_id}/events")
+ Single> fineTuneEvents(@Path("fine_tune_id") String fineTuneId);
+
+ /**
+ * 删除微调作业模型
+ * Delete a fine-tuned model. You must have the Owner role in your organization.
+ *
+ * @return Single DeleteResponse
+ */
+ @DELETE("v1/models/{model}")
+ Single deleteFineTuneModel(@Path("model") String model);
+
+
+ /**
+ * 引擎列表
+ * 官方已废弃此接口
+ *
+ * @return Single OpenAiResponse Engine
+ */
+ @Deprecated
+ @GET("v1/engines")
+ Single> engines();
+
+ /**
+ * 检索引擎
+ * 官方已废弃此接口
+ *
+ * @param engineId 引擎id
+ * @return Engine
+ */
+ @Deprecated
+ @GET("v1/engines/{engine_id}")
+ Single engine(@Path("engine_id") String engineId);
+
+
+ /**
+ * 最新版的GPT-3.5 chat completion 更加贴近官方网站的问答模型
+ *
+ * @param chatCompletion chat completion
+ * @return 返回答案
+ */
+ @POST("v1/chat/completions")
+ Single chatCompletion(@Body ChatCompletion chatCompletion);
+
+
+ /**
+ * 语音转文字
+ *
+ * @param file 语音文件
+ * @param requestBodyMap 参数
+ * @return 文本
+ */
+ @Multipart
+ @POST("v1/audio/transcriptions")
+ Single speechToTextTranscriptions(@Part MultipartBody.Part file,
+ @PartMap() Map requestBodyMap);
+
+ /**
+ * 语音翻译:目前仅支持翻译为英文
+ *
+ * @param file 语音文件
+ * @param requestBodyMap 参数
+ * @return 文本
+ */
+ @Multipart
+ @POST("v1/audio/translations")
+ Single speechToTextTranslations(@Part MultipartBody.Part file,
+ @PartMap() Map requestBodyMap);
+
+ /**
+ * 余额查询
+ * 官方禁止访问此接口
+ *
+ * @return 余额结果
+ */
+ @GET("dashboard/billing/credit_grants")
+ @Deprecated
+ Single creditGrants();
+
+ /**
+ * 账户信息查询:里面包含总金额(美元)等信息
+ *
+ * @return 账户信息
+ */
+ @GET("v1/dashboard/billing/subscription")
+ Single subscription();
+
+ /**
+ * 账户调用接口消耗金额信息查询
+ * totalUsage = 账户总使用金额(美分)
+ *
+ * @param starDate 开始时间
+ * @param endDate 结束时间
+ * @return 消耗金额信息
+ */
+ @GET("v1/dashboard/billing/usage")
+ Single billingUsage(@Query("start_date") LocalDate starDate, @Query("end_date") LocalDate endDate);
+
+ /**
+ * 最新版的GPT-4 chat completion 支持图片输入
+ *
+ * @param chatCompletion chat completion
+ * @return 返回答案
+ */
+ @POST("v1/chat/completions")
+ Single chatCompletionWithPicture(@Body ChatCompletionWithPicture chatCompletion);
+}
diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/openai/OpenAiClient.java b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/openai/OpenAiClient.java
new file mode 100644
index 00000000..81572e97
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/openai/OpenAiClient.java
@@ -0,0 +1,806 @@
+package com.xmzs.common.chat.openai;
+
+import cn.hutool.core.collection.CollectionUtil;
+import cn.hutool.core.util.StrUtil;
+
+
+import com.xmzs.common.chat.entity.images.*;
+import com.xmzs.common.core.exception.base.BaseException;
+import com.xmzs.common.chat.constant.OpenAIConst;
+import com.xmzs.common.chat.entity.billing.BillingUsage;
+import com.xmzs.common.chat.entity.billing.Subscription;
+import com.xmzs.common.chat.entity.chat.ChatCompletion;
+import com.xmzs.common.chat.entity.chat.ChatCompletionResponse;
+import com.xmzs.common.chat.entity.chat.Message;
+import com.xmzs.common.chat.entity.common.DeleteResponse;
+import com.xmzs.common.chat.entity.common.OpenAiResponse;
+import com.xmzs.common.chat.entity.completions.Completion;
+import com.xmzs.common.chat.entity.completions.CompletionResponse;
+import com.xmzs.common.chat.entity.edits.Edit;
+import com.xmzs.common.chat.entity.edits.EditResponse;
+import com.xmzs.common.chat.entity.embeddings.Embedding;
+import com.xmzs.common.chat.entity.embeddings.EmbeddingResponse;
+import com.xmzs.common.chat.entity.engines.Engine;
+import com.xmzs.common.chat.entity.files.File;
+import com.xmzs.common.chat.entity.files.UploadFileResponse;
+import com.xmzs.common.chat.entity.fineTune.Event;
+import com.xmzs.common.chat.entity.fineTune.FineTune;
+import com.xmzs.common.chat.entity.fineTune.FineTuneDeleteResponse;
+import com.xmzs.common.chat.entity.fineTune.FineTuneResponse;
+import com.xmzs.common.chat.entity.models.Model;
+import com.xmzs.common.chat.entity.models.ModelResponse;
+import com.xmzs.common.chat.entity.moderations.Moderation;
+import com.xmzs.common.chat.entity.moderations.ModerationResponse;
+import com.xmzs.common.chat.entity.whisper.Translations;
+import com.xmzs.common.chat.entity.whisper.WhisperResponse;
+import com.xmzs.common.chat.openai.exception.CommonError;
+import com.xmzs.common.chat.openai.function.KeyRandomStrategy;
+import com.xmzs.common.chat.openai.function.KeyStrategyFunction;
+import com.xmzs.common.chat.openai.interceptor.DefaultOpenAiAuthInterceptor;
+import com.xmzs.common.chat.openai.interceptor.DynamicKeyOpenAiAuthInterceptor;
+import com.xmzs.common.chat.openai.interceptor.OpenAiAuthInterceptor;
+import io.reactivex.Single;
+import lombok.Getter;
+import lombok.extern.slf4j.Slf4j;
+import okhttp3.*;
+import org.jetbrains.annotations.NotNull;
+import retrofit2.Retrofit;
+import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory;
+import retrofit2.converter.jackson.JacksonConverterFactory;
+
+
+import java.time.LocalDate;
+import java.util.*;
+import java.util.concurrent.TimeUnit;
+
+
+/**
+ * 描述: open ai 客户端
+ *
+ * @author https:www.unfbx.com
+ * @since 2023-02-11
+ */
+
+@Slf4j
+public class OpenAiClient {
+ /**
+ * keys
+ */
+ @Getter
+ @NotNull
+ private List apiKey;
+ /**
+ * 自定义api host使用builder的方式构造client
+ */
+ @Getter
+ private String apiHost;
+ @Getter
+ private OpenAiApi openAiApi;
+ /**
+ * 自定义的okHttpClient
+ * 如果不自定义 ,就是用sdk默认的OkHttpClient实例
+ */
+ @Getter
+ private OkHttpClient okHttpClient;
+ /**
+ * api key的获取策略
+ */
+ @Getter
+ private KeyStrategyFunction, String> keyStrategy;
+
+ /**
+ * 自定义鉴权处理拦截器
+ * 可以不设置,默认实现:DefaultOpenAiAuthInterceptor
+ * 如需自定义实现参考:DealKeyWithOpenAiAuthInterceptor
+ *
+ * @see DynamicKeyOpenAiAuthInterceptor
+ * @see DefaultOpenAiAuthInterceptor
+ */
+ @Getter
+ private OpenAiAuthInterceptor authInterceptor;
+
+ /**
+ * 构造器
+ *
+ * @return OpenAiClient.Builder
+ */
+ public static OpenAiClient.Builder builder() {
+ return new OpenAiClient.Builder();
+ }
+
+ /**
+ * 构造
+ *
+ * @param builder
+ */
+ private OpenAiClient(Builder builder) {
+ if (CollectionUtil.isEmpty(builder.apiKey)) {
+ throw new BaseException(CommonError.API_KEYS_NOT_NUL.msg()
+ );
+ }
+ apiKey = builder.apiKey;
+
+ if (StrUtil.isBlank(builder.apiHost)) {
+ builder.apiHost = OpenAIConst.OPENAI_HOST;
+ }
+ apiHost = builder.apiHost;
+
+ if (Objects.isNull(builder.keyStrategy)) {
+ builder.keyStrategy = new KeyRandomStrategy();
+ }
+ keyStrategy = builder.keyStrategy;
+
+ if (Objects.isNull(builder.authInterceptor)) {
+ builder.authInterceptor = new DefaultOpenAiAuthInterceptor();
+ }
+ authInterceptor = builder.authInterceptor;
+ authInterceptor.setApiKey(this.apiKey);
+ authInterceptor.setKeyStrategy(this.keyStrategy);
+
+ if (Objects.isNull(builder.okHttpClient)) {
+ builder.okHttpClient = this.okHttpClient();
+ } else {
+ //自定义的okhttpClient 需要增加api keys
+ builder.okHttpClient = builder.okHttpClient
+ .newBuilder()
+ .addInterceptor(authInterceptor)
+ .build();
+ }
+ okHttpClient = builder.okHttpClient;
+ this.openAiApi = new Retrofit.Builder()
+ .baseUrl(apiHost)
+ .client(okHttpClient)
+ .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
+ .addConverterFactory(JacksonConverterFactory.create())
+ .build().create(OpenAiApi.class);
+ }
+
+
+ /**
+ * 创建默认OkHttpClient
+ *
+ * @return
+ */
+ private OkHttpClient okHttpClient() {
+ if (Objects.isNull(this.authInterceptor)) {
+ this.authInterceptor = new DefaultOpenAiAuthInterceptor();
+ }
+ this.authInterceptor.setApiKey(this.apiKey);
+ this.authInterceptor.setKeyStrategy(this.keyStrategy);
+ return new OkHttpClient
+ .Builder()
+ .addInterceptor(this.authInterceptor)
+ .connectTimeout(30, TimeUnit.SECONDS)
+ .writeTimeout(30, TimeUnit.SECONDS)
+ .readTimeout(30, TimeUnit.SECONDS).build();
+ }
+
+ /**
+ * openAi模型列表
+ *
+ * @return Model list
+ */
+ public List models() {
+ Single models = this.openAiApi.models();
+ return models.blockingGet().getData();
+ }
+
+ /**
+ * openAi模型详细信息
+ *
+ * @param id 模型主键
+ * @return Model 模型类
+ */
+ public Model model(String id) {
+ if (Objects.isNull(id) || "".equals(id)) {
+ throw new BaseException(CommonError.PARAM_ERROR.msg());
+ }
+ Single model = this.openAiApi.model(id);
+ return model.blockingGet();
+ }
+
+
+ /**
+ * 问答接口
+ *
+ * @param completion 问答参数
+ * @return CompletionResponse
+ */
+ public CompletionResponse completions(Completion completion) {
+ Single completions = this.openAiApi.completions(completion);
+ return completions.blockingGet();
+ }
+
+ /**
+ * 问答接口-简易版
+ *
+ * @param question 问题描述
+ * @return CompletionResponse
+ */
+ public CompletionResponse completions(String question) {
+ Completion q = Completion.builder()
+ .prompt(question)
+ .build();
+ Single completions = this.openAiApi.completions(q);
+ return completions.blockingGet();
+ }
+
+ /**
+ * 文本修改
+ *
+ * @param edit 图片对象
+ * @return EditResponse
+ */
+ public EditResponse edit(Edit edit) {
+ Single edits = this.openAiApi.edits(edit);
+ return edits.blockingGet();
+ }
+
+ /**
+ * 根据描述生成图片
+ *
+ * @param prompt 描述信息
+ * @return ImageResponse
+ */
+ public ImageResponse genImages(String prompt) {
+ Image image = Image.builder().prompt(prompt).build();
+ return this.genImages(image);
+ }
+
+ /**
+ * 根据描述生成图片
+ *
+ * @param image 图片参数
+ * @return ImageResponse
+ */
+ public ImageResponse genImages(Image image) {
+ Single edits = this.openAiApi.genImages(image);
+ return edits.blockingGet();
+ }
+
+ /**
+ * Creates an edited or extended image given an original image and a prompt.
+ * 根据描述修改图片
+ *
+ * @param image 图片对象
+ * @param prompt 描述信息
+ * @return Item list
+ */
+ public List- editImages(java.io.File image, String prompt) {
+ ImageEdit imageEdit = ImageEdit.builder().prompt(prompt).build();
+ return this.editImages(image, null, imageEdit);
+ }
+
+ /**
+ * Creates an edited or extended image given an original image and a prompt.
+ * 根据描述修改图片
+ *
+ * @param image 图片对象
+ * @param imageEdit 图片参数
+ * @return Item list
+ */
+ public List
- editImages(java.io.File image, ImageEdit imageEdit) {
+ return this.editImages(image, null, imageEdit);
+ }
+
+ /**
+ * Creates an edited or extended image given an original image and a prompt.
+ * 根据描述修改图片
+ *
+ * @param image png格式的图片,最大4MB
+ * @param mask png格式的图片,最大4MB
+ * @param imageEdit 图片参数
+ * @return Item list
+ */
+ public List
- editImages(java.io.File image, java.io.File mask, ImageEdit imageEdit) {
+ checkImage(image);
+ checkImageFormat(image);
+ checkImageSize(image);
+ if (Objects.nonNull(mask)) {
+ checkImageFormat(image);
+ checkImageSize(image);
+ }
+ // 创建 RequestBody,用于封装构建RequestBody
+ RequestBody imageBody = RequestBody.create(MediaType.parse("multipart/form-data"), image);
+ MultipartBody.Part imageMultipartBody = MultipartBody.Part.createFormData("image", image.getName(), imageBody);
+ MultipartBody.Part maskMultipartBody = null;
+ if (Objects.nonNull(mask)) {
+ RequestBody maskBody = RequestBody.create(MediaType.parse("multipart/form-data"), mask);
+ maskMultipartBody = MultipartBody.Part.createFormData("mask", image.getName(), maskBody);
+ }
+ Map
requestBodyMap = new HashMap<>();
+ requestBodyMap.put("prompt", RequestBody.create(MediaType.parse("multipart/form-data"), imageEdit.getPrompt()));
+ requestBodyMap.put("n", RequestBody.create(MediaType.parse("multipart/form-data"), imageEdit.getN().toString()));
+ requestBodyMap.put("size", RequestBody.create(MediaType.parse("multipart/form-data"), imageEdit.getSize()));
+ requestBodyMap.put("response_format", RequestBody.create(MediaType.parse("multipart/form-data"), imageEdit.getResponseFormat()));
+ if (!(Objects.isNull(imageEdit.getUser()) || "".equals(imageEdit.getUser()))) {
+ requestBodyMap.put("user", RequestBody.create(MediaType.parse("multipart/form-data"), imageEdit.getUser()));
+ }
+ Single imageResponse = this.openAiApi.editImages(
+ imageMultipartBody,
+ maskMultipartBody,
+ requestBodyMap
+ );
+ return imageResponse.blockingGet().getData();
+ }
+
+ /**
+ * Creates a variation of a given image.
+ *
+ * 变化图片,类似ai重做图片
+ *
+ * @param image 图片对象
+ * @param imageVariations 图片参数
+ * @return ImageResponse
+ */
+ public ImageResponse variationsImages(java.io.File image, ImageVariations imageVariations) {
+ checkImage(image);
+ checkImageFormat(image);
+ checkImageSize(image);
+ RequestBody imageBody = RequestBody.create(MediaType.parse("multipart/form-data"), image);
+ MultipartBody.Part multipartBody = MultipartBody.Part.createFormData("image", image.getName(), imageBody);
+ Map requestBodyMap = new HashMap<>();
+ requestBodyMap.put("n", RequestBody.create(MediaType.parse("multipart/form-data"), imageVariations.getN().toString()));
+ requestBodyMap.put("size", RequestBody.create(MediaType.parse("multipart/form-data"), imageVariations.getSize()));
+ requestBodyMap.put("response_format", RequestBody.create(MediaType.parse("multipart/form-data"), imageVariations.getResponseFormat()));
+ if (!(Objects.isNull(imageVariations.getUser()) || "".equals(imageVariations.getUser()))) {
+ requestBodyMap.put("user", RequestBody.create(MediaType.parse("multipart/form-data"), imageVariations.getUser()));
+ }
+ Single variationsImages = this.openAiApi.variationsImages(
+ multipartBody,
+ requestBodyMap
+ );
+ return variationsImages.blockingGet();
+ }
+
+ /**
+ * Creates a variation of a given image.
+ *
+ * @param image 图片对象
+ * @return ImageResponse
+ */
+ public ImageResponse variationsImages(java.io.File image) {
+ checkImage(image);
+ checkImageFormat(image);
+ checkImageSize(image);
+ ImageVariations imageVariations = ImageVariations.builder().build();
+ return this.variationsImages(image, imageVariations);
+ }
+
+ /**
+ * 校验图片不能为空
+ *
+ * @param image
+ */
+ private void checkImage(java.io.File image) {
+ if (Objects.isNull(image)) {
+ log.error("image不能为空");
+ throw new BaseException(CommonError.PARAM_ERROR.msg());
+ }
+ }
+
+ /**
+ * 校验图片格式
+ *
+ * @param image
+ */
+ private void checkImageFormat(java.io.File image) {
+ if (!(image.getName().endsWith("png") || image.getName().endsWith("PNG"))) {
+ log.error("image格式错误");
+ throw new BaseException(CommonError.PARAM_ERROR.msg());
+ }
+ }
+
+ /**
+ * 校验图片大小
+ *
+ * @param image
+ */
+ private void checkImageSize(java.io.File image) {
+ if (image.length() > 4 * 1024 * 1024) {
+ log.error("image最大支持4MB");
+ throw new BaseException(CommonError.PARAM_ERROR.msg());
+ }
+ }
+
+ /**
+ * 向量计算:单文本
+ *
+ * @param input 单文本
+ * @return EmbeddingResponse
+ */
+ public EmbeddingResponse embeddings(String input) {
+ List inputs = new ArrayList<>(1);
+ inputs.add(input);
+ Embedding embedding = Embedding.builder().input(inputs).build();
+ return this.embeddings(embedding);
+ }
+
+ /**
+ * 向量计算:集合文本
+ *
+ * @param input 文本集合
+ * @return EmbeddingResponse
+ */
+ public EmbeddingResponse embeddings(List input) {
+ Embedding embedding = Embedding.builder().input(input).build();
+ return this.embeddings(embedding);
+ }
+
+ /**
+ * 文本转换向量
+ *
+ * @param embedding 入参
+ * @return EmbeddingResponse
+ */
+ public EmbeddingResponse embeddings(Embedding embedding) {
+ Single embeddings = this.openAiApi.embeddings(embedding);
+ return embeddings.blockingGet();
+ }
+
+ /**
+ * 获取文件列表
+ *
+ * @return File list
+ */
+ public List files() {
+ Single> files = this.openAiApi.files();
+ return files.blockingGet().getData();
+ }
+
+ /**
+ * 删除文件
+ *
+ * @param fileId 文件id
+ * @return DeleteResponse
+ */
+ public DeleteResponse deleteFile(String fileId) {
+ Single deleteFile = this.openAiApi.deleteFile(fileId);
+ return deleteFile.blockingGet();
+ }
+
+ /**
+ * 上传文件
+ *
+ * @param purpose purpose
+ * @param file 文件对象
+ * @return UploadFileResponse
+ */
+ public UploadFileResponse uploadFile(String purpose, java.io.File file) {
+ // 创建 RequestBody,用于封装构建RequestBody
+ RequestBody fileBody = RequestBody.create(MediaType.parse("multipart/form-data"), file);
+ MultipartBody.Part multipartBody = MultipartBody.Part.createFormData("file", file.getName(), fileBody);
+
+ RequestBody purposeBody = RequestBody.create(MediaType.parse("multipart/form-data"), purpose);
+ Single uploadFileResponse = this.openAiApi.uploadFile(multipartBody, purposeBody);
+ return uploadFileResponse.blockingGet();
+ }
+
+ /**
+ * 上传文件
+ *
+ * @param file 文件
+ * @return UploadFileResponse
+ */
+ public UploadFileResponse uploadFile(java.io.File file) {
+ //purpose 官网示例默认是:fine-tune
+ return this.uploadFile("fine-tune", file);
+ }
+
+ /**
+ * 检索文件
+ *
+ * @param fileId 文件id
+ * @return File
+ */
+ public File retrieveFile(String fileId) {
+ Single fileContent = this.openAiApi.retrieveFile(fileId);
+ return fileContent.blockingGet();
+ }
+
+ /**
+ * 检索文件内容
+ * 免费用户无法使用此接口 #未经过测试
+ *
+ * @param fileId
+ * @return ResponseBody
+ */
+// public ResponseBody retrieveFileContent(String fileId) {
+// Single fileContent = this.openAiApi.retrieveFileContent(fileId);
+// return fileContent.blockingGet();
+// }
+
+ /**
+ * 文本审核
+ *
+ * @param input 待检测数据
+ * @return ModerationResponse
+ */
+ public ModerationResponse moderations(String input) {
+ List content = new ArrayList<>(1);
+ content.add(input);
+ Moderation moderation = Moderation.builder().input(content).build();
+ return this.moderations(moderation);
+ }
+
+ /**
+ * 文本审核
+ *
+ * @param input 待检测数据集合
+ * @return ModerationResponse
+ */
+ public ModerationResponse moderations(List input) {
+ Moderation moderation = Moderation.builder().input(input).build();
+ return this.moderations(moderation);
+ }
+
+ /**
+ * 文本审核
+ *
+ * @param moderation 审核参数
+ * @return ModerationResponse
+ */
+ public ModerationResponse moderations(Moderation moderation) {
+ Single moderations = this.openAiApi.moderations(moderation);
+ return moderations.blockingGet();
+ }
+
+ /**
+ * 创建微调模型
+ *
+ * @param fineTune 微调作业id
+ * @return FineTuneResponse
+ */
+ public FineTuneResponse fineTune(FineTune fineTune) {
+ Single fineTuneResponse = this.openAiApi.fineTune(fineTune);
+ return fineTuneResponse.blockingGet();
+ }
+
+ /**
+ * 创建微调模型
+ *
+ * @param trainingFileId 文件id,文件上传返回的id
+ * @return FineTuneResponse
+ */
+ public FineTuneResponse fineTune(String trainingFileId) {
+ FineTune fineTune = FineTune.builder().trainingFile(trainingFileId).build();
+ return this.fineTune(fineTune);
+ }
+
+ /**
+ * 微调模型列表
+ *
+ * @return FineTuneResponse list
+ */
+ public List fineTunes() {
+ Single> fineTunes = this.openAiApi.fineTunes();
+ return fineTunes.blockingGet().getData();
+ }
+
+ /**
+ * 检索微调作业
+ *
+ * @param fineTuneId 微调作业id
+ * @return FineTuneResponse
+ */
+ public FineTuneResponse retrieveFineTune(String fineTuneId) {
+ Single fineTune = this.openAiApi.retrieveFineTune(fineTuneId);
+ return fineTune.blockingGet();
+ }
+
+ /**
+ * 取消微调作业
+ *
+ * @param fineTuneId 主键
+ * @return FineTuneResponse
+ */
+ public FineTuneResponse cancelFineTune(String fineTuneId) {
+ Single fineTune = this.openAiApi.cancelFineTune(fineTuneId);
+ return fineTune.blockingGet();
+ }
+
+ /**
+ * 微调作业事件列表
+ *
+ * @param fineTuneId 微调作业id
+ * @return Event List
+ */
+ public List fineTuneEvents(String fineTuneId) {
+ Single> events = this.openAiApi.fineTuneEvents(fineTuneId);
+ return events.blockingGet().getData();
+ }
+
+ /**
+ * 删除微调作业模型
+ * Delete a fine-tuned model. You must have the Owner role in your organization.
+ *
+ * @param model 模型名称
+ * @return FineTuneDeleteResponse
+ */
+ public FineTuneDeleteResponse deleteFineTuneModel(String model) {
+ Single delete = this.openAiApi.deleteFineTuneModel(model);
+ return delete.blockingGet();
+ }
+
+
+ /**
+ * 引擎列表
+ *
+ * @return Engine List
+ */
+ @Deprecated
+ public List engines() {
+ Single> engines = this.openAiApi.engines();
+ return engines.blockingGet().getData();
+ }
+
+ /**
+ * 引擎详细信息
+ *
+ * @param engineId 引擎id
+ * @return Engine
+ */
+ @Deprecated
+ public Engine engine(String engineId) {
+ Single engine = this.openAiApi.engine(engineId);
+ return engine.blockingGet();
+ }
+
+ /**
+ * 最新版的GPT-3.5 chat completion 更加贴近官方网站的问答模型
+ *
+ * @param chatCompletion 问答参数
+ * @return 答案
+ */
+ public ChatCompletionResponse chatCompletion(ChatCompletion chatCompletion) {
+ Single chatCompletionResponse = this.openAiApi.chatCompletion(chatCompletion);
+ return chatCompletionResponse.blockingGet();
+ }
+
+ /**
+ * 简易版
+ *
+ * @param messages 问答参数
+ * @return 答案
+ */
+ public ChatCompletionResponse chatCompletion(List messages) {
+ ChatCompletion chatCompletion = ChatCompletion.builder().messages(messages).build();
+ return this.chatCompletion(chatCompletion);
+ }
+
+
+ /**
+ * 语音翻译:目前仅支持翻译为英文
+ *
+ * @param translations 参数
+ * @param file 语音文件 最大支持25MB mp3, mp4, mpeg, mpga, m4a, wav, webm
+ * @return 翻译后文本
+ */
+ public WhisperResponse speechToTextTranslations(java.io.File file, Translations translations) {
+ //文件
+ RequestBody fileBody = RequestBody.create(MediaType.parse("multipart/form-data"), file);
+ MultipartBody.Part multipartBody = MultipartBody.Part.createFormData("file", file.getName(), fileBody);
+ //自定义参数
+ Map requestBodyMap = new HashMap<>(5,1L);
+
+ if (StrUtil.isNotBlank(translations.getModel())) {
+ requestBodyMap.put(Translations.Fields.model, RequestBody.create(MediaType.parse("multipart/form-data"), translations.getModel()));
+ }
+ if (StrUtil.isNotBlank(translations.getPrompt())) {
+ requestBodyMap.put(Translations.Fields.prompt, RequestBody.create(MediaType.parse("multipart/form-data"), translations.getPrompt()));
+ }
+ if (StrUtil.isNotBlank(translations.getResponseFormat())) {
+ requestBodyMap.put(Translations.Fields.responseFormat, RequestBody.create(MediaType.parse("multipart/form-data"), translations.getResponseFormat()));
+ }
+ requestBodyMap.put(Translations.Fields.temperature, RequestBody.create(MediaType.parse("multipart/form-data"), String.valueOf(translations.getTemperature())));
+ Single whisperResponse = this.openAiApi.speechToTextTranslations(multipartBody, requestBodyMap);
+ return whisperResponse.blockingGet();
+ }
+
+ /**
+ * 简易版 语音翻译:目前仅支持翻译为英文
+ *
+ * @param file 语音文件 最大支持25MB mp3, mp4, mpeg, mpga, m4a, wav, webm
+ * @return 翻译后文本
+ */
+ public WhisperResponse speechToTextTranslations(java.io.File file) {
+ Translations translations = Translations.builder().build();
+ return this.speechToTextTranslations(file, translations);
+ }
+
+ /**
+ * 校验语音文件大小给出提示,目前官方限制25MB,后续可能会改动所以不报错只做提示
+ *
+ * @param file
+ */
+ private void checkSpeechFileSize(java.io.File file) {
+ if (file.length() > 25 * 1204 * 1024) {
+ log.warn("2023-03-02官方文档提示:文件不能超出25MB");
+ }
+ }
+
+ /**
+ * 账户信息查询:里面包含总金额等信息
+ *
+ * @return 账户信息
+ */
+ public Subscription subscription() {
+ Single subscription = this.openAiApi.subscription();
+ return subscription.blockingGet();
+ }
+ /**
+ * 账户调用接口消耗金额信息查询
+ * 最多查询100天
+ *
+ * @param starDate 开始时间
+ * @param endDate 结束时间
+ * @return 消耗金额信息
+ */
+ public BillingUsage billingUsage(@NotNull LocalDate starDate, @NotNull LocalDate endDate) {
+ Single billingUsage = this.openAiApi.billingUsage(starDate, endDate);
+ return billingUsage.blockingGet();
+ }
+
+
+ public static final class Builder {
+ /**
+ * api keys
+ */
+ private @NotNull List apiKey;
+ /**
+ * api请求地址,结尾处有斜杠
+ *
+ */
+ private String apiHost;
+ /**
+ * 自定义OkhttpClient
+ */
+ private OkHttpClient okHttpClient;
+
+ /**
+ * api key的获取策略
+ */
+ private KeyStrategyFunction keyStrategy;
+
+ /**
+ * 自定义鉴权拦截器
+ */
+ private OpenAiAuthInterceptor authInterceptor;
+
+ public Builder() {
+ }
+
+ /**
+ * @param val api请求地址,结尾处有斜杠
+ * @return Builder对象
+ */
+ public Builder apiHost(String val) {
+ apiHost = val;
+ return this;
+ }
+
+ public Builder apiKey(@NotNull List val) {
+ apiKey = val;
+ return this;
+ }
+
+ public Builder keyStrategy(KeyStrategyFunction val) {
+ keyStrategy = val;
+ return this;
+ }
+
+ public Builder okHttpClient(OkHttpClient val) {
+ okHttpClient = val;
+ return this;
+ }
+
+ public Builder authInterceptor(OpenAiAuthInterceptor val) {
+ authInterceptor = val;
+ return this;
+ }
+
+ public OpenAiClient build() {
+ return new OpenAiClient(this);
+ }
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/openai/OpenAiStreamClient.java b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/openai/OpenAiStreamClient.java
new file mode 100644
index 00000000..3fc25f19
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/openai/OpenAiStreamClient.java
@@ -0,0 +1,419 @@
+package com.xmzs.common.chat.openai;
+
+import cn.hutool.core.collection.CollectionUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.http.ContentType;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.xmzs.common.chat.config.LocalCache;
+import com.xmzs.common.chat.entity.billing.BillingUsage;
+import com.xmzs.common.chat.entity.billing.KeyInfo;
+import com.xmzs.common.chat.entity.billing.Subscription;
+import com.xmzs.common.chat.entity.chat.BaseChatCompletion;
+import com.xmzs.common.chat.entity.chat.ChatCompletionResponse;
+import com.xmzs.common.chat.entity.chat.ChatCompletionWithPicture;
+import com.xmzs.common.chat.entity.images.Image;
+import com.xmzs.common.chat.entity.images.ImageResponse;
+import com.xmzs.common.chat.entity.models.Model;
+import com.xmzs.common.chat.entity.models.ModelResponse;
+import com.xmzs.common.chat.entity.whisper.Transcriptions;
+import com.xmzs.common.chat.entity.whisper.WhisperResponse;
+import com.xmzs.common.chat.openai.exception.CommonError;
+import com.xmzs.common.chat.openai.function.KeyRandomStrategy;
+import com.xmzs.common.chat.openai.function.KeyStrategyFunction;
+import com.xmzs.common.chat.openai.interceptor.DefaultOpenAiAuthInterceptor;
+import com.xmzs.common.chat.openai.interceptor.DynamicKeyOpenAiAuthInterceptor;
+import com.xmzs.common.chat.openai.interceptor.OpenAiAuthInterceptor;
+import com.xmzs.common.core.exception.ServiceException;
+import io.reactivex.Single;
+import lombok.Getter;
+import lombok.extern.slf4j.Slf4j;
+import okhttp3.*;
+import okhttp3.sse.EventSource;
+import okhttp3.sse.EventSourceListener;
+import okhttp3.sse.EventSources;
+import com.xmzs.common.core.exception.base.BaseException;
+import com.xmzs.common.chat.constant.OpenAIConst;
+import com.xmzs.common.chat.entity.chat.ChatCompletion;
+import org.jetbrains.annotations.NotNull;
+import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitter;
+import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
+import retrofit2.Retrofit;
+import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory;
+import retrofit2.converter.jackson.JacksonConverterFactory;
+
+import java.io.InputStream;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.nio.charset.StandardCharsets;
+import java.rmi.ServerException;
+import java.time.LocalDate;
+import java.time.ZoneId;
+import java.util.*;
+import java.util.concurrent.TimeUnit;
+import java.net.URI;
+import java.io.BufferedReader;
+import java.io.InputStreamReader;
+import java.net.http.HttpResponse;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonObject;
+
+/**
+ * 描述: open ai 客户端
+ *
+ * @author https:www.unfbx.com
+ * 2023-02-28
+ */
+
+@Slf4j
+public class OpenAiStreamClient {
+ @Getter
+ @NotNull
+ private List apiKey;
+ /**
+ * 自定义api host使用builder的方式构造client
+ */
+ @Getter
+ private String apiHost;
+ /**
+ * 自定义的okHttpClient
+ * 如果不自定义 ,就是用sdk默认的OkHttpClient实例
+ */
+ @Getter
+ private OkHttpClient okHttpClient;
+
+ /**
+ * api key的获取策略
+ */
+ @Getter
+ private KeyStrategyFunction, String> keyStrategy;
+
+ @Getter
+ private OpenAiApi openAiApi;
+
+ /**
+ * 自定义鉴权处理拦截器
+ * 可以不设置,默认实现:DefaultOpenAiAuthInterceptor
+ * 如需自定义实现参考:DealKeyWithOpenAiAuthInterceptor
+ *
+ * @see DynamicKeyOpenAiAuthInterceptor
+ * @see DefaultOpenAiAuthInterceptor
+ */
+ @Getter
+ private OpenAiAuthInterceptor authInterceptor;
+
+ private static final String API_KEY = "sk-Waea254YSRYVg4FZVCz2CDz73B22xRpmKpJ41kbczVgpPxvg";
+
+ HttpClient client = HttpClient.newHttpClient();
+
+ private static final String DONE_SIGNAL = "[DONE]";
+
+ /**
+ * 构造实例对象
+ *
+ * @param builder
+ */
+ private OpenAiStreamClient(Builder builder) {
+ if (CollectionUtil.isEmpty(builder.apiKey)) {
+ throw new BaseException(CommonError.API_KEYS_NOT_NUL.msg());
+ }
+ apiKey = builder.apiKey;
+
+ if (StrUtil.isBlank(builder.apiHost)) {
+ builder.apiHost = OpenAIConst.OPENAI_HOST;
+ }
+ apiHost = builder.apiHost;
+
+ if (Objects.isNull(builder.keyStrategy)) {
+ builder.keyStrategy = new KeyRandomStrategy();
+ }
+ keyStrategy = builder.keyStrategy;
+
+ if (Objects.isNull(builder.authInterceptor)) {
+ builder.authInterceptor = new DefaultOpenAiAuthInterceptor();
+ }
+ authInterceptor = builder.authInterceptor;
+ //设置apiKeys和key的获取策略
+ authInterceptor.setApiKey(this.apiKey);
+ authInterceptor.setKeyStrategy(this.keyStrategy);
+
+ if (Objects.isNull(builder.okHttpClient)) {
+ builder.okHttpClient = this.okHttpClient();
+ } else {
+ //自定义的okhttpClient 需要增加api keys
+ builder.okHttpClient = builder.okHttpClient
+ .newBuilder()
+ .addInterceptor(authInterceptor)
+ .build();
+ }
+ okHttpClient = builder.okHttpClient;
+
+ this.openAiApi = new Retrofit.Builder()
+ .baseUrl(apiHost)
+ .client(okHttpClient)
+ .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
+ .addConverterFactory(JacksonConverterFactory.create())
+ .build().create(OpenAiApi.class);
+ }
+
+ /**
+ * 创建默认的OkHttpClient
+ */
+ private OkHttpClient okHttpClient() {
+ if (Objects.isNull(this.authInterceptor)) {
+ this.authInterceptor = new DefaultOpenAiAuthInterceptor();
+ }
+ this.authInterceptor.setApiKey(this.apiKey);
+ this.authInterceptor.setKeyStrategy(this.keyStrategy);
+ OkHttpClient okHttpClient = new OkHttpClient
+ .Builder()
+ .addInterceptor(this.authInterceptor)
+ .connectTimeout(10, TimeUnit.SECONDS)
+ .writeTimeout(50, TimeUnit.SECONDS)
+ .readTimeout(50, TimeUnit.SECONDS)
+ .build();
+ return okHttpClient;
+ }
+
+
+ /**
+ * 流式输出,最新版的GPT-3.5 chat completion 更加贴近官方网站的问答模型
+ *
+ * @param chatCompletion 问答参数
+ * @param eventSourceListener 监听器
+ */
+ public void streamChatCompletion(T chatCompletion, EventSourceListener eventSourceListener) {
+ if (Objects.isNull(eventSourceListener)) {
+ log.error("参数异常:EventSourceListener不能为空!");
+ throw new BaseException(CommonError.PARAM_ERROR.msg());
+ }
+ try {
+ EventSource.Factory factory = EventSources.createFactory(this.okHttpClient);
+ ObjectMapper mapper = new ObjectMapper();
+ String requestBody = mapper.writeValueAsString(chatCompletion);
+ Request request = new Request.Builder()
+ .url(this.apiHost + "v1/chat/completions")
+ .post(RequestBody.create(MediaType.parse(ContentType.JSON.getValue()), requestBody))
+ .build();
+ factory.newEventSource(request, eventSourceListener);
+ } catch (Exception e) {
+ log.error("请求参数解析异常:{}", e.getMessage());
+ }
+ }
+
+
+ /**
+ * 根据描述生成图片
+ *
+ * @param image 图片参数
+ * @return ImageResponse
+ */
+ public ImageResponse genImages(Image image) {
+ Single edits = this.openAiApi.genImages(image);
+ return edits.blockingGet();
+ }
+
+ /**
+ * 最新版的GPT-3.5 chat completion 更加贴近官方网站的问答模型
+ *
+ * @param chatCompletion 问答参数
+ * @return 答案
+ */
+ public ChatCompletionResponse chatCompletion(T chatCompletion) {
+ if (chatCompletion instanceof ChatCompletion) {
+ Single chatCompletionResponse = this.openAiApi.chatCompletion((ChatCompletion) chatCompletion);
+ return chatCompletionResponse.blockingGet();
+ }
+ Single chatCompletionResponse = this.openAiApi.chatCompletionWithPicture((ChatCompletionWithPicture) chatCompletion);
+ return chatCompletionResponse.blockingGet();
+ }
+
+ /**
+ * 获取openKey账户信息(近90天)
+ *
+ * @param key
+ * @return KeyInfo
+ * @Date 2023/7/6
+ **/
+ public KeyInfo getKeyInfo(String key) {
+ Date now = new Date();
+ Date start = new Date(now.getTime() - (long) 90 * 24 * 60 * 60 * 1000);
+ Date end = new Date(now.getTime() + (long) 24 * 60 * 60 * 1000);
+
+ BillingUsage billingUsage = billingUsage(start.toInstant().atZone(ZoneId.systemDefault()).toLocalDate(), end.toInstant().atZone(ZoneId.systemDefault()).toLocalDate());
+ double totalUsage = billingUsage.getTotalUsage().doubleValue() / 100;
+ System.out.println(totalUsage);
+ Subscription subscription = subscription();
+ KeyInfo keyInfo = new KeyInfo();
+ String start_key = key.substring(0, 6);
+ String end_key = key.substring(key.length() - 6);
+ String mid_key = key.substring(6, key.length() - 6);
+ mid_key = mid_key.replaceAll(".", "*");
+
+ keyInfo.setKeyValue(start_key + mid_key + end_key);
+ keyInfo.setTotalAmount(subscription.getHardLimitUsd());
+ keyInfo.setRemaining(subscription.getHardLimitUsd() - totalUsage);
+ keyInfo.setTotalUsage(totalUsage);
+ keyInfo.setLimitDate(new Date(subscription.getAccessUntil() * 1000).toInstant().atZone(ZoneId.systemDefault()).toLocalDate());
+ keyInfo.setPlanTitle(subscription.getPlan() != null ? subscription.getPlan().getTitle() : "null");
+ keyInfo.setIsHasPaymentMethod(subscription.isHasPaymentMethod());
+ keyInfo.setModel(getModelName());
+ return keyInfo;
+ }
+
+ /**
+ * 获取可用模型
+ *
+ * @param
+ * @return String
+ * @Date 2023/7/6
+ **/
+ public String getModelName() {
+ Single models = this.openAiApi.models();
+ List modelList = models.blockingGet().getData();
+ for (Model model : modelList) {
+ if (Objects.equals(model.getId(), "gpt-4")) {
+ return "GPT-4.0";
+ }
+ }
+ return "GPT-3.5";
+ }
+
+ /**
+ * 账户调用接口消耗金额信息查询
+ * 最多查询100天
+ *
+ * @param starDate 开始时间
+ * @param endDate 结束时间
+ * @return 消耗金额信息
+ */
+ public BillingUsage billingUsage(@NotNull LocalDate starDate, @NotNull LocalDate endDate) {
+ Single billingUsage = this.openAiApi.billingUsage(starDate, endDate);
+ return billingUsage.blockingGet();
+ }
+
+ /**
+ * 账户信息查询:里面包含总金额等信息
+ *
+ * @return 账户信息
+ */
+ public Subscription subscription() {
+ Single subscription = this.openAiApi.subscription();
+ return subscription.blockingGet();
+ }
+
+ /**
+ * 语音转文字
+ *
+ * @param transcriptions 参数
+ * @param file 语音文件 最大支持25MB mp3, mp4, mpeg, mpga, m4a, wav, webm
+ * @return 语音文本
+ */
+ public WhisperResponse speechToTextTranscriptions(java.io.File file, Transcriptions transcriptions) {
+ //文件
+ RequestBody fileBody = RequestBody.create(MediaType.parse("multipart/form-data"), file);
+ MultipartBody.Part multipartBody = MultipartBody.Part.createFormData("file", file.getName(), fileBody);
+ //自定义参数
+ Map requestBodyMap = new HashMap<>(10);
+ if (StrUtil.isNotBlank(transcriptions.getLanguage())) {
+ requestBodyMap.put(Transcriptions.Fields.language, RequestBody.create(MediaType.parse("multipart/form-data"), transcriptions.getLanguage()));
+ }
+ if (StrUtil.isNotBlank(transcriptions.getModel())) {
+ requestBodyMap.put(Transcriptions.Fields.model, RequestBody.create(MediaType.parse("multipart/form-data"), transcriptions.getModel()));
+ }
+ if (StrUtil.isNotBlank(transcriptions.getPrompt())) {
+ requestBodyMap.put(Transcriptions.Fields.prompt, RequestBody.create(MediaType.parse("multipart/form-data"), transcriptions.getPrompt()));
+ }
+ if (StrUtil.isNotBlank(transcriptions.getResponseFormat())) {
+ requestBodyMap.put(Transcriptions.Fields.responseFormat, RequestBody.create(MediaType.parse("multipart/form-data"), transcriptions.getResponseFormat()));
+ }
+ if (Objects.nonNull(transcriptions.getTemperature())) {
+ requestBodyMap.put(Transcriptions.Fields.temperature, RequestBody.create(MediaType.parse("multipart/form-data"), String.valueOf(transcriptions.getTemperature())));
+ }
+ Single whisperResponse = this.openAiApi.speechToTextTranscriptions(multipartBody, requestBodyMap);
+ return whisperResponse.blockingGet();
+ }
+
+ /**
+ * 简易版 语音转文字
+ *
+ * @param file 语音文件 最大支持25MB mp3, mp4, mpeg, mpga, m4a, wav, webm
+ * @return 语音文本
+ */
+ public WhisperResponse speechToTextTranscriptions(java.io.File file) {
+ Transcriptions transcriptions = Transcriptions.builder().build();
+ return this.speechToTextTranscriptions(file, transcriptions);
+ }
+
+ /**
+ * 构造
+ *
+ * @return Builder
+ */
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ public static final class Builder {
+ private @NotNull List apiKey;
+ /**
+ * api请求地址,结尾处有斜杠
+ *
+ * @see OpenAIConst
+ */
+ private String apiHost;
+
+ /**
+ * 自定义OkhttpClient
+ */
+ private OkHttpClient okHttpClient;
+
+
+ /**
+ * api key的获取策略
+ */
+ private KeyStrategyFunction keyStrategy;
+
+ /**
+ * 自定义鉴权拦截器
+ */
+ private OpenAiAuthInterceptor authInterceptor;
+
+ public Builder() {
+ }
+
+ public Builder apiKey(@NotNull List val) {
+ apiKey = val;
+ return this;
+ }
+
+ /**
+ * @param val api请求地址,结尾处有斜杠
+ * @return Builder
+ * @see OpenAIConst
+ */
+ public Builder apiHost(String val) {
+ apiHost = val;
+ return this;
+ }
+
+ public Builder keyStrategy(KeyStrategyFunction val) {
+ keyStrategy = val;
+ return this;
+ }
+
+ public Builder okHttpClient(OkHttpClient val) {
+ okHttpClient = val;
+ return this;
+ }
+
+ public Builder authInterceptor(OpenAiAuthInterceptor val) {
+ authInterceptor = val;
+ return this;
+ }
+
+ public OpenAiStreamClient build() {
+ return new OpenAiStreamClient(this);
+ }
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/openai/TestOpenAIAPI.java b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/openai/TestOpenAIAPI.java
new file mode 100644
index 00000000..f4e8bd51
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/openai/TestOpenAIAPI.java
@@ -0,0 +1,32 @@
+package com.xmzs.common.chat.openai;
+
+import okhttp3.*;
+
+import java.io.IOException;
+
+public class TestOpenAIAPI {
+
+ private final OkHttpClient client = new OkHttpClient();
+ private static final String API_KEY = "sk-Waea254YSRYVg4FZVCz2CDz73B22xRpmKpJ41kbczVgpPxvg";
+ private static final String URL = "https://api.gptgod.online/v1/chat/completions";
+
+ public void getChatGptResponse(String prompt) throws IOException {
+ RequestBody body = RequestBody.create(MediaType.get("application/json; charset=utf-8"),
+ "{\"model\": \"gpt-3.5-turbo\", \"messages\": [{\"role\": \"system\", \"content\": \"You are a helpful assistant.\"}, {\"role\": \"user\", \"content\": \"" + prompt + "\"}]}");
+
+ Request request = new Request.Builder()
+ .url(URL)
+ .post(body)
+ .addHeader("Authorization", "Bearer " + API_KEY)
+ .build();
+
+ try (Response response = client.newCall(request).execute()) {
+ System.out.println(response.body().string());
+ }
+ }
+
+ public static void main(String[] args) throws IOException {
+ TestOpenAIAPI api = new TestOpenAIAPI();
+ api.getChatGptResponse("Hello, how are you?");
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/openai/exception/CommonError.java b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/openai/exception/CommonError.java
new file mode 100644
index 00000000..a02877d3
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/openai/exception/CommonError.java
@@ -0,0 +1,39 @@
+package com.xmzs.common.chat.openai.exception;
+
+/**
+ * 描述: 错误
+ *
+ * @author https:www.unfbx.com
+ * 2023-02-11
+ */
+public enum CommonError implements IError {
+ API_KEYS_NOT_NUL(500, "API KEYS 不能为空"),
+ NO_ACTIVE_API_KEYS(500, "没有可用的API KEYS"),
+ SYS_ERROR(500, "系统繁忙"),
+ PARAM_ERROR(501, "参数异常"),
+ RETRY_ERROR(502, "请求异常,请重试~"),
+ //官方的错误码列表:https://platform.openai.com/docs/guides/error-codes/api-errors
+ OPENAI_AUTHENTICATION_ERROR(401, "身份验证无效/提供的 API 密钥不正确/您必须是组织的成员才能使用 API"),
+ OPENAI_LIMIT_ERROR(429 , "达到请求的速率限制/您超出了当前配额,请检查您的计划和帐单详细信息/发动机当前过载,请稍后重试"),
+ OPENAI_SERVER_ERROR(500, "服务器在处理您的请求时出错"),
+ ;
+
+
+ private int code;
+ private String msg;
+
+ CommonError(int code, String msg) {
+ this.code = code;
+ this.msg = msg;
+ }
+
+ @Override
+ public String msg() {
+ return this.msg;
+ }
+
+ @Override
+ public int code() {
+ return this.code;
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/openai/exception/IError.java b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/openai/exception/IError.java
new file mode 100644
index 00000000..98f73b8e
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/openai/exception/IError.java
@@ -0,0 +1,12 @@
+package com.xmzs.common.chat.openai.exception;
+/**
+ * 描述:
+ *
+ * @author https:www.unfbx.com
+ * 2023-02-11
+ */
+public interface IError {
+ String msg();
+
+ int code();
+}
diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/openai/function/KeyRandomStrategy.java b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/openai/function/KeyRandomStrategy.java
new file mode 100644
index 00000000..8251db3c
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/openai/function/KeyRandomStrategy.java
@@ -0,0 +1,19 @@
+package com.xmzs.common.chat.openai.function;
+
+import cn.hutool.core.util.RandomUtil;
+
+import java.util.List;
+
+/**
+ * 描述:随机策略
+ *
+ * @author https:www.unfbx.com
+ * @since 2023-04-03
+ */
+public class KeyRandomStrategy implements KeyStrategyFunction, String> {
+
+ @Override
+ public String apply(List apiKeys) {
+ return RandomUtil.randomEle(apiKeys);
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/openai/function/KeyStrategyFunction.java b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/openai/function/KeyStrategyFunction.java
new file mode 100644
index 00000000..97d90a89
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/openai/function/KeyStrategyFunction.java
@@ -0,0 +1,24 @@
+package com.xmzs.common.chat.openai.function;
+
+import java.util.function.Function;
+
+/**
+ * 描述:key 的获取策略
+ * jdk默认实现
+ * @see Function
+ *
+ * @author https:www.unfbx.com
+ * @since 2023-04-03
+ */
+@FunctionalInterface
+public interface KeyStrategyFunction {
+
+ /**
+ * Applies this function to the given argument.
+ *
+ * @param t the function argument
+ * @return the function result
+ */
+ R apply(T t);
+
+}
diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/openai/interceptor/DefaultOpenAiAuthInterceptor.java b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/openai/interceptor/DefaultOpenAiAuthInterceptor.java
new file mode 100644
index 00000000..fb17ccf6
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/openai/interceptor/DefaultOpenAiAuthInterceptor.java
@@ -0,0 +1,65 @@
+package com.xmzs.common.chat.openai.interceptor;
+
+import lombok.extern.slf4j.Slf4j;
+import okhttp3.Request;
+import okhttp3.Response;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 描述:请求增加header apikey
+ *
+ * @author https:www.unfbx.com
+ * @since 2023-03-23
+ */
+@Slf4j
+public class DefaultOpenAiAuthInterceptor extends OpenAiAuthInterceptor {
+ /**
+ * 请求头处理
+ */
+ public DefaultOpenAiAuthInterceptor() {
+ super.setWarringConfig(null);
+ }
+
+ /**
+ * 构造方法
+ *
+ * @param warringConfig 所有的key都失效后的告警参数配置
+ */
+ public DefaultOpenAiAuthInterceptor(Map warringConfig) {
+ super.setWarringConfig(warringConfig);
+ }
+
+ /**
+ * 拦截器鉴权
+ *
+ * @param chain Chain
+ * @return Response对象
+ * @throws IOException io异常
+ */
+ @Override
+ public Response intercept(Chain chain) throws IOException {
+ Request original = chain.request();
+ return chain.proceed(auth(super.getKey(), original));
+ }
+
+ /**
+ * key失效或者禁用后的处理逻辑
+ * 默认不处理
+ *
+ * @param apiKey 返回新的api keys集合
+ * @return 新的apiKey集合
+ */
+ @Override
+ protected List onErrorDealApiKeys(String apiKey) {
+ return super.getApiKey();
+ }
+
+ @Override
+ protected void noHaveActiveKeyWarring() {
+ log.error("--------> [告警] 没有可用的key!!!");
+ return;
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/openai/interceptor/DynamicKeyOpenAiAuthInterceptor.java b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/openai/interceptor/DynamicKeyOpenAiAuthInterceptor.java
new file mode 100644
index 00000000..7deba375
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/openai/interceptor/DynamicKeyOpenAiAuthInterceptor.java
@@ -0,0 +1,109 @@
+package com.xmzs.common.chat.openai.interceptor;
+
+import cn.hutool.json.JSONUtil;
+import lombok.Getter;
+import lombok.extern.slf4j.Slf4j;
+import okhttp3.Request;
+import okhttp3.Response;
+import com.xmzs.common.core.exception.base.BaseException;
+import com.xmzs.common.chat.entity.common.OpenAiResponse;
+import com.xmzs.common.chat.openai.exception.CommonError;
+
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+/**
+ * 描述:动态处理key的鉴权拦截器
+ *
+ * @author https:www.unfbx.com
+ * @since 2023-04-25
+ */
+@Getter
+@Slf4j
+public class DynamicKeyOpenAiAuthInterceptor extends OpenAiAuthInterceptor {
+ /**
+ * 账号被封了
+ */
+ private static final String ACCOUNT_DEACTIVATED = "account_deactivated";
+ /**
+ * key不正确
+ */
+ private static final String INVALID_API_KEY = "invalid_api_key";
+
+ /**
+ * 请求头处理
+ *
+ */
+ public DynamicKeyOpenAiAuthInterceptor() {
+ this.setWarringConfig(null);
+ }
+
+ /**
+ * 构造方法
+ *
+ * @param warringConfig 所有的key都失效后的告警参数配置
+ */
+ public DynamicKeyOpenAiAuthInterceptor(Map warringConfig) {
+ this.setWarringConfig(warringConfig);
+ }
+
+ @Override
+ public Response intercept(Chain chain) throws IOException {
+ String key = getKey();
+ Request original = chain.request();
+ Request request = this.auth(key, original);
+ Response response = chain.proceed(request);
+ if (!response.isSuccessful()) {
+ String errorMsg = response.body().string();
+ if (response.code() == CommonError.OPENAI_AUTHENTICATION_ERROR.code()
+ || response.code() == CommonError.OPENAI_LIMIT_ERROR.code()
+ || response.code() == CommonError.OPENAI_SERVER_ERROR.code()) {
+ OpenAiResponse openAiResponse = JSONUtil.toBean(errorMsg, OpenAiResponse.class);
+ String errorCode = openAiResponse.getError().getCode();
+ log.error("--------> 请求openai异常,错误code:{}", errorCode);
+ log.error("--------> 请求异常:{}", errorMsg);
+ //账号被封或者key不正确就移除掉
+ if (ACCOUNT_DEACTIVATED.equals(errorCode) || INVALID_API_KEY.equals(errorCode)) {
+ super.setApiKey(this.onErrorDealApiKeys(key));
+ }
+ throw new BaseException(openAiResponse.getError().getMessage());
+ }
+ //非官方定义的错误code
+ log.error("--------> 请求异常:{}", errorMsg);
+ OpenAiResponse openAiResponse = JSONUtil.toBean(errorMsg, OpenAiResponse.class);
+ if (Objects.nonNull(openAiResponse.getError())) {
+ log.error(openAiResponse.getError().getMessage());
+ throw new BaseException(openAiResponse.getError().getMessage());
+ }
+ throw new BaseException(CommonError.RETRY_ERROR.msg());
+ }
+ return response;
+ }
+
+
+ @Override
+ protected List onErrorDealApiKeys(String errorKey) {
+ List apiKey = super.getApiKey().stream().filter(e -> !errorKey.equals(e)).collect(Collectors.toList());
+ log.error("--------> 当前ApiKey:[{}] 失效了,移除!", errorKey);
+ return apiKey;
+ }
+
+ /**
+ * 所有的key都失效后,自定义预警配置
+ * 不配置直接return
+ */
+ @Override
+ protected void noHaveActiveKeyWarring() {
+ log.error("--------> [告警] 没有可用的key!!!");
+ return;
+ }
+
+ @Override
+ public Request auth(String key, Request original) {
+ return super.auth(key, original);
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/openai/interceptor/OpenAILogger.java b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/openai/interceptor/OpenAILogger.java
new file mode 100644
index 00000000..5c24efb2
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/openai/interceptor/OpenAILogger.java
@@ -0,0 +1,18 @@
+package com.xmzs.common.chat.openai.interceptor;
+
+import lombok.extern.slf4j.Slf4j;
+import okhttp3.logging.HttpLoggingInterceptor;
+
+/**
+ * 描述: 日志
+ *
+ * @author https:www.unfbx.com
+ * 2023-02-28
+ */
+@Slf4j
+public class OpenAILogger implements HttpLoggingInterceptor.Logger {
+ @Override
+ public void log(String message) {
+ log.info("OkHttp-------->:{}", message);
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/openai/interceptor/OpenAiAuthInterceptor.java b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/openai/interceptor/OpenAiAuthInterceptor.java
new file mode 100644
index 00000000..f40d782a
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/openai/interceptor/OpenAiAuthInterceptor.java
@@ -0,0 +1,85 @@
+package com.xmzs.common.chat.openai.interceptor;
+
+
+import cn.hutool.core.collection.CollectionUtil;
+import cn.hutool.http.ContentType;
+import cn.hutool.http.Header;
+import lombok.Getter;
+import lombok.Setter;
+import okhttp3.Interceptor;
+import okhttp3.Request;
+import com.xmzs.common.core.exception.base.BaseException;
+import com.xmzs.common.chat.openai.exception.CommonError;
+import com.xmzs.common.chat.openai.function.KeyStrategyFunction;
+
+
+import java.util.List;
+import java.util.Map;
+
+public abstract class OpenAiAuthInterceptor implements Interceptor {
+
+
+ /**
+ * key 集合
+ */
+ @Getter
+ @Setter
+ private List apiKey;
+ /**
+ * 自定义的key的使用策略
+ */
+ @Getter
+ @Setter
+ private KeyStrategyFunction, String> keyStrategy;
+
+ /**
+ * 预警触发参数配置,配置参数实现飞书、钉钉、企业微信、邮箱预警等功能
+ */
+ @Getter
+ @Setter
+ private Map warringConfig;
+
+ /**
+ * 自定义apiKeys的处理逻辑
+ *
+ * @param errorKey 错误的key
+ * @return 返回值是新的apiKeys
+ */
+ protected abstract List onErrorDealApiKeys(String errorKey);
+
+ /**
+ * 所有的key都失效后,自定义预警配置
+ * 可以通过warringConfig配置参数实现飞书、钉钉、企业微信、邮箱预警等
+ */
+ protected abstract void noHaveActiveKeyWarring();
+
+
+ /**
+ * 获取请求key
+ *
+ * @return key
+ */
+ public final String getKey() {
+ if (CollectionUtil.isEmpty(apiKey)) {
+ this.noHaveActiveKeyWarring();
+ throw new BaseException(CommonError.NO_ACTIVE_API_KEYS.msg());
+ }
+ return keyStrategy.apply(apiKey);
+ }
+
+ /**
+ * 默认的鉴权处理方法
+ *
+ * @param key api key
+ * @param original 源请求体
+ * @return 请求体
+ */
+ public Request auth(String key, Request original) {
+ Request request = original.newBuilder()
+ .header(Header.AUTHORIZATION.getValue(), "Bearer " + key)
+ .header(Header.CONTENT_TYPE.getValue(), ContentType.JSON.getValue())
+ .method(original.method(), original.body())
+ .build();
+ return request;
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/openai/interceptor/OpenAiResponseInterceptor.java b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/openai/interceptor/OpenAiResponseInterceptor.java
new file mode 100644
index 00000000..4d3ade56
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/openai/interceptor/OpenAiResponseInterceptor.java
@@ -0,0 +1,48 @@
+package com.xmzs.common.chat.openai.interceptor;
+
+import cn.hutool.json.JSONUtil;
+import lombok.extern.slf4j.Slf4j;
+import okhttp3.Interceptor;
+import okhttp3.Request;
+import okhttp3.Response;
+import com.xmzs.common.core.exception.base.BaseException;
+import com.xmzs.common.chat.entity.common.OpenAiResponse;
+import com.xmzs.common.chat.openai.exception.CommonError;
+
+
+import java.io.IOException;
+import java.util.Objects;
+
+/**
+ * 描述:openai 返回值处理Interceptor
+ *
+ * @author https:www.unfbx.com
+ * @since 2023-03-23
+ */
+@Slf4j
+public class OpenAiResponseInterceptor implements Interceptor {
+ @Override
+ public Response intercept(Chain chain) throws IOException {
+
+ Request original = chain.request();
+ Response response = chain.proceed(original);
+ if (!response.isSuccessful()) {
+ if (response.code() == CommonError.OPENAI_AUTHENTICATION_ERROR.code()
+ || response.code() == CommonError.OPENAI_LIMIT_ERROR.code()
+ || response.code() == CommonError.OPENAI_SERVER_ERROR.code()) {
+ OpenAiResponse openAiResponse = JSONUtil.toBean(response.body().string(), OpenAiResponse.class);
+ log.error(openAiResponse.getError().getMessage());
+ throw new BaseException(openAiResponse.getError().getMessage());
+ }
+ String errorMsg = response.body().string();
+ log.error("--------> 请求异常:{}", errorMsg);
+ OpenAiResponse openAiResponse = JSONUtil.toBean(errorMsg, OpenAiResponse.class);
+ if (Objects.nonNull(openAiResponse.getError())) {
+ log.error(openAiResponse.getError().getMessage());
+ throw new BaseException(openAiResponse.getError().getMessage());
+ }
+ throw new BaseException(CommonError.RETRY_ERROR.msg());
+ }
+ return response;
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/utils/TikTokensUtil.java b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/utils/TikTokensUtil.java
new file mode 100644
index 00000000..34fd90e9
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/utils/TikTokensUtil.java
@@ -0,0 +1,234 @@
+package com.xmzs.common.chat.utils;
+
+import cn.hutool.core.util.StrUtil;
+import com.knuddels.jtokkit.Encodings;
+import com.knuddels.jtokkit.api.Encoding;
+import com.knuddels.jtokkit.api.EncodingRegistry;
+import com.knuddels.jtokkit.api.EncodingType;
+import com.knuddels.jtokkit.api.ModelType;
+import lombok.extern.slf4j.Slf4j;
+
+import com.xmzs.common.chat.entity.chat.ChatCompletion;
+import com.xmzs.common.chat.entity.chat.FunctionCall;
+import com.xmzs.common.chat.entity.chat.Message;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.*;
+
+/**
+ * 描述:token计算工具类
+ *
+ * @author https:www.unfbx.com
+ * @since 2023-04-04
+ */
+@Slf4j
+public class TikTokensUtil {
+ /**
+ * 模型名称对应Encoding
+ */
+ private static final Map modelMap = new HashMap<>();
+ /**
+ * registry实例
+ */
+ private static final EncodingRegistry registry = Encodings.newDefaultEncodingRegistry();
+
+ static {
+ for (ModelType modelType : ModelType.values()) {
+ modelMap.put(modelType.getName(), registry.getEncodingForModel(modelType));
+ }
+ modelMap.put(ChatCompletion.Model.GPT_3_5_TURBO_0613.getName(), registry.getEncodingForModel(ModelType.GPT_3_5_TURBO));
+ modelMap.put(ChatCompletion.Model.GPT_3_5_TURBO_16K.getName(), registry.getEncodingForModel(ModelType.GPT_3_5_TURBO));
+ modelMap.put(ChatCompletion.Model.GPT_3_5_TURBO_16K_0613.getName(), registry.getEncodingForModel(ModelType.GPT_3_5_TURBO));
+ modelMap.put(ChatCompletion.Model.GPT_4_32K.getName(), registry.getEncodingForModel(ModelType.GPT_4));
+ modelMap.put(ChatCompletion.Model.GPT_4_0613.getName(), registry.getEncodingForModel(ModelType.GPT_4));
+ modelMap.put(ChatCompletion.Model.GPT_4_32K_0613.getName(), registry.getEncodingForModel(ModelType.GPT_4));
+ modelMap.put(ChatCompletion.Model.GPT_4_1106_PREVIEW.getName(), registry.getEncodingForModel(ModelType.GPT_4));
+ modelMap.put(ChatCompletion.Model.GPT_4_VISION_PREVIEW.getName(), registry.getEncodingForModel(ModelType.GPT_4));
+ }
+
+ /**
+ * 通过Encoding和text获取编码数组
+ *
+ * @param enc Encoding类型
+ * @param text 文本信息
+ * @return 编码数组
+ */
+ public static List encode(@NotNull Encoding enc, String text) {
+ return StrUtil.isBlank(text) ? new ArrayList<>() : enc.encode(text);
+ }
+
+ /**
+ * 通过Encoding计算text信息的tokens
+ *
+ * @param enc Encoding类型
+ * @param text 文本信息
+ * @return tokens数量
+ */
+ public static int tokens(@NotNull Encoding enc, String text) {
+ return encode(enc, text).size();
+ }
+
+
+ /**
+ * 通过Encoding和encoded数组反推text信息
+ *
+ * @param enc Encoding
+ * @param encoded 编码数组
+ * @return 编码数组对应的文本信息
+ */
+ public static String decode(@NotNull Encoding enc, @NotNull List encoded) {
+ return enc.decode(encoded);
+ }
+
+ /**
+ * 获取一个Encoding对象,通过Encoding类型
+ *
+ * @param encodingType encodingType
+ * @return Encoding
+ */
+ public static Encoding getEncoding(@NotNull EncodingType encodingType) {
+ return registry.getEncoding(encodingType);
+ }
+
+ /**
+ * 获取encode的编码数组
+ *
+ * @param text 文本信息
+ * @return 编码数组
+ */
+ public static List encode(@NotNull EncodingType encodingType, String text) {
+ if (StrUtil.isBlank(text)) {
+ return new ArrayList<>();
+ }
+ Encoding enc = getEncoding(encodingType);
+ return enc.encode(text);
+ }
+
+ /**
+ * 计算指定字符串的tokens,通过EncodingType
+ *
+ * @param encodingType encodingType
+ * @param text 文本信息
+ * @return tokens数量
+ */
+ public static int tokens(@NotNull EncodingType encodingType, String text) {
+ return encode(encodingType, text).size();
+ }
+
+
+ /**
+ * 通过EncodingType和encoded编码数组,反推字符串文本
+ *
+ * @param encodingType encodingType
+ * @param encoded 编码数组
+ * @return 编码数组对应的字符串
+ */
+ public static String decode(@NotNull EncodingType encodingType, @NotNull List encoded) {
+ Encoding enc = getEncoding(encodingType);
+ return enc.decode(encoded);
+ }
+
+
+ /**
+ * 获取一个Encoding对象,通过模型名称
+ *
+ * @param modelName 模型名称
+ * @return Encoding
+ */
+ public static Encoding getEncoding(@NotNull String modelName) {
+ return modelMap.get(modelName);
+ }
+
+ /**
+ * 获取encode的编码数组,通过模型名称
+ *
+ * @param text 文本信息
+ * @return 编码数组
+ */
+ public static List encode(@NotNull String modelName, String text) {
+ if (StrUtil.isBlank(text)) {
+ return new ArrayList<>();
+ }
+ Encoding enc = getEncoding(modelName);
+ if (Objects.isNull(enc)) {
+ log.warn("[{}]模型不存在或者暂不支持计算tokens,直接返回tokens==0",modelName);
+ return new ArrayList<>();
+ }
+ return enc.encode(text);
+ }
+
+ /**
+ * 通过模型名称, 计算指定字符串的tokens
+ *
+ * @param modelName 模型名称
+ * @param text 文本信息
+ * @return tokens数量
+ */
+ public static int tokens(@NotNull String modelName, String text) {
+ return encode(modelName, text).size();
+ }
+
+
+ /**
+ * 通过模型名称计算messages获取编码数组
+ * 参考官方的处理逻辑:
+ * https://github.com/openai/openai-cookbook/blob/main/examples/How_to_count_tokens_with_tiktoken.ipynb
+ *
+ * @param modelName 模型名称
+ * @param messages 消息体
+ * @return tokens数量
+ */
+ public static int tokens(@NotNull String modelName, @NotNull List messages) {
+ Encoding encoding = getEncoding(modelName);
+ int tokensPerMessage = 0;
+ int tokensPerName = 0;
+ if (modelName.equals(ChatCompletion.Model.GPT_3_5_TURBO_0613.getName())
+ || modelName.equals(ChatCompletion.Model.GPT_3_5_TURBO_16K_0613.getName())
+ || modelName.equals(ChatCompletion.Model.GPT_4_0613.getName())
+ || modelName.equals(ChatCompletion.Model.GPT_4_32K_0613.getName())
+ || modelName.equals(ChatCompletion.Model.GPT_4_1106_PREVIEW.getName())
+ || modelName.equals(ChatCompletion.Model.GPT_4_VISION_PREVIEW.getName())
+ ) {
+ tokensPerMessage = 3;
+ tokensPerName = 1;
+ }else if(modelName.contains(ChatCompletion.Model.GPT_3_5_TURBO.getName())){
+ //"gpt-3.5-turbo" in model:
+ log.warn("Warning: gpt-3.5-turbo may update over time. Returning num tokens assuming gpt-3.5-turbo-0613.");
+ tokensPerMessage = 3;
+ tokensPerName = 1;
+ }else if(modelName.contains(ChatCompletion.Model.GPT_4.getName())){
+ log.warn("Warning: gpt-4 may update over time. Returning num tokens assuming gpt-4-0613.");
+ tokensPerMessage = 3;
+ tokensPerName = 1;
+ }else {
+ log.warn("不支持的model {}. See https://github.com/openai/openai-python/blob/main/chatml.md 更多信息.",modelName);
+ }
+ int sum = 0;
+ for (Message msg : messages) {
+ sum += tokensPerMessage;
+ sum += tokens(encoding, msg.getContent());
+ sum += tokens(encoding, msg.getRole());
+ sum += tokens(encoding, msg.getName());
+ FunctionCall functionCall = msg.getFunctionCall();
+ sum += Objects.isNull(functionCall) ? 0 : tokens(encoding, functionCall.toString());
+ if (StrUtil.isNotBlank(msg.getName())) {
+ sum += tokensPerName;
+ }
+ }
+ sum += 3;
+ return sum;
+ }
+
+ /**
+ * 通过模型名称和encoded编码数组,反推字符串文本
+ *
+ * @param modelName 模型名
+ * @param encoded 编码数组
+ * @return 返回源文本
+ */
+ public static String decode(@NotNull String modelName, @NotNull List encoded) {
+ Encoding enc = getEncoding(modelName);
+ return enc.decode(encoded);
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/utils/WebSocketUtils.java b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/utils/WebSocketUtils.java
new file mode 100644
index 00000000..1e71341f
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/utils/WebSocketUtils.java
@@ -0,0 +1,98 @@
+package com.xmzs.common.chat.utils;
+
+import cn.hutool.core.collection.CollUtil;
+import com.xmzs.common.redis.utils.RedisUtils;
+import com.xmzs.common.chat.entity.dto.WebSocketMessageDto;
+import com.xmzs.common.chat.holder.WebSocketSessionHolder;
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.socket.PongMessage;
+import org.springframework.web.socket.TextMessage;
+import org.springframework.web.socket.WebSocketMessage;
+import org.springframework.web.socket.WebSocketSession;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Consumer;
+
+import static com.xmzs.common.chat.constant.WebSocketConstants.WEB_SOCKET_TOPIC;
+
+/**
+ * 工具类
+ *
+ * @author zendwang
+ */
+@Slf4j
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public class WebSocketUtils {
+
+ /**
+ * 发送消息
+ *
+ * @param sessionKey session主键 一般为用户id
+ * @param message 消息文本
+ */
+ public static void sendMessage(Long sessionKey, String message) {
+ WebSocketSession session = WebSocketSessionHolder.getSessions(sessionKey);
+ sendMessage(session, message);
+ }
+
+ /**
+ * 订阅消息
+ *
+ * @param consumer 自定义处理
+ */
+ public static void subscribeMessage(Consumer consumer) {
+ RedisUtils.subscribe(WEB_SOCKET_TOPIC, WebSocketMessageDto.class, consumer);
+ }
+
+ /**
+ * 发布订阅的消息
+ *
+ * @param webSocketMessage 消息对象
+ */
+ public static void publishMessage(WebSocketMessageDto webSocketMessage) {
+ List unsentSessionKeys = new ArrayList<>();
+ // 当前服务内session,直接发送消息
+ for (Long sessionKey : webSocketMessage.getSessionKeys()) {
+ if (WebSocketSessionHolder.existSession(sessionKey)) {
+ WebSocketUtils.sendMessage(sessionKey, webSocketMessage.getMessage());
+ continue;
+ }
+ unsentSessionKeys.add(sessionKey);
+ }
+ // 不在当前服务内session,发布订阅消息
+ if (CollUtil.isNotEmpty(unsentSessionKeys)) {
+ WebSocketMessageDto broadcastMessage = new WebSocketMessageDto();
+ broadcastMessage.setMessage(webSocketMessage.getMessage());
+ broadcastMessage.setSessionKeys(unsentSessionKeys);
+ RedisUtils.publish(WEB_SOCKET_TOPIC, broadcastMessage, consumer -> {
+ log.info(" WebSocket发送主题订阅消息topic:{} session keys:{} message:{}",
+ WEB_SOCKET_TOPIC, unsentSessionKeys, webSocketMessage.getMessage());
+ });
+ }
+ }
+
+ public static void sendPongMessage(WebSocketSession session) {
+ sendMessage(session, new PongMessage());
+ }
+
+ public static void sendMessage(WebSocketSession session, String message) {
+ sendMessage(session, new TextMessage(message));
+ }
+
+ private static void sendMessage(WebSocketSession session, WebSocketMessage> message) {
+ if (session == null || !session.isOpen()) {
+ log.error("[send] session会话已经关闭");
+ } else {
+ try {
+ // 获取当前会话中的用户
+ session.sendMessage(message);
+ } catch (IOException e) {
+ log.error("[send] session({}) 发送消息({}) 异常", session, message, e);
+ }
+ }
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-chat/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/ruoyi-common/ruoyi-common-chat/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 00000000..25060231
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-chat/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1 @@
+com.xmzs.common.chat.config.WebSocketConfig
diff --git a/ruoyi-common/ruoyi-common-core/pom.xml b/ruoyi-common/ruoyi-common-core/pom.xml
new file mode 100644
index 00000000..020c13ea
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/pom.xml
@@ -0,0 +1,129 @@
+
+
+
+ com.xmzs
+ ruoyi-common
+ ${revision}
+ ../pom.xml
+
+ 4.0.0
+
+ ruoyi-common-core
+
+
+ ruoyi-common-core 核心模块
+
+
+
+
+
+ org.springframework
+ spring-context-support
+
+
+
+
+ org.springframework
+ spring-web
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
+
+
+
+ org.apache.commons
+ commons-lang3
+
+
+
+
+ jakarta.servlet
+ jakarta.servlet-api
+
+
+
+ cn.hutool
+ hutool-core
+
+
+
+ cn.hutool
+ hutool-http
+
+
+
+ cn.hutool
+ hutool-extra
+
+
+
+ cn.hutool
+ hutool-json
+ provided
+
+
+
+ org.projectlombok
+ lombok
+
+
+
+
+ org.springframework.boot
+ spring-boot-configuration-processor
+
+
+
+ org.springframework.boot
+ spring-boot-properties-migrator
+ runtime
+
+
+
+ io.github.linpeilie
+ mapstruct-plus-spring-boot-starter
+
+
+
+
+ org.lionsoul
+ ip2region
+
+
+
+
+ com.github.binarywang
+ weixin-java-miniapp
+ ${weixin-java-miniapp.version}
+
+
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+
+
+
+ com.squareup.okhttp3
+ okhttp
+
+
+
+ com.squareup.okhttp3
+ okhttp-sse
+
+
+
+ com.squareup.okhttp3
+ logging-interceptor
+
+
+
+
+
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/config/ApplicationConfig.java b/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/config/ApplicationConfig.java
new file mode 100644
index 00000000..96c5d3d5
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/config/ApplicationConfig.java
@@ -0,0 +1,16 @@
+package com.xmzs.common.core.config;
+
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.context.annotation.EnableAspectJAutoProxy;
+
+/**
+ * 程序注解配置
+ *
+ * @author Lion Li
+ */
+@AutoConfiguration
+// 表示通过aop框架暴露该代理对象,AopContext能够访问
+@EnableAspectJAutoProxy(exposeProxy = true)
+public class ApplicationConfig {
+
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/config/AsyncConfig.java b/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/config/AsyncConfig.java
new file mode 100644
index 00000000..294a5165
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/config/AsyncConfig.java
@@ -0,0 +1,54 @@
+package com.xmzs.common.core.config;
+
+import cn.hutool.core.util.ArrayUtil;
+import com.xmzs.common.core.exception.ServiceException;
+import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.scheduling.annotation.AsyncConfigurer;
+import org.springframework.scheduling.annotation.EnableAsync;
+
+import java.util.Arrays;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ScheduledExecutorService;
+
+/**
+ * 异步配置
+ *
+ * @author Lion Li
+ */
+@EnableAsync(proxyTargetClass = true)
+@AutoConfiguration
+public class AsyncConfig implements AsyncConfigurer {
+
+ @Autowired
+ @Qualifier("scheduledExecutorService")
+ private ScheduledExecutorService scheduledExecutorService;
+
+ /**
+ * 自定义 @Async 注解使用系统线程池
+ */
+ @Override
+ public Executor getAsyncExecutor() {
+ return scheduledExecutorService;
+ }
+
+ /**
+ * 异步执行异常处理
+ */
+ @Override
+ public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
+ return (throwable, method, objects) -> {
+ throwable.printStackTrace();
+ StringBuilder sb = new StringBuilder();
+ sb.append("Exception message - ").append(throwable.getMessage())
+ .append(", Method name - ").append(method.getName());
+ if (ArrayUtil.isNotEmpty(objects)) {
+ sb.append(", Parameter value - ").append(Arrays.toString(objects));
+ }
+ throw new ServiceException(sb.toString());
+ };
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/config/RuoYiConfig.java b/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/config/RuoYiConfig.java
new file mode 100644
index 00000000..a3f5195f
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/config/RuoYiConfig.java
@@ -0,0 +1,49 @@
+package com.xmzs.common.core.config;
+
+import lombok.Data;
+import lombok.Getter;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+/**
+ * 读取项目相关配置
+ *
+ * @author Lion Li
+ */
+
+@Data
+@Component
+@ConfigurationProperties(prefix = "ruoyi")
+public class RuoYiConfig {
+
+ /**
+ * 项目名称
+ */
+ private String name;
+
+ /**
+ * 版本
+ */
+ private String version;
+
+ /**
+ * 版权年份
+ */
+ private String copyrightYear;
+
+ /**
+ * 实例演示开关
+ */
+ private boolean demoEnabled;
+
+ /**
+ * 获取地址开关
+ */
+ @Getter
+ private static boolean addressEnabled;
+
+ public void setAddressEnabled(boolean addressEnabled) {
+ RuoYiConfig.addressEnabled = addressEnabled;
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/config/ThreadPoolConfig.java b/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/config/ThreadPoolConfig.java
new file mode 100644
index 00000000..e674ce11
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/config/ThreadPoolConfig.java
@@ -0,0 +1,57 @@
+package com.xmzs.common.core.config;
+
+import com.xmzs.common.core.config.properties.ThreadPoolProperties;
+import com.xmzs.common.core.utils.Threads;
+import org.apache.commons.lang3.concurrent.BasicThreadFactory;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
+
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.ThreadPoolExecutor;
+
+/**
+ * 线程池配置
+ *
+ * @author Lion Li
+ **/
+@AutoConfiguration
+@EnableConfigurationProperties(ThreadPoolProperties.class)
+public class ThreadPoolConfig {
+
+ /**
+ * 核心线程数 = cpu 核心数 + 1
+ */
+ private final int core = Runtime.getRuntime().availableProcessors() + 1;
+
+ @Bean(name = "threadPoolTaskExecutor")
+ @ConditionalOnProperty(prefix = "thread-pool", name = "enabled", havingValue = "true")
+ public ThreadPoolTaskExecutor threadPoolTaskExecutor(ThreadPoolProperties threadPoolProperties) {
+ ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
+ executor.setCorePoolSize(core);
+ executor.setMaxPoolSize(core * 2);
+ executor.setQueueCapacity(threadPoolProperties.getQueueCapacity());
+ executor.setKeepAliveSeconds(threadPoolProperties.getKeepAliveSeconds());
+ executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
+ return executor;
+ }
+
+ /**
+ * 执行周期性或定时任务
+ */
+ @Bean(name = "scheduledExecutorService")
+ protected ScheduledExecutorService scheduledExecutorService() {
+ return new ScheduledThreadPoolExecutor(core,
+ new BasicThreadFactory.Builder().namingPattern("schedule-pool-%d").daemon(true).build(),
+ new ThreadPoolExecutor.CallerRunsPolicy()) {
+ @Override
+ protected void afterExecute(Runnable r, Throwable t) {
+ super.afterExecute(r, t);
+ Threads.printException(r, t);
+ }
+ };
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/config/ValidatorConfig.java b/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/config/ValidatorConfig.java
new file mode 100644
index 00000000..a50c9da9
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/config/ValidatorConfig.java
@@ -0,0 +1,39 @@
+package com.xmzs.common.core.config;
+
+import jakarta.validation.Validator;
+import org.hibernate.validator.HibernateValidator;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.context.MessageSource;
+import org.springframework.context.annotation.Bean;
+import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
+
+import java.util.Properties;
+
+/**
+ * 校验框架配置类
+ *
+ * @author Lion Li
+ */
+@AutoConfiguration
+public class ValidatorConfig {
+
+ /**
+ * 配置校验框架 快速返回模式
+ */
+ @Bean
+ public Validator validator(MessageSource messageSource) {
+ LocalValidatorFactoryBean factoryBean = new LocalValidatorFactoryBean();
+ // 国际化
+ factoryBean.setValidationMessageSource(messageSource);
+ // 设置使用 HibernateValidator 校验器
+ factoryBean.setProviderClass(HibernateValidator.class);
+ Properties properties = new Properties();
+ // 设置 快速异常返回
+ properties.setProperty("hibernate.validator.fail_fast", "true");
+ factoryBean.setValidationProperties(properties);
+ // 加载配置
+ factoryBean.afterPropertiesSet();
+ return factoryBean.getValidator();
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/config/properties/ThreadPoolProperties.java b/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/config/properties/ThreadPoolProperties.java
new file mode 100644
index 00000000..50366ace
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/config/properties/ThreadPoolProperties.java
@@ -0,0 +1,30 @@
+package com.xmzs.common.core.config.properties;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+/**
+ * 线程池 配置属性
+ *
+ * @author Lion Li
+ */
+@Data
+@ConfigurationProperties(prefix = "thread-pool")
+public class ThreadPoolProperties {
+
+ /**
+ * 是否开启线程池
+ */
+ private boolean enabled;
+
+ /**
+ * 队列最大长度
+ */
+ private int queueCapacity;
+
+ /**
+ * 线程池维护线程所允许的空闲时间
+ */
+ private int keepAliveSeconds;
+
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/constant/CacheConstants.java b/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/constant/CacheConstants.java
new file mode 100644
index 00000000..13e0902e
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/constant/CacheConstants.java
@@ -0,0 +1,25 @@
+package com.xmzs.common.core.constant;
+
+/**
+ * 缓存的key 常量
+ *
+ * @author Lion Li
+ */
+public interface CacheConstants {
+
+ /**
+ * 在线用户 redis key
+ */
+ String ONLINE_TOKEN_KEY = "online_tokens:";
+
+ /**
+ * 参数管理 cache key
+ */
+ String SYS_CONFIG_KEY = "sys_config:";
+
+ /**
+ * 字典管理 cache key
+ */
+ String SYS_DICT_KEY = "sys_dict:";
+
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/constant/CacheNames.java b/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/constant/CacheNames.java
new file mode 100644
index 00000000..b9b48d73
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/constant/CacheNames.java
@@ -0,0 +1,63 @@
+package com.xmzs.common.core.constant;
+
+/**
+ * 缓存组名称常量
+ *
+ * key 格式为 cacheNames#ttl#maxIdleTime#maxSize
+ *
+ * ttl 过期时间 如果设置为0则不过期 默认为0
+ * maxIdleTime 最大空闲时间 根据LRU算法清理空闲数据 如果设置为0则不检测 默认为0
+ * maxSize 组最大长度 根据LRU算法清理溢出数据 如果设置为0则无限长 默认为0
+ *
+ * 例子: test#60s、test#0#60s、test#0#1m#1000、test#1h#0#500
+ *
+ * @author Lion Li
+ */
+public interface CacheNames {
+
+ /**
+ * 演示案例
+ */
+ String DEMO_CACHE = "demo:cache#60s#10m#20";
+
+ /**
+ * 系统配置
+ */
+ String SYS_CONFIG = "sys_config";
+
+ /**
+ * 数据字典
+ */
+ String SYS_DICT = "sys_dict";
+
+ /**
+ * 租户
+ */
+ String SYS_TENANT = GlobalConstants.GLOBAL_REDIS_KEY + "sys_tenant#30d";
+
+ /**
+ * 用户账户
+ */
+ String SYS_USER_NAME = "sys_user_name#30d";
+
+ /**
+ * 部门
+ */
+ String SYS_DEPT = "sys_dept#30d";
+
+ /**
+ * OSS内容
+ */
+ String SYS_OSS = "sys_oss#30d";
+
+ /**
+ * OSS配置
+ */
+ String SYS_OSS_CONFIG = "sys_oss_config";
+
+ /**
+ * 在线用户
+ */
+ String ONLINE_TOKEN = "online_tokens";
+
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/constant/Constants.java b/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/constant/Constants.java
new file mode 100644
index 00000000..d107f7c1
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/constant/Constants.java
@@ -0,0 +1,86 @@
+package com.xmzs.common.core.constant;
+
+/**
+ * 通用常量信息
+ *
+ * @author ruoyi
+ */
+public interface Constants {
+
+ /**
+ * UTF-8 字符集
+ */
+ String UTF8 = "UTF-8";
+
+ /**
+ * GBK 字符集
+ */
+ String GBK = "GBK";
+
+ /**
+ * www主域
+ */
+ String WWW = "www.";
+
+ /**
+ * http请求
+ */
+ String HTTP = "http://";
+
+ /**
+ * https请求
+ */
+ String HTTPS = "https://";
+
+ /**
+ * 通用成功标识
+ */
+ String SUCCESS = "0";
+
+ /**
+ * 通用失败标识
+ */
+ String FAIL = "1";
+
+ /**
+ * 登录成功
+ */
+ String LOGIN_SUCCESS = "Success";
+
+ /**
+ * 注销
+ */
+ String LOGOUT = "Logout";
+
+ /**
+ * 注册
+ */
+ String REGISTER = "Register";
+
+ /**
+ * 登录失败
+ */
+ String LOGIN_FAIL = "Error";
+
+ /**
+ * 验证码有效期(分钟)
+ */
+ Integer CAPTCHA_EXPIRATION = 20;
+
+ /**
+ * 令牌
+ */
+ String TOKEN = "token";
+
+ /**
+ * 顶级部门id
+ */
+ Long TOP_PARENT_ID = 0L;
+
+ /**
+ * 默认租户ID
+ **/
+ String TENANT_ID = "00000";
+
+}
+
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/constant/GlobalConstants.java b/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/constant/GlobalConstants.java
new file mode 100644
index 00000000..adedb2a2
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/constant/GlobalConstants.java
@@ -0,0 +1,34 @@
+package com.xmzs.common.core.constant;
+
+/**
+ * 全局的key常量 (业务无关的key)
+ *
+ * @author Lion Li
+ */
+public interface GlobalConstants {
+
+ /**
+ * 全局 redis key (业务无关的key)
+ */
+ String GLOBAL_REDIS_KEY = "global:";
+
+ /**
+ * 验证码 redis key
+ */
+ String CAPTCHA_CODE_KEY = GLOBAL_REDIS_KEY + "captcha_codes:";
+
+ /**
+ * 防重提交 redis key
+ */
+ String REPEAT_SUBMIT_KEY = GLOBAL_REDIS_KEY + "repeat_submit:";
+
+ /**
+ * 限流 redis key
+ */
+ String RATE_LIMIT_KEY = GLOBAL_REDIS_KEY + "rate_limit:";
+
+ /**
+ * 登录账户密码错误次数 redis key
+ */
+ String PWD_ERR_CNT_KEY = GLOBAL_REDIS_KEY + "pwd_err_cnt:";
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/constant/HttpStatus.java b/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/constant/HttpStatus.java
new file mode 100644
index 00000000..7e263c3f
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/constant/HttpStatus.java
@@ -0,0 +1,93 @@
+package com.xmzs.common.core.constant;
+
+/**
+ * 返回状态码
+ *
+ * @author Lion Li
+ */
+public interface HttpStatus {
+ /**
+ * 操作成功
+ */
+ int SUCCESS = 200;
+
+ /**
+ * 对象创建成功
+ */
+ int CREATED = 201;
+
+ /**
+ * 请求已经被接受
+ */
+ int ACCEPTED = 202;
+
+ /**
+ * 操作已经执行成功,但是没有返回数据
+ */
+ int NO_CONTENT = 204;
+
+ /**
+ * 资源已被移除
+ */
+ int MOVED_PERM = 301;
+
+ /**
+ * 重定向
+ */
+ int SEE_OTHER = 303;
+
+ /**
+ * 资源没有被修改
+ */
+ int NOT_MODIFIED = 304;
+
+ /**
+ * 参数列表错误(缺少,格式不匹配)
+ */
+ int BAD_REQUEST = 400;
+
+ /**
+ * 未授权
+ */
+ int UNAUTHORIZED = 401;
+
+ /**
+ * 访问受限,授权过期
+ */
+ int FORBIDDEN = 403;
+
+ /**
+ * 资源,服务未找到
+ */
+ int NOT_FOUND = 404;
+
+ /**
+ * 不允许的http方法
+ */
+ int BAD_METHOD = 405;
+
+ /**
+ * 资源冲突,或者资源被锁
+ */
+ int CONFLICT = 409;
+
+ /**
+ * 不支持的数据,媒体类型
+ */
+ int UNSUPPORTED_TYPE = 415;
+
+ /**
+ * 系统内部错误
+ */
+ int ERROR = 500;
+
+ /**
+ * 接口未实现
+ */
+ int NOT_IMPLEMENTED = 501;
+
+ /**
+ * 系统警告消息
+ */
+ int WARN = 601;
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/constant/TenantConstants.java b/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/constant/TenantConstants.java
new file mode 100644
index 00000000..0c54462d
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/constant/TenantConstants.java
@@ -0,0 +1,45 @@
+package com.xmzs.common.core.constant;
+
+/**
+ * 租户常量信息
+ *
+ * @author Lion Li
+ */
+public interface TenantConstants {
+
+ /**
+ * 租户正常状态
+ */
+ String NORMAL = "0";
+
+ /**
+ * 租户封禁状态
+ */
+ String DISABLE = "1";
+
+ /**
+ * 超级管理员ID
+ */
+ Long SUPER_ADMIN_ID = 1L;
+
+ /**
+ * 超级管理员角色 roleKey
+ */
+ String SUPER_ADMIN_ROLE_KEY = "superadmin";
+
+ /**
+ * 租户管理员角色 roleKey
+ */
+ String TENANT_ADMIN_ROLE_KEY = "admin";
+
+ /**
+ * 租户管理员角色名称
+ */
+ String TENANT_ADMIN_ROLE_NAME = "管理员";
+
+ /**
+ * 默认租户ID
+ */
+ String DEFAULT_TENANT_ID = "000000";
+
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/constant/UserConstants.java b/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/constant/UserConstants.java
new file mode 100644
index 00000000..5eb5a9d1
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/constant/UserConstants.java
@@ -0,0 +1,132 @@
+package com.xmzs.common.core.constant;
+
+/**
+ * 用户常量信息
+ *
+ * @author ruoyi
+ */
+public interface UserConstants {
+
+ /**
+ * 平台内系统用户的唯一标志
+ */
+ String SYS_USER = "SYS_USER";
+
+ /**
+ * 正常状态
+ */
+ String NORMAL = "0";
+
+ /**
+ * 异常状态
+ */
+ String EXCEPTION = "1";
+
+ /**
+ * 用户正常状态
+ */
+ String USER_NORMAL = "0";
+
+ /**
+ * 用户封禁状态
+ */
+ String USER_DISABLE = "1";
+
+ /**
+ * 角色正常状态
+ */
+ String ROLE_NORMAL = "0";
+
+ /**
+ * 角色封禁状态
+ */
+ String ROLE_DISABLE = "1";
+
+ /**
+ * 部门正常状态
+ */
+ String DEPT_NORMAL = "0";
+
+ /**
+ * 部门停用状态
+ */
+ String DEPT_DISABLE = "1";
+
+ /**
+ * 字典正常状态
+ */
+ String DICT_NORMAL = "0";
+
+ /**
+ * 是否为系统默认(是)
+ */
+ String YES = "Y";
+
+ /**
+ * 是否菜单外链(是)
+ */
+ String YES_FRAME = "0";
+
+ /**
+ * 是否菜单外链(否)
+ */
+ String NO_FRAME = "1";
+
+ /**
+ * 菜单正常状态
+ */
+ String MENU_NORMAL = "0";
+
+ /**
+ * 菜单停用状态
+ */
+ String MENU_DISABLE = "1";
+
+ /**
+ * 菜单类型(目录)
+ */
+ String TYPE_DIR = "M";
+
+ /**
+ * 菜单类型(菜单)
+ */
+ String TYPE_MENU = "C";
+
+ /**
+ * 菜单类型(按钮)
+ */
+ String TYPE_BUTTON = "F";
+
+ /**
+ * Layout组件标识
+ */
+ String LAYOUT = "Layout";
+
+ /**
+ * ParentView组件标识
+ */
+ String PARENT_VIEW = "ParentView";
+
+ /**
+ * InnerLink组件标识
+ */
+ String INNER_LINK = "InnerLink";
+
+ /**
+ * 用户名长度限制
+ */
+ int USERNAME_MIN_LENGTH = 2;
+ int USERNAME_MAX_LENGTH = 100;
+
+ /**
+ * 密码长度限制
+ */
+ int PASSWORD_MIN_LENGTH = 5;
+ int PASSWORD_MAX_LENGTH = 20;
+
+ /**
+ * 超级管理员ID
+ */
+ Long SUPER_ADMIN_ID = 1L;
+
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/domain/R.java b/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/domain/R.java
new file mode 100644
index 00000000..e614a862
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/domain/R.java
@@ -0,0 +1,110 @@
+package com.xmzs.common.core.domain;
+
+import com.xmzs.common.core.constant.HttpStatus;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 响应信息主体
+ *
+ * @author Lion Li
+ */
+@Data
+@NoArgsConstructor
+public class R implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 成功
+ */
+ public static final int SUCCESS = 200;
+
+ /**
+ * 失败
+ */
+ public static final int FAIL = 500;
+
+ private int code;
+
+ private String msg;
+
+ private T data;
+
+ public static R ok() {
+ return restResult(null, SUCCESS, "操作成功");
+ }
+
+ public static R ok(T data) {
+ return restResult(data, SUCCESS, "操作成功");
+ }
+
+ public static R ok(String msg) {
+ return restResult(null, SUCCESS, msg);
+ }
+
+ public static R ok(String msg, T data) {
+ return restResult(data, SUCCESS, msg);
+ }
+
+ public static R fail() {
+ return restResult(null, FAIL, "操作失败");
+ }
+
+ public static R fail(String msg) {
+ return restResult(null, FAIL, msg);
+ }
+
+ public static R fail(T data) {
+ return restResult(data, FAIL, "操作失败");
+ }
+
+ public static R fail(String msg, T data) {
+ return restResult(data, FAIL, msg);
+ }
+
+ public static R fail(int code, String msg) {
+ return restResult(null, code, msg);
+ }
+
+ /**
+ * 返回警告消息
+ *
+ * @param msg 返回内容
+ * @return 警告消息
+ */
+ public static R warn(String msg) {
+ return restResult(null, HttpStatus.WARN, msg);
+ }
+
+ /**
+ * 返回警告消息
+ *
+ * @param msg 返回内容
+ * @param data 数据对象
+ * @return 警告消息
+ */
+ public static R warn(String msg, T data) {
+ return restResult(data, HttpStatus.WARN, msg);
+ }
+
+ private static R restResult(T data, int code, String msg) {
+ R r = new R<>();
+ r.setCode(code);
+ r.setData(data);
+ r.setMsg(msg);
+ return r;
+ }
+
+ public static Boolean isError(R ret) {
+ return !isSuccess(ret);
+ }
+
+ public static Boolean isSuccess(R ret) {
+ return R.SUCCESS == ret.getCode();
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/domain/dto/RoleDTO.java b/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/domain/dto/RoleDTO.java
new file mode 100644
index 00000000..32713730
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/domain/dto/RoleDTO.java
@@ -0,0 +1,38 @@
+package com.xmzs.common.core.domain.dto;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+
+/**
+ * 角色
+ *
+ * @author Lion Li
+ */
+
+@Data
+@NoArgsConstructor
+public class RoleDTO implements Serializable {
+
+ /**
+ * 角色ID
+ */
+ private Long roleId;
+
+ /**
+ * 角色名称
+ */
+ private String roleName;
+
+ /**
+ * 角色权限
+ */
+ private String roleKey;
+
+ /**
+ * 数据范围(1:所有数据权限;2:自定义数据权限;3:本部门数据权限;4:本部门及以下数据权限;5:仅本人数据权限)
+ */
+ private String dataScope;
+
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/domain/dto/UserOnlineDTO.java b/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/domain/dto/UserOnlineDTO.java
new file mode 100644
index 00000000..deea41da
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/domain/dto/UserOnlineDTO.java
@@ -0,0 +1,62 @@
+package com.xmzs.common.core.domain.dto;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 当前在线会话
+ *
+ * @author ruoyi
+ */
+
+@Data
+@NoArgsConstructor
+public class UserOnlineDTO implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 会话编号
+ */
+ private String tokenId;
+
+ /**
+ * 部门名称
+ */
+ private String deptName;
+
+ /**
+ * 用户名称
+ */
+ private String userName;
+
+ /**
+ * 登录IP地址
+ */
+ private String ipaddr;
+
+ /**
+ * 登录地址
+ */
+ private String loginLocation;
+
+ /**
+ * 浏览器类型
+ */
+ private String browser;
+
+ /**
+ * 操作系统
+ */
+ private String os;
+
+ /**
+ * 登录时间
+ */
+ private Long loginTime;
+
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/domain/model/EmailLoginBody.java b/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/domain/model/EmailLoginBody.java
new file mode 100644
index 00000000..1c5553cd
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/domain/model/EmailLoginBody.java
@@ -0,0 +1,35 @@
+package com.xmzs.common.core.domain.model;
+
+import jakarta.validation.constraints.Email;
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+
+/**
+ * 短信登录对象
+ *
+ * @author Lion Li
+ */
+
+@Data
+public class EmailLoginBody {
+
+ /**
+ * 租户ID
+ */
+ @NotBlank(message = "{tenant.number.not.blank}")
+ private String tenantId;
+
+ /**
+ * 邮箱
+ */
+ @NotBlank(message = "{user.email.not.blank}")
+ @Email(message = "{user.email.not.valid}")
+ private String email;
+
+ /**
+ * 邮箱code
+ */
+ @NotBlank(message = "{email.code.not.blank}")
+ private String emailCode;
+
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/domain/model/LoginBody.java b/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/domain/model/LoginBody.java
new file mode 100644
index 00000000..c36905cf
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/domain/model/LoginBody.java
@@ -0,0 +1,47 @@
+package com.xmzs.common.core.domain.model;
+
+import com.xmzs.common.core.constant.UserConstants;
+import lombok.Data;
+import org.hibernate.validator.constraints.Length;
+
+import jakarta.validation.constraints.NotBlank;
+
+/**
+ * 用户登录对象
+ *
+ * @author Lion Li
+ */
+
+@Data
+public class LoginBody {
+
+ /**
+ * 租户ID
+ */
+ private String tenantId;
+
+ /**
+ * 用户名
+ */
+ @NotBlank(message = "{user.username.not.blank}")
+ // @Length(min = UserConstants.USERNAME_MIN_LENGTH, max = UserConstants.USERNAME_MAX_LENGTH, message = "{user.username.length.valid}")
+ private String username;
+
+ /**
+ * 用户密码
+ */
+ @NotBlank(message = "{user.password.not.blank}")
+ // @Length(min = UserConstants.PASSWORD_MIN_LENGTH, max = UserConstants.PASSWORD_MAX_LENGTH, message = "{user.password.length.valid}")
+ private String password;
+
+ /**
+ * 验证码
+ */
+ private String code;
+
+ /**
+ * 唯一标识
+ */
+ private String uuid;
+
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/domain/model/LoginUser.java b/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/domain/model/LoginUser.java
new file mode 100644
index 00000000..9fd909bd
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/domain/model/LoginUser.java
@@ -0,0 +1,133 @@
+package com.xmzs.common.core.domain.model;
+
+import com.xmzs.common.core.domain.dto.RoleDTO;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * 登录用户身份权限
+ *
+ * @author Lion Li
+ */
+
+@Data
+@NoArgsConstructor
+public class LoginUser implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 租户ID
+ */
+ private String tenantId;
+
+ /**
+ * 用户ID
+ */
+ private Long userId;
+
+ /**
+ * 部门ID
+ */
+ private Long deptId;
+
+ /**
+ * 部门名
+ */
+ private String deptName;
+
+ /**
+ * 用户唯一标识
+ */
+ private String token;
+
+ /**
+ * 用户类型
+ */
+ private String userType;
+
+ /**
+ * 登录时间
+ */
+ private Long loginTime;
+
+ /**
+ * 过期时间
+ */
+ private Long expireTime;
+
+ /**
+ * 登录IP地址
+ */
+ private String ipaddr;
+
+ /**
+ * 登录地点
+ */
+ private String loginLocation;
+
+ /**
+ * 浏览器类型
+ */
+ private String browser;
+
+ /**
+ * 操作系统
+ */
+ private String os;
+
+ /**
+ * 菜单权限
+ */
+ private Set menuPermission;
+
+ /**
+ * 角色权限
+ */
+ private Set rolePermission;
+
+ /**
+ * 用户名
+ */
+ private String username;
+
+ /**
+ * 用户名
+ */
+ private String nickName;
+
+ /**
+ * 微信头像
+ */
+ private String avatar;
+
+ /**
+ * 角色对象
+ */
+ private List roles;
+
+ /**
+ * 数据权限 当前角色ID
+ */
+ private Long roleId;
+
+ /**
+ * 获取登录id
+ */
+ public String getLoginId() {
+ if (userType == null) {
+ throw new IllegalArgumentException("用户类型不能为空");
+ }
+ if (userId == null) {
+ throw new IllegalArgumentException("用户ID不能为空");
+ }
+ return userType + ":" + userId;
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/domain/model/RegisterBody.java b/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/domain/model/RegisterBody.java
new file mode 100644
index 00000000..cb6ef6af
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/domain/model/RegisterBody.java
@@ -0,0 +1,17 @@
+package com.xmzs.common.core.domain.model;
+
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 用户注册对象
+ *
+ * @author Lion Li
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class RegisterBody extends LoginBody {
+
+ private String userType;
+
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/domain/model/SmsLoginBody.java b/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/domain/model/SmsLoginBody.java
new file mode 100644
index 00000000..818098d5
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/domain/model/SmsLoginBody.java
@@ -0,0 +1,34 @@
+package com.xmzs.common.core.domain.model;
+
+import lombok.Data;
+
+import jakarta.validation.constraints.NotBlank;
+
+/**
+ * 短信登录对象
+ *
+ * @author Lion Li
+ */
+
+@Data
+public class SmsLoginBody {
+
+ /**
+ * 租户ID
+ */
+ @NotBlank(message = "{tenant.number.not.blank}")
+ private String tenantId;
+
+ /**
+ * 手机号
+ */
+ @NotBlank(message = "{user.phonenumber.not.blank}")
+ private String phonenumber;
+
+ /**
+ * 短信code
+ */
+ @NotBlank(message = "{sms.code.not.blank}")
+ private String smsCode;
+
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/domain/model/VisitorLoginBody.java b/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/domain/model/VisitorLoginBody.java
new file mode 100644
index 00000000..94564b16
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/domain/model/VisitorLoginBody.java
@@ -0,0 +1,25 @@
+package com.xmzs.common.core.domain.model;
+
+import lombok.Data;
+
+import java.io.Serial;
+
+/**
+ * 游客登录用户身份权限
+ *
+ * @author Lion Li
+ */
+@Data
+public class VisitorLoginBody {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ private String code;
+
+ /**
+ * 登录类型(1.小程序访客 2.pc访客)
+ */
+ private String type;
+
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/domain/model/VisitorLoginUser.java b/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/domain/model/VisitorLoginUser.java
new file mode 100644
index 00000000..e346a4a5
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/domain/model/VisitorLoginUser.java
@@ -0,0 +1,28 @@
+package com.xmzs.common.core.domain.model;
+
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+
+import java.io.Serial;
+
+/**
+ * 小程序登录用户身份权限
+ *
+ * @author Lion Li
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@NoArgsConstructor
+public class VisitorLoginUser extends LoginUser {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * openid
+ */
+ private String openid;
+
+
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/enums/DeviceType.java b/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/enums/DeviceType.java
new file mode 100644
index 00000000..be899884
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/enums/DeviceType.java
@@ -0,0 +1,32 @@
+package com.xmzs.common.core.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * 设备类型
+ * 针对一套 用户体系
+ *
+ * @author Lion Li
+ */
+@Getter
+@AllArgsConstructor
+public enum DeviceType {
+
+ /**
+ * pc端
+ */
+ PC("pc"),
+
+ /**
+ * app端
+ */
+ APP("app"),
+
+ /**
+ * 小程序端
+ */
+ XCX("xcx");
+
+ private final String device;
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/enums/LoginType.java b/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/enums/LoginType.java
new file mode 100644
index 00000000..ec123eb9
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/enums/LoginType.java
@@ -0,0 +1,44 @@
+package com.xmzs.common.core.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * 登录类型
+ *
+ * @author Lion Li
+ */
+@Getter
+@AllArgsConstructor
+public enum LoginType {
+
+ /**
+ * 密码登录
+ */
+ PASSWORD("user.password.retry.limit.exceed", "user.password.retry.limit.count"),
+
+ /**
+ * 短信登录
+ */
+ SMS("sms.code.retry.limit.exceed", "sms.code.retry.limit.count"),
+
+ /**
+ * 邮箱登录
+ */
+ EMAIL("email.code.retry.limit.exceed", "email.code.retry.limit.count"),
+
+ /**
+ * 小程序登录
+ */
+ XCX("", "");
+
+ /**
+ * 登录重试超出限制提示
+ */
+ final String retryLimitExceed;
+
+ /**
+ * 登录重试限制计数提示
+ */
+ final String retryLimitCount;
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/enums/LoginUserType.java b/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/enums/LoginUserType.java
new file mode 100644
index 00000000..e0753edd
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/enums/LoginUserType.java
@@ -0,0 +1,21 @@
+package com.xmzs.common.core.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * 游客登录类型
+ *
+ * @author Lion Li
+ */
+@Getter
+@AllArgsConstructor
+public enum LoginUserType {
+
+ PC("1", "PC端用户"),
+
+ XCX("2", "小程序用户");
+
+ private final String code;
+ private final String content;
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/enums/TenantStatus.java b/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/enums/TenantStatus.java
new file mode 100644
index 00000000..7e7050ef
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/enums/TenantStatus.java
@@ -0,0 +1,30 @@
+package com.xmzs.common.core.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * 用户状态
+ *
+ * @author LionLi
+ */
+@Getter
+@AllArgsConstructor
+public enum TenantStatus {
+ /**
+ * 正常
+ */
+ OK("0", "正常"),
+ /**
+ * 停用
+ */
+ DISABLE("1", "停用"),
+ /**
+ * 删除
+ */
+ DELETED("2", "删除");
+
+ private final String code;
+ private final String info;
+
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/enums/UserStatus.java b/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/enums/UserStatus.java
new file mode 100644
index 00000000..16fe3bc1
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/enums/UserStatus.java
@@ -0,0 +1,30 @@
+package com.xmzs.common.core.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * 用户状态
+ *
+ * @author ruoyi
+ */
+@Getter
+@AllArgsConstructor
+public enum UserStatus {
+ /**
+ * 正常
+ */
+ OK("0", "正常"),
+ /**
+ * 停用
+ */
+ DISABLE("1", "停用"),
+ /**
+ * 删除
+ */
+ DELETED("2", "删除");
+
+ private final String code;
+ private final String info;
+
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/enums/UserType.java b/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/enums/UserType.java
new file mode 100644
index 00000000..6ebfba7d
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/enums/UserType.java
@@ -0,0 +1,37 @@
+package com.xmzs.common.core.enums;
+
+import com.xmzs.common.core.utils.StringUtils;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * 设备类型
+ * 针对多套 用户体系
+ *
+ * @author Lion Li
+ */
+@Getter
+@AllArgsConstructor
+public enum UserType {
+
+ /**
+ * pc端
+ */
+ SYS_USER("sys_user"),
+
+ /**
+ * app端
+ */
+ APP_USER("app_user");
+
+ private final String userType;
+
+ public static UserType getUserType(String str) {
+ for (UserType value : values()) {
+ if (StringUtils.contains(str, value.getUserType())) {
+ return value;
+ }
+ }
+ throw new RuntimeException("'UserType' not found By " + str);
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/exception/DemoModeException.java b/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/exception/DemoModeException.java
new file mode 100644
index 00000000..063bb4e4
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/exception/DemoModeException.java
@@ -0,0 +1,17 @@
+package com.xmzs.common.core.exception;
+
+import java.io.Serial;
+
+/**
+ * 演示模式异常
+ *
+ * @author ruoyi
+ */
+public class DemoModeException extends RuntimeException {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ public DemoModeException() {
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/exception/GlobalException.java b/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/exception/GlobalException.java
new file mode 100644
index 00000000..0b5ef2ae
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/exception/GlobalException.java
@@ -0,0 +1,53 @@
+package com.xmzs.common.core.exception;
+
+import java.io.Serial;
+
+/**
+ * 全局异常
+ *
+ * @author ruoyi
+ */
+public class GlobalException extends RuntimeException {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 错误提示
+ */
+ private String message;
+
+ /**
+ * 错误明细,内部调试错误
+ */
+ private String detailMessage;
+
+ /**
+ * 空构造方法,避免反序列化问题
+ */
+ public GlobalException() {
+ }
+
+ public GlobalException(String message) {
+ this.message = message;
+ }
+
+ public String getDetailMessage() {
+ return detailMessage;
+ }
+
+ public GlobalException setDetailMessage(String detailMessage) {
+ this.detailMessage = detailMessage;
+ return this;
+ }
+
+ @Override
+ public String getMessage() {
+ return message;
+ }
+
+ public GlobalException setMessage(String message) {
+ this.message = message;
+ return this;
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/exception/ServiceException.java b/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/exception/ServiceException.java
new file mode 100644
index 00000000..ff85efbf
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/exception/ServiceException.java
@@ -0,0 +1,67 @@
+package com.xmzs.common.core.exception;
+
+import java.io.Serial;
+
+/**
+ * 业务异常
+ *
+ * @author ruoyi
+ */
+public final class ServiceException extends RuntimeException {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 错误码
+ */
+ private Integer code;
+
+ /**
+ * 错误提示
+ */
+ private String message;
+
+ /**
+ * 错误明细,内部调试错误
+ */
+ private String detailMessage;
+
+ /**
+ * 空构造方法,避免反序列化问题
+ */
+ public ServiceException() {
+ }
+
+ public ServiceException(String message) {
+ this.message = message;
+ }
+
+ public ServiceException(String message, Integer code) {
+ this.message = message;
+ this.code = code;
+ }
+
+ public String getDetailMessage() {
+ return detailMessage;
+ }
+
+ @Override
+ public String getMessage() {
+ return message;
+ }
+
+ public Integer getCode() {
+ return code;
+ }
+
+ public ServiceException setMessage(String message) {
+ this.message = message;
+ return this;
+ }
+
+ public ServiceException setDetailMessage(String detailMessage) {
+ this.detailMessage = detailMessage;
+ return this;
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/exception/UtilException.java b/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/exception/UtilException.java
new file mode 100644
index 00000000..e2455364
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/exception/UtilException.java
@@ -0,0 +1,26 @@
+package com.xmzs.common.core.exception;
+
+import java.io.Serial;
+
+/**
+ * 工具类异常
+ *
+ * @author ruoyi
+ */
+public class UtilException extends RuntimeException {
+
+ @Serial
+ private static final long serialVersionUID = 8247610319171014183L;
+
+ public UtilException(Throwable e) {
+ super(e.getMessage(), e);
+ }
+
+ public UtilException(String message) {
+ super(message);
+ }
+
+ public UtilException(String message, Throwable throwable) {
+ super(message, throwable);
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/exception/base/BaseException.java b/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/exception/base/BaseException.java
new file mode 100644
index 00000000..a92efa84
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/exception/base/BaseException.java
@@ -0,0 +1,79 @@
+package com.xmzs.common.core.exception.base;
+
+import com.xmzs.common.core.utils.MessageUtils;
+import com.xmzs.common.core.utils.StringUtils;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+
+import java.io.Serial;
+
+/**
+ * 基础异常
+ *
+ * @author ruoyi
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@NoArgsConstructor
+public class BaseException extends RuntimeException {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 所属模块
+ */
+ private String module;
+
+ /**
+ * 错误码
+ */
+ private String code;
+
+ /**
+ * 错误码对应的参数
+ */
+ private Object[] args;
+
+ /**
+ * 错误消息
+ */
+ private String defaultMessage;
+
+ public BaseException(String module, String code, Object[] args, String defaultMessage) {
+ this.module = module;
+ this.code = code;
+ this.args = args;
+ this.defaultMessage = defaultMessage;
+ }
+
+ public BaseException(String module, String code, Object[] args) {
+ this(module, code, args, null);
+ }
+
+ public BaseException(String module, String defaultMessage) {
+ this(module, null, null, defaultMessage);
+ }
+
+ public BaseException(String code, Object[] args) {
+ this(null, code, args, null);
+ }
+
+ public BaseException(String defaultMessage) {
+ this(null, null, null, defaultMessage);
+ }
+
+ @Override
+ public String getMessage() {
+ String message = null;
+ if (!StringUtils.isEmpty(code)) {
+ message = MessageUtils.message(code, args);
+ }
+ if (message == null) {
+ message = defaultMessage;
+ }
+ return message;
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/exception/file/FileException.java b/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/exception/file/FileException.java
new file mode 100644
index 00000000..eb559996
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/exception/file/FileException.java
@@ -0,0 +1,21 @@
+package com.xmzs.common.core.exception.file;
+
+import com.xmzs.common.core.exception.base.BaseException;
+
+import java.io.Serial;
+
+/**
+ * 文件信息异常类
+ *
+ * @author ruoyi
+ */
+public class FileException extends BaseException {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ public FileException(String code, Object[] args) {
+ super("file", code, args, null);
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/exception/file/FileNameLengthLimitExceededException.java b/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/exception/file/FileNameLengthLimitExceededException.java
new file mode 100644
index 00000000..905883fc
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/exception/file/FileNameLengthLimitExceededException.java
@@ -0,0 +1,18 @@
+package com.xmzs.common.core.exception.file;
+
+import java.io.Serial;
+
+/**
+ * 文件名称超长限制异常类
+ *
+ * @author ruoyi
+ */
+public class FileNameLengthLimitExceededException extends FileException {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ public FileNameLengthLimitExceededException(int defaultFileNameLength) {
+ super("upload.filename.exceed.length", new Object[]{defaultFileNameLength});
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/exception/file/FileSizeLimitExceededException.java b/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/exception/file/FileSizeLimitExceededException.java
new file mode 100644
index 00000000..f39046ea
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/exception/file/FileSizeLimitExceededException.java
@@ -0,0 +1,18 @@
+package com.xmzs.common.core.exception.file;
+
+import java.io.Serial;
+
+/**
+ * 文件名大小限制异常类
+ *
+ * @author ruoyi
+ */
+public class FileSizeLimitExceededException extends FileException {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ public FileSizeLimitExceededException(long defaultMaxSize) {
+ super("upload.exceed.maxSize", new Object[]{defaultMaxSize});
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/exception/user/CaptchaException.java b/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/exception/user/CaptchaException.java
new file mode 100644
index 00000000..1a4b885e
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/exception/user/CaptchaException.java
@@ -0,0 +1,18 @@
+package com.xmzs.common.core.exception.user;
+
+import java.io.Serial;
+
+/**
+ * 验证码错误异常类
+ *
+ * @author ruoyi
+ */
+public class CaptchaException extends UserException {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ public CaptchaException() {
+ super("user.jcaptcha.error");
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/exception/user/CaptchaExpireException.java b/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/exception/user/CaptchaExpireException.java
new file mode 100644
index 00000000..5b7193f6
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/exception/user/CaptchaExpireException.java
@@ -0,0 +1,18 @@
+package com.xmzs.common.core.exception.user;
+
+import java.io.Serial;
+
+/**
+ * 验证码失效异常类
+ *
+ * @author ruoyi
+ */
+public class CaptchaExpireException extends UserException {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ public CaptchaExpireException() {
+ super("user.jcaptcha.expire");
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/exception/user/UserException.java b/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/exception/user/UserException.java
new file mode 100644
index 00000000..43c4b730
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/exception/user/UserException.java
@@ -0,0 +1,20 @@
+package com.xmzs.common.core.exception.user;
+
+import com.xmzs.common.core.exception.base.BaseException;
+
+import java.io.Serial;
+
+/**
+ * 用户信息异常类
+ *
+ * @author ruoyi
+ */
+public class UserException extends BaseException {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ public UserException(String code, Object... args) {
+ super("user", code, args, null);
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/exception/user/UserPasswordNotMatchException.java b/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/exception/user/UserPasswordNotMatchException.java
new file mode 100644
index 00000000..12970b0d
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/exception/user/UserPasswordNotMatchException.java
@@ -0,0 +1,18 @@
+package com.xmzs.common.core.exception.user;
+
+import java.io.Serial;
+
+/**
+ * 用户密码不正确或不符合规范异常类
+ *
+ * @author ruoyi
+ */
+public class UserPasswordNotMatchException extends UserException {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ public UserPasswordNotMatchException() {
+ super("user.password.not.match");
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/exception/user/UserPasswordRetryLimitExceedException.java b/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/exception/user/UserPasswordRetryLimitExceedException.java
new file mode 100644
index 00000000..80f3674a
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/exception/user/UserPasswordRetryLimitExceedException.java
@@ -0,0 +1,19 @@
+package com.xmzs.common.core.exception.user;
+
+import java.io.Serial;
+
+/**
+ * 用户错误最大次数异常类
+ *
+ * @author ruoyi
+ */
+public class UserPasswordRetryLimitExceedException extends UserException {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ public UserPasswordRetryLimitExceedException(int retryLimitCount, int lockTime) {
+ super("user.password.retry.limit.exceed", retryLimitCount, lockTime);
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/manager/ShutdownManager.java b/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/manager/ShutdownManager.java
new file mode 100644
index 00000000..9773f285
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/manager/ShutdownManager.java
@@ -0,0 +1,41 @@
+package com.xmzs.common.core.manager;
+
+import com.xmzs.common.core.utils.Threads;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.stereotype.Component;
+
+import jakarta.annotation.PreDestroy;
+import java.util.concurrent.ScheduledExecutorService;
+
+/**
+ * 确保应用退出时能关闭后台线程
+ *
+ * @author Lion Li
+ */
+@Slf4j
+@Component
+public class ShutdownManager {
+
+ @Autowired
+ @Qualifier("scheduledExecutorService")
+ private ScheduledExecutorService scheduledExecutorService;
+
+ @PreDestroy
+ public void destroy() {
+ shutdownAsyncManager();
+ }
+
+ /**
+ * 停止异步执行任务
+ */
+ private void shutdownAsyncManager() {
+ try {
+ log.info("====关闭后台任务任务线程池====");
+ Threads.shutdownAndAwaitTermination(scheduledExecutorService);
+ } catch (Exception e) {
+ log.error(e.getMessage(), e);
+ }
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/service/ConfigService.java b/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/service/ConfigService.java
new file mode 100644
index 00000000..e8d9eacc
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/service/ConfigService.java
@@ -0,0 +1,18 @@
+package com.xmzs.common.core.service;
+
+/**
+ * 通用 参数配置服务
+ *
+ * @author Lion Li
+ */
+public interface ConfigService {
+
+ /**
+ * 根据参数 key 获取参数值
+ *
+ * @param configKey 参数 key
+ * @return 参数值
+ */
+ String getConfigValue(String configKey);
+
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/service/DeptService.java b/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/service/DeptService.java
new file mode 100644
index 00000000..74be3b5f
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/service/DeptService.java
@@ -0,0 +1,18 @@
+package com.xmzs.common.core.service;
+
+/**
+ * 通用 部门服务
+ *
+ * @author Lion Li
+ */
+public interface DeptService {
+
+ /**
+ * 通过部门ID查询部门名称
+ *
+ * @param deptIds 部门ID串逗号分隔
+ * @return 部门名称串逗号分隔
+ */
+ String selectDeptNameByIds(String deptIds);
+
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/service/DictService.java b/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/service/DictService.java
new file mode 100644
index 00000000..8716ccf1
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/service/DictService.java
@@ -0,0 +1,57 @@
+package com.xmzs.common.core.service;
+
+/**
+ * 通用 字典服务
+ *
+ * @author Lion Li
+ */
+public interface DictService {
+
+ /**
+ * 分隔符
+ */
+ String SEPARATOR = ",";
+
+ /**
+ * 根据字典类型和字典值获取字典标签
+ *
+ * @param dictType 字典类型
+ * @param dictValue 字典值
+ * @return 字典标签
+ */
+ default String getDictLabel(String dictType, String dictValue) {
+ return getDictLabel(dictType, dictValue, SEPARATOR);
+ }
+
+ /**
+ * 根据字典类型和字典标签获取字典值
+ *
+ * @param dictType 字典类型
+ * @param dictLabel 字典标签
+ * @return 字典值
+ */
+ default String getDictValue(String dictType, String dictLabel) {
+ return getDictValue(dictType, dictLabel, SEPARATOR);
+ }
+
+ /**
+ * 根据字典类型和字典值获取字典标签
+ *
+ * @param dictType 字典类型
+ * @param dictValue 字典值
+ * @param separator 分隔符
+ * @return 字典标签
+ */
+ String getDictLabel(String dictType, String dictValue, String separator);
+
+ /**
+ * 根据字典类型和字典标签获取字典值
+ *
+ * @param dictType 字典类型
+ * @param dictLabel 字典标签
+ * @param separator 分隔符
+ * @return 字典值
+ */
+ String getDictValue(String dictType, String dictLabel, String separator);
+
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/service/OssService.java b/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/service/OssService.java
new file mode 100644
index 00000000..f3524aa5
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/service/OssService.java
@@ -0,0 +1,18 @@
+package com.xmzs.common.core.service;
+
+/**
+ * 通用 OSS服务
+ *
+ * @author Lion Li
+ */
+public interface OssService {
+
+ /**
+ * 通过ossId查询对应的url
+ *
+ * @param ossIds ossId串逗号分隔
+ * @return url串逗号分隔
+ */
+ String selectUrlByIds(String ossIds);
+
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/service/UserService.java b/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/service/UserService.java
new file mode 100644
index 00000000..3bd2de59
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/service/UserService.java
@@ -0,0 +1,18 @@
+package com.xmzs.common.core.service;
+
+/**
+ * 通用 用户服务
+ *
+ * @author Lion Li
+ */
+public interface UserService {
+
+ /**
+ * 通过用户ID查询用户账户
+ *
+ * @param userId 用户ID
+ * @return 用户账户
+ */
+ String selectUserNameById(Long userId);
+
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/utils/DateUtils.java b/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/utils/DateUtils.java
new file mode 100644
index 00000000..35718dba
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/utils/DateUtils.java
@@ -0,0 +1,168 @@
+package com.xmzs.common.core.utils;
+
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+import org.apache.commons.lang3.time.DateFormatUtils;
+
+import java.lang.management.ManagementFactory;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.util.Date;
+
+/**
+ * 时间工具类
+ *
+ * @author ruoyi
+ */
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public class DateUtils extends org.apache.commons.lang3.time.DateUtils {
+
+ public static final String YYYY = "yyyy";
+
+ public static final String YYYY_MM = "yyyy-MM";
+
+ public static final String YYYY_MM_DD = "yyyy-MM-dd";
+
+ public static final String YYYYMMDDHHMMSS = "yyyyMMddHHmmss";
+
+ public static final String YYYY_MM_DD_HH_MM_SS = "yyyy-MM-dd HH:mm:ss";
+
+ private static final String[] PARSE_PATTERNS = {
+ "yyyy-MM-dd", "yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd HH:mm", "yyyy-MM",
+ "yyyy/MM/dd", "yyyy/MM/dd HH:mm:ss", "yyyy/MM/dd HH:mm", "yyyy/MM",
+ "yyyy.MM.dd", "yyyy.MM.dd HH:mm:ss", "yyyy.MM.dd HH:mm", "yyyy.MM"};
+
+ /**
+ * 获取当前Date型日期
+ *
+ * @return Date() 当前日期
+ */
+ public static Date getNowDate() {
+ return new Date();
+ }
+
+ /**
+ * 获取当前日期, 默认格式为yyyy-MM-dd
+ *
+ * @return String
+ */
+ public static String getDate() {
+ return dateTimeNow(YYYY_MM_DD);
+ }
+
+ public static String getTime() {
+ return dateTimeNow(YYYY_MM_DD_HH_MM_SS);
+ }
+
+ public static String dateTimeNow() {
+ return dateTimeNow(YYYYMMDDHHMMSS);
+ }
+
+ public static String dateTimeNow(final String format) {
+ return parseDateToStr(format, new Date());
+ }
+
+ public static String dateTime(final Date date) {
+ return parseDateToStr(YYYY_MM_DD, date);
+ }
+
+ public static String parseDateToStr(final String format, final Date date) {
+ return new SimpleDateFormat(format).format(date);
+ }
+
+ public static Date dateTime(final String format, final String ts) {
+ try {
+ return new SimpleDateFormat(format).parse(ts);
+ } catch (ParseException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * 日期路径 即年/月/日 如2018/08/08
+ */
+ public static String datePath() {
+ Date now = new Date();
+ return DateFormatUtils.format(now, "yyyy/MM/dd");
+ }
+
+ /**
+ * 日期路径 即年/月/日 如20180808
+ */
+ public static String dateTime() {
+ Date now = new Date();
+ return DateFormatUtils.format(now, "yyyyMMdd");
+ }
+
+ /**
+ * 日期型字符串转化为日期 格式
+ */
+ public static Date parseDate(Object str) {
+ if (str == null) {
+ return null;
+ }
+ try {
+ return parseDate(str.toString(), PARSE_PATTERNS);
+ } catch (ParseException e) {
+ return null;
+ }
+ }
+
+ /**
+ * 获取服务器启动时间
+ */
+ public static Date getServerStartDate() {
+ long time = ManagementFactory.getRuntimeMXBean().getStartTime();
+ return new Date(time);
+ }
+
+ /**
+ * 计算相差天数
+ */
+ public static int differentDaysByMillisecond(Date date1, Date date2) {
+ return Math.abs((int) ((date2.getTime() - date1.getTime()) / (1000 * 3600 * 24)));
+ }
+
+ /**
+ * 计算两个时间差
+ */
+ public static String getDatePoor(Date endDate, Date nowDate) {
+ long nd = 1000 * 24 * 60 * 60;
+ long nh = 1000 * 60 * 60;
+ long nm = 1000 * 60;
+ // long ns = 1000;
+ // 获得两个时间的毫秒时间差异
+ long diff = endDate.getTime() - nowDate.getTime();
+ // 计算差多少天
+ long day = diff / nd;
+ // 计算差多少小时
+ long hour = diff % nd / nh;
+ // 计算差多少分钟
+ long min = diff % nd % nh / nm;
+ // 计算差多少秒//输出结果
+ // long sec = diff % nd % nh % nm / ns;
+ return day + "天" + hour + "小时" + min + "分钟";
+ }
+
+ /**
+ * 增加 LocalDateTime ==> Date
+ */
+ public static Date toDate(LocalDateTime temporalAccessor) {
+ ZonedDateTime zdt = temporalAccessor.atZone(ZoneId.systemDefault());
+ return Date.from(zdt.toInstant());
+ }
+
+ /**
+ * 增加 LocalDate ==> Date
+ */
+ public static Date toDate(LocalDate temporalAccessor) {
+ LocalDateTime localDateTime = LocalDateTime.of(temporalAccessor, LocalTime.of(0, 0, 0));
+ ZonedDateTime zdt = localDateTime.atZone(ZoneId.systemDefault());
+ return Date.from(zdt.toInstant());
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/utils/JsonUtils.java b/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/utils/JsonUtils.java
new file mode 100644
index 00000000..4827aa7b
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/utils/JsonUtils.java
@@ -0,0 +1,28 @@
+package com.xmzs.common.core.utils;
+
+import com.fasterxml.jackson.annotation.JsonInclude.Include;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+
+/**
+ * @author Binary Wang
+ */
+public class JsonUtils {
+ private static final ObjectMapper JSON = new ObjectMapper();
+
+ static {
+ JSON.setSerializationInclusion(Include.NON_NULL);
+ JSON.configure(SerializationFeature.INDENT_OUTPUT, Boolean.TRUE);
+ }
+
+ public static String toJson(Object obj) {
+ try {
+ return JSON.writeValueAsString(obj);
+ } catch (JsonProcessingException e) {
+ e.printStackTrace();
+ }
+
+ return null;
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/utils/MapstructUtils.java b/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/utils/MapstructUtils.java
new file mode 100644
index 00000000..0c5820bf
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/utils/MapstructUtils.java
@@ -0,0 +1,92 @@
+package com.xmzs.common.core.utils;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.map.MapUtil;
+import cn.hutool.core.util.ObjectUtil;
+import io.github.linpeilie.Converter;
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Mapstruct 工具类
+ * 参考文档:mapstruct-plus
+ *
+ * @author Michelle.Chung
+ */
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public class MapstructUtils {
+
+ private final static Converter CONVERTER = SpringUtils.getBean(Converter.class);
+
+ /**
+ * 将 T 类型对象,转换为 desc 类型的对象并返回
+ *
+ * @param source 数据来源实体
+ * @param desc 描述对象 转换后的对象
+ * @return desc
+ */
+ public static V convert(T source, Class desc) {
+ if (ObjectUtil.isNull(source)) {
+ return null;
+ }
+ if (ObjectUtil.isNull(desc)) {
+ return null;
+ }
+ return CONVERTER.convert(source, desc);
+ }
+
+ /**
+ * 将 T 类型对象,按照配置的映射字段规则,给 desc 类型的对象赋值并返回 desc 对象
+ *
+ * @param source 数据来源实体
+ * @param desc 转换后的对象
+ * @return desc
+ */
+ public static V convert(T source, V desc) {
+ if (ObjectUtil.isNull(source)) {
+ return null;
+ }
+ if (ObjectUtil.isNull(desc)) {
+ return null;
+ }
+ return CONVERTER.convert(source, desc);
+ }
+
+ /**
+ * 将 T 类型的集合,转换为 desc 类型的集合并返回
+ *
+ * @param sourceList 数据来源实体列表
+ * @param desc 描述对象 转换后的对象
+ * @return desc
+ */
+ public static List convert(List sourceList, Class desc) {
+ if (ObjectUtil.isNull(sourceList)) {
+ return null;
+ }
+ if (CollUtil.isEmpty(sourceList)) {
+ return CollUtil.newArrayList();
+ }
+ return CONVERTER.convert(sourceList, desc);
+ }
+
+ /**
+ * 将 Map 转换为 beanClass 类型的集合并返回
+ *
+ * @param map 数据来源
+ * @param beanClass bean类
+ * @return bean对象
+ */
+ public static T convert(Map map, Class beanClass) {
+ if (MapUtil.isEmpty(map)) {
+ return null;
+ }
+ if (ObjectUtil.isNull(beanClass)) {
+ return null;
+ }
+ return CONVERTER.convert(map, beanClass);
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/utils/MessageUtils.java b/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/utils/MessageUtils.java
new file mode 100644
index 00000000..c4936585
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/utils/MessageUtils.java
@@ -0,0 +1,28 @@
+package com.xmzs.common.core.utils;
+
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+import org.springframework.context.MessageSource;
+import org.springframework.context.i18n.LocaleContextHolder;
+
+/**
+ * 获取i18n资源文件
+ *
+ * @author Lion Li
+ */
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public class MessageUtils {
+
+ private static final MessageSource MESSAGE_SOURCE = SpringUtils.getBean(MessageSource.class);
+
+ /**
+ * 根据消息键和参数 获取消息 委托给spring messageSource
+ *
+ * @param code 消息键
+ * @param args 参数
+ * @return 获取国际化翻译值
+ */
+ public static String message(String code, Object... args) {
+ return MESSAGE_SOURCE.getMessage(code, args, LocaleContextHolder.getLocale());
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/utils/ServletUtils.java b/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/utils/ServletUtils.java
new file mode 100644
index 00000000..262b9efc
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/utils/ServletUtils.java
@@ -0,0 +1,193 @@
+package com.xmzs.common.core.utils;
+
+import cn.hutool.core.convert.Convert;
+import cn.hutool.extra.servlet.JakartaServletUtil;
+import cn.hutool.http.HttpStatus;
+import jakarta.servlet.ServletRequest;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+import org.springframework.http.MediaType;
+import org.springframework.web.context.request.RequestAttributes;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+
+import java.io.IOException;
+import java.net.URLDecoder;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 客户端工具类
+ *
+ * @author ruoyi
+ */
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public class ServletUtils extends JakartaServletUtil {
+
+ /**
+ * 获取String参数
+ */
+ public static String getParameter(String name) {
+ return getRequest().getParameter(name);
+ }
+
+ /**
+ * 获取String参数
+ */
+ public static String getParameter(String name, String defaultValue) {
+ return Convert.toStr(getRequest().getParameter(name), defaultValue);
+ }
+
+ /**
+ * 获取Integer参数
+ */
+ public static Integer getParameterToInt(String name) {
+ return Convert.toInt(getRequest().getParameter(name));
+ }
+
+ /**
+ * 获取Integer参数
+ */
+ public static Integer getParameterToInt(String name, Integer defaultValue) {
+ return Convert.toInt(getRequest().getParameter(name), defaultValue);
+ }
+
+ /**
+ * 获取Boolean参数
+ */
+ public static Boolean getParameterToBool(String name) {
+ return Convert.toBool(getRequest().getParameter(name));
+ }
+
+ /**
+ * 获取Boolean参数
+ */
+ public static Boolean getParameterToBool(String name, Boolean defaultValue) {
+ return Convert.toBool(getRequest().getParameter(name), defaultValue);
+ }
+
+ /**
+ * 获得所有请求参数
+ *
+ * @param request 请求对象{@link ServletRequest}
+ * @return Map
+ */
+ public static Map getParams(ServletRequest request) {
+ final Map map = request.getParameterMap();
+ return Collections.unmodifiableMap(map);
+ }
+
+ /**
+ * 获得所有请求参数
+ *
+ * @param request 请求对象{@link ServletRequest}
+ * @return Map
+ */
+ public static Map getParamMap(ServletRequest request) {
+ Map params = new HashMap<>();
+ for (Map.Entry entry : getParams(request).entrySet()) {
+ params.put(entry.getKey(), StringUtils.join(entry.getValue(), StringUtils.SEPARATOR));
+ }
+ return params;
+ }
+
+ /**
+ * 获取request
+ */
+ public static HttpServletRequest getRequest() {
+ return getRequestAttributes().getRequest();
+ }
+
+ /**
+ * 获取response
+ */
+ public static HttpServletResponse getResponse() {
+ return getRequestAttributes().getResponse();
+ }
+
+ /**
+ * 获取session
+ */
+ public static HttpSession getSession() {
+ return getRequest().getSession();
+ }
+
+ public static ServletRequestAttributes getRequestAttributes() {
+ RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
+ return (ServletRequestAttributes) attributes;
+ }
+
+ /**
+ * 将字符串渲染到客户端
+ *
+ * @param response 渲染对象
+ * @param string 待渲染的字符串
+ */
+ public static void renderString(HttpServletResponse response, String string) {
+ try {
+ response.setStatus(HttpStatus.HTTP_OK);
+ response.setContentType(MediaType.APPLICATION_JSON_VALUE);
+ response.setCharacterEncoding(StandardCharsets.UTF_8.toString());
+ response.getWriter().print(string);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * 是否是Ajax异步请求
+ *
+ * @param request
+ */
+ public static boolean isAjaxRequest(HttpServletRequest request) {
+
+ String accept = request.getHeader("accept");
+ if (accept != null && accept.contains(MediaType.APPLICATION_JSON_VALUE)) {
+ return true;
+ }
+
+ String xRequestedWith = request.getHeader("X-Requested-With");
+ if (xRequestedWith != null && xRequestedWith.contains("XMLHttpRequest")) {
+ return true;
+ }
+
+ String uri = request.getRequestURI();
+ if (StringUtils.equalsAnyIgnoreCase(uri, ".json", ".xml")) {
+ return true;
+ }
+
+ String ajax = request.getParameter("__ajax");
+ return StringUtils.equalsAnyIgnoreCase(ajax, "json", "xml");
+ }
+
+ public static String getClientIP() {
+ return getClientIP(getRequest());
+ }
+
+ /**
+ * 内容编码
+ *
+ * @param str 内容
+ * @return 编码后的内容
+ */
+ public static String urlEncode(String str) {
+ return URLEncoder.encode(str, StandardCharsets.UTF_8);
+ }
+
+ /**
+ * 内容解码
+ *
+ * @param str 内容
+ * @return 解码后的内容
+ */
+ public static String urlDecode(String str) {
+ return URLDecoder.decode(str, StandardCharsets.UTF_8);
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/utils/SpringUtils.java b/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/utils/SpringUtils.java
new file mode 100644
index 00000000..d69af178
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/utils/SpringUtils.java
@@ -0,0 +1,62 @@
+package com.xmzs.common.core.utils;
+
+import cn.hutool.extra.spring.SpringUtil;
+import org.springframework.aop.framework.AopContext;
+import org.springframework.beans.factory.NoSuchBeanDefinitionException;
+import org.springframework.context.ApplicationContext;
+import org.springframework.stereotype.Component;
+
+/**
+ * spring工具类
+ *
+ * @author Lion Li
+ */
+@Component
+public final class SpringUtils extends SpringUtil {
+
+ /**
+ * 如果BeanFactory包含一个与所给名称匹配的bean定义,则返回true
+ */
+ public static boolean containsBean(String name) {
+ return getBeanFactory().containsBean(name);
+ }
+
+ /**
+ * 判断以给定名字注册的bean定义是一个singleton还是一个prototype。
+ * 如果与给定名字相应的bean定义没有被找到,将会抛出一个异常(NoSuchBeanDefinitionException)
+ */
+ public static boolean isSingleton(String name) throws NoSuchBeanDefinitionException {
+ return getBeanFactory().isSingleton(name);
+ }
+
+ /**
+ * @return Class 注册对象的类型
+ */
+ public static Class> getType(String name) throws NoSuchBeanDefinitionException {
+ return getBeanFactory().getType(name);
+ }
+
+ /**
+ * 如果给定的bean名字在bean定义中有别名,则返回这些别名
+ */
+ public static String[] getAliases(String name) throws NoSuchBeanDefinitionException {
+ return getBeanFactory().getAliases(name);
+ }
+
+ /**
+ * 获取aop代理对象
+ */
+ @SuppressWarnings("unchecked")
+ public static T getAopProxy(T invoker) {
+ return (T) AopContext.currentProxy();
+ }
+
+
+ /**
+ * 获取spring上下文
+ */
+ public static ApplicationContext context() {
+ return getApplicationContext();
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/utils/StreamUtils.java b/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/utils/StreamUtils.java
new file mode 100644
index 00000000..8f6161fa
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/com/xmzs/common/core/utils/StreamUtils.java
@@ -0,0 +1,254 @@
+package com.xmzs.common.core.utils;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.map.MapUtil;
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+
+import java.util.*;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+
+/**
+ * stream 流工具类
+ *
+ * @author Lion Li
+ */
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public class StreamUtils {
+
+ /**
+ * 将collection过滤
+ *
+ * @param collection 需要转化的集合
+ * @param function 过滤方法
+ * @return 过滤后的list
+ */
+ public static List filter(Collection collection, Predicate function) {
+ if (CollUtil.isEmpty(collection)) {
+ return CollUtil.newArrayList();
+ }
+ // 注意此处不要使用 .toList() 新语法 因为返回的是不可变List 会导致序列化问题
+ return collection.stream().filter(function).collect(Collectors.toList());
+ }
+
+ /**
+ * 将collection拼接
+ *
+ * @param collection 需要转化的集合
+ * @param function 拼接方法
+ * @return 拼接后的list
+ */
+ public static String join(Collection collection, Function function) {
+ return join(collection, function, StringUtils.SEPARATOR);
+ }
+
+ /**
+ * 将collection拼接
+ *
+ * @param collection 需要转化的集合
+ * @param function 拼接方法
+ * @param delimiter 拼接符
+ * @return 拼接后的list
+ */
+ public static String join(Collection collection, Function function, CharSequence delimiter) {
+ if (CollUtil.isEmpty(collection)) {
+ return StringUtils.EMPTY;
+ }
+ return collection.stream().map(function).filter(Objects::nonNull).collect(Collectors.joining(delimiter));
+ }
+
+ /**
+ * 将collection排序
+ *
+ * @param collection 需要转化的集合
+ * @param comparing 排序方法
+ * @return 排序后的list
+ */
+ public static List sorted(Collection collection, Comparator comparing) {
+ if (CollUtil.isEmpty(collection)) {
+ return CollUtil.newArrayList();
+ }
+ // 注意此处不要使用 .toList() 新语法 因为返回的是不可变List 会导致序列化问题
+ return collection.stream().sorted(comparing).collect(Collectors.toList());
+ }
+
+ /**
+ * 将collection转化为类型不变的map
+ * {@code Collection ----> Map}
+ *
+ * @param collection 需要转化的集合
+ * @param key V类型转化为K类型的lambda方法
+ * @param collection中的泛型
+ * @param map中的key类型
+ * @return 转化后的map
+ */
+ public static Map toIdentityMap(Collection collection, Function key) {
+ if (CollUtil.isEmpty(collection)) {
+ return MapUtil.newHashMap();
+ }
+ return collection.stream().collect(Collectors.toMap(key, Function.identity(), (l, r) -> l));
+ }
+
+ /**
+ * 将Collection转化为map(value类型与collection的泛型不同)
+ * {@code Collection