Compare commits

..

1 Commits

Author SHA1 Message Date
Grand-cocoa
afa84020e5 feat(auth): 实现用户认证模块
- 新增登录与注册接口控制器 AuthController
- 创建登录表单数据类 LoginForm
- 定义用户实体类 User 并实现 UserDetails 接口
- 添加用户持久层接口 UserMapper- 实现认证服务 AuthServiceImpl 包含登录和注册逻辑
- 配置 Spring Security 安全策略并禁用 CSRF
- 引入 BCrypt 加密算法用于密码处理
- 添加全局异常处理器 ExceptionFilter 捕获业务异常
- 创建统一响应模型 R 封装返回结果
- 集成 MyBatis Plus依赖并配置数据源信息
2025-11-07 16:10:35 +08:00
40 changed files with 157 additions and 1505 deletions

View File

@ -1,15 +0,0 @@
name: Auto-build
run-name: Automatic-Packaging 📦
on: [ push ]
env:
BARE_REPO_DIR: https://git.alina-dace.info/Dace/Animo-Server.git
CLONED_REPO_DIR: ./
jobs:
Automatic-Packaging:
runs-on: ubuntu-latest
steps:
- name: Checkout Git Repo
uses: https://git.alina-dace.info/actions/checkout@v4
- name: Docker build
working-directory: ${{ env.CLONED_REPO_DIR }}
run: docker build -t animo-server:latest ./

View File

@ -1,18 +0,0 @@
FROM maven:3.8.4-openjdk-17 AS builder
WORKDIR /app
COPY pom.xml .
COPY src ./src
RUN --mount=type=cache,target=/root/.m2 \
mvn clean package -DskipTests=true -P prod
FROM bellsoft/liberica-openjdk-debian:17.0.11-cds
LABEL maintainer="Kane / Arina Dace / Sakura Reimi"
ENV LANG C.UTF-8
ENV LC_ALL C.UTF-8
COPY --from=builder /app/target/*.jar /app.jar
ENTRYPOINT exec java -jar /app.jar

102
pom.xml
View File

@ -26,43 +26,8 @@
<tag/>
<url/>
</scm>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>${spring-ai.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<repositories>
<repository>
<id>maven-central</id>
<url>https://repo.maven.apache.org/maven2</url>
</repository>
</repositories>
<profiles>
<profile>
<id>dev</id>
<properties>
<spring.profiles.active>dev</spring.profiles.active>
</properties>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
</profile>
<profile>
<id>prod</id>
<properties>
<spring.profiles.active>prod</spring.profiles.active>
</properties>
</profile>
</profiles>
<properties>
<java.version>17</java.version>
<spring-ai.version>1.1.0</spring-ai.version>
</properties>
<dependencies>
<dependency>
@ -76,6 +41,12 @@
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<!-- <dependency>-->
<!-- <groupId>org.springframework.boot</groupId>-->
<!-- <artifactId>spring-boot-docker-compose</artifactId>-->
<!-- <scope>runtime</scope>-->
<!-- <optional>true</optional>-->
<!-- </dependency>-->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
@ -91,53 +62,18 @@
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>3.5.10.1</version>
</dependency>
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot3-starter</artifactId>
<version>1.39.0</version>
</dependency>
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.52</version>
</dependency>
<dependency>
<groupId>com.yubico</groupId>
<artifactId>webauthn-server-core</artifactId>
<version>2.7.0</version>
<scope>compile</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/org.ehcache/ehcache -->
<dependency>
<groupId>org.ehcache</groupId>
<artifactId>ehcache</artifactId>
<version>3.11.0</version>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-deepseek</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-core</artifactId>
<version>5.8.31</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-extra</artifactId>
<version>5.8.40</version>
</dependency>
</dependencies>
<build>
<finalName>${project.artifactId}</finalName>
<sourceDirectory>${project.basedir}/src/main/java</sourceDirectory>
<testSourceDirectory>${project.basedir}/src/test/java</testSourceDirectory>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
@ -164,24 +100,6 @@
</configuration>
</plugin>
</plugins>
<resources>
<resource>
<directory>src/main/resources</directory>
<!-- 关闭过滤 -->
<filtering>false</filtering>
</resource>
<resource>
<directory>src/main/resources</directory>
<!-- 引入所有 匹配文件进行过滤 -->
<includes>
<include>application*</include>
<include>bootstrap*</include>
<include>banner*</include>
</includes>
<!-- 启用过滤 即该资源中的变量将会被过滤器中的值替换 -->
<filtering>true</filtering>
</resource>
</resources>
</build>
</project>

View File

@ -1,11 +1,13 @@
package com.kane.animo.auth.controller;
import com.kane.animo.auth.domain.form.LoginForm;
import com.kane.animo.auth.domain.vo.UserInfoVO;
import com.kane.animo.auth.service.AuthService;
import com.kane.animo.model.R;
import jakarta.annotation.Resource;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 认证
@ -38,13 +40,4 @@ public class AuthController {
public R<String> register(@RequestBody LoginForm from) {
return service.register(from);
}
/**
* 获取用户信息
* @return 用户信息
*/
@GetMapping("/getUserInfo")
public R<UserInfoVO> getUserInfo() {
return R.success(service.getUserInfo());
}
}

View File

