diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..549e00a --- /dev/null +++ b/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..3b6f5a0 --- /dev/null +++ b/README.md @@ -0,0 +1,69 @@ +# Getting Started + +> 声明:此项目只发布于 Github,基于 MIT 协议,免费且作为开源学习使用。并且不会有任何形式的卖号等行为,谨防受骗。 + + +![1691582252468](image/README/1691582252468.png) + +![1691583184761](image/README/1691583184761.png) + +![1691583124744](image/README/1691583124744.png) + +![1691583329105](image/README/1691583329105.png) + +该仓库为后端服务,前端项目见[aideepin-web](https://github.com/moyangzhan/aideepin-web) + +### 如何部署 + +#### 初始化 + +* 初始化数据库 + + * 创建数据库aideepin + * 执行docs/create.sql + * 填充openai的secret_key + +``` +update adi_sys_config set value = 'my_chatgpt_secret_key' where name = 'secret_key' +``` + +* 修改配置文件 + + * mysql: application-[dev|prod].xml中的spring.datasource + * redis: application-[dev|prod].xml中的spring.data.redis + * mail: application.xml中的spring.mail + +#### 编译及运行 + +* 进入项目 + +``` +cd aideepin +``` + +* 打包: + +``` +mvn clean package -Dmaven.test.skip=true +``` + +* 运行 + +a. jar包启动: + +``` +cd adi-chat/target +nohup java -jar -Xms768m -Xmx1024m -XX:+HeapDumpOnOutOfMemoryError adi-chat-0.0.1-SNAPSHOT.jar --spring.profiles.active=[dev|prod] dev/null 2>&1 & +``` + +b. docker启动 + +``` +cd adi-chat +docker build . -t aideepin:0.0.1 +docker run -d \ + --name=aideepin \ + -e APP_PROFILE=[dev|prod] \ + -v="/data/aideepin/logs:/data/logs" \ + aideepin:0.0.1 +``` diff --git a/adi-admin/README.md b/adi-admin/README.md new file mode 100644 index 0000000..319a1fa --- /dev/null +++ b/adi-admin/README.md @@ -0,0 +1,4 @@ +Admin site + + +TODO... \ No newline at end of file diff --git a/adi-admin/pom.xml b/adi-admin/pom.xml new file mode 100644 index 0000000..6ac206e --- /dev/null +++ b/adi-admin/pom.xml @@ -0,0 +1,32 @@ + + + 4.0.0 + + com.moyz + aideepin + 0.0.1-SNAPSHOT + + + adi-admin + + + 17 + 17 + UTF-8 + + + + + com.moyz + adi-bootstrap + 0.0.1-SNAPSHOT + + + com.moyz + adi-common + 0.0.1-SNAPSHOT + + + \ No newline at end of file diff --git a/adi-bootstrap/pom.xml b/adi-bootstrap/pom.xml new file mode 100644 index 0000000..b4e9695 --- /dev/null +++ b/adi-bootstrap/pom.xml @@ -0,0 +1,25 @@ + + + 4.0.0 + + com.moyz + aideepin + 0.0.1-SNAPSHOT + + + adi-bootstrap + + + + + src/main/resources + + **/* + + true + + + + \ No newline at end of file diff --git a/adi-bootstrap/src/main/java/com/moyz/adi/BootstrapApplication.java b/adi-bootstrap/src/main/java/com/moyz/adi/BootstrapApplication.java new file mode 100644 index 0000000..6008ece --- /dev/null +++ b/adi-bootstrap/src/main/java/com/moyz/adi/BootstrapApplication.java @@ -0,0 +1,17 @@ +package com.moyz.adi; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.annotation.EnableScheduling; + +@SpringBootApplication +@EnableAsync +@EnableScheduling +public class BootstrapApplication { + + public static void main(String[] args) { + SpringApplication.run(BootstrapApplication.class, args); + } + +} diff --git a/adi-bootstrap/src/main/resources/application-dev.yml b/adi-bootstrap/src/main/resources/application-dev.yml new file mode 100644 index 0000000..a61b950 --- /dev/null +++ b/adi-bootstrap/src/main/resources/application-dev.yml @@ -0,0 +1,42 @@ +spring: + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://localhost:3306/aideepin?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&tinyInt1isBit=false&allowMultiQueries=true + username: root + password: 123456 + data: + redis: + host: localhost + port: 6379 + password: + database: 0 + lettuce: + pool: + #连接池最大连接数 + max-active: 20 + #连接池最大阻塞等待时间 + max-wait: -1 + #连接池中的最大空闲连接 + max-idle: 5 + #连接池中的最小空闲连接 + min-idle: 1 + +logging: + file: + path: D:/data/logs + +openai: + proxy: + enable: true + host: 127.0.0.1 + http-port: 1087 + +adi: + frontend-url: http://localhost:1002 + backend-url: http://localhost:1002/api + + +local: + files: D:/data/files/ + images: D:/data/images/ + tmp_images: D:/data/tmp_images/ \ No newline at end of file diff --git a/adi-bootstrap/src/main/resources/application-prod.yml b/adi-bootstrap/src/main/resources/application-prod.yml new file mode 100644 index 0000000..3eaba77 --- /dev/null +++ b/adi-bootstrap/src/main/resources/application-prod.yml @@ -0,0 +1,22 @@ +spring: + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://localhost:3306/aideepin?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&tinyInt1isBit=false&allowMultiQueries=true + username: your-mysql-account + password: your-mysql-password + data: + redis: + host: localhost + port: 6379 + password: + database: 0 + lettuce: + pool: + #连接池最大连接数 + max-active: 20 + #连接池最大阻塞等待时间 + max-wait: -1 + #连接池中的最大空闲连接 + max-idle: 5 + #连接池中的最小空闲连接 + min-idle: 1 \ No newline at end of file diff --git a/adi-bootstrap/src/main/resources/application.yml b/adi-bootstrap/src/main/resources/application.yml new file mode 100644 index 0000000..7a65237 --- /dev/null +++ b/adi-bootstrap/src/main/resources/application.yml @@ -0,0 +1,60 @@ +server: + port: 9999 + context-path: / + session: + timeout: 28800 + tomcat: + uri-encoding: UTF-8 + +spring: + application: + name: AiDeepIn + profiles: + active: dev + jackson: + date-format: "yyyy-MM-dd HH:mm:ss" + time-zone: "GMT+8" + serialization: { write-dates-as-timestamps: false } + cache: + type: redis + redis: + key-prefix: CACHE + time-to-live: 1d + mail: + default-encoding: UTF-8 + protocol: smtps + host: your-email-host # smtp.exmail.qq.com + username: your-email-username # xxx@qq.com + password: your-email-password + port: 465 + properties: + mail: + smtp: + ssl: + enable: true + servlet: + multipart: + max-file-size: 10MB + max-request-size: 20MB + +springdoc: + swagger-ui: + path: /swagger-ui.html + +mybatis-plus: + # 支持统配符 * 或者 ; 分割 + mapper-locations: classpath*:/mapper/*.xml + configuration: + log-impl: org.apache.ibatis.logging.stdout.StdOutImpl + +logging: + file: /data/logs + +adi: + frontend-url: http://www.aideepin.com + backend-url: http://www.aideepin.com/api + +local: + files: /data/files/ + images: /data/images/ + tmp_images: /data/tmp_images/ \ No newline at end of file diff --git a/adi-bootstrap/src/main/resources/logback.xml b/adi-bootstrap/src/main/resources/logback.xml new file mode 100644 index 0000000..8db09b7 --- /dev/null +++ b/adi-bootstrap/src/main/resources/logback.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + ${CONSOLE_LOG_PATTERN} + UTF-8 + + + + + /data/logs/adi.log + + ${CONSOLE_LOG_PATTERN_NO_COLOR} + UTF-8 + + + + /data/logs/%d{yyyy-MM-dd}.%i.log + + ${LOG_FILEMAXDAY} + + ${LOG_MAXFILESIZE} + + + + INFO + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/adi-chat/Dockerfile b/adi-chat/Dockerfile new file mode 100644 index 0000000..eed310a --- /dev/null +++ b/adi-chat/Dockerfile @@ -0,0 +1,17 @@ +FROM openjdk:17 + +ENV LANG="en_US.UTF-8" \ + LANGUAGE="en_US:en" \ + LC_ALL="en_US.UTF-8" \ + APP_VERSION="1.0.0-SNAPSHOT" \ + TZ="Asia/Shanghai" \ + APP_PROFILE="dev" \ + JAVA_OPTS="-Xms768m -Xmx1024m -XX:+HeapDumpOnOutOfMemoryError" + +RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone + +ADD ./target/adi-chat-0.0.1-SNAPSHOT.jar /data/app/aideepin.jar + +ENTRYPOINT ["sh","-c", "java $JAVA_OPTS -jar /data/app/aideepin.jar --spring.profiles.active=$APP_PROFILE"] + +EXPOSE 9999 \ No newline at end of file diff --git a/adi-chat/README.md b/adi-chat/README.md new file mode 100644 index 0000000..d19fc5c --- /dev/null +++ b/adi-chat/README.md @@ -0,0 +1 @@ +User side \ No newline at end of file diff --git a/adi-chat/pom.xml b/adi-chat/pom.xml new file mode 100644 index 0000000..8fe63ac --- /dev/null +++ b/adi-chat/pom.xml @@ -0,0 +1,64 @@ + + + 4.0.0 + + com.moyz + aideepin + 0.0.1-SNAPSHOT + + + adi-chat + + + + com.moyz + adi-bootstrap + 0.0.1-SNAPSHOT + + + com.moyz + adi-common + 0.0.1-SNAPSHOT + + + + + com.ramostear + Happy-Captcha + system + 1.0.1 + ${project.basedir}/src/lib/Happy-Captcha-1.0.1.jar + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + com.moyz.adi.BootstrapApplication + true + + + org.projectlombok + lombok + + + + + + + build-info + repackage + + + + + + + + \ No newline at end of file diff --git a/adi-chat/src/lib/Happy-Captcha-1.0.1.jar b/adi-chat/src/lib/Happy-Captcha-1.0.1.jar new file mode 100644 index 0000000..2c85cf9 Binary files /dev/null and b/adi-chat/src/lib/Happy-Captcha-1.0.1.jar differ diff --git a/adi-chat/src/main/java/com/moyz/adi/chat/controller/AiImageController.java b/adi-chat/src/main/java/com/moyz/adi/chat/controller/AiImageController.java new file mode 100644 index 0000000..8171435 --- /dev/null +++ b/adi-chat/src/main/java/com/moyz/adi/chat/controller/AiImageController.java @@ -0,0 +1,64 @@ +package com.moyz.adi.chat.controller; + +import com.google.common.collect.Maps; +import com.moyz.adi.common.dto.*; +import com.moyz.adi.common.service.AiImageService; +import io.swagger.v3.oas.annotations.Operation; +import jakarta.annotation.Resource; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.collections4.MapUtils; +import org.hibernate.validator.constraints.Length; +import org.springframework.http.MediaType; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.Map; + +@RestController +@RequestMapping("/ai-image") +@Validated +public class AiImageController { + + @Resource + private AiImageService imageService; + + @PostMapping("/generation") + public Map generation(@RequestBody @Validated GenerateImageReq generateImageReq) { + String uuid = imageService.createByPrompt(generateImageReq); + return Map.of("uuid", uuid); + } + + @PostMapping("/regenerate/{uuid}") + public void regenerate(@PathVariable @Length(min = 32, max = 32) String uuid) { + imageService.regenerate(uuid); + } + + @Operation(summary = "Edit image") + @PostMapping("/edit") + public Map edit(@RequestBody EditImageReq editImageReq) { + String uuid = imageService.editByOriginalImage(editImageReq); + return Map.of("uuid", uuid); + } + + @Operation(summary = "Image variation") + @PostMapping("/variation") + public Map variation(@RequestBody VariationImageReq variationImageReq) { + String uuid = imageService.variationImage(variationImageReq); + return Map.of("uuid", uuid); + } + + @GetMapping("/list") + public AiImagesListResp list(@RequestParam Long maxId, @RequestParam int pageSize) { + return imageService.listAll(maxId, pageSize); + } + + @GetMapping("/detail/{uuid}") + public AiImageDto getOne(@PathVariable String uuid) { + return imageService.getOne(uuid); + } + + @GetMapping("/del/{uuid}") + public boolean del(@PathVariable String uuid) { + return imageService.del(uuid); + } +} diff --git a/adi-chat/src/main/java/com/moyz/adi/chat/controller/AuthController.java b/adi-chat/src/main/java/com/moyz/adi/chat/controller/AuthController.java new file mode 100644 index 0000000..7e5b2e6 --- /dev/null +++ b/adi-chat/src/main/java/com/moyz/adi/chat/controller/AuthController.java @@ -0,0 +1,126 @@ +package com.moyz.adi.chat.controller; + +import com.moyz.adi.common.dto.LoginReq; +import com.moyz.adi.common.dto.LoginResp; +import com.moyz.adi.common.dto.RegisterReq; +import com.moyz.adi.common.service.UserService; +import com.ramostear.captcha.HappyCaptcha; +import com.ramostear.captcha.support.CaptchaType; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.constraints.NotBlank; +import lombok.extern.slf4j.Slf4j; +import org.hibernate.validator.constraints.Length; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.MediaType; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.io.IOException; +import java.net.URLEncoder; + +import static org.springframework.http.HttpHeaders.AUTHORIZATION; + + +@Slf4j +@Tag(name = "权限controller", description = "权限controller") +@Validated +@RestController +@RequestMapping("auth") +public class AuthController { + + @Value("${adi.frontend-url}") + private String frontendUrl; + + @Resource + private UserService userService; + + @Operation(summary = "注册") + @PostMapping(value = "/register", produces = MediaType.TEXT_PLAIN_VALUE) + public String register(@RequestBody RegisterReq registerReq) { + userService.register(registerReq.getEmail(), registerReq.getPassword(), registerReq.getCaptchaId(), registerReq.getCaptchaCode()); + return "激活链接已经发送到邮箱,请登录邮箱进行激活"; + } + + @Operation(summary = "注册的验证码") + @GetMapping("/register/captcha") + public void registerCaptcha(@Parameter(description = "验证码ID") @RequestParam @Length(min = 32) String captchaId, + HttpServletRequest request, + HttpServletResponse response) { + HappyCaptcha happyCaptcha = HappyCaptcha.require(request, response).type(CaptchaType.WORD_NUMBER_UPPER).build().finish(); + String captchaCode = happyCaptcha.getCode(); + userService.cacheRegisterCaptcha(captchaId, captchaCode); + happyCaptcha.output(); + } + + @Operation(summary = "激活") + @GetMapping("active") + public boolean active(@RequestParam("code") String activeCode, HttpServletResponse response) { + + try { + userService.active(activeCode); + response.sendRedirect(frontendUrl + "/#/active?active=success&msg=" + URLEncoder.encode("激活成功,请登录")); + } catch (IOException e) { + log.error("auth.active:", e); + try { + response.sendRedirect(frontendUrl + "/#/active?active=fail&msg=" + URLEncoder.encode("激活失败:系统错误,请重新注册或者登录")); + } catch (IOException ex) { + log.error("auth.active:", ex); + throw new RuntimeException(ex); + } + } catch (Exception e) { + try { + response.sendRedirect(frontendUrl + "/#/active?active=fail&msg=" + URLEncoder.encode(e.getMessage())); + } catch (IOException ex) { + log.error("auth.active:", ex); + throw new RuntimeException(ex); + } + } + return true; + } + + @Operation(summary = "忘记密码") + @PostMapping("password/forgot") + public String forgotPassword(@RequestParam @NotBlank String email) { + userService.forgotPassword(email); + return "重置密码链接已发送"; + } + + + @Operation(summary = "重置密码") + @GetMapping("/password/reset") + public void resetPassword(@RequestParam @NotBlank String code, HttpServletResponse response) { + userService.resetPassword(code); + try { + response.sendRedirect(frontendUrl + "/#/active?active=success&msg=" + URLEncoder.encode("密码已经重置")); + } catch (IOException e) { + log.error("resetPassword:", e); + throw new RuntimeException(e); + } + } + + @Operation(summary = "登录") + @PostMapping("login") + public LoginResp login(@Validated @RequestBody LoginReq loginReq, HttpServletResponse response) { + LoginResp loginResp = userService.login(loginReq); + response.setHeader(AUTHORIZATION, loginResp.getToken()); + Cookie cookie = new Cookie(AUTHORIZATION, loginResp.getToken()); + response.addCookie(cookie); + return loginResp; + } + + @Operation(summary = "获取登录验证码") + @GetMapping("/login/captcha") + public void captcha(@RequestParam @Length(min = 32) String captchaId, HttpServletRequest request, HttpServletResponse response) { + HappyCaptcha happyCaptcha = HappyCaptcha.require(request, response).type(CaptchaType.WORD_NUMBER_UPPER).build().finish(); + String captchaCode = happyCaptcha.getCode(); + userService.cacheLoginCaptcha(captchaId, captchaCode); + happyCaptcha.output(); + } + +} diff --git a/adi-chat/src/main/java/com/moyz/adi/chat/controller/ConversationController.java b/adi-chat/src/main/java/com/moyz/adi/chat/controller/ConversationController.java new file mode 100644 index 0000000..617ac1f --- /dev/null +++ b/adi-chat/src/main/java/com/moyz/adi/chat/controller/ConversationController.java @@ -0,0 +1,54 @@ +package com.moyz.adi.chat.controller; + +import com.moyz.adi.common.dto.ConvDto; +import com.moyz.adi.common.dto.ConvEditReq; +import com.moyz.adi.common.service.ConversationService; +import com.moyz.adi.common.dto.ConvMsgListResp; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + + +/** + * 对话controller + */ +@Tag(name = "对话controller", description = "对话controller") +@RequestMapping("/conversation") +@RestController +public class ConversationController { + + @Resource + private ConversationService conversationService; + + @Operation(summary = "获取当前用户所有的对话") + @GetMapping("/list") + public List list() { + return conversationService.listByUser(); + } + + @Operation(summary = "查询某个对话的信息列表") + @GetMapping("/{uuid}") + public ConvMsgListResp detail( + @Parameter(name = "对话uuid") @PathVariable @NotBlank(message = "对话uuid不能为空") String uuid + , @Parameter(name = "最大uuid") @RequestParam String maxMsgUuid + , @Parameter(name = "每页数量") @RequestParam @Min(1) @Max(100) int pageSize) { + return conversationService.detail(uuid, maxMsgUuid, pageSize); + } + + @PostMapping("/edit/{uuid}") + public boolean edit(@PathVariable String uuid, @RequestBody ConvEditReq convEditReq) { + return conversationService.edit(uuid, convEditReq); + } + + @PostMapping("/del/{uuid}") + public boolean softDel(@PathVariable String uuid) { + return conversationService.softDel(uuid); + } +} diff --git a/adi-chat/src/main/java/com/moyz/adi/chat/controller/ConversationMessageController.java b/adi-chat/src/main/java/com/moyz/adi/chat/controller/ConversationMessageController.java new file mode 100644 index 0000000..429121b --- /dev/null +++ b/adi-chat/src/main/java/com/moyz/adi/chat/controller/ConversationMessageController.java @@ -0,0 +1,31 @@ +package com.moyz.adi.chat.controller; + +import com.moyz.adi.common.dto.AskReq; +import com.moyz.adi.common.service.ConversationMessageService; +import io.swagger.v3.oas.annotations.Operation; +import jakarta.annotation.Resource; +import org.springframework.http.MediaType; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +@RestController +@RequestMapping("/conversation/message") +@Validated +public class ConversationMessageController { + + @Resource + private ConversationMessageService conversationMessageService; + + @Operation(summary = "发送一个prompt给模型") + @PostMapping(value = "/process", produces = MediaType.TEXT_EVENT_STREAM_VALUE) + public SseEmitter ask(@RequestBody @Validated AskReq askReq) { + return conversationMessageService.sseAsk(askReq); + } + + @PostMapping("/del/{uuid}") + public boolean softDelete(@PathVariable String uuid) { + return conversationMessageService.softDelete(uuid); + } + +} diff --git a/adi-chat/src/main/java/com/moyz/adi/chat/controller/FileController.java b/adi-chat/src/main/java/com/moyz/adi/chat/controller/FileController.java new file mode 100644 index 0000000..d148e6e --- /dev/null +++ b/adi-chat/src/main/java/com/moyz/adi/chat/controller/FileController.java @@ -0,0 +1,49 @@ +package com.moyz.adi.chat.controller; + +import com.moyz.adi.common.service.FileService; +import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.hibernate.validator.constraints.Length; +import org.springframework.http.MediaType; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +@Slf4j +@RestController +@Validated +public class FileController { + + @Resource + private FileService fileService; + + @GetMapping(value = "/image/{uuid}", produces = MediaType.IMAGE_PNG_VALUE) + public void image(@Length(min = 32, max = 32) @PathVariable String uuid, HttpServletResponse response) { + BufferedImage bufferedImage = fileService.readBufferedImage(uuid); + //把图片写给浏览器 + try { + ImageIO.write(bufferedImage, "png", response.getOutputStream()); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @PostMapping(path = "/file/upload", headers = "content-type=multipart/form-data", produces = MediaType.APPLICATION_JSON_VALUE) + public Map upload(@RequestPart(value = "file") MultipartFile file) { + Map result = new HashMap<>(); + result.put("uuid", fileService.writeToLocal(file)); + return result; + } + + @PostMapping("/file/del/{uuid}") + public boolean del(@PathVariable String uuid) { + return fileService.softDel(uuid); + } +} diff --git a/adi-chat/src/main/java/com/moyz/adi/chat/controller/PromptController.java b/adi-chat/src/main/java/com/moyz/adi/chat/controller/PromptController.java new file mode 100644 index 0000000..2a1adfa --- /dev/null +++ b/adi-chat/src/main/java/com/moyz/adi/chat/controller/PromptController.java @@ -0,0 +1,75 @@ +package com.moyz.adi.chat.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.moyz.adi.common.base.ThreadContext; +import com.moyz.adi.common.dto.*; +import com.moyz.adi.common.entity.Prompt; +import com.moyz.adi.common.service.PromptService; +import com.moyz.adi.common.util.MPPageUtil; +import io.swagger.v3.oas.annotations.Operation; +import jakarta.annotation.Resource; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; + +@RestController +@RequestMapping("/prompt") +@Validated +public class PromptController { + + @Resource + private PromptService promptService; + + @Operation(summary = "查询列表") + @GetMapping(value = "/my/all") + public List myAll() { + return promptService.getAll(ThreadContext.getCurrentUserId()); + } + + @Operation(summary = "查询列表") + @GetMapping(value = "/my/listByUpdateTime") + public PromptListResp list(@RequestParam(required = false) LocalDateTime minUpdateTime) { + return promptService.listByMinUpdateTime(minUpdateTime); + } + + @Operation(summary = "搜索列表") + @GetMapping(value = "/my/search") + public Page search(String keyword, @NotNull @Min(1) Integer currentPage, @NotNull @Min(10) Integer pageSize) { + return promptService.search(keyword, currentPage, pageSize); + } + + @Operation(summary = "自动填充列表") + @GetMapping(value = "/my/autocomplete") + public List autocomplete(String keyword) { + return promptService.autocomplete(keyword); + } + + @Operation(summary = "保存列表") + @PostMapping(value = "/save") + public Map SavePrompts(@RequestBody PromptsSaveReq savePromptsReq) { + return promptService.savePrompts(savePromptsReq); + } + + @Operation(summary = "删除") + @PostMapping(value = "/del/{id}") + public boolean softDelete(@PathVariable Long id) { + return promptService.softDelete(id); + } + + @Operation(summary = "编辑") + @PostMapping(value = "/edit/{id}") + public boolean edit(@PathVariable Long id, @RequestBody PromptEditReq promptEditReq) { + return promptService.edit(id, promptEditReq.getTitle(), promptEditReq.getRemark()); + } + + @Operation(summary = "search") + @GetMapping(value = "/search") + public List search(@Validated SearchReq searchReq) { + return promptService.search(searchReq.getKeyword()); + } +} diff --git a/adi-chat/src/main/java/com/moyz/adi/chat/controller/UserController.java b/adi-chat/src/main/java/com/moyz/adi/chat/controller/UserController.java new file mode 100644 index 0000000..0a3ab06 --- /dev/null +++ b/adi-chat/src/main/java/com/moyz/adi/chat/controller/UserController.java @@ -0,0 +1,77 @@ +package com.moyz.adi.chat.controller; + +import com.moyz.adi.common.service.UserService; +import com.moyz.adi.common.base.ThreadContext; +import com.moyz.adi.common.dto.ConfigResp; +import com.moyz.adi.common.dto.ModifyPasswordReq; +import com.moyz.adi.common.dto.UserUpdateReq; +import com.moyz.adi.common.entity.User; +import com.talanlabs.avatargenerator.Avatar; +import com.talanlabs.avatargenerator.cat.CatAvatar; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.MediaType; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.IOException; + +@Slf4j +@Tag(name = "用户controller") +@RestController +@RequestMapping("/user") +public class UserController { + + @Resource + private UserService userService; + + @Operation(summary = "用户信息") + @GetMapping("/{uuid}") + public void login(@Validated @PathVariable String uuid) { + log.info(uuid); + } + + @Operation(summary = "配置信息") + @GetMapping("/config") + public ConfigResp configInfo() { + return userService.getConfig(); + } + + @Operation(summary = "更新信息") + @PostMapping("/edit") + public void update(@Validated UserUpdateReq userUpdateReq) { + userService.updateConfig(userUpdateReq); + } + + @Operation(summary = "修改密码") + @PostMapping("/password/modify") + public String modifyPassword(@RequestBody ModifyPasswordReq modifyPasswordReq) { + userService.modifyPassword(modifyPasswordReq.getOldPassword(), modifyPasswordReq.getNewPassword()); + return "修改成功"; + } + + @Operation(summary = "退出") + @PostMapping("/logout") + public void logout() { + userService.logout(); + } + + @Operation(summary = "头像") + @GetMapping(value = "/avatar", produces = MediaType.IMAGE_JPEG_VALUE) + public void avatar(HttpServletResponse response) { + User user = ThreadContext.getCurrentUser(); + Avatar avatar = CatAvatar.newAvatarBuilder().build(); + BufferedImage bufferedImage = avatar.create(user.getId()); + //把图片写给浏览器 + try { + ImageIO.write(bufferedImage, "png", response.getOutputStream()); + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/adi-common/pom.xml b/adi-common/pom.xml new file mode 100644 index 0000000..4db32c1 --- /dev/null +++ b/adi-common/pom.xml @@ -0,0 +1,14 @@ + + + 4.0.0 + + com.moyz + aideepin + 0.0.1-SNAPSHOT + + + adi-common + + \ No newline at end of file diff --git a/adi-common/src/main/java/com/moyz/adi/common/CodeGenerator.java b/adi-common/src/main/java/com/moyz/adi/common/CodeGenerator.java new file mode 100644 index 0000000..8dccb39 --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/CodeGenerator.java @@ -0,0 +1,44 @@ +package com.moyz.adi.common; + +import com.baomidou.mybatisplus.generator.FastAutoGenerator; +import com.baomidou.mybatisplus.generator.config.OutputFile; +import com.baomidou.mybatisplus.generator.config.rules.DbColumnType; +import com.baomidou.mybatisplus.generator.engine.FreemarkerTemplateEngine; + +import java.sql.Types; +import java.util.Collections; + +public class CodeGenerator { + public static void main(String[] args) { + FastAutoGenerator.create("jdbc:mysql://localhost:3306/aideepin?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&tinyInt1isBit=false&allowMultiQueries=true", "root", "123456") + .globalConfig(builder -> { + builder.author("moyz") // 设置作者 + .enableSwagger() // 开启 swagger 模式 + .fileOverride() // 覆盖已生成文件 + .outputDir("D://"); // 指定输出目录 + }) + .dataSourceConfig(builder -> builder.typeConvertHandler((globalConfig, typeRegistry, metaInfo) -> { + int typeCode = metaInfo.getJdbcType().TYPE_CODE; + if (typeCode == Types.SMALLINT) { + // 自定义类型转换 + return DbColumnType.INTEGER; + } + return typeRegistry.getColumnType(metaInfo); + + })) + .packageConfig(builder -> { + builder.mapper("com.adi.common.mapper") + .parent("") + .moduleName("") + .entity("po") + .serviceImpl("service.impl") + .pathInfo(Collections.singletonMap(OutputFile.xml, "D://mybatisplus-generatorcode")); // 设置mapperXml生成路径 + }) + .strategyConfig(builder -> { + builder.addInclude("adi_user,adi_conversation,adi_conversation_message") // 设置需要生成的表名 + .addTablePrefix("adi_"); + builder.mapperBuilder().enableBaseResultMap().enableMapperAnnotation().build(); + }) + .execute(); + } +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/annotation/AskReqCheck.java b/adi-common/src/main/java/com/moyz/adi/common/annotation/AskReqCheck.java new file mode 100644 index 0000000..c16fab7 --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/annotation/AskReqCheck.java @@ -0,0 +1,27 @@ +package com.moyz.adi.common.annotation; + +import com.moyz.adi.common.validator.AskReqValidator; +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.*; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + + +@Constraint(validatedBy = { + AskReqValidator.class, +}) +@Target({TYPE, FIELD, PARAMETER, METHOD, CONSTRUCTOR, ANNOTATION_TYPE}) +@Retention(RUNTIME) +@Documented +public @interface AskReqCheck { + String message() default "dddd"; + + Class[] groups() default {}; + + Class[] payload() default {}; +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/annotation/CreateImageReqCheck.java b/adi-common/src/main/java/com/moyz/adi/common/annotation/CreateImageReqCheck.java new file mode 100644 index 0000000..117db02 --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/annotation/CreateImageReqCheck.java @@ -0,0 +1,27 @@ +package com.moyz.adi.common.annotation; + +import com.moyz.adi.common.validator.CreateImageReqValidator; +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.*; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + + +@Constraint(validatedBy = { + CreateImageReqValidator.class, +}) +@Target({TYPE, FIELD, PARAMETER, METHOD, CONSTRUCTOR, ANNOTATION_TYPE}) +@Retention(RUNTIME) +@Documented +public @interface CreateImageReqCheck { + String message() default "dddd"; + + Class[] groups() default {}; + + Class[] payload() default {}; +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/annotation/DistributeLock.java b/adi-common/src/main/java/com/moyz/adi/common/annotation/DistributeLock.java new file mode 100644 index 0000000..e4e0612 --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/annotation/DistributeLock.java @@ -0,0 +1,46 @@ +package com.moyz.adi.common.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 分布式锁注解 + * + * @author moyz + * date:2021-07-15 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface DistributeLock { + + /** + * redis key + * + * @return + */ + String redisKey() default ""; + + /** + * clientId标识用来加锁的客户端 + * + * @return + */ + String clientId() default ""; + + /** + * 失效时间(秒) + * + * @return + */ + int expireInSeconds() default 0; + + /** + * 如果获取锁失败,是否继续执行 + * + * @return + */ + boolean continueIfAcquireFail() default true; + +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/annotation/NotAllFieldsEmptyCheck.java b/adi-common/src/main/java/com/moyz/adi/common/annotation/NotAllFieldsEmptyCheck.java new file mode 100644 index 0000000..e3d7f04 --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/annotation/NotAllFieldsEmptyCheck.java @@ -0,0 +1,27 @@ +package com.moyz.adi.common.annotation; + +import com.moyz.adi.common.validator.AskReqValidator; +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.*; +import static java.lang.annotation.ElementType.ANNOTATION_TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Constraint(validatedBy = { + AskReqValidator.class, +}) +@Target({TYPE, FIELD, PARAMETER, METHOD, CONSTRUCTOR, ANNOTATION_TYPE}) +@Retention(RUNTIME) +@Documented +public @interface NotAllFieldsEmptyCheck { + String message() default "all filed is null"; + + Class[] groups() default {}; + + Class[] payload() default {}; +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/annotation/ParamsLog.java b/adi-common/src/main/java/com/moyz/adi/common/annotation/ParamsLog.java new file mode 100644 index 0000000..24662f7 --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/annotation/ParamsLog.java @@ -0,0 +1,17 @@ +package com.moyz.adi.common.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 参数打印注解 + * + * @author moyz + * date:2021-07-15 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface ParamsLog { +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/aop/ControllerParamsLogAspect.java b/adi-common/src/main/java/com/moyz/adi/common/aop/ControllerParamsLogAspect.java new file mode 100644 index 0000000..63365bd --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/aop/ControllerParamsLogAspect.java @@ -0,0 +1,33 @@ +package com.moyz.adi.common.aop; + +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.aspectj.lang.annotation.Pointcut; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +/** + * 打印controller的请求参数 + * + * @author moyz + * date:2021-07-15 03:16:59 + */ +@Aspect +@Component +public class ControllerParamsLogAspect { + + private static final Logger logger = LoggerFactory.getLogger(ControllerParamsLogAspect.class); + + @Pointcut("execution(public * com.adi.*.controller..*.*(..))") + public void controllerMethods() { + } + + @Before("controllerMethods()") + public void before(JoinPoint joinPoint) { + ParamsLogAspect.paramsLog(joinPoint, logger); + } + + +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/aop/DistributeLockAspect.java b/adi-common/src/main/java/com/moyz/adi/common/aop/DistributeLockAspect.java new file mode 100644 index 0000000..1870c2c --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/aop/DistributeLockAspect.java @@ -0,0 +1,75 @@ +package com.moyz.adi.common.aop; + +import com.moyz.adi.common.annotation.DistributeLock; +import com.moyz.adi.common.util.RedisTemplateUtil; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +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.springframework.stereotype.Component; + +import java.util.UUID; + + +/** + * 通用分页式锁 + * + * @author moyz + */ +@Slf4j +@Aspect +@Component +public class DistributeLockAspect { + + @Resource + private RedisTemplateUtil redisTemplateUtil; + + @Around("@annotation(distributeLock)") + public Object around(ProceedingJoinPoint joinPoint, DistributeLock distributeLock) throws Throwable { + String key = distributeLock.redisKey(); + int expireInSeconds = distributeLock.expireInSeconds(); + boolean continueIfAcquireFail = distributeLock.continueIfAcquireFail(); + String clientId = distributeLock.clientId(); + boolean lockAndContinue = checkAndLock(key, clientId, expireInSeconds, continueIfAcquireFail, redisTemplateUtil); + if (!lockAndContinue) { + log.warn("该次请求忽略"); + return false; + } + try { + return joinPoint.proceed(); + } finally { + boolean unlockResult = redisTemplateUtil.unlock(key, clientId); + log.info("unlock:{},key:{},clientId:{}", unlockResult, key, clientId); + } + } + + /** + * 校验参数及加锁,如果没有加锁方标识(clientId),则自动生成uuid做为clientId + * + * @param key + * @param clientId 加锁方标识 + * @param expireInSeconds 超时时间 (秒) + * @param continueIfAcquireFail 获取锁失败是否继续执行后面的业务逻辑 + * @param redisTemplateUtil redis工具类 + * @return + * @throws Exception + */ + public static boolean checkAndLock(String key, String clientId, int expireInSeconds, boolean continueIfAcquireFail, RedisTemplateUtil redisTemplateUtil) throws Exception { + log.info("lock info,key:{},clientId:{},expireInSecond:{},continueIfAcquireFail:{}", key, clientId, expireInSeconds, continueIfAcquireFail); + if (StringUtils.isBlank(key) || expireInSeconds < 1) { + log.warn("加锁参数有误,请确认后再操作"); + throw new Exception("加锁参数有误,请确认后再操作"); + } + if (StringUtils.isBlank(clientId)) { + clientId = UUID.randomUUID().toString().replace("-", ""); + } + boolean lock = redisTemplateUtil.lock(key, clientId, expireInSeconds); + if (!lock && !continueIfAcquireFail) { + log.warn("由于参数continueIfAcquireFail为false并且获取锁失败,此次请求忽略"); + return false; + } + return lock; + } +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/aop/ParamsLogAspect.java b/adi-common/src/main/java/com/moyz/adi/common/aop/ParamsLogAspect.java new file mode 100644 index 0000000..0baf7a6 --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/aop/ParamsLogAspect.java @@ -0,0 +1,60 @@ +package com.moyz.adi.common.aop; + +import com.moyz.adi.common.annotation.ParamsLog; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.aspectj.lang.reflect.MethodSignature; +import org.slf4j.Logger; +import org.springframework.stereotype.Component; + +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; + +/** + * @author myz + */ +@Slf4j +@Aspect +@Component +public class ParamsLogAspect { + + @Before(value = "@annotation(paramsLog)") + public void before(JoinPoint joinPoint, ParamsLog paramsLog) { + paramsLog(joinPoint, log); + } + + /** + * 输出方法参数到日志 + * + * @param joinPoint joinPoint + * @param logger 日志 + */ + static void paramsLog(JoinPoint joinPoint, Logger logger) { + String className = joinPoint.getSignature().getDeclaringType().getName(); + Method method = ((MethodSignature) joinPoint.getSignature()).getMethod(); + Object[] args = joinPoint.getArgs(); + Parameter[] parameters = ((MethodSignature) joinPoint.getSignature()).getMethod().getParameters(); + StringBuilder sb = new StringBuilder(); + sb.append(className); + sb.append("."); + sb.append(method.getName()); + sb.append(" params:["); + for (int i = 0; i < args.length; i++) { + String paramName = parameters[i].getName(); + sb.append(parameters[i].getName()); + sb.append("=>"); + if ("password".equals(paramName)) { + sb.append("***"); + } else { + sb.append(args[i]); + } + sb.append(";"); + } + sb.append("]"); + String log = sb.toString(); + logger.info(StringUtils.substring(log, 0, 1000)); + } +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/base/BaseResponse.java b/adi-common/src/main/java/com/moyz/adi/common/base/BaseResponse.java new file mode 100644 index 0000000..e9a2508 --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/base/BaseResponse.java @@ -0,0 +1,51 @@ +package com.moyz.adi.common.base; + +import com.moyz.adi.common.enums.ErrorEnum; +import lombok.Data; + +import java.io.Serializable; + +@Data +public class BaseResponse implements Serializable { + + private static final long serialVersionUID = 1L; + /** + * 是否成功 + */ + private boolean success; + /** + * 状态码 + */ + private String code; + /** + * 提示 + */ + private String message; + /** + * 数据 + */ + private T data; + + public BaseResponse() { + } + + public BaseResponse(boolean success) { + this.success = success; + } + + public BaseResponse(boolean success, T data) { + this.data = data; + this.success = success; + } + + public BaseResponse(String code, String message, T data) { + this.code = code; + this.success = false; + this.message = message; + this.data = data; + } + + public static BaseResponse success(String message){ + return new BaseResponse(ErrorEnum.SUCCESS.getCode(), message, ""); + } +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/base/ResponseWrapper.java b/adi-common/src/main/java/com/moyz/adi/common/base/ResponseWrapper.java new file mode 100644 index 0000000..9db597a --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/base/ResponseWrapper.java @@ -0,0 +1,35 @@ +package com.moyz.adi.common.base; + +import com.moyz.adi.common.util.JsonUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.MethodParameter; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.http.server.ServerHttpRequest; +import org.springframework.http.server.ServerHttpResponse; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice; + +@Slf4j +@RestControllerAdvice(basePackages = {"com.moyz.adi"}) +public class ResponseWrapper implements ResponseBodyAdvice { + + @Override + public boolean supports(MethodParameter methodParameter, Class aClass) { + return true; + } + + @Override + public Object beforeBodyWrite(Object result, MethodParameter methodParameter, + MediaType mediaType, Class aClass, ServerHttpRequest serverHttpRequest, + ServerHttpResponse serverHttpResponse) { + if (result instanceof BaseResponse) { + return result; + } else if (result instanceof String) { + return JsonUtil.toJson(new BaseResponse(true, result)); + } + log.info("result:" + result); + return new BaseResponse(true, result); + } + +} \ No newline at end of file diff --git a/adi-common/src/main/java/com/moyz/adi/common/base/ThreadContext.java b/adi-common/src/main/java/com/moyz/adi/common/base/ThreadContext.java new file mode 100644 index 0000000..9019a96 --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/base/ThreadContext.java @@ -0,0 +1,38 @@ +package com.moyz.adi.common.base; + +import com.moyz.adi.common.entity.User; + +public class ThreadContext { + private static final ThreadLocal currentUser = new ThreadLocal<>(); + private static final ThreadLocal currentToken = new ThreadLocal<>(); + + public static void setCurrentUser(User user) { + currentUser.set(user); + } + + public static User getCurrentUser() { + return currentUser.get(); + } + + public static Long getCurrentUserId() { + return currentUser.get().getId(); + } + + public static void setToken(String token) { + currentToken.set(token); + } + + + public static String getToken() { + return currentToken.get(); + } + + + public static User getExistCurrentUser() { + User user = ThreadContext.getCurrentUser(); + if (null == user) { + throw new RuntimeException("用户不存在"); + } + return user; + } +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/config/BeanConfig.java b/adi-common/src/main/java/com/moyz/adi/common/config/BeanConfig.java new file mode 100644 index 0000000..6ffcc99 --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/config/BeanConfig.java @@ -0,0 +1,100 @@ +package com.moyz.adi.common.config; + +import com.baomidou.mybatisplus.annotation.DbType; +import com.baomidou.mybatisplus.core.config.GlobalConfig; +import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.BlockAttackInnerInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; +import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean; +import com.moyz.adi.common.util.LocalDateTimeUtil; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.google.common.collect.Lists; +import lombok.extern.slf4j.Slf4j; +import org.apache.ibatis.session.SqlSessionFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; +import org.springframework.core.io.support.PathMatchingResourcePatternResolver; +import org.springframework.core.task.AsyncTaskExecutor; +import org.springframework.http.client.BufferingClientHttpRequestFactory; +import org.springframework.http.client.SimpleClientHttpRequestFactory; +import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import org.springframework.web.client.RestTemplate; + +import javax.sql.DataSource; + +@Slf4j +@Configuration +public class BeanConfig { + + @Bean + public RestTemplate restTemplate() { + log.info("Configuration==create restTemplate"); + SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory(); + // 设置建立连接超时时间 毫秒 + requestFactory.setConnectTimeout(60000); + // 设置读取数据超时时间 毫秒 + requestFactory.setReadTimeout(60000); + RestTemplate restTemplate = new RestTemplate(); + // 注册LOG拦截器 + restTemplate.setInterceptors(Lists.newArrayList(new LogClientHttpRequestInterceptor())); + restTemplate.setRequestFactory(new BufferingClientHttpRequestFactory(requestFactory)); + + return restTemplate; + } + + @Bean + @Primary + public ObjectMapper objectMapper(Jackson2ObjectMapperBuilder builder) { + log.info("Configuration==create objectMapper"); + ObjectMapper objectMapper = builder.createXmlMapper(false).build(); + objectMapper.registerModules(LocalDateTimeUtil.getSimpleModule(), new JavaTimeModule(), new Jdk8Module()); + //设置null值不参与序列化(字段不被显示) + objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + return objectMapper; + } + + @Bean(name = "mainExecutor") + @Primary + public AsyncTaskExecutor mainExecutor() { + int processorsNum = Runtime.getRuntime().availableProcessors(); + log.info("mainExecutor,processorsNum:{}", processorsNum); + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setCorePoolSize(processorsNum * 2); + executor.setMaxPoolSize(100); + return executor; + } + + @Bean(name = "imagesExecutor") + public AsyncTaskExecutor imagesExecutor() { + int processorsNum = Runtime.getRuntime().availableProcessors(); + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + log.info("imagesExecutor corePoolSize:{},maxPoolSize:{}", processorsNum, processorsNum * 2); + executor.setCorePoolSize(processorsNum); + executor.setMaxPoolSize(processorsNum * 2); + return executor; + } + + @Bean + @Primary + public SqlSessionFactory sqlSessionFactory(DataSource dataSource) + throws Exception { + MybatisSqlSessionFactoryBean bean = new MybatisSqlSessionFactoryBean(); + bean.setDataSource(dataSource); + MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); + // 分页插件 + interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); + // 防止全表更新 + interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor()); + bean.setPlugins(interceptor); + bean.setMapperLocations( + new PathMatchingResourcePatternResolver().getResources("classpath*:/mapper/*.xml")); + return bean.getObject(); + } + +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/config/GlobalExceptionHandler.java b/adi-common/src/main/java/com/moyz/adi/common/config/GlobalExceptionHandler.java new file mode 100644 index 0000000..ed09d1d --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/config/GlobalExceptionHandler.java @@ -0,0 +1,53 @@ +package com.moyz.adi.common.config; + +import com.moyz.adi.common.base.BaseResponse; +import com.moyz.adi.common.enums.ErrorEnum; +import com.moyz.adi.common.exception.BaseException; +import lombok.extern.slf4j.Slf4j; +import org.springframework.validation.BindingResult; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import java.util.HashMap; +import java.util.Map; + +@Slf4j +@RestControllerAdvice +public class GlobalExceptionHandler { + /** + * 参数校验异常 + * + * @return BaseResponse + */ + @ExceptionHandler(MethodArgumentNotValidException.class) + private BaseResponse handleMethodArgumentNotValidException( + final MethodArgumentNotValidException exception) { + Map error = wrapperError(exception.getBindingResult()); + log.error("参数校验异常:{}", error); + return new BaseResponse(ErrorEnum.A_PARAMS_ERROR.getCode(), ErrorEnum.A_PARAMS_ERROR.getInfo(), error); + } + + @ExceptionHandler(BaseException.class) + private BaseResponse handleBaseException(final BaseException exception) { + log.error("拦截业务异常:{}", exception); + return new BaseResponse(exception.getCode(), exception.getInfo(), exception.getData()); + } + + /** + * 兜底 + * + * @return BaseResponse + */ + @ExceptionHandler(Exception.class) + private BaseResponse handleException(final Exception exception) { + log.error("拦截全局异常:", exception); + return new BaseResponse(ErrorEnum.B_GLOBAL_ERROR.getCode(), ErrorEnum.B_GLOBAL_ERROR.getInfo(), exception.getMessage()); + } + + private Map wrapperError(BindingResult result) { + Map errorMap = new HashMap<>(5); + result.getFieldErrors().forEach(x -> errorMap.put(x.getField(), x.getDefaultMessage())); + return errorMap; + } +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/config/LogClientHttpRequestInterceptor.java b/adi-common/src/main/java/com/moyz/adi/common/config/LogClientHttpRequestInterceptor.java new file mode 100644 index 0000000..5fd296e --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/config/LogClientHttpRequestInterceptor.java @@ -0,0 +1,49 @@ +package com.moyz.adi.common.config; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpRequest; +import org.springframework.http.MediaType; +import org.springframework.http.client.ClientHttpRequestExecution; +import org.springframework.http.client.ClientHttpRequestInterceptor; +import org.springframework.http.client.ClientHttpResponse; +import org.springframework.util.StopWatch; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; + +@Slf4j +public class LogClientHttpRequestInterceptor implements ClientHttpRequestInterceptor { + + @Override + public ClientHttpResponse intercept(HttpRequest request, byte[] body, + ClientHttpRequestExecution execution) throws IOException { + + StopWatch stopWatch = new StopWatch(); + stopWatch.start(); + ClientHttpResponse response = execution.execute(request, body); + + stopWatch.stop(); + StringBuilder resBody = new StringBuilder(); + try (BufferedReader bufferedReader = new BufferedReader( + new InputStreamReader(response.getBody(), StandardCharsets.UTF_8))) { + String line = bufferedReader.readLine(); + while (line != null) { + resBody.append(line); + line = bufferedReader.readLine(); + } + } + if (request.getHeaders().getContentType() != null && request.getHeaders().getContentType() + .includes(MediaType.MULTIPART_FORM_DATA)) { + body = new byte[]{}; + } + + log.info("rest log status:{},time:{},url:{},body:{},response:{}", + response.getRawStatusCode(), stopWatch.getLastTaskTimeMillis(), + request.getURI(), new String(body, StandardCharsets.UTF_8), resBody); + return response; + } + + +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/config/LogInterceptor.java b/adi-common/src/main/java/com/moyz/adi/common/config/LogInterceptor.java new file mode 100644 index 0000000..11049f6 --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/config/LogInterceptor.java @@ -0,0 +1,55 @@ +package com.moyz.adi.common.config; + +import com.moyz.adi.common.helper.HttpHelper; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpMethod; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.HandlerInterceptor; +import org.springframework.web.servlet.ModelAndView; + +@Slf4j +//@Service +public class LogInterceptor implements HandlerInterceptor { + + @Resource + private ObjectMapper objectMapper; + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, + Object handler) throws Exception { + if (handler instanceof HandlerMethod) { + HandlerMethod method = (HandlerMethod) handler; + if (HttpMethod.GET.matches(request.getMethod())) { + log.info("url:{},ip:{},method:{},param:{}", request.getRequestURL(), + request.getRemoteAddr(), method.getMethod().getName(), + objectMapper.writeValueAsString(request.getParameterMap())); + } else { + String bodyString = HttpHelper.getBodyString(request); + log.info("url:{},ip:{},method:{},param:{},body:{}", request.getRequestURL(), + request.getRemoteAddr(), method.getMethod().getName(), + objectMapper.writeValueAsString(request.getParameterMap()), bodyString); + } + } else { + log.info("url:{},ip:{}", request.getRequestURL(), + request.getRemoteAddr()); + } + + return true; + } + + @Override + public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, + ModelAndView modelAndView) throws Exception { + + } + + @Override + public void afterCompletion(HttpServletRequest request, HttpServletResponse response, + Object handler, Exception ex) throws Exception { + + } +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/config/OpenAiProperties.java b/adi-common/src/main/java/com/moyz/adi/common/config/OpenAiProperties.java new file mode 100644 index 0000000..542ee5d --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/config/OpenAiProperties.java @@ -0,0 +1,15 @@ +package com.moyz.adi.common.config; + + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConfigurationProperties("openai") +@Data +public class OpenAiProperties { + + private boolean proxyEnable; + +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/config/RequestReaderHttpServletRequestWrapper.java b/adi-common/src/main/java/com/moyz/adi/common/config/RequestReaderHttpServletRequestWrapper.java new file mode 100644 index 0000000..9e3e16f --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/config/RequestReaderHttpServletRequestWrapper.java @@ -0,0 +1,59 @@ +package com.moyz.adi.common.config; + +import jakarta.servlet.ReadListener; +import jakarta.servlet.ServletInputStream; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletRequestWrapper; +import org.springframework.util.StreamUtils; + +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStreamReader; + +/** + * 缓存请求的输入流,以便重复使用 + */ +public class RequestReaderHttpServletRequestWrapper extends HttpServletRequestWrapper { + + private final byte[] body; + + public RequestReaderHttpServletRequestWrapper(HttpServletRequest request) throws IOException { + super(request); + body = StreamUtils.copyToByteArray(request.getInputStream()); + } + + @Override + public BufferedReader getReader() throws IOException { + return new BufferedReader(new InputStreamReader(getInputStream())); + } + + @Override + public ServletInputStream getInputStream() throws IOException { + + final ByteArrayInputStream newInputStream = new ByteArrayInputStream(body); + + return new ServletInputStream() { + + @Override + public int read() throws IOException { + return newInputStream.read(); + } + + @Override + public boolean isFinished() { + return newInputStream.available() == 0; + } + + @Override + public boolean isReady() { + return newInputStream.available() > 0; + } + + @Override + public void setReadListener(ReadListener readListener) { + + } + }; + } +} \ No newline at end of file diff --git a/adi-common/src/main/java/com/moyz/adi/common/config/SpringdocConfig.java b/adi-common/src/main/java/com/moyz/adi/common/config/SpringdocConfig.java new file mode 100644 index 0000000..e463bd0 --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/config/SpringdocConfig.java @@ -0,0 +1,8 @@ +package com.moyz.adi.common.config; + +import org.springframework.context.annotation.Configuration; + +@Configuration +public class SpringdocConfig { + +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/config/WebMvcConfig.java b/adi-common/src/main/java/com/moyz/adi/common/config/WebMvcConfig.java new file mode 100644 index 0000000..9a88bd9 --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/config/WebMvcConfig.java @@ -0,0 +1,41 @@ +package com.moyz.adi.common.config; + +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.converter.ByteArrayHttpMessageConverter; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.StringHttpMessageConverter; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import java.util.List; + +@Slf4j +@Configuration +public class WebMvcConfig implements WebMvcConfigurer { + + @Resource + private ObjectMapper objectMapper; + + @Override + public void configureMessageConverters(List> converters) { + log.info("WebMvcConfig==configureMessageConverters"); + WebMvcConfigurer.super.configureMessageConverters(converters); + converters.add(new StringHttpMessageConverter()); + converters.add(new MappingJackson2HttpMessageConverter(objectMapper)); + converters.add(new ByteArrayHttpMessageConverter()); + } + + @Override + public void addInterceptors(InterceptorRegistry registry) { + //使用ParamsLogAspect代替 +// log.info("WebMvcConfig==addInterceptors"); +// registry.addInterceptor(logInterceptor) +// .addPathPatterns("/**") +// .excludePathPatterns("/swagger-resources/**", "/webjars/**", "/v3/**", "/swagger-ui.html"); + } + +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/cosntant/AdiConstant.java b/adi-common/src/main/java/com/moyz/adi/common/cosntant/AdiConstant.java new file mode 100644 index 0000000..20dbdc7 --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/cosntant/AdiConstant.java @@ -0,0 +1,81 @@ +package com.moyz.adi.common.cosntant; + +import java.util.List; + +public class AdiConstant { + + public static final int DEFAULT_PAGE_SIZE = 1; + + /** + * 验证码id过期时间:1小时 + */ + public static final int AUTH_CAPTCHA_ID_EXPIRE = 1; + + /** + * 验证码过期时间,5分钟 + */ + public static final int AUTH_CAPTCHA_EXPIRE = 5; + + /** + * 注册激活码有效时长,8小时 + */ + public static final int AUTH_ACTIVE_CODE_EXPIRE = 8; + + /** + * token存活时间(8小时) + */ + public static final int USER_TOKEN_EXPIRE = 8; + + public static final String DEFAULT_PASSWORD = "123456"; + + public static final int LOGIN_MAX_FAIL_TIMES = 3; + + public static final String[] WEB_RESOURCES = { + "/swagger-ui/index.html", + "/swagger-ui", + "/swagger-resources", + "/v3/api-docs", + "/favicon.ico", + ".css", + ".js", + "/doc.html" + }; + + public static final int SECRET_KEY_TYPE_SYSTEM = 1; + public static final int SECRET_KEY_TYPE_CUSTOM = 2; + + public static final String OPENAI_MESSAGE_DONE_FLAG = "[DONE]"; + + public static final String DEFAULT_MODEL = "gpt-3.5-turbo"; + + public static final String CREATE_IMAGE_RESP_FORMATS_B64JSON = "b64_json"; + public static final String OPENAI_CREATE_IMAGE_RESP_FORMATS_URL = "url"; + + public static final List OPENAI_CREATE_IMAGE_SIZES = List.of("256x256", "512x512", "1024x1024"); + + + + public static class GenerateImage{ + public static final int INTERACTING_METHOD_GENERATE_IMAGE = 1; + public static final int INTERACTING_METHOD_EDIT_IMAGE = 2; + public static final int INTERACTING_METHOD_VARIATION = 3; + + public static final int STATUS_DOING = 1; + public static final int STATUS_FAIL = 2; + public static final int STATUS_SUCCESS = 3; + } + + public static class SysConfigKey { + public static final String SECRET_KEY = "secret_key"; + public static final String REQUEST_TEXT_RATE_LIMIT = "request_text_rate_limit"; + public static final String REQUEST_IMAGE_RATE_LIMIT = "request_image_rate_limit"; + public static final String CONVERSATION_MAX_NUM = "conversation_max_num"; + public static final String QUOTA_BY_TOKEN_DAILY = "quota_by_token_daily"; + public static final String QUOTA_BY_TOKEN_MONTHLY = "quota_by_token_monthly"; + public static final String QUOTA_BY_REQUEST_DAILY = "quota_by_request_daily"; + public static final String QUOTA_BY_REQUEST_MONTHLY = "quota_by_request_monthly"; + public static final String QUOTA_BY_IMAGE_DAILY = "quota_by_image_daily"; + public static final String QUOTA_BY_IMAGE_MONTHLY = "quota_by_image_monthly"; + + } +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/cosntant/RedisKeyConstant.java b/adi-common/src/main/java/com/moyz/adi/common/cosntant/RedisKeyConstant.java new file mode 100644 index 0000000..42ffc2f --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/cosntant/RedisKeyConstant.java @@ -0,0 +1,81 @@ +package com.moyz.adi.common.cosntant; + +public class RedisKeyConstant { + + /** + * 账号激活码的key + */ + public static final String AUTH_ACTIVE_CODE = "auth:activeCode:{0}"; + + /** + * 注册时使用的验证码 + * 参数:验证码id + * 值:验证码 + */ + public static final String AUTH_REGISTER_CAPTCHA_ID = "auth:register:captcha:{0}"; + + /** + * 登录时使用的验证码id缓存 + * 参数:验证码id + * 值:验证码 + */ + public static final String AUTH_LOGIN_CAPTCHA_ID = "auth:login:captcha:{0}"; + + /** + * 注册验证码缓存 + * 参数:验证码 + * 值:1 + */ + public static final String AUTH_CAPTCHA = "auth:register:captcha:{0}"; + + + /** + * 登录token + * {0}:用户token + * 值:json.format(user) + */ + public static final String USER_TOKEN = "user:token:{0}"; + + /** + * 参数:游客的uuid + * 值:json.format(guest) + */ + public static final String GUEST_UUID = "guest:uuid:{0}"; + + /** + * 登录失败次数 + * 参数:用户邮箱 + * 值: 失效次数 + */ + public static final String LOGIN_FAIL_COUNT = "user:login:fail:{0}"; + + /** + * 用户是否请求ai中 + * 参数:用户id + * 值: 1或者0 + */ + public static final String USER_ASKING = "user:asking:{0}"; + + /** + * 用户是否画画中 + * 参数:用户id + * 值: 1或者0 + */ + public static final String USER_DRAWING = "user:drawing:{0}"; + + /** + * 用户提问限流计数 + * 参数:用户id + * 值: 当前时间窗口访问量 + */ + public static final String USER_REQUEST_TEXT_TIMES = "user:request-text:times:{0}"; + + public static final String USER_REQUEST_IMAGE_TIMES = "user:request-image:times:{0}"; + + /** + * 找回密码的请求绑在 + * 参数:随机数 + * 值: 用户id,用于校验后续流程中的重置密码使用 + */ + public static final String FIND_MY_PASSWORD = "user:find:password:{0}"; +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/dto/AiImageDto.java b/adi-common/src/main/java/com/moyz/adi/common/dto/AiImageDto.java new file mode 100644 index 0000000..bfeafce --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/dto/AiImageDto.java @@ -0,0 +1,29 @@ +package com.moyz.adi.common.dto; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.List; + +@Data +public class AiImageDto { + private Long id; + private String uuid; + private String prompt; + + private String originalImageUrl; + + private String maskImageUrl; + + private Integer interactingMethod; + + @JsonIgnore + private String generatedImages; + /** + * http url + */ + private List imageUrlList; + private Integer processStatus; + private LocalDateTime createTime; +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/dto/AiImagesListResp.java b/adi-common/src/main/java/com/moyz/adi/common/dto/AiImagesListResp.java new file mode 100644 index 0000000..efa9127 --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/dto/AiImagesListResp.java @@ -0,0 +1,11 @@ +package com.moyz.adi.common.dto; + +import lombok.Data; + +import java.util.List; + +@Data +public class AiImagesListResp { + private Long minId; + private List imageItems; +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/dto/AskReq.java b/adi-common/src/main/java/com/moyz/adi/common/dto/AskReq.java new file mode 100644 index 0000000..445de3e --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/dto/AskReq.java @@ -0,0 +1,24 @@ +package com.moyz.adi.common.dto; + +import com.moyz.adi.common.annotation.AskReqCheck; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.hibernate.validator.constraints.Length; + +@Schema(description = "对话的请求对象") +@Data +@AskReqCheck +public class AskReq { + + @Length(min = 32, max = 32) + private String conversationUuid; + + private String parentMessageId; + + private String prompt; + + /** + * If not empty, it means will request AI with the exist prompt, param {@code prompt} is ignored + */ + private String regenerateQuestionUuid; +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/dto/ConfigResp.java b/adi-common/src/main/java/com/moyz/adi/common/dto/ConfigResp.java new file mode 100644 index 0000000..a4b2c19 --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/dto/ConfigResp.java @@ -0,0 +1,22 @@ +package com.moyz.adi.common.dto; + +import lombok.Data; + +@Data +public class ConfigResp { + private String secretKey; + private Boolean contextEnable; + private Integer contextMsgPairNum; + private Integer quotaByTokenDaily; + private Integer quotaByTokenMonthly; + private Integer quotaByRequestDaily; + private Integer quotaByRequestMonthly; + private Integer quotaByImageDaily; + private Integer quotaByImageMonthly; + private Integer todayTokenCost; + private Integer todayRequestTimes; + private Integer todayGeneratedImageNumber; + private Integer currMonthTokenCost; + private Integer currMonthRequestTimes; + private Integer currMonthGeneratedImageNumber; +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/dto/ConvDto.java b/adi-common/src/main/java/com/moyz/adi/common/dto/ConvDto.java new file mode 100644 index 0000000..5276f05 --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/dto/ConvDto.java @@ -0,0 +1,21 @@ +package com.moyz.adi.common.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import lombok.Data; + +@Data +public class ConvDto { + + private String uuid; + + @NotBlank + private String title; + + private Integer tokens; + + @Schema(title = "set the system message to ai, ig: you are a lawyer") + private String aiSystemMessage; + + private Boolean understandContextEnable; +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/dto/ConvEditReq.java b/adi-common/src/main/java/com/moyz/adi/common/dto/ConvEditReq.java new file mode 100644 index 0000000..8fd022d --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/dto/ConvEditReq.java @@ -0,0 +1,17 @@ +package com.moyz.adi.common.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import lombok.Data; + +@Data +public class ConvEditReq { + + @NotBlank + private String title; + + @Schema(title = "set the system message to ai, ig: you are a lawyer") + private String aiSystemMessage; + + private Boolean understandContextEnable; +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/dto/ConvMsgListResp.java b/adi-common/src/main/java/com/moyz/adi/common/dto/ConvMsgListResp.java new file mode 100644 index 0000000..864a99f --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/dto/ConvMsgListResp.java @@ -0,0 +1,15 @@ +package com.moyz.adi.common.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; + +import java.util.List; + +@Data +@AllArgsConstructor +public class ConvMsgListResp { + + private String minMsgUuid; + + private List msgList; +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/dto/ConvMsgResp.java b/adi-common/src/main/java/com/moyz/adi/common/dto/ConvMsgResp.java new file mode 100644 index 0000000..729608f --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/dto/ConvMsgResp.java @@ -0,0 +1,38 @@ +package com.moyz.adi.common.dto; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.List; + +@Data +public class ConvMsgResp { + + @JsonIgnore + private Long id; + + @Schema(title = "消息的uuid") + private String uuid; + + @Schema(title = "父级消息id") + private Long parentMessageId; + + @Schema(title = "对话的消息") + @TableField("content") + private String content; + + @Schema(title = "产生该消息的角色:1: 用户,2:系统,3:助手") + private String messageRole; + + @Schema(title = "消耗的token数量") + private Integer tokens; + + @Schema(title = "创建时间") + private LocalDateTime createTime; + + @Schema(title = "子级消息(一般指的是AI的响应)") + private List children; +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/dto/CreateImageDto.java b/adi-common/src/main/java/com/moyz/adi/common/dto/CreateImageDto.java new file mode 100644 index 0000000..2b4aa3d --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/dto/CreateImageDto.java @@ -0,0 +1,13 @@ +package com.moyz.adi.common.dto; + +import lombok.Data; + +@Data +public class CreateImageDto { + private String prompt; + private String size; + private int number; + private int interactingMethod; + private String originalImage; + private String maskImage; +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/dto/EditImageReq.java b/adi-common/src/main/java/com/moyz/adi/common/dto/EditImageReq.java new file mode 100644 index 0000000..646ad3b --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/dto/EditImageReq.java @@ -0,0 +1,22 @@ +package com.moyz.adi.common.dto; + +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import lombok.Data; +import org.hibernate.validator.constraints.Length; + +@Data +public class EditImageReq { + @Length(min = 32, max = 32) + private String originalImage; + @Length(min = 32, max = 32) + private String maskImage; + @NotBlank + private String prompt; + @NotBlank + private String size; + @Min(1) + @Max(10) + private int number; +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/dto/GenerateImageReq.java b/adi-common/src/main/java/com/moyz/adi/common/dto/GenerateImageReq.java new file mode 100644 index 0000000..ab9414e --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/dto/GenerateImageReq.java @@ -0,0 +1,17 @@ +package com.moyz.adi.common.dto; + +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import lombok.Data; + +@Data +public class GenerateImageReq { + @NotBlank + private String prompt; + @NotBlank + private String size; + @Min(1) + @Max(10) + private int number; +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/dto/ImagesReq.java b/adi-common/src/main/java/com/moyz/adi/common/dto/ImagesReq.java new file mode 100644 index 0000000..552f07e --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/dto/ImagesReq.java @@ -0,0 +1,8 @@ +package com.moyz.adi.common.dto; + +import lombok.Data; + +@Data +public class ImagesReq { + private Integer generateStatus; +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/dto/LoginReq.java b/adi-common/src/main/java/com/moyz/adi/common/dto/LoginReq.java new file mode 100644 index 0000000..f9714a4 --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/dto/LoginReq.java @@ -0,0 +1,20 @@ +package com.moyz.adi.common.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import lombok.Data; + +@Schema(name = "登录请求参数") +@Data +public class LoginReq { + + @NotBlank(message = "邮箱不能为空") + String email; + + @NotBlank(message = "密码不能为空") + String password; + + String captchaId; + + String captchaCode; +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/dto/LoginResp.java b/adi-common/src/main/java/com/moyz/adi/common/dto/LoginResp.java new file mode 100644 index 0000000..5172698 --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/dto/LoginResp.java @@ -0,0 +1,13 @@ +package com.moyz.adi.common.dto; + +import lombok.Data; + +@Data +public class LoginResp { + + private String token; + private String name; + private String email; + private String activeTime; + private String captchaId; +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/dto/ModifyPasswordReq.java b/adi-common/src/main/java/com/moyz/adi/common/dto/ModifyPasswordReq.java new file mode 100644 index 0000000..9d6d897 --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/dto/ModifyPasswordReq.java @@ -0,0 +1,19 @@ +package com.moyz.adi.common.dto; + +import jakarta.validation.constraints.NotBlank; +import lombok.Data; +import org.hibernate.validator.constraints.Length; +import org.springframework.validation.annotation.Validated; + +@Data +@Validated +public class ModifyPasswordReq { + + @NotBlank + @Length(min = 6) + private String oldPassword; + + @NotBlank + @Length(min = 6) + private String newPassword; +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/dto/PromptDto.java b/adi-common/src/main/java/com/moyz/adi/common/dto/PromptDto.java new file mode 100644 index 0000000..ffa0a8a --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/dto/PromptDto.java @@ -0,0 +1,10 @@ +package com.moyz.adi.common.dto; + +import lombok.Data; + +@Data +public class PromptDto { + private Long id; + private String act; + private String prompt; +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/dto/PromptEditReq.java b/adi-common/src/main/java/com/moyz/adi/common/dto/PromptEditReq.java new file mode 100644 index 0000000..27ea2b3 --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/dto/PromptEditReq.java @@ -0,0 +1,16 @@ +package com.moyz.adi.common.dto; + +import jakarta.validation.constraints.NotBlank; +import lombok.Data; +import org.springframework.validation.annotation.Validated; + +@Data +@Validated +public class PromptEditReq { + + @NotBlank + private String title; + + @NotBlank + private String remark; +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/dto/PromptListResp.java b/adi-common/src/main/java/com/moyz/adi/common/dto/PromptListResp.java new file mode 100644 index 0000000..3887417 --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/dto/PromptListResp.java @@ -0,0 +1,11 @@ +package com.moyz.adi.common.dto; + +import lombok.Data; + +import java.util.List; + +@Data +public class PromptListResp { + private String maxUpdateTime; + private List prompts; +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/dto/PromptsSaveReq.java b/adi-common/src/main/java/com/moyz/adi/common/dto/PromptsSaveReq.java new file mode 100644 index 0000000..ebffa48 --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/dto/PromptsSaveReq.java @@ -0,0 +1,15 @@ +package com.moyz.adi.common.dto; + +import lombok.Data; +import org.hibernate.validator.constraints.Length; +import org.springframework.validation.annotation.Validated; + +import java.util.List; + +@Data +@Validated +public class PromptsSaveReq { + + @Length(min = 1) + private List prompts; +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/dto/RegenerateImageReq.java b/adi-common/src/main/java/com/moyz/adi/common/dto/RegenerateImageReq.java new file mode 100644 index 0000000..2d06e29 --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/dto/RegenerateImageReq.java @@ -0,0 +1,11 @@ +package com.moyz.adi.common.dto; + +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import lombok.Data; + +@Data +public class RegenerateImageReq { + private String uuid; +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/dto/RegisterReq.java b/adi-common/src/main/java/com/moyz/adi/common/dto/RegisterReq.java new file mode 100644 index 0000000..0b4ca3b --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/dto/RegisterReq.java @@ -0,0 +1,31 @@ +package com.moyz.adi.common.dto; + +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.Min; +import lombok.Data; +import org.hibernate.validator.constraints.Length; +import org.springframework.validation.annotation.Validated; + +@Schema(name = "注册请求参数") +@Data +@Validated +public class RegisterReq { + + @Parameter(description = "邮箱") + @Email + private String email; + + @Parameter(description = "密码") + @Min(6) + private String password; + + @Parameter(description = "验证码ID") + @Length(min = 32) + private String captchaId; + + @Parameter(description = "验证码") + @Length(min = 4, max = 4) + private String captchaCode; +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/dto/SearchReq.java b/adi-common/src/main/java/com/moyz/adi/common/dto/SearchReq.java new file mode 100644 index 0000000..aa09bc5 --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/dto/SearchReq.java @@ -0,0 +1,11 @@ +package com.moyz.adi.common.dto; + +import jakarta.validation.constraints.NotBlank; +import lombok.Data; + +@Data +public class SearchReq { + + @NotBlank + private String keyword; +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/dto/SessionResp.java b/adi-common/src/main/java/com/moyz/adi/common/dto/SessionResp.java new file mode 100644 index 0000000..3cd6373 --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/dto/SessionResp.java @@ -0,0 +1,10 @@ +package com.moyz.adi.common.dto; + +import lombok.Data; + +@Data +public class SessionResp { + private Boolean auth; + + private String model; +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/dto/UserUpdateReq.java b/adi-common/src/main/java/com/moyz/adi/common/dto/UserUpdateReq.java new file mode 100644 index 0000000..b8311d1 --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/dto/UserUpdateReq.java @@ -0,0 +1,12 @@ +package com.moyz.adi.common.dto; + +import com.moyz.adi.common.annotation.NotAllFieldsEmptyCheck; +import lombok.Data; + +@Data +@NotAllFieldsEmptyCheck +public class UserUpdateReq { + private String secretKey; + private Boolean contextEnable; + private Integer contextMsgPairNum; +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/dto/VariationImageReq.java b/adi-common/src/main/java/com/moyz/adi/common/dto/VariationImageReq.java new file mode 100644 index 0000000..9893df3 --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/dto/VariationImageReq.java @@ -0,0 +1,18 @@ +package com.moyz.adi.common.dto; + +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import lombok.Data; +import org.hibernate.validator.constraints.Length; + +@Data +public class VariationImageReq { + @Length(min = 32, max = 32) + private String originalImage; + @NotBlank + private String size; + @Min(1) + @Max(10) + private int number; +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/dto/VerifyResp.java b/adi-common/src/main/java/com/moyz/adi/common/dto/VerifyResp.java new file mode 100644 index 0000000..79e1cdf --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/dto/VerifyResp.java @@ -0,0 +1,7 @@ +package com.moyz.adi.common.dto; + +import lombok.Data; + +@Data +public class VerifyResp { +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/entity/AdiFile.java b/adi-common/src/main/java/com/moyz/adi/common/entity/AdiFile.java new file mode 100644 index 0000000..1df831d --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/entity/AdiFile.java @@ -0,0 +1,43 @@ +package com.moyz.adi.common.entity; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +@TableName("adi_file") +@Schema(title = "文件表") +public class AdiFile extends BaseEntity { + @Schema(title = "用户id") + @TableField(value = "user_id") + private Long userId; + + @Schema(title = "name") + @TableField(value = "name") + private String name; + + @Schema(title = "uuid") + @TableField(value = "uuid") + private String uuid; + + @Schema(title = "md5") + @TableField(value = "md5") + private String md5; + + @Schema(title = "file extension") + @TableField(value = "ext") + private String ext; + + @Schema(title = "路径") + @TableField(value = "path") + private String path; + + @Schema(title = "引用数量") + @TableField(value = "ref_count") + private Integer refCount; + + @Schema(title = "是否删除(0:未删除,1:已删除)") + @TableField(value = "is_delete") + private Boolean isDelete; +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/entity/AiImage.java b/adi-common/src/main/java/com/moyz/adi/common/entity/AiImage.java new file mode 100644 index 0000000..3a30a5e --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/entity/AiImage.java @@ -0,0 +1,51 @@ +package com.moyz.adi.common.entity; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +@TableName("adi_ai_image") +@Schema(title = "ai images", description = "Image generated by ai") +public class AiImage extends BaseEntity { + + @TableField("user_id") + private Long userId; + + @TableField("uuid") + private String uuid; + + @TableField("prompt") + private String prompt; + + @TableField("generate_size") + private String generateSize; + + @TableField("generate_number") + private Integer generateNumber; + + @Schema(title = "file uuid") + @TableField("original_image") + private String originalImage; + + @Schema(title = "file uuid") + @TableField("mask_image") + private String maskImage; + + @TableField("resp_images_path") + private String respImagesPath; + + @Schema(title = "generated image uuids") + @TableField("generated_images") + private String generatedImages; + + @TableField("interacting_method") + private Integer interactingMethod; + + @TableField("process_status") + private Integer processStatus; + + @TableField("is_delete") + private Boolean isDelete; +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/entity/AiModel.java b/adi-common/src/main/java/com/moyz/adi/common/entity/AiModel.java new file mode 100644 index 0000000..4fd4301 --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/entity/AiModel.java @@ -0,0 +1,25 @@ +package com.moyz.adi.common.entity; + +import com.moyz.adi.common.enums.AiModelStatus; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +@TableName("adi_ai_model") +@Schema(title = "AiModel对象", description = "AI模型表") +public class AiModel extends BaseEntity { + + @Schema(title = "模型名称") + @TableField("name") + private String name; + + @Schema(title = "说明") + @TableField("remark") + private String remark; + + @Schema(title = "状态(1:正常使用,2:不可用)") + @TableField("model_status") + private AiModelStatus modelStatus; +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/entity/BaseEntity.java b/adi-common/src/main/java/com/moyz/adi/common/entity/BaseEntity.java new file mode 100644 index 0000000..b96a84c --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/entity/BaseEntity.java @@ -0,0 +1,29 @@ +package com.moyz.adi.common.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +import java.io.Serializable; +import java.time.LocalDateTime; + +@Getter +@Setter +@ToString +public class BaseEntity implements Serializable { + + private static final long serialVersionUID = 1L; + + @TableId(type = IdType.AUTO) + private Long id; + + @TableField(value = "create_time") + private LocalDateTime createTime; + + @TableField(value = "update_time") + private LocalDateTime updateTime; + +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/entity/Conversation.java b/adi-common/src/main/java/com/moyz/adi/common/entity/Conversation.java new file mode 100644 index 0000000..d3e1384 --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/entity/Conversation.java @@ -0,0 +1,51 @@ +package com.moyz.adi.common.entity; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + *

