mirror of
https://github.com/ccmjga/zhilu-admin
synced 2026-04-05 11:47:32 +00:00
Compare commits
35 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
96de9a215f | ||
|
|
59631cea3d | ||
|
|
9425c2c88d | ||
|
|
38556f3417 | ||
|
|
f0a237fdf3 | ||
|
|
13d240342c | ||
|
|
8e20f561a4 | ||
|
|
b759275cf3 | ||
|
|
5f0f0fbd14 | ||
|
|
7088712560 | ||
|
|
2c302315b1 | ||
|
|
87c4706ca7 | ||
|
|
deece30554 | ||
|
|
eecc8bedae | ||
|
|
0a0174c01e | ||
|
|
36d285a61d | ||
|
|
5c685d4f74 | ||
|
|
1bd50f5de2 | ||
|
|
e646bdffa8 | ||
|
|
a767613b58 | ||
|
|
152f0fe07c | ||
|
|
b6ecc929b0 | ||
|
|
3e1d7e6fee | ||
|
|
f9c8e3808b | ||
|
|
56d6a992f8 | ||
|
|
6ec07686a9 | ||
|
|
4d70b49e61 | ||
|
|
8ed0b795f3 | ||
|
|
2fb08968ee | ||
|
|
2f7259ca9d | ||
|
|
19090b9c94 | ||
|
|
cbfbd6c5dd | ||
|
|
09f51fa91f | ||
|
|
5494181ae0 | ||
|
|
8d285e1abc |
14
README.md
14
README.md
@@ -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 国内镜像仓库
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
[](https://qm.qq.com/q/9mvVC57jPO)
|
[](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 @@
|
|||||||
[](https://github.com/ccmjga)
|
[](https://github.com/ccmjga)
|
||||||
|
|
||||||
[](https://qm.qq.com/q/9mvVC57jPO)
|
[](https://qm.qq.com/q/9mvVC57jPO)
|
||||||
|
|
||||||
## 💌 微信打赏
|
|
||||||
|
|
||||||
知路管理后台的发展离不开您的支持;再次对所有支持本项目的人们致以诚挚的谢意~
|
|
||||||
|
|
||||||

|
|
||||||
|
|||||||
BIN
assets/group.png
BIN
assets/group.png
Binary file not shown.
|
Before Width: | Height: | Size: 575 KiB After Width: | Height: | Size: 580 KiB |
@@ -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"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|
||||||
|
|||||||
15
backend/src/main/java/com/zl/mjga/annotation/SkipAopLog.java
Normal file
15
backend/src/main/java/com/zl/mjga/annotation/SkipAopLog.java
Normal 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 "";
|
||||||
|
}
|
||||||
312
backend/src/main/java/com/zl/mjga/aspect/LoggingAspect.java
Normal file
312
backend/src/main/java/com/zl/mjga/aspect/LoggingAspect.java
Normal 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("'");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
35
backend/src/main/java/com/zl/mjga/config/JacksonConfig.java
Normal file
35
backend/src/main/java/com/zl/mjga/config/JacksonConfig.java
Normal 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)")
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
|||||||
7
backend/src/main/java/com/zl/mjga/dto/ai/ChatDto.java
Normal file
7
backend/src/main/java/com/zl/mjga/dto/ai/ChatDto.java
Normal 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) {}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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) {}
|
||||||
@@ -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) {}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package com.zl.mjga.model.urp;
|
||||||
|
|
||||||
|
public enum ChatMode {
|
||||||
|
NORMAL,
|
||||||
|
WITH_LIBRARY
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
|||||||
61
backend/src/main/java/com/zl/mjga/service/AopLogService.java
Normal file
61
backend/src/main/java/com/zl/mjga/service/AopLogService.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
181
backend/src/main/java/com/zl/mjga/service/RagService.java
Normal file
181
backend/src/main/java/com/zl/mjga/service/RagService.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
81
backend/src/main/java/com/zl/mjga/service/UploadService.java
Normal file
81
backend/src/main/java/com/zl/mjga/service/UploadService.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -41,4 +41,7 @@ minio:
|
|||||||
endpoint: ${MINIO_ENDPOINT}
|
endpoint: ${MINIO_ENDPOINT}
|
||||||
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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
);
|
||||||
@@ -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);
|
||||||
@@ -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";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -53,7 +53,7 @@ public class SignE2ETest {
|
|||||||
.uri("/auth/sign-up")
|
.uri("/auth/sign-up")
|
||||||
.contentType(MediaType.APPLICATION_JSON)
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
.bodyValue(
|
.bodyValue(
|
||||||
"""
|
"""
|
||||||
{
|
{
|
||||||
"username": "test_5fab32c22a3e",
|
"username": "test_5fab32c22a3e",
|
||||||
"password": "test_eab28b939ba1"
|
"password": "test_eab28b939ba1"
|
||||||
@@ -75,7 +75,7 @@ public class SignE2ETest {
|
|||||||
.uri("/auth/sign-in")
|
.uri("/auth/sign-in")
|
||||||
.contentType(MediaType.APPLICATION_JSON)
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
.bodyValue(
|
.bodyValue(
|
||||||
"""
|
"""
|
||||||
{
|
{
|
||||||
"username": "test_5fab32c22a3e",
|
"username": "test_5fab32c22a3e",
|
||||||
"password": "test_eab28b939ba1"
|
"password": "test_eab28b939ba1"
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 {
|
||||||
|
|||||||
134
backend/src/test/java/com/zl/mjga/unit/AopLogServiceTest.java
Normal file
134
backend/src/test/java/com/zl/mjga/unit/AopLogServiceTest.java
Normal 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
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
);
|
||||||
@@ -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
3
frontend/.gitignore
vendored
@@ -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
|
||||||
|
|||||||
7
frontend/.vscode/extensions.json
vendored
7
frontend/.vscode/extensions.json
vendored
@@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"recommendations": [
|
|
||||||
"Vue.volar",
|
|
||||||
"vitest.explorer",
|
|
||||||
"ms-playwright.playwright"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
18
frontend/.vscode/settings.json
vendored
18
frontend/.vscode/settings.json
vendored
@@ -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*}", "(?:'|\"|`)([^']*)(?:'|\"|`)"]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -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>
|
||||||
|
|||||||
22
frontend/package-lock.json
generated
22
frontend/package-lock.json
generated
@@ -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": {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user