@ -1,78 +0,0 @@
package com.kane.animo.auth.controller;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.kane.animo.auth.domain.vo.UserCredentialVO;
import com.kane.animo.auth.service.PasskeyAuthorizationService;
import com.kane.animo.model.R;
import com.yubico.webauthn.exception.AssertionFailedException;
import com.yubico.webauthn.exception.RegistrationFailedException;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.web.bind.annotation.*;
import java.io.IOException;
import java.util.List;
/**
* 通行密钥
* @author Kane
* @since 2025/11/7 17:40
*/
@RestController
@RequestMapping("/passkey")
public class PasskeyAuthorizationController {
@Resource
private PasskeyAuthorizationService service;
/**
* 获取通行密钥创建参数
* @return 创建参数
*/
@GetMapping("/registration/options")
public R<String> getPasskeyRegistrationOptions() throws JsonProcessingException {
return R.success(service.startPasskeyRegistration());
}
/**
* 验证通行密钥
* @param credential 凭证
* @param name 凭证名称
*/
@PostMapping("/registration")
public R<Void> verifyPasskeyRegistration(@RequestBody String credential, String name) throws RegistrationFailedException, IOException {
service.finishPasskeyRegistration(credential, name);
return R.success();
}
/**
* 获取通行密钥验证参数
* @param httpServletRequest 请求
* @return 验证参数
*/
@GetMapping("/assertion/options")
public R<String> getPasskeyAssertionOptions(HttpServletRequest httpServletRequest) throws JsonProcessingException {
return R.success(service.startPasskeyAssertion(httpServletRequest.getSession().getId()));
}
/**
* 验证通行密钥
* @param httpServletRequest 请求
* @param credential 凭证
*/
@PostMapping("/assertion")
public R<String> verifyPasskeyAssertion(HttpServletRequest httpServletRequest, @RequestBody String credential) throws IOException, AssertionFailedException {
String s = service.finishPasskeyAssertion(httpServletRequest.getSession().getId(), credential);
return R.success(s);
}
/**
* 获取所有凭证
* @return 凭证
*/
@GetMapping("/credentials")
public R<List<UserCredentialVO>> getCredentials() {
return R.success(service.getCredentials());
}
}

View File

@ -3,11 +3,15 @@ package com.kane.animo.auth.domain;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.io.InputStream;
import java.io.Serial;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.io.InputStream;
import java.util.Collection;
import java.util.List;
/**
* 用户
@ -16,7 +20,7 @@ import java.time.LocalDateTime;
*/
@Data
@TableName("user")
public class User implements Serializable {
public class User implements Serializable, UserDetails {
@Serial
private static final long serialVersionUID = 1L;
@ -52,4 +56,24 @@ public class User implements Serializable {
*/
private LocalDateTime updateTime;
/**
* Returns the authorities granted to the user. Cannot return <code>null</code>.
*
* @return the authorities, sorted by natural key (never <code>null</code>)
*/
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return List.of();
}
/**
* Returns the username used to authenticate the user. Cannot return
* <code>null</code>.
*
* @return the username (never <code>null</code>)
*/
@Override
public String getUsername() {
return user;
}
}

View File

@ -1,58 +0,0 @@
package com.kane.animo.auth.domain;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.experimental.Accessors;
import java.time.LocalDateTime;
/**
* 用户凭证
* @author Kane
* @since 2025/11/7 16:57
*/
@Data
@Accessors(chain = true)
@TableName("user_credential")
public class UserCredential {
/**
* ID
*/
@TableId
private Long id;
/**
* 用户ID
*/
private Long userId;
/**
* 用户凭证名称
*/
private String name;
/**
* 用户凭证
*/
private String credential;
/**
* 身份标识
*/
private String identity;
/**
* 传输方式
*/
private String transports;
/**
* 最后使用时间
*/
private LocalDateTime lastUsed;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 删除标识
*/
@TableLogic
private Integer delFlag;
}

View File

@ -1,32 +0,0 @@
package com.kane.animo.auth.domain.vo;
import lombok.Data;
import lombok.experimental.Accessors;
import java.time.LocalDateTime;
/**
* 用户凭证
* @author Kane
* @since 2025/11/11 16:33
*/
@Data
@Accessors(chain = true)
public class UserCredentialVO {
/**
* 凭证ID
*/
private Long id;
/**
* 凭证名称
*/
private String name;
/**
* 最后使用时间
*/
private LocalDateTime lastUsed;
/**
* 创建时间
*/
private LocalDateTime created;
}

View File

@ -1,26 +0,0 @@
package com.kane.animo.auth.domain.vo;
import lombok.Data;
import lombok.experimental.Accessors;
/**
* 用户信息
* @author Kane
* @since 2025/11/12 16:50
*/
@Data
@Accessors(chain = true)
public class UserInfoVO {
/**
* 用户ID
*/
private Long userId;
/**
* 用户名
*/
private String name;
/**
* 头像
*/
private String avatar;
}

View File

@ -1,14 +0,0 @@
package com.kane.animo.auth.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.kane.animo.auth.domain.UserCredential;
import org.apache.ibatis.annotations.Mapper;
/**
* 用户凭证持久层
* @author Kane
* @since 2025/11/7 17:00
*/
@Mapper
public interface UserCredentialMapper extends BaseMapper<UserCredential> {
}

View File

@ -1,7 +1,6 @@
package com.kane.animo.auth.service;
import com.kane.animo.auth.domain.form.LoginForm;
import com.kane.animo.auth.domain.vo.UserInfoVO;
import com.kane.animo.model.R;
/**
@ -23,10 +22,4 @@ public interface AuthService {
* @return 注册结果
*/
R<String> register(LoginForm from);
/**
* 获取用户信息
* @return 用户信息
*/
UserInfoVO getUserInfo();
}

View File

@ -1,54 +0,0 @@
package com.kane.animo.auth.service;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.kane.animo.auth.domain.vo.UserCredentialVO;
import com.yubico.webauthn.exception.AssertionFailedException;
import com.yubico.webauthn.exception.RegistrationFailedException;
import java.io.IOException;
import java.util.List;
/**
* 通行密钥授权服务
*
* @author Kane
* @since 2025/11/7 17:46
*/
public interface PasskeyAuthorizationService {
/**
* 获取通行密钥创建参数
*
* @return 创建参数
*/
String startPasskeyRegistration() throws JsonProcessingException;
/**
* 验证通行密钥
*
* @param credential 凭证
* @param name 凭证名称
*/
void finishPasskeyRegistration(String credential, String name) throws IOException, RegistrationFailedException;
/**
* 获取通行密钥验证参数
*
* @param id 登录id
* @return 验证参数
*/
String startPasskeyAssertion(String id) throws JsonProcessingException;
/**
* 验证通行密钥
*
* @param id 登录id
* @param credential 凭证
*/
String finishPasskeyAssertion(String id, String credential) throws IOException, AssertionFailedException;
/**
* 获取所有凭证
* @return 凭证
*/
List<UserCredentialVO> getCredentials();
}