+ * 会话表 + *

+ * + * @author moyz + * @since 2023-04-11 + */ +@Data +@TableName("adi_conversation") +@Schema(title = "对话实体", description = "对话表") +public class Conversation extends BaseEntity { + + @Schema(title = "用户id") + @TableField("user_id") + private Long userId; + + @Schema(title = "对话uuid") + @TableField("uuid") + private String uuid; + + @Schema(title = "会话标题") + @TableField("title") + private String title; + + @Schema(title = "消耗的token数量") + @TableField("tokens") + private Integer tokens; + + @Schema(title = "ai model name") + @TableField("ai_model") + private String aiModel; + + @Schema(name = "是否开启理解上下文的功能") + @TableField("understand_context_enable") + private Boolean understandContextEnable; + + @Schema(title = "set the system message to ai, ig: you are a lawyer") + @TableField("ai_system_message") + private String aiSystemMessage; + + @TableField(value = "is_delete") + private Boolean isDelete; +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/entity/ConversationMessage.java b/adi-common/src/main/java/com/moyz/adi/common/entity/ConversationMessage.java new file mode 100644 index 0000000..1f1ee2d --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/entity/ConversationMessage.java @@ -0,0 +1,63 @@ +package com.moyz.adi.common.entity; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + *

+ * + *

+ * + * @author moyz + * @since 2023-04-11 + */ +@Data +@TableName("adi_conversation_message") +@Schema(title = "ConversationMessage对象") +public class ConversationMessage extends BaseEntity { + + @Schema(title = "消息的uuid") + @TableField("uuid") + private String uuid; + + @Schema(title = "父级消息id") + @TableField("parent_message_id") + private Long parentMessageId; + + @Schema(title = "对话id") + @TableField("conversation_id") + private Long conversationId; + + @Schema(title = "对话uuid") + @TableField("conversation_uuid") + private String conversationUuid; + + @Schema(title = "用户id") + @TableField("user_id") + private Long userId; + + @Schema(title = "对话的消息") + @TableField("content") + private String content; + + @Schema(title = "产生该消息的角色:1: 用户,2:系统,3:助手") + @TableField("message_role") + private String messageRole; + + @Schema(title = "消耗的token数量") + @TableField("tokens") + private Integer tokens; + + @Schema(title = "secret key type(1:system secret key,2:custom secret key)") + @TableField(value = "secret_key_type") + private Integer secretKeyType; + + @Schema(name = "上下文理解中携带的消息对数量(提示词及回复)") + @TableField("understand_context_msg_pair_num") + private Integer understandContextMsgPairNum; + + @TableField(value = "is_delete") + private Boolean isDelete; +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/entity/Prompt.java b/adi-common/src/main/java/com/moyz/adi/common/entity/Prompt.java new file mode 100644 index 0000000..d384ea0 --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/entity/Prompt.java @@ -0,0 +1,28 @@ +package com.moyz.adi.common.entity; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +@TableName("adi_prompt") +@Schema(title = "提示词实体") +public class Prompt extends BaseEntity { + + @Schema(title = "用户id") + @TableField(value = "user_id") + private Long userId; + + @Schema(title = "标题") + @TableField(value = "act") + private String act; + + @Schema(title = "内容") + @TableField(value = "prompt") + private String prompt; + + @Schema(title = "是否删除(0:未删除,1:已删除)") + @TableField(value = "is_delete") + private Boolean isDelete; +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/entity/SysConfig.java b/adi-common/src/main/java/com/moyz/adi/common/entity/SysConfig.java new file mode 100644 index 0000000..9c93779 --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/entity/SysConfig.java @@ -0,0 +1,23 @@ +package com.moyz.adi.common.entity; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +@TableName("adi_sys_config") +@Schema(title = "系统配置表") +public class SysConfig extends BaseEntity { + + @Schema(title = "配置名称") + @TableField("name") + private String name; + + @Schema(title = "配置项的值") + private String value; + + @Schema(title = "是否删除(0:未删除,1:已删除)") + @TableField(value = "is_delete") + private Boolean isDelete; +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/entity/User.java b/adi-common/src/main/java/com/moyz/adi/common/entity/User.java new file mode 100644 index 0000000..4631932 --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/entity/User.java @@ -0,0 +1,67 @@ +package com.moyz.adi.common.entity; + +import com.moyz.adi.common.enums.UserStatusEnum; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Data +@TableName("adi_user") +@Schema(title = "User对象") +public class User extends BaseEntity { + + @Schema(name = "用户名称") + @TableField("name") + private String name; + + @TableField("email") + private String email; + + @TableField("password") + private String password; + + @TableField("uuid") + private String uuid; + + @Schema(name = "openai secret key") + @TableField("secret_key") + private String secretKey; + + @Schema(name = "上下文理解中需要携带的消息对数量(提示词及回复)") + @TableField("understand_context_msg_pair_num") + private Integer understandContextMsgPairNum; + + @Schema(name = "token quota in one day") + @TableField("quota_by_token_daily") + private Integer quotaByTokenDaily; + + @Schema(name = "token quota in one month") + @TableField("quota_by_token_monthly") + private Integer quotaByTokenMonthly; + + @Schema(name = "request quota in one day") + @TableField("quota_by_request_daily") + private Integer quotaByRequestDaily; + + @Schema(name = "request quota in one month") + @TableField("quota_by_request_monthly") + private Integer quotaByRequestMonthly; + + @TableField("quota_by_image_daily") + private Integer quotaByImageDaily; + + @TableField("quota_by_image_monthly") + private Integer quotaByImageMonthly; + + @TableField("user_status") + private UserStatusEnum userStatus; + + @TableField("active_time") + private LocalDateTime activeTime; + + @TableField("is_delete") + private Boolean isDelete; +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/entity/UserDayCost.java b/adi-common/src/main/java/com/moyz/adi/common/entity/UserDayCost.java new file mode 100644 index 0000000..f44c433 --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/entity/UserDayCost.java @@ -0,0 +1,35 @@ +package com.moyz.adi.common.entity; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +@TableName("adi_user_day_cost") +@Schema(title = "用户每天使用量") +public class UserDayCost extends BaseEntity { + @Schema(title = "用户id") + @TableField(value = "user_id") + private Long userId; + + @Schema(title = "日期") + @TableField(value = "day") + private Integer day; + + @Schema(title = "请求量") + @TableField(value = "requests") + private Integer requests; + + @Schema(title = "token数量") + @TableField(value = "tokens") + private Integer tokens; + + @Schema(title = "The number of generated images") + @TableField(value = "images_number") + private Integer imagesNumber; + + @Schema(title = "secret key type(1:system secret key,2:custom secret key)") + @TableField(value = "secret_key_type") + private Integer secretKeyType; +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/enums/AiModelStatus.java b/adi-common/src/main/java/com/moyz/adi/common/enums/AiModelStatus.java new file mode 100644 index 0000000..47a3f7c --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/enums/AiModelStatus.java @@ -0,0 +1,14 @@ +package com.moyz.adi.common.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum AiModelStatus implements BaseEnum { + ACTIVE(1, "启用"), + INACTIVE(2, "停用"); + + private final Integer value; + private final String desc; +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/enums/BaseEnum.java b/adi-common/src/main/java/com/moyz/adi/common/enums/BaseEnum.java new file mode 100644 index 0000000..77b986b --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/enums/BaseEnum.java @@ -0,0 +1,12 @@ +package com.moyz.adi.common.enums; + +import com.baomidou.mybatisplus.annotation.IEnum; + +public interface BaseEnum extends IEnum { + /** + * 获取对应名称 + * + * @return String + */ + String getDesc(); +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/enums/ErrorEnum.java b/adi-common/src/main/java/com/moyz/adi/common/enums/ErrorEnum.java new file mode 100644 index 0000000..af190e3 --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/enums/ErrorEnum.java @@ -0,0 +1,67 @@ +package com.moyz.adi.common.enums; + +public enum ErrorEnum { + SUCCESS("00000", "成功"), + A_URL_NOT_FOUND("A0001", "地址不存在"), + A_PARAMS_ERROR("A0002", "参数校验不通过"), + A_REQUEST_TOO_MUCH("A0003", "访问次数太多"), + A_LOGIN_ERROR("A0004", "登陆失败,账号或密码错误"), + A_LOGIN_ERROR_MAX("A0005", "失败次数太多,请输入验证码重试"), + A_LOGIN_CAPTCHA_ERROR("A0006", "验证码不正确"), + A_USER_NOT_EXIST("A0007", "用户不存在"), + A_CONVERSATION_NOT_EXIST("A0008", "对话不存在"), + A_IMAGE_NUMBER_ERROR("A0009", "图片数量不对"), + A_IMAGE_SIZE_ERROR("A0010", "图片尺寸不对"), + A_FILE_NOT_EXIST("A0011", "文件不存在"), + A_DRAWING("A0012", "作图还未完成"), + A_REGISTER_USER_EXIST("A0013", "账号已经存在,请使用账号密码登录"), + A_FIND_PASSWORD_CODE_ERROR("A0014", "重置码已过期或不存在"), + A_USER_WAIT_CONFIRM("A0015", "用户未激活"), + B_UNCAUGHT_ERROR("B0001", "未捕捉异常"), + B_COMMON_ERROR("B0002", "业务出错"), + B_GLOBAL_ERROR("B0003", "全局异常"), + B_SAVE_IMAGE_ERROR("B0004", "保存图片异常"), + B_FIND_IMAGE_404("B0005", "无法找到图片"), + B_DAILY_QUOTA_USED("B0006", "今天额度已经用完"), + B_MONTHLY_QUOTA_USED("B0007", "当月额度已经用完"), + + B_MESSAGE_NOT_FOUND("B0008", "消息不存在"); + + private String code; + private String info; + + ErrorEnum(String code, String info) { + this.code = code; + this.info = info; + } + + public static ErrorEnum getErrorEnum(String code) { + ErrorEnum result = null; + for (ErrorEnum c : ErrorEnum.values()) { + if (c.getCode().equals(code)) { + result = c; + break; + } + } + if (null == result) { + result = B_COMMON_ERROR; + } + return result; + } + + public String getCode() { + return code; + } + + public void setCode(String code) { + this.code = code; + } + + public String getInfo() { + return info; + } + + public void setInfo(String info) { + this.info = info; + } +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/enums/UserStatusEnum.java b/adi-common/src/main/java/com/moyz/adi/common/enums/UserStatusEnum.java new file mode 100644 index 0000000..7026d58 --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/enums/UserStatusEnum.java @@ -0,0 +1,17 @@ +package com.moyz.adi.common.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum UserStatusEnum implements BaseEnum { + + WAIT_CONFIRM(1, "待验证"), + NORMAL(2, "正常"), + FREEZE(3, "冻结"); + + private final Integer value; + private final String desc; + +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/exception/BaseException.java b/adi-common/src/main/java/com/moyz/adi/common/exception/BaseException.java new file mode 100644 index 0000000..daf5494 --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/exception/BaseException.java @@ -0,0 +1,57 @@ +package com.moyz.adi.common.exception; + + +import com.moyz.adi.common.enums.ErrorEnum; + +import java.text.MessageFormat; + +public class BaseException extends RuntimeException { + private String code; + private String info; + + private Object data; + + public BaseException(String code, String info) { + super(code + ":" + info); + this.code = code; + this.info = info; + } + + public BaseException(ErrorEnum errorEnum, String... infoValues) { + super(errorEnum.getCode() + ":" + MessageFormat.format(errorEnum.getInfo(), infoValues)); + this.code = errorEnum.getCode(); + if (infoValues.length > 0) { + this.info = MessageFormat.format(errorEnum.getInfo(), infoValues); + } else { + this.info = errorEnum.getInfo(); + } + } + + public String getCode() { + return code; + } + + public void setCode(String code) { + this.code = code; + } + + public String getInfo() { + return info; + } + + public void setInfo(String info) { + this.info = info; + } + + public Object getData() { + if (null != data) { + return data; + } + return getMessage(); + } + + public BaseException setData(Object data) { + this.data = data; + return this; + } +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/filter/HttpServletRequestReplacedFilter.java b/adi-common/src/main/java/com/moyz/adi/common/filter/HttpServletRequestReplacedFilter.java new file mode 100644 index 0000000..0f6ba73 --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/filter/HttpServletRequestReplacedFilter.java @@ -0,0 +1,33 @@ +package com.moyz.adi.common.filter; + +import com.moyz.adi.common.config.RequestReaderHttpServletRequestWrapper; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +/** + * 过滤器 + * 当前主要是配合LogInterceptor使用,已使用ControllerParamsLogAspect代替LogInterceptor + */ +@Slf4j +//@Component +@Order(Ordered.HIGHEST_PRECEDENCE) +public class HttpServletRequestReplacedFilter extends OncePerRequestFilter { + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + log.info("HttpServletRequestReplacedFilter:" + request.getRequestURI()); + ServletRequest requestWrapper = new RequestReaderHttpServletRequestWrapper(request); + filterChain.doFilter(requestWrapper, response); + } + +} \ No newline at end of file diff --git a/adi-common/src/main/java/com/moyz/adi/common/filter/TokenFilter.java b/adi-common/src/main/java/com/moyz/adi/common/filter/TokenFilter.java new file mode 100644 index 0000000..7d78be9 --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/filter/TokenFilter.java @@ -0,0 +1,95 @@ +package com.moyz.adi.common.filter; + +import com.moyz.adi.common.base.ThreadContext; +import com.moyz.adi.common.cosntant.AdiConstant; +import com.moyz.adi.common.cosntant.RedisKeyConstant; +import com.moyz.adi.common.entity.User; +import com.moyz.adi.common.util.JsonUtil; +import io.micrometer.common.util.StringUtils; +import jakarta.annotation.Resource; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; +import java.text.MessageFormat; +import java.util.Arrays; + +import static org.springframework.http.HttpHeaders.AUTHORIZATION; + +@Slf4j +@Component +public class TokenFilter extends OncePerRequestFilter { + + public static final String[] EXCLUDE_API = { + "/auth/", + }; + + @Resource + private StringRedisTemplate stringRedisTemplate; + + @Value("${server.servlet.context-path:}") + private String contextPath; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + String requestUri = request.getRequestURI(); + if (excludePath(requestUri)) { + filterChain.doFilter(request, response); + return; + } + if (null == request.getCookies()) { + log.warn("未授权:{}", requestUri); + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + return; + } + Cookie cookie = Arrays.stream(request.getCookies()).filter(item -> item.getName().equals(AUTHORIZATION)).findFirst().orElse(null); + if (null == cookie || StringUtils.isBlank(cookie.getValue())) { + log.warn("未授权:{}", requestUri); + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + return; + } + String token = cookie.getValue(); + String tokenKey = MessageFormat.format(RedisKeyConstant.USER_TOKEN, token); + String userJson = stringRedisTemplate.opsForValue().get(tokenKey); + if (StringUtils.isBlank(userJson)) { + log.warn("未登录:{}", requestUri); + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + return; + } + + User user = JsonUtil.fromJson(userJson, User.class); + if (null == user) { + log.warn("用户不存在:{}", requestUri); + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + return; + } + ThreadContext.setCurrentUser(user); + ThreadContext.setToken(token); + log.info("response::" + response); + filterChain.doFilter(request, response); + } + + private boolean excludePath(String requestUri) { + for (String path : EXCLUDE_API) { + if (requestUri.startsWith(contextPath + path)) { +// log.info("path exclude{}", requestUri); + return true; + } + } + for (String path : AdiConstant.WEB_RESOURCES) { + if (requestUri.startsWith(contextPath + path) || requestUri.endsWith(path)) { +// log.info("path exclude{}", requestUri); + return true; + } + } + return false; + } +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/helper/AdiMailSender.java b/adi-common/src/main/java/com/moyz/adi/common/helper/AdiMailSender.java new file mode 100644 index 0000000..ded93af --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/helper/AdiMailSender.java @@ -0,0 +1,56 @@ +package com.moyz.adi.common.helper; + +import jakarta.annotation.PostConstruct; +import jakarta.annotation.Resource; +import jakarta.mail.internet.InternetAddress; +import jakarta.mail.internet.MimeMessage; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.MimeMessageHelper; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +public class AdiMailSender { + + @Resource + private JavaMailSender javaMailSender; + + @Value("${spring.mail.username}") + private String from; + + public void send(String subject, String content, String to) { + log.info("mail sender:{}", from); + if (StringUtils.isAnyBlank(from, to)) { + return; + } + MimeMessage message = javaMailSender.createMimeMessage(); + try { + MimeMessageHelper helper = new MimeMessageHelper(message, true); + // 设置发件人名称和地址 + InternetAddress fromAddress = new InternetAddress(from, "AIDeepIn"); + helper.setFrom(fromAddress); + + // 设置收件人、主题、内容等其他信息 + helper.setTo(to); + helper.setSubject(subject); + helper.setText(content); + + javaMailSender.send(message); + } catch (Exception e) { + log.error("发送邮件时发生异常", e); + } +// SimpleMailMessage message = new SimpleMailMessage(); +// message.setFrom(from); +// message.setTo(to); +// message.setSubject(subject); +// message.setText(content); +// try { +// javaMailSender.send(message); +// } catch (Exception e) { +// log.error("发送邮件时发生异常", e); +// } + } +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/helper/HttpHelper.java b/adi-common/src/main/java/com/moyz/adi/common/helper/HttpHelper.java new file mode 100644 index 0000000..ff60ea6 --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/helper/HttpHelper.java @@ -0,0 +1,45 @@ +package com.moyz.adi.common.helper; + +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.Charset; + +@Slf4j +public class HttpHelper { + public static String getBodyString(HttpServletRequest request) throws IOException { + StringBuilder sb = new StringBuilder(); + InputStream inputStream = null; + BufferedReader reader = null; + try { + inputStream = request.getInputStream(); + reader = new BufferedReader(new InputStreamReader(inputStream, Charset.forName("UTF-8"))); + String line = ""; + while ((line = reader.readLine()) != null) { + sb.append(line); + } + } catch (IOException e) { + log.error("error", e); + } finally { + if (inputStream != null) { + try { + inputStream.close(); + } catch (IOException e) { + log.error("error", e); + } + } + if (reader != null) { + try { + reader.close(); + } catch (IOException e) { + log.error("error", e); + } + } + } + return sb.toString(); + } +} \ No newline at end of file diff --git a/adi-common/src/main/java/com/moyz/adi/common/helper/OpenAiHelper.java b/adi-common/src/main/java/com/moyz/adi/common/helper/OpenAiHelper.java new file mode 100644 index 0000000..bd7d402 --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/helper/OpenAiHelper.java @@ -0,0 +1,231 @@ +package com.moyz.adi.common.helper; + +import com.didalgo.gpt3.ChatFormatDescriptor; +import com.didalgo.gpt3.Encoding; +import com.didalgo.gpt3.GPT3Tokenizer; +import com.didalgo.gpt3.TokenCount; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.moyz.adi.common.cosntant.AdiConstant; +import com.moyz.adi.common.entity.AiImage; +import com.moyz.adi.common.entity.User; +import com.moyz.adi.common.enums.ErrorEnum; +import com.moyz.adi.common.exception.BaseException; +import com.moyz.adi.common.model.AnswerMeta; +import com.moyz.adi.common.model.ChatMeta; +import com.moyz.adi.common.model.QuestionMeta; +import com.moyz.adi.common.service.FileService; +import com.moyz.adi.common.service.SysConfigService; +import com.moyz.adi.common.util.FileUtil; +import com.moyz.adi.common.util.ImageUtil; +import com.moyz.adi.common.util.JsonUtil; +import com.moyz.adi.common.util.TriConsumer; +import com.theokanning.openai.OpenAiApi; +import com.theokanning.openai.completion.chat.ChatCompletionChoice; +import com.theokanning.openai.completion.chat.ChatCompletionRequest; +import com.theokanning.openai.completion.chat.ChatMessage; +import com.theokanning.openai.image.*; +import com.theokanning.openai.service.OpenAiService; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import okhttp3.OkHttpClient; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; +import retrofit2.Retrofit; + +import java.io.File; +import java.net.InetSocketAddress; +import java.net.Proxy; +import java.time.Duration; +import java.time.temporal.ChronoUnit; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.UUID; + +import static com.moyz.adi.common.cosntant.AdiConstant.OPENAI_CREATE_IMAGE_RESP_FORMATS_URL; +import static com.moyz.adi.common.cosntant.AdiConstant.OPENAI_CREATE_IMAGE_SIZES; +import static com.theokanning.openai.service.OpenAiService.defaultClient; +import static com.theokanning.openai.service.OpenAiService.defaultRetrofit; + +@Slf4j +@Service +public class OpenAiHelper { + + @Value("${openai.proxy.enable:false}") + private boolean proxyEnable; + + @Value("${openai.proxy.host:0}") + private String proxyHost; + + @Value("${openai.proxy.http-port:0}") + private int proxyHttpPort; + + @Value("${local.images}") + private String localImagesPath; + + @Resource + private FileService fileService; + + @Resource + private ObjectMapper objectMapper; + + public OpenAiService getOpenAiService(User user) { + String secretKey = SysConfigService.getSecretKey(); + String userSecretKey = user.getSecretKey(); + if (StringUtils.isNotBlank(userSecretKey)) { + secretKey = userSecretKey; + } + if (proxyEnable) { + Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(proxyHost, proxyHttpPort)); + OkHttpClient client = defaultClient(secretKey, Duration.of(60, ChronoUnit.SECONDS)) + .newBuilder() + .proxy(proxy) + .build(); + Retrofit retrofit = defaultRetrofit(client, objectMapper); + OpenAiApi api = retrofit.create(OpenAiApi.class); + return new OpenAiService(api); + } + return new OpenAiService(secretKey, Duration.of(60, ChronoUnit.SECONDS)); + } + + /** + * Send http request to openai server
+ * Calculate token + * + * @param user + * @param regenerateQuestionUuid + * @param chatMessageList + * @param sseEmitter + * @param consumer + */ + public void sseAsk(User user, String regenerateQuestionUuid, List chatMessageList, SseEmitter sseEmitter, TriConsumer consumer) { + final int[] answerTokens = {0}; + StringBuilder response = new StringBuilder(); + OpenAiService service = getOpenAiService(user); + ChatCompletionRequest chatCompletionRequest = ChatCompletionRequest + .builder() + .model(AdiConstant.DEFAULT_MODEL) + .messages(chatMessageList) + .n(1) + .logitBias(new HashMap<>()) + .build(); + service.streamChatCompletion(chatCompletionRequest) + .doOnError(onError -> { + log.error("openai error", onError); + sseEmitter.send(SseEmitter.event().name("error").data(onError.getMessage())); + sseEmitter.complete(); + }).subscribe(completionChunk -> { + answerTokens[0]++; + List choices = completionChunk.getChoices(); + String content = choices.get(0).getMessage().getContent(); + log.info("get content:{}", content); + if (null == content && response.isEmpty()) { + return; + } + if (null == content || AdiConstant.OPENAI_MESSAGE_DONE_FLAG.equals(content)) { + log.info("OpenAI返回数据结束了"); + sseEmitter.send(AdiConstant.OPENAI_MESSAGE_DONE_FLAG); + + + GPT3Tokenizer tokenizer = new GPT3Tokenizer(Encoding.CL100K_BASE); + int questionTokens = 0; + try { + questionTokens = TokenCount.fromMessages(chatMessageList, tokenizer, ChatFormatDescriptor.forModel(AdiConstant.DEFAULT_MODEL)); + } catch (IllegalArgumentException e) { + log.error("该模型的token无法统计,model:{}", AdiConstant.DEFAULT_MODEL); + } + System.out.println("requestTokens:" + questionTokens); + System.out.println("返回内容:" + response); + + String questionUuid = StringUtils.isNotBlank(regenerateQuestionUuid) ? regenerateQuestionUuid : UUID.randomUUID().toString().replace("-", ""); + QuestionMeta questionMeta = new QuestionMeta(questionTokens, questionUuid); + AnswerMeta answerMeta = new AnswerMeta(answerTokens[0], UUID.randomUUID().toString().replace("-", "")); + ChatMeta chatMeta = new ChatMeta(questionMeta, answerMeta); +// String meta = JsonUtil.toJson(chatMeta).replaceAll("\r\n", ""); + String meta = JsonUtil.toJson(chatMeta).replaceAll("\r\n", ""); + log.info("meta:" + meta); + sseEmitter.send(" [META]" + meta); + // close eventSourceEmitter after tokens was calculated + sseEmitter.complete(); + consumer.accept(response.toString(), questionMeta, answerMeta); + return; + } + //加空格配合前端的fetchEventSource进行解析,见https://github.com/Azure/fetch-event-source/blob/45ac3cfffd30b05b79fbf95c21e67d4ef59aa56a/src/parse.ts#L129-L133 + sseEmitter.send(" " + content); + response.append(content); + }); + + + System.out.println("返回内容1111:" + response); + } + + public List createImage(User user, AiImage aiImage) { + if (aiImage.getGenerateNumber() < 1 || aiImage.getGenerateNumber() > 10) { + throw new BaseException(ErrorEnum.A_IMAGE_NUMBER_ERROR); + } + if (!OPENAI_CREATE_IMAGE_SIZES.contains(aiImage.getGenerateSize())) { + throw new BaseException(ErrorEnum.A_IMAGE_SIZE_ERROR); + } + OpenAiService service = getOpenAiService(user); + CreateImageRequest createImageRequest = new CreateImageRequest(); + createImageRequest.setPrompt(aiImage.getPrompt()); + createImageRequest.setN(aiImage.getGenerateNumber()); + createImageRequest.setSize(aiImage.getGenerateSize()); + createImageRequest.setResponseFormat(OPENAI_CREATE_IMAGE_RESP_FORMATS_URL); + createImageRequest.setUser(user.getUuid()); + try { + ImageResult imageResult = service.createImage(createImageRequest); + log.info("createImage response:{}", imageResult); + return imageResult.getData(); + } catch (Exception e) { + log.error("create image error", e); + } + return Collections.emptyList(); + } + + public List editImage(User user, AiImage aiImage) { + File originalFile = new File(fileService.getImagePath(aiImage.getOriginalImage())); + File maskFile = null; + if (StringUtils.isNotBlank(aiImage.getMaskImage())) { + maskFile = new File(fileService.getImagePath(aiImage.getMaskImage())); + } + //如果不是RGBA类型的图片,先转成RGBA + File rgbaOriginalImage = ImageUtil.rgbConvertToRgba(originalFile, fileService.getTmpImagesPath(aiImage.getOriginalImage())); + OpenAiService service = getOpenAiService(user); + CreateImageEditRequest request = new CreateImageEditRequest(); + request.setPrompt(aiImage.getPrompt()); + request.setN(aiImage.getGenerateNumber()); + request.setSize(aiImage.getGenerateSize()); + request.setResponseFormat(OPENAI_CREATE_IMAGE_RESP_FORMATS_URL); + request.setUser(user.getUuid()); + try { + ImageResult imageResult = service.createImageEdit(request, rgbaOriginalImage, maskFile); + log.info("editImage response:{}", imageResult); + return imageResult.getData(); + } catch (Exception e) { + log.error("edit image error", e); + } + return Collections.emptyList(); + } + + public List createImageVariation(User user, AiImage aiImage) { + File imagePath = new File(fileService.getImagePath(aiImage.getOriginalImage())); + OpenAiService service = getOpenAiService(user); + CreateImageVariationRequest request = new CreateImageVariationRequest(); + request.setN(aiImage.getGenerateNumber()); + request.setSize(aiImage.getGenerateSize()); + request.setResponseFormat(OPENAI_CREATE_IMAGE_RESP_FORMATS_URL); + request.setUser(user.getUuid()); + try { + ImageResult imageResult = service.createImageVariation(request, imagePath); + log.info("createImageVariation response:{}", imageResult); + return imageResult.getData(); + } catch (Exception e) { + log.error("image variation error", e); + } + return Collections.emptyList(); + } + +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/helper/QuotaHelper.java b/adi-common/src/main/java/com/moyz/adi/common/helper/QuotaHelper.java new file mode 100644 index 0000000..6be34f7 --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/helper/QuotaHelper.java @@ -0,0 +1,64 @@ +package com.moyz.adi.common.helper; + +import com.moyz.adi.common.entity.User; +import com.moyz.adi.common.enums.ErrorEnum; +import com.moyz.adi.common.model.CostStat; +import com.moyz.adi.common.service.UserDayCostService; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +public class QuotaHelper { + + @Resource + private UserDayCostService userDayCostService; + + public ErrorEnum checkTextQuota(User user) { + if (StringUtils.isNotBlank(user.getSecretKey())) { + log.info("Custom secret key,dont need to check text request quota,userId:{}", user.getId()); + return null; + } + int userQuotaByTokenDay = user.getQuotaByTokenDaily(); + int userQuotaByTokenMonth = user.getQuotaByTokenMonthly(); + int userQuotaByRequestDay = user.getQuotaByRequestDaily(); + int userQuotaByRequestMonth = user.getQuotaByRequestMonthly(); + CostStat costStat = userDayCostService.costStatByUser(user.getId()); + if (costStat.getTextTokenCostByDay() >= userQuotaByTokenDay || costStat.getTextRequestTimesByDay() >= userQuotaByRequestDay) { + log.warn("Reach limit of a day,userId:{},token:{},request:{},used token:{}, used request:{}", user.getId(), userQuotaByRequestDay, userQuotaByRequestDay, userQuotaByTokenMonth, userQuotaByRequestMonth); + return ErrorEnum.B_DAILY_QUOTA_USED; + } + if (costStat.getTextTokenCostByMonth() >= user.getQuotaByTokenMonthly() || costStat.getTextRequestTimesByMonth() >= user.getQuotaByRequestMonthly()) { + log.warn("Reach limit of a month,userId:{},token:{},request:{},used token:{}, used request:{}", user.getId(), user.getQuotaByTokenMonthly(), user.getQuotaByRequestMonthly(), costStat.getTextTokenCostByMonth(), costStat.getTextRequestTimesByMonth()); + return ErrorEnum.B_MONTHLY_QUOTA_USED; + } + return null; + } + + /** + * Check the generate image request if it can be accepted + * + * @param user + * @return + */ + public ErrorEnum checkImageQuota(User user) { + if (StringUtils.isNotBlank(user.getSecretKey())) { + log.info("Custom secret key,dont need to check image quota,userId:{}", user.getId()); + return null; + } + int userDailyQuota = user.getQuotaByImageDaily(); + int userMonthlyQuota = user.getQuotaByImageMonthly(); + CostStat costStat = userDayCostService.costStatByUser(user.getId()); + if (costStat.getImageGeneratedNumberByDay() >= userDailyQuota) { + log.warn("Generate image reach limit of a day,userId:{},request quota:{},used request times:{}", user.getId(), userDailyQuota, costStat.getImageGeneratedNumberByDay()); + return ErrorEnum.B_DAILY_QUOTA_USED; + } + if (costStat.getImageGeneratedNumberByMonth() >= userMonthlyQuota) { + log.warn("Generate image reach limit of a month,userId:{},token:{},request quota:{},used request times:{}", user.getId(), user.getQuotaByImageMonthly(), costStat.getImageGeneratedNumberByMonth()); + return ErrorEnum.B_MONTHLY_QUOTA_USED; + } + return null; + } +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/helper/RateLimitHelper.java b/adi-common/src/main/java/com/moyz/adi/common/helper/RateLimitHelper.java new file mode 100644 index 0000000..1e8734a --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/helper/RateLimitHelper.java @@ -0,0 +1,40 @@ +package com.moyz.adi.common.helper; + +import com.moyz.adi.common.model.RequestRateLimit; +import jakarta.annotation.Resource; +import org.apache.commons.lang3.StringUtils; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Service; + +import java.util.concurrent.TimeUnit; + +@Service +public class RateLimitHelper { + + @Resource + private StringRedisTemplate stringRedisTemplate; + + public boolean checkRequestTimes(String requestTimesKey, RequestRateLimit rateLimitConfig) { + int requestCountInTimeWindow = 0; + String rateLimitVal = stringRedisTemplate.opsForValue().get(requestTimesKey); + if (StringUtils.isNotBlank(rateLimitVal)) { + requestCountInTimeWindow = Integer.parseInt(rateLimitVal); + } + if (requestCountInTimeWindow >= rateLimitConfig.getTimes()) { + return false; + } + return true; + } + + public void increaseRequestTimes(String requestTimesKey, RequestRateLimit rateLimitConfig) { + long expireTime = stringRedisTemplate.getExpire(requestTimesKey).longValue(); + if (expireTime == -1) { + stringRedisTemplate.opsForValue().increment(requestTimesKey); + stringRedisTemplate.opsForValue().set(requestTimesKey, String.valueOf(1), rateLimitConfig.getMinutes(), TimeUnit.MINUTES); + } else if (expireTime > 3) { + //If expireTime <= 3, too short to cache + stringRedisTemplate.opsForValue().increment(requestTimesKey); + } + } + +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/mapper/AiImageMapper.java b/adi-common/src/main/java/com/moyz/adi/common/mapper/AiImageMapper.java new file mode 100644 index 0000000..7a69122 --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/mapper/AiImageMapper.java @@ -0,0 +1,9 @@ +package com.moyz.adi.common.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.moyz.adi.common.entity.AiImage; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface AiImageMapper extends BaseMapper { +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/mapper/AiModelMapper.java b/adi-common/src/main/java/com/moyz/adi/common/mapper/AiModelMapper.java new file mode 100644 index 0000000..205a932 --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/mapper/AiModelMapper.java @@ -0,0 +1,9 @@ +package com.moyz.adi.common.mapper; + +import com.moyz.adi.common.entity.AiModel; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface AiModelMapper extends BaseMapper { +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/mapper/ConversationMapper.java b/adi-common/src/main/java/com/moyz/adi/common/mapper/ConversationMapper.java new file mode 100644 index 0000000..0031045 --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/mapper/ConversationMapper.java @@ -0,0 +1,18 @@ +package com.moyz.adi.common.mapper; + +import com.moyz.adi.common.entity.Conversation; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; + +/** + *

+ * 会话表 Mapper 接口 + *

+ * + * @author moyz + * @since 2023-04-11 + */ +@Mapper +public interface ConversationMapper extends BaseMapper { + +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/mapper/ConversationMessageMapper.java b/adi-common/src/main/java/com/moyz/adi/common/mapper/ConversationMessageMapper.java new file mode 100644 index 0000000..9a69152 --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/mapper/ConversationMessageMapper.java @@ -0,0 +1,18 @@ +package com.moyz.adi.common.mapper; + +import com.moyz.adi.common.entity.ConversationMessage; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; + +/** + *

+ * Mapper 接口 + *

+ * + * @author moyz + * @since 2023-04-11 + */ +@Mapper +public interface ConversationMessageMapper extends BaseMapper { + +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/mapper/FileMapper.java b/adi-common/src/main/java/com/moyz/adi/common/mapper/FileMapper.java new file mode 100644 index 0000000..52417ce --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/mapper/FileMapper.java @@ -0,0 +1,9 @@ +package com.moyz.adi.common.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.moyz.adi.common.entity.AdiFile; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface FileMapper extends BaseMapper { +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/mapper/PromptMapper.java b/adi-common/src/main/java/com/moyz/adi/common/mapper/PromptMapper.java new file mode 100644 index 0000000..59c4261 --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/mapper/PromptMapper.java @@ -0,0 +1,9 @@ +package com.moyz.adi.common.mapper; + +import com.moyz.adi.common.entity.Prompt; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface PromptMapper extends BaseMapper { +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/mapper/SysConfigMapper.java b/adi-common/src/main/java/com/moyz/adi/common/mapper/SysConfigMapper.java new file mode 100644 index 0000000..f964cd7 --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/mapper/SysConfigMapper.java @@ -0,0 +1,9 @@ +package com.moyz.adi.common.mapper; + +import com.moyz.adi.common.entity.SysConfig; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface SysConfigMapper extends BaseMapper { +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/mapper/UserDayCostMapper.java b/adi-common/src/main/java/com/moyz/adi/common/mapper/UserDayCostMapper.java new file mode 100644 index 0000000..a1eb69b --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/mapper/UserDayCostMapper.java @@ -0,0 +1,9 @@ +package com.moyz.adi.common.mapper; + +import com.moyz.adi.common.entity.UserDayCost; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface UserDayCostMapper extends BaseMapper { +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/mapper/UserMapper.java b/adi-common/src/main/java/com/moyz/adi/common/mapper/UserMapper.java new file mode 100644 index 0000000..8edd1d5 --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/mapper/UserMapper.java @@ -0,0 +1,18 @@ +package com.moyz.adi.common.mapper; + +import com.moyz.adi.common.entity.User; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; + +/** + *

+ * 用户表 Mapper 接口 + *

+ * + * @author moyz + * @since 2023-04-11 + */ +@Mapper +public interface UserMapper extends BaseMapper { + +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/model/AnswerMeta.java b/adi-common/src/main/java/com/moyz/adi/common/model/AnswerMeta.java new file mode 100644 index 0000000..6b6f464 --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/model/AnswerMeta.java @@ -0,0 +1,11 @@ +package com.moyz.adi.common.model; + +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +public class AnswerMeta { + private Integer tokens; + private String uuid; +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/model/ChatMeta.java b/adi-common/src/main/java/com/moyz/adi/common/model/ChatMeta.java new file mode 100644 index 0000000..63acf80 --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/model/ChatMeta.java @@ -0,0 +1,11 @@ +package com.moyz.adi.common.model; + +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +public class ChatMeta { + private QuestionMeta question; + private AnswerMeta answer; +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/model/CostStat.java b/adi-common/src/main/java/com/moyz/adi/common/model/CostStat.java new file mode 100644 index 0000000..78dfb5b --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/model/CostStat.java @@ -0,0 +1,14 @@ +package com.moyz.adi.common.model; + +import lombok.Data; + +@Data +public class CostStat { + private int day; + private int textRequestTimesByDay; + private int textTokenCostByDay; + private int imageGeneratedNumberByDay; + private int textTokenCostByMonth; + private int textRequestTimesByMonth; + private int imageGeneratedNumberByMonth; +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/model/QuestionMeta.java b/adi-common/src/main/java/com/moyz/adi/common/model/QuestionMeta.java new file mode 100644 index 0000000..199c35d --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/model/QuestionMeta.java @@ -0,0 +1,11 @@ +package com.moyz.adi.common.model; + +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +public class QuestionMeta { + private Integer tokens; + private String uuid; +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/model/RequestRateLimit.java b/adi-common/src/main/java/com/moyz/adi/common/model/RequestRateLimit.java new file mode 100644 index 0000000..6984e96 --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/model/RequestRateLimit.java @@ -0,0 +1,13 @@ +package com.moyz.adi.common.model; + +import lombok.Data; + +@Data +public class RequestRateLimit { + private int times; + private int minutes; + private int type; + + public static final int TYPE_TEXT = 1; + public static final int TYPE_IMAGE = 2; +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/service/AiImageService.java b/adi-common/src/main/java/com/moyz/adi/common/service/AiImageService.java new file mode 100644 index 0000000..49f2bae --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/service/AiImageService.java @@ -0,0 +1,298 @@ +package com.moyz.adi.common.service; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.moyz.adi.common.base.ThreadContext; +import com.moyz.adi.common.cosntant.RedisKeyConstant; +import com.moyz.adi.common.dto.*; +import com.moyz.adi.common.entity.AdiFile; +import com.moyz.adi.common.entity.AiImage; +import com.moyz.adi.common.entity.User; +import com.moyz.adi.common.entity.UserDayCost; +import com.moyz.adi.common.enums.ErrorEnum; +import com.moyz.adi.common.exception.BaseException; +import com.moyz.adi.common.helper.OpenAiHelper; +import com.moyz.adi.common.helper.QuotaHelper; +import com.moyz.adi.common.helper.RateLimitHelper; +import com.moyz.adi.common.mapper.AiImageMapper; +import com.moyz.adi.common.util.LocalCache; +import com.moyz.adi.common.util.LocalDateTimeUtil; +import com.moyz.adi.common.util.UserUtil; +import com.theokanning.openai.image.Image; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Lazy; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; +import org.springframework.web.bind.annotation.RequestParam; + +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import static com.moyz.adi.common.cosntant.AdiConstant.GenerateImage.*; +import static com.moyz.adi.common.enums.ErrorEnum.*; + +@Slf4j +@Service +public class AiImageService extends ServiceImpl { + + @Resource + @Lazy + private AiImageService _this; + + @Resource + private OpenAiHelper openAiHelper; + + @Resource + private QuotaHelper quotaHelper; + + @Value("${local.images}") + private String localImagesPath; + + @Resource + private RateLimitHelper rateLimitHelper; + + @Resource + private UserDayCostService userDayCostService; + + @Resource + private StringRedisTemplate stringRedisTemplate; + + @Resource + private FileService fileService; + + public void check() { + User user = ThreadContext.getCurrentUser(); + String askingKey = MessageFormat.format(RedisKeyConstant.USER_DRAWING, user.getId()); + String askingVal = stringRedisTemplate.opsForValue().get(askingKey); + //check 1: still waiting response + if (StringUtils.isNotBlank(askingVal)) { + throw new BaseException(A_DRAWING); + } + + String requestTimesKey = MessageFormat.format(RedisKeyConstant.USER_REQUEST_TEXT_TIMES, user.getId()); + if (!rateLimitHelper.checkRequestTimes(requestTimesKey, LocalCache.TEXT_RATE_LIMIT_CONFIG)) { + throw new BaseException(A_REQUEST_TOO_MUCH); + } + ErrorEnum errorEnum = quotaHelper.checkImageQuota(user); + if (null != errorEnum) { + throw new BaseException(errorEnum); + } + } + + /** + * interacting method 1: Creates an image given a prompt + * + * @param generateImageReq + */ + public String createByPrompt(GenerateImageReq generateImageReq) { + _this.check(); + CreateImageDto createImageDto = new CreateImageDto(); + createImageDto.setInteractingMethod(INTERACTING_METHOD_GENERATE_IMAGE); + BeanUtils.copyProperties(generateImageReq, createImageDto); + return _this.createImage(createImageDto); + } + + /** + * Interacting method 2: Creates an edited or extended image given an original image and a prompt. + */ + public String editByOriginalImage(EditImageReq editImageReq) { + _this.check(); + CreateImageDto createImageDto = new CreateImageDto(); + createImageDto.setInteractingMethod(INTERACTING_METHOD_EDIT_IMAGE); + createImageDto.setPrompt(editImageReq.getPrompt()); + createImageDto.setSize(editImageReq.getSize()); + createImageDto.setNumber(editImageReq.getNumber()); + createImageDto.setMaskImage(editImageReq.getMaskImage()); + createImageDto.setOriginalImage(editImageReq.getOriginalImage()); + return _this.createImage(createImageDto); + } + + /** + * interacting method 3: Creates a variation of a given image. + */ + public String variationImage(VariationImageReq variationImageReq) { + _this.check(); + CreateImageDto createImageDto = new CreateImageDto(); + createImageDto.setInteractingMethod(INTERACTING_METHOD_VARIATION); + createImageDto.setSize(variationImageReq.getSize()); + createImageDto.setNumber(variationImageReq.getNumber()); + createImageDto.setOriginalImage(variationImageReq.getOriginalImage()); + return _this.createImage(createImageDto); + } + + public String createImage(CreateImageDto createImageDto) { + User user = ThreadContext.getCurrentUser(); + int generateNumber = Math.min(createImageDto.getNumber(), user.getQuotaByImageDaily()); + String uuid = UUID.randomUUID().toString().replace("-", ""); + AiImage aiImage = new AiImage(); + aiImage.setGenerateSize(createImageDto.getSize()); + aiImage.setGenerateNumber(generateNumber); + aiImage.setUuid(uuid); + aiImage.setUserId(user.getId()); + aiImage.setInteractingMethod(createImageDto.getInteractingMethod()); + aiImage.setProcessStatus(STATUS_DOING); + aiImage.setPrompt(createImageDto.getPrompt()); + aiImage.setOriginalImage(createImageDto.getOriginalImage()); + aiImage.setMaskImage(createImageDto.getMaskImage()); + getBaseMapper().insert(aiImage); + AiImage obj = this.lambdaQuery().eq(AiImage::getUuid, uuid).one(); + _this.createFromRemote(obj, user); + return uuid; + } + + /** + * Regenerate the image that was fail + * + * @param uuid + */ + public void regenerate(String uuid) { + User user = ThreadContext.getCurrentUser(); + AiImage obj = this.lambdaQuery() + .eq(AiImage::getUuid, uuid) + .eq(AiImage::getProcessStatus, STATUS_FAIL) + .oneOpt().orElseThrow(() -> new BaseException(B_FIND_IMAGE_404)); + + _this.createFromRemote(obj, user); + } + + @Async("imagesExecutor") + public void createFromRemote(AiImage aiImage, User user) { + String drawingKey = MessageFormat.format(RedisKeyConstant.USER_DRAWING, user.getId()); + stringRedisTemplate.opsForValue().set(drawingKey, "1", 30, TimeUnit.SECONDS); + + try { + //Increase the number of the request + String requestTimesKey = MessageFormat.format(RedisKeyConstant.USER_REQUEST_TEXT_TIMES, user.getId()); + rateLimitHelper.increaseRequestTimes(requestTimesKey, LocalCache.IMAGE_RATE_LIMIT_CONFIG); + + List images = new ArrayList<>(); + if (aiImage.getInteractingMethod() == INTERACTING_METHOD_GENERATE_IMAGE) { + images = openAiHelper.createImage(user, aiImage); + } else if (aiImage.getInteractingMethod() == INTERACTING_METHOD_EDIT_IMAGE) { + images = openAiHelper.editImage(user, aiImage); + } else if (aiImage.getInteractingMethod() == INTERACTING_METHOD_VARIATION) { + images = openAiHelper.createImageVariation(user, aiImage); + } + List imageUuids = new ArrayList(); + images.forEach(image -> { + String imageUuid = fileService.saveToLocal(user, image.getUrl()); + imageUuids.add(imageUuid); + }); + String imageUuidsJoin = imageUuids.stream().collect(Collectors.joining(",")); + if (StringUtils.isBlank(imageUuidsJoin)) { + _this.lambdaUpdate().eq(AiImage::getId, aiImage.getId()).set(AiImage::getProcessStatus, STATUS_FAIL).update(); + return; + } + String respImagesPath = images.stream().map(Image::getUrl).collect(Collectors.joining(",")); + updateAiImageStatus(aiImage.getId(), respImagesPath, imageUuidsJoin, STATUS_SUCCESS); + + //Update the cost of current user + UserDayCost userDayCost = userDayCostService.getTodayCost(user); + UserDayCost saveOrUpdateInst = new UserDayCost(); + if (null == userDayCost) { + saveOrUpdateInst.setUserId(user.getId()); + saveOrUpdateInst.setDay(LocalDateTimeUtil.getToday()); + saveOrUpdateInst.setImagesNumber(images.size()); + saveOrUpdateInst.setSecretKeyType(UserUtil.getSecretType(user)); + } else { + saveOrUpdateInst.setId(userDayCost.getId()); + saveOrUpdateInst.setImagesNumber(userDayCost.getImagesNumber() + images.size()); + } + userDayCostService.saveOrUpdate(saveOrUpdateInst); + } finally { + stringRedisTemplate.delete(drawingKey); + } + } + + public void updateAiImageStatus(Long aiImageId, String respImagesPath, String localImagesUuid, int generationStatus) { + AiImage updateImage = new AiImage(); + updateImage.setId(aiImageId); + updateImage.setRespImagesPath(respImagesPath); + updateImage.setGeneratedImages(localImagesUuid); + updateImage.setProcessStatus(generationStatus); + getBaseMapper().updateById(updateImage); + + AdiFile adiFile = fileService.lambdaQuery().eq(AdiFile::getUuid, localImagesUuid).oneOpt().orElse(null); + if (null != adiFile) { + fileService.lambdaUpdate().eq(AdiFile::getId, adiFile.getId()).set(AdiFile::getRefCount, adiFile.getRefCount() + 1).update(); + } + } + + + public AiImagesListResp listAll(@RequestParam Long maxId, @RequestParam int pageSize) { + List list = this.lambdaQuery() + .eq(AiImage::getUserId, ThreadContext.getCurrentUserId()) + .eq(AiImage::getIsDelete, false) + .lt(AiImage::getId, maxId) + .orderByDesc(AiImage::getId) + .last("limit " + pageSize) + .list(); + list.sort(Comparator.comparing(AiImage::getId)); + List dtoList = new ArrayList<>(); + list.forEach(item -> dtoList.add(convertAiImageToDto(item))); + AiImagesListResp result = new AiImagesListResp(); + result.setImageItems(dtoList); + result.setMinId(list.stream().map(AiImage::getId).reduce(Long.MAX_VALUE, Long::min)); + return result; + } + + public AiImageDto getOne(String uuid) { + AiImage aiImage = this.lambdaQuery() + .eq(AiImage::getUuid, uuid) + .eq(AiImage::getUserId, ThreadContext.getCurrentUserId()) + .oneOpt() + .orElse(null); + return convertAiImageToDto(aiImage); + } + + public boolean del(String uuid) { + AiImage aiImage = this.lambdaQuery() + .eq(AiImage::getUuid, uuid) + .eq(AiImage::getUserId, ThreadContext.getCurrentUserId()) + .oneOpt() + .orElse(null); + if (null == aiImage) { + return false; + } + if (StringUtils.isNotBlank(aiImage.getGeneratedImages())) { + String uuids[] = aiImage.getGeneratedImages().split(","); + for (String fileUuid : uuids) { + fileService.removeFileAndSoftDel(fileUuid); + } + } + return true; + } + + private AiImageDto convertAiImageToDto(AiImage aiImage) { + AiImageDto dto = new AiImageDto(); + BeanUtils.copyProperties(aiImage, dto); + fillImagesToDto(dto); + if (StringUtils.isNotBlank(aiImage.getOriginalImage())) { + dto.setOriginalImageUrl("/image/" + aiImage.getOriginalImage()); + } + if (StringUtils.isNotBlank(aiImage.getMaskImage())) { + dto.setMaskImageUrl("/image/" + aiImage.getMaskImage()); + } + return dto; + } + + private void fillImagesToDto(AiImageDto aiImageDto) { + List images = new ArrayList<>(); + aiImageDto.setImageUrlList(images); + if (StringUtils.isNotBlank(aiImageDto.getGeneratedImages())) { + String[] imageUuids = aiImageDto.getGeneratedImages().split(","); + for (String imageUuid : imageUuids) { + images.add("/image/" + imageUuid); + } + } + } +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/service/AiModelService.java b/adi-common/src/main/java/com/moyz/adi/common/service/AiModelService.java new file mode 100644 index 0000000..4ff35cf --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/service/AiModelService.java @@ -0,0 +1,44 @@ +package com.moyz.adi.common.service; + +import com.moyz.adi.common.enums.AiModelStatus; +import com.moyz.adi.common.entity.AiModel; +import com.moyz.adi.common.helper.OpenAiHelper; +import com.moyz.adi.common.mapper.AiModelMapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; + +@Service +public class AiModelService extends ServiceImpl { + + public final static List AI_MODELS = new ArrayList<>(); + + @Resource + private OpenAiHelper openAiHelper; + + @PostConstruct + public void init() { + List aiModels = this.lambdaQuery().eq(AiModel::getModelStatus, AiModelStatus.ACTIVE).list(); + AI_MODELS.addAll(aiModels); + + //get models from openai +// List openaiModels = openAiHelper.getModels(); +// for (Model model : openaiModels) { +// AiModel aiModel = this.lambdaQuery().eq(AiModel::getName, model.getId()).one(); +// if (null == aiModel) { +// aiModel = new AiModel(); +// aiModel.setName(model.getId()); +// aiModel.setModelStatus(AiModelStatus.INACTIVE); +// baseMapper.insert(aiModel); +// } +// } + //refresh models cache +// aiModels = this.lambdaQuery().eq(AiModel::getModelStatus, AiModelStatus.ACTIVE).list(); +// AI_MODELS.clear(); +// AI_MODELS.addAll(aiModels); + } +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/service/ConversationMessageService.java b/adi-common/src/main/java/com/moyz/adi/common/service/ConversationMessageService.java new file mode 100644 index 0000000..c73ff1b --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/service/ConversationMessageService.java @@ -0,0 +1,329 @@ +package com.moyz.adi.common.service; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.moyz.adi.common.base.ThreadContext; +import com.moyz.adi.common.cosntant.AdiConstant; +import com.moyz.adi.common.cosntant.RedisKeyConstant; +import com.moyz.adi.common.dto.AskReq; +import com.moyz.adi.common.entity.Conversation; +import com.moyz.adi.common.entity.ConversationMessage; +import com.moyz.adi.common.entity.User; +import com.moyz.adi.common.entity.UserDayCost; +import com.moyz.adi.common.enums.ErrorEnum; +import com.moyz.adi.common.exception.BaseException; +import com.moyz.adi.common.helper.OpenAiHelper; +import com.moyz.adi.common.helper.QuotaHelper; +import com.moyz.adi.common.helper.RateLimitHelper; +import com.moyz.adi.common.mapper.ConversationMessageMapper; +import com.moyz.adi.common.model.AnswerMeta; +import com.moyz.adi.common.model.QuestionMeta; +import com.moyz.adi.common.util.LocalCache; +import com.moyz.adi.common.util.LocalDateTimeUtil; +import com.moyz.adi.common.util.UserUtil; +import com.theokanning.openai.completion.chat.ChatMessage; +import com.theokanning.openai.completion.chat.ChatMessageRole; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.context.annotation.Lazy; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +import java.io.IOException; +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import static com.moyz.adi.common.enums.ErrorEnum.B_MESSAGE_NOT_FOUND; + +@Slf4j +@Service +public class ConversationMessageService extends ServiceImpl { + + @Lazy + @Resource + private ConversationMessageService _this; + + @Resource + private StringRedisTemplate stringRedisTemplate; + + @Resource + private OpenAiHelper openAiHelper; + + @Resource + private QuotaHelper quotaHelper; + + @Resource + private UserDayCostService userDayCostService; + + @Lazy + @Resource + private ConversationService conversationService; + + @Resource + private RateLimitHelper rateLimitHelper; + + + public SseEmitter sseAsk(AskReq askReq) { + SseEmitter sseEmitter = new SseEmitter(); + User user = ThreadContext.getCurrentUser(); + _this.asyncCheckAndPushToClient(sseEmitter, user, askReq); + return sseEmitter; + } + + private boolean check(SseEmitter sseEmitter, User user, AskReq askReq) { + try { + String askingKey = MessageFormat.format(RedisKeyConstant.USER_ASKING, user.getId()); + String askingVal = stringRedisTemplate.opsForValue().get(askingKey); + //check 1: still waiting response + if (StringUtils.isNotBlank(askingVal)) { + sendErrorMsg(sseEmitter, "正在回复中..."); + return false; + } + + //check 2: the conversation has been deleted + Conversation delConv = conversationService.lambdaQuery() + .eq(Conversation::getUuid, askReq.getConversationUuid()) + .eq(Conversation::getIsDelete, true) + .one(); + if (null != delConv) { + sendErrorMsg(sseEmitter, "该对话已经删除"); + return false; + } + + //check 3: conversation quota + Long convsCount = conversationService.lambdaQuery() + .eq(Conversation::getUserId, user.getId()) + .eq(Conversation::getIsDelete, false) + .count(); + long convsMax = Integer.parseInt(LocalCache.CONFIGS.get(AdiConstant.SysConfigKey.CONVERSATION_MAX_NUM)); + if (convsCount >= convsMax) { + sendErrorMsg(sseEmitter, "对话数量已经达到上限,当前对话上限为:" + convsMax); + return false; + } + + //check 4: current user's quota + ErrorEnum errorMsg = quotaHelper.checkTextQuota(user); + if (null != errorMsg) { + sendErrorMsg(sseEmitter, errorMsg.getInfo()); + return false; + } + } catch (Exception e) { + log.error("error", e); + sseEmitter.completeWithError(e); + return false; + } + return true; + } + + private void sendErrorMsg(SseEmitter sseEmitter, String errorMsg) { + try { + sseEmitter.send(SseEmitter.event().name("error").data(errorMsg)); + } catch (IOException e) { + throw new RuntimeException(e); + } + sseEmitter.complete(); + } + + @Async + public void asyncCheckAndPushToClient(SseEmitter sseEmitter, User user, AskReq askReq) { + log.info("asyncCheckAndPushToClient,userId:{}", user.getId()); + //rate limit by system + String requestTimesKey = MessageFormat.format(RedisKeyConstant.USER_REQUEST_TEXT_TIMES, user.getId()); + if (!rateLimitHelper.checkRequestTimes(requestTimesKey, LocalCache.TEXT_RATE_LIMIT_CONFIG)) { + sendErrorMsg(sseEmitter, "访问太过频繁"); + return; + } + + //check business rules + if (!check(sseEmitter, user, askReq)) { + return; + } + + String askingKey = MessageFormat.format(RedisKeyConstant.USER_ASKING, user.getId()); + stringRedisTemplate.opsForValue().set(askingKey, "1", 15, TimeUnit.SECONDS); + try { + sseEmitter.send(SseEmitter.event().name("start")); + } catch (IOException e) { + log.error("error", e); + sseEmitter.completeWithError(e); + stringRedisTemplate.delete(askingKey); + return; + } + + rateLimitHelper.increaseRequestTimes(requestTimesKey, LocalCache.TEXT_RATE_LIMIT_CONFIG); + + sseEmitter.onCompletion(() -> { + log.info("response complete,uid:{}", user.getId()); + }); + sseEmitter.onTimeout(() -> log.warn("sseEmitter timeout,uid:{},on timeout:{}", user.getId(), sseEmitter.getTimeout())); + sseEmitter.onError( + throwable -> { + try { + log.error("sseEmitter error,uid:{},on error:{}", user.getId(), throwable); + sseEmitter.send(SseEmitter.event().name("error").data(throwable.getMessage())); + } catch (IOException e) { + log.error("error", e); + } finally { + stringRedisTemplate.delete(askingKey); + } + } + ); + String prompt = askReq.getPrompt(); + if (StringUtils.isNotBlank(askReq.getRegenerateQuestionUuid())) { + prompt = getPromptMsgByQuestionUuid(askReq.getRegenerateQuestionUuid()).getContent(); + } + //questions + final List chatMessageList = new ArrayList<>(); + //system message + Conversation conversation = conversationService.lambdaQuery() + .eq(Conversation::getUuid, askReq.getConversationUuid()) + .oneOpt() + .orElse(null); + if (null != conversation) { + if (StringUtils.isNotBlank(conversation.getAiSystemMessage())) { + ChatMessage chatMessage = new ChatMessage(ChatMessageRole.SYSTEM.value(), conversation.getAiSystemMessage()); + chatMessageList.add(chatMessage); + } + //history message + if (Boolean.TRUE.equals(conversation.getUnderstandContextEnable()) && user.getUnderstandContextMsgPairNum() > 0) { + List historyMsgList = this.lambdaQuery() + .eq(ConversationMessage::getUserId, user.getId()) + .eq(ConversationMessage::getConversationId, askReq.getConversationUuid()) + .orderByDesc(ConversationMessage::getConversationId) + .last("limit " + user.getUnderstandContextMsgPairNum() * 2) + .list(); + if (!historyMsgList.isEmpty()) { + historyMsgList.sort(Comparator.comparing(ConversationMessage::getId)); + for (ConversationMessage historyMsg : historyMsgList) { + ChatMessage chatMessage = new ChatMessage(historyMsg.getMessageRole(), historyMsg.getContent()); + chatMessageList.add(chatMessage); + } + } + + } + } + //new user message + ChatMessage userMessage = new ChatMessage(ChatMessageRole.USER.value(), prompt); + chatMessageList.add(userMessage); + openAiHelper.sseAsk(user, askReq.getRegenerateQuestionUuid(), chatMessageList, sseEmitter, (response, questionMeta, answerMeta) -> { + try { + _this.saveAfterAiResponse(user, askReq, response, questionMeta, answerMeta); + } catch (Exception e) { + log.error("error:", e); + } finally { + stringRedisTemplate.delete(askingKey); + } + }); + } + + public List listQuestionsByConvId(long convId, long maxId, int pageSize) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(ConversationMessage::getConversationId, convId); + queryWrapper.eq(ConversationMessage::getParentMessageId, 0); + queryWrapper.lt(ConversationMessage::getId, maxId); + queryWrapper.eq(ConversationMessage::getIsDelete, false); + queryWrapper.last("limit " + pageSize); + queryWrapper.orderByDesc(ConversationMessage::getId); + return getBaseMapper().selectList(queryWrapper); + } + + @Transactional + public void saveAfterAiResponse(User user, AskReq askReq, String response, QuestionMeta questionMeta, AnswerMeta answerMeta) { + + int secretKeyType = StringUtils.isNotBlank(user.getSecretKey()) ? AdiConstant.SECRET_KEY_TYPE_CUSTOM : AdiConstant.SECRET_KEY_TYPE_SYSTEM; + Conversation conversation; + String prompt = askReq.getPrompt(); + String convUuid = askReq.getConversationUuid(); + conversation = conversationService.lambdaQuery() + .eq(Conversation::getUuid, convUuid) + .eq(Conversation::getUserId, user.getId()) + .oneOpt() + .orElseGet(() -> conversationService.createByFirstMessage(user.getId(), convUuid, prompt)); + //Check if regenerate question + ConversationMessage promptMsg; + if (StringUtils.isNotBlank(askReq.getRegenerateQuestionUuid())) { + promptMsg = getPromptMsgByQuestionUuid(askReq.getRegenerateQuestionUuid()); + } else { + //Save new question message + ConversationMessage question = new ConversationMessage(); + question.setUserId(user.getId()); + question.setUuid(questionMeta.getUuid()); + question.setConversationId(conversation.getId()); + question.setConversationUuid(convUuid); + question.setMessageRole(ChatMessageRole.USER.value()); + question.setContent(prompt); + question.setTokens(questionMeta.getTokens()); + question.setSecretKeyType(secretKeyType); + question.setUnderstandContextMsgPairNum(user.getUnderstandContextMsgPairNum()); + baseMapper.insert(question); + + promptMsg = this.lambdaQuery().eq(ConversationMessage::getUuid, questionMeta.getUuid()).one(); + } + + //save response message + ConversationMessage aiAnswer = new ConversationMessage(); + aiAnswer.setUserId(user.getId()); + aiAnswer.setUuid(answerMeta.getUuid()); + aiAnswer.setConversationId(conversation.getId()); + aiAnswer.setConversationUuid(convUuid); + aiAnswer.setMessageRole(ChatMessageRole.ASSISTANT.value()); + aiAnswer.setContent(response); + aiAnswer.setTokens(answerMeta.getTokens()); + aiAnswer.setParentMessageId(promptMsg.getId()); + aiAnswer.setSecretKeyType(secretKeyType); + baseMapper.insert(aiAnswer); + + calcTodayCost(user, conversation, questionMeta, answerMeta); + + } + + private void calcTodayCost(User user, Conversation conversation, QuestionMeta questionMeta, AnswerMeta answerMeta) { + + int todayTokenCost = questionMeta.getTokens() + answerMeta.getTokens(); + try { + //calculate conversation tokens + conversationService.lambdaUpdate() + .eq(Conversation::getId, conversation.getId()) + .set(Conversation::getTokens, conversation.getTokens() + todayTokenCost) + .update(); + + UserDayCost userDayCost = userDayCostService.getTodayCost(user); + UserDayCost saveOrUpdateInst = new UserDayCost(); + if (null == userDayCost) { + saveOrUpdateInst.setUserId(user.getId()); + saveOrUpdateInst.setDay(LocalDateTimeUtil.getToday()); + saveOrUpdateInst.setTokens(todayTokenCost); + saveOrUpdateInst.setRequests(1); + saveOrUpdateInst.setSecretKeyType(UserUtil.getSecretType(user)); + } else { + saveOrUpdateInst.setId(userDayCost.getId()); + saveOrUpdateInst.setTokens(userDayCost.getTokens() + todayTokenCost); + saveOrUpdateInst.setRequests(userDayCost.getRequests() + 1); + } + userDayCostService.saveOrUpdate(saveOrUpdateInst); + } catch (Exception e) { + log.error("calcTodayCost error", e); + } + } + + private ConversationMessage getPromptMsgByQuestionUuid(String questionUuid) { + return this.lambdaQuery().eq(ConversationMessage::getUuid, questionUuid).oneOpt().orElseThrow(() -> new BaseException(B_MESSAGE_NOT_FOUND)); + } + + public boolean softDelete(String uuid) { + return this.lambdaUpdate() + .eq(ConversationMessage::getUuid, uuid) + .eq(ConversationMessage::getUserId, ThreadContext.getCurrentUserId()) + .eq(ConversationMessage::getIsDelete, false) + .set(ConversationMessage::getIsDelete, true) + .update(); + } + +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/service/ConversationService.java b/adi-common/src/main/java/com/moyz/adi/common/service/ConversationService.java new file mode 100644 index 0000000..2adc1c0 --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/service/ConversationService.java @@ -0,0 +1,151 @@ +package com.moyz.adi.common.service; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.moyz.adi.common.base.ThreadContext; +import com.moyz.adi.common.dto.ConvDto; +import com.moyz.adi.common.dto.ConvEditReq; +import com.moyz.adi.common.dto.ConvMsgListResp; +import com.moyz.adi.common.dto.ConvMsgResp; +import com.moyz.adi.common.entity.Conversation; +import com.moyz.adi.common.entity.ConversationMessage; +import com.moyz.adi.common.exception.BaseException; +import com.moyz.adi.common.mapper.ConversationMapper; +import com.moyz.adi.common.util.MPPageUtil; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.BeanUtils; +import org.springframework.stereotype.Service; + +import java.util.*; +import java.util.stream.Collectors; + +import static com.moyz.adi.common.enums.ErrorEnum.A_CONVERSATION_NOT_EXIST; + +@Slf4j +@Service +public class ConversationService extends ServiceImpl { + + @Resource + private SysConfigService sysConfigService; + + @Resource + private ConversationMessageService conversationMessageService; + + public List listByUser() { + List list = this.lambdaQuery() + .eq(Conversation::getUserId, ThreadContext.getCurrentUserId()) + .eq(Conversation::getIsDelete, false) + .orderByDesc(Conversation::getId) + .last("limit " + sysConfigService.getConversationMaxNum()) + .list(); + return MPPageUtil.convertTo(list, ConvDto.class); + } + + /** + * 查询对话{@code uuid}的消息列表 + * + * @param uuid 对话的uuid + * @param maxMsgUuid + * @param pageSize + * @return + */ + public ConvMsgListResp detail(String uuid, String maxMsgUuid, int pageSize) { + Conversation conversation = this.lambdaQuery().eq(Conversation::getUuid, uuid).one(); + if (null == conversation) { + throw new RuntimeException("找不到对应的会话"); + } + + long maxId = Long.MAX_VALUE; + if (StringUtils.isNotBlank(maxMsgUuid)) { + ConversationMessage maxMsg = conversationMessageService.lambdaQuery() + .select(ConversationMessage::getId) + .eq(ConversationMessage::getUuid, maxMsgUuid) + .eq(ConversationMessage::getIsDelete, false) + .one(); + if (null == maxMsg) { + throw new RuntimeException("找不到对应的消息"); + } + maxId = maxMsg.getId(); + } + + List questions = conversationMessageService.listQuestionsByConvId(conversation.getId(), maxId, pageSize); + if (questions.isEmpty()) { + return new ConvMsgListResp(StringUtils.EMPTY, Collections.emptyList()); + } + String minUuid = questions.stream().reduce(questions.get(0), (a, b) -> { + if (a.getId() < b.getId()) { + return a; + } + return b; + }).getUuid(); + //Wrap question content + List userMessages = MPPageUtil.convertTo(questions, ConvMsgResp.class); + ConvMsgListResp result = new ConvMsgListResp(minUuid, userMessages); + + //Wrap answer content + List parentIds = questions.stream().map(ConversationMessage::getId).toList(); + List childMessages = conversationMessageService + .lambdaQuery() + .in(ConversationMessage::getParentMessageId, parentIds) + .eq(ConversationMessage::getIsDelete, false) + .list(); + Map> idToMessages = childMessages.stream().collect(Collectors.groupingBy(ConversationMessage::getParentMessageId)); + + //Fill AI answer to the request of user + result.getMsgList().forEach(item -> { + List children = MPPageUtil.convertTo(idToMessages.get(item.getId()), ConvMsgResp.class); + if (children.size() > 1) { + children = children.stream().sorted(Comparator.comparing(ConvMsgResp::getCreateTime).reversed()).collect(Collectors.toList()); + } + item.setChildren(children); + }); + return result; + } + + public int createDefault(Long userId) { + String uuid = UUID.randomUUID().toString().replace("-", ""); + Conversation conversation = new Conversation(); + conversation.setUuid(uuid); + conversation.setUserId(userId); + conversation.setTitle("New Chat"); + return baseMapper.insert(conversation); + } + + public Conversation createByFirstMessage(Long userId, String uuid, String title) { + Conversation conversation = new Conversation(); + conversation.setUuid(uuid); + conversation.setUserId(userId); + conversation.setTitle(StringUtils.substring(title, 0, 45)); + baseMapper.insert(conversation); + + return this.lambdaQuery().eq(Conversation::getUuid, uuid).oneOpt().orElse(null); + } + + public boolean edit(String uuid, ConvEditReq convEditReq) { + Conversation conversation = this.lambdaQuery() + .eq(Conversation::getUuid, uuid) + .eq(Conversation::getUserId, ThreadContext.getCurrentUserId()) + .eq(Conversation::getIsDelete, false) + .one(); + if (null == conversation) { + throw new BaseException(A_CONVERSATION_NOT_EXIST); + } + Conversation one = new Conversation(); + BeanUtils.copyProperties(convEditReq, one); + one.setId(conversation.getId()); + if (null != convEditReq.getUnderstandContextEnable()) { + one.setUnderstandContextEnable(convEditReq.getUnderstandContextEnable()); + } + return baseMapper.updateById(one) > 0 ? true : false; + } + + public boolean softDel(String uuid) { + return this.lambdaUpdate() + .eq(Conversation::getUuid, uuid) + .eq(Conversation::getUserId, ThreadContext.getCurrentUserId()) + .set(Conversation::getIsDelete, true) + .update(); + } + +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/service/FileService.java b/adi-common/src/main/java/com/moyz/adi/common/service/FileService.java new file mode 100644 index 0000000..844bc21 --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/service/FileService.java @@ -0,0 +1,139 @@ +package com.moyz.adi.common.service; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.moyz.adi.common.base.ThreadContext; +import com.moyz.adi.common.entity.AdiFile; +import com.moyz.adi.common.entity.User; +import com.moyz.adi.common.exception.BaseException; +import com.moyz.adi.common.mapper.FileMapper; +import com.moyz.adi.common.util.FileUtil; +import com.moyz.adi.common.util.MD5Utils; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.io.FileUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.tuple.Pair; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.net.URL; +import java.util.Optional; +import java.util.UUID; + +import static com.moyz.adi.common.enums.ErrorEnum.A_FILE_NOT_EXIST; +import static com.moyz.adi.common.enums.ErrorEnum.B_SAVE_IMAGE_ERROR; + +@Slf4j +@Service +public class FileService extends ServiceImpl { + + @Value("${local.images}") + private String imagePath; + + @Value("${local.tmp_images}") + private String tmpImagesPath; + + public String writeToLocal(MultipartFile file) { + String md5 = MD5Utils.md5ByMultipartFile(file); + Optional existFile = this.lambdaQuery() + .eq(AdiFile::getMd5, md5) + .eq(AdiFile::getIsDelete, false) + .oneOpt(); + if (existFile.isPresent()) { + return existFile.get().getUuid(); + } + String uuid = UUID.randomUUID().toString().replace("-", ""); + Pair originalFile = FileUtil.saveToLocal(file, imagePath, uuid); + AdiFile adiFile = new AdiFile(); + adiFile.setUuid(uuid); + adiFile.setMd5(md5); + adiFile.setPath(originalFile.getLeft()); + adiFile.setExt(originalFile.getRight()); + adiFile.setUserId(ThreadContext.getCurrentUserId()); + this.getBaseMapper().insert(adiFile); + return uuid; + } + + public String saveToLocal(User user, String sourceImageUrl) { + String uuid = UUID.randomUUID().toString().replace("-", ""); + String localPath = imagePath + uuid + ".png"; + File target = new File(localPath); + try { + FileUtils.createParentDirectories(target); + FileUtils.copyURLToFile(new URL(sourceImageUrl), target); + } catch (IOException e) { + log.error("saveToLocal", e); + throw new BaseException(B_SAVE_IMAGE_ERROR); + } + AdiFile adiFile = new AdiFile(); + adiFile.setUuid(uuid); + adiFile.setMd5(MD5Utils.calculateMD5(localPath)); + adiFile.setPath(localPath); + adiFile.setUserId(user.getId()); + this.getBaseMapper().insert(adiFile); + return uuid; + } + + public boolean softDel(String uuid) { + return this.lambdaUpdate() + .eq(AdiFile::getUserId, ThreadContext.getCurrentUserId()) + .eq(AdiFile::getUuid, uuid) + .set(AdiFile::getIsDelete, true) + .update(); + } + + public boolean removeFileAndSoftDel(String uuid) { + AdiFile adiFile = this.lambdaQuery() + .eq(AdiFile::getUserId, ThreadContext.getCurrentUserId()) + .eq(AdiFile::getUuid, uuid) + .oneOpt() + .orElse(null); + if (null == adiFile) { + return false; + } + if (StringUtils.isNotBlank(adiFile.getPath())) { + File file = new File(adiFile.getPath()); + file.delete(); + } + return this.softDel(uuid); + } + + public byte[] readBytes(String uuid) { + AdiFile adiFile = this.lambdaQuery() + .eq(AdiFile::getUuid, uuid) + .eq(AdiFile::getUserId, ThreadContext.getCurrentUserId()) + .oneOpt().orElse(null); + if (null == adiFile) { + throw new BaseException(A_FILE_NOT_EXIST); + } + return FileUtil.readBytes(adiFile.getPath()); + } + + public BufferedImage readBufferedImage(String uuid) { + AdiFile adiFile = this.lambdaQuery() + .eq(AdiFile::getUuid, uuid) + .eq(AdiFile::getUserId, ThreadContext.getCurrentUserId()) + .oneOpt().orElse(null); + if (null == adiFile) { + throw new BaseException(A_FILE_NOT_EXIST); + } + try { + return ImageIO.read(new FileInputStream(adiFile.getPath())); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public String getImagePath(String uuid) { + return imagePath + uuid + ".png"; + } + + public String getTmpImagesPath(String uuid) { + return tmpImagesPath + uuid + ".png"; + } +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/service/PromptService.java b/adi-common/src/main/java/com/moyz/adi/common/service/PromptService.java new file mode 100644 index 0000000..998117b --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/service/PromptService.java @@ -0,0 +1,175 @@ +package com.moyz.adi.common.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.moyz.adi.common.dto.PromptsSaveReq; +import com.moyz.adi.common.base.ThreadContext; +import com.moyz.adi.common.dto.PromptDto; +import com.moyz.adi.common.dto.PromptListResp; +import com.moyz.adi.common.entity.Prompt; +import com.moyz.adi.common.mapper.PromptMapper; +import com.moyz.adi.common.util.LocalDateTimeUtil; +import com.moyz.adi.common.util.MPPageUtil; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Slf4j +@Service +public class PromptService extends ServiceImpl { + + public List getAll(long userId) { + List prompts = this.lambdaQuery().eq(Prompt::getUserId, userId).eq(Prompt::getIsDelete, false).list(); + return MPPageUtil.convertTo(prompts, PromptDto.class); + } + + public Page search(String keyword, int currentPage, int pageSize) { + Page promptPage; + if (StringUtils.isNotBlank(keyword)) { + promptPage = this.lambdaQuery() + .eq(Prompt::getUserId, ThreadContext.getCurrentUserId()) + .eq(Prompt::getIsDelete, false) + .like(Prompt::getAct, keyword) + .page(new Page<>(currentPage, pageSize)); + } else { + promptPage = this.lambdaQuery() + .eq(Prompt::getUserId, ThreadContext.getCurrentUserId()) + .eq(Prompt::getIsDelete, false) + .page(new Page<>(currentPage, pageSize)); + } + return MPPageUtil.convertTo(promptPage, new Page<>(), PromptDto.class); + } + + public List autocomplete(String keyword) { + List promptPage; + if (StringUtils.isNotBlank(keyword)) { + promptPage = this.lambdaQuery() + .eq(Prompt::getUserId, ThreadContext.getCurrentUserId()) + .eq(Prompt::getIsDelete, false) + .like(Prompt::getAct, keyword) + .last("limit 10") + .list(); + } else { + promptPage = this.lambdaQuery() + .eq(Prompt::getUserId, ThreadContext.getCurrentUserId()) + .eq(Prompt::getIsDelete, false) + .last("limit 10") + .list(); + } + return MPPageUtil.convertTo(promptPage, PromptDto.class); + } + + public PromptListResp listByMinUpdateTime(LocalDateTime minUpdateTime) { + LocalDateTime tmpUpdatTime = minUpdateTime; + if (null == tmpUpdatTime) { + tmpUpdatTime = LocalDateTime.of(2023, 1, 1, 1, 1); + } + PromptListResp resp = new PromptListResp(); + List list = this.lambdaQuery() + .eq(Prompt::getUserId, ThreadContext.getCurrentUserId()) + .gt(Prompt::getUpdateTime, tmpUpdatTime) + .orderByAsc(Prompt::getUpdateTime, Prompt::getId) + .last("limit 100") + .list(); + if (list.isEmpty()) { + resp.setMaxUpdateTime(LocalDateTimeUtil.format(LocalDateTime.now())); + resp.setPrompts(new ArrayList<>()); + return resp; + } + LocalDateTime maxUpdateTime = list.stream().reduce((a, b) -> { + if (a.getUpdateTime().isAfter(b.getUpdateTime())) { + return a; + } + return b; + }).get().getUpdateTime(); + List promptDtos = MPPageUtil.convertTo(list, PromptDto.class); + resp.setMaxUpdateTime(LocalDateTimeUtil.format(maxUpdateTime)); + resp.setPrompts(promptDtos); + return resp; + } + + public Map savePrompts(PromptsSaveReq savePromptsReq) { + Map titleToId = new HashMap<>(); + + Long userId = ThreadContext.getCurrentUserId(); + for (PromptDto promptDto : savePromptsReq.getPrompts()) { + + String title = promptDto.getAct(); + Prompt prompt = new Prompt(); + + Prompt existOne = this.lambdaQuery() + .eq(Prompt::getUserId, userId) + .eq(Prompt::getAct, title) + .eq(Prompt::getIsDelete, false) + .one(); + if (null != existOne) { + //modify + prompt.setId(existOne.getId()); + prompt.setUserId(userId); + prompt.setAct(title); + prompt.setPrompt(promptDto.getPrompt()); + this.updateById(prompt); + titleToId.put(title, existOne.getId()); + } else { + //create + prompt.setUserId(userId); + prompt.setAct(title); + prompt.setPrompt(promptDto.getPrompt()); + this.save(prompt); + + Prompt addedOne = this.lambdaQuery() + .eq(Prompt::getUserId, userId) + .eq(Prompt::getAct, title) + .one(); + titleToId.put(title, addedOne.getId()); + } + } + return titleToId; + } + + public boolean softDelete(Long id) { + Prompt prompt = this.lambdaQuery() + .eq(Prompt::getUserId, ThreadContext.getCurrentUserId()) + .eq(Prompt::getId, id) + .eq(Prompt::getIsDelete, false) + .one(); + if (null == prompt) { + return false; + } + Prompt updateOne = new Prompt(); + updateOne.setId(id); + updateOne.setIsDelete(true); + return this.updateById(updateOne); + } + + public boolean edit(Long id, String title, String remark) { + Prompt prompt = this.lambdaQuery() + .eq(Prompt::getId, id) + .eq(Prompt::getUserId, ThreadContext.getCurrentUserId()) + .eq(Prompt::getIsDelete, false) + .one(); + if (null == prompt) { + return false; + } + Prompt updateOne = new Prompt(); + updateOne.setId(id); + updateOne.setAct(title); + updateOne.setPrompt(remark); + return this.updateById(updateOne); + } + + public List search(String keyword) { + List prompts = this.lambdaQuery() + .eq(Prompt::getUserId, ThreadContext.getCurrentUserId()) + .like(Prompt::getAct, keyword) + .last("limit 10") + .list(); + return MPPageUtil.convertTo(prompts, PromptDto.class); + } +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/service/SysConfigService.java b/adi-common/src/main/java/com/moyz/adi/common/service/SysConfigService.java new file mode 100644 index 0000000..0ded621 --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/service/SysConfigService.java @@ -0,0 +1,61 @@ +package com.moyz.adi.common.service; + +import com.moyz.adi.common.cosntant.AdiConstant; +import com.moyz.adi.common.model.RequestRateLimit; +import com.moyz.adi.common.util.JsonUtil; +import com.moyz.adi.common.util.LocalCache; +import com.moyz.adi.common.entity.SysConfig; +import com.moyz.adi.common.mapper.SysConfigMapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; + +@Slf4j +@Service +public class SysConfigService extends ServiceImpl { + + @Scheduled(fixedDelay = 20 * 60 * 1000) + public void reload() { + log.info("reload system config"); + List configsFromDB = this.lambdaQuery().eq(SysConfig::getIsDelete, false).list(); + if (LocalCache.CONFIGS.isEmpty()) { + configsFromDB.stream().forEach(item -> LocalCache.CONFIGS.put(item.getName(), item.getValue())); + } else { + //remove deleted config + List deletedKeys = new ArrayList<>(); + LocalCache.CONFIGS.forEach((k, v) -> { + boolean deleted = configsFromDB.stream().noneMatch(sysConfig -> sysConfig.getName().equals(k)); + if (deleted) { + deletedKeys.add(k); + } + }); + if (!deletedKeys.isEmpty()) { + deletedKeys.forEach(k -> LocalCache.CONFIGS.remove(k)); + } + + //add or update config + for (SysConfig item : configsFromDB) { + String key = item.getName(); + LocalCache.CONFIGS.put(key, item.getValue()); + } + } + LocalCache.TEXT_RATE_LIMIT_CONFIG = JsonUtil.fromJson(LocalCache.CONFIGS.get(AdiConstant.SysConfigKey.REQUEST_TEXT_RATE_LIMIT), RequestRateLimit.class); + LocalCache.IMAGE_RATE_LIMIT_CONFIG = JsonUtil.fromJson(LocalCache.CONFIGS.get(AdiConstant.SysConfigKey.REQUEST_IMAGE_RATE_LIMIT), RequestRateLimit.class); + LocalCache.TEXT_RATE_LIMIT_CONFIG.setType(RequestRateLimit.TYPE_TEXT); + LocalCache.IMAGE_RATE_LIMIT_CONFIG.setType(RequestRateLimit.TYPE_IMAGE); + } + + public int getConversationMaxNum() { + String maxNum = LocalCache.CONFIGS.get(AdiConstant.SysConfigKey.CONVERSATION_MAX_NUM); + return Integer.parseInt(maxNum); + } + + public static String getSecretKey() { + return LocalCache.CONFIGS.get(AdiConstant.SysConfigKey.SECRET_KEY); + } + +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/service/UserDayCostService.java b/adi-common/src/main/java/com/moyz/adi/common/service/UserDayCostService.java new file mode 100644 index 0000000..d9d3c51 --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/service/UserDayCostService.java @@ -0,0 +1,53 @@ +package com.moyz.adi.common.service; + +import com.moyz.adi.common.cosntant.AdiConstant; +import com.moyz.adi.common.entity.User; +import com.moyz.adi.common.util.LocalDateTimeUtil; +import com.moyz.adi.common.entity.UserDayCost; +import com.moyz.adi.common.mapper.UserDayCostMapper; +import com.moyz.adi.common.model.CostStat; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.moyz.adi.common.util.UserUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.util.List; + +@Slf4j +@Service +public class UserDayCostService extends ServiceImpl { + + public CostStat costStatByUser(long userId) { + CostStat result = new CostStat(); + + int today = LocalDateTimeUtil.getIntDay(LocalDateTime.now()); + int start = LocalDateTimeUtil.getIntDay(LocalDateTime.now().withDayOfMonth(1)); + int end = LocalDateTimeUtil.getIntDay(LocalDateTime.now().plusMonths(1).withDayOfMonth(1).minusDays(1)); + + List userDayCostList = this.lambdaQuery() + .eq(UserDayCost::getUserId, userId) + .eq(UserDayCost::getSecretKeyType, AdiConstant.SECRET_KEY_TYPE_SYSTEM) + .between(UserDayCost::getDay, start, end) + .list(); + for (UserDayCost userDayCost : userDayCostList) { + result.setTextTokenCostByMonth(result.getTextTokenCostByMonth() + userDayCost.getTokens()); + result.setTextRequestTimesByMonth(result.getTextRequestTimesByMonth() + userDayCost.getRequests()); + result.setImageGeneratedNumberByMonth(result.getImageGeneratedNumberByMonth() + userDayCost.getImagesNumber()); + if (userDayCost.getDay() == today) { + result.setTextTokenCostByDay(userDayCost.getTokens()); + result.setTextRequestTimesByDay(userDayCost.getRequests()); + result.setImageGeneratedNumberByDay(userDayCost.getImagesNumber()); + } + } + return result; + } + + public UserDayCost getTodayCost(User user) { + return this.lambdaQuery() + .eq(UserDayCost::getUserId, user.getId()) + .eq(UserDayCost::getDay, LocalDateTimeUtil.getToday()) + .eq(UserDayCost::getSecretKeyType, UserUtil.getSecretType(user)) + .one(); + } +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/service/UserService.java b/adi-common/src/main/java/com/moyz/adi/common/service/UserService.java new file mode 100644 index 0000000..062401d --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/service/UserService.java @@ -0,0 +1,317 @@ +package com.moyz.adi.common.service; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.baomidou.mybatisplus.extension.toolkit.ChainWrappers; +import com.moyz.adi.common.cosntant.AdiConstant; +import com.moyz.adi.common.enums.ErrorEnum; +import com.moyz.adi.common.enums.UserStatusEnum; +import com.moyz.adi.common.exception.BaseException; +import com.moyz.adi.common.util.JsonUtil; +import com.moyz.adi.common.util.LocalCache; +import com.moyz.adi.common.base.ThreadContext; +import com.moyz.adi.common.cosntant.RedisKeyConstant; +import com.moyz.adi.common.dto.ConfigResp; +import com.moyz.adi.common.dto.LoginReq; +import com.moyz.adi.common.dto.LoginResp; +import com.moyz.adi.common.dto.UserUpdateReq; +import com.moyz.adi.common.entity.User; +import com.moyz.adi.common.helper.AdiMailSender; +import com.moyz.adi.common.mapper.UserMapper; +import com.moyz.adi.common.model.CostStat; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.mindrot.jbcrypt.BCrypt; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Service; + +import java.text.MessageFormat; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +import static com.moyz.adi.common.cosntant.RedisKeyConstant.*; +import static com.moyz.adi.common.enums.ErrorEnum.*; + +/** + *

+ * 用户表 服务实现类 + *

+ * + * @author moyz + * @since 2023-04-11 + */ +@Slf4j +@Service +public class UserService extends ServiceImpl { + + @Resource + private UserDayCostService userDayCostService; + + @Resource + private AdiMailSender adiMailSender; + + @Resource + private StringRedisTemplate stringRedisTemplate; + + @Resource + private ConversationService conversationService; + + @Value("${adi.backend-url}") + private String backendUrl; + + @Value("${spring.application.name}") + private String appName; + + public User getByEmail(String email) { + if (StringUtils.isBlank(email)) { + throw new BaseException(ErrorEnum.A_PARAMS_ERROR); + } + return this.lambdaQuery() + .eq(User::getEmail, email) + .eq(User::getIsDelete, false) + .oneOpt() + .orElseThrow(() -> new BaseException(A_USER_NOT_EXIST)); + } + + public void forgotPassword(String email) { + User user = getByEmail(email); + String code = UUID.randomUUID().toString().replace("-", ""); + String key = MessageFormat.format(FIND_MY_PASSWORD, code); + stringRedisTemplate.opsForValue().set(key, user.getId().toString(), 8, TimeUnit.HOURS); + adiMailSender.send(appName + "重置密码", "点击链接将密码重置为" + AdiConstant.DEFAULT_PASSWORD + ",链接(" + AdiConstant.AUTH_ACTIVE_CODE_EXPIRE + "小时内有效):" + backendUrl + "/auth/password/reset?code=" + code, email); + } + + public void register(String email, String password, String captchaId, String captcha) { + //验证码 + String captchaIdKey = MessageFormat.format(AUTH_REGISTER_CAPTCHA_ID, captchaId); + String captchaInCache = stringRedisTemplate.opsForValue().get(captchaIdKey); + if (StringUtils.isBlank(captchaInCache) || !captchaInCache.equalsIgnoreCase(captcha)) { + throw new BaseException(A_LOGIN_CAPTCHA_ERROR); + } + stringRedisTemplate.delete(captchaInCache); + + User user = ChainWrappers.lambdaQueryChain(baseMapper) + .eq(User::getIsDelete, false) + .eq(User::getEmail, email) + .one(); + if (null != user && user.getUserStatus() == UserStatusEnum.NORMAL) { + throw new BaseException(A_REGISTER_USER_EXIST); + } + if (null != user) { + sendActiveEmail(email); + return; + } + + //发送激活链接 + sendActiveEmail(email); + + String hashed = BCrypt.hashpw(password, BCrypt.gensalt()); + + //创建用户 + User newOne = new User(); + newOne.setName(email.substring(0, email.indexOf("@"))); + newOne.setUuid(UUID.randomUUID().toString().replace("-", "")); + newOne.setEmail(email); + newOne.setPassword(hashed); + newOne.setUserStatus(UserStatusEnum.WAIT_CONFIRM); + baseMapper.insert(newOne); + } + + public void resetPassword(String code) { + String key = MessageFormat.format(FIND_MY_PASSWORD, code); + String userId = stringRedisTemplate.opsForValue().get(key); + if (StringUtils.isBlank(userId)) { + throw new BaseException(A_FIND_PASSWORD_CODE_ERROR); + } + User updateUser = new User(); + updateUser.setId(Long.parseLong(userId)); + updateUser.setPassword(BCrypt.hashpw(AdiConstant.DEFAULT_PASSWORD, BCrypt.gensalt())); + baseMapper.updateById(updateUser); + stringRedisTemplate.delete(key); + } + + public void modifyPassword(String oldPassword, String newPassword) { + User user = ThreadContext.getExistCurrentUser(); + + if (!BCrypt.checkpw(oldPassword, user.getPassword())) { + throw new RuntimeException("原密码不正确"); + } + + String hashed = BCrypt.hashpw(newPassword, BCrypt.gensalt()); + User updateUser = new User(); + updateUser.setId(user.getId()); + updateUser.setPassword(hashed); + baseMapper.updateById(updateUser); + } + + public void active(String activeCode) { + String activeCodeKey = MessageFormat.format(AUTH_ACTIVE_CODE, activeCode); + String email = stringRedisTemplate.opsForValue().get(activeCodeKey); + if (StringUtils.isBlank(email)) { + throw new RuntimeException("激活码已失效"); + } + + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + User user = this.lambdaQuery() + .eq(User::getEmail, email) + .eq(User::getIsDelete, false) + .oneOpt() + .orElse(null); + if (null == user) { + throw new RuntimeException("用户不存在"); + } + + stringRedisTemplate.delete(activeCodeKey); + + User updateUser = new User(); + updateUser.setId(user.getId()); + updateUser.setUserStatus(UserStatusEnum.NORMAL); + baseMapper.updateById(updateUser); + + setLoginToken(user); + + //Create default conversation + conversationService.createDefault(user.getId()); + } + + public LoginResp login(LoginReq loginReq) { + //captcha check + String failCountKey = MessageFormat.format(RedisKeyConstant.LOGIN_FAIL_COUNT, loginReq.getEmail()); + int passwordFailCount = 0; + String failCountVal = stringRedisTemplate.opsForValue().get(failCountKey); + if (StringUtils.isNotBlank(failCountVal)) { + passwordFailCount = Integer.parseInt(failCountVal); + } + if (passwordFailCount >= AdiConstant.LOGIN_MAX_FAIL_TIMES) { + if (StringUtils.isAnyBlank(loginReq.getCaptchaCode(), loginReq.getCaptchaId())) { + String captchaId = setAndGetLoginCaptchaId(); + LoginResp loginResp = new LoginResp(); + loginResp.setCaptchaId(captchaId); + throw new BaseException(ErrorEnum.A_LOGIN_ERROR_MAX).setData(loginResp); + } + String captchaIdKey = MessageFormat.format(AUTH_LOGIN_CAPTCHA_ID, loginReq.getCaptchaId()); + String captcha = stringRedisTemplate.opsForValue().get(captchaIdKey); + if (StringUtils.isBlank(captcha) || !captcha.equalsIgnoreCase(loginReq.getCaptchaCode())) { + throw new BaseException(A_LOGIN_CAPTCHA_ERROR); + } + } + //captcha check end + + User user = this.lambdaQuery() + .eq(User::getIsDelete, false) + .eq(User::getEmail, loginReq.getEmail()) + .oneOpt() + .orElseThrow(() -> new BaseException(ErrorEnum.A_USER_NOT_EXIST)); + if (user.getUserStatus() == UserStatusEnum.WAIT_CONFIRM) { + throw new BaseException(ErrorEnum.A_USER_WAIT_CONFIRM); + } + if (!BCrypt.checkpw(loginReq.getPassword(), user.getPassword())) { + + //计算错误次数并判断下次登录是否要输入验证码 + passwordFailCount = passwordFailCount + 1; + stringRedisTemplate.opsForValue().set(failCountKey, String.valueOf(passwordFailCount), AdiConstant.USER_TOKEN_EXPIRE, TimeUnit.HOURS); + + throw new BaseException(ErrorEnum.A_LOGIN_ERROR); + } + + //login success + stringRedisTemplate.delete(failCountKey); + String token = setLoginToken(user); + LoginResp loginResp = new LoginResp(); + loginResp.setToken(token); + BeanUtils.copyProperties(user, loginResp); + return loginResp; + } + + public String setAndGetLoginCaptchaId() { + String captchaId = UUID.randomUUID().toString().replace("-", ""); + String captchaIdKey = MessageFormat.format(AUTH_LOGIN_CAPTCHA_ID, captchaId); + stringRedisTemplate.opsForValue().set(captchaIdKey, captchaId, AdiConstant.AUTH_CAPTCHA_ID_EXPIRE, TimeUnit.HOURS); + return captchaId; + } + + public void cacheLoginCaptcha(String captchaId, String captcha) { + String captchaIdKey = MessageFormat.format(AUTH_LOGIN_CAPTCHA_ID, captchaId); + stringRedisTemplate.opsForValue().set(captchaIdKey, captcha, AdiConstant.AUTH_CAPTCHA_ID_EXPIRE, TimeUnit.HOURS); + } + + public void cacheRegisterCaptcha(String captchaId, String captcha) { + String captchaIdKey = MessageFormat.format(AUTH_REGISTER_CAPTCHA_ID, captchaId); + stringRedisTemplate.opsForValue().set(captchaIdKey, captcha, AdiConstant.AUTH_CAPTCHA_ID_EXPIRE, TimeUnit.HOURS); + } + + public ConfigResp getConfig() { + ConfigResp result = new ConfigResp(); + User user = ThreadContext.getCurrentUser(); + BeanUtils.copyProperties(user, result); + + CostStat costStat = userDayCostService.costStatByUser(user.getId()); + result.setTodayTokenCost(costStat.getTextTokenCostByDay()); + result.setTodayRequestTimes(costStat.getTextRequestTimesByDay()); + result.setTodayGeneratedImageNumber(costStat.getImageGeneratedNumberByDay()); + result.setCurrMonthTokenCost(costStat.getTextTokenCostByMonth()); + result.setCurrMonthRequestTimes(costStat.getTextRequestTimesByMonth()); + result.setCurrMonthGeneratedImageNumber(costStat.getImageGeneratedNumberByMonth()); + return result; + } + + public void updateConfig(UserUpdateReq userUpdateReq) { + User user = new User(); + user.setId(ThreadContext.getCurrentUserId()); + BeanUtils.copyProperties(userUpdateReq, user); + baseMapper.updateById(user); + } + + public void logout() { + String token = ThreadContext.getToken(); + if (null == token) { + log.warn("logout token is null"); + return; + } + String tokenKey = MessageFormat.format(USER_TOKEN, token); + stringRedisTemplate.delete(tokenKey); + } + + private String setLoginToken(User user) { + if (user.getQuotaByTokenDaily() == 0) { + user.setQuotaByTokenDaily(Integer.parseInt(LocalCache.CONFIGS.get(AdiConstant.SysConfigKey.QUOTA_BY_TOKEN_DAILY))); + } + if (user.getQuotaByTokenMonthly() == 0) { + user.setQuotaByTokenMonthly(Integer.parseInt(LocalCache.CONFIGS.get(AdiConstant.SysConfigKey.QUOTA_BY_TOKEN_MONTHLY))); + } + if (user.getQuotaByRequestDaily() == 0) { + user.setQuotaByRequestDaily(Integer.parseInt(LocalCache.CONFIGS.get(AdiConstant.SysConfigKey.QUOTA_BY_REQUEST_DAILY))); + } + if (user.getQuotaByRequestMonthly() == 0) { + user.setQuotaByRequestMonthly(Integer.parseInt(LocalCache.CONFIGS.get(AdiConstant.SysConfigKey.QUOTA_BY_REQUEST_MONTHLY))); + } + if (user.getQuotaByImageDaily() == 0) { + user.setQuotaByImageDaily(Integer.parseInt(LocalCache.CONFIGS.get(AdiConstant.SysConfigKey.QUOTA_BY_IMAGE_DAILY))); + } + if (user.getQuotaByImageMonthly() == 0) { + user.setQuotaByImageMonthly(Integer.parseInt(LocalCache.CONFIGS.get(AdiConstant.SysConfigKey.QUOTA_BY_IMAGE_MONTHLY))); + } + String token = UUID.randomUUID().toString().replace("-", ""); + String tokenKey = MessageFormat.format(USER_TOKEN, token); + String jsonUser = JsonUtil.toJson(user); + log.info("jsonUser:{}", jsonUser); + stringRedisTemplate.opsForValue().set(tokenKey, jsonUser, AdiConstant.USER_TOKEN_EXPIRE, TimeUnit.HOURS); + return token; + } + + /** + * 发送激活链接 + * + * @param email + */ + public void sendActiveEmail(String email) { + String activeCode = UUID.randomUUID().toString().replace("-", ""); + String activeCodeKey = MessageFormat.format(AUTH_ACTIVE_CODE, activeCode); + stringRedisTemplate.opsForValue().set(activeCodeKey, email, AdiConstant.AUTH_ACTIVE_CODE_EXPIRE, TimeUnit.HOURS); + adiMailSender.send("欢迎注册AIDeepIn", "激活链接(" + AdiConstant.AUTH_ACTIVE_CODE_EXPIRE + "小时内有效):" + backendUrl + "/auth/active?code=" + activeCode, email); + } + +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/util/BizPager.java b/adi-common/src/main/java/com/moyz/adi/common/util/BizPager.java new file mode 100644 index 0000000..df9ec63 --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/util/BizPager.java @@ -0,0 +1,96 @@ +package com.moyz.adi.common.util; + +import com.moyz.adi.common.cosntant.AdiConstant; +import com.moyz.adi.common.entity.BaseEntity; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.support.SFunction; +import com.baomidou.mybatisplus.extension.service.IService; +import org.apache.commons.collections4.CollectionUtils; + +import java.util.List; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Supplier; + +public class BizPager { + + /** + * 逐行获取数据 + *
以Long类型的惟一字段(通常为id)为锚点,批量查询后台数据库,基于mybatis-plus + *
回调函数传值为单行 + * + * @param queryWrapper + * @param service 业务的service + * @param idSupplier 锚点字段 + * @param consumer 回调 + * @param 数据表对应的实体类 + */ + public static void oneByOneWithAnchor(LambdaQueryWrapper queryWrapper, IService service, SFunction idSupplier, Consumer consumer) { + long minId = 0; + List records; + do { + queryWrapper.gt(idSupplier, minId); + queryWrapper.orderByAsc(idSupplier); + queryWrapper.last("limit " + AdiConstant.DEFAULT_PAGE_SIZE); + records = service.list(queryWrapper); + if (CollectionUtils.isNotEmpty(records)) { + minId = records.stream().map(idSupplier).reduce(Long::max).get(); + for (T t : records) { + consumer.accept(t); + } + } + } while (!Thread.currentThread().isInterrupted() && records.size() == AdiConstant.DEFAULT_PAGE_SIZE); + } + + /** + * 按页获取数据 + *
以Long类型的惟一字段(通常为id)为锚点,批量查询后台数据库,基于mybatis-plus + *
回调函数传值为列表 + * + * @param queryWrapper + * @param service 业务的service + * @param idSupplier 锚点字段 + * @param consumer 回调 + * @param 数据表对应的实体类 + */ + public static void batchWithAnchor(LambdaQueryWrapper queryWrapper, IService service, SFunction idSupplier, Consumer> consumer) { + long minId = 0; + List records; + do { + queryWrapper.gt(idSupplier, minId); + queryWrapper.orderByAsc(idSupplier); + queryWrapper.last("limit " + AdiConstant.DEFAULT_PAGE_SIZE); + records = service.list(queryWrapper); + if (CollectionUtils.isNotEmpty(records)) { + minId = records.stream().map(idSupplier).reduce(Long::max).get(); + consumer.accept(records); + } + } while (!Thread.currentThread().isInterrupted() && records.size() == AdiConstant.DEFAULT_PAGE_SIZE); + } + + /** + * 以Long类型的惟一字段(通常为id)为锚点,按页获取数据 + *
不依赖mybatis-plus + * + * @param supplier 每页查询出来的数据 + * @param consumer 回调函数 + * @param idSupplier id字段名提供者 + * @param asc 是否升序 + * @param + */ + public static void batchWithAnchor(Supplier> supplier, BiConsumer, Long> consumer, SFunction idSupplier, boolean asc) { + long anchorId; + List records; + do { + records = supplier.get(); + if (CollectionUtils.isNotEmpty(records)) { + if (asc) { + anchorId = records.stream().map(idSupplier).reduce(Long::max).get(); + } else { + anchorId = records.stream().map(idSupplier).reduce(Long::min).get(); + } + consumer.accept(records, anchorId); + } + } while (!Thread.currentThread().isInterrupted() && records.size() == AdiConstant.DEFAULT_PAGE_SIZE); + } +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/util/FileUtil.java b/adi-common/src/main/java/com/moyz/adi/common/util/FileUtil.java new file mode 100644 index 0000000..45daee6 --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/util/FileUtil.java @@ -0,0 +1,54 @@ +package com.moyz.adi.common.util; + +import com.moyz.adi.common.exception.BaseException; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.io.FileUtils; +import org.apache.commons.lang3.tuple.ImmutablePair; +import org.apache.commons.lang3.tuple.Pair; +import org.springframework.util.StringUtils; +import org.springframework.web.multipart.MultipartFile; + +import java.io.File; +import java.io.IOException; + +import static com.moyz.adi.common.enums.ErrorEnum.A_FILE_NOT_EXIST; + +@Slf4j +public class FileUtil { + + public static Pair saveToLocal(MultipartFile file, String path, String newName) { + if (file.isEmpty()) { + log.info("save to local,file is empty"); + throw new BaseException(A_FILE_NOT_EXIST); + } + String fileName = StringUtils.cleanPath(file.getOriginalFilename()); + String fileExt = getFileExtension(fileName); + log.info("save to local,original name:{},new name:{}", fileName, newName); + String pathName = path + newName + "." + fileExt; + try { + // 将文件保存到目标路径 + file.transferTo(new File(pathName)); + } catch (IOException e) { + log.error("save to local error", e); + } + return new ImmutablePair<>(pathName, fileExt); + } + + public static byte[] readBytes(String localPath) { + try { + return FileUtils.readFileToByteArray(new File(localPath)); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public static String getFileExtension(String fileName) { + int dotIndex = fileName.lastIndexOf("."); + if (dotIndex == -1 || dotIndex == fileName.length() - 1) { + // 文件名中没有后缀或者后缀位于文件名的末尾 + return ""; + } else { + return fileName.substring(dotIndex + 1); + } + } +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/util/IPAddressUtil.java b/adi-common/src/main/java/com/moyz/adi/common/util/IPAddressUtil.java new file mode 100644 index 0000000..fe81ebc --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/util/IPAddressUtil.java @@ -0,0 +1,37 @@ +package com.moyz.adi.common.util; + +import jakarta.servlet.http.HttpServletRequest; + +public class IPAddressUtil { + + public static String getClientIpAddress(HttpServletRequest request) { + String ip = null; + //X-Forwarded-For:Squid 服务代理 + String ipAddresses = request.getHeader("X-Forwarded-For"); + //Proxy-Client-IP:apache 服务代理 + if (ipAddresses == null || ipAddresses.length() == 0 || "unknown".equalsIgnoreCase(ipAddresses)) { + ipAddresses = request.getHeader("Proxy-Client-IP"); + } + //WL-Proxy-Client-IP:weblogic 服务代理 + if (ipAddresses == null || ipAddresses.length() == 0 || "unknown".equalsIgnoreCase(ipAddresses)) { + ipAddresses = request.getHeader("WL-Proxy-Client-IP"); + } + //HTTP_CLIENT_IP:有些代理服务器 + if (ipAddresses == null || ipAddresses.length() == 0 || "unknown".equalsIgnoreCase(ipAddresses)) { + ipAddresses = request.getHeader("HTTP_CLIENT_IP"); + } + //X-Real-IP:nginx服务代理 + if (ipAddresses == null || ipAddresses.length() == 0 || "unknown".equalsIgnoreCase(ipAddresses)) { + ipAddresses = request.getHeader("X-Real-IP"); + } + //有些网络通过多层代理,那么获取到的ip就会有多个,一般都是通过逗号(,)分割开来,并且第一个ip为客户端的真实IP + if (ipAddresses != null && ipAddresses.length() != 0) { + ip = ipAddresses.split(",")[0]; + } + //还是不能获取到,最后再通过request.getRemoteAddr();获取 + if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ipAddresses)) { + ip = request.getRemoteAddr(); + } + return ip; + } +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/util/ImageUtil.java b/adi-common/src/main/java/com/moyz/adi/common/util/ImageUtil.java new file mode 100644 index 0000000..84d93d3 --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/util/ImageUtil.java @@ -0,0 +1,75 @@ +package com.moyz.adi.common.util; + +import lombok.extern.slf4j.Slf4j; + +import javax.imageio.ImageIO; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; + +@Slf4j +public class ImageUtil { + + public static boolean isRGB(String imagePath) { + try { + // 读取图片 + BufferedImage image = ImageIO.read(new File(imagePath)); + // 获取图片的颜色模型 + int colorModel = image.getColorModel().getColorSpace().getType(); + if (colorModel == BufferedImage.TYPE_INT_RGB) { + return true; + } + } catch (IOException e) { + log.error("isARGB error", e); + } + return false; + } + + public static void rgbConvertToRgba(String rbgPath, String argbPath) { + try { + // 读取RGB图片 + BufferedImage rgbImage = ImageIO.read(new File(rbgPath)); + + // 创建一个RGBA图片,与原始RGB图片大小相同 + BufferedImage rgbaImage = new BufferedImage(rgbImage.getWidth(), rgbImage.getHeight(), BufferedImage.TYPE_INT_ARGB); + + // 将RGB图片绘制到RGBA图片上,并设置透明度为不透明 + Graphics2D g = rgbaImage.createGraphics(); + g.drawImage(rgbImage, 0, 0, null); + g.dispose(); + + // 保存RGBA图片 + ImageIO.write(rgbaImage, "png", new File(argbPath)); + + System.out.println("转换完成!"); + } catch (IOException e) { + e.printStackTrace(); + } + } + + public static File rgbConvertToRgba(File file, String rgbaPath) { + try { + BufferedImage image = ImageIO.read(file); + + // 获取图片的颜色模型 + int colorModel = image.getColorModel().getColorSpace().getType(); + if (colorModel != BufferedImage.TYPE_INT_ARGB) { + // 创建一个RGBA图片,与原始RGB图片大小相同 + BufferedImage rgbaImage = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_ARGB); + + // 将RGB图片绘制到RGBA图片上,并设置透明度为不透明 + Graphics2D g = rgbaImage.createGraphics(); + g.drawImage(image, 0, 0, null); + g.dispose(); + // 保存RGBA图片 + ImageIO.write(rgbaImage, "png", new File(rgbaPath)); + + return new File(rgbaPath); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + return file; + } +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/util/JsonUtil.java b/adi-common/src/main/java/com/moyz/adi/common/util/JsonUtil.java new file mode 100644 index 0000000..da79e53 --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/util/JsonUtil.java @@ -0,0 +1,125 @@ +package com.moyz.adi.common.util; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.core.JsonGenerationException; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParseException; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.io.StringWriter; + +@Slf4j +@Component +public class JsonUtil { + + private static final ObjectMapper objectMapper = new ObjectMapper(); + + static { + objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + objectMapper.configure(SerializationFeature.INDENT_OUTPUT, Boolean.FALSE); + objectMapper.registerModules(LocalDateTimeUtil.getSimpleModule(), new JavaTimeModule(), new Jdk8Module()); + } + + public static String toJsonString(Object obj) { + String resp = null; + try { + resp = objectMapper.writeValueAsString(obj); + } catch (JsonGenerationException e) { + log.error("JsonUtil error", e); + } catch (JsonMappingException e) { + log.error("JsonUtil error", e); + } catch (IOException e) { + log.error("JsonUtil error", e); + } + return resp; + } + + + /** + * 创建JSON处理器的静态方法 + * + * @param content JSON字符串 + * @return + */ + private static JsonParser getParser(String content) { + if(StringUtils.isNotBlank(content)){ + try { + return objectMapper.getFactory().createParser(content); + } catch (IOException ioe) { + log.error("JsonUtil getParser error", ioe); + } + } + return null; + } + + /** + * 创建JSON生成器的静态方法, 使用标准输出 + * + * @return + */ + private static JsonGenerator getGenerator(StringWriter sw) { + try { + return objectMapper.getFactory().createGenerator(sw); + } catch (IOException e) { + log.error("JsonUtil getGenerator error", e); + } + return null; + } + + /** + * JSON对象序列化 + */ + public static String toJson(Object obj) { + StringWriter sw = new StringWriter(); + JsonGenerator jsonGen = getGenerator(sw); + if (jsonGen == null) { + try { + sw.close(); + } catch (IOException e) { + log.error("JsonUtil toJSON error", e); + } + return null; + } + try { + //由于在getGenerator方法中指定了OutputStream为sw + //因此调用writeObject会将数据输出到sw + jsonGen.writeObject(obj == null ? "" : obj); + //由于采用流式输出 在输出完毕后务必清空缓冲区并关闭输出流 + jsonGen.flush(); + jsonGen.close(); + return sw.toString(); + } catch (JsonGenerationException jge) { + log.error("JSON生成错误", jge); + } catch (IOException ioe) { + log.error("JSON输入输出错误", ioe); + } + return null; + } + + /** + * JSON对象反序列化 + */ + public static T fromJson(String json, Class clazz) { + try { + JsonParser jp = getParser(json); + return jp.readValueAs(clazz); + } catch (JsonParseException jpe) { + log.error("反序列化失败", jpe); + } catch (JsonMappingException jme) { + log.error("反序列化失败", jme); + } catch (IOException ioe) { + log.error("反序列化失败", ioe); + } + return null; + } + +} \ No newline at end of file diff --git a/adi-common/src/main/java/com/moyz/adi/common/util/LocalCache.java b/adi-common/src/main/java/com/moyz/adi/common/util/LocalCache.java new file mode 100644 index 0000000..288f2b6 --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/util/LocalCache.java @@ -0,0 +1,14 @@ +package com.moyz.adi.common.util; + +import com.moyz.adi.common.model.RequestRateLimit; + +import java.util.HashMap; +import java.util.Map; + +public class LocalCache { + public static final Map CONFIGS = new HashMap<>(); + + public static RequestRateLimit TEXT_RATE_LIMIT_CONFIG; + + public static RequestRateLimit IMAGE_RATE_LIMIT_CONFIG; +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/util/LocalDateTimeDeserializer.java b/adi-common/src/main/java/com/moyz/adi/common/util/LocalDateTimeDeserializer.java new file mode 100644 index 0000000..2fe304c --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/util/LocalDateTimeDeserializer.java @@ -0,0 +1,19 @@ +package com.moyz.adi.common.util; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; + +import java.io.IOException; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +public class LocalDateTimeDeserializer extends JsonDeserializer { + + @Override + public LocalDateTime deserialize(JsonParser p, + DeserializationContext deserializationContext) + throws IOException { + return LocalDateTime.parse(p.getValueAsString(), DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); + } +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/util/LocalDateTimeSerializer.java b/adi-common/src/main/java/com/moyz/adi/common/util/LocalDateTimeSerializer.java new file mode 100644 index 0000000..766fb82 --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/util/LocalDateTimeSerializer.java @@ -0,0 +1,19 @@ +package com.moyz.adi.common.util; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; + +import java.io.IOException; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +public class LocalDateTimeSerializer extends JsonSerializer { + + @Override + public void serialize(LocalDateTime value, JsonGenerator gen, + SerializerProvider serializers) + throws IOException { + gen.writeString(value.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); + } +} \ No newline at end of file diff --git a/adi-common/src/main/java/com/moyz/adi/common/util/LocalDateTimeUtil.java b/adi-common/src/main/java/com/moyz/adi/common/util/LocalDateTimeUtil.java new file mode 100644 index 0000000..b8928ef --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/util/LocalDateTimeUtil.java @@ -0,0 +1,48 @@ +package com.moyz.adi.common.util; + +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; +import org.apache.commons.lang3.StringUtils; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +public class LocalDateTimeUtil { + + public static SimpleModule getSimpleModule() { + // jackson中自定义处理序列化和反序列化 + SimpleModule customModule = new SimpleModule(); + customModule.addSerializer(Long.class, ToStringSerializer.instance); + // 时间序列化 + customModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer()); + customModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer()); + return customModule; + } + + public static LocalDateTime parse(String localDateTime) { + return LocalDateTime.parse(localDateTime, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); + } + + public static String format(LocalDateTime localDateTime) { + if (null == localDateTime) { + return StringUtils.EMPTY; + } + return localDateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); + } + + public static String format(LocalDateTime localDateTime, String pattern) { + if (null == localDateTime) { + return StringUtils.EMPTY; + } + return localDateTime.format(DateTimeFormatter.ofPattern(pattern)); + } + + public static int getIntDay(LocalDateTime localDateTime) { + return localDateTime.getYear() * 10000 + localDateTime.getMonthValue() * 100 + localDateTime.getDayOfMonth(); + } + + public static int getToday() { + LocalDateTime now = LocalDateTime.now(); + return now.getYear() * 10000 + now.getMonthValue() * 100 + now.getDayOfMonth(); + } +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/util/MD5Utils.java b/adi-common/src/main/java/com/moyz/adi/common/util/MD5Utils.java new file mode 100644 index 0000000..2f23ee0 --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/util/MD5Utils.java @@ -0,0 +1,58 @@ +package com.moyz.adi.common.util; + +import org.springframework.util.DigestUtils; +import org.springframework.web.multipart.MultipartFile; + +import java.io.FileInputStream; +import java.io.IOException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +public class MD5Utils { + + public static String generateMD5(String input) { + try { + MessageDigest md = MessageDigest.getInstance("MD5"); + byte[] messageDigest = md.digest(input.getBytes()); + StringBuilder sb = new StringBuilder(); + for (byte b : messageDigest) { + sb.append(String.format("%02x", b & 0xff)); + } + return sb.toString(); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + } + + public static String calculateMD5(String filePath) { + try { + MessageDigest md = MessageDigest.getInstance("MD5"); + try (FileInputStream fis = new FileInputStream(filePath)) { + byte[] buffer = new byte[8192]; + int bytesRead; + while ((bytesRead = fis.read(buffer)) != -1) { + md.update(buffer, 0, bytesRead); + } + } + byte[] md5Bytes = md.digest(); + StringBuilder sb = new StringBuilder(); + for (byte md5Byte : md5Bytes) { + sb.append(Integer.toString((md5Byte & 0xff) + 0x100, 16).substring(1)); + } + return sb.toString(); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } catch (IOException ioException) { + throw new RuntimeException(ioException); + } + } + + public static String md5ByMultipartFile(MultipartFile file) { + try { + return DigestUtils.md5DigestAsHex(file.getBytes()); + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} + diff --git a/adi-common/src/main/java/com/moyz/adi/common/util/MPPageUtil.java b/adi-common/src/main/java/com/moyz/adi/common/util/MPPageUtil.java new file mode 100644 index 0000000..68eba5c --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/util/MPPageUtil.java @@ -0,0 +1,69 @@ +package com.moyz.adi.common.util; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; +import org.springframework.beans.BeanUtils; + +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +@Slf4j +public class MPPageUtil { + + public static Page convertTo(Page source, Page target, Class targetRecordClass) { + BeanUtils.copyProperties(source, target); + List records = new ArrayList<>(); + target.setRecords(records); + try { + for (T t : source.getRecords()) { + U u = targetRecordClass.getDeclaredConstructor().newInstance(); + BeanUtils.copyProperties(t, u); + records.add(u); + } + } catch (NoSuchMethodException e1) { + log.error("convertTo error1", e1); + } catch (Exception e2) { + log.error("convertTo error2", e2); + } + + return target; + } + + public static List convertTo(List source, Class targetRecordClass) { + if (CollectionUtils.isEmpty(source)) { + return Collections.emptyList(); + } + List result = new ArrayList<>(); + for (T t : source) { + try { + U u = targetRecordClass.getDeclaredConstructor().newInstance(); + BeanUtils.copyProperties(t, u); + result.add(u); + } catch (NoSuchMethodException e1) { + log.error("convertTo error1", e1); + } catch (Exception e2) { + log.error("convertTo error2", e2); + } + } + return result; + } + + public static U convertTo(T source, Class targetClass) { + try { + U target = targetClass.getDeclaredConstructor().newInstance(); + BeanUtils.copyProperties(source, target); + return target; + } catch (InstantiationException e) { + throw new RuntimeException(e); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } catch (InvocationTargetException e) { + throw new RuntimeException(e); + } catch (NoSuchMethodException e) { + throw new RuntimeException(e); + } + } +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/util/RedisTemplateUtil.java b/adi-common/src/main/java/com/moyz/adi/common/util/RedisTemplateUtil.java new file mode 100644 index 0000000..360eec6 --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/util/RedisTemplateUtil.java @@ -0,0 +1,27 @@ +package com.moyz.adi.common.util; + +import jakarta.annotation.Resource; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Component; + +import java.util.concurrent.TimeUnit; + +@Component +public class RedisTemplateUtil { + + @Resource + private StringRedisTemplate stringRedisTemplate; + + public boolean lock(String key, String clientId, int lockExpireInSecond) { + return stringRedisTemplate.opsForValue().setIfAbsent(key, clientId, lockExpireInSecond, TimeUnit.SECONDS); + } + + public boolean unlock(String key, String clientId) { + boolean result = false; + if (clientId.equals(stringRedisTemplate.opsForValue().get(key))) { + result = stringRedisTemplate.delete(key); + } + return result; + } + +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/util/TriConsumer.java b/adi-common/src/main/java/com/moyz/adi/common/util/TriConsumer.java new file mode 100644 index 0000000..e276d55 --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/util/TriConsumer.java @@ -0,0 +1,6 @@ +package com.moyz.adi.common.util; + +@FunctionalInterface +public interface TriConsumer { + void accept(T t, U u, V v); +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/util/UserUtil.java b/adi-common/src/main/java/com/moyz/adi/common/util/UserUtil.java new file mode 100644 index 0000000..9d520bb --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/util/UserUtil.java @@ -0,0 +1,12 @@ +package com.moyz.adi.common.util; + +import com.moyz.adi.common.cosntant.AdiConstant; +import com.moyz.adi.common.entity.User; +import org.apache.commons.lang3.StringUtils; + +public class UserUtil { + + public static int getSecretType(User user) { + return StringUtils.isNotBlank(user.getSecretKey()) ? AdiConstant.SECRET_KEY_TYPE_CUSTOM : AdiConstant.SECRET_KEY_TYPE_SYSTEM; + } +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/validator/AskReqValidator.java b/adi-common/src/main/java/com/moyz/adi/common/validator/AskReqValidator.java new file mode 100644 index 0000000..5dc00bc --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/validator/AskReqValidator.java @@ -0,0 +1,40 @@ +package com.moyz.adi.common.validator; + +import com.moyz.adi.common.annotation.AskReqCheck; +import com.moyz.adi.common.dto.AskReq; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import org.apache.commons.lang3.StringUtils; + +import java.util.regex.Pattern; + +public class AskReqValidator implements + ConstraintValidator { + + @Override + public void initialize(AskReqCheck constraintAnnotation) { + } + + @Override + public boolean isValid(AskReq value, ConstraintValidatorContext context) { + if (StringUtils.isAllBlank(value.getPrompt(), value.getRegenerateQuestionUuid())) { + throw new IllegalArgumentException("prompt and regenerateMsgUuid are empty"); + } + +// String uuidRegex = "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$"; + String uuidRegex = "^[0-9a-fA-F]{8}[0-9a-fA-F]{4}4[0-9a-fA-F]{3}[89abAB][0-9a-fA-F]{3}[0-9a-fA-F]{12}$"; + //check conversation uuid + boolean isValid = Pattern.matches(uuidRegex, value.getConversationUuid()); + if (!isValid) { + throw new IllegalArgumentException("conversation uuid error"); + } + //check regenerate msg uuid + if (StringUtils.isNotBlank(value.getRegenerateQuestionUuid())) { + boolean isValid2 = Pattern.matches(uuidRegex, value.getRegenerateQuestionUuid()); + if (!isValid2) { + throw new IllegalArgumentException("msg uuid error"); + } + } + return true; + } +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/validator/CreateImageReqValidator.java b/adi-common/src/main/java/com/moyz/adi/common/validator/CreateImageReqValidator.java new file mode 100644 index 0000000..0a19366 --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/validator/CreateImageReqValidator.java @@ -0,0 +1,26 @@ +package com.moyz.adi.common.validator; + +import com.moyz.adi.common.annotation.CreateImageReqCheck; +import com.moyz.adi.common.dto.CreateImageDto; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import org.apache.commons.lang3.StringUtils; + +import static com.moyz.adi.common.cosntant.AdiConstant.GenerateImage.*; + +public class CreateImageReqValidator implements + ConstraintValidator { + @Override + public boolean isValid(CreateImageDto createImageDto, ConstraintValidatorContext constraintValidatorContext) { + int interactingMethod = createImageDto.getInteractingMethod(); + if (interactingMethod == INTERACTING_METHOD_GENERATE_IMAGE && StringUtils.isBlank(createImageDto.getPrompt())) { + throw new IllegalArgumentException("Prompt can not be empty"); + } else if (interactingMethod == INTERACTING_METHOD_EDIT_IMAGE + && StringUtils.isAnyBlank(createImageDto.getOriginalImage(), createImageDto.getMaskImage(), createImageDto.getPrompt())) { + throw new IllegalArgumentException("Edit image,mask image or prompt can not be empty"); + } else if (interactingMethod == INTERACTING_METHOD_VARIATION && StringUtils.isBlank(createImageDto.getOriginalImage())) { + throw new IllegalArgumentException("Edit image can not be empty"); + } + return true; + } +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/validator/NotAllFieldsNullValidator.java b/adi-common/src/main/java/com/moyz/adi/common/validator/NotAllFieldsNullValidator.java new file mode 100644 index 0000000..bc64836 --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/validator/NotAllFieldsNullValidator.java @@ -0,0 +1,35 @@ +package com.moyz.adi.common.validator; + +import com.moyz.adi.common.annotation.NotAllFieldsEmptyCheck; +import com.moyz.adi.common.dto.UserUpdateReq; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import org.apache.commons.lang3.StringUtils; + +import java.lang.reflect.Field; + +public class NotAllFieldsNullValidator implements + ConstraintValidator { + + @Override + public void initialize(NotAllFieldsEmptyCheck constraintAnnotation) { + } + + @Override + public boolean isValid(UserUpdateReq value, ConstraintValidatorContext context) { + Field[] fields = UserUpdateReq.class.getDeclaredFields(); + try { + for (Field field : fields) { + Object object = field.get(value); + if (object instanceof String) { + return StringUtils.isNotBlank((String) object); + } else if (null != object) { + return true; + } + } + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + return true; + } +} diff --git a/adi-common/src/main/resources/mapper/ConversationMapper.xml b/adi-common/src/main/resources/mapper/ConversationMapper.xml new file mode 100644 index 0000000..2fb1e01 --- /dev/null +++ b/adi-common/src/main/resources/mapper/ConversationMapper.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/adi-common/src/main/resources/mapper/ConversationMessageMapper.xml b/adi-common/src/main/resources/mapper/ConversationMessageMapper.xml new file mode 100644 index 0000000..6931a7a --- /dev/null +++ b/adi-common/src/main/resources/mapper/ConversationMessageMapper.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/adi-common/src/main/resources/mapper/UserMapper.xml b/adi-common/src/main/resources/mapper/UserMapper.xml new file mode 100644 index 0000000..f6bf245 --- /dev/null +++ b/adi-common/src/main/resources/mapper/UserMapper.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/docker-compose/.env b/docker-compose/.env new file mode 100644 index 0000000..15cd811 --- /dev/null +++ b/docker-compose/.env @@ -0,0 +1,4 @@ +TZ=Asia/Shanghai +IMAGE_VERSION=v1.1.0-dev +APP_VERSION=1.0.0-SNAPSHOT +APP_PROFILE=dev \ No newline at end of file diff --git a/docker-compose/.env.prod b/docker-compose/.env.prod new file mode 100644 index 0000000..e28418b --- /dev/null +++ b/docker-compose/.env.prod @@ -0,0 +1,4 @@ +TZ=Asia/Shanghai +IMAGE_VERSION=v1.0.0 +APP_VERSION=1.0.0-SNAPSHOT +APP_PROFILE=prod diff --git a/docker-compose/docker-compose.yml b/docker-compose/docker-compose.yml new file mode 100644 index 0000000..c5d90cd --- /dev/null +++ b/docker-compose/docker-compose.yml @@ -0,0 +1,24 @@ +version: '3.7' +services: + redis: + image: redis:6.2.5 + container_name: redis + ports: + - 6379:6379 + volumes: + - /data/redis/data:/data + - /data/redis/config:/usr/local/etc/redis/redis.conf + - /data/redis/logs:/logs + command: ["redis-server","/usr/local/etc/redis/redis.conf"] + aideepin: + image: adi-bootstrap:${IMAGE_VERSION} + container_name: aideepin + build: ../adi-chat + environment: + APP_VERSION: ${APP_VERSION} + TZ: ${TZ} + APP_PROFILE: ${APP_PROFILE} + ports: + - 9999:9999 + volumes: + - /data/aideepin/logs:/data/logs \ No newline at end of file diff --git a/docs/create.sql b/docs/create.sql new file mode 100644 index 0000000..ce10e15 --- /dev/null +++ b/docs/create.sql @@ -0,0 +1,186 @@ +CREATE TABLE `adi_ai_model` +( + `id` bigint NOT NULL AUTO_INCREMENT, + `name` varchar(45) NOT NULL DEFAULT '', + `remark` varchar(1000) DEFAULT NULL, + `model_status` tinyint NOT NULL DEFAULT '1' COMMENT '1:正常使用,2:不可用', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_general_ci COMMENT ='ai模型'; + +CREATE TABLE `adi_sys_config` +( + `id` bigint NOT NULL AUTO_INCREMENT, + `name` varchar(100) NOT NULL DEFAULT '', + `value` varchar(100) NOT NULL DEFAULT '', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `is_delete` tinyint NOT NULL DEFAULT '0', + PRIMARY KEY (`id`) +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_general_ci COMMENT ='系统配置表'; + +INSERT INTO `adi_sys_config` (`name`, `value`) +VALUES ('secret_key', ''); +INSERT INTO `adi_sys_config` (`name`, `value`) +VALUES ('request_text_rate_limit', '{"times":24,"minutes":3}'); +INSERT INTO `adi_sys_config` (`name`, `value`) +VALUES ('request_image_rate_limit', '{"times":6,"minutes":3}'); +INSERT INTO `adi_sys_config` (`name`, `value`) +VALUES ('conversation_max_num', '50'); +INSERT INTO `adi_sys_config` (`name`, `value`) +VALUES ('quota_by_token_daily', '10000'); +INSERT INTO `adi_sys_config` (`name`, `value`) +VALUES ('quota_by_token_monthly', '200000'); +INSERT INTO `adi_sys_config` (`name`, `value`) +VALUES ('quota_by_request_daily', '150'); +INSERT INTO `adi_sys_config` (`name`, `value`) +VALUES ('quota_by_request_monthly', '3000'); +INSERT INTO `adi_sys_config` (`name`, `value`) +VALUES ('quota_by_image_daily', '30'); +INSERT INTO `adi_sys_config` (`name`, `value`) +VALUES ('quota_by_image_monthly', '300'); + +CREATE TABLE `adi_conversation` +( + `id` bigint NOT NULL AUTO_INCREMENT, + `user_id` bigint NOT NULL DEFAULT '0' COMMENT '用户id', + `title` varchar(45) NOT NULL DEFAULT '' COMMENT '对话标题', + `uuid` varchar(32) NOT NULL DEFAULT '', + `understand_context_enable` tinyint NOT NULL default '0' COMMENT '是否开启上下文理解', + `ai_model` varchar(45) NOT NULL DEFAULT '' COMMENT 'ai model', + `ai_system_message` varchar(1024) NOT NULL DEFAULT '' COMMENT 'set the system message to ai, ig: you are a lawyer', + `tokens` int NOT NULL DEFAULT '0' COMMENT '消耗token数量', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `is_delete` tinyint NOT NULL DEFAULT '0', + PRIMARY KEY (`id`) +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_general_ci COMMENT ='对话表'; + +CREATE TABLE `adi_conversation_message` +( + `id` bigint NOT NULL AUTO_INCREMENT, + `parent_message_id` bigint NOT NULL DEFAULT '0' COMMENT '父级消息id', + `conversation_id` bigint NOT NULL DEFAULT '0', + `conversation_uuid` varchar(32) NOT NULL DEFAULT '', + `user_id` bigint NOT NULL DEFAULT '0' COMMENT 'User id', + `content` text NOT NULL COMMENT '对话的消息', + `uuid` varchar(32) NOT NULL DEFAULT '', + `message_role` varchar(25) NOT NULL DEFAULT '' COMMENT '产生该消息的角色:1:user,2:system,3:assistant', + `tokens` int NOT NULL DEFAULT '0' COMMENT '消耗的token数量', + `secret_key_type` int NOT NULL DEFAULT '1' COMMENT '1:System secret key,2:User secret key', + `understand_context_msg_pair_num` int NOT NULL DEFAULT '0' COMMENT 'If context understanding enable, context_pair_msg_num > 0', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `is_delete` tinyint NOT NULL DEFAULT '0' COMMENT '是否删除,0:未删除,1:已删除', + PRIMARY KEY (`id`), + UNIQUE KEY `udx_uuid` (`uuid`) +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_general_ci COMMENT ='对话信息表'; + +CREATE TABLE `adi_ai_image` +( + `id` bigint NOT NULL AUTO_INCREMENT, + `user_id` bigint NOT NULL DEFAULT '0' COMMENT 'The user who generated the image', + `uuid` varchar(32) NOT NULL DEFAULT '' COMMENT 'The uuid of the request of generated images', + `prompt` varchar(1024) NOT NULL DEFAULT '' COMMENT 'The prompt for generating images', + `generate_size` varchar(20) NOT NULL DEFAULT '' COMMENT 'The size of the generated images. Must be one of "256x256", "512x512", or "1024x1024"', + `generate_number` int NOT NULL DEFAULT '1' COMMENT 'The number of images to generate. Must be between 1 and 10. Defaults to 1.', + `original_image` varchar(32) NOT NULL DEFAULT '' COMMENT 'The original image uuid,interacting_method must be 2/3', + `mask_image` varchar(32) NOT NULL DEFAULT '' COMMENT 'The mask image uuid,interacting_method must be 2', + `resp_images_path` varchar(2048) NOT NULL DEFAULT '' COMMENT 'The url of the generated images which from openai response,separated by commas', + `generated_images` varchar(512) NOT NULL DEFAULT '' COMMENT 'The uuid of the generated images,separated by commas', + `interacting_method` smallint NOT NULL DEFAULT '1' COMMENT '1:Creating images from scratch based on a text prompt;2:Creating edits of an existing image based on a new text prompt;3:Creating variations of an existing image', + `process_status` smallint NOT NULL DEFAULT '1' COMMENT 'Process status,1:processing,2:fail,3:success', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `is_delete` tinyint NOT NULL DEFAULT '0', + PRIMARY KEY (`id`), + UNIQUE KEY `udx_uuid` (`uuid`) +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_general_ci COMMENT ='Images generated by ai'; + +CREATE TABLE `adi_user` +( + `id` bigint NOT NULL AUTO_INCREMENT, + `name` varchar(45) NOT NULL DEFAULT '', + `password` varchar(120) NOT NULL DEFAULT '', + `uuid` varchar(32) NOT NULL DEFAULT '', + `email` varchar(120) NOT NULL DEFAULT '', + `active_time` datetime NULL COMMENT '激活时间', + `secret_key` varchar(120) NOT NULL default '' COMMENT 'Custom openai secret key', + `understand_context_msg_pair_num` int NOT NULL default '0' COMMENT '上下文理解中需要携带的消息对数量(提示词及回复)', + `quota_by_token_daily` int NOT NULL DEFAULT '0' COMMENT 'The quota of token daily', + `quota_by_token_monthly` int NOT NULL DEFAULT '0' COMMENT 'The quota of token monthly', + `quota_by_request_daily` int NOT NULL DEFAULT '0' COMMENT 'The quota of http request daily', + `quota_by_request_monthly` int NOT NULL DEFAULT '0' COMMENT 'The quota of http request monthly', + `quota_by_image_daily` int NOT NULL DEFAULT '0' COMMENT 'The quota of generate images daily', + `quota_by_image_monthly` int NOT NULL DEFAULT '0' COMMENT 'The quota of generate images monthly', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `user_status` smallint NOT NULL DEFAULT '1' COMMENT '用户状态,1:待验证;2:正常;3:冻结', + `is_delete` tinyint NOT NULL DEFAULT '0' COMMENT '0:未删除;1:已删除', + PRIMARY KEY (`id`), + UNIQUE KEY `udx_uuid` (`uuid`) +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_general_ci COMMENT ='用户表'; + +CREATE TABLE `adi_user_day_cost` +( + `id` bigint NOT NULL AUTO_INCREMENT, + `user_id` bigint NOT NULL DEFAULT '0', + `day` int NOT NULL DEFAULT '0' COMMENT '日期,用7位整数表示,如20230901', + `requests` int NOT NULL DEFAULT '0' COMMENT 'The number of http request', + `tokens` int NOT NULL DEFAULT '0' COMMENT 'The cost of the tokens', + `images_number` int NOT NULL DEFAULT '0' COMMENT 'The number of images', + `secret_key_type` int NOT NULL DEFAULT '1' COMMENT '1:System secret key,2:Custom secret key', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_general_ci COMMENT ='用户每天消耗总量表'; + + +CREATE TABLE `adi_prompt` +( + `id` bigint NOT NULL AUTO_INCREMENT, + `user_id` bigint NOT NULL DEFAULT '0' COMMENT '0:System,other:User', + `act` varchar(120) NOT NULL DEFAULT '' COMMENT 'Short prompt for search/autocomplete', + `prompt` varchar(1024) NOT NULL COMMENT 'Prompt content', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `is_delete` tinyint NOT NULL DEFAULT '0' COMMENT '0:Normal;1:Deleted', + PRIMARY KEY (`id`), + KEY `idx_title` (`act`) +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_general_ci COMMENT ='提示词'; + +CREATE TABLE `adi_file` +( + `id` bigint NOT NULL AUTO_INCREMENT, + `name` varchar(32) NOT NULL DEFAULT '', + `uuid` varchar(32) NOT NULL DEFAULT '', + `md5` varchar(128) NOT NULL DEFAULT '', + `ext` varchar(32) NOT NULL DEFAULT '', + `user_id` bigint NOT NULL DEFAULT '0' COMMENT '0:System,other:User', + `path` varchar(250) NOT NULL DEFAULT '', + `ref_count` int NOT NULL DEFAULT '0' COMMENT 'The number of references to this file', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `is_delete` tinyint NOT NULL DEFAULT '0' COMMENT '0:Normal;1:Deleted', + PRIMARY KEY (`id`), + KEY `idx_uuid` (`uuid`) +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_general_ci COMMENT ='提示词'; \ No newline at end of file diff --git a/image/README/1691582252468.png b/image/README/1691582252468.png new file mode 100644 index 0000000..85b5b9d Binary files /dev/null and b/image/README/1691582252468.png differ diff --git a/image/README/1691583124744.png b/image/README/1691583124744.png new file mode 100644 index 0000000..d959bcf Binary files /dev/null and b/image/README/1691583124744.png differ diff --git a/image/README/1691583184761.png b/image/README/1691583184761.png new file mode 100644 index 0000000..3358048 Binary files /dev/null and b/image/README/1691583184761.png differ diff --git a/image/README/1691583329105.png b/image/README/1691583329105.png new file mode 100644 index 0000000..a641ab1 Binary files /dev/null and b/image/README/1691583329105.png differ diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..707252f --- /dev/null +++ b/pom.xml @@ -0,0 +1,140 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.0.5 + + + com.moyz + aideepin + 0.0.1-SNAPSHOT + pom + aideepin + chatgpt + + adi-common + adi-chat + adi-bootstrap + adi-admin + + + 17 + 17 + 17 + UTF-8 + + + + org.springframework.boot + spring-boot-starter-mail + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-data-redis + + + org.springframework.boot + spring-boot-starter-cache + + + org.springframework.boot + spring-boot-configuration-processor + true + + + org.projectlombok + lombok + true + + + com.baomidou + mybatis-plus-boot-starter + 3.5.3.1 + + + com.google.guava + guava + 31.1-jre + + + com.mysql + mysql-connector-j + 8.0.32 + + + com.theokanning.openai-gpt3-java + client + 0.12.0 + + + com.theokanning.openai-gpt3-java + service + 0.12.0 + + + com.didalgo + gpt3-tokenizer + 0.1.2 + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + 2.1.0 + + + org.apache.commons + commons-collections4 + 4.4 + + + org.aspectj + aspectjweaver + + + com.talanlabs + avatar-generator-cat + 1.1.0 + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + + + org.mindrot + jbcrypt + 0.4 + + + commons-io + commons-io + 2.13.0 + + + com.baomidou + mybatis-plus-generator + 3.5.3.1 + + + org.apache.velocity + velocity-engine-core + 2.3 + + + org.springframework.boot + spring-boot-starter-test + test + + + +