update 集成 snail-ai-server

update 增加 ruoyi-ai服务
This commit is contained in:
疯狂的狮子Li
2026-06-02 14:11:32 +08:00
parent f19d567b6e
commit 629e6a4551
30 changed files with 1754 additions and 1 deletions
+12
View File
@@ -0,0 +1,12 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="ruoyi-ai" type="docker-deploy" factoryName="dockerfile" server-name="Docker">
<deployment type="dockerfile">
<settings>
<option name="imageTag" value="ruoyi/ruoyi-ai:2.5.3" />
<option name="buildOnly" value="true" />
<option name="sourceFilePath" value="ruoyi-modules/ruoyi-ai/Dockerfile" />
</settings>
</deployment>
<method v="2" />
</configuration>
</component>
+12
View File
@@ -0,0 +1,12 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="ruoyi-snailai-server" type="docker-deploy" factoryName="dockerfile" server-name="Docker">
<deployment type="dockerfile">
<settings>
<option name="imageTag" value="ruoyi/ruoyi-snailai-server:2.5.3" />
<option name="buildOnly" value="true" />
<option name="sourceFilePath" value="ruoyi-visual/ruoyi-snailai-server/Dockerfile" />
</settings>
</deployment>
<method v="2" />
</configuration>
</component>
+15 -1
View File
@@ -34,6 +34,7 @@
<redisson.version>4.4.0</redisson.version> <redisson.version>4.4.0</redisson.version>
<lock4j.version>2.2.7</lock4j.version> <lock4j.version>2.2.7</lock4j.version>
<snailjob.version>2.0.0</snailjob.version> <snailjob.version>2.0.0</snailjob.version>
<snailai.version>0.0.3</snailai.version>
<satoken.version>1.45.0</satoken.version> <satoken.version>1.45.0</satoken.version>
<lombok.version>1.18.42</lombok.version> <lombok.version>1.18.42</lombok.version>
<logstash.version>7.4</logstash.version> <logstash.version>7.4</logstash.version>
@@ -61,7 +62,7 @@
<!-- mq配置 --> <!-- mq配置 -->
<rocketmq.version>2.3.4</rocketmq.version> <rocketmq.version>2.3.4</rocketmq.version>
<!-- Spring AI 2.0 预览版,正式版发布后仅需调整此版本号 --> <!-- Spring AI 2.0 预览版,正式版发布后仅需调整此版本号 -->
<spring-ai.version>2.0.0-M6</spring-ai.version> <spring-ai.version>2.0.0-M8</spring-ai.version>
<!-- 插件版本 --> <!-- 插件版本 -->
<maven-compiler-plugin.version>3.14.0</maven-compiler-plugin.version> <maven-compiler-plugin.version>3.14.0</maven-compiler-plugin.version>
@@ -278,6 +279,19 @@
<version>${snailjob.version}</version> <version>${snailjob.version}</version>
</dependency> </dependency>
<!-- Snail AI Agent 启动器 -->
<dependency>
<groupId>com.aizuda</groupId>
<artifactId>snail-ai-agent-starter</artifactId>
<version>${snailai.version}</version>
</dependency>
<!-- Snail AI OpenAPI 启动器 -->
<dependency>
<groupId>com.aizuda</groupId>
<artifactId>snail-ai-openapi-starter</artifactId>
<version>${snailai.version}</version>
</dependency>
<!-- 加密包引入 --> <!-- 加密包引入 -->
<dependency> <dependency>
<groupId>org.bouncycastle</groupId> <groupId>org.bouncycastle</groupId>
+1
View File
@@ -40,6 +40,7 @@
<module>ruoyi-common-social</module> <module>ruoyi-common-social</module>
<module>ruoyi-common-nacos</module> <module>ruoyi-common-nacos</module>
<module>ruoyi-common-bus</module> <module>ruoyi-common-bus</module>
<module>ruoyi-common-ai</module>
<module>ruoyi-common-mqtt</module> <module>ruoyi-common-mqtt</module>
<module>ruoyi-common-mcp</module> <module>ruoyi-common-mcp</module>
</modules> </modules>
+39
View File
@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common</artifactId>
<version>${revision}</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>ruoyi-common-ai</artifactId>
<description>
ruoyi-common-ai AI公共模块
</description>
<dependencies>
<!-- 核心模块 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-core</artifactId>
</dependency>
<!-- Snail AI Agent 启动器 -->
<dependency>
<groupId>com.aizuda</groupId>
<artifactId>snail-ai-agent-starter</artifactId>
</dependency>
<!-- Snail AI OpenAPI 启动器 -->
<dependency>
<groupId>com.aizuda</groupId>
<artifactId>snail-ai-openapi-starter</artifactId>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,16 @@
package org.dromara.common.ai.config;
import com.aizuda.snail.ai.agent.starter.EnableSnailAiAgent;
import com.aizuda.snail.ai.openapi.client.starter.EnableSnailAiOpenApi;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
/**
* Snail AI 自动配置
*/
@AutoConfiguration
@ConditionalOnProperty(prefix = "snail-ai", name = "enabled", havingValue = "true")
@EnableSnailAiAgent
@EnableSnailAiOpenApi
public class SnailAiConfig {
}
+7
View File
@@ -223,6 +223,13 @@
<version>${revision}</version> <version>${revision}</version>
</dependency> </dependency>
<!-- AI模块 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-ai</artifactId>
<version>${revision}</version>
</dependency>
<dependency> <dependency>
<groupId>org.dromara</groupId> <groupId>org.dromara</groupId>
<artifactId>ruoyi-common-mqtt</artifactId> <artifactId>ruoyi-common-mqtt</artifactId>
+1
View File
@@ -14,6 +14,7 @@
<module>ruoyi-job</module> <module>ruoyi-job</module>
<module>ruoyi-resource</module> <module>ruoyi-resource</module>
<module>ruoyi-workflow</module> <module>ruoyi-workflow</module>
<module>ruoyi-ai</module>
</modules> </modules>
<artifactId>ruoyi-modules</artifactId> <artifactId>ruoyi-modules</artifactId>
+26
View File
@@ -0,0 +1,26 @@
# 贝尔实验室 Spring 官方推荐镜像 JDK下载地址 https://bell-sw.com/pages/downloads/
FROM bellsoft/liberica-openjdk-rocky:17.0.16-cds
#FROM bellsoft/liberica-openjdk-rocky:21.0.8-cds
#FROM findepi/graalvm:java17-native
LABEL maintainer="Lion Li"
RUN mkdir -p /ruoyi/ai/logs \
/ruoyi/skywalking/agent
WORKDIR /ruoyi/ai
ENV SERVER_PORT=9206 LANG=C.UTF-8 LC_ALL=C.UTF-8 JAVA_OPTS=""
EXPOSE ${SERVER_PORT}
EXPOSE 18889
ADD ./target/ruoyi-ai.jar ./app.jar
SHELL ["/bin/bash", "-c"]
ENTRYPOINT java -Djava.security.egd=file:/dev/./urandom -Dserver.port=${SERVER_PORT} \
#-Dskywalking.agent.service_name=ruoyi-ai \
#-javaagent:/ruoyi/skywalking/agent/skywalking-agent.jar \
-XX:+HeapDumpOnOutOfMemoryError -XX:+UseZGC ${JAVA_OPTS} \
-jar app.jar
+103
View File
@@ -0,0 +1,103 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-modules</artifactId>
<version>${revision}</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<packaging>jar</packaging>
<artifactId>ruoyi-ai</artifactId>
<description>
ai模块
</description>
<dependencies>
<!-- Snail AI Agent 0.0.3 需要 protobuf 4.x,避免被 Dubbo 传递的 3.x 抢先选中 -->
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>4.35.0</version>
</dependency>
<!-- 局部提升 ruoyi-ai 的 gRPC 运行时,与 Snail AI 0.0.3 保持一致 -->
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-protobuf</artifactId>
<version>1.81.0</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-stub</artifactId>
<version>1.81.0</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-netty-shaded</artifactId>
<version>1.81.0</version>
</dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-nacos</artifactId>
</dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-ai</artifactId>
</dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-doc</artifactId>
</dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-satoken</artifactId>
</dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-dubbo</artifactId>
</dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-web</artifactId>
</dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-security</artifactId>
</dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-api-system</artifactId>
</dependency>
</dependencies>
<build>
<finalName>${project.artifactId}</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot.version}</version>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,22 @@
package org.dromara.ai;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.metrics.buffering.BufferingApplicationStartup;
/**
* AI模块
*
* @author opensnail
*/
@SpringBootApplication
public class RuoYiAiApplication {
public static void main(String[] args) {
SpringApplication application = new SpringApplication(RuoYiAiApplication.class);
application.setApplicationStartup(new BufferingApplicationStartup(2048));
application.run(args);
System.out.println("(♥◠‿◠)ノ゙ AI模块启动成功 ლ(´ڡ`ლ)゙ ");
}
}
@@ -0,0 +1,364 @@
package org.dromara.ai.controller;
import com.aizuda.snail.ai.common.execption.SnailAiException;
import com.aizuda.snail.ai.common.model.Result;
import com.aizuda.snail.ai.common.openapi.dto.*;
import com.aizuda.snail.ai.openapi.client.core.api.OpenApiAgentClient;
import com.aizuda.snail.ai.openapi.client.core.api.OpenApiChatClient;
import com.aizuda.snail.ai.openapi.client.core.api.OpenApiConversationClient;
import com.aizuda.snail.ai.openapi.client.core.api.OpenApiUserClient;
import com.aizuda.snail.ai.openapi.client.core.listener.SseEventListener;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.domain.PageResult;
import org.dromara.common.core.domain.R;
import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.common.web.core.BaseController;
import org.dromara.system.api.model.LoginUser;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.MediaType;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Snail AI OpenAPI 控制器
*
* @author opensnail
* @date 2026-04-25
*/
@Slf4j
@Validated
@RestController
@RequestMapping
@RequiredArgsConstructor
public class SnailAiController extends BaseController {
/**
* Snail AI 成功状态码。
*/
private static final int SNAIL_AI_SUCCESS = 1;
/**
* SSE 超时时间。
*/
private static final long SSE_TIMEOUT = 300000L;
private final OpenApiAgentClient agentClient;
private final OpenApiChatClient chatClient;
private final OpenApiConversationClient conversationClient;
private final OpenApiUserClient userClient;
/**
* 聊天模式。
*/
@Value("${snail-ai.chat-mode:stream}")
private String chatMode;
/**
* 注册当前登录用户并返回 OpenAPI 用户信息。
*/
@PostMapping("/user/register")
public R<OpenApiUserVO> registerCurrentUser() {
return R.ok(ensureOpenApiUser());
}
/**
* 查询当前登录用户对应的 OpenAPI 用户信息。
*/
@GetMapping("/user")
public R<OpenApiUserVO> getUser() {
OpenApiUserQueryRequest request = new OpenApiUserQueryRequest();
request.setOpenId(ensureOpenId());
return toR(userClient.getUser(request));
}
/**
* 查询当前用户可访问的智能体列表。
*/
@GetMapping("/agents")
public R<List<OpenApiAgentVO>> listAgents() {
return toR(agentClient.listAgents());
}
/**
* 根据智能体 ID 查询智能体详情。
*/
@GetMapping("/agent/{agentId}")
public R<OpenApiAgentVO> getAgent(@NotNull(message = "智能体ID不能为空") @PathVariable Long agentId) {
OpenApiAgentIdentityRequest request = new OpenApiAgentIdentityRequest();
request.setAgentId(agentId);
return toR(agentClient.getAgent(request));
}
/**
* 为指定智能体创建新会话。
*/
@PostMapping("/agent/{agentId}/conversation")
public R<OpenApiConversationVO> createConversation(
@NotNull(message = "智能体ID不能为空") @PathVariable Long agentId,
@RequestBody OpenApiCreateConversationRequest request) {
request.setAgentId(agentId);
request.setOpenId(ensureOpenId());
return toR(conversationClient.createConversation(request));
}
/**
* 分页查询指定智能体下的会话列表。
*/
@GetMapping("/agent/{agentId}/conversations")
public R<PageResult<OpenApiConversationVO>> listConversations(
@NotNull(message = "智能体ID不能为空") @PathVariable Long agentId,
@Min(value = 1, message = "页码不能小于1") @RequestParam(defaultValue = "1") int page,
@Min(value = 1, message = "每页条数不能小于1") @RequestParam(defaultValue = "10") int size) {
OpenApiConversationQueryRequest request = new OpenApiConversationQueryRequest();
request.setAgentId(agentId);
request.setOpenId(ensureOpenId());
request.setPage(page);
request.setSize(size);
return toPageR(conversationClient.listConversations(request));
}
/**
* 查询指定会话的消息历史。
*/
@GetMapping("/agent/{agentId}/conversation/{conversationId}/messages")
public R<List<OpenApiMessageVO>> getMessages(
@NotNull(message = "智能体ID不能为空") @PathVariable Long agentId,
@NotBlank(message = "会话ID不能为空") @PathVariable String conversationId) {
OpenApiConversationIdentityRequest request = new OpenApiConversationIdentityRequest();
request.setAgentId(agentId);
request.setConversationId(conversationId);
request.setOpenId(ensureOpenId());
return toR(conversationClient.getMessages(request));
}
/**
* 删除指定会话。
*/
@DeleteMapping("/agent/{agentId}/conversation/{conversationId}")
public R<Void> deleteConversation(
@NotNull(message = "智能体ID不能为空") @PathVariable Long agentId,
@NotBlank(message = "会话ID不能为空") @PathVariable String conversationId) {
OpenApiConversationIdentityRequest request = new OpenApiConversationIdentityRequest();
request.setAgentId(agentId);
request.setConversationId(conversationId);
request.setOpenId(ensureOpenId());
return toR(conversationClient.deleteConversation(request));
}
/**
* 获取当前聊天发送模式。
*/
@GetMapping("/chat/mode")
public R<Map<String, String>> getChatMode() {
String mode = "sync".equalsIgnoreCase(chatMode) ? "sync" : "stream";
return R.ok(Map.of("mode", mode));
}
/**
* 同步对话接口。
*/
@PostMapping("/agent/{agentId}/chat/sync")
public R<OpenApiChatSyncResponse> chatSync(
@NotNull(message = "智能体ID不能为空") @PathVariable Long agentId,
@RequestBody OpenApiChatRequest request) {
request.setAgentId(agentId);
request.setOpenId(ensureOpenId());
log.info("Sync chat request: agentId={}", agentId);
return toR(chatClient.chatSync(request));
}
/**
* 流式对话接口,按 SSE 事件返回消息分片。
*/
@PostMapping(value = "/agent/{agentId}/chat/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter chatStream(
@NotNull(message = "智能体ID不能为空") @PathVariable Long agentId,
@RequestBody OpenApiChatRequest request,
HttpServletResponse response) {
prepareSseResponse(response);
SseEmitter emitter = new SseEmitter(SSE_TIMEOUT);
AtomicBoolean closed = new AtomicBoolean(false);
emitter.onTimeout(() -> {
if (!closed.get()) {
safeSend(emitter, closed, "error", "SSE stream timeout");
safeSend(emitter, closed, "done", "");
if (closed.compareAndSet(false, true)) {
emitter.complete();
}
}
});
emitter.onCompletion(() -> closed.set(true));
emitter.onError(error -> {
closed.set(true);
log.warn("SSE emitter error: {}", error.getMessage());
});
request.setAgentId(agentId);
request.setOpenId(ensureOpenId());
log.info("Stream chat request: agentId={}", agentId);
try {
chatClient.chatStream(request, new SseEventListener() {
/**
* 推送普通文本片段。
*
* @param text 文本片段
*/
@Override
public void onText(String text) {
safeSend(emitter, closed, "text", text);
}
/**
* 推送思考内容片段。
*
* @param thinking 思考内容
*/
@Override
public void onThinking(String thinking) {
safeSend(emitter, closed, "thinking", thinking);
}
/**
* 处理流式对话完成事件。
*
* @param data 完成数据
*/
@Override
public void onComplete(String data) {
if (!closed.get()) {
safeSend(emitter, closed, "done", data);
log.info("Stream chat completed");
if (closed.compareAndSet(false, true)) {
emitter.complete();
}
}
}
/**
* 处理流式对话异常事件。
*
* @param errorMessage 错误信息
*/
@Override
public void onError(String errorMessage) {
log.error("Stream chat error: {}", errorMessage);
if (!closed.get()) {
safeSend(emitter, closed, "error", errorMessage);
safeSend(emitter, closed, "done", "");
if (closed.compareAndSet(false, true)) {
emitter.complete();
}
}
}
});
} catch (Exception e) {
log.error("Stream chat exception", e);
if (!closed.get()) {
safeSend(emitter, closed, "error", "stream exception: " + e.getMessage());
safeSend(emitter, closed, "done", "");
if (closed.compareAndSet(false, true)) {
emitter.complete();
}
}
}
return emitter;
}
/**
* 转换 Snail AI 普通响应为项目统一响应。
*/
private <T> R<T> toR(Result<T> result) {
if (result == null) {
return R.fail("Snail AI 服务返回为空");
}
if (result.getStatus() != SNAIL_AI_SUCCESS) {
return R.fail(result.getMessage());
}
return R.ok(result.getData());
}
/**
* 转换 Snail AI 分页响应为项目统一分页响应。
*/
private <T> R<PageResult<T>> toPageR(com.aizuda.snail.ai.common.model.PageResult<List<T>> result) {
R<List<T>> response = toR(result);
if (R.isError(response)) {
return R.fail(response.getMsg());
}
return R.ok(PageResult.build(response.getData(), result.getTotal()));
}
/**
* 输出一条 SSE 事件。
*/
private boolean safeSend(SseEmitter emitter, AtomicBoolean closed, String event, String data) {
if (closed.get()) {
return false;
}
try {
emitter.send(SseEmitter.event().name(event).data(data == null ? "" : data));
return true;
} catch (IOException e) {
closed.set(true);
log.warn("SSE send failed, event={}", event, e);
emitter.completeWithError(e);
return false;
}
}
/**
* 设置 SSE 响应头,覆盖统一鉴权成功路径中的默认 JSON 响应类型。
*
* @param response 当前响应
*/
private void prepareSseResponse(HttpServletResponse response) {
response.setContentType(MediaType.TEXT_EVENT_STREAM_VALUE);
response.setCharacterEncoding("UTF-8");
response.setHeader("Cache-Control", "no-cache");
response.setHeader("X-Accel-Buffering", "no");
}
/**
* 获取当前登录用户对应的 openId,不存在时会自动注册。
*/
private String ensureOpenId() {
return ensureOpenApiUser().getOpenId();
}
/**
* 确保当前登录用户已注册为 OpenAPI 用户。
*/
private OpenApiUserVO ensureOpenApiUser() {
Long userId = LoginHelper.getUserId();
LoginUser loginUser = LoginHelper.getLoginUser();
if (loginUser == null || userId == null) {
throw new SnailAiException("当前登录用户为空");
}
OpenApiUserRegisterRequest registerRequest = new OpenApiUserRegisterRequest();
registerRequest.setExternalId(String.valueOf(userId));
registerRequest.setNickname(loginUser.getNickname());
Result<OpenApiUserVO> registerResult = userClient.register(registerRequest);
if (registerResult == null) {
throw new SnailAiException("注册 OpenAPI 用户失败,返回为空");
}
if (registerResult.getStatus() != SNAIL_AI_SUCCESS) {
throw new SnailAiException(registerResult.getMessage());
}
if (registerResult.getData() == null) {
throw new SnailAiException("注册 OpenAPI 用户失败,返回为空");
}
return registerResult.getData();
}
}
@@ -0,0 +1,33 @@
# Tomcat
server:
port: 9206
# Spring
spring:
application:
# 应用名称
name: ruoyi-ai
profiles:
# 环境配置
active: @profiles.active@
--- # nacos 配置
spring:
cloud:
nacos:
# nacos 服务地址
server-addr: @nacos.server@
username: @nacos.username@
password: @nacos.password@
discovery:
# 注册组
group: DEFAULT_GROUP
namespace: public
config:
# 配置组
group: DEFAULT_GROUP
namespace: public
config:
import:
- optional:nacos:application-common.yml
- optional:nacos:${spring.application.name}.yml
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="60 seconds" debug="false">
<!-- 日志存放路径 -->
<property name="log.path" value="logs/${project.artifactId}"/>
<!-- 日志输出格式 -->
<property name="console.log.pattern"
value="%cyan(%d{yyyy-MM-dd HH:mm:ss}) %green([%thread]) %highlight(%-5level) %boldMagenta(%logger{36}%n) - %msg%n"/>
<!-- 控制台输出 -->
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${console.log.pattern}</pattern>
<charset>utf-8</charset>
</encoder>
</appender>
<include resource="logback-common.xml" />
<include resource="logback-logstash.xml" />
<!--系统操作日志-->
<root level="info">
<appender-ref ref="console"/>
</root>
</configuration>
+1
View File
@@ -10,6 +10,7 @@
<modules> <modules>
<module>ruoyi-monitor</module> <module>ruoyi-monitor</module>
<module>ruoyi-snailai-server</module>
<module>ruoyi-snailjob-server</module> <module>ruoyi-snailjob-server</module>
</modules> </modules>
@@ -0,0 +1,22 @@
# 贝尔实验室 Spring 官方推荐镜像 JDK下载地址 https://bell-sw.com/pages/downloads/
FROM bellsoft/liberica-openjdk-rocky:21.0.8-cds
#FROM findepi/graalvm:java21-native
LABEL maintainer="Lion Li"
RUN mkdir -p /ruoyi/snailai/logs
WORKDIR /ruoyi/snailai
ENV LANG=C.UTF-8 LC_ALL=C.UTF-8 JAVA_OPTS=""
EXPOSE 8900
EXPOSE 18888
ADD ./target/ruoyi-snailai-server.jar ./app.jar
SHELL ["/bin/bash", "-c"]
ENTRYPOINT java -Djava.security.egd=file:/dev/./urandom \
-XX:+HeapDumpOnOutOfMemoryError -XX:+UseZGC ${JAVA_OPTS} \
-jar app.jar
+53
View File
@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-visual</artifactId>
<version>${revision}</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>ruoyi-snailai-server</artifactId>
<dependencies>
<!-- Snail AI 服务端 -->
<dependency>
<groupId>com.aizuda</groupId>
<artifactId>snail-ai-starter</artifactId>
<version>${snailai.version}</version>
</dependency>
<!-- Spring Boot Admin 客户端 -->
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-starter-client</artifactId>
<version>${spring-boot-admin.version}</version>
</dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-nacos</artifactId>
</dependency>
</dependencies>
<build>
<finalName>${project.artifactId}</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot.version}</version>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,97 @@
package com.aizuda.snail.ai.starter.filter;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.FilterConfig;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
/**
* Actuator Basic Auth 认证过滤器。
*
* @author Lion Li
*/
public class ActuatorAuthFilter implements Filter {
/**
* 认证用户名。
*/
private final String username;
/**
* 认证密码。
*/
private final String password;
/**
* 创建 Actuator 认证过滤器。
*
* @param username 认证用户名
* @param password 认证密码
*/
public ActuatorAuthFilter(String username, String password) {
this.username = username;
this.password = password;
}
/**
* 校验 Actuator Basic Auth 请求。
*
* @param servletRequest 原始请求
* @param servletResponse 原始响应
* @param filterChain 过滤器链
* @throws IOException IO 异常
* @throws ServletException Servlet 异常
*/
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
String authHeader = request.getHeader("Authorization");
if (authHeader == null || !authHeader.startsWith("Basic ")) {
response.setHeader("WWW-Authenticate", "Basic realm=\"realm\"");
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
return;
}
String base64Credentials = authHeader.substring("Basic ".length());
byte[] credDecoded = Base64.getDecoder().decode(base64Credentials);
String credentials = new String(credDecoded, StandardCharsets.UTF_8);
String[] split = credentials.split(":");
if (split.length != 2) {
response.setHeader("WWW-Authenticate", "Basic realm=\"realm\"");
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
return;
}
if (!username.equals(split[0]) || !password.equals(split[1])) {
response.setHeader("WWW-Authenticate", "Basic realm=\"realm\"");
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
return;
}
filterChain.doFilter(request, response);
}
/**
* 初始化过滤器。
*
* @param filterConfig 过滤器配置
*/
@Override
public void init(FilterConfig filterConfig) {
}
/**
* 销毁过滤器。
*/
@Override
public void destroy() {
}
}
@@ -0,0 +1,40 @@
package com.aizuda.snail.ai.starter.filter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 权限安全配置
*
* @author Lion Li
*/
@Configuration
public class SecurityConfig {
/**
* 认证用户名。
*/
@Value("${spring.cloud.nacos.discovery.metadata.username}")
private String username;
/**
* 认证密码。
*/
@Value("${spring.cloud.nacos.discovery.metadata.userpassword}")
private String password;
/**
* 注册 Actuator Basic Auth 过滤器。
*
* @return Actuator 过滤器注册对象
*/
@Bean
public FilterRegistrationBean<ActuatorAuthFilter> actuatorFilterRegistrationBean() {
FilterRegistrationBean<ActuatorAuthFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new ActuatorAuthFilter(username, password));
registrationBean.addUrlPatterns("/actuator", "/actuator/*");
return registrationBean;
}
}
@@ -0,0 +1,23 @@
package org.dromara.snailai;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* Snail AI Server 启动程序
*
* @author Lion Li
* @date 2026-05-26
*/
@SpringBootApplication
public class SnailAiServerApplication {
/**
* Snail AI 服务启动入口。
*
* @param args 启动参数
*/
public static void main(String[] args) {
com.aizuda.snail.ai.starter.SnailAiSpringbootApplication.main(args);
}
}
@@ -0,0 +1,90 @@
server:
port: 8900
servlet:
context-path: /snail-ai
spring:
application:
name: ruoyi-snailai-server
servlet:
multipart:
enabled: true
# 单个文件的最大限制 (根据 RAG 需求建议设为 50MB - 100MB)
max-file-size: 50MB
# 单次请求的总文件大小限制 (如果支持多文件并行上传,调大此项)
max-request-size: 100MB
web:
resources:
static-locations: classpath:admin/
profiles:
active: @profiles.active@
snail-ai:
server:
grpc-port: 18888
skill:
upload-dir: ./upload/skills
crypto:
secret-key: ${SNAIL_AI_CRYPTO_KEY:0123456789abcdef0123456789abcdef}
iv: ${SNAIL_AI_CRYPTO_IV:fedcba9876543210fedcba9876543210}
resource:
storage-type: LOCAL
upload-dir: ./upload/resource
minio:
endpoint: http://localhost:9000
access-key: minioadmin
secret-key: minioadmin
bucket: snail-ai
# 短期记忆配置
memory:
short-term:
# 存储类型选项: memory(内存) | db(数据库)
# - memory: 适用于单机部署,高性能,重启后数据丢失
# - db: 适用于分布式部署,数据持久化,性能较低
store-type: db
mybatis-plus:
mapper-locations: classpath:/mapper/*.xml
typeAliasesPackage: com.example.snail.job.po
global-config:
db-config:
capital-mode: false
logic-delete-value: 1
logic-not-delete-value: 0
configuration:
map-underscore-to-camel-case: true
cache-enabled: true
logging:
config: classpath:logback-plus.xml
management:
endpoints:
web:
exposure:
include: '*'
endpoint:
health:
show-details: ALWAYS
logfile:
external-file: ./logs/${spring.application.name}/console.log
--- # nacos 配置
spring:
cloud:
nacos:
# nacos 服务地址
server-addr: @nacos.server@
username: @nacos.username@
password: @nacos.password@
discovery:
# 注册组
group: DEFAULT_GROUP
namespace: public
config:
# 配置组
group: DEFAULT_GROUP
namespace: public
config:
import:
- optional:nacos:datasource.yml
- optional:nacos:${spring.application.name}.yml
@@ -0,0 +1,9 @@
Application Version: ${revision}
Spring Boot Version: ${spring-boot.version}
_ _ _
(_) | (_)
___ _ __ __ _ _| |______ __ _ _
/ __| '_ \ / _` | | |______/ _` | |
\__ \ | | | (_| | | | | (_| | |
|___/_| |_|\__,_|_|_| \__,_|_|
@@ -0,0 +1,87 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<property name="log.path" value="./logs/ruoyi-snailai-server" />
<property name="console.log.pattern"
value="%cyan(%d{yyyy-MM-dd HH:mm:ss}) %green([%thread]) %highlight(%-5level) %boldMagenta(%logger{36}%n) - %msg%n"/>
<property name="log.pattern" value="%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"/>
<!-- 控制台输出 -->
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${console.log.pattern}</pattern>
<charset>utf-8</charset>
</encoder>
</appender>
<!-- 控制台输出 -->
<appender name="file_console" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.path}/console.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 日志文件名格式 -->
<fileNamePattern>${log.path}/console.%d{yyyy-MM-dd}.log</fileNamePattern>
<!-- 日志最大 1天 -->
<maxHistory>1</maxHistory>
</rollingPolicy>
<encoder>
<pattern>${log.pattern}</pattern>
<charset>utf-8</charset>
</encoder>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<!-- 过滤的级别 -->
<level>INFO</level>
</filter>
</appender>
<appender name="file_info" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.path}/info.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<FileNamePattern>${log.path}/info.%d{yyyy-MM-dd}.log</FileNamePattern>
<MaxHistory>60</MaxHistory>
</rollingPolicy>
<encoder>
<pattern>${log.pattern}</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>INFO</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<appender name="file_error" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.path}/error.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<FileNamePattern>${log.path}/error.%d{yyyy-MM-dd}.log
</FileNamePattern>
<MaxHistory>60</MaxHistory>
</rollingPolicy>
<encoder>
<pattern>${log.pattern}</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<appender name ="async_info" class= "ch.qos.logback.classic.AsyncAppender">
<discardingThreshold >100</discardingThreshold>
<queueSize>1024</queueSize>
<appender-ref ref ="file_info"/>
</appender>
<appender name ="async_error" class= "ch.qos.logback.classic.AsyncAppender">
<discardingThreshold >100</discardingThreshold>
<queueSize>1024</queueSize>
<appender-ref ref ="file_error"/>
</appender>
<!-- 控制台输出日志级别 -->
<root level="info">
<appender-ref ref="console" />
<appender-ref ref="file_console" />
<appender-ref ref="async_info" />
<appender-ref ref="async_error" />
</root>
</configuration>
+4
View File
@@ -13,6 +13,10 @@ datasource:
url: jdbc:mysql://localhost:3306/ry-job?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true url: jdbc:mysql://localhost:3306/ry-job?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
username: root username: root
password: password password: password
ai:
url: jdbc:mysql://localhost:3306/ry-ai?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
username: root
password: password
workflow: workflow:
url: jdbc:mysql://localhost:3306/ry-workflow?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true url: jdbc:mysql://localhost:3306/ry-workflow?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true
username: root username: root
+46
View File
@@ -0,0 +1,46 @@
snail-ai:
# 启用客户端模式
enabled: true
# 聊天发送模式: stream(流式) / sync(同步)
chat-mode: stream
# ==================== Server 连接 ====================
# Server 端 gRPC 地址(即 ruoyi-snailai-server 的 snail-ai.server.grpc-port)
server-host: 127.0.0.1
server-port: 18888
# ==================== 客户端配置 ====================
# 应用 ID(在 Server「应用管理」页面创建后获取)
app-id: 1
# 认证令牌(在 Server「应用管理」页面创建时自动生成)
token: SAI_8f1c789e35454ebdabce0b739963da9a
# 本客户端 gRPC 端口(Server 通过此端口分发 Chat 请求)
port: 18889
# 客户端 IP 指定,留空默认自动获取本地 IP
host:
# 最大并发对话数
max-concurrent-chats: 10
# Skill 文件临时目录
skill-temp-dir: /tmp/snail-ai-agent/skills
# Server 回调配置
server-callback:
# Server 回调超时时间(毫秒)
timeout: 5000
# Server 回调重试次数
retry-times: 3
# Server 回调重试间隔(毫秒)
retry-interval: 1000
# ==================== OpenAPI Client 配置 ====================
openapi:
# 启用 OpenAPI Client
enabled: true
# OpenAPI 服务端地址,不配置则取 snail-ai.server-host
server-host:
# Server HTTP 端口
web-port: 8900
# 是否使用 HTTPS
https: false
# API 路径前缀
prefix: snail-ai
# 超时配置(毫秒)
connect-timeout-ms: 5000
read-timeout-ms: 60000
chat-timeout-ms: 300000
+5
View File
@@ -56,6 +56,11 @@ spring:
- Path=/resource/** - Path=/resource/**
filters: filters:
- StripPrefix=1 - StripPrefix=1
# AI服务
- id: ruoyi-ai
uri: lb://ruoyi-ai
predicates:
- Path=/snail-ai/**
# workflow服务 # workflow服务
- id: ruoyi-workflow - id: ruoyi-workflow
uri: lb://ruoyi-workflow uri: lb://ruoyi-workflow
@@ -0,0 +1,48 @@
spring:
datasource:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: ${datasource.ai.url}
username: ${datasource.ai.username}
password: ${datasource.ai.password}
hikari:
connection-timeout: 30000
validation-timeout: 5000
minimum-idle: 10
maximum-pool-size: 20
idle-timeout: 600000
max-lifetime: 900000
keepaliveTime: 30000
cloud:
nacos:
discovery:
metadata:
# 解决服务有 context-path 无法监控问题
management.context-path: ${server.servlet.context-path}/actuator
# 监控账号密码
username: ruoyi
userpassword: 123456
snail-ai:
server:
grpc-port: 18888
skill:
upload-dir: ./upload/skills
crypto:
secret-key: ${SNAIL_AI_CRYPTO_KEY:0123456789abcdef0123456789abcdef}
iv: ${SNAIL_AI_CRYPTO_IV:fedcba9876543210fedcba9876543210}
resource:
storage-type: LOCAL
upload-dir: ./upload/resource
minio:
endpoint: http://localhost:9000
access-key: minioadmin
secret-key: minioadmin
bucket: snail-ai
# 短期记忆配置
memory:
short-term:
# 存储类型选项: memory(内存) | db(数据库)
# - memory: 适用于单机部署,高性能,重启后数据丢失
# - db: 适用于分布式部署,数据持久化,性能较低
store-type: db
+34
View File
@@ -165,6 +165,21 @@ services:
privileged: true privileged: true
network_mode: "host" network_mode: "host"
ruoyi-snailai-server:
image: ruoyi/ruoyi-snailai-server:2.5.3
container_name: ruoyi-snailai-server
environment:
# 时区上海
TZ: Asia/Shanghai
ports:
- "8900:8900"
- "18888:18888"
volumes:
- /docker/snailai/logs/:/ruoyi/snailai/logs
- /docker/snailai/upload/:/ruoyi/snailai/upload
privileged: true
network_mode: "host"
ruoyi-gateway: ruoyi-gateway:
image: ruoyi/ruoyi-gateway:2.5.3 image: ruoyi/ruoyi-gateway:2.5.3
container_name: ruoyi-gateway container_name: ruoyi-gateway
@@ -279,6 +294,25 @@ services:
privileged: true privileged: true
network_mode: "host" network_mode: "host"
ruoyi-ai:
image: ruoyi/ruoyi-ai:2.5.3
container_name: ruoyi-ai
environment:
# 时区上海
TZ: Asia/Shanghai
ports:
- "9206:9206"
- "18889:18889"
volumes:
# 配置文件
- /docker/ruoyi-ai/logs/:/ruoyi/ai/logs
# skywalking 探针
- /docker/skywalking/agent/:/ruoyi/skywalking/agent
# Snail AI Skill 临时目录
- /docker/snailai/agent/skills/:/tmp/snail-ai-agent/skills
privileged: true
network_mode: "host"
################################################################################################# #################################################################################################
#################################### 以下为扩展根据需求搭建 ######################################### #################################### 以下为扩展根据需求搭建 #########################################
+518
View File
@@ -0,0 +1,518 @@
-- ============================================================
-- Snail AI MySQL 全量建表脚本(仅 CREATE,无 ALTER)
-- 使用:mysql -u user -p database < snail_ai_schema.sql
-- ============================================================
-- ============================================================
-- Enterprise RAG Schema for MySQL
-- ============================================================
-- Knowledge Base
CREATE TABLE sai_rag
(
id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
description TEXT,
icon VARCHAR(512),
embedding_model_id BIGINT(128) NOT NULL,
dimension_of_vector_model INT NOT NULL COMMENT '向量维度',
rerank_model_id BIGINT(128),
search_engine_instance_id BIGINT(128),
vector_store_instance_id BIGINT(128),
search_engine_enable TINYINT(1) DEFAULT 0,
delimiter VARCHAR(32) DEFAULT '\n\n',
rag_enhancement TEXT,
config TEXT DEFAULT NULL COMMENT 'RAG检索和问答的页面配置参数',
dedup_strategy TINYINT(1) NOT NULL DEFAULT 2 COMMENT '去重策略: 0=NONE 1=BY_NAME 2=BY_CONTENT 3=BY_NAME_OR_CONTENT',
dedup_action TINYINT(1) NOT NULL DEFAULT 0 COMMENT '冲突动作: 0=REJECT 1=SKIP 2=OVERWRITE',
upload_confirm TINYINT(1) NOT NULL DEFAULT 1 COMMENT '上传前二次确认: 0-关 1-开',
create_dt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_dt TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4
COLLATE = utf8mb4_unicode_ci;
-- RAG Documents
CREATE TABLE sai_rag_document
(
id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
rag_id BIGINT NOT NULL,
name VARCHAR(255),
file_type VARCHAR(32),
source_type VARCHAR(32),
source_path VARCHAR(1024),
storage_path VARCHAR(1024),
storage_type VARCHAR(32) DEFAULT 'LOCAL',
file_size BIGINT DEFAULT 0,
content TEXT,
status TINYINT(1) DEFAULT 0 COMMENT '状态: 0-待处理 1-解析中 2-处理中 3-处理完成 4-处理失败',
error_msg TEXT,
chunk_count INT DEFAULT 0,
content_hash VARCHAR(64) DEFAULT NULL COMMENT '文件内容SHA-256哈希,用于去重',
resource_id BIGINT DEFAULT NULL COMMENT '关联资源库 sai_resource.id',
create_dt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_dt TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4
COLLATE = utf8mb4_unicode_ci;
CREATE INDEX idx_rag_doc_rag ON sai_rag_document (rag_id);
CREATE INDEX idx_rag_content_hash ON sai_rag_document (rag_id, content_hash);
CREATE INDEX idx_rag_name ON sai_rag_document (rag_id, name);
CREATE INDEX idx_rag_doc_resource ON sai_rag_document (resource_id);
-- RAG Chunks
CREATE TABLE sai_rag_chunk
(
id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
rag_id BIGINT NOT NULL,
document_id BIGINT NOT NULL,
paragraph_index INT,
chunk_index INT,
content TEXT,
token_count INT,
vector_id VARCHAR(128),
content_hash VARCHAR(64) DEFAULT NULL COMMENT 'chunk内容SHA-256,用于向量去重',
create_dt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_dt TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4
COLLATE = utf8mb4_unicode_ci;
CREATE INDEX idx_rag_chunk_rag ON sai_rag_chunk (rag_id);
CREATE INDEX idx_rag_chunk_document ON sai_rag_chunk (document_id);
CREATE INDEX idx_chunk_rag_hash ON sai_rag_chunk (rag_id, content_hash);
CREATE TABLE sai_user
(
id BIGINT AUTO_INCREMENT PRIMARY KEY,
role INT,
totals INT,
username VARCHAR(255),
email VARCHAR(64),
password VARCHAR(255) NOT NULL,
create_dt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
update_dt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uk_username (username)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4;
-- 默认管理员:admin / admin123
INSERT INTO sai_user (id, role, username, email, password, create_dt, update_dt)
VALUES (1, 2, 'admin', '', 'pbkdf2$120000$c25haWwtYWktYWRtaW4tMQ==$kakglT/wYKOgv/77Ah1stie58d/JbY2nGgq5DwgUBw4=',
'2026-02-11 13:56:48.210429', '2026-02-11 13:56:48.210429');
-- ============================================
-- 1. AI 模型提供商表
-- ============================================
CREATE TABLE IF NOT EXISTS sai_model_provider
(
id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
provider_name VARCHAR(255) NOT NULL COMMENT '提供商名称',
provider_key VARCHAR(50) NOT NULL COMMENT '提供商标识符',
description TEXT COMMENT '提供商描述',
icon_url VARCHAR(500) COMMENT 'LOGO图标URL',
is_enabled TINYINT(1) DEFAULT 1 COMMENT '是否启用',
created_dt TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updated_dt TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
UNIQUE KEY uk_provider_name (provider_name),
UNIQUE KEY uk_provider_key (provider_key)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4
COLLATE = utf8mb4_unicode_ci COMMENT = 'AI模型提供商表';
CREATE INDEX idx_provider_key ON sai_model_provider (provider_key);
CREATE INDEX idx_is_enabled ON sai_model_provider (is_enabled);
-- ============================================
-- 2. AI模型配置表
-- ============================================
CREATE TABLE IF NOT EXISTS sai_model_config
(
id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
provider_id BIGINT NOT NULL COMMENT '提供商ID',
model_name VARCHAR(255) NOT NULL COMMENT '模型名称',
model_key VARCHAR(100) NOT NULL COMMENT '模型标识符',
model_type VARCHAR(50) NOT NULL COMMENT '模型类型(CHAT/EMBEDDING/RERANKER/IMAGE/SPEECH)',
description VARCHAR(1000) COMMENT '模型描述',
api_key VARCHAR(1000) COMMENT 'API密钥(加密存储)',
api_endpoint VARCHAR(500) COMMENT 'API端点URL',
config_json TEXT COMMENT '模型参数配置(JSON格式)',
owner_id BIGINT COMMENT '所有者ID(NULL=全局,具体值=用户ID)',
scope VARCHAR(20) NOT NULL DEFAULT 'GLOBAL' COMMENT '作用域(GLOBAL/PERSONAL)',
is_default TINYINT(1) DEFAULT 0 COMMENT '是否为默认模型',
is_enabled TINYINT(1) DEFAULT 1 COMMENT '是否启用',
created_dt TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updated_dt TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
KEY fk_provider_id (provider_id)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4
COLLATE = utf8mb4_unicode_ci COMMENT = 'AI模型配置表';
CREATE INDEX idx_provider_model_type ON sai_model_config (provider_id, model_type);
CREATE INDEX idx_model_type_enabled ON sai_model_config (model_type, is_enabled);
CREATE INDEX idx_owner_id ON sai_model_config (owner_id);
CREATE INDEX idx_is_default ON sai_model_config (is_default);
CREATE INDEX idx_scope ON sai_model_config (scope);
CREATE INDEX idx_model_key ON sai_model_config (model_key);
-- ============================================
-- 3. 模型使用统计表
-- ============================================
CREATE TABLE IF NOT EXISTS sai_model_usage_stat
(
id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
model_id BIGINT NOT NULL COMMENT '模型ID',
user_id BIGINT NOT NULL COMMENT '用户ID',
total_calls BIGINT DEFAULT 0 COMMENT '总调用次数',
success_calls BIGINT DEFAULT 0 COMMENT '成功调用次数',
failed_calls BIGINT DEFAULT 0 COMMENT '失败调用次数',
total_tokens_used BIGINT DEFAULT 0 COMMENT '总Token使用量',
total_cost DECIMAL(18, 8) DEFAULT 0 COMMENT '总费用',
avg_response_time BIGINT DEFAULT 0 COMMENT '平均响应时间(毫秒)',
last_used_dt TIMESTAMP NULL DEFAULT NULL COMMENT '最后使用时间',
created_dt TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updated_dt TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
UNIQUE KEY unique_model_user (model_id, user_id),
KEY fk_stat_model_id (model_id)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4
COLLATE = utf8mb4_unicode_ci COMMENT = '模型使用统计表';
CREATE INDEX idx_model_id ON sai_model_usage_stat (model_id);
CREATE INDEX idx_user_id ON sai_model_usage_stat (user_id);
CREATE INDEX idx_last_used_dt ON sai_model_usage_stat (last_used_dt);
-- ============================================
-- 初始化数据 (可选)
-- ============================================
-- 插入常见的AI提供商(重复 provider_key 则忽略)
INSERT IGNORE INTO sai_model_provider (provider_name, provider_key, description, is_enabled)
VALUES ('OpenAI', 'openai', 'OpenAI官方模型 (GPT-4, GPT-3.5等)', 1),
('Claude', 'claude', 'Anthropic Claude模型', 1),
('Ollama', 'ollama', '本地开源模型 (Llama, Mistral等)', 1),
('Google Gemini', 'gemini', 'Google Gemini模型', 1),
('阿里云百炼', 'qwen', '阿里云百炼 OpenAI 兼容模型 (Qwen等)', 1),
('DeepSeek', 'deepseek', 'DeepSeek OpenAI 兼容模型', 1),
('智谱AI', 'zhipu', '智谱AI OpenAI 兼容模型 (GLM等)', 1);
-- ============================================
-- 智能体相关表
-- ============================================
-- 智能体主表
CREATE TABLE IF NOT EXISTS sai_agent
(
id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL COMMENT '智能体名称',
description TEXT COMMENT '智能体描述',
avatar VARCHAR(512) COMMENT '头像URL',
instruction TEXT COMMENT '系统指令(System Prompt)',
greeting TEXT COMMENT '欢迎语',
preset_questions TEXT COMMENT '预设问题列表(JSON数组字符串)',
chat_model_id BIGINT COMMENT '关联的对话模型ID',
memory_enabled TINYINT(1) DEFAULT 0 COMMENT '是否启用记忆库',
mcp_enabled TINYINT(1) DEFAULT 0 COMMENT '是否启用MCP',
skill_enabled TINYINT(1) DEFAULT 0 COMMENT '是否启用Skill',
web_search_enabled TINYINT(1) DEFAULT 0 COMMENT '是否启用联网搜索',
rag_enabled TINYINT(1) DEFAULT 0 COMMENT '是否启用RAG',
rag_id BIGINT NULL COMMENT '绑定的RAG ID',
short_term_memory_size INT DEFAULT 20 COMMENT '短期记忆滑动窗口保留条数',
creator_id BIGINT COMMENT '创建者用户ID',
is_featured TINYINT(1) DEFAULT 0 COMMENT '是否精选',
view_count INT DEFAULT 0 COMMENT '浏览次数',
status TINYINT DEFAULT 1 COMMENT '状态: 1-活跃 2-非活跃 3-已废弃 4-已禁用',
config TEXT COMMENT '扩展配置(预留)',
app_id VARCHAR(128) NULL COMMENT '关联应用ID(NULL=本地执行)',
create_dt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_dt TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4
COLLATE = utf8mb4_unicode_ci COMMENT = '智能体表';
CREATE INDEX idx_agent_creator ON sai_agent (creator_id);
CREATE INDEX idx_agent_featured ON sai_agent (is_featured);
-- 智能体对话表
CREATE TABLE IF NOT EXISTS sai_agent_conversation
(
id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
agent_id BIGINT NOT NULL COMMENT '智能体ID',
user_id BIGINT NOT NULL COMMENT '用户ID',
conversation_id VARCHAR(64) NOT NULL COMMENT '对话ID(UUID)',
title VARCHAR(255) COMMENT '对话标题',
create_dt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_dt TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uk_conv_id (conversation_id)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4
COLLATE = utf8mb4_unicode_ci COMMENT = '智能体对话表';
CREATE INDEX idx_agent_conv_agent ON sai_agent_conversation (agent_id);
CREATE INDEX idx_agent_conv_user ON sai_agent_conversation (user_id);
-- 智能体对话消息记录表
CREATE TABLE IF NOT EXISTS sai_agent_conversation_record
(
id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
agent_id BIGINT NOT NULL COMMENT '智能体ID',
conversation_id VARCHAR(64) NOT NULL COMMENT '对话ID',
user_id BIGINT NOT NULL COMMENT '用户ID',
role VARCHAR(16) DEFAULT 'user' COMMENT 'user/assistant',
content TEXT COMMENT '消息内容',
thinking TEXT COMMENT '思考过程(仅assistant)',
status INT DEFAULT 1 COMMENT '1=成功,2=失败,3=进行中',
token_count INT DEFAULT 0 COMMENT 'Token数',
create_dt TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4
COLLATE = utf8mb4_unicode_ci COMMENT = '智能体对话消息记录';
CREATE INDEX idx_agent_rec_conv ON sai_agent_conversation_record (conversation_id);
-- 智能体使用统计表
CREATE TABLE IF NOT EXISTS sai_agent_usage_stat
(
id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
agent_id BIGINT NOT NULL COMMENT '智能体ID',
user_id BIGINT NOT NULL COMMENT '用户ID',
user_name VARCHAR(255) COMMENT '用户名',
department VARCHAR(255) DEFAULT '' COMMENT '部门',
message_count INT DEFAULT 0 COMMENT '消息条数',
conversation_count INT DEFAULT 0 COMMENT '对话轮次',
stat_date DATE NOT NULL COMMENT '统计日期',
create_dt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_dt TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uk_agent_user_date (agent_id, user_id, stat_date)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4
COLLATE = utf8mb4_unicode_ci COMMENT = '智能体使用统计';
CREATE INDEX idx_usage_agent ON sai_agent_usage_stat (agent_id);
CREATE INDEX idx_usage_date ON sai_agent_usage_stat (stat_date);
-- ============================================
-- MCP 服务管理
-- ============================================
-- MCP 服务配置表
CREATE TABLE IF NOT EXISTS sai_mcp_server
(
id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL COMMENT 'MCP服务名称',
description TEXT COMMENT 'MCP服务描述',
transport_type TINYINT(1) DEFAULT 1 COMMENT '传输类型: 1-SSE 2-Streamable HTTP 3-Stdio',
base_uri VARCHAR(1024) COMMENT '服务基础地址(SSE/Streamable HTTP时使用)',
endpoint VARCHAR(1024) COMMENT '端点路径(SSE/Streamable HTTP时可选)',
command VARCHAR(1024) COMMENT 'Stdio命令(Stdio时必填)',
args TEXT COMMENT 'Stdio命令参数(JSON数组)',
env_vars TEXT COMMENT 'Stdio环境变量(JSON对象)',
version VARCHAR(32) DEFAULT '1.0.0' COMMENT '版本',
auth_type TINYINT(1) DEFAULT 0 COMMENT '认证方式: 0-无需认证 1-API Key 2-OAuth 3-Basic Auth',
auth_config TEXT COMMENT '认证配置(JSON)',
status TINYINT(1) DEFAULT 0 COMMENT '状态: 0-未连接 1-已连接 2-异常',
capabilities TEXT COMMENT '能力列表(JSON数组)',
last_connect_dt TIMESTAMP NULL COMMENT '最后连接时间',
creator_id BIGINT COMMENT '创建者用户ID',
create_dt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_dt TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4
COLLATE = utf8mb4_unicode_ci COMMENT = 'MCP服务配置表';
CREATE INDEX idx_mcp_server_creator ON sai_mcp_server (creator_id);
CREATE INDEX idx_mcp_server_status ON sai_mcp_server (status);
-- 智能体与MCP服务关联表(多对多)
CREATE TABLE IF NOT EXISTS sai_agent_mcp_server
(
id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
agent_id BIGINT NOT NULL COMMENT '智能体ID',
mcp_server_id BIGINT NOT NULL COMMENT 'MCP服务ID',
create_dt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uk_agent_mcp (agent_id, mcp_server_id)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4
COLLATE = utf8mb4_unicode_ci COMMENT = '智能体MCP服务关联表';
CREATE INDEX idx_agent_mcp_agent ON sai_agent_mcp_server (agent_id);
CREATE INDEX idx_agent_mcp_server ON sai_agent_mcp_server (mcp_server_id);
-- 用户订阅的智能体(多对多)
CREATE TABLE IF NOT EXISTS sai_user_agent
(
id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT NOT NULL COMMENT '用户ID',
agent_id BIGINT NOT NULL COMMENT '智能体ID',
create_dt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uk_user_agent (user_id, agent_id)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4
COLLATE = utf8mb4_unicode_ci COMMENT = '用户订阅的智能体';
CREATE INDEX idx_user_agent_user ON sai_user_agent (user_id);
-- ============================================
-- Skill 技能包管理
-- ============================================
-- Skill 技能包表
CREATE TABLE IF NOT EXISTS sai_skill
(
id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL COMMENT 'Skill名称(从SKILL.md解析)',
description TEXT COMMENT 'Skill描述(从SKILL.md解析)',
file_name VARCHAR(255) COMMENT '上传的zip文件名',
file_path VARCHAR(1024) COMMENT '解压后存储路径',
file_size BIGINT DEFAULT 0 COMMENT '文件大小(字节)',
skill_content LONGTEXT COMMENT 'SKILL.md正文内容(去除frontmatter)',
storage_path VARCHAR(500) DEFAULT NULL COMMENT '对象存储相对路径前缀(如 skills/123/)',
version BIGINT DEFAULT 0 COMMENT '版本号,文件变更时自增,用于缓存一致性校验',
has_files TINYINT(1) DEFAULT 0 COMMENT '是否包含支撑文件(0=仅SKILL.md,1=有scripts/references等)',
creator_id BIGINT COMMENT '创建者用户ID',
create_dt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_dt TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4
COLLATE = utf8mb4_unicode_ci COMMENT = 'Skill技能包表';
CREATE INDEX idx_skill_creator ON sai_skill (creator_id);
-- Skill 支撑文件内容表
CREATE TABLE IF NOT EXISTS sai_skill_file
(
id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
skill_id BIGINT NOT NULL COMMENT 'Skill ID',
file_path VARCHAR(255) NOT NULL COMMENT '文件相对路径',
content LONGTEXT NOT NULL COMMENT '文件内容',
file_size INT NOT NULL COMMENT '文件大小(字节)',
encoding VARCHAR(50) DEFAULT 'utf-8' COMMENT '编码方式',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
UNIQUE KEY uk_skill_path (skill_id, file_path) COMMENT '同一Skill不能有重复的文件路径',
KEY idx_skill_id (skill_id) COMMENT '技能ID索引'
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4
COLLATE = utf8mb4_unicode_ci COMMENT = 'Skill支撑文件内容表';
-- 智能体与Skill关联表(多对多)
CREATE TABLE IF NOT EXISTS sai_agent_skill
(
id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
agent_id BIGINT NOT NULL COMMENT '智能体ID',
skill_id BIGINT NOT NULL COMMENT 'Skill ID',
create_dt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uk_agent_skill (agent_id, skill_id)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4
COLLATE = utf8mb4_unicode_ci COMMENT = '智能体Skill关联表';
CREATE INDEX idx_agent_skill_agent ON sai_agent_skill (agent_id);
CREATE INDEX idx_agent_skill_skill ON sai_agent_skill (skill_id);
-- ============================================================
CREATE TABLE IF NOT EXISTS sai_store_instance
(
id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(128) NOT NULL COMMENT '实例名称',
category TINYINT(1) NOT NULL COMMENT '分类: 1-向量库 2-搜索引擎',
type TINYINT(1) NOT NULL COMMENT '类型: 1-PG_VECTOR 2-MILVUS 3-ELASTICSEARCH 4-PG_FULLTEXT',
config TEXT DEFAULT NULL COMMENT '连接参数 JSON',
status TINYINT(1) DEFAULT 1 COMMENT '状态: 0-停用 1-启用',
is_default TINYINT(1) DEFAULT 0 COMMENT '是否为该 category 下默认实例',
create_dt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_dt TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4
COLLATE = utf8mb4_unicode_ci COMMENT = '存储实例';
CREATE INDEX idx_store_instance_category ON sai_store_instance (category);
CREATE INDEX idx_store_instance_type ON sai_store_instance (type);
-- ============================================================
-- 记忆系统(配置 / 主表 / 历史 / 摘要 / 统计 / 提取进度)
-- 依赖:sai_store_instance(sai_memory_conversation 外键)
-- ============================================================
-- 客户端应用
-- ----------------------------
CREATE TABLE IF NOT EXISTS sai_app
(
id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
app_id VARCHAR(128) NOT NULL COMMENT '应用唯一标识',
app_name VARCHAR(255) NOT NULL COMMENT '应用名称',
description VARCHAR(512) COMMENT '应用描述',
token VARCHAR(128) NOT NULL COMMENT '通信认证令牌',
route_strategy VARCHAR(32) DEFAULT 'LEAST_LOAD' COMMENT '路由策略',
status TINYINT(1) DEFAULT 1 COMMENT '1=启用, 0=停用',
create_dt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_dt TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uk_app_id (app_id)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4
COLLATE = utf8mb4_unicode_ci COMMENT = '客户端应用';
-- ----------------------------
-- AI客户端实例节点
-- ----------------------------
CREATE TABLE IF NOT EXISTS sai_client_node
(
id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
app_id VARCHAR(128) NOT NULL COMMENT '所属应用ID',
host_id VARCHAR(128) NOT NULL COMMENT '客户端实例唯一标识',
host_ip VARCHAR(64) NOT NULL COMMENT '客户端IP',
grpc_port INT NOT NULL COMMENT '客户端gRPC端口',
max_concurrent INT DEFAULT 10 COMMENT '最大并发对话数',
active_chats INT DEFAULT 0 COMMENT '当前活跃对话数',
supported_providers TEXT COMMENT '支持的模型提供商(JSON数组)',
labels TEXT COMMENT '路由标签',
expire_dt DATETIME NOT NULL COMMENT '过期时间(心跳更新)',
create_dt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_dt TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uk_client_node (app_id, host_id),
INDEX idx_app_expire (app_id, expire_dt)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4
COLLATE = utf8mb4_unicode_ci COMMENT = 'AI客户端实例节点';
-- OpenAPI 外部用户映射表
CREATE TABLE sai_openapi_user
(
id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
app_id VARCHAR(128) NOT NULL COMMENT '关联 sai_app.app_id',
open_id VARCHAR(64) NOT NULL COMMENT '平台分配的唯一标识(UUID)',
platform_user_id BIGINT NOT NULL COMMENT '关联 sai_user.id,注册时自动创建',
external_id VARCHAR(256) DEFAULT NULL COMMENT '外部系统的用户标识(可选,幂等用)',
nickname VARCHAR(128) DEFAULT NULL COMMENT '外部用户昵称',
create_dt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_dt TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uk_app_open (app_id, open_id),
UNIQUE KEY uk_app_external (app_id, external_id),
INDEX idx_open_id (open_id),
INDEX idx_platform_user (platform_user_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
COMMENT='OpenAPI 外部用户映射表';
-- ----------------------------
-- 通用资源存储
-- ----------------------------
CREATE TABLE IF NOT EXISTS sai_resource
(
id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
storage_key VARCHAR(512) NOT NULL COMMENT '存储键(相对路径或对象Key)',
original_name VARCHAR(255) NOT NULL COMMENT '原始文件名',
file_size BIGINT DEFAULT 0 COMMENT '文件大小(bytes)',
mime_type VARCHAR(128) COMMENT 'MIME类型',
storage_type VARCHAR(32) NOT NULL DEFAULT 'LOCAL' COMMENT '存储类型: LOCAL/MINIO',
access_url VARCHAR(1024) COMMENT '访问URL',
biz_type VARCHAR(64) NOT NULL DEFAULT 'GENERAL' COMMENT '业务类型: AVATAR/ATTACHMENT/DOCUMENT/GENERAL',
biz_id BIGINT COMMENT '关联业务ID',
creator_id BIGINT COMMENT '上传者ID',
create_dt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_dt TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uk_storage_key (storage_key),
INDEX idx_biz (biz_type, biz_id),
INDEX idx_creator (creator_id)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4
COLLATE = utf8mb4_unicode_ci COMMENT = '通用资源存储';