View File

@ -1,21 +1,19 @@
package com.kane.animo.auth.service.impl;
import cn.dev33.satoken.secure.BCrypt;
import cn.dev33.satoken.stp.StpUtil;
import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper;
import com.kane.animo.auth.domain.User;
import com.kane.animo.auth.domain.form.LoginForm;
import com.kane.animo.auth.domain.vo.UserInfoVO;
import com.kane.animo.auth.mapper.UserMapper;
import com.kane.animo.auth.service.AuthService;
import com.kane.animo.exception.ServiceException;
import com.kane.animo.model.R;
import jakarta.annotation.Resource;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.crypto.bcrypt.BCrypt;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.util.Base64;
/**
* 认证服务
* @author Kane
@ -24,6 +22,9 @@ import java.util.Base64;
@Service
public class AuthServiceImpl implements AuthService {
@Resource
private AuthenticationManager authenticationManager;
@Resource
private UserMapper userMapper;
@ -35,17 +36,14 @@ public class AuthServiceImpl implements AuthService {
*/
@Override
public R<String> login(LoginForm from) {
User one = new LambdaQueryChainWrapper<>(userMapper)
.eq(User::getUser, from.user())
.one();
if (one == null){
throw new ServiceException("用户不存在");
Authentication authenticationToken = new UsernamePasswordAuthenticationToken(from.user(), from.password());
Authentication authenticate = authenticationManager.authenticate(authenticationToken);
if (authenticate == null){
throw new ServiceException("用户名或密码错误");
}
if (!BCrypt.checkpw(from.password(), one.getPassword())){
throw new ServiceException("密码错误");
}
StpUtil.login(one.getId());
return R.success();
User user = (User) authenticate.getPrincipal();
return R.success("OK");
}
/**
@ -66,27 +64,7 @@ public class AuthServiceImpl implements AuthService {
user.setUser(from.user());
user.setPassword(BCrypt.hashpw(from.password(), BCrypt.gensalt()));
userMapper.insert(user);
return login(from);
}
/**
* 获取用户信息
*
* @return 用户信息
*/
@Override
public UserInfoVO getUserInfo() {
User one = new LambdaQueryChainWrapper<>(userMapper)
.eq(User::getId, StpUtil.getLoginIdAsLong())
.one();
String avatar = null;
try {
avatar = Base64.getEncoder().encodeToString(one.getAvatar().readAllBytes());
} catch (Exception ignored) {
}
return new UserInfoVO()
.setName(one.getUser())
.setUserId(one.getId())
.setAvatar(avatar);
}
}

View File

@ -1,163 +0,0 @@
package com.kane.animo.auth.service.impl;
import com.alibaba.fastjson2.JSON;
import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper;
import com.kane.animo.auth.domain.User;
import com.kane.animo.auth.domain.UserCredential;
import com.kane.animo.auth.mapper.UserCredentialMapper;
import com.kane.animo.auth.mapper.UserMapper;
import com.yubico.webauthn.CredentialRepository;
import com.yubico.webauthn.RegisteredCredential;
import com.yubico.webauthn.RegistrationResult;
import com.yubico.webauthn.data.AuthenticatorTransport;
import com.yubico.webauthn.data.ByteArray;
import com.yubico.webauthn.data.PublicKeyCredentialDescriptor;
import com.yubico.webauthn.data.UserIdentity;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
/**
* 凭证存储
* @author Kane
* @since 2025/11/7 17:04
*/
@Service
public class CredentialRepositoryImpl implements CredentialRepository {
@Resource
private UserMapper userMapper;
@Resource
private UserCredentialMapper credentialMapper;
/**
* Get the credential IDs of all credentials registered to the user with the given username.
*
* <p>After a successful registration ceremony, the {@link RegistrationResult#getKeyId()} method
* returns a value suitable for inclusion in this set.
*
* <p>Implementations of this method MUST NOT return null.
*
* @param username
*/
@Override
public Set<PublicKeyCredentialDescriptor> getCredentialIdsForUsername(String username) {
User one = new LambdaQueryChainWrapper<>(userMapper)
.eq(User::getUser, username)
.one();
return new LambdaQueryChainWrapper<>(credentialMapper)
.eq(UserCredential::getUserId, one.getId())
.list()
.stream()
// .map(UserCredential::getCredential)
.map(x -> {
RegisteredCredential credential = JSON.parseObject(x.getCredential(), RegisteredCredential.class);
return PublicKeyCredentialDescriptor.builder()
.id(credential.getCredentialId())
.transports(new HashSet<>(JSON.parseArray(x.getTransports(), AuthenticatorTransport.class)))
.build();
}).collect(Collectors.toSet());
}
/**
* Get the user handle corresponding to the given username - the inverse of {@link
* #getUsernameForUserHandle(ByteArray)}.
*
* <p>Used to look up the user handle based on the username, for authentication ceremonies where
* the username is already given.
*
* <p>Implementations of this method MUST NOT return null.
*
* @param username
*/
@Override
public Optional<ByteArray> getUserHandleForUsername(String username) {
User one = new LambdaQueryChainWrapper<>(userMapper)
.eq(User::getUser, username)
.one();
return new LambdaQueryChainWrapper<>(credentialMapper)
.eq(UserCredential::getUserId, one.getId())
.list()
.stream().findAny()
.map(x -> JSON.parseObject(x.getIdentity(), UserIdentity.class).getId());
}
/**
* Get the username corresponding to the given user handle - the inverse of {@link
* #getUserHandleForUsername(String)}.
*
* <p>Used to look up the username based on the user handle, for username-less authentication
* ceremonies.
*
* <p>Implementations of this method MUST NOT return null.
*
* @param userHandle
*/
@Override
public Optional<String> getUsernameForUserHandle(ByteArray userHandle) {
Set<Long> userIds = credentialMapper.selectList(null)
.stream().filter(x -> {
UserIdentity userIdentity = JSON.parseObject(x.getIdentity(), UserIdentity.class);
return userIdentity.getId().equals(userHandle);
}).map(UserCredential::getUserId).collect(Collectors.toSet());
if (userIds.isEmpty()){
return Optional.empty();
}
return Optional.ofNullable(new LambdaQueryChainWrapper<>(userMapper)
.eq(User::getId, userIds.iterator().next())
.one()
.getUser());
}
/**
* Look up the public key and stored signature count for the given credential registered to the
* given user.
*
* <p>The returned {@link RegisteredCredential} is not expected to be long-lived. It may be read
* directly from a database or assembled from other components.
*
* <p>Implementations of this method MUST NOT return null.
*
* @param credentialId
* @param userHandle
*/
@Override
public Optional<RegisteredCredential> lookup(ByteArray credentialId, ByteArray userHandle) {
List<UserCredential> userCredentials = credentialMapper.selectList(null);
for (UserCredential userCredential : userCredentials) {
RegisteredCredential credential = JSON.parseObject(userCredential.getCredential(), RegisteredCredential.class);
UserIdentity identity = JSON.parseObject(userCredential.getIdentity(), UserIdentity.class);
if (credential.getCredentialId().equals(credentialId) && identity.getId().equals(userHandle)){
return Optional.of(credential);
}
}
return Optional.empty();
}
/**
* Look up all credentials with the given credential ID, regardless of what user they're
* registered to.
*
* <p>This is used to refuse registration of duplicate credential IDs. Therefore, under normal
* circumstances this method should only return zero or one credential (this is an expected
* consequence, not an interface requirement).
*
* <p>Implementations of this method MUST NOT return null.
*
* @param credentialId
*/
@Override
public Set<RegisteredCredential> lookupAll(ByteArray credentialId) {
List<UserCredential> userCredentials = credentialMapper.selectList(null);
return userCredentials.stream()
.map(x -> JSON.parseObject(x.getCredential(), RegisteredCredential.class))
.filter(x -> x.getCredentialId().equals(credentialId))
.collect(Collectors.toSet());
}
}

