35 Commits

Author SHA1 Message Date
Chuck1sn
96de9a215f 更新 README.md 2026-03-20 10:19:30 +08:00
Chuck1sn
59631cea3d 更新用户管理页面中的推广横幅文本,修改为“AI 时代的 Java 测试驱动开发” 2025-08-12 10:52:50 +08:00
Chuck1sn
9425c2c88d 更新group.png图片文件 2025-07-29 16:27:33 +08:00
Chuck1sn
38556f3417 remove comment 2025-07-25 06:33:11 +08:00
Chuck1sn
f0a237fdf3 更新group.png图片文件 2025-07-24 11:03:25 +08:00
Chuck1sn
13d240342c 优化Assistant.vue组件中的Modal初始化,调整useAiChat.ts中的消息处理逻辑,简化package.json中的workerDirectory格式。 2025-07-17 21:36:38 +08:00
Chuck1sn
8e20f561a4 remove md 2025-07-17 17:01:41 +08:00
Chuck1sn
b759275cf3 更新tailwindcss版本至4.1.11,优化Assistant.vue和BaseDialog.vue组件,移除不必要的Modal参数,调整workerDirectory格式。 2025-07-17 11:58:32 +08:00
Chuck1sn
5f0f0fbd14 update lib 2025-07-17 09:41:15 +08:00
Chuck1sn
7088712560 更新.group.png图片文件,修改.gitignore以忽略.vscode目录,删除.vscode中的extensions.json和settings.json文件。 2025-07-14 11:39:02 +08:00
Chuck1sn
2c302315b1 新增mjga.png图片文件,更新PromotionBanner组件以支持新的后端脚手架链接,并调整用户管理页面布局以展示多个推广横幅。 2025-07-14 10:05:18 +08:00
Chuck1sn
87c4706ca7 优化AOP日志模拟数据,删除不必要的删除接口逻辑,简化响应返回值。 2025-07-08 16:29:15 +08:00
Chuck1sn
deece30554 更新AopLogQueryDto中的时间字段类型为OffsetDateTime,优化AopLogRepository中的时间范围查询逻辑,调整前端模拟数据生成器以支持新的时间格式,修复日期选择器样式,优化日志管理页面的用户体验。 2025-07-08 14:36:51 +08:00
Chuck1sn
eecc8bedae 重构AOP日志功能,新增日志查询、删除接口及相关页面,优化日志管理体验;更新前端组件以支持日志展示和操作。 2025-07-08 13:03:59 +08:00
Chuck1sn
0a0174c01e add curl field 2025-07-08 10:48:20 +08:00
Chuck1sn
36d285a61d init aop log 2025-07-07 16:13:52 +08:00
Chuck1sn
5c685d4f74 更新页面标题为“知路 AI 后台管理”,修改头部组件中的标题以保持一致性,同时更新 group.png 图片文件。 2025-07-02 09:05:12 +08:00
ccmjga
1bd50f5de2 更新聊天功能,调整调用 chat 函数以支持传递知识库名称,提升知识库使用体验。 2025-06-29 11:12:14 +08:00
ccmjga
e646bdffa8 优化表单提交错误处理逻辑,调整错误提示的属性名称;移除未使用的文档获取函数,简化知识查询逻辑。 2025-06-28 22:51:52 +08:00
ccmjga
a767613b58 publicEndPointMatcher fix 2025-06-28 22:41:24 +08:00
ccmjga
152f0fe07c Merge branch 'dev' 2025-06-28 22:31:49 +08:00
ccmjga
b6ecc929b0 新增 ChatDto 数据传输对象,更新聊天接口以支持知识库功能,优化聊天服务逻辑,调整前端组件以提升用户体验。 2025-06-28 22:31:20 +08:00
Chuck1sn
3e1d7e6fee 新增表单和对话框组件,优化头像处理逻辑,更新相关工具函数,提升用户界面和交互体验。 2025-06-28 09:16:36 +08:00
Chuck1sn
f9c8e3808b 新增 CardBase 组件,重构知识文档卡片、知识库卡片和分段卡片以使用新组件,优化按钮组件并更新相关页面以提升用户体验。 2025-06-28 08:43:54 +08:00
Chuck1sn
56d6a992f8 新增知识库相关组件,包括文档卡片、知识库卡片、状态徽章和分段卡片,优化日期格式化工具函数,更新文档管理和知识库管理页面以使用新组件。 2025-06-28 08:12:59 +08:00
Chuck1sn
6ec07686a9 Long libraryId 2025-06-27 22:57:00 +08:00
Chuck1sn
4d70b49e61 fix parse bugs 2025-06-27 18:47:17 +08:00
Chuck1sn
8ed0b795f3 新增知识库管理功能,包括知识库和文档的增删改查,优化文档上传和状态管理,更新相关API接口和前端页面,添加知识库和文档的视图组件。 2025-06-27 16:51:48 +08:00
Chuck1sn
2fb08968ee description 2025-06-26 19:55:53 +08:00
Chuck1sn
2f7259ca9d fix endpoint 2025-06-26 19:27:51 +08:00
Chuck1sn
19090b9c94 add async 2025-06-26 18:03:00 +08:00
Chuck1sn
cbfbd6c5dd remove data_count 2025-06-26 15:59:56 +08:00
Chuck1sn
09f51fa91f add desc 2025-06-26 15:58:17 +08:00
Chuck1sn
5494181ae0 init library 2025-06-26 15:54:38 +08:00
Chuck1sn
8d285e1abc add document ingestor 2025-06-25 15:10:02 +08:00
113 changed files with 7142 additions and 438 deletions

View File

