feat: Implement initial AI-native CRM backend with dynamic entity and data management, AI agent tools, and core configurations.
This commit is contained in:
+19
@@ -0,0 +1,19 @@
|
|||||||
|
# Maven
|
||||||
|
target/
|
||||||
|
pom.xml.tag
|
||||||
|
pom.xml.releaseBackup
|
||||||
|
pom.xml.versionsBackup
|
||||||
|
pom.xml.restore
|
||||||
|
release.properties
|
||||||
|
dependency-reduced-pom.xml
|
||||||
|
buildNumber.properties
|
||||||
|
.mvn/timing.properties
|
||||||
|
# https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||||
|
.idea/
|
||||||
|
*.iml
|
||||||
|
*.iws
|
||||||
|
.idea_modules/
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
Generated
+1
-1
@@ -8,5 +8,5 @@
|
|||||||
</list>
|
</list>
|
||||||
</option>
|
</option>
|
||||||
</component>
|
</component>
|
||||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="corretto-21" project-jdk-type="JavaSDK" />
|
<component name="ProjectRootManager" version="2" languageLevel="JDK_19" default="true" project-jdk-name="19" project-jdk-type="JavaSDK" />
|
||||||
</project>
|
</project>
|
||||||
Generated
+6
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
# AI-Native CRM: Agent Handoff Guide
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
> This document is for the AI Agent to quickly understand the project context, architecture, and current status during session resume.
|
||||||
|
|
||||||
|
## 🎯 Project Vision
|
||||||
|
An **AI-Native CRM** where business modules and data are managed through AI-driven messages and tools, rather than traditional REST parameters.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🏗️ Architecture Overview
|
||||||
|
|
||||||
|
### Backend (`/backend`)
|
||||||
|
* **Env**: JDK 19 + Spring Boot 3.2.4 + PostgreSQL.
|
||||||
|
* **Key Logic**:
|
||||||
|
* `CrmTools.java`: **The Message Hub**. Contains `@Tool` methods for **Define, Save, and Delete**. Directly interacts with Repositories.
|
||||||
|
* `AgentController.java`: **Primary Entry Point**. Handles natural language commands or "Business Fact Reports".
|
||||||
|
* **AI-Only Policy**: Create/Update/Delete endpoints have been removed from traditional controllers to force AI-driven flows.
|
||||||
|
|
||||||
|
### Frontend (`/frontend`)
|
||||||
|
* **Env**: Next.js 15 (App Router).
|
||||||
|
* **Components**:
|
||||||
|
* `DynamicForm.tsx`: Shared component supporting **Entity Associations (`x-link-entity`)**, Enums, and Format constraints.
|
||||||
|
* `EntityDataPage` (`/data/[entityCode]`): Full CRUD UI. Actions (Edit/Delete) route through AI messages.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠️ Key File Responsibilities
|
||||||
|
|
||||||
|
| File | Role |
|
||||||
|
| :--- | :--- |
|
||||||
|
| `backend/.../CrmTools.java` | **Source of Truth** for all data-changing tools. |
|
||||||
|
| `frontend/src/components/DynamicForm.tsx` | Reusable Form Renderer (Schema + AI messaging). |
|
||||||
|
| `frontend/src/app/data/[entityCode]/page.tsx` | Dynamic List View with Agent-driven Actions. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 Core Conventions
|
||||||
|
|
||||||
|
1. **AI-First Mutation**: DO NOT create manual REST services for data mutation. Use the AI Tool interface.
|
||||||
|
2. **Schema Extensions**:
|
||||||
|
* Associations: `x-link-entity: 'module_code'`.
|
||||||
|
* Constraints: `enum`, `pattern`, `format: 'date'`.
|
||||||
|
3. **Fact-Based Messaging**: Frontend should report business facts (e.g., "User filled form X") and let the AI decide the tool to call.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Current Progress & Next Steps
|
||||||
|
- [x] AI-Native CRUD implemented (Agent-driven Insert/Update/Delete).
|
||||||
|
- [x] Entity Associations & Selectors supported.
|
||||||
|
- [x] Unified DynamicForm for Generation & Management.
|
||||||
|
- [ ] Next: Implement Advanced Search & Filtering via AI Query generation.
|
||||||
|
- [ ] Next: Dashboard Analytics module for dynamic data summary.
|
||||||
|
|
||||||
|
---
|
||||||
|
*Updated at: 2026-03-25. Follow this guide for AI-Native CRM development.*
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
# AI-Native CRM 系统开发手册
|
||||||
|
|
||||||
|
## 🎯 系统定位 (Core Vision)
|
||||||
|
本系统是一款 **AI-Native (原生 AI)** 的声明式 CRM。与传统 CRM 的本质区别在于:
|
||||||
|
* **非硬编码业务逻辑**:业务模块、字段定义和 UI 呈现均由 AI 动态生成并持久化。
|
||||||
|
* **零研发排期**:业务专家通过自然语言描述需求,即可立刻生成生产可用的功能模块。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 核心特性 (Key Features)
|
||||||
|
|
||||||
|
### 1. 生成式 UI (Generative UI)
|
||||||
|
* 集成 **CopilotKit**,在前端实现 AI 对应用的深度操控。
|
||||||
|
* 支持通过自然语言一键渲染(Render)基于 JSON Schema 的复杂表单。
|
||||||
|
* **UniversalModuleRenderer**:万能组件渲染引擎,适配多种动态业务场景。
|
||||||
|
|
||||||
|
### 2. 动态多租户架构
|
||||||
|
* 后端使用 `SysEntity` (元数据) 和 `SysDynamicData` (实例数据) 支撑无限扩展。
|
||||||
|
* 支持不同租户、不同模块的字段在运行时动态注入,无需繁琐的数据库迁移动作。
|
||||||
|
|
||||||
|
### 3. AI 交互中枢 (AI Hub)
|
||||||
|
* **后端驱动**:基于 **LangChain4j** 构建 Java 原生 AI 逻辑,支持 RAG 及复杂工作流。
|
||||||
|
* **会话持久化**:原生支持 AI 会话的数据库存储,重启不丢失交互上下文。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 技术基准 (Benchmarks & Tech Stack)
|
||||||
|
|
||||||
|
### Backend (Java Ecosystem)
|
||||||
|
* **Runner**: OpenJDK 19
|
||||||
|
* **Framework**: Spring Boot 3.2.4 (Latest Stable)
|
||||||
|
* **Persistence**: Spring Data JPA + Hibernate 6.x
|
||||||
|
* **Database**: PostgreSQL (Support JSONB for Dynamic Data)
|
||||||
|
* **AI SDK**: LangChain4j 0.30.0
|
||||||
|
* **Build Tool**: Maven
|
||||||
|
|
||||||
|
### Frontend (Modern Web)
|
||||||
|
* **Framework**: Next.js (App Router)
|
||||||
|
* **Language**: TypeScript
|
||||||
|
* **Styling**: Tailwind CSS + Shadcn UI (Visual Esthetics)
|
||||||
|
* **Copilot**: CopilotKit (React SDK + Popup UI)
|
||||||
|
* **Font**: Geist (Advanced Typography)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠 开发规范
|
||||||
|
1. **动态性优先**:任何新功能应考虑是否可通过配置或 AI 生成,而非硬编码 Controller。
|
||||||
|
2. **Schema 为王**:业务定义必须严格遵循 JSON Schema 标准,以保证前后端渲染的一致性。
|
||||||
|
3. **Prompt 工程**:系统 Prompt 存储于前端 `page.tsx` 或后端专用 AI 服务类中,需定期维护。
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-parent</artifactId>
|
<artifactId>spring-boot-starter-parent</artifactId>
|
||||||
<version>3.2.4</version> <!-- using stable 3.2.x version -->
|
<version>3.5.12</version> <!-- using stable 3.2.x version -->
|
||||||
<relativePath/> <!-- lookup parent from repository -->
|
<relativePath/> <!-- lookup parent from repository -->
|
||||||
</parent>
|
</parent>
|
||||||
<groupId>com.example</groupId>
|
<groupId>com.example</groupId>
|
||||||
@@ -14,8 +14,8 @@
|
|||||||
<name>ai-native-backend</name>
|
<name>ai-native-backend</name>
|
||||||
<description>AI-Native CRM Backend</description>
|
<description>AI-Native CRM Backend</description>
|
||||||
<properties>
|
<properties>
|
||||||
<java.version>21</java.version>
|
<java.version>19</java.version>
|
||||||
<langchain4j.version>0.30.0</langchain4j.version>
|
<langchain4j.version>1.12.2-beta22</langchain4j.version>
|
||||||
</properties>
|
</properties>
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<dependency>
|
<dependency>
|
||||||
|
|||||||
@@ -6,16 +6,33 @@ import dev.langchain4j.service.UserMessage;
|
|||||||
import dev.langchain4j.service.V;
|
import dev.langchain4j.service.V;
|
||||||
import dev.langchain4j.service.spring.AiService;
|
import dev.langchain4j.service.spring.AiService;
|
||||||
|
|
||||||
@AiService
|
@AiService(tools = "crmTools")
|
||||||
public interface CrmAgent {
|
public interface CrmAgent {
|
||||||
|
|
||||||
@SystemMessage("""
|
@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.
|
你是一个 AI 原生 CRM 系统的核心引擎。你的任务是通过自然语言管理业务模块(Schema)和动态数据,替代传统的 REST 接口。
|
||||||
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}}
|
## 核心运行准则(严禁违反)
|
||||||
|
1. **执行重于对话**:系统已禁用传统的增删改接口,所有数据变更必须通过调用工具实现。
|
||||||
|
2. **禁止虚假模拟**:严禁在未成功调用工具的情况下,回复任何暗示操作已完成的内容(如“已保存”、“已创建”)。
|
||||||
|
3. **事实驱动**:前端会报告“业务事实”(如:用户提交了表单内容),你必须根据事实判断该调用哪个工具(如:insertDynamicData)。
|
||||||
|
|
||||||
|
## 建模协议 (defineDynamicModule)
|
||||||
|
当定义或更新模块结构时,生成的 JSON Schema 需遵循:
|
||||||
|
- **实体关联**:若字段需关联其他实体,必须在属性中加入 `"x-link-entity": "目标实体Code"`。
|
||||||
|
- **约束增强**:优先使用 `enum` (下拉选项)、`pattern` (正则校验)、`format: "date"` 等标准 JSON Schema 约束。
|
||||||
|
- **命名规范**:entityCode 采用下划线命名法(如:student_course)。
|
||||||
|
|
||||||
|
## 数据操作协议
|
||||||
|
- **入库 (insert)**:将用户提供的或事实报告中的信息提取为 JSON 字符串。
|
||||||
|
- **物理删除 (delete)**:仅在用户明确指令或业务流程需要时执行。
|
||||||
|
|
||||||
|
## 输出风格
|
||||||
|
- 保持简洁。
|
||||||
|
- 必须在工具执行成功后,再简要反馈执行结果或后续 UI 操作建议。
|
||||||
|
|
||||||
|
当前租户上下文 ID: {{userContext}}
|
||||||
""")
|
""")
|
||||||
String chat(@MemoryId String memoryId, @UserMessage String userMessage, @V("userContext") String userContext);
|
String chat(@MemoryId String memoryId, @UserMessage String userMessage, @V("userContext") String userContext);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,54 +1,99 @@
|
|||||||
package com.example.ainative.ai;
|
package com.example.ainative.ai;
|
||||||
|
|
||||||
|
import com.example.ainative.config.UserContextHolder;
|
||||||
|
|
||||||
|
import com.example.ainative.entity.SysDynamicData;
|
||||||
import com.example.ainative.entity.SysEntity;
|
import com.example.ainative.entity.SysEntity;
|
||||||
|
import com.example.ainative.repository.SysDynamicDataRepository;
|
||||||
import com.example.ainative.repository.SysEntityRepository;
|
import com.example.ainative.repository.SysEntityRepository;
|
||||||
import dev.langchain4j.agent.tool.Tool;
|
import dev.langchain4j.agent.tool.Tool;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Component
|
@Component("crmTools")
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class CrmTools {
|
public class CrmTools {
|
||||||
|
|
||||||
private final SysEntityRepository sysEntityRepository;
|
private final SysEntityRepository sysEntityRepository;
|
||||||
|
private final SysDynamicDataRepository sysDynamicDataRepository;
|
||||||
|
|
||||||
/**
|
@Transactional
|
||||||
* AI 工具:用于定义新的动态业务对象(Entity)。
|
@Tool("核心业务处理:根据消息意图定义或执行模型变更 (Schema)。支持以下高级能力:\n" +
|
||||||
* 当用户试图创建新表单、新模块(比如“线索管理”、“合同管理”)时,大模型会自动决定调用本工具,
|
"1. [实体关联]:若字段需要关联其他实体,需在 Schema 属性中加入 `x-link-entity: '目标实体Code'`。\n" +
|
||||||
* 并将它根据系统指令生成的参数传入进来。
|
"2. [值约束]:支持标准 JSON Schema 的 `enum` (下拉选择)、`pattern` (正则校验)、`format` (如 date, email)、`minLength` 等约束。\n" +
|
||||||
*
|
"3. [示例]:商品关联分类,分类字段可定义为 { 'type': 'string', 'x-link-entity': 'category' }")
|
||||||
* @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) {
|
public String defineDynamicModule(String entityCode, String entityName, String jsonSchema) {
|
||||||
log.info("Agent 正在调用工具执行创建新模块: Code={}, Name={}", entityCode, entityName);
|
log.info("Agent Tool 正在处理消息:定义模块: Code={}, Name={}", entityCode, entityName);
|
||||||
|
try {
|
||||||
// 1. 业务校验:防重和容错
|
|
||||||
SysEntity existing = sysEntityRepository.findByEntityCode(entityCode);
|
SysEntity existing = sysEntityRepository.findByEntityCode(entityCode);
|
||||||
if (existing != null) {
|
if (existing != null) {
|
||||||
return "模块代码 [" + entityCode + "] 已存在,创建失败,请让用户考虑使用其他代码名称。";
|
existing.setSchemaDefinition(jsonSchema);
|
||||||
|
existing.setEntityName(entityName);
|
||||||
|
existing.setCreatorId(UserContextHolder.getCurrentUser());
|
||||||
|
sysEntityRepository.save(existing);
|
||||||
|
return "AI 已更新现有模块 [" + entityName + "]。";
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 实体赋值与入库
|
|
||||||
try {
|
|
||||||
SysEntity entity = new SysEntity();
|
SysEntity entity = new SysEntity();
|
||||||
entity.setEntityCode(entityCode);
|
entity.setEntityCode(entityCode);
|
||||||
entity.setEntityName(entityName);
|
entity.setEntityName(entityName);
|
||||||
// 此处直接存储大模型生成的 JSON 字符串(PostgreSQL 的 Hibernate 驱动会将其封装为 JSONB)
|
|
||||||
entity.setSchemaDefinition(jsonSchema);
|
entity.setSchemaDefinition(jsonSchema);
|
||||||
|
entity.setCreatorId(UserContextHolder.getCurrentUser());
|
||||||
SysEntity saved = sysEntityRepository.save(entity);
|
SysEntity saved = sysEntityRepository.save(entity);
|
||||||
return "核心模块 [" + entityName + "] 已成功持久化并部署到系统中。后台对应的 Entity ID (UUID) 为: " + saved.getId();
|
return "AI 已成功解析并持久化业务模块 [" + entityName + " (" + entityCode + ")],ID: " + saved.getId();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("保存 Schema 时出现异常", e);
|
log.error("AI 处理定义请求失败: {}", e.getMessage());
|
||||||
return "数据库保存异常,请检查 JSON 结构是否合规。";
|
return "处理失败: " + e.getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI 工具封装:直接处理业务数据入库意图。
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
@Tool("核心业务处理:将业务消息中的动态数据持久化到指定模块。参数:entityCode (模块代码), jsonData (JSON 数据)。")
|
||||||
|
public String insertDynamicData(String entityCode, String jsonData) {
|
||||||
|
log.info("Agent Tool 正在处理消息:将数据保存至模块: Code={}", entityCode);
|
||||||
|
try {
|
||||||
|
SysEntity entity = sysEntityRepository.findByEntityCode(entityCode);
|
||||||
|
if (entity == null) {
|
||||||
|
return "AI 无法找到模块 [" + entityCode + "],请先下达创建模块的指令。";
|
||||||
|
}
|
||||||
|
|
||||||
|
SysDynamicData record = new SysDynamicData();
|
||||||
|
record.setEntityId(entity.getId());
|
||||||
|
record.setData(jsonData);
|
||||||
|
record.setCreatorId(UserContextHolder.getCurrentUser());
|
||||||
|
SysDynamicData saved = sysDynamicDataRepository.save(record);
|
||||||
|
return "AI 已自动完成数据入库,记录主键 ID: " + saved.getId();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("AI 自动化入库失败: {}", e.getMessage());
|
||||||
|
return "入库失败: " + e.getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI 工具封装:删除指定的业务数据。
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
@Tool("核心业务处理:根据 ID 物理删除指定的业务记录。参数:id (记录的唯一 UUID)。")
|
||||||
|
public String deleteDynamicData(String id) {
|
||||||
|
log.info("Agent Tool 正在处理消息:删除记录 ID: {}", id);
|
||||||
|
try {
|
||||||
|
java.util.UUID uuid = java.util.UUID.fromString(id);
|
||||||
|
if (!sysDynamicDataRepository.existsById(uuid)) {
|
||||||
|
return "AI 无法找到 ID 为 [" + id + "] 的记录,可能已被删除。";
|
||||||
|
}
|
||||||
|
sysDynamicDataRepository.deleteById(uuid);
|
||||||
|
return "AI 已成功从系统中移除该条记录 (ID: " + id + ")。";
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("AI 物理删除失败: {}", e.getMessage());
|
||||||
|
return "删除操作遇到技术故障: " + e.getMessage();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -3,11 +3,13 @@ package com.example.ainative.controller;
|
|||||||
import com.example.ainative.ai.CrmAgent;
|
import com.example.ainative.ai.CrmAgent;
|
||||||
import com.example.ainative.config.UserContextHolder;
|
import com.example.ainative.config.UserContextHolder;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/agent")
|
@RequestMapping("/api/agent")
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
|
@Slf4j
|
||||||
public class AgentController {
|
public class AgentController {
|
||||||
|
|
||||||
private final CrmAgent crmAgent;
|
private final CrmAgent crmAgent;
|
||||||
@@ -24,7 +26,7 @@ public class AgentController {
|
|||||||
public String chat(@RequestParam String sessionId, @RequestBody String message) {
|
public String chat(@RequestParam String sessionId, @RequestBody String message) {
|
||||||
// 先从拦截器上下文中拿到刚才设置的调用者身份
|
// 先从拦截器上下文中拿到刚才设置的调用者身份
|
||||||
String currentUser = UserContextHolder.getCurrentUser();
|
String currentUser = UserContextHolder.getCurrentUser();
|
||||||
|
log.info("message:{}",message);
|
||||||
// 传递给底层的 Agent:这背后的复杂工作流完全交由 Langchain4J 托管!
|
// 传递给底层的 Agent:这背后的复杂工作流完全交由 Langchain4J 托管!
|
||||||
// 如果用户的 message 意图命中“建表”,框架会在返回前自己拦截调去跑 CrmTools 里的 defineDynamicModule。
|
// 如果用户的 message 意图命中“建表”,框架会在返回前自己拦截调去跑 CrmTools 里的 defineDynamicModule。
|
||||||
return crmAgent.chat(sessionId, message, currentUser);
|
return crmAgent.chat(sessionId, message, currentUser);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.example.ainative.controller;
|
package com.example.ainative.controller;
|
||||||
|
|
||||||
|
import com.example.ainative.ai.CrmTools;
|
||||||
import com.example.ainative.config.UserContextHolder;
|
import com.example.ainative.config.UserContextHolder;
|
||||||
import com.example.ainative.entity.SysDynamicData;
|
import com.example.ainative.entity.SysDynamicData;
|
||||||
import com.example.ainative.entity.SysEntity;
|
import com.example.ainative.entity.SysEntity;
|
||||||
@@ -18,7 +19,7 @@ import java.util.UUID;
|
|||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/dynamic")
|
@RequestMapping("/api/dynamic")
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@CrossOrigin(origins = "*") // 允许前端 Next.js (通常是 3000 端口) 进行跨域调用
|
@CrossOrigin(origins = "*")
|
||||||
public class DynamicDataController {
|
public class DynamicDataController {
|
||||||
|
|
||||||
private final SysDynamicDataRepository dataRepository;
|
private final SysDynamicDataRepository dataRepository;
|
||||||
@@ -29,88 +30,19 @@ public class DynamicDataController {
|
|||||||
*/
|
*/
|
||||||
@GetMapping("/{entityId}")
|
@GetMapping("/{entityId}")
|
||||||
public List<SysDynamicData> getRecords(@PathVariable UUID entityId) {
|
public List<SysDynamicData> getRecords(@PathVariable UUID entityId) {
|
||||||
return dataRepository.findByEntityId(entityId);
|
return dataRepository.findByEntityIdAndCreatorId(entityId, UserContextHolder.getCurrentUser());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据模块 code 获取数据列表(前端动态表格主要用此接口方便查询)
|
* 根据模块 code 获取数据列表
|
||||||
*/
|
*/
|
||||||
@GetMapping("/code/{entityCode}")
|
@GetMapping("/code/{entityCode}")
|
||||||
public List<SysDynamicData> getRecordsByCode(@PathVariable String entityCode) {
|
public List<SysDynamicData> getRecordsByCode(@PathVariable String entityCode) {
|
||||||
SysEntity entity = entityRepository.findByEntityCode(entityCode);
|
SysEntity entity = entityRepository.findByEntityCodeAndCreatorId(entityCode, UserContextHolder.getCurrentUser());
|
||||||
if (entity != null) {
|
if (entity != null) {
|
||||||
return dataRepository.findByEntityId(entity.getId());
|
return dataRepository.findByEntityIdAndCreatorId(entity.getId(), UserContextHolder.getCurrentUser());
|
||||||
}
|
}
|
||||||
return List.of();
|
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -10,24 +10,27 @@ import java.util.List;
|
|||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/entities")
|
@RequestMapping("/api/entities")
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@CrossOrigin(origins = "*") // Allow frontend to fetch menus
|
@CrossOrigin(origins = "*")
|
||||||
public class SysEntityController {
|
public class SysEntityController {
|
||||||
|
|
||||||
private final SysEntityRepository sysEntityRepository;
|
private final SysEntityRepository sysEntityRepository;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取所有已定义的模块列表,供左侧菜单动态渲染使用。
|
* 获取所有已定义的模块列表,供左侧菜单动态渲染使用。
|
||||||
|
* 该接口属于传统查询,用于 UI 展现。
|
||||||
*/
|
*/
|
||||||
@GetMapping
|
@GetMapping
|
||||||
public List<SysEntity> getAllEntities() {
|
public List<SysEntity> getAllEntities() {
|
||||||
return sysEntityRepository.findAll();
|
return sysEntityRepository.findByCreatorId(com.example.ainative.config.UserContextHolder.getCurrentUser());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据 code 获取单一实体定义,方便前台表格按需渲染列等动态行为。
|
* 根据 code 获取单一实体定义。
|
||||||
*/
|
*/
|
||||||
@GetMapping("/code/{entityCode}")
|
@GetMapping("/code/{entityCode}")
|
||||||
public SysEntity getEntityByCode(@PathVariable String entityCode) {
|
public SysEntity getEntityByCode(@PathVariable String entityCode) {
|
||||||
return sysEntityRepository.findByEntityCode(entityCode);
|
return sysEntityRepository.findByEntityCodeAndCreatorId(entityCode, com.example.ainative.config.UserContextHolder.getCurrentUser());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,9 @@ public class SysEntity {
|
|||||||
@Column(name = "schema_definition", columnDefinition = "jsonb", nullable = false)
|
@Column(name = "schema_definition", columnDefinition = "jsonb", nullable = false)
|
||||||
private String schemaDefinition;
|
private String schemaDefinition;
|
||||||
|
|
||||||
|
@Column(name = "creator_id", length = 50)
|
||||||
|
private String creatorId;
|
||||||
|
|
||||||
@CreationTimestamp
|
@CreationTimestamp
|
||||||
@Column(name = "created_at", updatable = false)
|
@Column(name = "created_at", updatable = false)
|
||||||
private ZonedDateTime createdAt;
|
private ZonedDateTime createdAt;
|
||||||
|
|||||||
@@ -8,4 +8,5 @@ import java.util.UUID;
|
|||||||
|
|
||||||
public interface SysDynamicDataRepository extends JpaRepository<SysDynamicData, UUID> {
|
public interface SysDynamicDataRepository extends JpaRepository<SysDynamicData, UUID> {
|
||||||
List<SysDynamicData> findByEntityId(UUID entityId);
|
List<SysDynamicData> findByEntityId(UUID entityId);
|
||||||
|
List<SysDynamicData> findByEntityIdAndCreatorId(UUID entityId, String creatorId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,11 @@ package com.example.ainative.repository;
|
|||||||
import com.example.ainative.entity.SysEntity;
|
import com.example.ainative.entity.SysEntity;
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
public interface SysEntityRepository extends JpaRepository<SysEntity, UUID> {
|
public interface SysEntityRepository extends JpaRepository<SysEntity, UUID> {
|
||||||
SysEntity findByEntityCode(String entityCode);
|
SysEntity findByEntityCode(String entityCode);
|
||||||
|
List<SysEntity> findByCreatorId(String creatorId);
|
||||||
|
SysEntity findByEntityCodeAndCreatorId(String entityCode, String creatorId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ spring:
|
|||||||
jpa:
|
jpa:
|
||||||
hibernate:
|
hibernate:
|
||||||
ddl-auto: none
|
ddl-auto: none
|
||||||
show-sql: true
|
show-sql: false
|
||||||
properties:
|
properties:
|
||||||
hibernate:
|
hibernate:
|
||||||
format_sql: true
|
format_sql: true
|
||||||
@@ -26,3 +26,8 @@ langchain4j:
|
|||||||
|
|
||||||
server:
|
server:
|
||||||
port: 8080
|
port: 8080
|
||||||
|
|
||||||
|
logging:
|
||||||
|
level:
|
||||||
|
dev.ai4j: debug
|
||||||
|
org.hibernate.sql: error
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ spring:
|
|||||||
jpa:
|
jpa:
|
||||||
hibernate:
|
hibernate:
|
||||||
ddl-auto: none
|
ddl-auto: none
|
||||||
show-sql: true
|
show-sql: false
|
||||||
properties:
|
properties:
|
||||||
hibernate:
|
hibernate:
|
||||||
format_sql: true
|
format_sql: true
|
||||||
@@ -26,3 +26,8 @@ langchain4j:
|
|||||||
|
|
||||||
server:
|
server:
|
||||||
port: 8080
|
port: 8080
|
||||||
|
|
||||||
|
logging:
|
||||||
|
level:
|
||||||
|
dev.ai4j: debug
|
||||||
|
org.hibernate.sql: error
|
||||||
|
|||||||
Reference in New Issue
Block a user