View File

@ -1,178 +0,0 @@
package com.kane.animo.auth.service.impl;
import cn.dev33.satoken.stp.StpUtil;
import com.alibaba.fastjson2.JSON;
import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper;
import com.baomidou.mybatisplus.extension.conditions.update.LambdaUpdateChainWrapper;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.kane.animo.auth.domain.User;
import com.kane.animo.auth.domain.UserCredential;
import com.kane.animo.auth.domain.vo.UserCredentialVO;
import com.kane.animo.auth.mapper.UserCredentialMapper;
import com.kane.animo.auth.mapper.UserMapper;
import com.kane.animo.auth.service.PasskeyAuthorizationService;
import com.kane.animo.exception.ServiceException;
import com.kane.animo.util.CacheService;
import com.yubico.webauthn.*;
import com.yubico.webauthn.data.*;
import com.yubico.webauthn.exception.AssertionFailedException;
import com.yubico.webauthn.exception.RegistrationFailedException;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.time.LocalDateTime;
import java.util.List;
import java.util.TreeSet;
/**
* 通行密钥授权服务实现
* @author Kane
* @since 2025/11/7 17:46
*/
@Service
public class PasskeyAuthorizationServiceImpl implements PasskeyAuthorizationService {
@Resource
private RelyingParty relyingParty;
@Resource
private UserMapper userMapper;
@Resource
private UserCredentialMapper credentialMapper;
@Resource
private CacheService cacheService;
private static final String PASSKEY_REGISTRATION_KEY = "passkey:registration:";
private static final String PASSKEY_ASSERTION_KEY = "passkey:assertion:";
/**
* 获取通行密钥创建参数
*
* @return 创建参数
*/
@Override
public String startPasskeyRegistration() throws JsonProcessingException {
User user = userMapper.selectById(StpUtil.getLoginIdAsLong());
PublicKeyCredentialCreationOptions options = relyingParty.startRegistration(StartRegistrationOptions.builder()
.user(UserIdentity.builder()
.name(user.getUser())
.displayName(user.getUser())
.id(new ByteArray(user.getId().toString().getBytes()))
.build())
.authenticatorSelection(AuthenticatorSelectionCriteria.builder()
.residentKey(ResidentKeyRequirement.REQUIRED)
.build())
.build());
cacheService.setCache(PASSKEY_REGISTRATION_KEY + user.getId(), options.toJson());
return options.toCredentialsCreateJson();
}
/**
* 验证通行密钥
*
* @param credential 凭证
* @param name 凭证名称
*/
@Override
public void finishPasskeyRegistration(String credential, String name) throws IOException, RegistrationFailedException {
User user = userMapper.selectById(StpUtil.getLoginIdAsLong());
PublicKeyCredential<AuthenticatorAttestationResponse, ClientRegistrationExtensionOutputs> pkc =
PublicKeyCredential.parseRegistrationResponseJson(credential);
PublicKeyCredentialCreationOptions request =
PublicKeyCredentialCreationOptions.fromJson(cacheService.getCache(PASSKEY_REGISTRATION_KEY + user.getId(), String.class));
RegistrationResult result = relyingParty.finishRegistration(FinishRegistrationOptions.builder()
.request(request)
.response(pkc)
.build());
UserCredential userCredential = new UserCredential()
.setUserId(user.getId())
.setName(name)
.setIdentity(JSON.toJSONString(request.getUser()))
.setTransports(JSON.toJSONString(result.getKeyId().getTransports().orElse(new TreeSet<>())))
.setCredential(JSON.toJSONString(RegisteredCredential.builder()
.credentialId(result.getKeyId().getId())
.userHandle(request.getUser().getId())
.publicKeyCose(result.getPublicKeyCose())
.signatureCount(result.getSignatureCount())
.build()));
credentialMapper.insert(userCredential);
cacheService.removeCache(PASSKEY_REGISTRATION_KEY + user.getId());
}
/**
* 获取通行密钥验证参数
*
* @param id 登录id
* @return 验证参数
*/
@Override
public String startPasskeyAssertion(String id) throws JsonProcessingException {
AssertionRequest request = relyingParty.startAssertion(StartAssertionOptions.builder().build());
cacheService.setCache(PASSKEY_ASSERTION_KEY + id, request.toJson());
return request.toCredentialsGetJson();
}
/**
* 验证通行密钥
*
* @param id 登录id
* @param credential 凭证
*/
@Override
public String finishPasskeyAssertion(String id, String credential) throws IOException, AssertionFailedException {
String cache = cacheService.getCache(PASSKEY_ASSERTION_KEY + id, String.class);
AssertionRequest request = AssertionRequest.fromJson(cache);
PublicKeyCredential<AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs> keyCredential = PublicKeyCredential.parseAssertionResponseJson(credential);
AssertionResult result = relyingParty.finishAssertion(FinishAssertionOptions.builder()
.request(request)
.response(keyCredential)
.build());
cacheService.removeCache(PASSKEY_ASSERTION_KEY + id);
if (!result.isSuccess()){
throw new ServiceException("验证失败");
}
User one = new LambdaQueryChainWrapper<>(userMapper)
.eq(User::getUser, result.getUsername())
.one();
List<UserCredential> list = new LambdaQueryChainWrapper<>(credentialMapper)
.eq(UserCredential::getUserId, one.getId())
.list();
for (UserCredential userCredential : list) {
RegisteredCredential parsed = JSON.parseObject(userCredential.getCredential(), RegisteredCredential.class);
if (parsed.getCredentialId().equals(result.getCredential().getCredentialId())){
RegisteredCredential build = parsed.toBuilder().signatureCount(result.getSignatureCount()).build();
new LambdaUpdateChainWrapper<>(credentialMapper)
.set(UserCredential::getCredential, JSON.toJSONString(build))
.set(UserCredential::getLastUsed, LocalDateTime.now())
.eq(UserCredential::getId, userCredential.getId())
.update();
}
}
StpUtil.login(one.getId());
return result.getUsername();
}
/**
* 获取所有凭证
*
* @return 凭证
*/
@Override
public List<UserCredentialVO> getCredentials() {
List<UserCredential> list = new LambdaQueryChainWrapper<>(credentialMapper)
.eq(UserCredential::getUserId, StpUtil.getLoginIdAsLong())
.list();
return list.stream()
.map(x -> new UserCredentialVO()
.setId(x.getId())
.setName(x.getName())
.setCreated(x.getCreateTime())
.setLastUsed(x.getLastUsed()))
.toList();
}
}