@@ -30,31 +30,27 @@
- [🍑 更多](#-更多) - [🍑 更多](#-更多)
- [🍒 部分技术选型](#-部分技术选型) - [🍒 部分技术选型](#-部分技术选型)
- [🔮 防失联,关注各大社区账号](#-防失联关注各大社区账号) - [🔮 防失联,关注各大社区账号](#-防失联关注各大社区账号)
- [💌 微信打赏](#-微信打赏)
## 🥝 产品社群 ## 🥝 产品社群
**加 QQ 群或微信群立送以下装备,瞬间秒杀全服!!**
1. 一键部署脚本(包含数据库 Redis 消息队列等所有中间件!) 1. 一键部署脚本(包含数据库 Redis 消息队列等所有中间件!)
2. 永久免费的 Https 证书 2. 永久免费的 Https 证书
3. 永久免费的分布式对象存储 3. 永久免费的分布式对象存储
4. 永久免费的 AI 模型 4. 永久免费的 AI 模型
5. 永久免费的 Node、Docker、Maven 国内镜像仓库 5. 永久免费的 Node、Docker、Maven 国内镜像仓库
![group](assets/group.png)
[![点击按钮加入 QQ群](https://img.shields.io/badge/-white?style=social&logo=QQ&label=或点击按钮加入QQ群)](https://qm.qq.com/q/9mvVC57jPO) [![点击按钮加入 QQ群](https://img.shields.io/badge/-white?style=social&logo=QQ&label=或点击按钮加入QQ群)](https://qm.qq.com/q/9mvVC57jPO)
- QQ群638254979(目前人较多) - QQ群638254979
- 微信Chuck9996(若微信群已过期可以加我 vx)
## 🍅 相关课程 ## 🍅 相关课程
已上线: 已上线:
- [国内首个无幻觉式 AI 编程指南](https://www.bilibili.com/cheese/play/ep1615343) - [AI 时代的 Java 测试驱动开发](https://www.bilibili.com/cheese/play/ep1615343)
敬请期待:(加群获取) 敬请期待:(加群获取)
@@ -209,9 +205,3 @@
[![Github](https://img.shields.io/badge/-white?style=social&logo=github&label=github)](https://github.com/ccmjga) [![Github](https://img.shields.io/badge/-white?style=social&logo=github&label=github)](https://github.com/ccmjga)
[![QQ](https://img.shields.io/badge/-white?style=social&logo=QQ&label=QQ群)](https://qm.qq.com/q/9mvVC57jPO) [![QQ](https://img.shields.io/badge/-white?style=social&logo=QQ&label=QQ群)](https://qm.qq.com/q/9mvVC57jPO)
## 💌 微信打赏
知路管理后台的发展离不开您的支持;再次对所有支持本项目的人们致以诚挚的谢意~
![pay](/assets/pay.png)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 575 KiB

After

Width:  |  Height:  |  Size: 580 KiB

View File

@@ -64,6 +64,8 @@ dependencies {
implementation("dev.langchain4j:langchain4j-open-ai:1.0.0") implementation("dev.langchain4j:langchain4j-open-ai:1.0.0")
implementation("dev.langchain4j:langchain4j-pgvector:1.0.1-beta6") implementation("dev.langchain4j:langchain4j-pgvector:1.0.1-beta6")
implementation("dev.langchain4j:langchain4j-community-zhipu-ai:1.0.1-beta6") implementation("dev.langchain4j:langchain4j-community-zhipu-ai:1.0.1-beta6")
implementation("dev.langchain4j:langchain4j-document-parser-apache-tika:1.1.0-beta7")
implementation("dev.langchain4j:langchain4j-document-loader-amazon-s3:1.1.0-beta7")
implementation("io.projectreactor:reactor-core:3.7.6") implementation("io.projectreactor:reactor-core:3.7.6")
testImplementation("org.testcontainers:junit-jupiter:$testcontainersVersion") testImplementation("org.testcontainers:junit-jupiter:$testcontainersVersion")
testImplementation("org.testcontainers:postgresql:$testcontainersVersion") testImplementation("org.testcontainers:postgresql:$testcontainersVersion")
@@ -100,14 +102,14 @@ tasks.jacocoTestReport {
} }
jacoco { jacoco {
toolVersion = "0.8.12" toolVersion = "0.8.13"
reportsDirectory.set(layout.buildDirectory.dir("reports/jacoco")) reportsDirectory.set(layout.buildDirectory.dir("reports/jacoco"))
} }
pmd { pmd {
sourceSets = listOf(java.sourceSets.findByName("main")) sourceSets = listOf(java.sourceSets.findByName("main"))
isConsoleOutput = true isConsoleOutput = true
toolVersion = "7.9.0" toolVersion = "7.15.0"
rulesMinimumPriority.set(5) rulesMinimumPriority.set(5)
ruleSetFiles = files("pmd-rules.xml") ruleSetFiles = files("pmd-rules.xml")
} }
@@ -123,7 +125,7 @@ spotless {
} }
java { java {
googleJavaFormat("1.25.2").reflowLongStrings() googleJavaFormat("1.28.0").reflowLongStrings()
formatAnnotations() formatAnnotations()
} }
@@ -168,14 +170,8 @@ jooq {
} }
forcedTypes { forcedTypes {
forcedType { forcedType {
name = "varchar" isJsonConverter = true
includeExpression = ".*" includeTypes = "(?i:JSON|JSONB)"
includeTypes = "JSONB?"
}
forcedType {
name = "varchar"
includeExpression = ".*"
includeTypes = "INET"
} }
} }
} }

View File

@@ -2,7 +2,9 @@ package com.zl.mjga;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;
@EnableAsync
@SpringBootApplication(scanBasePackages = {"com.zl.mjga", "org.jooq.generated"}) @SpringBootApplication(scanBasePackages = {"com.zl.mjga", "org.jooq.generated"})
public class ApplicationService { public class ApplicationService {

View File

@@ -0,0 +1,15 @@
package com.zl.mjga.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SkipAopLog {
String reason() default "";
}

View File

@@ -0,0 +1,312 @@
package com.zl.mjga.aspect;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.zl.mjga.annotation.SkipAopLog;
import com.zl.mjga.repository.UserRepository;
import com.zl.mjga.service.AopLogService;
import jakarta.servlet.http.HttpServletRequest;
import java.io.BufferedReader;
import java.io.IOException;
import java.lang.reflect.Method;
import java.time.Duration;
import java.time.Instant;
import java.util.Collections;
import java.util.Enumeration;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.jooq.generated.mjga.tables.pojos.AopLog;
import org.jooq.generated.mjga.tables.pojos.User;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
@Aspect
@Component
@Slf4j
@RequiredArgsConstructor
@ConditionalOnProperty(name = "aop.logging.enabled", havingValue = "true", matchIfMissing = true)
public class LoggingAspect {
private final AopLogService aopLogService;
private final ObjectMapper objectMapper;
private final UserRepository userRepository;
@Around("execution(* com.zl.mjga.controller..*(..))")
public Object logController(ProceedingJoinPoint joinPoint) throws Throwable {
AopLog aopLog = new AopLog();
setRequestInfo(aopLog);
return processWithLogging(joinPoint, aopLog);
}
// @Around("execution(* com.zl.mjga.service..*(..))")
// public Object logService(ProceedingJoinPoint joinPoint) throws Throwable {
// AopLog aopLog = new AopLog();
// return processWithLogging(joinPoint, aopLog);
// }
private Object processWithLogging(ProceedingJoinPoint joinPoint, AopLog aopLog) throws Throwable {
if (shouldSkipLogging(joinPoint) || !isUserAuthenticated()) {
return joinPoint.proceed();
}
return logMethodExecution(joinPoint, aopLog);
}
private boolean shouldSkipLogging(ProceedingJoinPoint joinPoint) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
return method.isAnnotationPresent(SkipAopLog.class);
}
private boolean isUserAuthenticated() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
return authentication != null
&& authentication.isAuthenticated()
&& !"anonymousUser".equals(authentication.getName());
}
private Long getCurrentUserId() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String username = authentication.getName();
User user = userRepository.fetchOneByUsername(username);
return user.getId();
}
private Object logMethodExecution(ProceedingJoinPoint joinPoint, AopLog aopLog) throws Throwable {
Instant startTime = Instant.now();
String className = joinPoint.getTarget().getClass().getSimpleName();
String methodName = joinPoint.getSignature().getName();
populateBasicLogInfo(aopLog, className, methodName, joinPoint.getArgs());
Object result = null;
Exception executionException = null;
try {
result = joinPoint.proceed();
aopLog.setReturnValue(serializeReturnValue(result));
} catch (Exception e) {
executionException = e;
aopLog.setErrorMessage(e.getMessage());
log.error("Method execution failed: {}.{}", className, methodName, e);
} finally {
aopLog.setExecutionTime(Duration.between(startTime, Instant.now()).toMillis());
aopLog.setSuccess(executionException == null);
saveLogSafely(aopLog);
}
if (executionException != null) {
throw executionException;
}
return result;
}
private void populateBasicLogInfo(
AopLog aopLog, String className, String methodName, Object[] args) {
aopLog.setClassName(className);
aopLog.setMethodName(methodName);
aopLog.setMethodArgs(serializeArgs(args));
aopLog.setUserId(getCurrentUserId());
}
private void saveLogSafely(AopLog aopLog) {
try {
aopLogService.saveLogAsync(aopLog);
} catch (Exception e) {
log.error(
"Failed to save AOP log for {}.{}", aopLog.getClassName(), aopLog.getMethodName(), e);
}
}
private void setRequestInfo(AopLog aopLog) {
ServletRequestAttributes attributes =
(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes == null) {
return;
}
HttpServletRequest request = attributes.getRequest();
aopLog.setIpAddress(getClientIpAddress(request));
aopLog.setUserAgent(request.getHeader("User-Agent"));
aopLog.setCurl(generateCurlCommand(request));
}
private String getClientIpAddress(HttpServletRequest request) {
String xForwardedFor = request.getHeader("X-Forwarded-For");
if (xForwardedFor != null
&& !xForwardedFor.isEmpty()
&& !"unknown".equalsIgnoreCase(xForwardedFor)) {
return xForwardedFor.split(",")[0].trim();
}
String xRealIp = request.getHeader("X-Real-IP");
if (xRealIp != null && !xRealIp.isEmpty() && !"unknown".equalsIgnoreCase(xRealIp)) {
return xRealIp;
}
return request.getRemoteAddr();
}
private String serializeArgs(Object[] args) {
if (ArrayUtils.isEmpty(args)) {
return null;
} else {
return serializeObject(args);
}
}
private String serializeReturnValue(Object returnValue) {
if (returnValue == null) {
return null;
} else {
return serializeObject(returnValue);
}
}
private String serializeObject(Object obj) {
try {
return objectMapper.writeValueAsString(obj);
} catch (JsonProcessingException e) {
log.error("Failed to serialize {} ", obj, e);
return e.getMessage();
}
}
public String generateCurlCommand(HttpServletRequest request) {
try {
StringBuilder curl = new StringBuilder("curl -X ");
curl.append(request.getMethod());
String url = getFullRequestUrl(request);
curl.append(" '").append(url).append("'");
appendHeaders(curl, request);
if (hasRequestBody(request.getMethod())) {
appendRequestBody(curl, request);
}
return curl.toString();
} catch (Exception e) {
log.error("Failed to generate curl command", e);
return "curl command generation failed: " + e.getMessage();
}
}
private String getFullRequestUrl(HttpServletRequest request) {
StringBuilder url = new StringBuilder();
String scheme = request.getScheme();
String serverName = request.getServerName();
int serverPort = request.getServerPort();
if (scheme == null) {
scheme = "http";
}
if (serverName == null) {
serverName = "localhost";
}
url.append(scheme).append("://").append(serverName);
if ((scheme.equals("http") && serverPort != 80)
|| (scheme.equals("https") && serverPort != 443)) {
url.append(":").append(serverPort);
}
url.append(request.getRequestURI());
if (request.getQueryString() != null) {
url.append("?").append(request.getQueryString());
}
return url.toString();
}
private void appendHeaders(StringBuilder curl, HttpServletRequest request) {
Enumeration<String> headerNames = request.getHeaderNames();
for (String headerName : Collections.list(headerNames)) {
if (shouldSkipHeader(headerName)) {
continue;
}
String headerValue = request.getHeader(headerName);
curl.append(" -H '").append(headerName).append(": ").append(headerValue).append("'");
}
}
private boolean shouldSkipHeader(String headerName) {
String lowerName = headerName.toLowerCase();
return lowerName.equals("host")
|| lowerName.equals("content-length")
|| lowerName.equals("connection")
|| lowerName.startsWith("sec-")
|| lowerName.equals("upgrade-insecure-requests");
}
private boolean hasRequestBody(String method) {
return "POST".equalsIgnoreCase(method)
|| "PUT".equalsIgnoreCase(method)
|| "PATCH".equalsIgnoreCase(method);
}
private void appendRequestBody(StringBuilder curl, HttpServletRequest request) {
try {
String contentType = request.getContentType();
if (StringUtils.contains(contentType, "application/json")) {
String body = getRequestBody(request);
if (StringUtils.isNotEmpty(body)) {
curl.append(" -d '").append(body.replace("'", "\\'")).append("'");
}
} else if (StringUtils.contains(contentType, "application/x-www-form-urlencoded")) {
appendFormData(curl, request);
}
} catch (Exception e) {
log.warn("Failed to append request body to curl command", e);
}
}
private String getRequestBody(HttpServletRequest request) {
try (BufferedReader reader = request.getReader()) {
if (reader == null) {
return null;
}
StringBuilder body = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
body.append(line);
}
return body.toString();
} catch (IOException e) {
log.warn("Failed to read request body", e);
return null;
}
}
private void appendFormData(StringBuilder curl, HttpServletRequest request) {
Enumeration<String> paramNames = request.getParameterNames();
StringBuilder formData = new StringBuilder();
while (paramNames.hasMoreElements()) {
String paramName = paramNames.nextElement();
String[] paramValues = request.getParameterValues(paramName);
for (String paramValue : paramValues) {
if (!formData.isEmpty()) {
formData.append("&");
}
formData.append(paramName).append("=").append(paramValue);
}
}
if (!formData.isEmpty()) {
curl.append(" -d '").append(formData).append("'");
}
}
}

View File

@@ -0,0 +1,35 @@
package com.zl.mjga.config;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
import java.io.IOException;
import org.jooq.JSON;
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class JacksonConfig {
@Bean
public Jackson2ObjectMapperBuilderCustomizer jsonCustomizer() {
return builder ->
builder
.serializationInclusion(JsonInclude.Include.USE_DEFAULTS)
.serializers(new JooqJsonSerializer());
}
private static class JooqJsonSerializer extends StdSerializer<JSON> {
public JooqJsonSerializer() {
super(JSON.class);
}
@Override
public void serialize(JSON value, JsonGenerator gen, SerializerProvider serializers)
throws IOException {
gen.writeRawValue(value.data());
}
}
}

View File

@@ -1,11 +1,17 @@
package com.zl.mjga.config.ai; package com.zl.mjga.config.ai;
import static dev.langchain4j.store.embedding.filter.MetadataFilterBuilder.metadataKey;
import com.zl.mjga.component.PromptConfiguration; import com.zl.mjga.component.PromptConfiguration;
import com.zl.mjga.service.LlmService; import com.zl.mjga.service.LlmService;
import dev.langchain4j.community.model.zhipu.ZhipuAiStreamingChatModel; import dev.langchain4j.community.model.zhipu.ZhipuAiStreamingChatModel;
import dev.langchain4j.data.segment.TextSegment;
import dev.langchain4j.memory.chat.MessageWindowChatMemory; import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import dev.langchain4j.model.embedding.EmbeddingModel;
import dev.langchain4j.model.openai.OpenAiStreamingChatModel; import dev.langchain4j.model.openai.OpenAiStreamingChatModel;
import dev.langchain4j.rag.content.retriever.EmbeddingStoreContentRetriever;
import dev.langchain4j.service.AiServices; import dev.langchain4j.service.AiServices;
import dev.langchain4j.store.embedding.EmbeddingStore;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.jooq.generated.mjga.enums.LlmCodeEnum; import org.jooq.generated.mjga.enums.LlmCodeEnum;
import org.jooq.generated.mjga.tables.pojos.AiLlmConfig; import org.jooq.generated.mjga.tables.pojos.AiLlmConfig;
@@ -54,11 +60,26 @@ public class ChatModelInitializer {
@Bean @Bean
@DependsOn("flywayInitializer") @DependsOn("flywayInitializer")
public AiChatAssistant zhiPuChatAssistant(ZhipuAiStreamingChatModel zhipuChatModel) { public AiChatAssistant zhiPuChatAssistant(
ZhipuAiStreamingChatModel zhipuChatModel,
EmbeddingStore<TextSegment> zhiPuLibraryEmbeddingStore,
EmbeddingModel zhipuEmbeddingModel) {
return AiServices.builder(AiChatAssistant.class) return AiServices.builder(AiChatAssistant.class)
.streamingChatModel(zhipuChatModel) .streamingChatModel(zhipuChatModel)
.systemMessageProvider(chatMemoryId -> promptConfiguration.getSystem()) .systemMessageProvider(chatMemoryId -> promptConfiguration.getSystem())
.chatMemoryProvider(memoryId -> MessageWindowChatMemory.withMaxMessages(10)) .chatMemoryProvider(memoryId -> MessageWindowChatMemory.withMaxMessages(10))
.contentRetriever(
EmbeddingStoreContentRetriever.builder()
.embeddingStore(zhiPuLibraryEmbeddingStore)
.embeddingModel(zhipuEmbeddingModel)
.minScore(0.75)
.maxResults(5)
.dynamicFilter(
query -> {
String libraryId = (String) query.metadata().chatMemoryId();
return metadataKey("libraryId").isEqualTo(libraryId);
})
.build())
.build(); .build();
} }

View File

@@ -1,7 +1,10 @@
package com.zl.mjga.config.ai; package com.zl.mjga.config.ai;
import com.zl.mjga.config.minio.MinIoConfig;
import com.zl.mjga.service.LlmService; import com.zl.mjga.service.LlmService;
import dev.langchain4j.community.model.zhipu.ZhipuAiEmbeddingModel; import dev.langchain4j.community.model.zhipu.ZhipuAiEmbeddingModel;
import dev.langchain4j.data.document.loader.amazon.s3.AmazonS3DocumentLoader;
import dev.langchain4j.data.document.loader.amazon.s3.AwsCredentials;
import dev.langchain4j.data.segment.TextSegment; import dev.langchain4j.data.segment.TextSegment;
import dev.langchain4j.model.embedding.EmbeddingModel; import dev.langchain4j.model.embedding.EmbeddingModel;
import dev.langchain4j.store.embedding.EmbeddingStore; import dev.langchain4j.store.embedding.EmbeddingStore;
@@ -42,7 +45,7 @@ public class EmbeddingInitializer {
} }
@Bean @Bean
public EmbeddingStore<TextSegment> zhiPuEmbeddingStore(EmbeddingModel zhipuEmbeddingModel) { public EmbeddingStore<TextSegment> zhiPuEmbeddingStore() {
String hostPort = env.getProperty("DATABASE_HOST_PORT"); String hostPort = env.getProperty("DATABASE_HOST_PORT");
String host = hostPort.split(":")[0]; String host = hostPort.split(":")[0];
return PgVectorEmbeddingStore.builder() return PgVectorEmbeddingStore.builder()
@@ -55,4 +58,28 @@ public class EmbeddingInitializer {
.dimension(2048) .dimension(2048)
.build(); .build();
} }
@Bean
public EmbeddingStore<TextSegment> zhiPuLibraryEmbeddingStore() {
String hostPort = env.getProperty("DATABASE_HOST_PORT");
String host = hostPort.split(":")[0];
return PgVectorEmbeddingStore.builder()
.host(host)
.port(env.getProperty("DATABASE_EXPOSE_PORT", Integer.class))
.database(env.getProperty("DATABASE_DB"))
.user(env.getProperty("DATABASE_USER"))
.password(env.getProperty("DATABASE_PASSWORD"))
.table("mjga.zhipu_library_embedding_store")
.dimension(2048)
.build();
}
@Bean
public AmazonS3DocumentLoader amazonS3DocumentLoader(MinIoConfig minIoConfig) {
return AmazonS3DocumentLoader.builder()
.endpointUrl(minIoConfig.getEndpoint())
.forcePathStyle(true)
.awsCredentials(new AwsCredentials(minIoConfig.getAccessKey(), minIoConfig.getSecretKey()))
.build();
}
} }

View File

@@ -2,13 +2,14 @@ package com.zl.mjga.controller;
import com.zl.mjga.dto.PageRequestDto; import com.zl.mjga.dto.PageRequestDto;
import com.zl.mjga.dto.PageResponseDto; import com.zl.mjga.dto.PageResponseDto;
import com.zl.mjga.dto.ai.ChatDto;
import com.zl.mjga.dto.ai.LlmQueryDto; import com.zl.mjga.dto.ai.LlmQueryDto;
import com.zl.mjga.dto.ai.LlmVm; import com.zl.mjga.dto.ai.LlmVm;
import com.zl.mjga.exception.BusinessException; import com.zl.mjga.exception.BusinessException;
import com.zl.mjga.repository.*; import com.zl.mjga.repository.*;
import com.zl.mjga.service.AiChatService; import com.zl.mjga.service.AiChatService;
import com.zl.mjga.service.EmbeddingService;
import com.zl.mjga.service.LlmService; import com.zl.mjga.service.LlmService;
import com.zl.mjga.service.RagService;
import dev.langchain4j.service.TokenStream; import dev.langchain4j.service.TokenStream;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import java.security.Principal; import java.security.Principal;
@@ -35,7 +36,7 @@ public class AiController {
private final AiChatService aiChatService; private final AiChatService aiChatService;
private final LlmService llmService; private final LlmService llmService;
private final EmbeddingService embeddingService; private final RagService ragService;
private final UserRepository userRepository; private final UserRepository userRepository;
private final DepartmentRepository departmentRepository; private final DepartmentRepository departmentRepository;
private final PositionRepository positionRepository; private final PositionRepository positionRepository;
@@ -72,9 +73,9 @@ public class AiController {
} }
@PostMapping(value = "/chat", produces = MediaType.TEXT_EVENT_STREAM_VALUE) @PostMapping(value = "/chat", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> chat(Principal principal, @RequestBody String userMessage) { public Flux<String> chat(Principal principal, @RequestBody ChatDto chatDto) {
Sinks.Many<String> sink = Sinks.many().unicast().onBackpressureBuffer(); Sinks.Many<String> sink = Sinks.many().unicast().onBackpressureBuffer();
TokenStream chat = aiChatService.chatPrecedenceLlmWith(principal.getName(), userMessage); TokenStream chat = aiChatService.chat(principal.getName(), chatDto);
chat.onPartialResponse( chat.onPartialResponse(
text -> text ->
sink.tryEmitNext( sink.tryEmitNext(
@@ -109,7 +110,7 @@ public class AiController {
if (!aiLlmConfig.getEnable()) { if (!aiLlmConfig.getEnable()) {
throw new BusinessException("命令模型未启用,请开启后再试。"); throw new BusinessException("命令模型未启用,请开启后再试。");
} }
return embeddingService.searchAction(message); return ragService.searchAction(message);
} }
@PreAuthorize("hasAuthority(T(com.zl.mjga.model.urp.EPermission).WRITE_USER_ROLE_PERMISSION)") @PreAuthorize("hasAuthority(T(com.zl.mjga.model.urp.EPermission).WRITE_USER_ROLE_PERMISSION)")

View File

@@ -0,0 +1,73 @@
package com.zl.mjga.controller;
import com.zl.mjga.dto.PageRequestDto;
import com.zl.mjga.dto.PageResponseDto;
import com.zl.mjga.dto.aoplog.AopLogQueryDto;
import com.zl.mjga.dto.aoplog.AopLogRespDto;
import com.zl.mjga.repository.AopLogRepository;
import com.zl.mjga.service.AopLogService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import java.time.OffsetDateTime;
import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/aop-log")
@RequiredArgsConstructor
@Slf4j
@Tag(name = "AOP日志管理", description = "AOP日志查看和管理接口")
public class AopLogController {
private final AopLogService aopLogService;
private final AopLogRepository aopLogRepository;
@GetMapping("/page-query")
@ResponseStatus(HttpStatus.OK)
@Operation(summary = "分页查询AOP日志", description = "支持多种条件筛选的分页查询")
@PreAuthorize("hasAuthority(T(com.zl.mjga.model.urp.EPermission).READ_USER_ROLE_PERMISSION)")
public PageResponseDto<List<AopLogRespDto>> pageQueryAopLogs(
@ModelAttribute @Valid PageRequestDto pageRequestDto,
@ModelAttribute AopLogQueryDto queryDto) {
return aopLogService.pageQueryAopLogs(pageRequestDto, queryDto);
}
@GetMapping("/{id}")
@ResponseStatus(HttpStatus.OK)
@Operation(summary = "查询日志详情", description = "根据ID查询单条日志的详细信息")
@PreAuthorize("hasAuthority(T(com.zl.mjga.model.urp.EPermission).READ_USER_ROLE_PERMISSION)")
public AopLogRespDto getAopLogById(@Parameter(description = "日志ID") @PathVariable Long id) {
return aopLogService.getAopLogById(id);
}
@DeleteMapping("/batch")
@ResponseStatus(HttpStatus.OK)
@Operation(summary = "批量删除日志", description = "根据ID列表批量删除日志")
@PreAuthorize("hasAuthority(T(com.zl.mjga.model.urp.EPermission).WRITE_USER_ROLE_PERMISSION)")
public int deleteAopLogs(@Parameter(description = "日志ID列表") @RequestBody List<Long> ids) {
return aopLogRepository.deleteByIds(ids);
}
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.OK)
@Operation(summary = "删除单条日志", description = "根据ID删除单条日志")
@PreAuthorize("hasAuthority(T(com.zl.mjga.model.urp.EPermission).WRITE_USER_ROLE_PERMISSION)")
public void deleteAopLog(@Parameter(description = "日志ID") @PathVariable Long id) {
aopLogRepository.deleteById(id);
}
@DeleteMapping("/before")
@ResponseStatus(HttpStatus.OK)
@Operation(summary = "删除指定时间前的日志", description = "删除指定时间之前的所有日志")
@PreAuthorize("hasAuthority(T(com.zl.mjga.model.urp.EPermission).WRITE_USER_ROLE_PERMISSION)")
public int deleteLogsBeforeTime(
@Parameter(description = "截止时间") @RequestParam OffsetDateTime beforeTime) {
return aopLogService.deleteLogsBeforeTime(beforeTime);
}
}

View File

@@ -1,6 +1,6 @@
package com.zl.mjga.controller; package com.zl.mjga.controller;
import com.zl.mjga.config.minio.MinIoConfig; import com.zl.mjga.annotation.SkipAopLog;
import com.zl.mjga.dto.PageRequestDto; import com.zl.mjga.dto.PageRequestDto;
import com.zl.mjga.dto.PageResponseDto; import com.zl.mjga.dto.PageResponseDto;
import com.zl.mjga.dto.department.DepartmentBindDto; import com.zl.mjga.dto.department.DepartmentBindDto;
@@ -13,17 +13,11 @@ import com.zl.mjga.repository.PermissionRepository;
import com.zl.mjga.repository.RoleRepository; import com.zl.mjga.repository.RoleRepository;
import com.zl.mjga.repository.UserRepository; import com.zl.mjga.repository.UserRepository;
import com.zl.mjga.service.IdentityAccessService; import com.zl.mjga.service.IdentityAccessService;
import io.minio.MinioClient; import com.zl.mjga.service.UploadService;
import io.minio.PutObjectArgs;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import java.awt.image.BufferedImage;
import java.security.Principal; import java.security.Principal;
import java.time.Instant;
import java.util.List; import java.util.List;
import javax.imageio.ImageIO;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.lang3.StringUtils;
import org.jooq.generated.mjga.tables.pojos.User; import org.jooq.generated.mjga.tables.pojos.User;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
@@ -41,8 +35,7 @@ public class IdentityAccessController {
private final UserRepository userRepository; private final UserRepository userRepository;
private final RoleRepository roleRepository; private final RoleRepository roleRepository;
private final PermissionRepository permissionRepository; private final PermissionRepository permissionRepository;
private final MinioClient minioClient; private final UploadService uploadService;
private final MinIoConfig minIoConfig;
@PreAuthorize("hasAuthority(T(com.zl.mjga.model.urp.EPermission).WRITE_USER_ROLE_PERMISSION)") @PreAuthorize("hasAuthority(T(com.zl.mjga.model.urp.EPermission).WRITE_USER_ROLE_PERMISSION)")
@PostMapping( @PostMapping(
@@ -50,40 +43,7 @@ public class IdentityAccessController {
consumes = MediaType.MULTIPART_FORM_DATA_VALUE, consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
produces = MediaType.TEXT_PLAIN_VALUE) produces = MediaType.TEXT_PLAIN_VALUE)
public String uploadAvatar(@RequestPart("file") MultipartFile multipartFile) throws Exception { public String uploadAvatar(@RequestPart("file") MultipartFile multipartFile) throws Exception {
String originalFilename = multipartFile.getOriginalFilename(); return uploadService.uploadAvatarFile(multipartFile);
if (StringUtils.isEmpty(originalFilename)) {
throw new BusinessException("文件名不能为空");
}
String contentType = multipartFile.getContentType();
String extension = "";
if ("image/jpeg".equals(contentType)) {
extension = ".jpg";
} else if ("image/png".equals(contentType)) {
extension = ".png";
}
String objectName =
String.format(
"/avatar/%d%s%s",
Instant.now().toEpochMilli(),
RandomStringUtils.insecure().nextAlphabetic(6),
extension);
if (multipartFile.isEmpty()) {
throw new BusinessException("上传的文件不能为空");
}
long size = multipartFile.getSize();
if (size > 200 * 1024) {
throw new BusinessException("头像文件大小不能超过200KB");
}
BufferedImage img = ImageIO.read(multipartFile.getInputStream());
if (img == null) {
throw new BusinessException("非法的上传文件");
}
minioClient.putObject(
PutObjectArgs.builder().bucket(minIoConfig.getDefaultBucket()).object(objectName).stream(
multipartFile.getInputStream(), size, -1)
.contentType(multipartFile.getContentType())
.build());
return objectName;
} }
@GetMapping("/me") @GetMapping("/me")
@@ -97,6 +57,7 @@ public class IdentityAccessController {
} }
@PostMapping("/me") @PostMapping("/me")
@SkipAopLog
void upsertMe(Principal principal, @RequestBody UserUpsertDto userUpsertDto) { void upsertMe(Principal principal, @RequestBody UserUpsertDto userUpsertDto) {
String name = principal.getName(); String name = principal.getName();
User user = userRepository.fetchOneByUsername(name); User user = userRepository.fetchOneByUsername(name);
@@ -106,6 +67,7 @@ public class IdentityAccessController {
@PreAuthorize("hasAuthority(T(com.zl.mjga.model.urp.EPermission).WRITE_USER_ROLE_PERMISSION)") @PreAuthorize("hasAuthority(T(com.zl.mjga.model.urp.EPermission).WRITE_USER_ROLE_PERMISSION)")
@PostMapping("/user") @PostMapping("/user")
@SkipAopLog
void upsertUser(@RequestBody @Valid UserUpsertDto userUpsertDto) { void upsertUser(@RequestBody @Valid UserUpsertDto userUpsertDto) {
identityAccessService.upsertUser(userUpsertDto); identityAccessService.upsertUser(userUpsertDto);
} }

View File

@@ -0,0 +1,90 @@
package com.zl.mjga.controller;
import com.zl.mjga.dto.knowledge.DocUpdateDto;
import com.zl.mjga.dto.knowledge.LibraryUpsertDto;
import com.zl.mjga.repository.LibraryDocRepository;
import com.zl.mjga.repository.LibraryDocSegmentRepository;
import com.zl.mjga.repository.LibraryRepository;
import com.zl.mjga.service.RagService;
import com.zl.mjga.service.UploadService;
import jakarta.validation.Valid;
import java.util.Comparator;
import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.jooq.generated.mjga.tables.pojos.Library;
import org.jooq.generated.mjga.tables.pojos.LibraryDoc;
import org.jooq.generated.mjga.tables.pojos.LibraryDocSegment;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
@RestController
@RequestMapping("/knowledge")
@RequiredArgsConstructor
@Slf4j
public class LibraryController {
private final UploadService uploadService;
private final RagService ragService;
private final LibraryRepository libraryRepository;
private final LibraryDocRepository libraryDocRepository;
private final LibraryDocSegmentRepository libraryDocSegmentRepository;
@GetMapping("/libraries")
public List<Library> queryLibraries() {
return libraryRepository.findAll().stream()
.sorted(Comparator.comparing(Library::getId).reversed())
.toList();
}
@GetMapping("/docs")
public List<LibraryDoc> queryLibraryDocs(@RequestParam Long libraryId) {
return libraryDocRepository.fetchByLibId(libraryId).stream()
.sorted(Comparator.comparing(LibraryDoc::getId).reversed())
.toList();
}
@GetMapping("/segments")
public List<LibraryDocSegment> queryLibraryDocSegments(@RequestParam Long libraryDocId) {
return libraryDocSegmentRepository.fetchByDocId(libraryDocId);
}
@PostMapping("/library")
public void upsertLibrary(@RequestBody @Valid LibraryUpsertDto libraryUpsertDto) {
Library library = new Library();
library.setId(libraryUpsertDto.id());
library.setName(libraryUpsertDto.name());
library.setDescription(libraryUpsertDto.description());
libraryRepository.merge(library);
}
@DeleteMapping("/library")
public void deleteLibrary(@RequestParam Long libraryId) {
ragService.deleteLibraryBy(libraryId);
}
@DeleteMapping("/doc")
public void deleteLibraryDoc(@RequestParam Long libraryDocId) {
ragService.deleteDocBy(libraryDocId);
}
@PutMapping("/doc")
public void updateLibraryDoc(@RequestBody @Valid DocUpdateDto docUpdateDto) {
LibraryDoc exist = libraryDocRepository.fetchOneById(docUpdateDto.id());
exist.setEnable(docUpdateDto.enable());
libraryDocRepository.merge(exist);
}
@PostMapping(value = "/doc/upload", produces = MediaType.TEXT_PLAIN_VALUE)
public String uploadLibraryDoc(
@RequestPart("libraryId") String libraryId, @RequestPart("file") MultipartFile multipartFile)
throws Exception {
String objectName = uploadService.uploadLibraryDoc(multipartFile);
Long libraryDocId =
ragService.createLibraryDocBy(
Long.valueOf(libraryId), objectName, multipartFile.getOriginalFilename());
ragService.embeddingAndCreateDocSegment(Long.valueOf(libraryId), libraryDocId, objectName);
return objectName;
}
}

View File

@@ -1,5 +1,6 @@
package com.zl.mjga.controller; package com.zl.mjga.controller;
import com.zl.mjga.annotation.SkipAopLog;
import com.zl.mjga.config.security.Jwt; import com.zl.mjga.config.security.Jwt;
import com.zl.mjga.dto.sign.SignInDto; import com.zl.mjga.dto.sign.SignInDto;
import com.zl.mjga.dto.sign.SignUpDto; import com.zl.mjga.dto.sign.SignUpDto;
@@ -22,6 +23,7 @@ public class SignController {
@ResponseStatus(HttpStatus.OK) @ResponseStatus(HttpStatus.OK)
@PostMapping("/sign-in") @PostMapping("/sign-in")
@SkipAopLog
void signIn( void signIn(
HttpServletRequest request, HttpServletRequest request,
HttpServletResponse response, HttpServletResponse response,

View File

@@ -0,0 +1,7 @@
package com.zl.mjga.dto.ai;
import com.zl.mjga.model.urp.ChatMode;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
public record ChatDto(@NotNull ChatMode mode, Long libraryId, @NotEmpty String message) {}

View File

@@ -0,0 +1,43 @@
package com.zl.mjga.dto.aoplog;
import java.time.OffsetDateTime;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/** AOP日志查询DTO */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class AopLogQueryDto {
/** ID */
private Long id;
/** 类名 */
private String className;
/** 方法名 */
private String methodName;
/** 是否成功 */
private Boolean success;
/** 用户ID */
private Long userId;
/** IP地址 */
private String ipAddress;
/** 开始时间 */
private OffsetDateTime startTime;
/** 结束时间 */
private OffsetDateTime endTime;
/** 最小执行时间(毫秒) */
private Long minExecutionTime;
/** 最大执行时间(毫秒) */
private Long maxExecutionTime;
}

View File

@@ -0,0 +1,56 @@
package com.zl.mjga.dto.aoplog;
import java.time.OffsetDateTime;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class AopLogRespDto {
/** 主键ID */
private Long id;
/** 类名 */
private String className;
/** 方法名 */
private String methodName;
/** 方法参数 */
private String methodArgs;
/** 返回值 */
private String returnValue;
/** 执行时间(毫秒) */
private Long executionTime;
/** 是否成功 */
private Boolean success;
/** 错误信息 */
private String errorMessage;
/** 用户ID */
private Long userId;
/** 用户名 */
private String username;
/** IP地址 */
private String ipAddress;
/** 用户代理 */
private String userAgent;
/** curl命令 */
private String curl;
/** 创建时间 */
private OffsetDateTime createTime;
}

View File

@@ -0,0 +1,5 @@
package com.zl.mjga.dto.knowledge;
import jakarta.validation.constraints.NotNull;
public record DocUpdateDto(@NotNull Long id, @NotNull Long libId, @NotNull Boolean enable) {}

View File

@@ -0,0 +1,5 @@
package com.zl.mjga.dto.knowledge;
import jakarta.validation.constraints.NotEmpty;
public record LibraryUpsertDto(Long id, @NotEmpty String name, String description) {}

View File

@@ -0,0 +1,6 @@
package com.zl.mjga.model.urp;
public enum ChatMode {
NORMAL,
WITH_LIBRARY
}

View File

@@ -0,0 +1,127 @@
package com.zl.mjga.repository;
import static org.jooq.generated.mjga.tables.AopLog.AOP_LOG;
import static org.jooq.generated.mjga.tables.User.USER;
import static org.jooq.impl.DSL.*;
import com.zl.mjga.dto.PageRequestDto;
import com.zl.mjga.dto.aoplog.AopLogQueryDto;
import java.time.OffsetDateTime;
import java.util.List;
import org.apache.commons.lang3.StringUtils;
import org.jooq.*;
import org.jooq.Record;
import org.jooq.generated.mjga.tables.daos.AopLogDao;
import org.jooq.generated.mjga.tables.pojos.AopLog;
import org.jooq.impl.DSL;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
/** AOP日志Repository */
@Repository
public class AopLogRepository extends AopLogDao {
@Autowired
public AopLogRepository(Configuration configuration) {
super(configuration);
}
public Result<Record> pageFetchBy(PageRequestDto pageRequestDto, AopLogQueryDto queryDto) {
return selectByWithoutReturnValue(queryDto)
.orderBy(pageRequestDto.getSortFields())
.limit(pageRequestDto.getSize())
.offset(pageRequestDto.getOffset())
.fetch();
}
public List<AopLog> fetchBy(AopLogQueryDto queryDto) {
return selectBy(queryDto).fetchInto(AopLog.class);
}
public SelectConditionStep<Record> selectBy(AopLogQueryDto queryDto) {
return ctx()
.select(AOP_LOG.asterisk(), USER.USERNAME, DSL.count().over().as("total_count"))
.from(AOP_LOG)
.leftJoin(USER)
.on(AOP_LOG.USER_ID.eq(USER.ID))
.where(buildConditions(queryDto));
}
public SelectConditionStep<Record> selectByWithoutReturnValue(AopLogQueryDto queryDto) {
return ctx()
.select(
AOP_LOG.asterisk().except(AOP_LOG.RETURN_VALUE, AOP_LOG.METHOD_ARGS),
USER.USERNAME,
DSL.count().over().as("total_count"))
.from(AOP_LOG)
.leftJoin(USER)
.on(AOP_LOG.USER_ID.eq(USER.ID))
.where(buildConditions(queryDto));
}
private Condition buildConditions(AopLogQueryDto queryDto) {
Condition condition = noCondition();
if (queryDto == null) {
return condition;
}
// ID精确查询
if (queryDto.getId() != null) {
condition = condition.and(AOP_LOG.ID.eq(queryDto.getId()));
}
// 类名模糊查询
if (StringUtils.isNotBlank(queryDto.getClassName())) {
condition = condition.and(AOP_LOG.CLASS_NAME.like("%" + queryDto.getClassName() + "%"));
}
// 方法名模糊查询
if (StringUtils.isNotBlank(queryDto.getMethodName())) {
condition = condition.and(AOP_LOG.METHOD_NAME.like("%" + queryDto.getMethodName() + "%"));
}
// 成功状态
if (queryDto.getSuccess() != null) {
condition = condition.and(AOP_LOG.SUCCESS.eq(queryDto.getSuccess()));
}
// 用户ID
if (queryDto.getUserId() != null) {
condition = condition.and(AOP_LOG.USER_ID.eq(queryDto.getUserId()));
}
// IP地址模糊查询
if (StringUtils.isNotBlank(queryDto.getIpAddress())) {
condition = condition.and(AOP_LOG.IP_ADDRESS.like("%" + queryDto.getIpAddress() + "%"));
}
// 时间范围查询
if (queryDto.getStartTime() != null) {
condition = condition.and(AOP_LOG.CREATE_TIME.ge(queryDto.getStartTime()));
}
if (queryDto.getEndTime() != null) {
condition = condition.and(AOP_LOG.CREATE_TIME.le(queryDto.getEndTime()));
}
// 执行时间范围
if (queryDto.getMinExecutionTime() != null) {
condition = condition.and(AOP_LOG.EXECUTION_TIME.ge(queryDto.getMinExecutionTime()));
}
if (queryDto.getMaxExecutionTime() != null) {
condition = condition.and(AOP_LOG.EXECUTION_TIME.le(queryDto.getMaxExecutionTime()));
}
return condition;
}
public int deleteByIds(List<Long> ids) {
return ctx().deleteFrom(AOP_LOG).where(AOP_LOG.ID.in(ids)).execute();
}
public int deleteBeforeTime(OffsetDateTime beforeTime) {
return ctx().deleteFrom(AOP_LOG).where(AOP_LOG.CREATE_TIME.lt(beforeTime)).execute();
}
}

View File

@@ -0,0 +1,14 @@
package com.zl.mjga.repository;
import org.jooq.Configuration;
import org.jooq.generated.mjga.tables.daos.LibraryDocDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
@Repository
public class LibraryDocRepository extends LibraryDocDao {
@Autowired
public LibraryDocRepository(Configuration configuration) {
super(configuration);
}
}

View File

@@ -0,0 +1,14 @@
package com.zl.mjga.repository;
import org.jooq.Configuration;
import org.jooq.generated.mjga.tables.daos.LibraryDocSegmentDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
@Repository
public class LibraryDocSegmentRepository extends LibraryDocSegmentDao {
@Autowired
public LibraryDocSegmentRepository(Configuration configuration) {
super(configuration);
}
}

View File

@@ -0,0 +1,15 @@
package com.zl.mjga.repository;
import org.jooq.Configuration;
import org.jooq.generated.mjga.tables.daos.LibraryDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
@Repository
public class LibraryRepository extends LibraryDao {
@Autowired
public LibraryRepository(Configuration configuration) {
super(configuration);
}
}

View File

@@ -2,6 +2,7 @@ package com.zl.mjga.service;
import com.zl.mjga.config.ai.AiChatAssistant; import com.zl.mjga.config.ai.AiChatAssistant;
import com.zl.mjga.config.ai.SystemToolAssistant; import com.zl.mjga.config.ai.SystemToolAssistant;
import com.zl.mjga.dto.ai.ChatDto;
import com.zl.mjga.exception.BusinessException; import com.zl.mjga.exception.BusinessException;
import dev.langchain4j.service.TokenStream; import dev.langchain4j.service.TokenStream;
import java.util.Optional; import java.util.Optional;
@@ -39,8 +40,20 @@ public class AiChatService {
}; };
} }
public TokenStream chatPrecedenceLlmWith(String sessionIdentifier, String userMessage) { public TokenStream chat(String sessionIdentifier, ChatDto chatDto) {
return switch (chatDto.mode()) {
case NORMAL -> chatWithPrecedenceLlm(sessionIdentifier, chatDto);
case WITH_LIBRARY -> chatWithLibrary(chatDto.libraryId(), chatDto);
};
}
public TokenStream chatWithLibrary(Long libraryId, ChatDto chatDto) {
return zhiPuChatAssistant.chat(String.valueOf(libraryId), chatDto.message());
}
public TokenStream chatWithPrecedenceLlm(String sessionIdentifier, ChatDto chatDto) {
LlmCodeEnum code = getPrecedenceLlmCode(); LlmCodeEnum code = getPrecedenceLlmCode();
String userMessage = chatDto.message();
return switch (code) { return switch (code) {
case ZHI_PU -> zhiPuChatAssistant.chat(sessionIdentifier, userMessage); case ZHI_PU -> zhiPuChatAssistant.chat(sessionIdentifier, userMessage);
case DEEP_SEEK -> deepSeekChatAssistant.chat(sessionIdentifier, userMessage); case DEEP_SEEK -> deepSeekChatAssistant.chat(sessionIdentifier, userMessage);

View File

@@ -0,0 +1,61 @@
package com.zl.mjga.service;
import com.zl.mjga.dto.PageRequestDto;
import com.zl.mjga.dto.PageResponseDto;
import com.zl.mjga.dto.aoplog.AopLogQueryDto;
import com.zl.mjga.dto.aoplog.AopLogRespDto;
import com.zl.mjga.repository.AopLogRepository;
import java.time.OffsetDateTime;
import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.jooq.Record;
import org.jooq.Result;
import org.jooq.SelectConditionStep;
import org.jooq.generated.mjga.tables.pojos.AopLog;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@Slf4j
@RequiredArgsConstructor
public class AopLogService {
private final AopLogRepository aopLogRepository;
@Async
public void saveLogAsync(AopLog aopLog) {
try {
aopLogRepository.insert(aopLog);
} catch (Exception e) {
log.error("Failed to save AOP log asynchronously", e);
}
}
public PageResponseDto<List<AopLogRespDto>> pageQueryAopLogs(
PageRequestDto pageRequestDto, AopLogQueryDto queryDto) {
Result<Record> records = aopLogRepository.pageFetchBy(pageRequestDto, queryDto);
if (records.isEmpty()) {
return PageResponseDto.empty();
}
List<AopLogRespDto> aopLogs = records.map((record -> record.into(AopLogRespDto.class)));
Long totalCount = records.get(0).getValue("total_count", Long.class);
return new PageResponseDto<>(totalCount, aopLogs);
}
public AopLogRespDto getAopLogById(Long id) {
AopLogQueryDto queryDto = new AopLogQueryDto();
queryDto.setId(id);
SelectConditionStep<Record> selectStep = aopLogRepository.selectBy(queryDto);
return selectStep.fetchOneInto(AopLogRespDto.class);
}
@Transactional
public int deleteLogsBeforeTime(OffsetDateTime beforeTime) {
return aopLogRepository.deleteBeforeTime(beforeTime);
}
}

View File

@@ -1,73 +0,0 @@
package com.zl.mjga.service;
import static dev.langchain4j.store.embedding.filter.MetadataFilterBuilder.metadataKey;
import com.zl.mjga.config.ai.ZhiPuEmbeddingModelConfig;
import com.zl.mjga.model.urp.Actions;
import dev.langchain4j.data.document.Metadata;
import dev.langchain4j.data.embedding.Embedding;
import dev.langchain4j.data.segment.TextSegment;
import dev.langchain4j.model.embedding.EmbeddingModel;
import dev.langchain4j.store.embedding.EmbeddingSearchRequest;
import dev.langchain4j.store.embedding.EmbeddingSearchResult;
import dev.langchain4j.store.embedding.EmbeddingStore;
import dev.langchain4j.store.embedding.filter.Filter;
import jakarta.annotation.PostConstruct;
import java.util.HashMap;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Service;
@Configuration
@RequiredArgsConstructor
@Service
public class EmbeddingService {
private final EmbeddingModel zhipuEmbeddingModel;
private final EmbeddingStore<TextSegment> zhiPuEmbeddingStore;
private final ZhiPuEmbeddingModelConfig zhiPuEmbeddingModelConfig;
public Map<String, String> searchAction(String message) {
Map<String, String> result = new HashMap<>();
EmbeddingSearchRequest embeddingSearchRequest =
EmbeddingSearchRequest.builder()
.queryEmbedding(zhipuEmbeddingModel.embed(message).content())
.minScore(0.89)
.build();
EmbeddingSearchResult<TextSegment> embeddingSearchResult =
zhiPuEmbeddingStore.search(embeddingSearchRequest);
if (!embeddingSearchResult.matches().isEmpty()) {
Metadata metadata = embeddingSearchResult.matches().getFirst().embedded().metadata();
result.put(Actions.INDEX_KEY, metadata.getString(Actions.INDEX_KEY));
}
return result;
}
@PostConstruct
public void initActionIndex() {
if (!zhiPuEmbeddingModelConfig.getEnable()) {
return;
}
for (Actions action : Actions.values()) {
Embedding queryEmbedding = zhipuEmbeddingModel.embed(action.getContent()).content();
Filter createUserFilter = metadataKey(Actions.INDEX_KEY).isEqualTo(action.getCode());
EmbeddingSearchRequest embeddingSearchRequest =
EmbeddingSearchRequest.builder()
.queryEmbedding(queryEmbedding)
.filter(createUserFilter)
.build();
EmbeddingSearchResult<TextSegment> embeddingSearchResult =
zhiPuEmbeddingStore.search(embeddingSearchRequest);
if (embeddingSearchResult.matches().isEmpty()) {
TextSegment segment =
TextSegment.from(
action.getContent(), Metadata.metadata(Actions.INDEX_KEY, action.getCode()));
Embedding embedding = zhipuEmbeddingModel.embed(segment).content();
zhiPuEmbeddingStore.add(embedding, segment);
}
}
}
}

View File

@@ -0,0 +1,181 @@
package com.zl.mjga.service;
import static dev.langchain4j.store.embedding.filter.MetadataFilterBuilder.metadataKey;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.zl.mjga.config.ai.ZhiPuEmbeddingModelConfig;
import com.zl.mjga.config.minio.MinIoConfig;
import com.zl.mjga.model.urp.Actions;
import com.zl.mjga.repository.LibraryDocRepository;
import com.zl.mjga.repository.LibraryRepository;
import dev.langchain4j.data.document.Document;
import dev.langchain4j.data.document.Metadata;
import dev.langchain4j.data.document.loader.amazon.s3.AmazonS3DocumentLoader;
import dev.langchain4j.data.document.parser.apache.tika.ApacheTikaDocumentParser;
import dev.langchain4j.data.document.splitter.DocumentByParagraphSplitter;
import dev.langchain4j.data.embedding.Embedding;
import dev.langchain4j.data.segment.TextSegment;
import dev.langchain4j.model.embedding.EmbeddingModel;
import dev.langchain4j.model.output.Response;
import dev.langchain4j.store.embedding.*;
import dev.langchain4j.store.embedding.filter.Filter;
import jakarta.annotation.PostConstruct;
import java.time.Instant;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.RandomStringUtils;
import org.jooq.JSON;
import org.jooq.generated.mjga.enums.LibraryDocStatusEnum;
import org.jooq.generated.mjga.tables.daos.LibraryDocSegmentDao;
import org.jooq.generated.mjga.tables.pojos.LibraryDoc;
import org.jooq.generated.mjga.tables.pojos.LibraryDocSegment;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.Async;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
@Configuration
@RequiredArgsConstructor
@Service
@Slf4j
public class RagService {
private final EmbeddingModel zhipuEmbeddingModel;
private final EmbeddingStore<TextSegment> zhiPuEmbeddingStore;
private final EmbeddingStore<TextSegment> zhiPuLibraryEmbeddingStore;
private final ZhiPuEmbeddingModelConfig zhiPuEmbeddingModelConfig;
private final AmazonS3DocumentLoader amazonS3DocumentLoader;
private final MinIoConfig minIoConfig;
private final LibraryRepository libraryRepository;
private final LibraryDocRepository libraryDocRepository;
private final LibraryDocSegmentDao libraryDocSegmentDao;
public void deleteLibraryBy(Long libraryId) {
List<LibraryDoc> libraryDocs = libraryDocRepository.fetchByLibId(libraryId);
List<Long> docIds = libraryDocs.stream().map(LibraryDoc::getId).toList();
for (Long docId : docIds) {
deleteDocBy(docId);
}
libraryRepository.deleteById(libraryId);
}
public void deleteDocBy(Long docId) {
List<LibraryDocSegment> libraryDocSegments = libraryDocSegmentDao.fetchByDocId(docId);
List<String> embeddingIdList =
libraryDocSegments.stream().map(LibraryDocSegment::getEmbeddingId).toList();
if (CollectionUtils.isNotEmpty(embeddingIdList)) {
zhiPuLibraryEmbeddingStore.removeAll(embeddingIdList);
}
libraryDocRepository.deleteById(docId);
}
public Long createLibraryDocBy(Long libraryId, String objectName, String originalName)
throws JsonProcessingException {
String username = SecurityContextHolder.getContext().getAuthentication().getName();
String identify =
String.format(
"%d%s_%s",
Instant.now().toEpochMilli(),
RandomStringUtils.insecure().nextAlphabetic(6),
originalName);
Map<String, String> meta = new HashMap<>();
meta.put("uploader", username);
LibraryDoc libraryDoc = new LibraryDoc();
ObjectMapper objectMapper = new ObjectMapper();
String metaJson = objectMapper.writeValueAsString(meta);
libraryDoc.setMeta(JSON.valueOf(metaJson));
libraryDoc.setPath(objectName);
libraryDoc.setName(originalName);
libraryDoc.setIdentify(identify);
libraryDoc.setLibId(libraryId);
libraryDoc.setStatus(LibraryDocStatusEnum.INDEXING);
libraryDoc.setEnable(Boolean.TRUE);
libraryDocRepository.insert(libraryDoc);
return libraryDocRepository.fetchOneByIdentify(identify).getId();
}
@Async
public void embeddingAndCreateDocSegment(Long libraryId, Long libraryDocId, String objectName) {
Document document =
amazonS3DocumentLoader.loadDocument(
minIoConfig.getDefaultBucket(), objectName, new ApacheTikaDocumentParser());
List<LibraryDocSegment> libraryDocSegments = new ArrayList<>();
DocumentByParagraphSplitter documentByParagraphSplitter =
new DocumentByParagraphSplitter(500, 150);
documentByParagraphSplitter
.split(document)
.forEach(
textSegment -> {
Response<Embedding> embed = zhipuEmbeddingModel.embed(textSegment);
Integer tokenUsage = embed.tokenUsage().totalTokenCount();
Embedding vector = embed.content();
textSegment.metadata().put("libraryId", libraryId);
String embeddingId = zhiPuLibraryEmbeddingStore.add(vector, textSegment);
LibraryDocSegment libraryDocSegment = new LibraryDocSegment();
libraryDocSegment.setEmbeddingId(embeddingId);
libraryDocSegment.setContent(textSegment.text());
libraryDocSegment.setTokenUsage(tokenUsage);
libraryDocSegment.setDocId(libraryDocId);
libraryDocSegments.add(libraryDocSegment);
});
libraryDocSegmentDao.insert(libraryDocSegments);
LibraryDoc libraryDoc = libraryDocRepository.fetchOneById(libraryDocId);
libraryDoc.setStatus(LibraryDocStatusEnum.SUCCESS);
libraryDocRepository.update(libraryDoc);
}
public Map<String, String> searchAction(String message) {
Map<String, String> result = new HashMap<>();
EmbeddingSearchRequest embeddingSearchRequest =
EmbeddingSearchRequest.builder()
.queryEmbedding(zhipuEmbeddingModel.embed(message).content())
.minScore(0.89)
.build();
EmbeddingSearchResult<TextSegment> embeddingSearchResult =
zhiPuEmbeddingStore.search(embeddingSearchRequest);
if (!embeddingSearchResult.matches().isEmpty()) {
Metadata metadata = embeddingSearchResult.matches().getFirst().embedded().metadata();
result.put(Actions.INDEX_KEY, metadata.getString(Actions.INDEX_KEY));
}
return result;
}
@PostConstruct
public void initActionIndex() {
if (!zhiPuEmbeddingModelConfig.getEnable()) {
return;
}
for (Actions action : Actions.values()) {
Embedding queryEmbedding = zhipuEmbeddingModel.embed(action.getContent()).content();
Filter createUserFilter = metadataKey(Actions.INDEX_KEY).isEqualTo(action.getCode());
EmbeddingSearchRequest embeddingSearchRequest =
EmbeddingSearchRequest.builder()
.queryEmbedding(queryEmbedding)
.filter(createUserFilter)
.build();
EmbeddingSearchResult<TextSegment> embeddingSearchResult =
zhiPuEmbeddingStore.search(embeddingSearchRequest);
if (embeddingSearchResult.matches().isEmpty()) {
TextSegment segment =
TextSegment.from(
action.getContent(), Metadata.metadata(Actions.INDEX_KEY, action.getCode()));
Embedding embedding = zhipuEmbeddingModel.embed(segment).content();
zhiPuEmbeddingStore.add(embedding, segment);
}
}
}
}

View File

@@ -0,0 +1,81 @@
package com.zl.mjga.service;
import com.zl.mjga.config.minio.MinIoConfig;
import com.zl.mjga.exception.BusinessException;
import io.minio.*;
import java.awt.image.BufferedImage;
import java.time.Instant;
import javax.imageio.ImageIO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
@Service
@RequiredArgsConstructor
@Slf4j
public class UploadService {
private final MinioClient minioClient;
private final MinIoConfig minIoConfig;
public String uploadAvatarFile(MultipartFile multipartFile) throws Exception {
String originalFilename = multipartFile.getOriginalFilename();
if (StringUtils.isEmpty(originalFilename)) {
throw new BusinessException("文件名不能为空");
}
String contentType = multipartFile.getContentType();
String extension = "";
if ("image/jpeg".equals(contentType)) {
extension = ".jpg";
} else if ("image/png".equals(contentType)) {
extension = ".png";
}
String objectName =
String.format(
"/library/%d%s%s",
Instant.now().toEpochMilli(),
RandomStringUtils.insecure().nextAlphabetic(6),
extension);
if (multipartFile.isEmpty()) {
throw new BusinessException("上传的文件不能为空");
}
long size = multipartFile.getSize();
if (size > 200 * 1024) {
throw new BusinessException("头像大小不能超过200KB");
}
BufferedImage img = ImageIO.read(multipartFile.getInputStream());
if (img == null) {
throw new BusinessException("非法的上传文件");
}
minioClient.putObject(
PutObjectArgs.builder().bucket(minIoConfig.getDefaultBucket()).object(objectName).stream(
multipartFile.getInputStream(), size, -1)
.contentType(multipartFile.getContentType())
.build());
return objectName;
}
public String uploadLibraryDoc(MultipartFile multipartFile) throws Exception {
String originalFilename = multipartFile.getOriginalFilename();
if (StringUtils.isEmpty(originalFilename)) {
throw new BusinessException("文件名不能为空");
}
String objectName = String.format("/library/%s", originalFilename);
if (multipartFile.isEmpty()) {
throw new BusinessException("上传的文件不能为空");
}
long size = multipartFile.getSize();
if (size > 1024 * 1024) {
throw new BusinessException("知识库文档大小不能超过1MB");
}
minioClient.putObject(
PutObjectArgs.builder().bucket(minIoConfig.getDefaultBucket()).object(objectName).stream(
multipartFile.getInputStream(), size, -1)
.contentType(multipartFile.getContentType())
.build());
return objectName;
}
}

View File

@@ -42,3 +42,6 @@ minio:
access-key: ${MINIO_ROOT_USER} access-key: ${MINIO_ROOT_USER}
secret-key: ${MINIO_ROOT_PASSWORD} secret-key: ${MINIO_ROOT_PASSWORD}
default-bucket: ${MINIO_DEFAULT_BUCKETS} default-bucket: ${MINIO_DEFAULT_BUCKETS}
aop:
logging:
enabled: true

View File

@@ -4,7 +4,7 @@ CREATE TABLE mjga.user (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
username VARCHAR NOT NULL UNIQUE, username VARCHAR NOT NULL UNIQUE,
avatar VARCHAR, avatar VARCHAR,
create_time TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, create_time TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
password VARCHAR NOT NULL, password VARCHAR NOT NULL,
enable BOOLEAN NOT NULL DEFAULT TRUE enable BOOLEAN NOT NULL DEFAULT TRUE
); );
@@ -39,7 +39,7 @@ CREATE TABLE mjga.user_role_map (
CREATE TABLE mjga.department ( CREATE TABLE mjga.department (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL UNIQUE, name VARCHAR NOT NULL UNIQUE,
parent_id BIGINT, parent_id BIGINT,
FOREIGN KEY (parent_id) FOREIGN KEY (parent_id)
REFERENCES mjga.department(id) REFERENCES mjga.department(id)
@@ -56,7 +56,7 @@ CREATE TABLE mjga.user_department_map (
CREATE TABLE mjga.position ( CREATE TABLE mjga.position (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL UNIQUE name VARCHAR NOT NULL UNIQUE
); );
CREATE TABLE mjga.user_position_map ( CREATE TABLE mjga.user_position_map (
@@ -80,12 +80,12 @@ CREATE TYPE "llm_type_enum" AS ENUM (
CREATE TABLE mjga.ai_llm_config ( CREATE TABLE mjga.ai_llm_config (
id BIGSERIAL NOT NULL UNIQUE, id BIGSERIAL NOT NULL UNIQUE,
name VARCHAR(255) NOT NULL UNIQUE, name VARCHAR NOT NULL UNIQUE,
code mjga.llm_code_enum NOT NULL UNIQUE, code mjga.llm_code_enum NOT NULL UNIQUE,
model_name VARCHAR(255) NOT NULL, model_name VARCHAR NOT NULL,
type LLM_TYPE_ENUM NOT NULL, type LLM_TYPE_ENUM NOT NULL,
api_key VARCHAR(255) NOT NULL, api_key VARCHAR NOT NULL,
url VARCHAR(255) NOT NULL, url VARCHAR NOT NULL,
enable BOOLEAN NOT NULL DEFAULT true, enable BOOLEAN NOT NULL DEFAULT true,
priority SMALLINT NOT NULL DEFAULT 0, priority SMALLINT NOT NULL DEFAULT 0,
PRIMARY KEY(id) PRIMARY KEY(id)

View File

@@ -0,0 +1,34 @@
CREATE TABLE mjga.library (
id BIGSERIAL PRIMARY KEY,
name VARCHAR NOT NULL UNIQUE,
description VARCHAR,
create_time TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TYPE mjga.library_doc_status_enum AS ENUM (
'SUCCESS',
'INDEXING'
);
CREATE TABLE mjga.library_doc (
id BIGSERIAL PRIMARY KEY,
lib_id BIGINT NOT NULL,
name VARCHAR NOT NULL,
identify VARCHAR NOT NULL UNIQUE,
path VARCHAR NOT NULL,
meta JSON NOT NULL,
enable BOOLEAN NOT NULL DEFAULT true,
status mjga.library_doc_status_enum NOT NULL,
create_time TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
update_time TIMESTAMPTZ,
FOREIGN KEY (lib_id) REFERENCES mjga.library (id) ON DELETE CASCADE
);
CREATE TABLE mjga.library_doc_segment (
id BIGSERIAL PRIMARY KEY,
doc_id BIGINT NOT NULL,
embedding_id VARCHAR NOT NULL UNIQUE,
content TEXT,
token_usage INTEGER NOT NULL DEFAULT 0,
FOREIGN KEY (doc_id) REFERENCES mjga.library_doc (id) ON DELETE CASCADE
);

View File

@@ -0,0 +1,22 @@
CREATE TABLE mjga.aop_log (
id BIGSERIAL PRIMARY KEY,
class_name VARCHAR NOT NULL,
method_name VARCHAR NOT NULL,
method_args VARCHAR,
return_value VARCHAR,
execution_time BIGINT NOT NULL,
success BOOLEAN NOT NULL DEFAULT TRUE,
error_message VARCHAR,
user_id BIGINT,
ip_address VARCHAR,
user_agent VARCHAR,
curl VARCHAR,
create_time TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES mjga.user(id) ON DELETE SET NULL
);
CREATE INDEX idx_aop_log_class_name ON mjga.aop_log(class_name);
CREATE INDEX idx_aop_log_method_name ON mjga.aop_log(method_name);
CREATE INDEX idx_aop_log_create_time ON mjga.aop_log(create_time);
CREATE INDEX idx_aop_log_user_id ON mjga.aop_log(user_id);
CREATE INDEX idx_aop_log_success ON mjga.aop_log(success);

View File

@@ -0,0 +1,483 @@
package com.zl.mjga.integration.aspect;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.zl.mjga.annotation.SkipAopLog;
import com.zl.mjga.aspect.LoggingAspect;
import com.zl.mjga.repository.UserRepository;
import com.zl.mjga.service.AopLogService;
import jakarta.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.reflect.MethodSignature;
import org.jooq.generated.mjga.tables.pojos.AopLog;
import org.jooq.generated.mjga.tables.pojos.User;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT)
class LoggingAspectTest {
@Mock private AopLogService aopLogService;
@Mock private ObjectMapper objectMapper;
@Mock private UserRepository userRepository;
@Mock private ProceedingJoinPoint joinPoint;
@Mock private MethodSignature methodSignature;
@Mock private SecurityContext securityContext;
@Mock private Authentication authentication;
@Mock private ServletRequestAttributes servletRequestAttributes;
@Mock private HttpServletRequest httpServletRequest;
@InjectMocks LoggingAspect loggingAspect;
@BeforeEach
void setUp() {
SecurityContextHolder.setContext(securityContext);
}
@AfterEach
void tearDown() {
SecurityContextHolder.clearContext();
}
@Test
void logController_givenSuccessfulExecution_shouldSaveSuccessLog() throws Throwable {
// arrange
TestController target = new TestController();
Object[] args = {"arg1", "arg2"};
String expectedResult = "success";
User mockUser = createMockUser(123L, "testUser");
setupAuthenticatedUser("testUser", mockUser);
setupJoinPoint(target, "testMethod", args, expectedResult);
setupSerialization("[\"arg1\",\"arg2\"]", "\"success\"");
try (MockedStatic<RequestContextHolder> mockedRequestContextHolder =
setupRequestContext("192.168.1.1", "Test-Agent")) {
// action
Object result = loggingAspect.logController(joinPoint);
// assert
assertThat(result).isEqualTo(expectedResult);
verifyLogSaved(
log -> {
assertThat(log.getClassName()).isEqualTo("TestController");
assertThat(log.getMethodName()).isEqualTo("testMethod");
assertThat(log.getMethodArgs()).isEqualTo("[\"arg1\",\"arg2\"]");
assertThat(log.getReturnValue()).isEqualTo("\"success\"");
assertThat(log.getSuccess()).isTrue();
assertThat(log.getUserId()).isEqualTo(123L);
assertThat(log.getIpAddress()).isEqualTo("192.168.1.1");
assertThat(log.getUserAgent()).isEqualTo("Test-Agent");
assertThat(log.getExecutionTime()).isGreaterThanOrEqualTo(0L);
});
}
}
@Test
void logController_givenFailedExecution_shouldSaveFailLog() throws Throwable {
// arrange
TestController target = new TestController();
Object[] args = {"arg1"};
RuntimeException exception = new RuntimeException("Test error");
User mockUser = createMockUser(123L, "testUser");
setupAuthenticatedUser("testUser", mockUser);
setupJoinPoint(target, "failMethod", args, exception);
setupSerialization("[\"arg1\"]", null);
try (MockedStatic<RequestContextHolder> mockedRequestContextHolder =
setupRequestContext("192.168.1.1", "Test-Agent")) {
// action & assert
assertThatThrownBy(() -> loggingAspect.logController(joinPoint))
.isInstanceOf(RuntimeException.class)
.hasMessage("Test error");
verifyLogSaved(
log -> {
assertThat(log.getClassName()).isEqualTo("TestController");
assertThat(log.getMethodName()).isEqualTo("failMethod");
assertThat(log.getSuccess()).isFalse();
assertThat(log.getErrorMessage()).isEqualTo("Test error");
assertThat(log.getReturnValue()).isNull();
assertThat(log.getUserId()).isEqualTo(123L);
});
}
}
@Test
void logService_givenSuccessfulExecution_shouldSaveSuccessLogWithoutRequestInfo()
throws Throwable {
// arrange
TestService target = new TestService();
Object[] args = {"serviceArg"};
String expectedResult = "serviceResult";
User mockUser = createMockUser(123L, "testUser");
setupAuthenticatedUser("testUser", mockUser);
setupJoinPoint(target, "serviceMethod", args, expectedResult);
setupSerialization("[\"serviceArg\"]", "\"serviceResult\"");
// action
Object result = loggingAspect.logService(joinPoint);
// assert
assertThat(result).isEqualTo(expectedResult);
verifyLogSaved(
log -> {
assertThat(log.getClassName()).isEqualTo("TestService");
assertThat(log.getMethodName()).isEqualTo("serviceMethod");
assertThat(log.getSuccess()).isTrue();
assertThat(log.getUserId()).isEqualTo(123L);
assertThat(log.getIpAddress()).isNull();
assertThat(log.getUserAgent()).isNull();
});
}
@Test
void logRepository_givenSuccessfulExecution_shouldSaveSuccessLogWithoutRequestInfo()
throws Throwable {
// arrange
TestRepository target = new TestRepository();
Object[] args = {1L};
Object expectedResult = new Object();
User mockUser = createMockUser(123L, "testUser");
setupAuthenticatedUser("testUser", mockUser);
setupJoinPoint(target, "findById", args, expectedResult);
setupSerialization("[1]", "{}");
// action
Object result = loggingAspect.logRepository(joinPoint);
// assert
assertThat(result).isEqualTo(expectedResult);
verifyLogSaved(
log -> {
assertThat(log.getClassName()).isEqualTo("TestRepository");
assertThat(log.getMethodName()).isEqualTo("findById");
assertThat(log.getSuccess()).isTrue();
assertThat(log.getUserId()).isEqualTo(123L);
assertThat(log.getIpAddress()).isNull();
assertThat(log.getUserAgent()).isNull();
});
}
@Test
void logController_givenUnauthenticatedUser_shouldNotLog() throws Throwable {
// arrange
TestController target = new TestController();
String expectedResult = "success";
Method testMethod = TestController.class.getMethod("testMethod");
when(joinPoint.getTarget()).thenReturn(target);
when(joinPoint.proceed()).thenReturn(expectedResult);
when(joinPoint.getSignature()).thenReturn(methodSignature);
when(methodSignature.getMethod()).thenReturn(testMethod);
// Mock SecurityContextHolder to return null authentication
when(securityContext.getAuthentication()).thenReturn(null);
try (MockedStatic<RequestContextHolder> mockedRequestContextHolder =
mockStatic(RequestContextHolder.class)) {
mockedRequestContextHolder.when(RequestContextHolder::getRequestAttributes).thenReturn(null);
// action
Object result = loggingAspect.logController(joinPoint);
// assert
assertThat(result).isEqualTo(expectedResult);
verify(aopLogService, never()).saveLogAsync(any());
}
}
@Test
void logController_givenAnonymousUser_shouldNotLog() throws Throwable {
// arrange
TestController target = new TestController();
String expectedResult = "success";
Method testMethod = TestController.class.getMethod("testMethod");
when(authentication.isAuthenticated()).thenReturn(true);
when(authentication.getName()).thenReturn("anonymousUser");
when(joinPoint.getTarget()).thenReturn(target);
when(joinPoint.proceed()).thenReturn(expectedResult);
when(joinPoint.getSignature()).thenReturn(methodSignature);
when(methodSignature.getMethod()).thenReturn(testMethod);
// Mock SecurityContextHolder to return anonymous authentication
when(securityContext.getAuthentication()).thenReturn(authentication);
try (MockedStatic<RequestContextHolder> mockedRequestContextHolder =
mockStatic(RequestContextHolder.class)) {
mockedRequestContextHolder.when(RequestContextHolder::getRequestAttributes).thenReturn(null);
// action
Object result = loggingAspect.logController(joinPoint);
// assert
assertThat(result).isEqualTo(expectedResult);
verify(aopLogService, never()).saveLogAsync(any());
}
}
@Test
void logController_givenSkipAopLogAnnotation_shouldNotLog() throws Throwable {
// arrange
TestController target = new TestController();
String expectedResult = "success";
when(joinPoint.getTarget()).thenReturn(target);
when(joinPoint.getSignature()).thenReturn(methodSignature);
when(methodSignature.getMethod()).thenReturn(getSkipLogMethod());
when(joinPoint.proceed()).thenReturn(expectedResult);
// action
Object result = loggingAspect.logController(joinPoint);
// assert
assertThat(result).isEqualTo(expectedResult);
verify(aopLogService, never()).saveLogAsync(any());
}
@Test
void logController_givenNullArgs_shouldHandleGracefully() throws Throwable {
// arrange
TestController target = new TestController();
User mockUser = createMockUser(123L, "testUser");
setupAuthenticatedUser("testUser", mockUser);
setupJoinPoint(target, "noArgsMethod", null, "result");
setupSerialization(null, "\"result\"");
try (MockedStatic<RequestContextHolder> mockedRequestContextHolder =
setupRequestContext("127.0.0.1", "Test-Agent")) {
// action
Object result = loggingAspect.logController(joinPoint);
// assert
assertThat(result).isEqualTo("result");
verifyLogSaved(
log -> {
assertThat(log.getMethodArgs()).isNull();
assertThat(log.getSuccess()).isTrue();
});
}
}
@Test
void logController_givenEmptyArgs_shouldHandleGracefully() throws Throwable {
// arrange
TestController target = new TestController();
Object[] emptyArgs = {};
User mockUser = createMockUser(123L, "testUser");
setupAuthenticatedUser("testUser", mockUser);
setupJoinPoint(target, "noArgsMethod", emptyArgs, "result");
setupSerialization(null, "\"result\"");
try (MockedStatic<RequestContextHolder> mockedRequestContextHolder =
setupRequestContext("127.0.0.1", "Test-Agent")) {
// action
Object result = loggingAspect.logController(joinPoint);
// assert
assertThat(result).isEqualTo("result");
verifyLogSaved(
log -> {
assertThat(log.getMethodArgs()).isNull();
assertThat(log.getSuccess()).isTrue();
});
}
}
@Test
void logController_givenSerializationError_shouldHandleGracefully() throws Throwable {
// arrange
TestController target = new TestController();
Object[] args = {"arg1"};
String expectedResult = "success";
User mockUser = createMockUser(123L, "testUser");
setupAuthenticatedUser("testUser", mockUser);
setupJoinPoint(target, "testMethod", args, expectedResult);
// Mock serialization error
when(objectMapper.writeValueAsString(args))
.thenThrow(new JsonProcessingException("Serialization failed") {});
when(objectMapper.writeValueAsString(expectedResult)).thenReturn("\"success\"");
try (MockedStatic<RequestContextHolder> mockedRequestContextHolder =
setupRequestContext("127.0.0.1", "Test-Agent")) {
// action
Object result = loggingAspect.logController(joinPoint);
// assert
assertThat(result).isEqualTo(expectedResult);
verifyLogSaved(
log -> {
assertThat(log.getMethodArgs()).isEqualTo("Serialization failed");
assertThat(log.getSuccess()).isTrue();
});
}
}
// Helper methods
private User createMockUser(Long id, String username) {
User user = new User();
user.setId(id);
user.setUsername(username);
return user;
}
private void setupAuthenticatedUser(String username, User user) {
when(securityContext.getAuthentication()).thenReturn(authentication);
when(authentication.isAuthenticated()).thenReturn(true);
when(authentication.getName()).thenReturn(username);
when(authentication.getPrincipal()).thenReturn(username);
when(userRepository.fetchOneByUsername(username)).thenReturn(user);
}
private void setupJoinPoint(Object target, String methodName, Object[] args, Object result)
throws Throwable {
when(joinPoint.getTarget()).thenReturn(target);
when(joinPoint.getSignature()).thenReturn(methodSignature);
when(methodSignature.getName()).thenReturn(methodName);
when(methodSignature.getMethod()).thenReturn(getTestMethod());
when(joinPoint.getArgs()).thenReturn(args);
if (result instanceof Throwable) {
when(joinPoint.proceed()).thenThrow((Throwable) result);
} else {
when(joinPoint.proceed()).thenReturn(result);
}
}
private void setupSerialization(String argsJson, String resultJson)
throws JsonProcessingException {
if (argsJson != null) {
when(objectMapper.writeValueAsString(any(Object[].class))).thenReturn(argsJson);
}
if (resultJson != null) {
when(objectMapper.writeValueAsString(argThat(arg -> !(arg instanceof Object[]))))
.thenReturn(resultJson);
}
}
private MockedStatic<RequestContextHolder> setupRequestContext(
String ipAddress, String userAgent) {
MockedStatic<RequestContextHolder> mockedRequestContextHolder =
mockStatic(RequestContextHolder.class);
mockedRequestContextHolder
.when(RequestContextHolder::getRequestAttributes)
.thenReturn(servletRequestAttributes);
when(servletRequestAttributes.getRequest()).thenReturn(httpServletRequest);
when(httpServletRequest.getHeader("X-Forwarded-For")).thenReturn(ipAddress);
when(httpServletRequest.getHeader("User-Agent")).thenReturn(userAgent);
when(httpServletRequest.getRemoteAddr()).thenReturn("127.0.0.1");
return mockedRequestContextHolder;
}
private void verifyLogSaved(java.util.function.Consumer<AopLog> logVerifier) {
ArgumentCaptor<AopLog> logCaptor = ArgumentCaptor.forClass(AopLog.class);
verify(aopLogService, times(1)).saveLogAsync(logCaptor.capture());
logVerifier.accept(logCaptor.getValue());
}
private java.lang.reflect.Method getTestMethod() throws NoSuchMethodException {
return TestController.class.getMethod("testMethod");
}
private java.lang.reflect.Method getSkipLogMethod() throws NoSuchMethodException {
return TestController.class.getMethod("skipLogMethod");
}
@Test
void logController_givenHttpRequest_shouldGenerateCurlCommand() throws Throwable {
// arrange
TestController target = new TestController();
Object[] args = {"arg1"};
String expectedResult = "success";
User mockUser = createMockUser(123L, "testUser");
setupAuthenticatedUser("testUser", mockUser);
setupJoinPoint(target, "testMethod", args, expectedResult);
setupSerialization("[\"arg1\"]", "\"success\"");
// Setup HTTP request mocks before setupRequestContext
when(httpServletRequest.getMethod()).thenReturn("POST");
when(httpServletRequest.getScheme()).thenReturn("http");
when(httpServletRequest.getServerName()).thenReturn("localhost");
when(httpServletRequest.getServerPort()).thenReturn(8080);
when(httpServletRequest.getRequestURI()).thenReturn("/api/test");
when(httpServletRequest.getQueryString()).thenReturn("param1=value1");
when(httpServletRequest.getContentType()).thenReturn("application/json");
when(httpServletRequest.getHeaderNames())
.thenReturn(
java.util.Collections.enumeration(
java.util.Arrays.asList("Content-Type", "Authorization")));
when(httpServletRequest.getHeader("Content-Type")).thenReturn("application/json");
when(httpServletRequest.getHeader("Authorization")).thenReturn("Bearer token123");
try (MockedStatic<RequestContextHolder> mockedRequestContextHolder =
setupRequestContext("127.0.0.1", "Test-Agent")) {
// action
Object result = loggingAspect.logController(joinPoint);
// assert
assertThat(result).isEqualTo(expectedResult);
verifyLogSaved(
log -> {
assertThat(log.getCurl()).isNotNull();
assertThat(log.getCurl()).contains("curl -X POST");
assertThat(log.getCurl()).contains("'http://localhost:8080/api/test?param1=value1'");
assertThat(log.getCurl()).contains("-H 'Content-Type: application/json'");
assertThat(log.getCurl()).contains("-H 'Authorization: Bearer token123'");
});
}
}
// Test classes for mocking
private static class TestController {
public String testMethod() {
return "test";
}
@SkipAopLog(reason = "测试跳过日志记录")
public String skipLogMethod() {
return "test";
}
}
private static class TestService {
public String testMethod() {
return "test";
}
}
private static class TestRepository {
public String testMethod() {
return "test";
}
}
}

View File

@@ -0,0 +1,187 @@
package com.zl.mjga.integration.mvc;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.zl.mjga.config.minio.MinIoConfig;
import com.zl.mjga.config.security.HttpFireWallConfig;
import com.zl.mjga.controller.AopLogController;
import com.zl.mjga.dto.PageRequestDto;
import com.zl.mjga.dto.PageResponseDto;
import com.zl.mjga.dto.aoplog.AopLogQueryDto;
import com.zl.mjga.dto.aoplog.AopLogRespDto;
import com.zl.mjga.repository.AopLogRepository;
import com.zl.mjga.repository.PermissionRepository;
import com.zl.mjga.repository.RoleRepository;
import com.zl.mjga.repository.UserRepository;
import com.zl.mjga.service.AopLogService;
import io.minio.MinioClient;
import java.time.OffsetDateTime;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Import;
import org.springframework.http.MediaType;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.web.servlet.MockMvc;
@WebMvcTest(value = {AopLogController.class})
@Import({HttpFireWallConfig.class})
public class AopLogControllerTest {
@Autowired private MockMvc mockMvc;
@Autowired private ObjectMapper objectMapper;
@MockBean private AopLogService aopLogService;
@MockBean private AopLogRepository aopLogRepository;
@MockBean private UserRepository userRepository;
@MockBean private RoleRepository roleRepository;
@MockBean private PermissionRepository permissionRepository;
@MockBean private MinioClient minioClient;
@MockBean private MinIoConfig minIoConfig;
@Test
@WithMockUser(authorities = "READ_USER_ROLE_PERMISSION")
void pageQueryAopLogs_givenValidRequest_shouldReturnOk() throws Exception {
// arrange
PageResponseDto<List<AopLogRespDto>> mockResponse =
new PageResponseDto<>(1L, List.of(createTestAopLogRespDto()));
when(aopLogService.pageQueryAopLogs(any(PageRequestDto.class), any(AopLogQueryDto.class)))
.thenReturn(mockResponse);
// action & assert
mockMvc
.perform(
get("/aop-log/page-query")
.param("page", "1")
.param("size", "10")
.param("className", "TestController"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.total").value(1))
.andExpect(jsonPath("$.data").isArray())
.andExpect(jsonPath("$.data[0].className").value("TestController"));
}
@Test
void pageQueryAopLogs_givenNoAuth_shouldReturnUnauthorized() throws Exception {
// action & assert
mockMvc
.perform(get("/aop-log/page-query").param("page", "1").param("size", "10"))
.andExpect(status().isUnauthorized());
}
@Test
@WithMockUser(authorities = "READ_USER_ROLE_PERMISSION")
void getAopLogById_givenValidId_shouldReturnOk() throws Exception {
// arrange
Long id = 1L;
AopLogRespDto mockResponse = createTestAopLogRespDto();
when(aopLogService.getAopLogById(id)).thenReturn(mockResponse);
// action & assert
mockMvc
.perform(get("/aop-log/{id}", id))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").value(1))
.andExpect(jsonPath("$.className").value("TestController"))
.andExpect(jsonPath("$.methodName").value("testMethod"));
}
@Test
@WithMockUser(authorities = "READ_USER_ROLE_PERMISSION")
void getAopLogById_givenNonExistingId_shouldReturnOkWithNull() throws Exception {
// arrange
Long id = 999L;
when(aopLogService.getAopLogById(id)).thenReturn(null);
// action & assert
mockMvc
.perform(get("/aop-log/{id}", id))
.andExpect(status().isOk())
.andExpect(content().string(""));
}
@Test
@WithMockUser(authorities = "WRITE_USER_ROLE_PERMISSION")
void deleteAopLogs_givenValidIds_shouldReturnOk() throws Exception {
// arrange
List<Long> ids = List.of(1L, 2L, 3L);
when(aopLogRepository.deleteByIds(ids)).thenReturn(3);
// action & assert
mockMvc
.perform(
delete("/aop-log/batch")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(ids))
.with(csrf()))
.andExpect(status().isOk())
.andExpect(content().string("3"));
}
@Test
void deleteAopLogs_givenNoAuth_shouldReturnUnauthorized() throws Exception {
// arrange
List<Long> ids = List.of(1L, 2L, 3L);
// action & assert
mockMvc
.perform(
delete("/aop-log/batch")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(ids))
.with(csrf()))
.andExpect(status().isUnauthorized());
}
@Test
@WithMockUser(authorities = "WRITE_USER_ROLE_PERMISSION")
void deleteAopLog_givenValidId_shouldReturnOk() throws Exception {
// arrange
Long id = 1L;
when(aopLogRepository.deleteByIds(List.of(id))).thenReturn(1);
// action & assert
mockMvc.perform(delete("/aop-log/{id}", id).with(csrf())).andExpect(status().isOk());
}
@Test
@WithMockUser(authorities = "WRITE_USER_ROLE_PERMISSION")
void deleteLogsBeforeTime_givenValidTime_shouldReturnOk() throws Exception {
// arrange
OffsetDateTime beforeTime = OffsetDateTime.now().minusDays(7);
when(aopLogService.deleteLogsBeforeTime(beforeTime)).thenReturn(5);
// action & assert
mockMvc
.perform(delete("/aop-log/before").param("beforeTime", beforeTime.toString()).with(csrf()))
.andExpect(status().isOk())
.andExpect(content().string("5"));
}
private AopLogRespDto createTestAopLogRespDto() {
return AopLogRespDto.builder()
.id(1L)
.className("TestController")
.methodName("testMethod")
.methodArgs("[\"arg1\"]")
.returnValue("\"result\"")
.executionTime(100L)
.success(true)
.userId(1L)
.username("testUser")
.ipAddress("127.0.0.1")
.userAgent("Test Agent")
.curl("curl -X GET 'http://localhost:8080/test' -H 'Content-Type: application/json'")
.createTime(OffsetDateTime.now())
.build();
}
}

View File

@@ -16,6 +16,7 @@ import com.zl.mjga.repository.PermissionRepository;
import com.zl.mjga.repository.RoleRepository; import com.zl.mjga.repository.RoleRepository;
import com.zl.mjga.repository.UserRepository; import com.zl.mjga.repository.UserRepository;
import com.zl.mjga.service.IdentityAccessService; import com.zl.mjga.service.IdentityAccessService;
import com.zl.mjga.service.UploadService;
import io.minio.MinioClient; import io.minio.MinioClient;
import java.util.List; import java.util.List;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@@ -38,6 +39,7 @@ public class JacksonAnnotationMvcTest {
@MockBean private PermissionRepository permissionRepository; @MockBean private PermissionRepository permissionRepository;
@MockBean private MinioClient minioClient; @MockBean private MinioClient minioClient;
@MockBean private MinIoConfig minIoConfig; @MockBean private MinIoConfig minIoConfig;
@MockBean private UploadService uploadService;
@Test @Test
@WithMockUser @WithMockUser

View File

@@ -17,6 +17,7 @@ import com.zl.mjga.repository.PermissionRepository;
import com.zl.mjga.repository.RoleRepository; import com.zl.mjga.repository.RoleRepository;
import com.zl.mjga.repository.UserRepository; import com.zl.mjga.repository.UserRepository;
import com.zl.mjga.service.IdentityAccessService; import com.zl.mjga.service.IdentityAccessService;
import com.zl.mjga.service.UploadService;
import io.minio.MinioClient; import io.minio.MinioClient;
import java.util.List; import java.util.List;
import org.jooq.generated.mjga.tables.pojos.User; import org.jooq.generated.mjga.tables.pojos.User;
@@ -40,6 +41,7 @@ class UserRolePermissionMvcTest {
@MockBean private PermissionRepository permissionRepository; @MockBean private PermissionRepository permissionRepository;
@MockBean private MinioClient minioClient; @MockBean private MinioClient minioClient;
@MockBean private MinIoConfig minIoConfig; @MockBean private MinIoConfig minIoConfig;
@MockBean private UploadService uploadService;
@Test @Test
@WithMockUser @WithMockUser

View File

@@ -0,0 +1,192 @@
package com.zl.mjga.integration.persistence;
import static org.assertj.core.api.Assertions.assertThat;
import com.zl.mjga.dto.PageRequestDto;
import com.zl.mjga.dto.aoplog.AopLogQueryDto;
import com.zl.mjga.repository.AopLogRepository;
import java.time.OffsetDateTime;
import java.util.List;
import org.jooq.Record;
import org.jooq.Result;
import org.jooq.generated.mjga.tables.pojos.AopLog;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.jdbc.Sql;
public class AopLogRepositoryTest extends AbstractDataAccessLayerTest {
@Autowired private AopLogRepository aopLogRepository;
@Test
@Sql(
statements = {
"INSERT INTO mjga.user (id, username, password) VALUES (1, 'testUser', 'password')",
"INSERT INTO mjga.aop_log (id, class_name, method_name, method_args, return_value,"
+ " execution_time, success, user_id, ip_address) VALUES (1, 'TestController',"
+ " 'testMethod', '[\"arg1\"]', '\"result\"', 100, true, 1, '127.0.0.1')",
"INSERT INTO mjga.aop_log (id, class_name, method_name, method_args, return_value,"
+ " execution_time, success, error_message) VALUES (2, 'TestService', 'failMethod',"
+ " '[\"arg2\"]', null, 200, false, 'Test error')",
"INSERT INTO mjga.aop_log (id, class_name, method_name, execution_time, success) VALUES (3,"
+ " 'TestRepository', 'queryMethod', 50, true)"
})
void pageFetchBy_givenValidQuery_shouldReturnCorrectResults() {
// arrange
AopLogQueryDto queryDto = new AopLogQueryDto();
queryDto.setClassName("Test");
PageRequestDto pageRequestDto = PageRequestDto.of(1, 10);
// action
Result<Record> result = aopLogRepository.pageFetchBy(pageRequestDto, queryDto);
// assert
assertThat(result).hasSize(3);
assertThat(result.get(0).getValue("total_count", Long.class)).isEqualTo(3L);
}
@Test
@Sql(
statements = {
"INSERT INTO mjga.aop_log (id, class_name, method_name, execution_time, success) VALUES (1,"
+ " 'TestController', 'method1', 100, true)",
"INSERT INTO mjga.aop_log (id, class_name, method_name, execution_time, success) VALUES (2,"
+ " 'TestService', 'method2', 200, false)",
"INSERT INTO mjga.aop_log (id, class_name, method_name, execution_time, success) VALUES (3,"
+ " 'TestRepository', 'method3', 50, true)"
})
void fetchBy_givenClassNameQuery_shouldReturnFilteredResults() {
// arrange
AopLogQueryDto queryDto = new AopLogQueryDto();
queryDto.setClassName("TestController");
// action
List<AopLog> result = aopLogRepository.fetchBy(queryDto);
// assert
assertThat(result).hasSize(1);
assertThat(result.get(0).getClassName()).isEqualTo("TestController");
assertThat(result.get(0).getMethodName()).isEqualTo("method1");
}
@Test
@Sql(
statements = {
"INSERT INTO mjga.aop_log (id, class_name, method_name, execution_time, success) VALUES (1,"
+ " 'TestController', 'method1', 100, true)",
"INSERT INTO mjga.aop_log (id, class_name, method_name, execution_time, success) VALUES (2,"
+ " 'TestService', 'method2', 200, false)"
})
void fetchBy_givenSuccessQuery_shouldReturnOnlySuccessfulLogs() {
// arrange
AopLogQueryDto queryDto = new AopLogQueryDto();
queryDto.setSuccess(true);
// action
List<AopLog> result = aopLogRepository.fetchBy(queryDto);
// assert
assertThat(result).hasSize(1);
assertThat(result.get(0).getSuccess()).isTrue();
}
@Test
@Sql(
statements = {
"INSERT INTO mjga.aop_log (id, class_name, method_name, execution_time, success) VALUES (1,"
+ " 'TestController', 'method1', 50, true)",
"INSERT INTO mjga.aop_log (id, class_name, method_name, execution_time, success) VALUES (2,"
+ " 'TestService', 'method2', 150, false)",
"INSERT INTO mjga.aop_log (id, class_name, method_name, execution_time, success) VALUES (3,"
+ " 'TestRepository', 'method3', 250, true)"
})
void fetchBy_givenExecutionTimeRange_shouldReturnFilteredResults() {
// arrange
AopLogQueryDto queryDto = new AopLogQueryDto();
queryDto.setMinExecutionTime(100L);
queryDto.setMaxExecutionTime(200L);
// action
List<AopLog> result = aopLogRepository.fetchBy(queryDto);
// assert
assertThat(result).hasSize(1);
assertThat(result.get(0).getExecutionTime()).isEqualTo(150L);
}
@Test
@Sql(
statements = {
"INSERT INTO mjga.aop_log (id, class_name, method_name, execution_time, success) VALUES (1,"
+ " 'TestController', 'method1', 100, true)",
"INSERT INTO mjga.aop_log (id, class_name, method_name, execution_time, success) VALUES (2,"
+ " 'TestService', 'method2', 200, false)",
"INSERT INTO mjga.aop_log (id, class_name, method_name, execution_time, success) VALUES (3,"
+ " 'TestRepository', 'method3', 50, true)"
})
void deleteByIds_givenValidIds_shouldDeleteCorrectRecords() {
// arrange
List<Long> idsToDelete = List.of(1L, 3L);
// action
int deletedCount = aopLogRepository.deleteByIds(idsToDelete);
// assert
assertThat(deletedCount).isEqualTo(2);
// verify remaining record
List<AopLog> remaining = aopLogRepository.findAll();
assertThat(remaining).hasSize(1);
assertThat(remaining.get(0).getId()).isEqualTo(2L);
}
@Test
@Sql(
statements = {
"INSERT INTO mjga.aop_log (id, class_name, method_name, execution_time, success,"
+ " create_time) VALUES (1, 'TestController', 'method1', 100, true, '2023-01-01"
+ " 00:00:00+00')",
"INSERT INTO mjga.aop_log (id, class_name, method_name, execution_time, success,"
+ " create_time) VALUES (2, 'TestService', 'method2', 200, false, '2023-06-01"
+ " 00:00:00+00')",
"INSERT INTO mjga.aop_log (id, class_name, method_name, execution_time, success,"
+ " create_time) VALUES (3, 'TestRepository', 'method3', 50, true, '2023-12-01"
+ " 00:00:00+00')"
})
void deleteBeforeTime_givenValidTime_shouldDeleteOldRecords() {
// arrange
OffsetDateTime cutoffTime = OffsetDateTime.parse("2023-07-01T00:00:00Z");
// action
int deletedCount = aopLogRepository.deleteBeforeTime(cutoffTime);
// assert
assertThat(deletedCount).isEqualTo(2);
// verify remaining record
List<AopLog> remaining = aopLogRepository.findAll();
assertThat(remaining).hasSize(1);
assertThat(remaining.get(0).getId()).isEqualTo(3L);
}
@Test
void deleteByIds_givenEmptyList_shouldReturnZero() {
// arrange
List<Long> emptyIds = List.of();
// action
int deletedCount = aopLogRepository.deleteByIds(emptyIds);
// assert
assertThat(deletedCount).isEqualTo(0);
}
@Test
void deleteByIds_givenNullList_shouldReturnZero() {
// arrange & action
int deletedCount = aopLogRepository.deleteByIds(null);
// assert
assertThat(deletedCount).isEqualTo(0);
}
}

View File

@@ -20,6 +20,7 @@ import com.zl.mjga.repository.RoleRepository;
import com.zl.mjga.repository.UserRepository; import com.zl.mjga.repository.UserRepository;
import com.zl.mjga.service.IdentityAccessService; import com.zl.mjga.service.IdentityAccessService;
import com.zl.mjga.service.SignService; import com.zl.mjga.service.SignService;
import com.zl.mjga.service.UploadService;
import io.minio.MinioClient; import io.minio.MinioClient;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import java.util.Collections; import java.util.Collections;
@@ -53,6 +54,7 @@ public class AuthenticationAndAuthorityTest {
@MockBean private PermissionRepository permissionRepository; @MockBean private PermissionRepository permissionRepository;
@MockBean private MinioClient minioClient; @MockBean private MinioClient minioClient;
@MockBean private MinIoConfig minIoConfig; @MockBean private MinIoConfig minIoConfig;
@MockBean private UploadService uploadService;
@Test @Test
public void givenRequestOnPublicService_shouldSucceedWith200() throws Exception { public void givenRequestOnPublicService_shouldSucceedWith200() throws Exception {

View File

@@ -0,0 +1,134 @@
package com.zl.mjga.unit;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
import com.zl.mjga.dto.PageRequestDto;
import com.zl.mjga.dto.PageResponseDto;
import com.zl.mjga.dto.aoplog.AopLogQueryDto;
import com.zl.mjga.dto.aoplog.AopLogRespDto;
import com.zl.mjga.repository.AopLogRepository;
import com.zl.mjga.service.AopLogService;
import java.time.OffsetDateTime;
import java.util.List;
import org.jooq.Record;
import org.jooq.Result;
import org.jooq.generated.mjga.tables.pojos.AopLog;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
@ExtendWith(MockitoExtension.class)
public class AopLogServiceTest {
@Mock private AopLogRepository aopLogRepository;
@Mock private Result<Record> mockResult;
@Mock private Record mockRecord;
@InjectMocks private AopLogService aopLogService;
@Test
void saveLogAsync_givenValidAopLog_shouldCallRepositoryInsert() {
// arrange
AopLog aopLog = createTestAopLog();
// action
aopLogService.saveLogAsync(aopLog);
// assert
verify(aopLogRepository, times(1)).insert(aopLog);
}
@Test
void pageQueryAopLogs_givenValidRequest_shouldReturnPageResponse() {
// arrange
PageRequestDto pageRequestDto = PageRequestDto.of(1, 10);
AopLogQueryDto queryDto = new AopLogQueryDto();
when(aopLogRepository.pageFetchBy(pageRequestDto, queryDto)).thenReturn(mockResult);
when(mockResult.isEmpty()).thenReturn(false);
when(mockResult.map(any())).thenReturn(List.of(createTestAopLogRespDto()));
when(mockResult.get(0)).thenReturn(mockRecord);
when(mockRecord.getValue("total_count", Long.class)).thenReturn(1L);
// action
PageResponseDto<List<AopLogRespDto>> result =
aopLogService.pageQueryAopLogs(pageRequestDto, queryDto);
// assert
assertThat(result).isNotNull();
assertThat(result.getTotal()).isEqualTo(1L);
assertThat(result.getData()).hasSize(1);
verify(aopLogRepository, times(1)).pageFetchBy(pageRequestDto, queryDto);
}
@Test
void pageQueryAopLogs_givenEmptyResult_shouldReturnEmptyPage() {
// arrange
PageRequestDto pageRequestDto = PageRequestDto.of(1, 10);
AopLogQueryDto queryDto = new AopLogQueryDto();
when(aopLogRepository.pageFetchBy(pageRequestDto, queryDto)).thenReturn(mockResult);
when(mockResult.isEmpty()).thenReturn(true);
// action
PageResponseDto<List<AopLogRespDto>> result =
aopLogService.pageQueryAopLogs(pageRequestDto, queryDto);
// assert
assertThat(result).isNotNull();
assertThat(result.getTotal()).isEqualTo(0L);
assertThat(result.getData()).isNull();
}
@Test
void deleteLogsBeforeTime_givenValidTime_shouldReturnDeletedCount() {
// arrange
OffsetDateTime beforeTime = OffsetDateTime.now().minusDays(30);
when(aopLogRepository.deleteBeforeTime(beforeTime)).thenReturn(10);
// action
int result = aopLogService.deleteLogsBeforeTime(beforeTime);
// assert
assertThat(result).isEqualTo(10);
verify(aopLogRepository, times(1)).deleteBeforeTime(beforeTime);
}
private AopLog createTestAopLog() {
AopLog aopLog = new AopLog();
aopLog.setClassName("TestController");
aopLog.setMethodName("testMethod");
aopLog.setMethodArgs("[\"arg1\"]");
aopLog.setReturnValue("\"result\"");
aopLog.setExecutionTime(100L);
aopLog.setSuccess(true);
aopLog.setUserId(1L);
aopLog.setIpAddress("127.0.0.1");
aopLog.setUserAgent("Test Agent");
aopLog.setCreateTime(OffsetDateTime.now());
return aopLog;
}
private AopLogRespDto createTestAopLogRespDto() {
return AopLogRespDto.builder()
.id(1L)
.className("TestController")
.methodName("testMethod")
.methodArgs("[\"arg1\"]")
.returnValue("\"result\"")
.executionTime(100L)
.success(true)
.userId(1L)
.username("testUser")
.ipAddress("127.0.0.1")
.userAgent("Test Agent")
.createTime(OffsetDateTime.now())
.build();
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -4,7 +4,7 @@ CREATE TABLE mjga.user (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
username VARCHAR NOT NULL UNIQUE, username VARCHAR NOT NULL UNIQUE,
avatar VARCHAR, avatar VARCHAR,
create_time TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, create_time TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
password VARCHAR NOT NULL, password VARCHAR NOT NULL,
enable BOOLEAN NOT NULL DEFAULT TRUE enable BOOLEAN NOT NULL DEFAULT TRUE
); );
@@ -39,7 +39,7 @@ CREATE TABLE mjga.user_role_map (
CREATE TABLE mjga.department ( CREATE TABLE mjga.department (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL UNIQUE, name VARCHAR NOT NULL UNIQUE,
parent_id BIGINT, parent_id BIGINT,
FOREIGN KEY (parent_id) FOREIGN KEY (parent_id)
REFERENCES mjga.department(id) REFERENCES mjga.department(id)
@@ -56,7 +56,7 @@ CREATE TABLE mjga.user_department_map (
CREATE TABLE mjga.position ( CREATE TABLE mjga.position (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL UNIQUE name VARCHAR NOT NULL UNIQUE
); );
CREATE TABLE mjga.user_position_map ( CREATE TABLE mjga.user_position_map (
@@ -80,12 +80,12 @@ CREATE TYPE "llm_type_enum" AS ENUM (
CREATE TABLE mjga.ai_llm_config ( CREATE TABLE mjga.ai_llm_config (
id BIGSERIAL NOT NULL UNIQUE, id BIGSERIAL NOT NULL UNIQUE,
name VARCHAR(255) NOT NULL UNIQUE, name VARCHAR NOT NULL UNIQUE,
code mjga.llm_code_enum NOT NULL UNIQUE, code mjga.llm_code_enum NOT NULL UNIQUE,
model_name VARCHAR(255) NOT NULL, model_name VARCHAR NOT NULL,
type LLM_TYPE_ENUM NOT NULL, type LLM_TYPE_ENUM NOT NULL,
api_key VARCHAR(255) NOT NULL, api_key VARCHAR NOT NULL,
url VARCHAR(255) NOT NULL, url VARCHAR NOT NULL,
enable BOOLEAN NOT NULL DEFAULT true, enable BOOLEAN NOT NULL DEFAULT true,
priority SMALLINT NOT NULL DEFAULT 0, priority SMALLINT NOT NULL DEFAULT 0,
PRIMARY KEY(id) PRIMARY KEY(id)

View File

@@ -0,0 +1,28 @@
CREATE TABLE mjga.library (
id BIGSERIAL PRIMARY KEY,
name VARCHAR NOT NULL UNIQUE,
data_count INTEGER NOT NULL DEFAULT 0,
create_time TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE mjga.library_doc (
id BIGSERIAL PRIMARY KEY,
lib_id BIGINT NOT NULL,
name VARCHAR NOT NULL,
identify VARCHAR NOT NULL UNIQUE,
path VARCHAR NOT NULL,
meta JSON NOT NULL,
enable BOOLEAN NOT NULL DEFAULT true,
create_time TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
update_time TIMESTAMPTZ,
FOREIGN KEY (lib_id) REFERENCES mjga.library (id) ON DELETE CASCADE
);
CREATE TABLE mjga.library_doc_segment (
id BIGSERIAL PRIMARY KEY,
doc_id BIGINT NOT NULL,
embedding_id VARCHAR NOT NULL UNIQUE,
content TEXT,
token_usage INTEGER NOT NULL DEFAULT 0,
FOREIGN KEY (doc_id) REFERENCES mjga.library_doc (id) ON DELETE CASCADE
);

View File

@@ -0,0 +1,22 @@
CREATE TABLE mjga.aop_log (
id BIGSERIAL PRIMARY KEY,
class_name VARCHAR NOT NULL,
method_name VARCHAR NOT NULL,
method_args VARCHAR,
return_value VARCHAR,
execution_time BIGINT NOT NULL,
success BOOLEAN NOT NULL DEFAULT TRUE,
error_message VARCHAR,
user_id BIGINT,
ip_address VARCHAR,
user_agent VARCHAR,
curl VARCHAR,
create_time TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES mjga.user(id) ON DELETE SET NULL
);
CREATE INDEX idx_aop_log_class_name ON mjga.aop_log(class_name);
CREATE INDEX idx_aop_log_method_name ON mjga.aop_log(method_name);
CREATE INDEX idx_aop_log_create_time ON mjga.aop_log(create_time);
CREATE INDEX idx_aop_log_user_id ON mjga.aop_log(user_id);
CREATE INDEX idx_aop_log_success ON mjga.aop_log(success);

3
frontend/.gitignore vendored
View File

@@ -18,6 +18,7 @@ coverage
/cypress/screenshots/ /cypress/screenshots/
# Editor directories and files # Editor directories and files
.vscode
.vscode/* .vscode/*
!.vscode/extensions.json !.vscode/extensions.json
.idea .idea
@@ -186,3 +187,5 @@ compose.yaml
Dockerfile Dockerfile
Caddyfile Caddyfile
start.sh start.sh
.cursor

View File

@@ -1,7 +0,0 @@
{
"recommendations": [
"Vue.volar",
"vitest.explorer",
"ms-playwright.playwright"
]
}

View File

@@ -1,18 +0,0 @@
{
"explorer.fileNesting.enabled": true,
"explorer.fileNesting.patterns": {
"tsconfig.json": "tsconfig.*.json, env.d.ts",
"vite.config.*": "jsconfig*, vitest.config.*, cypress.config.*, playwright.config.*",
"package.json": "package-lock.json, pnpm*, .yarnrc*, yarn*, .eslint*, eslint*, .prettier*, prettier*, .editorconfig"
},
"files.associations": {
"*.css": "tailwindcss"
},
"editor.quickSuggestions": {
"strings": "on"
},
"tailwindCSS.classAttributes": ["class", "ui"],
"tailwindCSS.experimental.classRegex": [
["ui:\\s*{([^)]*)\\s*}", "(?:'|\"|`)([^']*)(?:'|\"|`)"]
]
}

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<link rel="icon" href="/java.svg"> <link rel="icon" href="/java.svg">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>知路后台管理</title> <title>知路 AI 后台管理</title>
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>

View File

@@ -21,7 +21,7 @@
"marked": "^15.0.12", "marked": "^15.0.12",
"openapi-fetch": "^0.13.5", "openapi-fetch": "^0.13.5",
"pinia": "^3.0.1", "pinia": "^3.0.1",
"tailwindcss": "^4.0.14", "tailwindcss": "^4.1.11",
"vue": "^3.5.13", "vue": "^3.5.13",
"vue-router": "^4.5.0", "vue-router": "^4.5.0",
"zod": "^3.24.2" "zod": "^3.24.2"
@@ -1832,6 +1832,12 @@
"tailwindcss": "4.1.6" "tailwindcss": "4.1.6"
} }
}, },
"node_modules/@tailwindcss/node/node_modules/tailwindcss": {
"version": "4.1.6",
"resolved": "http://mirrors.tencent.com/npm/tailwindcss/-/tailwindcss-4.1.6.tgz",
"integrity": "sha512-j0cGLTreM6u4OWzBeLBpycK0WIh8w7kSwcUsQZoGLHZ7xDTdM69lN64AgoIEEwFi0tnhs4wSykUa5YWxAzgFYg==",
"license": "MIT"
},
"node_modules/@tailwindcss/oxide": { "node_modules/@tailwindcss/oxide": {
"version": "4.1.6", "version": "4.1.6",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.6.tgz", "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.6.tgz",
@@ -2079,6 +2085,12 @@
"vite": "^5.2.0 || ^6" "vite": "^5.2.0 || ^6"
} }
}, },
"node_modules/@tailwindcss/vite/node_modules/tailwindcss": {
"version": "4.1.6",
"resolved": "http://mirrors.tencent.com/npm/tailwindcss/-/tailwindcss-4.1.6.tgz",
"integrity": "sha512-j0cGLTreM6u4OWzBeLBpycK0WIh8w7kSwcUsQZoGLHZ7xDTdM69lN64AgoIEEwFi0tnhs4wSykUa5YWxAzgFYg==",
"license": "MIT"
},
"node_modules/@testing-library/dom": { "node_modules/@testing-library/dom": {
"version": "10.4.0", "version": "10.4.0",
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz",
@@ -3655,7 +3667,7 @@
}, },
"node_modules/flowbite": { "node_modules/flowbite": {
"version": "3.1.2", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/flowbite/-/flowbite-3.1.2.tgz", "resolved": "http://mirrors.tencent.com/npm/flowbite/-/flowbite-3.1.2.tgz",
"integrity": "sha512-MkwSgbbybCYgMC+go6Da5idEKUFfMqc/AmSjm/2ZbdmvoKf5frLPq/eIhXc9P+rC8t9boZtUXzHDgt5whZ6A/Q==", "integrity": "sha512-MkwSgbbybCYgMC+go6Da5idEKUFfMqc/AmSjm/2ZbdmvoKf5frLPq/eIhXc9P+rC8t9boZtUXzHDgt5whZ6A/Q==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -5705,9 +5717,9 @@
} }
}, },
"node_modules/tailwindcss": { "node_modules/tailwindcss": {
"version": "4.1.6", "version": "4.1.11",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.6.tgz", "resolved": "http://mirrors.tencent.com/npm/tailwindcss/-/tailwindcss-4.1.11.tgz",
"integrity": "sha512-j0cGLTreM6u4OWzBeLBpycK0WIh8w7kSwcUsQZoGLHZ7xDTdM69lN64AgoIEEwFi0tnhs4wSykUa5YWxAzgFYg==", "integrity": "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/tapable": { "node_modules/tapable": {

View File

@@ -31,7 +31,7 @@
"marked": "^15.0.12", "marked": "^15.0.12",
"openapi-fetch": "^0.13.5", "openapi-fetch": "^0.13.5",
"pinia": "^3.0.1", "pinia": "^3.0.1",
"tailwindcss": "^4.0.14", "tailwindcss": "^4.1.11",
"vue": "^3.5.13", "vue": "^3.5.13",
"vue-router": "^4.5.0", "vue-router": "^4.5.0",
"zod": "^3.24.2" "zod": "^3.24.2"

Some files were not shown because too many files have changed in this diff Show More