feat: Implement the initial AI-native CRM backend application with Spring Boot, LangChain4j, and JPA persistence.
This commit is contained in:
Generated
+10
@@ -0,0 +1,10 @@
|
|||||||
|
# 默认忽略的文件
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# 已忽略包含查询文件的默认文件夹
|
||||||
|
/queries/
|
||||||
|
# Datasource local storage ignored files
|
||||||
|
/dataSources/
|
||||||
|
/dataSources.local.xml
|
||||||
|
# 基于编辑器的 HTTP 客户端请求
|
||||||
|
/httpRequests/
|
||||||
Generated
+19
@@ -0,0 +1,19 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="CompilerConfiguration">
|
||||||
|
<annotationProcessing>
|
||||||
|
<profile default="true" name="Default" enabled="true" />
|
||||||
|
<profile name="Maven default annotation processors profile" enabled="true">
|
||||||
|
<sourceOutputDir name="target/generated-sources/annotations" />
|
||||||
|
<sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
|
||||||
|
<outputRelativeToContentRoot value="true" />
|
||||||
|
<module name="ai-native-backend" />
|
||||||
|
</profile>
|
||||||
|
</annotationProcessing>
|
||||||
|
</component>
|
||||||
|
<component name="JavacSettings">
|
||||||
|
<option name="ADDITIONAL_OPTIONS_OVERRIDE">
|
||||||
|
<module name="ai-native-backend" options="-parameters" />
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
Generated
+6
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="Encoding">
|
||||||
|
<file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
Generated
+20
@@ -0,0 +1,20 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="RemoteRepositoriesConfiguration">
|
||||||
|
<remote-repository>
|
||||||
|
<option name="id" value="central" />
|
||||||
|
<option name="name" value="Maven Central repository" />
|
||||||
|
<option name="url" value="https://repo1.maven.org/maven2" />
|
||||||
|
</remote-repository>
|
||||||
|
<remote-repository>
|
||||||
|
<option name="id" value="jboss.community" />
|
||||||
|
<option name="name" value="JBoss Community repository" />
|
||||||
|
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
|
||||||
|
</remote-repository>
|
||||||
|
<remote-repository>
|
||||||
|
<option name="id" value="central" />
|
||||||
|
<option name="name" value="Central Repository" />
|
||||||
|
<option name="url" value="https://maven.aliyun.com/repository/public" />
|
||||||
|
</remote-repository>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
Generated
+12
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||||
|
<component name="MavenProjectsManager">
|
||||||
|
<option name="originalFiles">
|
||||||
|
<list>
|
||||||
|
<option value="$PROJECT_DIR$/pom.xml" />
|
||||||
|
</list>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="corretto-21" project-jdk-type="JavaSDK" />
|
||||||
|
</project>
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
<parent>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-parent</artifactId>
|
||||||
|
<version>3.2.4</version> <!-- using stable 3.2.x version -->
|
||||||
|
<relativePath/> <!-- lookup parent from repository -->
|
||||||
|
</parent>
|
||||||
|
<groupId>com.example</groupId>
|
||||||
|
<artifactId>ai-native-backend</artifactId>
|
||||||
|
<version>0.0.1-SNAPSHOT</version>
|
||||||
|
<name>ai-native-backend</name>
|
||||||
|
<description>AI-Native CRM Backend</description>
|
||||||
|
<properties>
|
||||||
|
<java.version>21</java.version>
|
||||||
|
<langchain4j.version>0.30.0</langchain4j.version>
|
||||||
|
</properties>
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-web</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.postgresql</groupId>
|
||||||
|
<artifactId>postgresql</artifactId>
|
||||||
|
<scope>runtime</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok</artifactId>
|
||||||
|
<optional>true</optional>
|
||||||
|
</dependency>
|
||||||
|
<!-- LangChain4j for AI communication -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>dev.langchain4j</groupId>
|
||||||
|
<artifactId>langchain4j-spring-boot-starter</artifactId>
|
||||||
|
<version>${langchain4j.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>dev.langchain4j</groupId>
|
||||||
|
<artifactId>langchain4j-open-ai-spring-boot-starter</artifactId>
|
||||||
|
<version>${langchain4j.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<!-- JSON type mapping for Hibernate -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.hypersistence</groupId>
|
||||||
|
<artifactId>hypersistence-utils-hibernate-63</artifactId>
|
||||||
|
<version>3.7.3</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-test</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||||
|
<configuration>
|
||||||
|
<excludes>
|
||||||
|
<exclude>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok</artifactId>
|
||||||
|
</exclude>
|
||||||
|
</excludes>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
</project>
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package com.example.ainative;
|
||||||
|
|
||||||
|
import org.springframework.boot.SpringApplication;
|
||||||
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
|
||||||
|
@SpringBootApplication
|
||||||
|
public class AiNativeApplication {
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
SpringApplication.run(AiNativeApplication.class, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.example.ainative.ai;
|
||||||
|
|
||||||
|
import dev.langchain4j.service.MemoryId;
|
||||||
|
import dev.langchain4j.service.SystemMessage;
|
||||||
|
import dev.langchain4j.service.UserMessage;
|
||||||
|
import dev.langchain4j.service.V;
|
||||||
|
import dev.langchain4j.service.spring.AiService;
|
||||||
|
|
||||||
|
@AiService
|
||||||
|
public interface CrmAgent {
|
||||||
|
|
||||||
|
@SystemMessage("""
|
||||||
|
You are a smart AI assistant embedded within a modern enterprise CRM system.
|
||||||
|
Your goal is to help the user configure business modules, dynamic forms, and insert data.
|
||||||
|
You have tools to define database schema structures (JSON Schema) and manipulate dynamic data.
|
||||||
|
Always respond concisely. If you need to manipulate the UI, explain what you are doing.
|
||||||
|
|
||||||
|
Current User/Tenant context ID: {{userContext}}
|
||||||
|
""")
|
||||||
|
String chat(@MemoryId String memoryId, @UserMessage String userMessage, @V("userContext") String userContext);
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package com.example.ainative.ai;
|
||||||
|
|
||||||
|
import com.example.ainative.entity.SysEntity;
|
||||||
|
import com.example.ainative.repository.SysEntityRepository;
|
||||||
|
import dev.langchain4j.agent.tool.Tool;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class CrmTools {
|
||||||
|
|
||||||
|
private final SysEntityRepository sysEntityRepository;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI 工具:用于定义新的动态业务对象(Entity)。
|
||||||
|
* 当用户试图创建新表单、新模块(比如“线索管理”、“合同管理”)时,大模型会自动决定调用本工具,
|
||||||
|
* 并将它根据系统指令生成的参数传入进来。
|
||||||
|
*
|
||||||
|
* @param entityCode 模块唯一标识符,英文,比如 crm_lead (便于后期通过接口精确操作某模块)
|
||||||
|
* @param entityName 模块中文名称跨,比如 销售线索
|
||||||
|
* @param jsonSchema 这里是大模型输出的最核心数据:一个合规的 JSON Schema (如属性定义、校验规则),由前端随后解析渲染
|
||||||
|
* @return 返回一段让 AI 知道是否成功的指令结果,这个结果大模型能看到并进一步回答用户。
|
||||||
|
*/
|
||||||
|
@Tool("创建一个新的业务表单模块,当你确定了用户创建新功能的意图后调用。参数包括:entityCode (英文唯一标识), entityName (中文名称), jsonSchema。")
|
||||||
|
public String defineDynamicModule(String entityCode, String entityName, String jsonSchema) {
|
||||||
|
log.info("Agent 正在调用工具执行创建新模块: Code={}, Name={}", entityCode, entityName);
|
||||||
|
|
||||||
|
// 1. 业务校验:防重和容错
|
||||||
|
SysEntity existing = sysEntityRepository.findByEntityCode(entityCode);
|
||||||
|
if (existing != null) {
|
||||||
|
return "模块代码 [" + entityCode + "] 已存在,创建失败,请让用户考虑使用其他代码名称。";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 实体赋值与入库
|
||||||
|
try {
|
||||||
|
SysEntity entity = new SysEntity();
|
||||||
|
entity.setEntityCode(entityCode);
|
||||||
|
entity.setEntityName(entityName);
|
||||||
|
// 此处直接存储大模型生成的 JSON 字符串(PostgreSQL 的 Hibernate 驱动会将其封装为 JSONB)
|
||||||
|
entity.setSchemaDefinition(jsonSchema);
|
||||||
|
|
||||||
|
SysEntity saved = sysEntityRepository.save(entity);
|
||||||
|
return "核心模块 [" + entityName + "] 已成功持久化并部署到系统中。后台对应的 Entity ID (UUID) 为: " + saved.getId();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("保存 Schema 时出现异常", e);
|
||||||
|
return "数据库保存异常,请检查 JSON 结构是否合规。";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package com.example.ainative.ai;
|
||||||
|
|
||||||
|
import com.example.ainative.entity.ChatMemoryEntity;
|
||||||
|
import com.example.ainative.repository.ChatMemoryRepository;
|
||||||
|
import dev.langchain4j.data.message.ChatMessage;
|
||||||
|
import dev.langchain4j.data.message.ChatMessageDeserializer;
|
||||||
|
import dev.langchain4j.data.message.ChatMessageSerializer;
|
||||||
|
import dev.langchain4j.store.memory.chat.ChatMemoryStore;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class PersistentChatMemoryStore implements ChatMemoryStore {
|
||||||
|
|
||||||
|
private final ChatMemoryRepository repository;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<ChatMessage> getMessages(Object memoryId) {
|
||||||
|
String sessionId = (String) memoryId;
|
||||||
|
List<ChatMemoryEntity> entities = repository.findByChatSessionIdOrderByIdAsc(sessionId);
|
||||||
|
return entities.stream()
|
||||||
|
.map(entity -> ChatMessageDeserializer.messageFromJson(entity.getContent()))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public void updateMessages(Object memoryId, List<ChatMessage> messages) {
|
||||||
|
String sessionId = (String) memoryId;
|
||||||
|
repository.deleteByChatSessionId(sessionId);
|
||||||
|
|
||||||
|
List<ChatMemoryEntity> entities = messages.stream().map(message -> {
|
||||||
|
ChatMemoryEntity entity = new ChatMemoryEntity();
|
||||||
|
entity.setChatSessionId(sessionId);
|
||||||
|
entity.setContent(ChatMessageSerializer.messageToJson(message));
|
||||||
|
return entity;
|
||||||
|
}).collect(Collectors.toList());
|
||||||
|
|
||||||
|
repository.saveAll(entities);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public void deleteMessages(Object memoryId) {
|
||||||
|
repository.deleteByChatSessionId((String) memoryId);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.example.ainative.config;
|
||||||
|
|
||||||
|
import dev.langchain4j.memory.chat.ChatMemoryProvider;
|
||||||
|
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
|
||||||
|
import dev.langchain4j.store.memory.chat.ChatMemoryStore;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
public class AiConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public ChatMemoryProvider chatMemoryProvider(ChatMemoryStore chatMemoryStore) {
|
||||||
|
return memoryId -> MessageWindowChatMemory.builder()
|
||||||
|
.id(memoryId)
|
||||||
|
.maxMessages(50) // Keep last 50 messages in context
|
||||||
|
.chatMemoryStore(chatMemoryStore)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package com.example.ainative.config;
|
||||||
|
|
||||||
|
public class UserContextHolder {
|
||||||
|
private static final ThreadLocal<String> currentUser = new ThreadLocal<>();
|
||||||
|
|
||||||
|
public static void setCurrentUser(String userId) {
|
||||||
|
currentUser.set(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getCurrentUser() {
|
||||||
|
return currentUser.get() != null ? currentUser.get() : "anonymous";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void clear() {
|
||||||
|
currentUser.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package com.example.ainative.config;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.web.servlet.HandlerInterceptor;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class UserInterceptor implements HandlerInterceptor {
|
||||||
|
@Override
|
||||||
|
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
|
||||||
|
String userId = request.getHeader("X-Creator-Id");
|
||||||
|
if (userId != null && !userId.isBlank()) {
|
||||||
|
UserContextHolder.setCurrentUser(userId);
|
||||||
|
} else {
|
||||||
|
UserContextHolder.setCurrentUser("anonymous");
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
|
||||||
|
UserContextHolder.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package com.example.ainative.config;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||||
|
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class WebMvcConfig implements WebMvcConfigurer {
|
||||||
|
|
||||||
|
private final UserInterceptor userInterceptor;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addInterceptors(InterceptorRegistry registry) {
|
||||||
|
registry.addInterceptor(userInterceptor).addPathPatterns("/**");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package com.example.ainative.controller;
|
||||||
|
|
||||||
|
import com.example.ainative.ai.CrmAgent;
|
||||||
|
import com.example.ainative.config.UserContextHolder;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/agent")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class AgentController {
|
||||||
|
|
||||||
|
private final CrmAgent crmAgent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 对话交互核心入口
|
||||||
|
* 这里提供了一个传统的 REST 包装版本。当前端(如 CopilotKit 或测试客户端)发送自然语言时,会调用此处。
|
||||||
|
*
|
||||||
|
* @param sessionId 唯一会话识别(与底层的 ChatMemory 持久化紧密对应,确保 AI 能“记住”对话)
|
||||||
|
* @param message 用户的原始需求 (比如:"帮我建一个客户关系系统的线索录入表,有姓名和手机号")
|
||||||
|
* @return AI 所思所想或执行完 @Tool 后的结果字符串反馈
|
||||||
|
*/
|
||||||
|
@PostMapping("/chat")
|
||||||
|
public String chat(@RequestParam String sessionId, @RequestBody String message) {
|
||||||
|
// 先从拦截器上下文中拿到刚才设置的调用者身份
|
||||||
|
String currentUser = UserContextHolder.getCurrentUser();
|
||||||
|
|
||||||
|
// 传递给底层的 Agent:这背后的复杂工作流完全交由 Langchain4J 托管!
|
||||||
|
// 如果用户的 message 意图命中“建表”,框架会在返回前自己拦截调去跑 CrmTools 里的 defineDynamicModule。
|
||||||
|
return crmAgent.chat(sessionId, message, currentUser);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
package com.example.ainative.controller;
|
||||||
|
|
||||||
|
import com.example.ainative.config.UserContextHolder;
|
||||||
|
import com.example.ainative.entity.SysDynamicData;
|
||||||
|
import com.example.ainative.entity.SysEntity;
|
||||||
|
import com.example.ainative.repository.SysDynamicDataRepository;
|
||||||
|
import com.example.ainative.repository.SysEntityRepository;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 动态数据交互控制器
|
||||||
|
* 负责接收 AI 生成的 UI 提交过来的所有结构不固定的 JSON 数据。
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/dynamic")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@CrossOrigin(origins = "*") // 允许前端 Next.js (通常是 3000 端口) 进行跨域调用
|
||||||
|
public class DynamicDataController {
|
||||||
|
|
||||||
|
private final SysDynamicDataRepository dataRepository;
|
||||||
|
private final SysEntityRepository entityRepository;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据模块 UUID 获取数据列表
|
||||||
|
*/
|
||||||
|
@GetMapping("/{entityId}")
|
||||||
|
public List<SysDynamicData> getRecords(@PathVariable UUID entityId) {
|
||||||
|
return dataRepository.findByEntityId(entityId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据模块 code 获取数据列表(前端动态表格主要用此接口方便查询)
|
||||||
|
*/
|
||||||
|
@GetMapping("/code/{entityCode}")
|
||||||
|
public List<SysDynamicData> getRecordsByCode(@PathVariable String entityCode) {
|
||||||
|
SysEntity entity = entityRepository.findByEntityCode(entityCode);
|
||||||
|
if (entity != null) {
|
||||||
|
return dataRepository.findByEntityId(entity.getId());
|
||||||
|
}
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自动注册/获取动态模块接口:解决前端渲染表单和后端实体记录未创建的时序问题
|
||||||
|
* 允许前端在提交实际数据前,主动将推断出的模块结构注册到数据库。
|
||||||
|
*/
|
||||||
|
@PostMapping("/schema")
|
||||||
|
public SysEntity defineSchema(@RequestBody SysEntity requestedEntity) {
|
||||||
|
SysEntity entity = entityRepository.findByEntityCode(requestedEntity.getEntityCode());
|
||||||
|
if (entity == null) {
|
||||||
|
entity = new SysEntity();
|
||||||
|
entity.setEntityCode(requestedEntity.getEntityCode());
|
||||||
|
entity.setEntityName(requestedEntity.getEntityName());
|
||||||
|
entity.setSchemaDefinition(requestedEntity.getSchemaDefinition());
|
||||||
|
return entityRepository.save(entity);
|
||||||
|
}
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 核心接口:按 entityCode 直接入库
|
||||||
|
* 前端不需要知道复杂的 UUID,只需根据 AI 生成的 crm_lead 等 Code 即可提交。
|
||||||
|
*/
|
||||||
|
@PostMapping("/code/{entityCode}")
|
||||||
|
public SysDynamicData createRecordByCode(@PathVariable String entityCode, @RequestBody String jsonData) {
|
||||||
|
// 1. 先通过 Code 找到模块定义的 ID
|
||||||
|
SysEntity entity = entityRepository.findByEntityCode(entityCode);
|
||||||
|
if (entity == null) {
|
||||||
|
throw new RuntimeException("模块 " + entityCode + " 尚未定义,无法入库。请先让 AI 创建该模块。");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 构造动态数据记录
|
||||||
|
SysDynamicData record = new SysDynamicData();
|
||||||
|
record.setEntityId(entity.getId());
|
||||||
|
record.setData(jsonData); // 这里的 String 在 JSONB 映射下会自动存为二进制 JSON 格式
|
||||||
|
record.setCreatorId(UserContextHolder.getCurrentUser());
|
||||||
|
|
||||||
|
return dataRepository.save(record);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 原始的 UUID 入库接口 (保留备用)
|
||||||
|
*/
|
||||||
|
@PostMapping("/{entityId}")
|
||||||
|
public SysDynamicData createRecord(@PathVariable UUID entityId, @RequestBody String jsonData) {
|
||||||
|
SysDynamicData record = new SysDynamicData();
|
||||||
|
record.setEntityId(entityId);
|
||||||
|
record.setData(jsonData);
|
||||||
|
record.setCreatorId(UserContextHolder.getCurrentUser());
|
||||||
|
|
||||||
|
return dataRepository.save(record);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修改现有记录
|
||||||
|
*/
|
||||||
|
@PutMapping("/{id}")
|
||||||
|
public SysDynamicData updateRecord(@PathVariable UUID id, @RequestBody String jsonData) {
|
||||||
|
return dataRepository.findById(id).map(record -> {
|
||||||
|
record.setData(jsonData);
|
||||||
|
return dataRepository.save(record);
|
||||||
|
}).orElseThrow(() -> new RuntimeException("Record not found"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除现有记录
|
||||||
|
*/
|
||||||
|
@DeleteMapping("/{id}")
|
||||||
|
public void deleteRecord(@PathVariable UUID id) {
|
||||||
|
dataRepository.deleteById(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package com.example.ainative.controller;
|
||||||
|
|
||||||
|
import com.example.ainative.entity.SysEntity;
|
||||||
|
import com.example.ainative.repository.SysEntityRepository;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/entities")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@CrossOrigin(origins = "*") // Allow frontend to fetch menus
|
||||||
|
public class SysEntityController {
|
||||||
|
|
||||||
|
private final SysEntityRepository sysEntityRepository;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有已定义的模块列表,供左侧菜单动态渲染使用。
|
||||||
|
*/
|
||||||
|
@GetMapping
|
||||||
|
public List<SysEntity> getAllEntities() {
|
||||||
|
return sysEntityRepository.findAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据 code 获取单一实体定义,方便前台表格按需渲染列等动态行为。
|
||||||
|
*/
|
||||||
|
@GetMapping("/code/{entityCode}")
|
||||||
|
public SysEntity getEntityByCode(@PathVariable String entityCode) {
|
||||||
|
return sysEntityRepository.findByEntityCode(entityCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package com.example.ainative.entity;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import lombok.Data;
|
||||||
|
import org.hibernate.annotations.CreationTimestamp;
|
||||||
|
|
||||||
|
import java.time.ZonedDateTime;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Entity
|
||||||
|
@Table(name = "chat_memory")
|
||||||
|
public class ChatMemoryEntity {
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Column(name = "chat_session_id", nullable = false)
|
||||||
|
private String chatSessionId;
|
||||||
|
|
||||||
|
@Column(name = "content", nullable = false, columnDefinition = "TEXT")
|
||||||
|
private String content;
|
||||||
|
|
||||||
|
@CreationTimestamp
|
||||||
|
@Column(name = "created_at", updatable = false)
|
||||||
|
private ZonedDateTime createdAt;
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package com.example.ainative.entity;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import lombok.Data;
|
||||||
|
import org.hibernate.annotations.CreationTimestamp;
|
||||||
|
import org.hibernate.annotations.JdbcTypeCode;
|
||||||
|
import org.hibernate.type.SqlTypes;
|
||||||
|
|
||||||
|
import java.time.ZonedDateTime;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Entity
|
||||||
|
@Table(name = "sys_dynamic_data")
|
||||||
|
public class SysDynamicData {
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.UUID)
|
||||||
|
private UUID id;
|
||||||
|
|
||||||
|
@Column(name = "entity_id")
|
||||||
|
private UUID entityId;
|
||||||
|
|
||||||
|
@JdbcTypeCode(SqlTypes.JSON)
|
||||||
|
@Column(name = "data", columnDefinition = "jsonb", nullable = false)
|
||||||
|
private String data;
|
||||||
|
|
||||||
|
@Column(name = "creator_id", length = 50)
|
||||||
|
private String creatorId;
|
||||||
|
|
||||||
|
@CreationTimestamp
|
||||||
|
@Column(name = "created_at", updatable = false)
|
||||||
|
private ZonedDateTime createdAt;
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package com.example.ainative.entity;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import lombok.Data;
|
||||||
|
import org.hibernate.annotations.CreationTimestamp;
|
||||||
|
import org.hibernate.annotations.JdbcTypeCode;
|
||||||
|
import org.hibernate.annotations.UpdateTimestamp;
|
||||||
|
import org.hibernate.type.SqlTypes;
|
||||||
|
|
||||||
|
import java.time.ZonedDateTime;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Entity
|
||||||
|
@Table(name = "sys_entities")
|
||||||
|
public class SysEntity {
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.UUID)
|
||||||
|
private UUID id;
|
||||||
|
|
||||||
|
@Column(name = "entity_code", unique = true, nullable = false, length = 50)
|
||||||
|
private String entityCode;
|
||||||
|
|
||||||
|
@Column(name = "entity_name", nullable = false, length = 100)
|
||||||
|
private String entityName;
|
||||||
|
|
||||||
|
@JdbcTypeCode(SqlTypes.JSON)
|
||||||
|
@Column(name = "schema_definition", columnDefinition = "jsonb", nullable = false)
|
||||||
|
private String schemaDefinition;
|
||||||
|
|
||||||
|
@CreationTimestamp
|
||||||
|
@Column(name = "created_at", updatable = false)
|
||||||
|
private ZonedDateTime createdAt;
|
||||||
|
|
||||||
|
@UpdateTimestamp
|
||||||
|
@Column(name = "updated_at")
|
||||||
|
private ZonedDateTime updatedAt;
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.example.ainative.repository;
|
||||||
|
|
||||||
|
import com.example.ainative.entity.ChatMemoryEntity;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface ChatMemoryRepository extends JpaRepository<ChatMemoryEntity, Long> {
|
||||||
|
List<ChatMemoryEntity> findByChatSessionIdOrderByIdAsc(String chatSessionId);
|
||||||
|
void deleteByChatSessionId(String chatSessionId);
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.example.ainative.repository;
|
||||||
|
|
||||||
|
import com.example.ainative.entity.SysDynamicData;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public interface SysDynamicDataRepository extends JpaRepository<SysDynamicData, UUID> {
|
||||||
|
List<SysDynamicData> findByEntityId(UUID entityId);
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package com.example.ainative.repository;
|
||||||
|
|
||||||
|
import com.example.ainative.entity.SysEntity;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public interface SysEntityRepository extends JpaRepository<SysEntity, UUID> {
|
||||||
|
SysEntity findByEntityCode(String entityCode);
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
spring:
|
||||||
|
application:
|
||||||
|
name: ai-native-backend
|
||||||
|
datasource:
|
||||||
|
url: jdbc:postgresql://localhost:5432/ai_native_db
|
||||||
|
username: ai_agent_user
|
||||||
|
password: Agent@2026
|
||||||
|
driver-class-name: org.postgresql.Driver
|
||||||
|
jpa:
|
||||||
|
hibernate:
|
||||||
|
ddl-auto: none
|
||||||
|
show-sql: true
|
||||||
|
properties:
|
||||||
|
hibernate:
|
||||||
|
format_sql: true
|
||||||
|
dialect: org.hibernate.dialect.PostgreSQLDialect
|
||||||
|
|
||||||
|
langchain4j:
|
||||||
|
open-ai:
|
||||||
|
chat-model:
|
||||||
|
api-key: ${ZHIPU_API_KEY:61833436cbd642ed844d0128a99b2e89.PTbpERpysO3qSf8w}
|
||||||
|
base-url: https://open.bigmodel.cn/api/paas/v4/
|
||||||
|
model-name: glm-4-plus
|
||||||
|
log-requests: true
|
||||||
|
log-responses: true
|
||||||
|
|
||||||
|
server:
|
||||||
|
port: 8080
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
spring:
|
||||||
|
application:
|
||||||
|
name: ai-native-backend
|
||||||
|
datasource:
|
||||||
|
url: jdbc:postgresql://localhost:5432/ai_native_db
|
||||||
|
username: ai_agent_user
|
||||||
|
password: Agent@2026
|
||||||
|
driver-class-name: org.postgresql.Driver
|
||||||
|
jpa:
|
||||||
|
hibernate:
|
||||||
|
ddl-auto: none
|
||||||
|
show-sql: true
|
||||||
|
properties:
|
||||||
|
hibernate:
|
||||||
|
format_sql: true
|
||||||
|
dialect: org.hibernate.dialect.PostgreSQLDialect
|
||||||
|
|
||||||
|
langchain4j:
|
||||||
|
open-ai:
|
||||||
|
chat-model:
|
||||||
|
api-key: ${ZHIPU_API_KEY:61833436cbd642ed844d0128a99b2e89.PTbpERpysO3qSf8w}
|
||||||
|
base-url: https://open.bigmodel.cn/api/paas/v4/
|
||||||
|
model-name: glm-4-plus
|
||||||
|
log-requests: true
|
||||||
|
log-responses: true
|
||||||
|
|
||||||
|
server:
|
||||||
|
port: 8080
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
/Users/hui/project/ai/backend/src/main/java/com/example/ainative/entity/SysDynamicData.java
|
||||||
|
/Users/hui/project/ai/backend/src/main/java/com/example/ainative/controller/AgentController.java
|
||||||
|
/Users/hui/project/ai/backend/src/main/java/com/example/ainative/config/UserContextHolder.java
|
||||||
|
/Users/hui/project/ai/backend/src/main/java/com/example/ainative/controller/DynamicDataController.java
|
||||||
|
/Users/hui/project/ai/backend/src/main/java/com/example/ainative/ai/PersistentChatMemoryStore.java
|
||||||
|
/Users/hui/project/ai/backend/src/main/java/com/example/ainative/entity/SysEntity.java
|
||||||
|
/Users/hui/project/ai/backend/src/main/java/com/example/ainative/ai/CrmTools.java
|
||||||
|
/Users/hui/project/ai/backend/src/main/java/com/example/ainative/entity/ChatMemoryEntity.java
|
||||||
|
/Users/hui/project/ai/backend/src/main/java/com/example/ainative/controller/SysEntityController.java
|
||||||
|
/Users/hui/project/ai/backend/src/main/java/com/example/ainative/AiNativeApplication.java
|
||||||
|
/Users/hui/project/ai/backend/src/main/java/com/example/ainative/config/WebMvcConfig.java
|
||||||
|
/Users/hui/project/ai/backend/src/main/java/com/example/ainative/repository/SysDynamicDataRepository.java
|
||||||
|
/Users/hui/project/ai/backend/src/main/java/com/example/ainative/config/UserInterceptor.java
|
||||||
|
/Users/hui/project/ai/backend/src/main/java/com/example/ainative/repository/ChatMemoryRepository.java
|
||||||
|
/Users/hui/project/ai/backend/src/main/java/com/example/ainative/repository/SysEntityRepository.java
|
||||||
|
/Users/hui/project/ai/backend/src/main/java/com/example/ainative/ai/CrmAgent.java
|
||||||
|
/Users/hui/project/ai/backend/src/main/java/com/example/ainative/config/AiConfig.java
|
||||||
Reference in New Issue
Block a user