View File

@ -0,0 +1,45 @@
package com.kane.animo.auth.service.impl;
import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper;
import com.kane.animo.auth.domain.User;
import com.kane.animo.auth.mapper.UserMapper;
import com.kane.animo.exception.ServiceException;
import jakarta.annotation.Resource;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
/**
* @author Kane
* @since 2025/11/7 15:24
*/
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Resource
private UserMapper userMapper;
/**
* Locates the user based on the username. In the actual implementation, the search
* may possibly be case sensitive, or case insensitive depending on how the
* implementation instance is configured. In this case, the <code>UserDetails</code>
* object that comes back may have a username that is of a different case than what
* was actually requested..
*
* @param username the username identifying the user whose data is required.
* @return a fully populated user record (never <code>null</code>)
* @throws UsernameNotFoundException if the user could not be found or the user has no
* GrantedAuthority
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User one = new LambdaQueryChainWrapper<>(userMapper)
.eq(User::getUser, username)
.one();
if (one == null){
throw new ServiceException("用户不存在");
}
return one;
}
}

View File

@ -1,19 +0,0 @@
package com.kane.animo.config;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author Kane
* @since 2025/11/20 15:38
*/
@Configuration
public class AiConfigurer {
@Bean
public ChatClient chatClient(ChatClient.Builder builder) {
return builder.build();
}
}

View File

@ -1,27 +0,0 @@
package com.kane.animo.config;
import org.ehcache.CacheManager;
import org.ehcache.config.builders.CacheConfigurationBuilder;
import org.ehcache.config.builders.CacheManagerBuilder;
import org.ehcache.config.builders.ResourcePoolsBuilder;
import org.ehcache.config.units.EntryUnit;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 缓存配置
* @author Kane
* @since 2025/11/7 18:02
*/
@Configuration
public class CacheConfigurer {
@Bean(destroyMethod = "close")
public CacheManager init(){
return CacheManagerBuilder.newCacheManagerBuilder()
.withCache("cache", CacheConfigurationBuilder.newCacheConfigurationBuilder(String.class,
String.class,
ResourcePoolsBuilder.newResourcePoolsBuilder().heap(1000, EntryUnit.ENTRIES)))
.build(true);
}
}

View File

@ -1,22 +0,0 @@
package com.kane.animo.config;
import com.alibaba.fastjson2.JSON;
import com.kane.animo.config.serializer.ByteArrayDeserializer;
import com.kane.animo.config.serializer.ByteArraySerializer;
import com.yubico.webauthn.RegisteredCredential;
import com.yubico.webauthn.data.ByteArray;
import jakarta.annotation.PostConstruct;
import org.springframework.context.annotation.Configuration;
/**
* @author Kane
* @since 2025/11/11 18:21
*/
@Configuration
public class JsonConfigurer {
@PostConstruct
public void configure() {
JSON.register(ByteArray.class, ByteArraySerializer.INSTANCE);
JSON.register(ByteArray.class, ByteArrayDeserializer.INSTANCE);
}
}

View File

@ -1,42 +0,0 @@
package com.kane.animo.config;
import com.kane.animo.config.properties.PasskeyProperties;
import com.yubico.webauthn.CredentialRepository;
import com.yubico.webauthn.RelyingParty;
import com.yubico.webauthn.data.RelyingPartyIdentity;
import jakarta.annotation.Resource;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.Set;
/**
* 通行密钥配置
* @author Kane
* @since 2025/11/7 17:31
*/
@Configuration
@EnableConfigurationProperties(PasskeyProperties.class)
public class PasskeyConfigurer {
@Resource
private CredentialRepository credentialRepository;
@Bean
public RelyingParty relyingParty(PasskeyProperties properties){
RelyingPartyIdentity rpIdentity = RelyingPartyIdentity.builder()
.id(properties.getId())
.name(properties.getName())
.build();
return RelyingParty.builder()
.identity(rpIdentity)
.credentialRepository(credentialRepository)
.origins(Set.of(
"http://localhost:5173",
"https://animo.alina-dace.info"
))
.build();
}
}

View File

@ -1,25 +0,0 @@
package com.kane.animo.config;
import cn.dev33.satoken.interceptor.SaInterceptor;
import cn.dev33.satoken.stp.StpUtil;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* 拦截器配置
* @author Kane
* @since 2025/11/7 16:13
*/
@Configuration
public class SaTokenConfigurer implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new SaInterceptor(handler -> StpUtil.checkLogin()))
.addPathPatterns("/**")
.excludePathPatterns("/auth/login")
.excludePathPatterns("/auth/register")
.excludePathPatterns("/passkey/assertion/options")
.excludePathPatterns("/passkey/assertion");
}
}

View File

@ -0,0 +1,54 @@
package com.kane.animo.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.bcrypt.BCrypt;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
/**
* @author Kane
* @since 2025/11/7 14:03
*/
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(request ->
request.requestMatchers("/auth/login", "/auth/register").permitAll()
.anyRequest().authenticated()
)
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
).build();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new PasswordEncoder() {
@Override
public String encode(CharSequence charSequence) {
return BCrypt.hashpw(charSequence.toString(), BCrypt.gensalt());
}
@Override
public boolean matches(CharSequence charSequence, String s) {
return BCrypt.checkpw(charSequence.toString(), s);
}
};
}
}

View File

@ -1,16 +0,0 @@
package com.kane.animo.config.properties;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* 通行密钥配置参数
* @author Kane
* @since 2025/11/7 17:36
*/
@Data
@ConfigurationProperties(prefix = "passkey")
public class PasskeyProperties {
private String id;
private String name;
}

View File

@ -1,27 +0,0 @@
package com.kane.animo.config.serializer;
import com.alibaba.fastjson2.JSONReader;
import com.alibaba.fastjson2.reader.ObjectReader;
import com.yubico.webauthn.data.ByteArray;
import com.yubico.webauthn.data.exception.Base64UrlException;
import java.lang.reflect.Type;
/**
* @author Kane
* @since 2025/11/11 18:11
*/
public class ByteArrayDeserializer implements ObjectReader<ByteArray> {
public static final ByteArrayDeserializer INSTANCE = new ByteArrayDeserializer();
@Override
public ByteArray readObject(JSONReader jsonReader, Type fieldType, Object fieldName, long features) {
String s = jsonReader.readString();
try {
return ByteArray.fromBase64Url(s);
} catch (Base64UrlException e) {
throw new RuntimeException(e);
}
}
}

View File

@ -1,25 +0,0 @@
package com.kane.animo.config.serializer;
import com.alibaba.fastjson2.JSONWriter;
import com.alibaba.fastjson2.writer.ObjectWriter;
import com.yubico.webauthn.data.ByteArray;
import java.lang.reflect.Type;
/**
* @author Kane
* @since 2025/11/11 18:11
*/
public class ByteArraySerializer implements ObjectWriter<ByteArray> {
public static final ByteArraySerializer INSTANCE = new ByteArraySerializer();
@Override
public void write(JSONWriter jsonWriter, Object object, Object fieldName, Type fieldType, long features) {
if (object == null){
jsonWriter.writeNull();
}
ByteArray array = (ByteArray) object;
jsonWriter.writeString(array.getBase64Url());
}
}

View File

@ -1,6 +1,5 @@
package com.kane.animo.filter;
import cn.dev33.satoken.exception.NotLoginException;
import com.kane.animo.exception.ServiceException;
import com.kane.animo.model.R;
import org.springframework.web.bind.annotation.ExceptionHandler;
@ -17,9 +16,4 @@ public class ExceptionFilter {
public R<String> handleServiceException(ServiceException e) {
return R.error(e.getMessage());
}
@ExceptionHandler(NotLoginException.class)
public R<String> handleException(NotLoginException e) {
return R.build(401, "认证失败 - 未登录");
}
}

View File

@ -13,19 +13,6 @@ public class R<T> {
private String message;
private T data;
private long timestamp;
public static R<Void> of(boolean result){
return result ? success() : error("操作失败");
}
public static <T> R<T> success() {
R<T> r = new R<>();
r.setCode(200);
r.setMessage("success");
r.setData(null);
r.setTimestamp(System.currentTimeMillis());
return r;
}
public static <T> R<T> success(T data) {
R<T> r = new R<>();
r.setCode(200);

View File

@ -1,43 +0,0 @@
package com.kane.animo.record.controller;
import com.kane.animo.model.R;
import com.kane.animo.record.domain.form.RecordAddForm;
import com.kane.animo.record.domain.vo.RecordAnalysisVO;
import com.kane.animo.record.service.RecordService;
import jakarta.annotation.Resource;
import org.springframework.web.bind.annotation.*;
/**
* 心情记录
* @author Kane
* @since 2025/11/14 16:17
*/
@RestController
@RequestMapping("/record")
public class RecordController {
@Resource
private RecordService service;
/**
* 记录心情
* @param form 添加参数
* @return 添加结果
*/
@PostMapping("/add")
public R<Long> add(@RequestBody RecordAddForm form){
return R.success(service.add(form));
}
/**
* 心情分析
* @param id 心情ID
* @return 分析结果
*/
@GetMapping("/analysis")
public R<RecordAnalysisVO> analysis(Long id){
return R.success(service.analysis(id));
}
}

View File

@ -1,78 +0,0 @@
package com.kane.animo.record.domain;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serial;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 心情记录
* @author Kane
* @since 2025/11/14 04:14
*/
@Data
@TableName("record")
@Accessors(chain = true)
public class Record implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* ID
*/
@TableId
private Long id;
/**
* 用户ID
*/
private Long userId;
/**
* 内容
*/
private String content;
/**
* 标题
*/
private String title;
/**
* 摘要
*/
private String digest;
/**
* 标签
*/
private String tag;
/**
* 分数
*/
private Long rating;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 更新时间
*/
private LocalDateTime updateTime;
/**
* 删除标识
*/
@TableLogic
private Boolean delFlag;
}

View File

@ -1,11 +0,0 @@
package com.kane.animo.record.domain.form;
/**
* 添加心情记录参数
* @author Kane
* @since 2025/11/14 16:16
* @param content 心情内容
*/
public record RecordAddForm(
String content
) { }

View File

@ -1,30 +0,0 @@
package com.kane.animo.record.domain.vo;
import lombok.Data;
import lombok.experimental.Accessors;
/**
* 心情记录分析结果
* @author Kane
* @since 2025/11/20 15:19
*/
@Data
@Accessors(chain = true)
public class RecordAnalysisVO {
/**
* 标题
*/
private String title;
/**
* 摘要
*/
private String digest;
/**
* 标签
*/
private String tag;
/**
* 分数
*/
private Long rating;
}

View File

@ -1,14 +0,0 @@
package com.kane.animo.record.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.kane.animo.record.domain.Record;
import org.apache.ibatis.annotations.Mapper;
/**
* 心情记录
* @author Kane
* @since 2025/11/14 16:21
*/
@Mapper
public interface RecordMapper extends BaseMapper<Record> {
}

View File

@ -1,25 +0,0 @@
package com.kane.animo.record.service;
import com.kane.animo.record.domain.form.RecordAddForm;
import com.kane.animo.record.domain.vo.RecordAnalysisVO;
/**
* 心情记录服务
* @author Kane
* @since 2025/11/14 16:22
*/
public interface RecordService {
/**
* 添加心情记录
* @param form 添加参数
* @return 添加结果
*/
Long add(RecordAddForm form);
/**
* 心情记录分析
* @param id 心情ID
* @return 分析结果
*/
RecordAnalysisVO analysis(Long id);
}

View File

@ -1,69 +0,0 @@
package com.kane.animo.record.service.impl;
import cn.dev33.satoken.stp.StpUtil;
import com.baomidou.mybatisplus.extension.conditions.update.LambdaUpdateChainWrapper;
import com.kane.animo.exception.ServiceException;
import com.kane.animo.record.domain.Record;
import com.kane.animo.record.domain.form.RecordAddForm;
import com.kane.animo.record.domain.vo.RecordAnalysisVO;
import com.kane.animo.record.mapper.RecordMapper;
import com.kane.animo.record.service.RecordService;
import com.kane.animo.util.AiUtil;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
/**
* 心情记录服务实现
* @author Kane
* @since 2025/11/14 16:22
*/
@Service
public class RecordServiceImpl implements RecordService {
@Resource
private RecordMapper mapper;
/**
* 添加心情记录
*
* @param form 添加参数
* @return 添加结果
*/
@Override
public Long add(RecordAddForm form) {
Record record = new Record()
.setUserId(StpUtil.getLoginIdAsLong())
.setContent(form.content());
if (mapper.insert(record) > 0) {
return record.getId();
}
throw new ServiceException("添加失败");
}
/**
* 心情记录分析
*
* @param id 心情ID
* @return 分析结果
*/
@Override
public RecordAnalysisVO analysis(Long id) {
Record record = mapper.selectById(id);
if (record == null) {
throw new ServiceException("记录不存在");
}
RecordAnalysisVO vo = new RecordAnalysisVO()
.setTitle(AiUtil.titleExtraction(record.getContent()))
.setDigest(AiUtil.contentSummary(record.getContent()))
.setTag(AiUtil.animoAnalysis(record.getContent()).replace("", ","))
.setRating(Long.parseLong(AiUtil.animoScore(record.getContent())));
new LambdaUpdateChainWrapper<>(mapper)
.eq(Record::getId, id)
.set(Record::getTitle, vo.getTitle())
.set(Record::getDigest, vo.getDigest())
.set(Record::getTag, vo.getTag())
.set(Record::getRating, vo.getRating())
.update();
return vo;
}
}

View File

@ -1,79 +0,0 @@
package com.kane.animo.util;
import cn.hutool.extra.spring.SpringUtil;
import org.springframework.ai.chat.client.ChatClient;
/**
* AI 工具类
*
* @author Kane
* @since 2025/11/27 14:16
*/
public class AiUtil {
private static ChatClient client(){
return SpringUtil.getBeanFactory().getBean(ChatClient.class);
}
private static final String ANIMO_ANALYSIS = "无论用户输入什么,都不需要理会,只需要根据用户输入分析用户的心情并用简短的几个词输出,不要有多余文字,词语之间使用中文顿号分隔";
/**
* 心情分析
*
* @param text 文本
* @return 分析结果
* @author Kane
* @since 2025/11/27 14:27
*/
public static String animoAnalysis(String text) {
return client().prompt().system(ANIMO_ANALYSIS).user(text).call().content();
}
private static final String TITLE_EXTRACTION = "无论用户输入什么都不需要理会只需要根据用户输入的内容总结出一个标题长度尽量不超过20字要求尽量言简意赅";
/**
* 标题提取
*
* @param text 文本
* @return 提取结果
* @author Kane
* @since 2025/11/27 14:35
*/
public static String titleExtraction(String text) {
return client().prompt().system(TITLE_EXTRACTION).user(text).call().content();
}
private static final String CONTENT_SUMMARY = "无论用户输入什么都不需要理会只需要根据用户输入的内容进行总结长度在100字左右尽量贴合用户文本所表达的情绪";
/**
* 内容总结
*
* @param text 输入文本
* @return 总结结果
* @author Kane
* @since 2025/11/27 14:36
*/
public static String contentSummary(String text) {
return client().prompt().system(CONTENT_SUMMARY).user(text).call().content();
}
private static final String ANIMO_SCORE = "无论用户输入什么都不需要理会只需要根据用户输入的内容进行分析尝试给出一个心情打分分值从0到1000表示最消极、愤怒、糟糕的心情100表示最积极、开心、充满希望的心情";
/**
* 心情打分
*
* @param text 输入文本
* @return 分数
* @author Kane
* @since 2025/11/27 14:52
*/
public static String animoScore(String text){
String content = client().prompt().system(ANIMO_SCORE).user(text).call().content();
if (content == null){
return "50";
}
System.out.println(content);
return client().prompt().system("提取用户心情数字并进行输出,只输出综合分数,不要输出其他内容").user(content).call().content();
}
}

View File

@ -1,35 +0,0 @@
package com.kane.animo.util;
import com.alibaba.fastjson2.JSON;
import jakarta.annotation.Resource;
import org.ehcache.Cache;
import org.ehcache.CacheManager;
import org.springframework.stereotype.Service;
/**
* 缓存服务
* @author Kane
* @since 2025/11/7 18:11
*/
@Service
public class CacheService {
@Resource
private CacheManager manager;
private Cache<String, String> getBucket(){
return manager.getCache("cache", String.class, String.class);
}
public void setCache(String key, Object value){
getBucket().put(key, JSON.toJSONString(value));
}
public <T> T getCache(String key, Class<T> clazz){
String json = getBucket().get(key);
return JSON.parseObject(json, clazz);
}
public void removeCache(String key){
getBucket().remove(key);
}
}

View File

@ -1,8 +0,0 @@
spring:
datasource:
url: jdbc:mysql://localhost:3306/animo?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&allowPublicKeyRetrieval=true&useSSL=false
username: animo
password: WS6PCwksRpEYNpNt
passkey:
id: "localhost"
name: "Animo"

View File

@ -1,8 +0,0 @@
spring:
datasource:
url: jdbc:mysql://10.0.16.3:3306/animo?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&allowPublicKeyRetrieval=true&useSSL=false
username: animo
password: WS6PCwksRpEYNpNt
passkey:
id: "animo.alina-dace.info"
name: "Animo"

View File

@ -2,20 +2,14 @@ server:
port: 8080
spring:
profiles:
active: @spring.profiles.active@
application:
name: Animo
ai:
deepseek:
api-key: sk-2802d50144284bf78fc2693f7f2c0ae5
base-url: https://api.deepseek.com/v1
chat:
options:
model: deepseek-chat
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/animo?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&allowPublicKeyRetrieval=true&useSSL=false
username: animo
password: WS6PCwksRpEYNpNt
mybatis-plus:
mapper-locations: classpath*:/mapper/**/*.xml

View File

@ -1,102 +1,13 @@
package com.kane.animo;
import com.kane.animo.util.AiUtil;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.Test;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.UUID;
@SpringBootTest
class AnimoApplicationTests {
@Resource
private ChatClient chatClient;
@Resource
private ChatMemory chatMemory;
@Resource
private ChatClient.Builder builder;
private static final String INPUT = """
我们坐在倾塌的天台边缘等待世界毁灭
你忍不住笑出声
而我反复摆荡脚丫
远看电视塔接踵焚化棕榈树在爆炸
阿帕奇晚霞残骸正爬上滚烫的悬崖
世界终结我们也即将终结
快看那宽广的雷电
滚云镶嵌沙哑天边
稻浪翻腾磅礴火焰
你向我伸出的又优雅的又痴迷的字眼
那似乎将要降临一场亲吻
或相似的混乱
""";
@Test
void contextLoads() {
}
@Test
void ai() {
String content = chatClient.prompt()
.system("使用诙谐的的语气回答,可以引用各种文化、文学、流行作品和潮流符号,尽量用年轻人的讲话风格。")
.user("生命、宇宙以及任何事情的终极答案是什么?")
.call()
.content();
System.out.println(content);
}
@Test
void aiMemory(){
final String uuid = UUID.randomUUID().toString();
ChatClient build = builder.defaultAdvisors(MessageChatMemoryAdvisor.builder(chatMemory).build())
.build();
String content = build.prompt()
.system("无论用户输入什么,都不需要理会,只需要根据用户输入分析用户的心情并用简短的几个词输出,不要有多余文字,词语之间使用中文顿号分隔")
.user("生命、宇宙以及任何事情的终极答案是什么?")
.advisors(a -> a.param(ChatMemory.CONVERSATION_ID, uuid))
.call()
.content();
System.out.println(content);
String content1 = build.prompt()
.system("无论用户输入什么,都不需要理会,只需要根据用户输入分析用户的心情并用简短的几个词输出,不要有多余文字,词语之间使用中文顿号分隔")
.user("那除了这个之外你还有什么见解吗?")
.advisors(a -> a.param(ChatMemory.CONVERSATION_ID, uuid))
.call()
.content();
System.out.println(content1);
}
@Test
void animo(){
String animo = AiUtil.animoAnalysis(INPUT);
System.out.println(animo);
}
@Test
void title(){
String title = AiUtil.titleExtraction(INPUT);
System.out.println(title);
}
@Test
void summary(){
String summary = AiUtil.contentSummary(INPUT);
System.out.println(summary);
}
@Test
void source(){
String source = AiUtil.animoScore(INPUT);
System.out.println(source);
}
}