Merge branch 'develop' of https://gitee.com/youlaiorg/youlai-boot into develop
# Conflicts: # sql/mysql/youlai_admin.sql
This commit is contained in:
@@ -1,11 +1,12 @@
|
||||
package com.youlai.boot.auth.controller;
|
||||
|
||||
import com.youlai.boot.auth.model.vo.CaptchaVO;
|
||||
import com.youlai.boot.auth.model.dto.WxMiniAppPhoneLoginDTO;
|
||||
import com.youlai.boot.auth.model.dto.WxMiniAppCodeLoginDto;
|
||||
import com.youlai.boot.auth.model.dto.WxMiniAppPhoneLoginDto;
|
||||
import com.youlai.boot.auth.model.vo.CaptchaVo;
|
||||
import com.youlai.boot.auth.model.dto.LoginRequest;
|
||||
import com.youlai.boot.common.enums.LogModuleEnum;
|
||||
import com.youlai.boot.core.web.Result;
|
||||
import com.youlai.boot.auth.service.AuthService;
|
||||
import com.youlai.boot.auth.model.dto.WxMiniAppCodeLoginDTO;
|
||||
import com.youlai.boot.common.annotation.Log;
|
||||
import com.youlai.boot.security.model.AuthenticationToken;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
@@ -16,12 +17,11 @@ import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
|
||||
/**
|
||||
* 认证控制层
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 2022/10/16
|
||||
* @since 0.0.1
|
||||
*/
|
||||
@Tag(name = "01.认证中心")
|
||||
@RestController
|
||||
@@ -34,18 +34,17 @@ public class AuthController {
|
||||
|
||||
@Operation(summary = "获取验证码")
|
||||
@GetMapping("/captcha")
|
||||
public Result<CaptchaVO> getCaptcha() {
|
||||
CaptchaVO captcha = authService.getCaptcha();
|
||||
public Result<CaptchaVo> getCaptcha() {
|
||||
CaptchaVo captcha = authService.getCaptcha();
|
||||
return Result.success(captcha);
|
||||
}
|
||||
|
||||
@Operation(summary = "账号密码登录")
|
||||
@PostMapping("/login")
|
||||
@Log(value = "登录", module = LogModuleEnum.LOGIN)
|
||||
public Result<AuthenticationToken> login(
|
||||
@Parameter(description = "用户名", example = "admin") @RequestParam String username,
|
||||
@Parameter(description = "密码", example = "123456") @RequestParam String password
|
||||
) {
|
||||
public Result<?> login(@RequestBody @Valid LoginRequest request) {
|
||||
String username = request.getUsername();
|
||||
String password = request.getPassword();
|
||||
AuthenticationToken authenticationToken = authService.login(username, password);
|
||||
return Result.success(authenticationToken);
|
||||
}
|
||||
@@ -82,19 +81,18 @@ public class AuthController {
|
||||
|
||||
@Operation(summary = "微信小程序登录(Code)")
|
||||
@PostMapping("/wx/miniapp/code-login")
|
||||
public Result<AuthenticationToken> loginByWxMiniAppCode(@RequestBody @Valid WxMiniAppCodeLoginDTO loginDTO) {
|
||||
AuthenticationToken token = authService.loginByWxMiniAppCode(loginDTO);
|
||||
public Result<AuthenticationToken> loginByWxMiniAppCode(@RequestBody @Valid WxMiniAppCodeLoginDto loginDto) {
|
||||
AuthenticationToken token = authService.loginByWxMiniAppCode(loginDto);
|
||||
return Result.success(token);
|
||||
}
|
||||
|
||||
@Operation(summary = "微信小程序登录(手机号)")
|
||||
@PostMapping("/wx/miniapp/phone-login")
|
||||
public Result<AuthenticationToken> loginByWxMiniAppPhone(@RequestBody @Valid WxMiniAppPhoneLoginDTO loginDTO) {
|
||||
AuthenticationToken token = authService.loginByWxMiniAppPhone(loginDTO);
|
||||
public Result<AuthenticationToken> loginByWxMiniAppPhone(@RequestBody @Valid WxMiniAppPhoneLoginDto loginDto) {
|
||||
AuthenticationToken token = authService.loginByWxMiniAppPhone(loginDto);
|
||||
return Result.success(token);
|
||||
}
|
||||
|
||||
|
||||
@Operation(summary = "退出登录")
|
||||
@DeleteMapping("/logout")
|
||||
@Log(value = "退出登录", module = LogModuleEnum.LOGIN)
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.youlai.boot.auth.model.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
|
||||
/**
|
||||
* 登录请求参数
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 3.0.0
|
||||
*/
|
||||
@Schema(description = "登录请求参数")
|
||||
@Data
|
||||
public class LoginRequest {
|
||||
|
||||
@Schema(description = "用户名", requiredMode = Schema.RequiredMode.REQUIRED, example = "admin")
|
||||
@NotBlank(message = "用户名不能为空")
|
||||
private String username;
|
||||
|
||||
@Schema(description = "密码", requiredMode = Schema.RequiredMode.REQUIRED, example = "123456")
|
||||
@NotBlank(message = "密码不能为空")
|
||||
private String password;
|
||||
|
||||
@Schema(description = "验证码缓存ID", example = "captcha_id_123")
|
||||
private String captchaId;
|
||||
|
||||
@Schema(description = "验证码", example = "1234")
|
||||
private String captchaCode;
|
||||
}
|
||||
|
||||
@@ -6,17 +6,16 @@ import lombok.Data;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
|
||||
/**
|
||||
* 微信小程序Code登录请求参数
|
||||
*
|
||||
* @author 有来技术团队
|
||||
* @since 2.0.0
|
||||
*微信小程序Code登录请求参数
|
||||
*/
|
||||
@Schema(description = "微信小程序Code登录请求参数")
|
||||
@Data
|
||||
public class WxMiniAppCodeLoginDTO {
|
||||
public class WxMiniAppCodeLoginDto {
|
||||
|
||||
@Schema(description = "微信小程序登录时获取的code", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@NotBlank(message = "code不能为空")
|
||||
private String code;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -7,13 +7,10 @@ import jakarta.validation.constraints.NotBlank;
|
||||
|
||||
/**
|
||||
* 微信小程序手机号登录请求参数
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Schema(description = "微信小程序手机号登录请求参数")
|
||||
@Data
|
||||
public class WxMiniAppPhoneLoginDTO {
|
||||
public class WxMiniAppPhoneLoginDto {
|
||||
|
||||
@Schema(description = "微信小程序登录时获取的code", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@NotBlank(message = "code不能为空")
|
||||
@@ -25,4 +22,6 @@ public class WxMiniAppPhoneLoginDTO {
|
||||
@Schema(description = "加密算法的初始向量")
|
||||
private String iv;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -13,10 +13,10 @@ import lombok.Data;
|
||||
@Schema(description = "验证码信息")
|
||||
@Data
|
||||
@Builder
|
||||
public class CaptchaVO {
|
||||
public class CaptchaVo {
|
||||
|
||||
@Schema(description = "验证码缓存 Key")
|
||||
private String captchaKey;
|
||||
@Schema(description = "验证码缓存 ID")
|
||||
private String captchaId;
|
||||
|
||||
@Schema(description = "验证码图片Base64字符串")
|
||||
private String captchaBase64;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package com.youlai.boot.auth.service;
|
||||
|
||||
import com.youlai.boot.auth.model.vo.CaptchaVO;
|
||||
import com.youlai.boot.auth.model.dto.WxMiniAppPhoneLoginDTO;
|
||||
import com.youlai.boot.auth.model.vo.CaptchaVo;
|
||||
import com.youlai.boot.auth.model.dto.WxMiniAppPhoneLoginDto;
|
||||
import com.youlai.boot.security.model.AuthenticationToken;
|
||||
import com.youlai.boot.auth.model.dto.WxMiniAppCodeLoginDTO;
|
||||
import com.youlai.boot.auth.model.dto.WxMiniAppCodeLoginDto;
|
||||
|
||||
/**
|
||||
* 认证服务接口
|
||||
@@ -32,7 +32,7 @@ public interface AuthService {
|
||||
*
|
||||
* @return 验证码
|
||||
*/
|
||||
CaptchaVO getCaptcha();
|
||||
CaptchaVo getCaptcha();
|
||||
|
||||
/**
|
||||
* 刷新令牌
|
||||
@@ -53,18 +53,18 @@ public interface AuthService {
|
||||
/**
|
||||
* 微信小程序Code登录
|
||||
*
|
||||
* @param loginDTO 登录参数
|
||||
* @param loginDto 登录参数
|
||||
* @return 访问令牌
|
||||
*/
|
||||
AuthenticationToken loginByWxMiniAppCode(WxMiniAppCodeLoginDTO loginDTO);
|
||||
AuthenticationToken loginByWxMiniAppCode(WxMiniAppCodeLoginDto loginDto);
|
||||
|
||||
/**
|
||||
* 微信小程序手机号登录
|
||||
*
|
||||
* @param loginDTO 登录参数
|
||||
* @param loginDto 登录参数
|
||||
* @return 访问令牌
|
||||
*/
|
||||
AuthenticationToken loginByWxMiniAppPhone(WxMiniAppPhoneLoginDTO loginDTO);
|
||||
AuthenticationToken loginByWxMiniAppPhone(WxMiniAppPhoneLoginDto loginDto);
|
||||
|
||||
/**
|
||||
* 发送短信验证码
|
||||
|
||||
@@ -5,9 +5,9 @@ import cn.hutool.captcha.CaptchaUtil;
|
||||
import cn.hutool.captcha.generator.CodeGenerator;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.youlai.boot.auth.model.dto.WxMiniAppCodeLoginDTO;
|
||||
import com.youlai.boot.auth.model.dto.WxMiniAppPhoneLoginDTO;
|
||||
import com.youlai.boot.auth.model.vo.CaptchaVO;
|
||||
import com.youlai.boot.auth.model.dto.WxMiniAppCodeLoginDto;
|
||||
import com.youlai.boot.auth.model.dto.WxMiniAppPhoneLoginDto;
|
||||
import com.youlai.boot.auth.model.vo.CaptchaVo;
|
||||
import com.youlai.boot.auth.service.AuthService;
|
||||
import com.youlai.boot.common.constant.RedisConstants;
|
||||
import com.youlai.boot.common.constant.SecurityConstants;
|
||||
@@ -70,6 +70,11 @@ public class AuthServiceImpl implements AuthService {
|
||||
new UsernamePasswordAuthenticationToken(username.trim(), password);
|
||||
|
||||
// 2. 执行认证(认证中)
|
||||
// 说明:这里的认证流程由 Spring Security 提供的 AuthenticationManager 执行。
|
||||
// 默认情况下会委托给 DaoAuthenticationProvider:
|
||||
// 1) retrieveUser(...):内部通过 UserDetailsService.loadUserByUsername(...) 获取用户信息(本项目为 SysUserDetailsService 实现)
|
||||
// 2) additionalAuthenticationChecks(...):对比请求密码与用户存储密码(由 PasswordEncoder 完成匹配)
|
||||
// 认证通过后返回已认证的 Authentication(principal 为 SysUserDetails,authorities 为角色/权限集合)。
|
||||
Authentication authentication = authenticationManager.authenticate(authenticationToken);
|
||||
|
||||
// 3. 认证成功后生成 JWT 令牌,并存入 Security 上下文,供登录日志 AOP 使用(已认证)
|
||||
@@ -152,10 +157,8 @@ public class AuthServiceImpl implements AuthService {
|
||||
*/
|
||||
@Override
|
||||
public void logout() {
|
||||
String token = SecurityUtils.getTokenFromRequest();
|
||||
if (StrUtil.isNotBlank(token) && token.startsWith(SecurityConstants.BEARER_TOKEN_PREFIX )) {
|
||||
token = token.substring(SecurityConstants.BEARER_TOKEN_PREFIX .length());
|
||||
// 将JWT令牌加入黑名单
|
||||
String token = SecurityUtils.getAccessToken();
|
||||
if (StrUtil.isNotBlank(token)) {
|
||||
tokenManager.invalidateToken(token);
|
||||
// 清除Security上下文
|
||||
SecurityContextHolder.clearContext();
|
||||
@@ -168,7 +171,7 @@ public class AuthServiceImpl implements AuthService {
|
||||
* @return 验证码
|
||||
*/
|
||||
@Override
|
||||
public CaptchaVO getCaptcha() {
|
||||
public CaptchaVo getCaptcha() {
|
||||
|
||||
String captchaType = captchaProperties.getType();
|
||||
int width = captchaProperties.getWidth();
|
||||
@@ -196,16 +199,16 @@ public class AuthServiceImpl implements AuthService {
|
||||
String imageBase64Data = captcha.getImageBase64Data();
|
||||
|
||||
// 验证码文本缓存至Redis,用于登录校验
|
||||
String captchaKey = IdUtil.fastSimpleUUID();
|
||||
String captchaId = IdUtil.fastSimpleUUID();
|
||||
redisTemplate.opsForValue().set(
|
||||
StrUtil.format(RedisConstants.Captcha.IMAGE_CODE, captchaKey),
|
||||
StrUtil.format(RedisConstants.Captcha.IMAGE_CODE, captchaId),
|
||||
captchaCode,
|
||||
captchaProperties.getExpireSeconds(),
|
||||
TimeUnit.SECONDS
|
||||
);
|
||||
|
||||
return CaptchaVO.builder()
|
||||
.captchaKey(captchaKey)
|
||||
return CaptchaVo.builder()
|
||||
.captchaId(captchaId)
|
||||
.captchaBase64(imageBase64Data)
|
||||
.build();
|
||||
}
|
||||
@@ -224,13 +227,13 @@ public class AuthServiceImpl implements AuthService {
|
||||
/**
|
||||
* 微信小程序Code登录
|
||||
*
|
||||
* @param loginDTO 登录参数
|
||||
* @param loginDto 登录参数
|
||||
* @return 访问令牌
|
||||
*/
|
||||
@Override
|
||||
public AuthenticationToken loginByWxMiniAppCode(WxMiniAppCodeLoginDTO loginDTO) {
|
||||
public AuthenticationToken loginByWxMiniAppCode(WxMiniAppCodeLoginDto loginDto) {
|
||||
// 1. 创建微信小程序认证令牌(未认证)
|
||||
WxMiniAppCodeAuthenticationToken authenticationToken = new WxMiniAppCodeAuthenticationToken(loginDTO.getCode());
|
||||
WxMiniAppCodeAuthenticationToken authenticationToken = new WxMiniAppCodeAuthenticationToken(loginDto.getCode());
|
||||
|
||||
// 2. 执行认证(认证中)
|
||||
Authentication authentication = authenticationManager.authenticate(authenticationToken);
|
||||
@@ -245,16 +248,16 @@ public class AuthServiceImpl implements AuthService {
|
||||
/**
|
||||
* 微信小程序手机号登录
|
||||
*
|
||||
* @param loginDTO 登录参数
|
||||
* @param loginDto 登录参数
|
||||
* @return 访问令牌
|
||||
*/
|
||||
@Override
|
||||
public AuthenticationToken loginByWxMiniAppPhone(WxMiniAppPhoneLoginDTO loginDTO) {
|
||||
public AuthenticationToken loginByWxMiniAppPhone(WxMiniAppPhoneLoginDto loginDto) {
|
||||
// 创建微信小程序手机号认证Token
|
||||
WxMiniAppPhoneAuthenticationToken authenticationToken = new WxMiniAppPhoneAuthenticationToken(
|
||||
loginDTO.getCode(),
|
||||
loginDTO.getEncryptedData(),
|
||||
loginDTO.getIv()
|
||||
loginDto.getCode(),
|
||||
loginDto.getEncryptedData(),
|
||||
loginDto.getIv()
|
||||
);
|
||||
|
||||
// 执行认证
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
package com.youlai.boot.common.base;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.ToString;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 视图对象基类
|
||||
*
|
||||
* @author haoxr
|
||||
* @since 2022/10/22
|
||||
*/
|
||||
@Data
|
||||
@ToString
|
||||
public class BaseVO implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
}
|
||||
@@ -7,10 +7,16 @@ import com.baomidou.mybatisplus.extension.plugins.inner.DataPermissionIntercepto
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
|
||||
import com.youlai.boot.plugin.mybatis.MyDataPermissionHandler;
|
||||
import com.youlai.boot.plugin.mybatis.MyMetaObjectHandler;
|
||||
import org.apache.ibatis.mapping.DatabaseIdProvider;
|
||||
import org.apache.ibatis.mapping.VendorDatabaseIdProvider;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.transaction.annotation.EnableTransactionManagement;
|
||||
|
||||
import java.util.Properties;
|
||||
|
||||
/**
|
||||
* mybatis-plus 配置类
|
||||
*
|
||||
@@ -21,16 +27,29 @@ import org.springframework.transaction.annotation.EnableTransactionManagement;
|
||||
@EnableTransactionManagement
|
||||
public class MybatisConfig {
|
||||
|
||||
@Value("${app.db-type:mysql}")
|
||||
private String dbType;
|
||||
|
||||
/**
|
||||
* 分页插件和数据权限插件
|
||||
*/
|
||||
@Bean
|
||||
public MybatisPlusInterceptor mybatisPlusInterceptor() {
|
||||
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
|
||||
//数据权限
|
||||
|
||||
// 数据权限
|
||||
interceptor.addInnerInterceptor(new DataPermissionInterceptor(new MyDataPermissionHandler()));
|
||||
//分页插件
|
||||
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
|
||||
|
||||
// 分页插件,根据配置动态选择数据库类型
|
||||
DbType mpDbType = DbType.MYSQL;
|
||||
String type = dbType == null ? "mysql" : dbType.toLowerCase();
|
||||
if ("postgres".equals(type) || "postgresql".equals(type)) {
|
||||
mpDbType = DbType.POSTGRE_SQL;
|
||||
} else if ("dm".equals(type) || "dameng".equals(type)) {
|
||||
// 达梦更接近 Oracle 语法,这里选择 ORACLE 方言以获得较好兼容性
|
||||
mpDbType = DbType.ORACLE;
|
||||
}
|
||||
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(mpDbType));
|
||||
|
||||
return interceptor;
|
||||
}
|
||||
@@ -45,4 +64,17 @@ public class MybatisConfig {
|
||||
return globalConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据库类型自动识别
|
||||
*/
|
||||
@Bean
|
||||
public DatabaseIdProvider databaseIdProvider() {
|
||||
DatabaseIdProvider databaseIdProvider = new VendorDatabaseIdProvider();
|
||||
Properties properties = new Properties();
|
||||
properties.setProperty("DM", "dm");
|
||||
properties.setProperty("MySQL", "mysql");
|
||||
databaseIdProvider.setProperties(properties);
|
||||
return databaseIdProvider;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ public class RepeatSubmitAspect {
|
||||
|
||||
boolean locked = lock.tryLock(0, expire, TimeUnit.SECONDS);
|
||||
if (!locked) {
|
||||
throw new BusinessException(ResultCode.USER_DUPLICATE_REQUEST);
|
||||
throw new BusinessException(ResultCode.DUPLICATE_SUBMISSION);
|
||||
}
|
||||
return pjp.proceed();
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import com.youlai.boot.common.constant.RedisConstants;
|
||||
import com.youlai.boot.common.constant.SystemConstants;
|
||||
import com.youlai.boot.core.web.ResultCode;
|
||||
import com.youlai.boot.common.util.IPUtils;
|
||||
import com.youlai.boot.core.web.WebResponseHelper;
|
||||
import com.youlai.boot.core.web.WebResponseWriter;
|
||||
import com.youlai.boot.system.service.ConfigService;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
@@ -88,7 +88,7 @@ public class RateLimiterFilter extends OncePerRequestFilter {
|
||||
// 判断是否限流
|
||||
if (rateLimit(ip)) {
|
||||
// 返回限流错误信息
|
||||
WebResponseHelper.writeError(response, ResultCode.REQUEST_CONCURRENCY_LIMIT_EXCEEDED);
|
||||
WebResponseWriter.writeError(response, ResultCode.REQUEST_CONCURRENCY_LIMIT_EXCEEDED);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -56,6 +56,14 @@ public class Result<T> implements Serializable {
|
||||
return result(resultCode.getCode(), StrUtil.isNotBlank(msg) ? msg : resultCode.getMsg(), null);
|
||||
}
|
||||
|
||||
public static <T> Result<T> failed(IResultCode resultCode, T data) {
|
||||
return result(resultCode.getCode(), resultCode.getMsg(), data);
|
||||
}
|
||||
|
||||
public static <T> Result<T> failed(IResultCode resultCode, String msg, T data) {
|
||||
return result(resultCode.getCode(), StrUtil.isNotBlank(msg) ? msg : resultCode.getMsg(), data);
|
||||
}
|
||||
|
||||
private static <T> Result<T> result(IResultCode resultCode, T data) {
|
||||
return result(resultCode.getCode(), resultCode.getMsg(), data);
|
||||
}
|
||||
|
||||
@@ -1,269 +1,128 @@
|
||||
package com.youlai.boot.core.web;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 响应码枚举
|
||||
* <p>
|
||||
* 参考阿里巴巴开发手册响应码规范
|
||||
* 00000 正常
|
||||
* A**** 用户端错误
|
||||
* B**** 系统执行出错
|
||||
* C**** 调用第三方服务出错
|
||||
* 参考《阿里巴巴 Java 开发手册》错误码设计建议:
|
||||
* 00000 表示成功。
|
||||
* A**** 表示用户端错误(如参数错误、认证失败等)。
|
||||
* B**** 表示当前系统执行出错(如系统超时等)。
|
||||
* C**** 表示调用第三方服务出错(如中间件、数据库等外部依赖)。
|
||||
* <p>
|
||||
* 错误码位数与号段说明:
|
||||
* - 错误码为字符串类型,共 5 位:错误产生来源(A/B/C) + 四位数字编号。
|
||||
* - 四位数字编号范围 0001~9999,大类之间建议按步长 100 预留号段(如 A0200、A0300、A0400)。
|
||||
* - 错误码后三位编号与 HTTP 状态码无关。
|
||||
* <p>
|
||||
* 说明:
|
||||
* - 本项目仅保留实际使用的错误码,并在 A/B/C 各保留少量示例,避免枚举无限膨胀。
|
||||
* - 如需扩展业务错误码,建议在对应宏观分类下按场景划分号段并保持全局唯一。
|
||||
* <p>
|
||||
* 附表(节选):错误码列表(示例/项目使用项)
|
||||
* <pre>
|
||||
* | 错误码 | 中文描述 | 说明 |
|
||||
* |-------|----------------------|------------------|
|
||||
* | 00000 | 成功 | 正常执行后的返回 |
|
||||
* | A0001 | 用户端错误 | 一级宏观错误码 |
|
||||
* | A0100 | 用户注册错误 | 二级宏观错误码 |
|
||||
* | A0101 | 用户未同意隐私协议 | 二级宏观错误码 |
|
||||
* | A0200 | 用户登录异常 | 二级宏观错误码 |
|
||||
* | A0201 | 用户账户不存在 | 二级宏观错误码 |
|
||||
* | A0202 | 用户账户被冻结 | 二级宏观错误码 |
|
||||
* | A0230 | 访问令牌无效或已过期 | 令牌校验失败 |
|
||||
* | A0241 | 用户验证码尝试次数超限 | 二级宏观错误码 |
|
||||
* | A0300 | 访问权限异常 | 二级宏观错误码 |
|
||||
* | A0301 | 访问未授权 | 二级宏观错误码 |
|
||||
* | A0400 | 用户请求参数错误 | 二级宏观错误码 |
|
||||
* | A0410 | 请求必填参数为空 | 二级宏观错误码 |
|
||||
* | A0500 | 用户请求服务异常 | 二级宏观错误码 |
|
||||
* | A0502 | 请求并发数超出限制 | 二级宏观错误码 |
|
||||
* | A0506 | 请勿重复提交 | 二级宏观错误码 |
|
||||
* | B0001 | 系统执行出错 | 一级宏观错误码 |
|
||||
* | B0100 | 系统执行超时 | 二级宏观错误码 |
|
||||
* | C0001 | 调用第三方服务出错 | 一级宏观错误码 |
|
||||
* | C0113 | 接口不存在 | 二级宏观错误码 |
|
||||
* | C0300 | 数据库服务出错 | 二级宏观错误码 |
|
||||
* </pre>
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 2020/6/23
|
||||
**/
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public enum ResultCode implements IResultCode, Serializable {
|
||||
|
||||
SUCCESS("00000", "一切ok"),
|
||||
|
||||
/** 一级宏观错误码 */
|
||||
SUCCESS("00000", "成功"),
|
||||
|
||||
/** 一级宏观错误码:用户端错误(由客户端输入/认证/权限/请求方式等引起,需客户端配合修正) */
|
||||
USER_ERROR("A0001", "用户端错误"),
|
||||
|
||||
|
||||
/** 二级宏观错误码 */
|
||||
/** 二级宏观错误码:用户端具体错误(按号段细分,便于定位是注册/登录/令牌/参数/防重等问题) */
|
||||
|
||||
/** A01xx:用户注册错误 */
|
||||
USER_REGISTRATION_ERROR("A0100", "用户注册错误"),
|
||||
USER_NOT_AGREE_PRIVACY_AGREEMENT("A0101", "用户未同意隐私协议"),
|
||||
REGISTRATION_COUNTRY_OR_REGION_RESTRICTED("A0102", "注册国家或地区受限"),
|
||||
|
||||
USERNAME_VERIFICATION_FAILED("A0110", "用户名校验失败"),
|
||||
USERNAME_ALREADY_EXISTS("A0111", "用户名已存在"),
|
||||
USERNAME_CONTAINS_SENSITIVE_WORDS("A0112", "用户名包含敏感词"),
|
||||
USERNAME_CONTAINS_SPECIAL_CHARACTERS("A0113", "用户名包含特殊字符"),
|
||||
|
||||
PASSWORD_VERIFICATION_FAILED("A0120", "密码校验失败"),
|
||||
PASSWORD_LENGTH_NOT_ENOUGH("A0121", "密码长度不够"),
|
||||
PASSWORD_STRENGTH_NOT_ENOUGH("A0122", "密码强度不够"),
|
||||
|
||||
/** A013x:校验码输入错误 */
|
||||
VERIFICATION_CODE_INPUT_ERROR("A0130", "校验码输入错误"),
|
||||
SMS_VERIFICATION_CODE_INPUT_ERROR("A0131", "短信校验码输入错误"),
|
||||
EMAIL_VERIFICATION_CODE_INPUT_ERROR("A0132", "邮件校验码输入错误"),
|
||||
VOICE_VERIFICATION_CODE_INPUT_ERROR("A0133", "语音校验码输入错误"),
|
||||
|
||||
USER_CERTIFICATE_EXCEPTION("A0140", "用户证件异常"),
|
||||
USER_CERTIFICATE_TYPE_NOT_SELECTED("A0141", "用户证件类型未选择"),
|
||||
MAINLAND_ID_NUMBER_VERIFICATION_ILLEGAL("A0142", "大陆身份证编号校验非法"),
|
||||
|
||||
USER_BASIC_INFORMATION_VERIFICATION_FAILED("A0150", "用户基本信息校验失败"),
|
||||
PHONE_FORMAT_VERIFICATION_FAILED("A0151", "手机格式校验失败"),
|
||||
ADDRESS_FORMAT_VERIFICATION_FAILED("A0152", "地址格式校验失败"),
|
||||
EMAIL_FORMAT_VERIFICATION_FAILED("A0153", "邮箱格式校验失败"),
|
||||
|
||||
/** 二级宏观错误码 */
|
||||
/** A02xx:用户登录异常 */
|
||||
USER_LOGIN_EXCEPTION("A0200", "用户登录异常"),
|
||||
USER_ACCOUNT_FROZEN("A0201", "用户账户被冻结"),
|
||||
USER_ACCOUNT_ABOLISHED("A0202", "用户账户已作废"),
|
||||
|
||||
ACCOUNT_NOT_FOUND("A0201", "用户账户不存在"),
|
||||
ACCOUNT_FROZEN("A0202", "用户账户被冻结"),
|
||||
USER_PASSWORD_ERROR("A0210", "用户名或密码错误"),
|
||||
USER_INPUT_PASSWORD_ERROR_LIMIT_EXCEEDED("A0211", "用户输入密码错误次数超限"),
|
||||
USER_NOT_EXIST("A0212", "用户不存在"),
|
||||
|
||||
USER_IDENTITY_VERIFICATION_FAILED("A0220", "用户身份校验失败"),
|
||||
USER_FINGERPRINT_RECOGNITION_FAILED("A0221", "用户指纹识别失败"),
|
||||
USER_FACE_RECOGNITION_FAILED("A0222", "用户面容识别失败"),
|
||||
USER_NOT_AUTHORIZED_THIRD_PARTY_LOGIN("A0223", "用户未获得第三方登录授权"),
|
||||
|
||||
/** A023x:令牌无效或已过期 */
|
||||
ACCESS_TOKEN_INVALID("A0230", "访问令牌无效或已过期"),
|
||||
REFRESH_TOKEN_INVALID("A0231", "刷新令牌无效或已过期"),
|
||||
|
||||
// 验证码错误
|
||||
/** A024x:验证码错误 */
|
||||
USER_VERIFICATION_CODE_ERROR("A0240", "验证码错误"),
|
||||
USER_VERIFICATION_CODE_ATTEMPT_LIMIT_EXCEEDED("A0241", "用户验证码尝试次数超限"),
|
||||
USER_VERIFICATION_CODE_EXPIRED("A0242", "用户验证码过期"),
|
||||
|
||||
/** 二级宏观错误码 */
|
||||
/** A03xx:访问权限异常 */
|
||||
ACCESS_PERMISSION_EXCEPTION("A0300", "访问权限异常"),
|
||||
ACCESS_UNAUTHORIZED("A0301", "访问未授权"),
|
||||
AUTHORIZATION_IN_PROGRESS("A0302", "正在授权中"),
|
||||
USER_AUTHORIZATION_APPLICATION_REJECTED("A0303", "用户授权申请被拒绝"),
|
||||
|
||||
ACCESS_OBJECT_PRIVACY_SETTINGS_BLOCKED("A0310", "因访问对象隐私设置被拦截"),
|
||||
AUTHORIZATION_EXPIRED("A0311", "授权已过期"),
|
||||
NO_PERMISSION_TO_USE_API("A0312", "无权限使用 API"),
|
||||
|
||||
USER_ACCESS_BLOCKED("A0320", "用户访问被拦截"),
|
||||
BLACKLISTED_USER("A0321", "黑名单用户"),
|
||||
ACCOUNT_FROZEN("A0322", "账号被冻结"),
|
||||
ILLEGAL_IP_ADDRESS("A0323", "非法 IP 地址"),
|
||||
GATEWAY_ACCESS_RESTRICTED("A0324", "网关访问受限"),
|
||||
REGION_BLACKLIST("A0325", "地域黑名单"),
|
||||
|
||||
SERVICE_ARREARS("A0330", "服务已欠费"),
|
||||
|
||||
USER_SIGNATURE_EXCEPTION("A0340", "用户签名异常"),
|
||||
RSA_SIGNATURE_ERROR("A0341", "RSA 签名错误"),
|
||||
|
||||
/** 二级宏观错误码 */
|
||||
/** A04xx:用户请求参数错误 */
|
||||
USER_REQUEST_PARAMETER_ERROR("A0400", "用户请求参数错误"),
|
||||
CONTAINS_ILLEGAL_MALICIOUS_REDIRECT_LINK("A0401", "包含非法恶意跳转链接"),
|
||||
INVALID_USER_INPUT("A0402", "无效的用户输入"),
|
||||
|
||||
REQUEST_REQUIRED_PARAMETER_IS_EMPTY("A0410", "请求必填参数为空"),
|
||||
|
||||
REQUEST_PARAMETER_VALUE_EXCEEDS_ALLOWED_RANGE("A0420", "请求参数值超出允许的范围"),
|
||||
PARAMETER_FORMAT_MISMATCH("A0421", "参数格式不匹配"),
|
||||
|
||||
USER_INPUT_CONTENT_ILLEGAL("A0430", "用户输入内容非法"),
|
||||
CONTAINS_PROHIBITED_SENSITIVE_WORDS("A0431", "包含违禁敏感词"),
|
||||
|
||||
USER_OPERATION_EXCEPTION("A0440", "用户操作异常"),
|
||||
|
||||
/** 二级宏观错误码 */
|
||||
/** A05xx:用户请求服务异常 */
|
||||
USER_REQUEST_SERVICE_EXCEPTION("A0500", "用户请求服务异常"),
|
||||
REQUEST_LIMIT_EXCEEDED("A0501", "请求次数超出限制"),
|
||||
REQUEST_CONCURRENCY_LIMIT_EXCEEDED("A0502", "请求并发数超出限制"),
|
||||
USER_OPERATION_PLEASE_WAIT("A0503", "用户操作请等待"),
|
||||
WEBSOCKET_CONNECTION_EXCEPTION("A0504", "WebSocket 连接异常"),
|
||||
WEBSOCKET_CONNECTION_DISCONNECTED("A0505", "WebSocket 连接断开"),
|
||||
USER_DUPLICATE_REQUEST("A0506", "请求过于频繁,请稍后再试。"),
|
||||
DUPLICATE_SUBMISSION("A0506", "请勿重复提交"),
|
||||
|
||||
/** 二级宏观错误码 */
|
||||
USER_RESOURCE_EXCEPTION("A0600", "用户资源异常"),
|
||||
ACCOUNT_BALANCE_INSUFFICIENT("A0601", "账户余额不足"),
|
||||
USER_DISK_SPACE_INSUFFICIENT("A0602", "用户磁盘空间不足"),
|
||||
USER_MEMORY_SPACE_INSUFFICIENT("A0603", "用户内存空间不足"),
|
||||
USER_OSS_CAPACITY_INSUFFICIENT("A0604", "用户 OSS 容量不足"),
|
||||
USER_QUOTA_EXHAUSTED("A0605", "用户配额已用光"),
|
||||
USER_RESOURCE_NOT_FOUND("A0606", "用户资源不存在"),
|
||||
|
||||
/** 二级宏观错误码 */
|
||||
/** A07xx:文件处理异常 */
|
||||
UPLOAD_FILE_EXCEPTION("A0700", "上传文件异常"),
|
||||
UPLOAD_FILE_TYPE_MISMATCH("A0701", "上传文件类型不匹配"),
|
||||
UPLOAD_FILE_TOO_LARGE("A0702", "上传文件太大"),
|
||||
UPLOAD_IMAGE_TOO_LARGE("A0703", "上传图片太大"),
|
||||
UPLOAD_VIDEO_TOO_LARGE("A0704", "上传视频太大"),
|
||||
UPLOAD_COMPRESSED_FILE_TOO_LARGE("A0705", "上传压缩文件太大"),
|
||||
|
||||
DELETE_FILE_EXCEPTION("A0710", "删除文件异常"),
|
||||
|
||||
/** 二级宏观错误码 */
|
||||
USER_CURRENT_VERSION_EXCEPTION("A0800", "用户当前版本异常"),
|
||||
USER_INSTALLED_VERSION_NOT_MATCH_SYSTEM("A0801", "用户安装版本与系统不匹配"),
|
||||
USER_INSTALLED_VERSION_TOO_LOW("A0802", "用户安装版本过低"),
|
||||
USER_INSTALLED_VERSION_TOO_HIGH("A0803", "用户安装版本过高"),
|
||||
USER_INSTALLED_VERSION_EXPIRED("A0804", "用户安装版本已过期"),
|
||||
USER_API_REQUEST_VERSION_NOT_MATCH("A0805", "用户 API 请求版本不匹配"),
|
||||
USER_API_REQUEST_VERSION_TOO_HIGH("A0806", "用户 API 请求版本过高"),
|
||||
USER_API_REQUEST_VERSION_TOO_LOW("A0807", "用户 API 请求版本过低"),
|
||||
|
||||
/** 二级宏观错误码 */
|
||||
USER_PRIVACY_NOT_AUTHORIZED("A0900", "用户隐私未授权"),
|
||||
USER_PRIVACY_NOT_SIGNED("A0901", "用户隐私未签署"),
|
||||
USER_CAMERA_NOT_AUTHORIZED("A0903", "用户相机未授权"),
|
||||
USER_PHOTO_LIBRARY_NOT_AUTHORIZED("A0904", "用户图片库未授权"),
|
||||
USER_FILE_NOT_AUTHORIZED("A0905", "用户文件未授权"),
|
||||
USER_LOCATION_INFORMATION_NOT_AUTHORIZED("A0906", "用户位置信息未授权"),
|
||||
USER_CONTACTS_NOT_AUTHORIZED("A0907", "用户通讯录未授权"),
|
||||
|
||||
/** 二级宏观错误码 */
|
||||
USER_DEVICE_EXCEPTION("A1000", "用户设备异常"),
|
||||
USER_CAMERA_EXCEPTION("A1001", "用户相机异常"),
|
||||
USER_MICROPHONE_EXCEPTION("A1002", "用户麦克风异常"),
|
||||
USER_EARPIECE_EXCEPTION("A1003", "用户听筒异常"),
|
||||
USER_SPEAKER_EXCEPTION("A1004", "用户扬声器异常"),
|
||||
USER_GPS_POSITIONING_EXCEPTION("A1005", "用户 GPS 定位异常"),
|
||||
|
||||
/** 一级宏观错误码 */
|
||||
|
||||
/** 一级宏观错误码:系统端错误(服务端内部异常/超时/不可用等,需后端排查修复) */
|
||||
SYSTEM_ERROR("B0001", "系统执行出错"),
|
||||
|
||||
/** 二级宏观错误码 */
|
||||
/** 二级宏观错误码:系统端具体错误(按号段细分,便于定位超时/限流/资源耗尽等) */
|
||||
SYSTEM_EXECUTION_TIMEOUT("B0100", "系统执行超时"),
|
||||
|
||||
/** 二级宏观错误码 */
|
||||
SYSTEM_DISASTER_RECOVERY_FUNCTION_TRIGGERED("B0200", "系统容灾功能被触发"),
|
||||
|
||||
SYSTEM_RATE_LIMITING("B0210", "系统限流"),
|
||||
|
||||
SYSTEM_FUNCTION_DEGRADATION("B0220", "系统功能降级"),
|
||||
|
||||
/** 二级宏观错误码 */
|
||||
SYSTEM_RESOURCE_EXCEPTION("B0300", "系统资源异常"),
|
||||
SYSTEM_RESOURCE_EXHAUSTED("B0310", "系统资源耗尽"),
|
||||
SYSTEM_DISK_SPACE_EXHAUSTED("B0311", "系统磁盘空间耗尽"),
|
||||
SYSTEM_MEMORY_EXHAUSTED("B0312", "系统内存耗尽"),
|
||||
FILE_HANDLE_EXHAUSTED("B0313", "文件句柄耗尽"),
|
||||
SYSTEM_CONNECTION_POOL_EXHAUSTED("B0314", "系统连接池耗尽"),
|
||||
SYSTEM_THREAD_POOL_EXHAUSTED("B0315", "系统线程池耗尽"),
|
||||
|
||||
SYSTEM_RESOURCE_ACCESS_EXCEPTION("B0320", "系统资源访问异常"),
|
||||
SYSTEM_READ_DISK_FILE_FAILED("B0321", "系统读取磁盘文件失败"),
|
||||
|
||||
|
||||
/** 一级宏观错误码 */
|
||||
/** 一级宏观错误码:第三方服务错误(外部依赖/中间件/数据库等引起,需检查依赖健康与配置) */
|
||||
THIRD_PARTY_SERVICE_ERROR("C0001", "调用第三方服务出错"),
|
||||
|
||||
/** 二级宏观错误码 */
|
||||
MIDDLEWARE_SERVICE_ERROR("C0100", "中间件服务出错"),
|
||||
|
||||
RPC_SERVICE_ERROR("C0110", "RPC 服务出错"),
|
||||
RPC_SERVICE_NOT_FOUND("C0111", "RPC 服务未找到"),
|
||||
RPC_SERVICE_NOT_REGISTERED("C0112", "RPC 服务未注册"),
|
||||
/** 二级宏观错误码:第三方服务具体错误(按号段细分,便于定位是接口不存在/数据库异常等) */
|
||||
INTERFACE_NOT_EXIST("C0113", "接口不存在"),
|
||||
|
||||
MESSAGE_SERVICE_ERROR("C0120", "消息服务出错"),
|
||||
MESSAGE_DELIVERY_ERROR("C0121", "消息投递出错"),
|
||||
MESSAGE_CONSUMPTION_ERROR("C0122", "消息消费出错"),
|
||||
MESSAGE_SUBSCRIPTION_ERROR("C0123", "消息订阅出错"),
|
||||
MESSAGE_GROUP_NOT_FOUND("C0124", "消息分组未查到"),
|
||||
|
||||
CACHE_SERVICE_ERROR("C0130", "缓存服务出错"),
|
||||
KEY_LENGTH_EXCEEDS_LIMIT("C0131", "key 长度超过限制"),
|
||||
VALUE_LENGTH_EXCEEDS_LIMIT("C0132", "value 长度超过限制"),
|
||||
STORAGE_CAPACITY_FULL("C0133", "存储容量已满"),
|
||||
UNSUPPORTED_DATA_FORMAT("C0134", "不支持的数据格式"),
|
||||
|
||||
CONFIGURATION_SERVICE_ERROR("C0140", "配置服务出错"),
|
||||
|
||||
NETWORK_RESOURCE_SERVICE_ERROR("C0150", "网络资源服务出错"),
|
||||
VPN_SERVICE_ERROR("C0151", "VPN 服务出错"),
|
||||
CDN_SERVICE_ERROR("C0152", "CDN 服务出错"),
|
||||
DOMAIN_NAME_RESOLUTION_SERVICE_ERROR("C0153", "域名解析服务出错"),
|
||||
GATEWAY_SERVICE_ERROR("C0154", "网关服务出错"),
|
||||
|
||||
/** 二级宏观错误码 */
|
||||
THIRD_PARTY_SYSTEM_EXECUTION_TIMEOUT("C0200", "第三方系统执行超时"),
|
||||
|
||||
RPC_EXECUTION_TIMEOUT("C0210", "RPC 执行超时"),
|
||||
|
||||
MESSAGE_DELIVERY_TIMEOUT("C0220", "消息投递超时"),
|
||||
|
||||
CACHE_SERVICE_TIMEOUT("C0230", "缓存服务超时"),
|
||||
|
||||
CONFIGURATION_SERVICE_TIMEOUT("C0240", "配置服务超时"),
|
||||
|
||||
DATABASE_SERVICE_TIMEOUT("C0250", "数据库服务超时"),
|
||||
|
||||
/** 二级宏观错误码 */
|
||||
DATABASE_SERVICE_ERROR("C0300", "数据库服务出错"),
|
||||
|
||||
TABLE_NOT_EXIST("C0311", "表不存在"),
|
||||
COLUMN_NOT_EXIST("C0312", "列不存在"),
|
||||
DATABASE_EXECUTION_SYNTAX_ERROR("C0313", "数据库执行语法错误"),
|
||||
|
||||
MULTIPLE_SAME_NAME_COLUMNS_IN_MULTI_TABLE_ASSOCIATION("C0321", "多表关联中存在多个相同名称的列"),
|
||||
|
||||
DATABASE_DEADLOCK("C0331", "数据库死锁"),
|
||||
|
||||
PRIMARY_KEY_CONFLICT("C0341", "主键冲突"),
|
||||
INTEGRITY_CONSTRAINT_VIOLATION("C0342", "违反了完整性约束"),
|
||||
DATABASE_ACCESS_DENIED("C0351", "演示环境已禁用数据库写入功能,请本地部署修改数据库链接或开启Mock模式进行体验");
|
||||
|
||||
DATABASE_ACCESS_DENIED("C0351", "演示环境已禁用数据库写入功能,请本地部署修改数据库链接或开启Mock模式进行体验"),
|
||||
private final String code;
|
||||
|
||||
/** 二级宏观错误码 */
|
||||
THIRD_PARTY_DISASTER_RECOVERY_SYSTEM_TRIGGERED("C0400", "第三方容灾系统被触发"),
|
||||
THIRD_PARTY_SYSTEM_RATE_LIMITING("C0401", "第三方系统限流"),
|
||||
THIRD_PARTY_FUNCTION_DEGRADATION("C0402", "第三方功能降级"),
|
||||
private final String msg;
|
||||
|
||||
/** 二级宏观错误码 */
|
||||
NOTIFICATION_SERVICE_ERROR("C0500", "通知服务出错"),
|
||||
SMS_REMINDER_SERVICE_FAILED("C0501", "短信提醒服务失败"),
|
||||
VOICE_REMINDER_SERVICE_FAILED("C0502", "语音提醒服务失败"),
|
||||
EMAIL_REMINDER_SERVICE_FAILED("C0503", "邮件提醒服务失败");
|
||||
ResultCode(String code, String msg) {
|
||||
this.code = code;
|
||||
this.msg = msg;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
@@ -276,10 +135,6 @@ public enum ResultCode implements IResultCode, Serializable {
|
||||
return msg;
|
||||
}
|
||||
|
||||
private String code;
|
||||
|
||||
private String msg;
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "{" +
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
package com.youlai.boot.core.web;
|
||||
|
||||
import cn.hutool.extra.servlet.JakartaServletUtil;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
* Web响应辅助类
|
||||
* <p>
|
||||
* 用于在过滤器、处理器等无法使用 @RestControllerAdvice 的场景中统一处理响应
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Slf4j
|
||||
public class WebResponseHelper {
|
||||
|
||||
/**
|
||||
* 写入错误响应
|
||||
*
|
||||
* @param response HttpServletResponse
|
||||
* @param resultCode 响应结果码
|
||||
*/
|
||||
public static void writeError(HttpServletResponse response, ResultCode resultCode) {
|
||||
writeError(response, resultCode, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入错误响应(带自定义消息)
|
||||
*
|
||||
* @param response HttpServletResponse
|
||||
* @param resultCode 响应结果码
|
||||
* @param message 自定义消息
|
||||
*/
|
||||
public static void writeError(HttpServletResponse response, ResultCode resultCode, String message) {
|
||||
try {
|
||||
// 设置HTTP状态码
|
||||
int httpStatus = mapHttpStatus(resultCode);
|
||||
response.setStatus(httpStatus);
|
||||
response.setCharacterEncoding(StandardCharsets.UTF_8.toString());
|
||||
// 构建响应对象
|
||||
Result<?> result = message == null
|
||||
? Result.failed(resultCode)
|
||||
: Result.failed(resultCode, message);
|
||||
|
||||
// 写入响应
|
||||
JakartaServletUtil.write(response,
|
||||
JSONUtil.toJsonStr(result),
|
||||
MediaType.APPLICATION_JSON_VALUE
|
||||
);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("写入错误响应失败: resultCode={}, message={}", resultCode, message, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据业务结果码映射HTTP状态码
|
||||
*
|
||||
* @param resultCode 业务结果码
|
||||
* @return HTTP状态码
|
||||
*/
|
||||
private static int mapHttpStatus(ResultCode resultCode) {
|
||||
return switch (resultCode) {
|
||||
case ACCESS_UNAUTHORIZED,
|
||||
ACCESS_TOKEN_INVALID,
|
||||
REFRESH_TOKEN_INVALID -> HttpStatus.UNAUTHORIZED.value();
|
||||
default -> HttpStatus.BAD_REQUEST.value();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
122
src/main/java/com/youlai/boot/core/web/WebResponseWriter.java
Normal file
122
src/main/java/com/youlai/boot/core/web/WebResponseWriter.java
Normal file
@@ -0,0 +1,122 @@
|
||||
package com.youlai.boot.core.web;
|
||||
|
||||
import cn.hutool.extra.servlet.JakartaServletUtil;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
* Web响应写入器
|
||||
* <p>
|
||||
* 用于在过滤器、Security处理器等无法使用 @RestControllerAdvice 的场景中统一写入HTTP响应。
|
||||
* 支持写入成功响应和错误响应。
|
||||
* 此类为工具类,所有方法均为静态方法,禁止实例化。
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Slf4j
|
||||
public final class WebResponseWriter {
|
||||
|
||||
/**
|
||||
* 私有构造函数,防止实例化
|
||||
*/
|
||||
private WebResponseWriter() {
|
||||
throw new UnsupportedOperationException("工具类不允许实例化");
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入成功响应
|
||||
*
|
||||
* @param response HttpServletResponse
|
||||
* @param data 响应数据(可选)
|
||||
*/
|
||||
public static void writeSuccess(HttpServletResponse response, Object data) {
|
||||
writeResult(response, Result.success(data), HttpStatus.OK.value());
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入成功响应(无数据)
|
||||
*
|
||||
* @param response HttpServletResponse
|
||||
*/
|
||||
public static void writeSuccess(HttpServletResponse response) {
|
||||
writeSuccess(response, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入错误响应
|
||||
*
|
||||
* @param response HttpServletResponse
|
||||
* @param resultCode 响应结果码
|
||||
*/
|
||||
public static void writeError(HttpServletResponse response, ResultCode resultCode) {
|
||||
writeError(response, resultCode, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入错误响应(带自定义消息)
|
||||
*
|
||||
* @param response HttpServletResponse
|
||||
* @param resultCode 响应结果码
|
||||
* @param message 自定义消息(可选,为 null 时使用 resultCode 的默认消息)
|
||||
*/
|
||||
public static void writeError(HttpServletResponse response, ResultCode resultCode, String message) {
|
||||
Result<?> result = message == null
|
||||
? Result.failed(resultCode)
|
||||
: Result.failed(resultCode, message);
|
||||
|
||||
int httpStatus = mapHttpStatus(resultCode);
|
||||
writeResult(response, result, httpStatus);
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入响应结果(通用方法)
|
||||
*
|
||||
* @param response HttpServletResponse
|
||||
* @param result 响应结果对象
|
||||
* @param httpStatus HTTP状态码
|
||||
*/
|
||||
private static void writeResult(HttpServletResponse response, Result<?> result, int httpStatus) {
|
||||
try {
|
||||
// 设置HTTP状态码
|
||||
response.setStatus(httpStatus);
|
||||
|
||||
// 设置响应编码和内容类型
|
||||
response.setCharacterEncoding(StandardCharsets.UTF_8.toString());
|
||||
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
|
||||
|
||||
// 写入响应
|
||||
JakartaServletUtil.write(response,
|
||||
JSONUtil.toJsonStr(result),
|
||||
MediaType.APPLICATION_JSON_VALUE
|
||||
);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("写入响应时发生未知异常: httpStatus={}, result={}", httpStatus, result, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据业务结果码映射HTTP状态码
|
||||
*
|
||||
* @param resultCode 业务结果码
|
||||
* @return HTTP状态码
|
||||
*/
|
||||
private static int mapHttpStatus(ResultCode resultCode) {
|
||||
return switch (resultCode) {
|
||||
case ACCESS_UNAUTHORIZED,
|
||||
ACCESS_TOKEN_INVALID,
|
||||
REFRESH_TOKEN_INVALID -> HttpStatus.UNAUTHORIZED.value();
|
||||
default -> HttpStatus.BAD_REQUEST.value();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
package com.youlai.boot.platform.ai.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.youlai.boot.core.web.PageResult;
|
||||
import com.youlai.boot.core.web.Result;
|
||||
import com.youlai.boot.platform.ai.model.dto.AiExecuteRequestDto;
|
||||
import com.youlai.boot.platform.ai.model.dto.AiParseRequestDto;
|
||||
import com.youlai.boot.platform.ai.model.dto.AiParseResponseDto;
|
||||
import com.youlai.boot.platform.ai.model.query.AiAssistantPageQuery;
|
||||
import com.youlai.boot.platform.ai.model.vo.AiAssistantRecordVo;
|
||||
import com.youlai.boot.platform.ai.service.AiAssistantRecordService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* AI 助手控制器
|
||||
* <p>
|
||||
* 负责 AI 命令的解析、执行、记录管理及回滚操作,
|
||||
* 表示一次 AI 助手完整的指令生命周期。
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 3.0.0
|
||||
*/
|
||||
@Tag(name = "AI 助手接口")
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/ai/assistant")
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class AiAssistantController {
|
||||
|
||||
private final AiAssistantRecordService aiAssistantRecordService;
|
||||
|
||||
@Operation(summary = "解析自然语言命令")
|
||||
@PostMapping("/parse")
|
||||
public Result<AiParseResponseDto> parseCommand(
|
||||
@RequestBody AiParseRequestDto request,
|
||||
HttpServletRequest httpRequest
|
||||
) {
|
||||
log.info("收到 AI 命令解析请求: {}", request.getCommand());
|
||||
try {
|
||||
AiParseResponseDto response = aiAssistantRecordService.parseCommand(request, httpRequest);
|
||||
return Result.success(response);
|
||||
} catch (Exception e) {
|
||||
log.error("命令解析失败", e);
|
||||
return Result.success(AiParseResponseDto.builder()
|
||||
.success(false)
|
||||
.error(e.getMessage())
|
||||
.build());
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "执行已解析的命令")
|
||||
@PostMapping("/execute")
|
||||
public Result<Object> executeCommand(
|
||||
@RequestBody AiExecuteRequestDto request,
|
||||
HttpServletRequest httpRequest
|
||||
) {
|
||||
log.info("收到 AI 命令执行请求: {}", request.getFunctionCall().getName());
|
||||
try {
|
||||
Object result = aiAssistantRecordService.executeCommand(request, httpRequest);
|
||||
return Result.success(result);
|
||||
} catch (Exception e) {
|
||||
log.error("命令执行失败", e);
|
||||
return Result.failed(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "获取 AI 命令记录分页列表")
|
||||
@GetMapping("/records")
|
||||
public PageResult<AiAssistantRecordVo> getRecordPage(AiAssistantPageQuery queryParams) {
|
||||
IPage<AiAssistantRecordVo> page = aiAssistantRecordService.getRecordPage(queryParams);
|
||||
return PageResult.success(page);
|
||||
}
|
||||
|
||||
@Operation(summary = "删除 AI 命令记录")
|
||||
@DeleteMapping("/records/{ids}")
|
||||
public Result<Void> deleteRecords(
|
||||
@Parameter(description = "记录ID,多个以英文逗号(,)分割")
|
||||
@PathVariable String ids
|
||||
) {
|
||||
List<Long> idList = Arrays.stream(ids.split(","))
|
||||
.filter(s -> s != null && !s.isBlank())
|
||||
.map(String::trim)
|
||||
.map(Long::valueOf)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
boolean removed = aiAssistantRecordService.deleteRecords(idList);
|
||||
return Result.judge(removed);
|
||||
}
|
||||
|
||||
@Operation(summary = "撤销命令执行")
|
||||
@PostMapping("/records/{recordId}/rollback")
|
||||
public Result<Void> rollbackCommand(
|
||||
@Parameter(description = "记录ID")
|
||||
@PathVariable String recordId
|
||||
) {
|
||||
aiAssistantRecordService.rollbackCommand(recordId);
|
||||
return Result.success();
|
||||
}
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
package com.youlai.boot.platform.ai.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.youlai.boot.core.web.PageResult;
|
||||
import com.youlai.boot.core.web.Result;
|
||||
import com.youlai.boot.platform.ai.model.dto.AiExecuteRequestDTO;
|
||||
import com.youlai.boot.platform.ai.model.dto.AiParseRequestDTO;
|
||||
import com.youlai.boot.platform.ai.model.dto.AiParseResponseDTO;
|
||||
import com.youlai.boot.platform.ai.model.query.AiCommandPageQuery;
|
||||
import com.youlai.boot.platform.ai.model.vo.AiCommandRecordVO;
|
||||
import com.youlai.boot.platform.ai.service.AiCommandRecordService;
|
||||
import com.youlai.boot.platform.ai.service.AiCommandService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
/**
|
||||
* AI 命令控制器(基于 Spring AI)
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 3.0.0
|
||||
*/
|
||||
@Tag(name = "AI命令接口")
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/ai/command")
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class AiCommandController {
|
||||
|
||||
private final AiCommandService aiCommandService;
|
||||
private final AiCommandRecordService recordService;
|
||||
|
||||
@Operation(summary = "解析自然语言命令")
|
||||
@PostMapping("/parse")
|
||||
public Result<AiParseResponseDTO> parseCommand(
|
||||
@RequestBody AiParseRequestDTO request,
|
||||
HttpServletRequest httpRequest
|
||||
) {
|
||||
log.info("收到AI命令解析请求: {}", request.getCommand());
|
||||
|
||||
try {
|
||||
AiParseResponseDTO response = aiCommandService.parseCommand(request, httpRequest);
|
||||
return Result.success(response);
|
||||
} catch (Exception e) {
|
||||
log.error("命令解析失败", e);
|
||||
return Result.success(AiParseResponseDTO.builder()
|
||||
.success(false)
|
||||
.error(e.getMessage())
|
||||
.build());
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "执行已解析的命令")
|
||||
@PostMapping("/execute")
|
||||
public Result<Object> executeCommand(
|
||||
@RequestBody AiExecuteRequestDTO request,
|
||||
HttpServletRequest httpRequest
|
||||
) {
|
||||
log.info("收到AI命令执行请求: {}", request.getFunctionCall().getName());
|
||||
try {
|
||||
Object result = aiCommandService.executeCommand(request, httpRequest);
|
||||
return Result.success(result);
|
||||
} catch (Exception e) {
|
||||
log.error("命令执行失败", e);
|
||||
return Result.failed(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "获取AI命令记录分页列表")
|
||||
@GetMapping("/records")
|
||||
public PageResult<AiCommandRecordVO> getRecordPage(AiCommandPageQuery queryParams) {
|
||||
IPage<AiCommandRecordVO> page = recordService.getRecordPage(queryParams);
|
||||
return PageResult.success(page);
|
||||
}
|
||||
|
||||
@Operation(summary = "撤销命令执行")
|
||||
@PostMapping("/rollback/{recordId}")
|
||||
public Result<?> rollbackCommand(
|
||||
@Parameter(description = "记录ID") @PathVariable String recordId
|
||||
) {
|
||||
recordService.rollbackCommand(recordId);
|
||||
return Result.success("撤销成功");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.youlai.boot.platform.ai.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.youlai.boot.platform.ai.model.entity.AiAssistantRecord;
|
||||
import com.youlai.boot.platform.ai.model.query.AiAssistantPageQuery;
|
||||
import com.youlai.boot.platform.ai.model.vo.AiAssistantRecordVo;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface AiAssistantRecordMapper extends BaseMapper<AiAssistantRecord> {
|
||||
|
||||
/**
|
||||
* AI 助手行为记录分页列表
|
||||
*
|
||||
* @param page 分页参数
|
||||
* @param queryParams 查询参数
|
||||
* @return 分页结果
|
||||
*/
|
||||
IPage<AiAssistantRecordVo> getRecordPage(Page<AiAssistantRecordVo> page, AiAssistantPageQuery queryParams);
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
package com.youlai.boot.platform.ai.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.youlai.boot.platform.ai.model.entity.AiCommandRecord;
|
||||
import com.youlai.boot.platform.ai.model.query.AiCommandPageQuery;
|
||||
import com.youlai.boot.platform.ai.model.vo.AiCommandRecordVO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* AI 命令记录 Mapper
|
||||
*/
|
||||
@Mapper
|
||||
public interface AiCommandRecordMapper extends BaseMapper<AiCommandRecord> {
|
||||
|
||||
/**
|
||||
* 获取 AI 命令记录分页列表
|
||||
*/
|
||||
IPage<AiCommandRecordVO> getRecordPage(Page<AiCommandRecordVO> page, AiCommandPageQuery queryParams);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,13 +3,13 @@ package com.youlai.boot.platform.ai.model.dto;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* AI 命令执行请求 DTO
|
||||
* AI 命令执行请求 Dto
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 3.0.0
|
||||
*/
|
||||
@Data
|
||||
public class AiExecuteRequestDTO {
|
||||
public class AiExecuteRequestDto {
|
||||
|
||||
/**
|
||||
* 关联的解析日志ID
|
||||
@@ -24,7 +24,7 @@ public class AiExecuteRequestDTO {
|
||||
/**
|
||||
* 要执行的函数调用
|
||||
*/
|
||||
private AiFunctionCallDTO functionCall;
|
||||
private AiFunctionCallDto functionCall;
|
||||
|
||||
/**
|
||||
* 确认模式:auto=自动执行, manual=需要用户确认
|
||||
@@ -46,6 +46,3 @@ public class AiExecuteRequestDTO {
|
||||
*/
|
||||
private String currentRoute;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* AI 命令执行响应 DTO
|
||||
* AI 命令执行响应 Dto
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 3.0.0
|
||||
@@ -15,7 +15,7 @@ import lombok.NoArgsConstructor;
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class AiExecuteResponseDTO {
|
||||
public class AiExecuteResponseDto {
|
||||
|
||||
/**
|
||||
* 是否执行成功
|
||||
@@ -57,6 +57,3 @@ public class AiExecuteResponseDTO {
|
||||
*/
|
||||
private String confirmationPrompt;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import lombok.NoArgsConstructor;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* AI 函数调用 DTO
|
||||
* AI 函数调用 Dto
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 3.0.0
|
||||
@@ -16,7 +16,7 @@ import java.util.Map;
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class AiFunctionCallDTO {
|
||||
public class AiFunctionCallDto {
|
||||
|
||||
/**
|
||||
* 函数名称
|
||||
|
||||
@@ -4,13 +4,13 @@ import lombok.Data;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* AI 解析请求 DTO
|
||||
* AI 解析请求 Dto
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 3.0.0
|
||||
*/
|
||||
@Data
|
||||
public class AiParseRequestDTO {
|
||||
public class AiParseRequestDto {
|
||||
|
||||
/**
|
||||
* 用户输入的自然语言命令
|
||||
@@ -32,4 +32,3 @@ public class AiParseRequestDTO {
|
||||
*/
|
||||
private Map<String, Object> context;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import lombok.NoArgsConstructor;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* AI 解析响应 DTO
|
||||
* AI 解析响应 Dto
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 3.0.0
|
||||
@@ -16,7 +16,7 @@ import java.util.List;
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class AiParseResponseDTO {
|
||||
public class AiParseResponseDto {
|
||||
|
||||
/**
|
||||
* 解析日志ID(用于关联执行记录)
|
||||
@@ -31,7 +31,7 @@ public class AiParseResponseDTO {
|
||||
/**
|
||||
* 解析后的函数调用列表
|
||||
*/
|
||||
private List<AiFunctionCallDTO> functionCalls;
|
||||
private List<AiFunctionCallDto> functionCalls;
|
||||
|
||||
/**
|
||||
* AI 的理解和说明
|
||||
@@ -53,4 +53,3 @@ public class AiParseResponseDTO {
|
||||
*/
|
||||
private String rawResponse;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
package com.youlai.boot.platform.ai.model.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.youlai.boot.common.base.BaseEntity;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/**
|
||||
* AI 助手行为记录实体
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 3.0.0
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@TableName("ai_assistant_record")
|
||||
public class AiAssistantRecord extends BaseEntity {
|
||||
|
||||
/** 用户ID */
|
||||
private Long userId;
|
||||
|
||||
/** 用户名 */
|
||||
private String username;
|
||||
|
||||
/** 原始命令 */
|
||||
private String originalCommand;
|
||||
|
||||
// ==================== 解析相关字段 ====================
|
||||
|
||||
/** AI 供应商(qwen/openai/deepseek等) */
|
||||
private String aiProvider;
|
||||
|
||||
/** AI 模型(qwen-plus/qwen-max/gpt-4-turbo等) */
|
||||
private String aiModel;
|
||||
|
||||
/** 解析状态(0-失败, 1-成功) */
|
||||
private Integer parseStatus;
|
||||
|
||||
/** 解析出的函数调用列表(JSON) */
|
||||
private String functionCalls;
|
||||
|
||||
/** AI 的理解说明 */
|
||||
private String explanation;
|
||||
|
||||
/** 置信度(0.00-1.00) */
|
||||
private BigDecimal confidence;
|
||||
|
||||
/** 解析错误信息 */
|
||||
private String parseErrorMessage;
|
||||
|
||||
/** 输入 Token 数量 */
|
||||
private Integer inputTokens;
|
||||
|
||||
/** 输出 Token 数量 */
|
||||
private Integer outputTokens;
|
||||
|
||||
/** 解析耗时(毫秒) */
|
||||
private Integer parseDurationMs;
|
||||
|
||||
// ==================== 执行相关字段 ====================
|
||||
|
||||
/** 执行的函数名称 */
|
||||
private String functionName;
|
||||
|
||||
/** 函数参数(JSON) */
|
||||
private String functionArguments;
|
||||
|
||||
/** 执行状态(0-待执行, 1-成功, -1-失败) */
|
||||
private Integer executeStatus;
|
||||
|
||||
/** 执行错误信息 */
|
||||
private String executeErrorMessage;
|
||||
|
||||
// ==================== 通用字段 ====================
|
||||
|
||||
/** IP 地址 */
|
||||
private String ipAddress;
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
package com.youlai.boot.platform.ai.model.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.youlai.boot.common.base.BaseEntity;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/**
|
||||
* AI 命令记录实体(合并解析和执行记录)
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 3.0.0
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@TableName("ai_command_record")
|
||||
public class AiCommandRecord extends BaseEntity {
|
||||
|
||||
/** 用户ID */
|
||||
private Long userId;
|
||||
|
||||
/** 用户名 */
|
||||
private String username;
|
||||
|
||||
/** 原始命令 */
|
||||
private String originalCommand;
|
||||
|
||||
// ==================== 解析相关字段 ====================
|
||||
|
||||
/** AI 供应商(qwen/openai/deepseek等) */
|
||||
private String provider;
|
||||
|
||||
/** AI 模型(qwen-plus/qwen-max/gpt-4-turbo等) */
|
||||
private String model;
|
||||
|
||||
/** 解析是否成功 */
|
||||
private Boolean parseSuccess;
|
||||
|
||||
/** 解析出的函数调用列表(JSON) */
|
||||
private String functionCalls;
|
||||
|
||||
/** AI 的理解说明 */
|
||||
private String explanation;
|
||||
|
||||
/** 置信度(0.00-1.00) */
|
||||
private BigDecimal confidence;
|
||||
|
||||
/** 解析错误信息 */
|
||||
private String parseErrorMessage;
|
||||
|
||||
/** 输入 Token 数量 */
|
||||
private Integer inputTokens;
|
||||
|
||||
/** 输出 Token 数量 */
|
||||
private Integer outputTokens;
|
||||
|
||||
/** 总 Token 数量 */
|
||||
private Integer totalTokens;
|
||||
|
||||
/** 解析耗时(毫秒) */
|
||||
private Long parseTime;
|
||||
|
||||
// ==================== 执行相关字段 ====================
|
||||
|
||||
/** 执行的函数名称 */
|
||||
private String functionName;
|
||||
|
||||
/** 函数参数(JSON) */
|
||||
private String functionArguments;
|
||||
|
||||
/** 执行状态:pending, success, failed */
|
||||
private String executeStatus;
|
||||
|
||||
/** 执行结果(JSON) */
|
||||
private String executeResult;
|
||||
|
||||
/** 执行错误信息 */
|
||||
private String executeErrorMessage;
|
||||
|
||||
/** 影响的记录数 */
|
||||
private Integer affectedRows;
|
||||
|
||||
/** 是否危险操作 */
|
||||
private Boolean isDangerous;
|
||||
|
||||
/** 是否需要确认 */
|
||||
private Boolean requiresConfirmation;
|
||||
|
||||
/** 用户是否确认 */
|
||||
private Boolean userConfirmed;
|
||||
|
||||
/** 幂等性令牌(防止重复执行) */
|
||||
private String idempotencyKey;
|
||||
|
||||
/** 执行耗时(毫秒) */
|
||||
private Long executionTime;
|
||||
|
||||
// ==================== 通用字段 ====================
|
||||
|
||||
/** IP 地址 */
|
||||
private String ipAddress;
|
||||
|
||||
/** 用户代理 */
|
||||
private String userAgent;
|
||||
|
||||
/** 当前页面路由 */
|
||||
private String currentRoute;
|
||||
|
||||
/** 备注 */
|
||||
private String remark;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.youlai.boot.platform.ai.model.query;
|
||||
|
||||
import com.youlai.boot.common.base.BasePageQuery;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* AI 助手行为记录分页查询对象
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 3.0.0
|
||||
*/
|
||||
@Schema(description = "AI 助手行为记录分页查询对象")
|
||||
@Getter
|
||||
@Setter
|
||||
public class AiAssistantPageQuery extends BasePageQuery {
|
||||
|
||||
@Schema(description = "关键字(原始命令/函数名称/用户名)")
|
||||
private String keywords;
|
||||
|
||||
@Schema(description = "执行状态(0-待执行, 1-成功, -1-失败)")
|
||||
private Integer executeStatus;
|
||||
|
||||
@Schema(description = "用户ID")
|
||||
private Long userId;
|
||||
|
||||
@Schema(description = "解析状态(0-失败, 1-成功)")
|
||||
private Integer parseStatus;
|
||||
|
||||
@Schema(description = "创建时间范围")
|
||||
private List<String> createTime;
|
||||
|
||||
@Schema(description = "函数名称")
|
||||
private String functionName;
|
||||
|
||||
@Schema(description = "AI供应商")
|
||||
private String aiProvider;
|
||||
|
||||
@Schema(description = "AI模型")
|
||||
private String aiModel;
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
package com.youlai.boot.platform.ai.model.query;
|
||||
|
||||
import com.youlai.boot.common.base.BasePageQuery;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* AI命令记录分页查询对象
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 3.0.0
|
||||
*/
|
||||
@Schema(description = "AI命令记录分页查询对象")
|
||||
@Getter
|
||||
@Setter
|
||||
public class AiCommandPageQuery extends BasePageQuery {
|
||||
|
||||
@Schema(description = "关键字(原始命令/函数名称/用户名)")
|
||||
private String keywords;
|
||||
|
||||
@Schema(description = "执行状态(pending-待执行, success-成功, failed-失败)")
|
||||
private String executeStatus;
|
||||
|
||||
@Schema(description = "用户ID")
|
||||
private Long userId;
|
||||
|
||||
@Schema(description = "是否危险操作")
|
||||
private Boolean isDangerous;
|
||||
|
||||
@Schema(description = "创建时间范围")
|
||||
private List<String> createTime;
|
||||
|
||||
@Schema(description = "函数名称")
|
||||
private String functionName;
|
||||
}
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
package com.youlai.boot.platform.ai.model.query;
|
||||
|
||||
import com.youlai.boot.common.base.BasePageQuery;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* AI命令解析日志分页查询对象
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 3.0.0
|
||||
*/
|
||||
@Schema(description = "AI命令解析日志分页查询对象")
|
||||
@Getter
|
||||
@Setter
|
||||
public class AiParseLogPageQuery extends BasePageQuery {
|
||||
|
||||
@Schema(description = "关键字(原始命令/用户名)")
|
||||
private String keywords;
|
||||
|
||||
@Schema(description = "解析是否成功(0-失败, 1-成功)")
|
||||
private Boolean parseSuccess;
|
||||
|
||||
@Schema(description = "用户ID")
|
||||
private Long userId;
|
||||
|
||||
@Schema(description = "AI提供商(qwen/openai/deepseek/gemini等)")
|
||||
private String provider;
|
||||
|
||||
@Schema(description = "AI模型(qwen-plus/qwen-max/gpt-4-turbo等)")
|
||||
private String model;
|
||||
|
||||
@Schema(description = "创建时间范围")
|
||||
private List<String> createTime;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
package com.youlai.boot.platform.ai.model.vo;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* AI 助手行为记录Vo
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 3.0.0
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "AI 助手行为记录Vo")
|
||||
public class AiAssistantRecordVo implements Serializable {
|
||||
|
||||
@Schema(description = "主键ID")
|
||||
private String id;
|
||||
|
||||
@Schema(description = "用户ID")
|
||||
private Long userId;
|
||||
|
||||
@Schema(description = "用户名")
|
||||
private String username;
|
||||
|
||||
@Schema(description = "原始命令")
|
||||
private String originalCommand;
|
||||
|
||||
// ==================== 解析相关字段 ====================
|
||||
|
||||
@Schema(description = "AI供应商")
|
||||
private String aiProvider;
|
||||
|
||||
@Schema(description = "AI模型")
|
||||
private String aiModel;
|
||||
|
||||
@Schema(description = "解析状态(0-失败, 1-成功)")
|
||||
private Integer parseStatus;
|
||||
|
||||
@Schema(description = "解析出的函数调用列表(JSON)")
|
||||
private String functionCalls;
|
||||
|
||||
@Schema(description = "AI的理解说明")
|
||||
private String explanation;
|
||||
|
||||
@Schema(description = "置信度")
|
||||
private BigDecimal confidence;
|
||||
|
||||
@Schema(description = "解析错误信息")
|
||||
private String parseErrorMessage;
|
||||
|
||||
@Schema(description = "输入Token数量")
|
||||
private Integer inputTokens;
|
||||
|
||||
@Schema(description = "输出Token数量")
|
||||
private Integer outputTokens;
|
||||
|
||||
@Schema(description = "解析耗时(毫秒)")
|
||||
private Integer parseDurationMs;
|
||||
|
||||
// ==================== 执行相关字段 ====================
|
||||
|
||||
@Schema(description = "执行的函数名称")
|
||||
private String functionName;
|
||||
|
||||
@Schema(description = "函数参数(JSON)")
|
||||
private String functionArguments;
|
||||
|
||||
@Schema(description = "执行状态(0-待执行, 1-成功, -1-失败)")
|
||||
private Integer executeStatus;
|
||||
|
||||
@Schema(description = "执行错误信息")
|
||||
private String executeErrorMessage;
|
||||
|
||||
// ==================== 通用字段 ====================
|
||||
|
||||
@Schema(description = "IP地址")
|
||||
private String ipAddress;
|
||||
|
||||
@Schema(description = "创建时间")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime createTime;
|
||||
|
||||
@Schema(description = "更新时间")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime updateTime;
|
||||
}
|
||||
@@ -1,120 +0,0 @@
|
||||
package com.youlai.boot.platform.ai.model.vo;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* AI命令记录VO(合并解析和执行记录)
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "AI命令记录VO")
|
||||
public class AiCommandRecordVO implements Serializable {
|
||||
|
||||
@Schema(description = "主键ID")
|
||||
private String id;
|
||||
|
||||
@Schema(description = "用户ID")
|
||||
private Long userId;
|
||||
|
||||
@Schema(description = "用户名")
|
||||
private String username;
|
||||
|
||||
@Schema(description = "原始命令")
|
||||
private String originalCommand;
|
||||
|
||||
// ==================== 解析相关字段 ====================
|
||||
|
||||
@Schema(description = "AI供应商")
|
||||
private String provider;
|
||||
|
||||
@Schema(description = "AI模型")
|
||||
private String model;
|
||||
|
||||
@Schema(description = "解析是否成功")
|
||||
private Boolean parseSuccess;
|
||||
|
||||
@Schema(description = "解析出的函数调用列表(JSON)")
|
||||
private String functionCalls;
|
||||
|
||||
@Schema(description = "AI的理解说明")
|
||||
private String explanation;
|
||||
|
||||
@Schema(description = "置信度")
|
||||
private BigDecimal confidence;
|
||||
|
||||
@Schema(description = "解析错误信息")
|
||||
private String parseErrorMessage;
|
||||
|
||||
@Schema(description = "输入Token数量")
|
||||
private Integer inputTokens;
|
||||
|
||||
@Schema(description = "输出Token数量")
|
||||
private Integer outputTokens;
|
||||
|
||||
@Schema(description = "总Token数量")
|
||||
private Integer totalTokens;
|
||||
|
||||
@Schema(description = "解析耗时(毫秒)")
|
||||
private Long parseTime;
|
||||
|
||||
// ==================== 执行相关字段 ====================
|
||||
|
||||
@Schema(description = "执行的函数名称")
|
||||
private String functionName;
|
||||
|
||||
@Schema(description = "函数参数(JSON)")
|
||||
private String functionArguments;
|
||||
|
||||
@Schema(description = "执行状态")
|
||||
private String executeStatus;
|
||||
|
||||
@Schema(description = "执行结果(JSON)")
|
||||
private String executeResult;
|
||||
|
||||
@Schema(description = "执行错误信息")
|
||||
private String executeErrorMessage;
|
||||
|
||||
@Schema(description = "影响的记录数")
|
||||
private Integer affectedRows;
|
||||
|
||||
@Schema(description = "是否危险操作")
|
||||
private Boolean isDangerous;
|
||||
|
||||
@Schema(description = "是否需要确认")
|
||||
private Boolean requiresConfirmation;
|
||||
|
||||
@Schema(description = "用户是否确认")
|
||||
private Boolean userConfirmed;
|
||||
|
||||
@Schema(description = "执行耗时(毫秒)")
|
||||
private Long executionTime;
|
||||
|
||||
// ==================== 通用字段 ====================
|
||||
|
||||
@Schema(description = "IP地址")
|
||||
private String ipAddress;
|
||||
|
||||
@Schema(description = "用户代理")
|
||||
private String userAgent;
|
||||
|
||||
@Schema(description = "当前页面路由")
|
||||
private String currentRoute;
|
||||
|
||||
@Schema(description = "创建时间")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime createTime;
|
||||
|
||||
@Schema(description = "更新时间")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime updateTime;
|
||||
|
||||
@Schema(description = "备注")
|
||||
private String remark;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
package com.youlai.boot.platform.ai.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.youlai.boot.platform.ai.model.dto.AiExecuteRequestDto;
|
||||
import com.youlai.boot.platform.ai.model.dto.AiParseRequestDto;
|
||||
import com.youlai.boot.platform.ai.model.dto.AiParseResponseDto;
|
||||
import com.youlai.boot.platform.ai.model.entity.AiAssistantRecord;
|
||||
import com.youlai.boot.platform.ai.model.query.AiAssistantPageQuery;
|
||||
import com.youlai.boot.platform.ai.model.vo.AiAssistantRecordVo;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* AI 助手行为记录服务接口
|
||||
*
|
||||
* 负责 AI 助手指令的解析/执行审计记录的分页查询、删除与回滚。
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 3.0.0
|
||||
*/
|
||||
public interface AiAssistantRecordService extends IService<AiAssistantRecord> {
|
||||
|
||||
/**
|
||||
* 解析自然语言命令。
|
||||
*
|
||||
* @param request 解析请求参数
|
||||
* @param httpRequest HTTP 请求(用于获取 IP 等上下文)
|
||||
* @return 解析结果(包含 functionCalls 等信息)
|
||||
*/
|
||||
AiParseResponseDto parseCommand(AiParseRequestDto request, HttpServletRequest httpRequest);
|
||||
|
||||
/**
|
||||
* 执行已解析的命令。
|
||||
*
|
||||
* @param request 执行请求参数
|
||||
* @param httpRequest HTTP 请求(用于获取 IP 等上下文)
|
||||
* @return 执行结果
|
||||
* @throws Exception 执行异常
|
||||
*/
|
||||
Object executeCommand(AiExecuteRequestDto request, HttpServletRequest httpRequest) throws Exception;
|
||||
|
||||
/**
|
||||
* 获取 AI 助手行为记录分页列表
|
||||
*
|
||||
* @param queryParams 查询参数
|
||||
* @return 分页列表
|
||||
*/
|
||||
IPage<AiAssistantRecordVo> getRecordPage(AiAssistantPageQuery queryParams);
|
||||
|
||||
/**
|
||||
* 删除 AI 助手行为记录。
|
||||
*
|
||||
* @param ids 记录ID列表
|
||||
* @return 是否删除成功
|
||||
*/
|
||||
boolean deleteRecords(List<Long> ids);
|
||||
|
||||
/**
|
||||
* 撤销命令执行
|
||||
*
|
||||
* @param logId 记录ID
|
||||
*/
|
||||
void rollbackCommand(String logId);
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
package com.youlai.boot.platform.ai.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.youlai.boot.platform.ai.model.entity.AiCommandRecord;
|
||||
import com.youlai.boot.platform.ai.model.query.AiCommandPageQuery;
|
||||
import com.youlai.boot.platform.ai.model.vo.AiCommandRecordVO;
|
||||
|
||||
/**
|
||||
* AI 命令记录服务接口
|
||||
*/
|
||||
public interface AiCommandRecordService extends IService<AiCommandRecord> {
|
||||
|
||||
/**
|
||||
* 获取命令记录分页列表
|
||||
*
|
||||
* @param queryParams 查询参数
|
||||
* @return 命令记录分页列表
|
||||
*/
|
||||
IPage<AiCommandRecordVO> getRecordPage(AiCommandPageQuery queryParams);
|
||||
|
||||
/**
|
||||
* 撤销命令执行
|
||||
*
|
||||
* @param recordId 记录ID
|
||||
*/
|
||||
void rollbackCommand(String recordId);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
package com.youlai.boot.platform.ai.service;
|
||||
|
||||
import com.youlai.boot.platform.ai.model.dto.AiExecuteRequestDTO;
|
||||
import com.youlai.boot.platform.ai.model.dto.AiParseRequestDTO;
|
||||
import com.youlai.boot.platform.ai.model.dto.AiParseResponseDTO;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
|
||||
/**
|
||||
* AI 命令编排服务:负责对外的解析与执行编排
|
||||
*/
|
||||
public interface AiCommandService {
|
||||
|
||||
/**
|
||||
* 解析自然语言命令
|
||||
*/
|
||||
AiParseResponseDTO parseCommand(AiParseRequestDTO request, HttpServletRequest httpRequest);
|
||||
|
||||
/**
|
||||
* 执行已解析的命令
|
||||
*
|
||||
* @param request 执行请求
|
||||
* @param httpRequest HTTP 请求
|
||||
* @return 执行结果数据(成功时返回)
|
||||
* @throws Exception 执行失败时抛出异常
|
||||
*/
|
||||
Object executeCommand(AiExecuteRequestDTO request, HttpServletRequest httpRequest) throws Exception;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,321 @@
|
||||
package com.youlai.boot.platform.ai.service.impl;
|
||||
|
||||
import cn.hutool.core.lang.TypeReference;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.extra.servlet.JakartaServletUtil;
|
||||
import cn.hutool.json.JSONArray;
|
||||
import cn.hutool.json.JSONObject;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.youlai.boot.platform.ai.mapper.AiAssistantRecordMapper;
|
||||
import com.youlai.boot.platform.ai.model.dto.AiExecuteRequestDto;
|
||||
import com.youlai.boot.platform.ai.model.dto.AiFunctionCallDto;
|
||||
import com.youlai.boot.platform.ai.model.dto.AiParseRequestDto;
|
||||
import com.youlai.boot.platform.ai.model.dto.AiParseResponseDto;
|
||||
import com.youlai.boot.platform.ai.model.entity.AiAssistantRecord;
|
||||
import com.youlai.boot.platform.ai.model.query.AiAssistantPageQuery;
|
||||
import com.youlai.boot.platform.ai.model.vo.AiAssistantRecordVo;
|
||||
import com.youlai.boot.platform.ai.service.AiAssistantRecordService;
|
||||
import com.youlai.boot.platform.ai.tools.UserTools;
|
||||
import com.youlai.boot.security.util.SecurityUtils;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.ai.chat.client.ChatClient;
|
||||
import org.springframework.ai.chat.model.ChatResponse;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* AI 助手行为记录服务实现类
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 3.0.0
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class AiAssistantRecordServiceImpl
|
||||
extends ServiceImpl<AiAssistantRecordMapper, AiAssistantRecord>
|
||||
implements AiAssistantRecordService {
|
||||
|
||||
private static final String SYSTEM_PROMPT = """
|
||||
你是一个智能的企业操作助手,需要将用户的自然语言命令解析成标准的函数调用。
|
||||
请返回严格的 JSON 格式,包含字段:
|
||||
- success: boolean
|
||||
- explanation: string
|
||||
- confidence: number (0-1)
|
||||
- error: string
|
||||
- provider: string
|
||||
- model: string
|
||||
- functionCalls: 数组,每个元素包含 name、description、arguments(对象)
|
||||
当无法识别命令时,success=false,并给出 error。
|
||||
""";
|
||||
|
||||
private final UserTools userTools;
|
||||
private final ChatClient chatClient;
|
||||
|
||||
@Override
|
||||
public AiParseResponseDto parseCommand(AiParseRequestDto request, HttpServletRequest httpRequest) {
|
||||
long startTime = System.currentTimeMillis();
|
||||
String command = Optional.ofNullable(request.getCommand()).orElse("").trim();
|
||||
|
||||
if (StrUtil.isBlank(command)) {
|
||||
return AiParseResponseDto.builder()
|
||||
.success(false)
|
||||
.error("命令不能为空")
|
||||
.functionCalls(Collections.emptyList())
|
||||
.build();
|
||||
}
|
||||
|
||||
Long userId = SecurityUtils.getUserId();
|
||||
String username = SecurityUtils.getUsername();
|
||||
String ipAddress = JakartaServletUtil.getClientIP(httpRequest);
|
||||
|
||||
AiAssistantRecord commandRecord = new AiAssistantRecord();
|
||||
commandRecord.setUserId(userId);
|
||||
commandRecord.setUsername(username);
|
||||
commandRecord.setOriginalCommand(command);
|
||||
commandRecord.setIpAddress(ipAddress);
|
||||
commandRecord.setAiProvider("spring-ai");
|
||||
commandRecord.setAiModel("auto");
|
||||
|
||||
String systemPrompt = buildSystemPrompt();
|
||||
String userPrompt = buildUserPrompt(request);
|
||||
|
||||
try {
|
||||
log.info("📤 发送命令至 AI 模型: {}", command);
|
||||
ChatResponse chatResponse = chatClient.prompt()
|
||||
.system(systemPrompt)
|
||||
.user(userPrompt)
|
||||
.call().chatResponse();
|
||||
|
||||
String rawContent = Optional.ofNullable(chatResponse.getResult())
|
||||
.map(result -> result.getOutput().getText())
|
||||
.orElse("");
|
||||
|
||||
ParseResult parseResult = parseAiResponse(rawContent);
|
||||
|
||||
commandRecord.setAiProvider(StrUtil.emptyToDefault(parseResult.provider(), "spring-ai"));
|
||||
commandRecord.setAiModel(StrUtil.emptyToDefault(parseResult.model(), "auto"));
|
||||
commandRecord.setParseStatus(parseResult.success() ? 1 : 0);
|
||||
commandRecord.setExplanation(parseResult.explanation());
|
||||
commandRecord.setFunctionCalls(JSONUtil.toJsonStr(parseResult.functionCalls()));
|
||||
commandRecord.setConfidence(parseResult.confidence() != null ? BigDecimal.valueOf(parseResult.confidence()) : null);
|
||||
commandRecord.setParseErrorMessage(parseResult.success() ? null : StrUtil.emptyToDefault(parseResult.error(), "解析失败"));
|
||||
long duration = System.currentTimeMillis() - startTime;
|
||||
commandRecord.setParseDurationMs((int) duration);
|
||||
|
||||
this.save(commandRecord);
|
||||
|
||||
return AiParseResponseDto.builder()
|
||||
.parseLogId(commandRecord.getId())
|
||||
.success(parseResult.success())
|
||||
.functionCalls(parseResult.functionCalls())
|
||||
.explanation(parseResult.explanation())
|
||||
.confidence(parseResult.confidence())
|
||||
.error(parseResult.error())
|
||||
.rawResponse(rawContent)
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
long duration = System.currentTimeMillis() - startTime;
|
||||
commandRecord.setParseStatus(0);
|
||||
commandRecord.setFunctionCalls(JSONUtil.toJsonStr(Collections.emptyList()));
|
||||
commandRecord.setParseErrorMessage(e.getMessage());
|
||||
commandRecord.setParseDurationMs((int) duration);
|
||||
this.save(commandRecord);
|
||||
|
||||
log.error("❌ 解析命令失败: {}", e.getMessage(), e);
|
||||
throw new RuntimeException("解析命令失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private String buildSystemPrompt() {
|
||||
return SYSTEM_PROMPT;
|
||||
}
|
||||
|
||||
private String buildUserPrompt(AiParseRequestDto request) {
|
||||
JSONObject payload = JSONUtil.createObj()
|
||||
.set("command", request.getCommand())
|
||||
.set("currentRoute", request.getCurrentRoute())
|
||||
.set("currentComponent", request.getCurrentComponent())
|
||||
.set("context", Optional.ofNullable(request.getContext()).orElse(Collections.emptyMap()))
|
||||
.set("availableFunctions", availableFunctions());
|
||||
|
||||
return StrUtil.format("""
|
||||
请根据以下上下文识别用户意图,并输出符合系统提示要求的 JSON:
|
||||
{}
|
||||
""", JSONUtil.toJsonPrettyStr(payload));
|
||||
}
|
||||
|
||||
private List<Map<String, Object>> availableFunctions() {
|
||||
return List.of(
|
||||
Map.of(
|
||||
"name", "updateUserNickname",
|
||||
"description", "根据用户名更新用户昵称",
|
||||
"requiredParameters", List.of("username", "nickname")
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private ParseResult parseAiResponse(String rawContent) {
|
||||
if (StrUtil.isBlank(rawContent)) {
|
||||
throw new IllegalStateException("AI 返回内容为空");
|
||||
}
|
||||
|
||||
try {
|
||||
JSONObject jsonObject = JSONUtil.parseObj(rawContent);
|
||||
boolean success = jsonObject.getBool("success", false);
|
||||
String explanation = jsonObject.getStr("explanation");
|
||||
Double confidence = jsonObject.containsKey("confidence") ? jsonObject.getDouble("confidence") : null;
|
||||
String error = jsonObject.getStr("error");
|
||||
String provider = jsonObject.getStr("provider");
|
||||
String model = jsonObject.getStr("model");
|
||||
|
||||
List<AiFunctionCallDto> functionCalls = toFunctionCallList(jsonObject.getJSONArray("functionCalls"));
|
||||
|
||||
return new ParseResult(success, explanation, confidence, error, provider, model, functionCalls);
|
||||
} catch (Exception ex) {
|
||||
throw new IllegalStateException("无法解析 AI 响应: " + ex.getMessage(), ex);
|
||||
}
|
||||
}
|
||||
|
||||
private List<AiFunctionCallDto> toFunctionCallList(JSONArray array) {
|
||||
if (array == null || array.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
List<AiFunctionCallDto> result = new ArrayList<>();
|
||||
for (Object element : array) {
|
||||
JSONObject functionJson = JSONUtil.parseObj(element);
|
||||
Map<String, Object> arguments = Optional.ofNullable(functionJson.getJSONObject("arguments"))
|
||||
.map(obj -> obj.toBean(new TypeReference<Map<String, Object>>() {
|
||||
}))
|
||||
.orElse(Collections.emptyMap());
|
||||
|
||||
result.add(AiFunctionCallDto.builder()
|
||||
.name(functionJson.getStr("name"))
|
||||
.description(functionJson.getStr("description"))
|
||||
.arguments(arguments)
|
||||
.build());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private record ParseResult(
|
||||
boolean success,
|
||||
String explanation,
|
||||
Double confidence,
|
||||
String error,
|
||||
String provider,
|
||||
String model,
|
||||
List<AiFunctionCallDto> functionCalls
|
||||
) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object executeCommand(AiExecuteRequestDto request, HttpServletRequest httpRequest) throws Exception {
|
||||
Long userId = SecurityUtils.getUserId();
|
||||
String username = SecurityUtils.getUsername();
|
||||
String ipAddress = JakartaServletUtil.getClientIP(httpRequest);
|
||||
|
||||
AiFunctionCallDto functionCall = request.getFunctionCall();
|
||||
|
||||
AiAssistantRecord commandRecord;
|
||||
if (StrUtil.isNotBlank(request.getParseLogId())) {
|
||||
commandRecord = this.getById(request.getParseLogId());
|
||||
if (commandRecord == null) {
|
||||
throw new IllegalStateException("未找到对应的解析记录,ID: " + request.getParseLogId());
|
||||
}
|
||||
} else {
|
||||
commandRecord = new AiAssistantRecord();
|
||||
commandRecord.setUserId(userId);
|
||||
commandRecord.setUsername(username);
|
||||
commandRecord.setOriginalCommand(request.getOriginalCommand());
|
||||
commandRecord.setIpAddress(ipAddress);
|
||||
this.save(commandRecord);
|
||||
}
|
||||
|
||||
commandRecord.setFunctionName(functionCall.getName());
|
||||
commandRecord.setFunctionArguments(JSONUtil.toJsonStr(functionCall.getArguments()));
|
||||
commandRecord.setExecuteStatus(0);
|
||||
|
||||
try {
|
||||
Object result = executeFunctionCall(functionCall);
|
||||
commandRecord.setExecuteStatus(1);
|
||||
commandRecord.setExecuteErrorMessage(null);
|
||||
this.updateById(commandRecord);
|
||||
log.info("✅ 命令执行成功,审计记录ID: {}", commandRecord.getId());
|
||||
return result;
|
||||
} catch (Exception e) {
|
||||
commandRecord.setExecuteStatus(-1);
|
||||
commandRecord.setExecuteErrorMessage(e.getMessage());
|
||||
this.updateById(commandRecord);
|
||||
log.error("❌ 命令执行失败,审计记录ID: {}", commandRecord.getId(), e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
private Object executeFunctionCall(AiFunctionCallDto functionCall) {
|
||||
String functionName = functionCall.getName();
|
||||
Map<String, Object> arguments = functionCall.getArguments();
|
||||
|
||||
log.info("🎯 执行函数: {}, 参数: {}", functionName, arguments);
|
||||
|
||||
switch (functionName) {
|
||||
case "updateUserNickname":
|
||||
return executeUpdateUserNickname(arguments);
|
||||
default:
|
||||
throw new UnsupportedOperationException("不支持的函数: " + functionName);
|
||||
}
|
||||
}
|
||||
|
||||
private Object executeUpdateUserNickname(Map<String, Object> arguments) {
|
||||
String username = (String) arguments.get("username");
|
||||
String nickname = (String) arguments.get("nickname");
|
||||
|
||||
log.info("🔧 [Tool] 更新用户昵称: username={}, nickname={}", username, nickname);
|
||||
String resultMsg = userTools.updateUserNickname(username, nickname);
|
||||
|
||||
boolean success = resultMsg != null && resultMsg.contains("成功");
|
||||
if (!success) {
|
||||
throw new RuntimeException(resultMsg != null ? resultMsg : "更新用户昵称失败");
|
||||
}
|
||||
|
||||
return Map.of("username", username, "nickname", nickname, "message", resultMsg);
|
||||
}
|
||||
|
||||
@Override
|
||||
public IPage<AiAssistantRecordVo> getRecordPage(AiAssistantPageQuery queryParams) {
|
||||
Page<AiAssistantRecordVo> page = new Page<>(queryParams.getPageNum(), queryParams.getPageSize());
|
||||
return this.baseMapper.getRecordPage(page, queryParams);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean deleteRecords(List<Long> ids) {
|
||||
return this.removeByIds(ids);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void rollbackCommand(String logId) {
|
||||
AiAssistantRecord commandRecord = this.getById(logId);
|
||||
if (commandRecord == null) {
|
||||
throw new RuntimeException("命令记录不存在");
|
||||
}
|
||||
|
||||
if (commandRecord.getExecuteStatus() == null || commandRecord.getExecuteStatus() != 1) {
|
||||
throw new RuntimeException("只能撤销成功执行的命令");
|
||||
}
|
||||
|
||||
log.info("撤销命令执行: logId={}, function={}", logId, commandRecord.getFunctionName());
|
||||
throw new UnsupportedOperationException("回滚功能尚未实现");
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
package com.youlai.boot.platform.ai.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.youlai.boot.platform.ai.mapper.AiCommandRecordMapper;
|
||||
import com.youlai.boot.platform.ai.model.entity.AiCommandRecord;
|
||||
import com.youlai.boot.platform.ai.model.query.AiCommandPageQuery;
|
||||
import com.youlai.boot.platform.ai.model.vo.AiCommandRecordVO;
|
||||
import com.youlai.boot.platform.ai.service.AiCommandRecordService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* AI 命令记录服务实现类
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class AiCommandRecordServiceImpl extends ServiceImpl<AiCommandRecordMapper, AiCommandRecord>
|
||||
implements AiCommandRecordService {
|
||||
|
||||
@Override
|
||||
public IPage<AiCommandRecordVO> getRecordPage(AiCommandPageQuery queryParams) {
|
||||
Page<AiCommandRecordVO> page = new Page<>(queryParams.getPageNum(), queryParams.getPageSize());
|
||||
return this.baseMapper.getRecordPage(page, queryParams);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void rollbackCommand(String recordId) {
|
||||
AiCommandRecord record = this.getById(recordId);
|
||||
if (record == null) {
|
||||
throw new RuntimeException("命令记录不存在");
|
||||
}
|
||||
|
||||
if (!"success".equals(record.getExecuteStatus())) {
|
||||
throw new RuntimeException("只能撤销成功执行的命令");
|
||||
}
|
||||
|
||||
// TODO: 实现具体的回滚逻辑
|
||||
log.info("撤销命令执行: recordId={}, function={}", recordId, record.getFunctionName());
|
||||
throw new UnsupportedOperationException("回滚功能尚未实现");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,364 +0,0 @@
|
||||
package com.youlai.boot.platform.ai.service.impl;
|
||||
|
||||
import cn.hutool.core.lang.TypeReference;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.extra.servlet.JakartaServletUtil;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import cn.hutool.json.JSONArray;
|
||||
import cn.hutool.json.JSONObject;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.youlai.boot.platform.ai.model.dto.AiExecuteRequestDTO;
|
||||
import com.youlai.boot.platform.ai.model.dto.AiFunctionCallDTO;
|
||||
import com.youlai.boot.platform.ai.model.dto.AiParseRequestDTO;
|
||||
import com.youlai.boot.platform.ai.model.dto.AiParseResponseDTO;
|
||||
import com.youlai.boot.platform.ai.model.entity.AiCommandRecord;
|
||||
import com.youlai.boot.platform.ai.service.AiCommandRecordService;
|
||||
import com.youlai.boot.platform.ai.service.AiCommandService;
|
||||
import com.youlai.boot.platform.ai.tools.UserTools;
|
||||
import com.youlai.boot.security.util.SecurityUtils;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.ai.chat.client.ChatClient;
|
||||
import org.springframework.ai.chat.model.ChatResponse;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* AI 命令编排服务实现
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class AiCommandServiceImpl implements AiCommandService {
|
||||
|
||||
private static final String SYSTEM_PROMPT = """
|
||||
你是一个智能的企业操作助手,需要将用户的自然语言命令解析成标准的函数调用。
|
||||
请返回严格的 JSON 格式,包含字段:
|
||||
- success: boolean
|
||||
- explanation: string
|
||||
- confidence: number (0-1)
|
||||
- error: string
|
||||
- provider: string
|
||||
- model: string
|
||||
- functionCalls: 数组,每个元素包含 name、description、arguments(对象)
|
||||
当无法识别命令时,success=false,并给出 error。
|
||||
""";
|
||||
|
||||
private final AiCommandRecordService recordService;
|
||||
private final UserTools userTools;
|
||||
private final ChatClient chatClient;
|
||||
|
||||
@Override
|
||||
public AiParseResponseDTO parseCommand(AiParseRequestDTO request, HttpServletRequest httpRequest) {
|
||||
long startTime = System.currentTimeMillis();
|
||||
String command = Optional.ofNullable(request.getCommand()).orElse("").trim();
|
||||
|
||||
if (StrUtil.isBlank(command)) {
|
||||
return AiParseResponseDTO.builder()
|
||||
.success(false)
|
||||
.error("命令不能为空")
|
||||
.functionCalls(Collections.emptyList())
|
||||
.build();
|
||||
}
|
||||
|
||||
Long userId = SecurityUtils.getUserId();
|
||||
String username = SecurityUtils.getUsername();
|
||||
String ipAddress = JakartaServletUtil.getClientIP(httpRequest);
|
||||
|
||||
AiCommandRecord record = new AiCommandRecord();
|
||||
record.setUserId(userId);
|
||||
record.setUsername(username);
|
||||
record.setOriginalCommand(command);
|
||||
record.setIpAddress(ipAddress);
|
||||
record.setCurrentRoute(request.getCurrentRoute());
|
||||
record.setProvider("spring-ai");
|
||||
record.setModel("auto");
|
||||
|
||||
String systemPrompt = buildSystemPrompt();
|
||||
String userPrompt = buildUserPrompt(request);
|
||||
|
||||
try {
|
||||
log.info("📤 发送命令至 AI 模型: {}", command);
|
||||
ChatResponse chatResponse = chatClient.prompt()
|
||||
.system(systemPrompt)
|
||||
.user(userPrompt)
|
||||
.call().chatResponse();
|
||||
|
||||
String rawContent = Optional.ofNullable(chatResponse.getResult())
|
||||
.map(result -> result.getOutput().getText())
|
||||
.orElse("");
|
||||
|
||||
ParseResult parseResult = parseAiResponse(rawContent);
|
||||
|
||||
record.setProvider(StrUtil.emptyToDefault(parseResult.provider(), "spring-ai"));
|
||||
record.setModel(StrUtil.emptyToDefault(parseResult.model(), "auto"));
|
||||
record.setParseSuccess(parseResult.success());
|
||||
record.setExplanation(parseResult.explanation());
|
||||
record.setFunctionCalls(JSONUtil.toJsonStr(parseResult.functionCalls()));
|
||||
record.setConfidence(parseResult.confidence() != null ? BigDecimal.valueOf(parseResult.confidence()) : null);
|
||||
record.setParseErrorMessage(parseResult.success() ? null : StrUtil.emptyToDefault(parseResult.error(), "解析失败"));
|
||||
record.setParseTime(System.currentTimeMillis() - startTime);
|
||||
|
||||
recordService.save(record);
|
||||
|
||||
AiParseResponseDTO response = AiParseResponseDTO.builder()
|
||||
.parseLogId(record.getId())
|
||||
.success(parseResult.success())
|
||||
.functionCalls(parseResult.functionCalls())
|
||||
.explanation(parseResult.explanation())
|
||||
.confidence(parseResult.confidence())
|
||||
.error(parseResult.error())
|
||||
.rawResponse(rawContent)
|
||||
.build();
|
||||
|
||||
if (!parseResult.success()) {
|
||||
log.warn("❗️ AI 未能解析命令: {}", parseResult.error());
|
||||
} else {
|
||||
log.info("✅ 解析成功,审计记录ID: {}", record.getId());
|
||||
}
|
||||
|
||||
return response;
|
||||
} catch (Exception e) {
|
||||
long duration = System.currentTimeMillis() - startTime;
|
||||
record.setParseSuccess(false);
|
||||
record.setFunctionCalls(JSONUtil.toJsonStr(Collections.emptyList()));
|
||||
record.setParseErrorMessage(e.getMessage());
|
||||
record.setParseTime(duration);
|
||||
recordService.save(record);
|
||||
|
||||
log.error("❌ 解析命令失败: {}", e.getMessage(), e);
|
||||
throw new RuntimeException("解析命令失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private String buildSystemPrompt() {
|
||||
return SYSTEM_PROMPT;
|
||||
}
|
||||
|
||||
private String buildUserPrompt(AiParseRequestDTO request) {
|
||||
JSONObject payload = JSONUtil.createObj()
|
||||
.set("command", request.getCommand())
|
||||
.set("currentRoute", request.getCurrentRoute())
|
||||
.set("currentComponent", request.getCurrentComponent())
|
||||
.set("context", Optional.ofNullable(request.getContext()).orElse(Collections.emptyMap()))
|
||||
.set("availableFunctions", availableFunctions());
|
||||
|
||||
return StrUtil.format("""
|
||||
请根据以下上下文识别用户意图,并输出符合系统提示要求的 JSON:
|
||||
{}
|
||||
""", JSONUtil.toJsonPrettyStr(payload));
|
||||
}
|
||||
|
||||
private List<Map<String, Object>> availableFunctions() {
|
||||
return List.of(
|
||||
Map.of(
|
||||
"name", "updateUserNickname",
|
||||
"description", "根据用户名更新用户昵称",
|
||||
"requiredParameters", List.of("username", "nickname")
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private ParseResult parseAiResponse(String rawContent) {
|
||||
if (StrUtil.isBlank(rawContent)) {
|
||||
throw new IllegalStateException("AI 返回内容为空");
|
||||
}
|
||||
|
||||
try {
|
||||
JSONObject jsonObject = JSONUtil.parseObj(rawContent);
|
||||
boolean success = jsonObject.getBool("success", false);
|
||||
String explanation = jsonObject.getStr("explanation");
|
||||
Double confidence = jsonObject.containsKey("confidence") ? jsonObject.getDouble("confidence") : null;
|
||||
String error = jsonObject.getStr("error");
|
||||
String provider = jsonObject.getStr("provider");
|
||||
String model = jsonObject.getStr("model");
|
||||
|
||||
List<AiFunctionCallDTO> functionCalls = toFunctionCallList(jsonObject.getJSONArray("functionCalls"));
|
||||
|
||||
return new ParseResult(success, explanation, confidence, error, provider, model, functionCalls);
|
||||
} catch (Exception ex) {
|
||||
throw new IllegalStateException("无法解析 AI 响应: " + ex.getMessage(), ex);
|
||||
}
|
||||
}
|
||||
|
||||
private List<AiFunctionCallDTO> toFunctionCallList(JSONArray array) {
|
||||
if (array == null || array.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
List<AiFunctionCallDTO> result = new ArrayList<>();
|
||||
for (Object element : array) {
|
||||
JSONObject functionJson = JSONUtil.parseObj(element);
|
||||
Map<String, Object> arguments = Optional.ofNullable(functionJson.getJSONObject("arguments"))
|
||||
.map(obj -> obj.toBean(new TypeReference<Map<String, Object>>() {
|
||||
}))
|
||||
.orElse(Collections.emptyMap());
|
||||
|
||||
result.add(AiFunctionCallDTO.builder()
|
||||
.name(functionJson.getStr("name"))
|
||||
.description(functionJson.getStr("description"))
|
||||
.arguments(arguments)
|
||||
.build());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private record ParseResult(
|
||||
boolean success,
|
||||
String explanation,
|
||||
Double confidence,
|
||||
String error,
|
||||
String provider,
|
||||
String model,
|
||||
List<AiFunctionCallDTO> functionCalls
|
||||
) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object executeCommand(AiExecuteRequestDTO request, HttpServletRequest httpRequest) throws Exception {
|
||||
long startTime = System.currentTimeMillis();
|
||||
|
||||
// 获取用户信息
|
||||
Long userId = SecurityUtils.getUserId();
|
||||
String username = SecurityUtils.getUsername();
|
||||
String ipAddress = JakartaServletUtil.getClientIP(httpRequest);
|
||||
|
||||
AiFunctionCallDTO functionCall = request.getFunctionCall();
|
||||
|
||||
// 判断是否为危险操作
|
||||
boolean isDangerous = isDangerousOperation(functionCall.getName());
|
||||
|
||||
// 根据解析日志ID获取审计记录,如果不存在则创建新记录
|
||||
AiCommandRecord record;
|
||||
if (StrUtil.isNotBlank(request.getParseLogId())) {
|
||||
// 更新已存在的审计记录(解析阶段已创建)
|
||||
record = recordService.getById(request.getParseLogId());
|
||||
if (record == null) {
|
||||
throw new IllegalStateException("未找到对应的解析记录,ID: " + request.getParseLogId());
|
||||
}
|
||||
} else {
|
||||
// 如果没有解析日志ID,创建新记录(兼容直接执行的情况)
|
||||
record = new AiCommandRecord();
|
||||
record.setUserId(userId);
|
||||
record.setUsername(username);
|
||||
record.setOriginalCommand(request.getOriginalCommand());
|
||||
record.setIpAddress(ipAddress);
|
||||
record.setCurrentRoute(request.getCurrentRoute());
|
||||
recordService.save(record);
|
||||
}
|
||||
|
||||
// 更新执行相关字段
|
||||
record.setFunctionName(functionCall.getName());
|
||||
record.setFunctionArguments(JSONUtil.toJsonStr(functionCall.getArguments()));
|
||||
record.setIsDangerous(isDangerous);
|
||||
record.setRequiresConfirmation(request.getConfirmMode() != null &&
|
||||
"manual".equals(request.getConfirmMode()));
|
||||
record.setUserConfirmed(request.getUserConfirmed());
|
||||
record.setIdempotencyKey(request.getIdempotencyKey());
|
||||
record.setUserAgent(httpRequest.getHeader("User-Agent"));
|
||||
record.setExecuteStatus("pending");
|
||||
|
||||
try {
|
||||
// 幂等性检查
|
||||
if (StrUtil.isNotBlank(request.getIdempotencyKey())) {
|
||||
AiCommandRecord existing = recordService.getOne(
|
||||
new LambdaQueryWrapper<AiCommandRecord>()
|
||||
.eq(AiCommandRecord::getIdempotencyKey, request.getIdempotencyKey())
|
||||
.ne(AiCommandRecord::getId, record.getId()) // 排除当前记录
|
||||
);
|
||||
if (existing != null) {
|
||||
log.warn("⚠️ 检测到重复执行,幂等性令牌: {}", request.getIdempotencyKey());
|
||||
throw new IllegalStateException("该操作已执行,请勿重复提交");
|
||||
}
|
||||
}
|
||||
|
||||
// 🎯 执行具体的函数调用
|
||||
Object result = executeFunctionCall(functionCall);
|
||||
|
||||
// 更新执行成功
|
||||
record.setExecuteStatus("success");
|
||||
record.setExecuteResult(JSONUtil.toJsonStr(result));
|
||||
record.setExecutionTime(System.currentTimeMillis() - startTime);
|
||||
|
||||
// 更新审计记录
|
||||
recordService.updateById(record);
|
||||
|
||||
log.info("✅ 命令执行成功,审计记录ID: {}", record.getId());
|
||||
|
||||
return result;
|
||||
|
||||
} catch (Exception e) {
|
||||
// 更新执行失败
|
||||
record.setExecuteStatus("failed");
|
||||
record.setExecuteErrorMessage(e.getMessage());
|
||||
record.setExecutionTime(System.currentTimeMillis() - startTime);
|
||||
|
||||
// 更新审计记录
|
||||
recordService.updateById(record);
|
||||
|
||||
log.error("❌ 命令执行失败,审计记录ID: {}", record.getId(), e);
|
||||
|
||||
// 抛出异常,由 Controller 统一处理
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为危险操作
|
||||
*/
|
||||
private boolean isDangerousOperation(String functionName) {
|
||||
String[] dangerousKeywords = {"delete", "remove", "drop", "truncate", "clear"};
|
||||
String lowerName = functionName.toLowerCase();
|
||||
for (String keyword : dangerousKeywords) {
|
||||
if (lowerName.contains(keyword)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行具体的函数调用
|
||||
*/
|
||||
private Object executeFunctionCall(AiFunctionCallDTO functionCall) {
|
||||
String functionName = functionCall.getName();
|
||||
Map<String, Object> arguments = functionCall.getArguments();
|
||||
|
||||
log.info("🎯 执行函数: {}, 参数: {}", functionName, arguments);
|
||||
|
||||
// 根据函数名称路由到不同的处理器
|
||||
switch (functionName) {
|
||||
case "updateUserNickname":
|
||||
return executeUpdateUserNickname(arguments);
|
||||
default:
|
||||
throw new UnsupportedOperationException("不支持的函数: " + functionName);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用 Tool: 根据用户名更新用户昵称
|
||||
*/
|
||||
private Object executeUpdateUserNickname(Map<String, Object> arguments) {
|
||||
String username = (String) arguments.get("username");
|
||||
String nickname = (String) arguments.get("nickname");
|
||||
|
||||
log.info("🔧 [Tool] 更新用户昵称: username={}, nickname={}", username, nickname);
|
||||
String resultMsg = userTools.updateUserNickname(username, nickname);
|
||||
|
||||
boolean success = resultMsg != null && resultMsg.contains("成功");
|
||||
if (!success) {
|
||||
throw new RuntimeException(resultMsg != null ? resultMsg : "更新用户昵称失败");
|
||||
}
|
||||
|
||||
return Map.of("username", username, "nickname", nickname, "message", resultMsg);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,28 +17,28 @@ import org.springframework.ai.tool.annotation.ToolParam;
|
||||
@RequiredArgsConstructor
|
||||
public class UserTools {
|
||||
|
||||
private final UserService userService;
|
||||
private final UserService userService;
|
||||
|
||||
@Tool(description = "根据关键字在用户列表中筛选用户")
|
||||
public String queryUser(
|
||||
@ToolParam(description = "搜索关键字,用于在列表中搜索筛选") String keywords
|
||||
) {
|
||||
// 返回搜索关键字,前端会在用户列表页面进行筛选
|
||||
return "将在用户列表中搜索:" + keywords;
|
||||
}
|
||||
@Tool(description = "根据关键字在用户列表中筛选用户")
|
||||
public String queryUser(
|
||||
@ToolParam(description = "搜索关键字,用于在列表中搜索筛选") String keywords
|
||||
) {
|
||||
// 返回搜索关键字,前端会在用户列表页面进行筛选
|
||||
return "将在用户列表中搜索:" + keywords;
|
||||
}
|
||||
|
||||
@Tool(description = "根据用户名更新用户昵称")
|
||||
public String updateUserNickname(
|
||||
@ToolParam(description = "用户名") String username,
|
||||
@ToolParam(description = "新的昵称") String nickname
|
||||
) {
|
||||
@Tool(description = "根据用户名更新用户昵称")
|
||||
public String updateUserNickname(
|
||||
@ToolParam(description = "用户名") String username,
|
||||
@ToolParam(description = "新的昵称") String nickname
|
||||
) {
|
||||
|
||||
boolean ok = userService.update(new LambdaUpdateWrapper<User>()
|
||||
.eq(User::getUsername, username)
|
||||
.set(User::getNickname, nickname)
|
||||
);
|
||||
return ok ? "用户昵称更新成功" : "用户昵称更新失败";
|
||||
}
|
||||
boolean ok = userService.update(new LambdaUpdateWrapper<User>()
|
||||
.eq(User::getUsername, username)
|
||||
.set(User::getNickname, nickname)
|
||||
);
|
||||
return ok ? "用户昵称更新成功" : "用户昵称更新失败";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -8,10 +8,10 @@ import com.youlai.boot.common.enums.LogModuleEnum;
|
||||
import com.youlai.boot.platform.codegen.service.CodegenService;
|
||||
import com.youlai.boot.platform.codegen.model.form.GenConfigForm;
|
||||
import com.youlai.boot.platform.codegen.model.query.TablePageQuery;
|
||||
import com.youlai.boot.platform.codegen.model.vo.CodegenPreviewVO;
|
||||
import com.youlai.boot.platform.codegen.model.vo.TablePageVO;
|
||||
import com.youlai.boot.platform.codegen.model.vo.CodegenPreviewVo;
|
||||
import com.youlai.boot.platform.codegen.model.vo.TablePageVo;
|
||||
import com.youlai.boot.common.annotation.Log;
|
||||
import com.youlai.boot.platform.codegen.service.GenConfigService;
|
||||
import com.youlai.boot.platform.codegen.service.GenTableService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
@@ -40,25 +40,25 @@ import java.util.List;
|
||||
public class CodegenController {
|
||||
|
||||
private final CodegenService codegenService;
|
||||
private final GenConfigService genConfigService;
|
||||
private final GenTableService genTableService;
|
||||
private final CodegenProperties codegenProperties;
|
||||
|
||||
@Operation(summary = "获取数据表分页列表")
|
||||
@GetMapping("/table/page")
|
||||
@Log(value = "代码生成分页列表", module = LogModuleEnum.OTHER)
|
||||
public PageResult<TablePageVO> getTablePage(
|
||||
public PageResult<TablePageVo> getTablePage(
|
||||
TablePageQuery queryParams
|
||||
) {
|
||||
Page<TablePageVO> result = codegenService.getTablePage(queryParams);
|
||||
Page<TablePageVo> result = codegenService.getTablePage(queryParams);
|
||||
return PageResult.success(result);
|
||||
}
|
||||
|
||||
@Operation(summary = "获取代码生成配置")
|
||||
@GetMapping("/{tableName}/config")
|
||||
public Result<GenConfigForm> getGenConfigFormData(
|
||||
public Result<GenConfigForm> getGenTableFormData(
|
||||
@Parameter(description = "表名", example = "sys_user") @PathVariable String tableName
|
||||
) {
|
||||
GenConfigForm formData = genConfigService.getGenConfigFormData(tableName);
|
||||
GenConfigForm formData = genTableService.getGenTableFormData(tableName);
|
||||
return Result.success(formData);
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ public class CodegenController {
|
||||
@PostMapping("/{tableName}/config")
|
||||
@Log(value = "生成代码", module = LogModuleEnum.OTHER)
|
||||
public Result<?> saveGenConfig(@RequestBody GenConfigForm formData) {
|
||||
genConfigService.saveGenConfig(formData);
|
||||
genTableService.saveGenConfig(formData);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@@ -75,16 +75,16 @@ public class CodegenController {
|
||||
public Result<?> deleteGenConfig(
|
||||
@Parameter(description = "表名", example = "sys_user") @PathVariable String tableName
|
||||
) {
|
||||
genConfigService.deleteGenConfig(tableName);
|
||||
genTableService.deleteGenConfig(tableName);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@Operation(summary = "获取预览生成代码")
|
||||
@GetMapping("/{tableName}/preview")
|
||||
@Log(value = "预览生成代码", module = LogModuleEnum.OTHER)
|
||||
public Result<List<CodegenPreviewVO>> getTablePreviewData(@PathVariable String tableName,
|
||||
public Result<List<CodegenPreviewVo>> getTablePreviewData(@PathVariable String tableName,
|
||||
@RequestParam(value = "pageType", required = false, defaultValue = "classic") String pageType) {
|
||||
List<CodegenPreviewVO> list = codegenService.getCodegenPreviewData(tableName, pageType);
|
||||
List<CodegenPreviewVo> list = codegenService.getCodegenPreviewData(tableName, pageType);
|
||||
return Result.success(list);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.youlai.boot.platform.codegen.converter;
|
||||
|
||||
import com.youlai.boot.platform.codegen.model.entity.GenConfig;
|
||||
import com.youlai.boot.platform.codegen.model.entity.GenFieldConfig;
|
||||
import com.youlai.boot.platform.codegen.model.entity.GenTable;
|
||||
import com.youlai.boot.platform.codegen.model.entity.GenTableColumn;
|
||||
import com.youlai.boot.platform.codegen.model.form.GenConfigForm;
|
||||
import org.mapstruct.Mapper;
|
||||
import org.mapstruct.Mapping;
|
||||
@@ -17,25 +17,25 @@ import java.util.List;
|
||||
@Mapper(componentModel = "spring")
|
||||
public interface CodegenConverter {
|
||||
|
||||
@Mapping(source = "genConfig.tableName", target = "tableName")
|
||||
@Mapping(source = "genConfig.businessName", target = "businessName")
|
||||
@Mapping(source = "genConfig.moduleName", target = "moduleName")
|
||||
@Mapping(source = "genConfig.packageName", target = "packageName")
|
||||
@Mapping(source = "genConfig.entityName", target = "entityName")
|
||||
@Mapping(source = "genConfig.author", target = "author")
|
||||
@Mapping(source = "genConfig.pageType", target = "pageType")
|
||||
@Mapping(source = "genConfig.removeTablePrefix", target = "removeTablePrefix")
|
||||
@Mapping(source = "genTable.tableName", target = "tableName")
|
||||
@Mapping(source = "genTable.businessName", target = "businessName")
|
||||
@Mapping(source = "genTable.moduleName", target = "moduleName")
|
||||
@Mapping(source = "genTable.packageName", target = "packageName")
|
||||
@Mapping(source = "genTable.entityName", target = "entityName")
|
||||
@Mapping(source = "genTable.author", target = "author")
|
||||
@Mapping(source = "genTable.pageType", target = "pageType")
|
||||
@Mapping(source = "genTable.removeTablePrefix", target = "removeTablePrefix")
|
||||
@Mapping(source = "fieldConfigs", target = "fieldConfigs")
|
||||
GenConfigForm toGenConfigForm(GenConfig genConfig, List<GenFieldConfig> fieldConfigs);
|
||||
GenConfigForm toGenConfigForm(GenTable genTable, List<GenTableColumn> fieldConfigs);
|
||||
|
||||
List<GenConfigForm.FieldConfig> toGenFieldConfigForm(List<GenFieldConfig> fieldConfigs);
|
||||
List<GenConfigForm.FieldConfig> toGenTableColumnForm(List<GenTableColumn> fieldConfigs);
|
||||
|
||||
GenConfigForm.FieldConfig toGenFieldConfigForm(GenFieldConfig genFieldConfig);
|
||||
GenConfigForm.FieldConfig toGenTableColumnForm(GenTableColumn genTableColumn);
|
||||
|
||||
GenConfig toGenConfig(GenConfigForm formData);
|
||||
GenTable toGenTable(GenConfigForm formData);
|
||||
|
||||
List<GenFieldConfig> toGenFieldConfig(List<GenConfigForm.FieldConfig> fieldConfigs);
|
||||
List<GenTableColumn> toGenTableColumn(List<GenConfigForm.FieldConfig> fieldConfigs);
|
||||
|
||||
GenFieldConfig toGenFieldConfig(GenConfigForm.FieldConfig fieldConfig);
|
||||
GenTableColumn toGenTableColumn(GenConfigForm.FieldConfig fieldConfig);
|
||||
|
||||
}
|
||||
@@ -27,9 +27,11 @@ public enum JavaTypeEnum {
|
||||
FLOAT("float", "Float", "number"),
|
||||
DOUBLE("double", "Double", "number"),
|
||||
DECIMAL("decimal", "BigDecimal", "number"),
|
||||
DATE("date", "LocalDate", "Date"),
|
||||
DATETIME("datetime", "LocalDateTime", "Date"),
|
||||
TIMESTAMP("timestamp", "LocalDateTime", "Date");
|
||||
DATE("date", "LocalDate", "string"),
|
||||
DATETIME("datetime", "LocalDateTime", "string"),
|
||||
TIMESTAMP("timestamp", "LocalDateTime", "string"),
|
||||
BOOLEAN("boolean", "Boolean", "boolean"),
|
||||
BIT("bit", "Boolean", "boolean");
|
||||
|
||||
// 数据库类型
|
||||
private final String dbType;
|
||||
@@ -61,11 +63,12 @@ public enum JavaTypeEnum {
|
||||
* @return 对应的Java类型
|
||||
*/
|
||||
public static String getJavaTypeByColumnType(String columnType) {
|
||||
JavaTypeEnum javaTypeEnum = typeMap.get(columnType);
|
||||
String normalized = normalizeColumnType(columnType);
|
||||
JavaTypeEnum javaTypeEnum = typeMap.get(normalized);
|
||||
if (javaTypeEnum != null) {
|
||||
return javaTypeEnum.getJavaType();
|
||||
}
|
||||
return null;
|
||||
return "String";
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -75,11 +78,31 @@ public enum JavaTypeEnum {
|
||||
* @return 对应的TypeScript类型
|
||||
*/
|
||||
public static String getTsTypeByJavaType(String javaType) {
|
||||
if (javaType == null) {
|
||||
return "any";
|
||||
}
|
||||
for (JavaTypeEnum javaTypeEnum : JavaTypeEnum.values()) {
|
||||
if (javaTypeEnum.getJavaType().equals(javaType)) {
|
||||
return javaTypeEnum.getTsType();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
return "any";
|
||||
}
|
||||
|
||||
private static String normalizeColumnType(String columnType) {
|
||||
if (columnType == null) {
|
||||
return "";
|
||||
}
|
||||
// Handle values like: varchar(255), bigint unsigned, INT
|
||||
String normalized = columnType.trim().toLowerCase();
|
||||
int parenIndex = normalized.indexOf('(');
|
||||
if (parenIndex > -1) {
|
||||
normalized = normalized.substring(0, parenIndex);
|
||||
}
|
||||
// Remove modifiers
|
||||
normalized = normalized.replace("unsigned", "").replace("zerofill", "").trim();
|
||||
// Collapse repeated spaces
|
||||
normalized = normalized.replaceAll("\\s+", " ");
|
||||
return normalized;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.youlai.boot.platform.codegen.model.bo.ColumnMetaData;
|
||||
import com.youlai.boot.platform.codegen.model.bo.TableMetaData;
|
||||
import com.youlai.boot.platform.codegen.model.query.TablePageQuery;
|
||||
import com.youlai.boot.platform.codegen.model.vo.TablePageVO;
|
||||
import com.youlai.boot.platform.codegen.model.vo.TablePageVo;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
import java.util.List;
|
||||
@@ -27,7 +27,7 @@ public interface DatabaseMapper extends BaseMapper {
|
||||
* @param queryParams
|
||||
* @return
|
||||
*/
|
||||
Page<TablePageVO> getTablePage(Page<TablePageVO> page, TablePageQuery queryParams);
|
||||
Page<TablePageVo> getTablePage(Page<TablePageVo> page, TablePageQuery queryParams);
|
||||
|
||||
/**
|
||||
* 获取表字段列表
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
package com.youlai.boot.platform.codegen.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.youlai.boot.platform.codegen.model.entity.GenFieldConfig;
|
||||
import com.youlai.boot.platform.codegen.model.entity.GenTableColumn;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 代码生成字段配置访问层
|
||||
* 代码生成表字段配置访问层
|
||||
*
|
||||
* @author Ray
|
||||
* @since 2.10.0
|
||||
*/
|
||||
@Mapper
|
||||
public interface GenFieldConfigMapper extends BaseMapper<GenFieldConfig> {
|
||||
public interface GenTableColumnMapper extends BaseMapper<GenTableColumn> {
|
||||
|
||||
}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
package com.youlai.boot.platform.codegen.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.youlai.boot.platform.codegen.model.entity.GenConfig;
|
||||
import com.youlai.boot.platform.codegen.model.entity.GenTable;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 代码生成基础配置访问层
|
||||
* 代码生成表配置访问层
|
||||
*
|
||||
* @author Ray
|
||||
* @since 2.10.0
|
||||
*/
|
||||
@Mapper
|
||||
public interface GenConfigMapper extends BaseMapper<GenConfig> {
|
||||
public interface GenTableMapper extends BaseMapper<GenTable> {
|
||||
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ package com.youlai.boot.platform.codegen.model.bo;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
@Schema(description = "数据表字段VO")
|
||||
@Schema(description = "数据表字段Vo")
|
||||
@Data
|
||||
public class ColumnMetaData {
|
||||
|
||||
|
||||
@@ -7,15 +7,15 @@ import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
/**
|
||||
* 代码生成基础配置
|
||||
* 代码生成表配置
|
||||
*
|
||||
* @author Ray
|
||||
* @since 2.10.0
|
||||
*/
|
||||
@TableName(value = "gen_config")
|
||||
@TableName(value = "gen_table")
|
||||
@Getter
|
||||
@Setter
|
||||
public class GenConfig extends BaseEntity {
|
||||
public class GenTable extends BaseEntity {
|
||||
|
||||
/**
|
||||
* 表名
|
||||
@@ -61,4 +61,5 @@ public class GenConfig extends BaseEntity {
|
||||
* 要移除的表前缀,如: sys_
|
||||
*/
|
||||
private String removeTablePrefix;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,21 +11,21 @@ import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
/**
|
||||
* 字段生成配置实体
|
||||
* 代码生成表字段配置实体
|
||||
*
|
||||
* @author Ray
|
||||
* @since 2.10.0
|
||||
*/
|
||||
@TableName(value = "gen_field_config")
|
||||
@TableName(value = "gen_table_column")
|
||||
@Getter
|
||||
@Setter
|
||||
public class GenFieldConfig extends BaseEntity {
|
||||
public class GenTableColumn extends BaseEntity {
|
||||
|
||||
|
||||
/**
|
||||
* 关联的配置ID
|
||||
* 关联的表配置ID
|
||||
*/
|
||||
private Long configId;
|
||||
private Long tableId;
|
||||
|
||||
/**
|
||||
* 列名
|
||||
@@ -104,3 +104,4 @@ public class GenFieldConfig extends BaseEntity {
|
||||
*/
|
||||
private String dictType;
|
||||
}
|
||||
|
||||
@@ -3,9 +3,9 @@ package com.youlai.boot.platform.codegen.model.vo;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
@Schema(description = "代码生成代码预览VO")
|
||||
@Schema(description = "代码生成代码预览Vo")
|
||||
@Data
|
||||
public class CodegenPreviewVO {
|
||||
public class CodegenPreviewVo {
|
||||
|
||||
@Schema(description = "生成文件路径")
|
||||
private String path;
|
||||
|
||||
@@ -6,7 +6,7 @@ import lombok.Data;
|
||||
|
||||
@Schema(description = "表视图对象")
|
||||
@Data
|
||||
public class TablePageVO {
|
||||
public class TablePageVo {
|
||||
|
||||
@Schema(description = "表名称", example = "sys_user")
|
||||
private String tableName;
|
||||
|
||||
@@ -2,8 +2,8 @@ package com.youlai.boot.platform.codegen.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.youlai.boot.platform.codegen.model.query.TablePageQuery;
|
||||
import com.youlai.boot.platform.codegen.model.vo.CodegenPreviewVO;
|
||||
import com.youlai.boot.platform.codegen.model.vo.TablePageVO;
|
||||
import com.youlai.boot.platform.codegen.model.vo.CodegenPreviewVo;
|
||||
import com.youlai.boot.platform.codegen.model.vo.TablePageVo;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@@ -21,7 +21,7 @@ public interface CodegenService {
|
||||
* @param queryParams 查询参数
|
||||
* @return
|
||||
*/
|
||||
Page<TablePageVO> getTablePage(TablePageQuery queryParams);
|
||||
Page<TablePageVo> getTablePage(TablePageQuery queryParams);
|
||||
|
||||
/**
|
||||
* 获取预览生成代码
|
||||
@@ -29,7 +29,7 @@ public interface CodegenService {
|
||||
* @param tableName 表名
|
||||
* @return
|
||||
*/
|
||||
List<CodegenPreviewVO> getCodegenPreviewData(String tableName, String pageType);
|
||||
List<CodegenPreviewVo> getCodegenPreviewData(String tableName, String pageType);
|
||||
|
||||
/**
|
||||
* 下载代码
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.youlai.boot.platform.codegen.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.youlai.boot.platform.codegen.model.entity.GenFieldConfig;
|
||||
import com.youlai.boot.platform.codegen.model.entity.GenTableColumn;
|
||||
|
||||
/**
|
||||
* 代码生成配置接口
|
||||
@@ -9,6 +9,6 @@ import com.youlai.boot.platform.codegen.model.entity.GenFieldConfig;
|
||||
* @author Ray
|
||||
* @since 2.10.0
|
||||
*/
|
||||
public interface GenFieldConfigService extends IService<GenFieldConfig> {
|
||||
public interface GenTableColumnService extends IService<GenTableColumn> {
|
||||
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.youlai.boot.platform.codegen.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.youlai.boot.platform.codegen.model.entity.GenConfig;
|
||||
import com.youlai.boot.platform.codegen.model.entity.GenTable;
|
||||
import com.youlai.boot.platform.codegen.model.form.GenConfigForm;
|
||||
|
||||
/**
|
||||
@@ -10,7 +10,7 @@ import com.youlai.boot.platform.codegen.model.form.GenConfigForm;
|
||||
* @author Ray
|
||||
* @since 2.10.0
|
||||
*/
|
||||
public interface GenConfigService extends IService<GenConfig> {
|
||||
public interface GenTableService extends IService<GenTable> {
|
||||
|
||||
/**
|
||||
* 获取代码生成配置
|
||||
@@ -18,7 +18,7 @@ public interface GenConfigService extends IService<GenConfig> {
|
||||
* @param tableName 表名
|
||||
* @return
|
||||
*/
|
||||
GenConfigForm getGenConfigFormData(String tableName);
|
||||
GenConfigForm getGenTableFormData(String tableName);
|
||||
|
||||
/**
|
||||
* 保存代码生成配置
|
||||
@@ -13,16 +13,16 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.youlai.boot.platform.codegen.enums.JavaTypeEnum;
|
||||
import com.youlai.boot.config.property.CodegenProperties;
|
||||
import com.youlai.boot.platform.codegen.service.GenConfigService;
|
||||
import com.youlai.boot.platform.codegen.service.GenFieldConfigService;
|
||||
import com.youlai.boot.platform.codegen.service.GenTableService;
|
||||
import com.youlai.boot.platform.codegen.service.GenTableColumnService;
|
||||
import com.youlai.boot.platform.codegen.service.CodegenService;
|
||||
import com.youlai.boot.core.exception.BusinessException;
|
||||
import com.youlai.boot.platform.codegen.mapper.DatabaseMapper;
|
||||
import com.youlai.boot.platform.codegen.model.entity.GenConfig;
|
||||
import com.youlai.boot.platform.codegen.model.entity.GenFieldConfig;
|
||||
import com.youlai.boot.platform.codegen.model.entity.GenTable;
|
||||
import com.youlai.boot.platform.codegen.model.entity.GenTableColumn;
|
||||
import com.youlai.boot.platform.codegen.model.query.TablePageQuery;
|
||||
import com.youlai.boot.platform.codegen.model.vo.CodegenPreviewVO;
|
||||
import com.youlai.boot.platform.codegen.model.vo.TablePageVO;
|
||||
import com.youlai.boot.platform.codegen.model.vo.CodegenPreviewVo;
|
||||
import com.youlai.boot.platform.codegen.model.vo.TablePageVo;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -36,7 +36,11 @@ import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
/**
|
||||
* 数据库服务实现类
|
||||
* 代码生成服务实现类。
|
||||
*
|
||||
* <p>
|
||||
* 根据代码生成配置({@link CodegenProperties})与表/字段元数据,渲染模板并提供预览与下载能力。
|
||||
* </p>
|
||||
*
|
||||
* @author Ray
|
||||
* @since 2.10.0
|
||||
@@ -48,8 +52,8 @@ public class CodegenServiceImpl implements CodegenService {
|
||||
|
||||
private final DatabaseMapper databaseMapper;
|
||||
private final CodegenProperties codegenProperties;
|
||||
private final GenConfigService genConfigService;
|
||||
private final GenFieldConfigService genFieldConfigService;
|
||||
private final GenTableService genTableService;
|
||||
private final GenTableColumnService genTableColumnService;
|
||||
|
||||
/**
|
||||
* 数据表分页列表
|
||||
@@ -57,8 +61,8 @@ public class CodegenServiceImpl implements CodegenService {
|
||||
* @param queryParams 查询参数
|
||||
* @return 分页结果
|
||||
*/
|
||||
public Page<TablePageVO> getTablePage(TablePageQuery queryParams) {
|
||||
Page<TablePageVO> page = new Page<>(queryParams.getPageNum(), queryParams.getPageSize());
|
||||
public Page<TablePageVo> getTablePage(TablePageQuery queryParams) {
|
||||
Page<TablePageVo> page = new Page<>(queryParams.getPageNum(), queryParams.getPageSize());
|
||||
// 设置排除的表
|
||||
List<String> excludeTables = codegenProperties.getExcludeTables();
|
||||
queryParams.setExcludeTables(excludeTables);
|
||||
@@ -73,20 +77,20 @@ public class CodegenServiceImpl implements CodegenService {
|
||||
* @return 预览数据
|
||||
*/
|
||||
@Override
|
||||
public List<CodegenPreviewVO> getCodegenPreviewData(String tableName, String pageType) {
|
||||
public List<CodegenPreviewVo> getCodegenPreviewData(String tableName, String pageType) {
|
||||
|
||||
List<CodegenPreviewVO> list = new ArrayList<>();
|
||||
List<CodegenPreviewVo> list = new ArrayList<>();
|
||||
|
||||
GenConfig genConfig = genConfigService.getOne(new LambdaQueryWrapper<GenConfig>()
|
||||
.eq(GenConfig::getTableName, tableName)
|
||||
GenTable genTable = genTableService.getOne(new LambdaQueryWrapper<GenTable>()
|
||||
.eq(GenTable::getTableName, tableName)
|
||||
);
|
||||
if (genConfig == null) {
|
||||
if (genTable == null) {
|
||||
throw new BusinessException("未找到表生成配置");
|
||||
}
|
||||
|
||||
List<GenFieldConfig> fieldConfigs = genFieldConfigService.list(new LambdaQueryWrapper<GenFieldConfig>()
|
||||
.eq(GenFieldConfig::getConfigId, genConfig.getId())
|
||||
.orderByAsc(GenFieldConfig::getFieldSort)
|
||||
List<GenTableColumn> fieldConfigs = genTableColumnService.list(new LambdaQueryWrapper<GenTableColumn>()
|
||||
.eq(GenTableColumn::getTableId, genTable.getId())
|
||||
.orderByAsc(GenTableColumn::getFieldSort)
|
||||
|
||||
);
|
||||
if (CollectionUtil.isEmpty(fieldConfigs)) {
|
||||
@@ -96,13 +100,13 @@ public class CodegenServiceImpl implements CodegenService {
|
||||
// 遍历模板配置
|
||||
Map<String, CodegenProperties.TemplateConfig> templateConfigs = codegenProperties.getTemplateConfigs();
|
||||
for (Map.Entry<String, CodegenProperties.TemplateConfig> templateConfigEntry : templateConfigs.entrySet()) {
|
||||
CodegenPreviewVO previewVO = new CodegenPreviewVO();
|
||||
CodegenPreviewVo previewVo = new CodegenPreviewVo();
|
||||
|
||||
CodegenProperties.TemplateConfig templateConfig = templateConfigEntry.getValue();
|
||||
|
||||
/* 1. 生成文件名 UserController */
|
||||
// User Role Menu Dept
|
||||
String entityName = genConfig.getEntityName();
|
||||
String entityName = genTable.getEntityName();
|
||||
// Controller Service Mapper Entity
|
||||
String templateName = templateConfigEntry.getKey();
|
||||
// .java .ts .vue
|
||||
@@ -110,37 +114,39 @@ public class CodegenServiceImpl implements CodegenService {
|
||||
|
||||
// 文件名 UserController.java
|
||||
String fileName = getFileName(entityName, templateName, extension);
|
||||
previewVO.setFileName(fileName);
|
||||
previewVo.setFileName(fileName);
|
||||
|
||||
/* 2. 生成文件路径 */
|
||||
// 包名:com.youlai.boot
|
||||
String packageName = genConfig.getPackageName();
|
||||
String packageName = genTable.getPackageName();
|
||||
// 模块名:system
|
||||
String moduleName = genConfig.getModuleName();
|
||||
String moduleName = genTable.getModuleName();
|
||||
// 子包名:controller
|
||||
String subpackageName = templateConfig.getSubpackageName();
|
||||
// 组合成文件路径:src/main/java/com/youlai/boot/system/controller
|
||||
String filePath = getFilePath(templateName, moduleName, packageName, subpackageName, entityName);
|
||||
previewVO.setPath(filePath);
|
||||
previewVo.setPath(filePath);
|
||||
|
||||
/* 3. 生成文件内容 */
|
||||
// 将模板文件中的变量替换为具体的值 生成代码内容
|
||||
// 优先使用保存的 ui,没有则使用请求参数
|
||||
String finalType = StrUtil.blankToDefault(genConfig.getPageType(), pageType);
|
||||
String content = getCodeContent(templateConfig, genConfig, fieldConfigs, finalType);
|
||||
previewVO.setContent(content);
|
||||
String finalType = StrUtil.blankToDefault(genTable.getPageType(), pageType);
|
||||
String content = getCodeContent(templateConfig, genTable, fieldConfigs, finalType);
|
||||
previewVo.setContent(content);
|
||||
|
||||
list.add(previewVO);
|
||||
list.add(previewVo);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成文件名
|
||||
* 生成文件名。
|
||||
*
|
||||
* @param entityName 实体类名 UserController
|
||||
* @param templateName 模板名 Entity
|
||||
* @param extension 文件后缀 .java
|
||||
* <p>部分模板需要使用约定的命名规则(例如前端 API 文件)。</p>
|
||||
*
|
||||
* @param entityName 实体名(例如 User)
|
||||
* @param templateName 模板名(例如 Entity、Controller、API)
|
||||
* @param extension 文件后缀(例如 .java、.ts)
|
||||
* @return 文件名
|
||||
*/
|
||||
private String getFileName(String entityName, String templateName, String extension) {
|
||||
@@ -149,8 +155,11 @@ public class CodegenServiceImpl implements CodegenService {
|
||||
} else if ("MapperXml".equals(templateName)) {
|
||||
return entityName + "Mapper" + extension;
|
||||
} else if ("API".equals(templateName)) {
|
||||
// 生成 user-api.ts 命名
|
||||
return StrUtil.toSymbolCase(entityName, '-') + "-api" + extension;
|
||||
// 生成 user.ts 命名
|
||||
return StrUtil.toSymbolCase(entityName, '-') + extension;
|
||||
} else if ("API_TYPES".equals(templateName)) {
|
||||
// 生成 types/api/user.ts
|
||||
return StrUtil.toSymbolCase(entityName, '-') + extension;
|
||||
} else if ("VIEW".equals(templateName)) {
|
||||
return "index.vue";
|
||||
}
|
||||
@@ -158,14 +167,14 @@ public class CodegenServiceImpl implements CodegenService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成文件路径
|
||||
* 生成文件路径。
|
||||
*
|
||||
* @param templateName 模板名 Entity
|
||||
* @param moduleName 模块名 system
|
||||
* @param packageName 包名 com.youlai
|
||||
* @param subPackageName 子包名 controller
|
||||
* @param entityName 实体类名 UserController
|
||||
* @return 文件路径 src/main/java/com/youlai/system/controller
|
||||
* @param templateName 模板名
|
||||
* @param moduleName 模块名(例如 system)
|
||||
* @param packageName 包名(例如 com.youlai.boot)
|
||||
* @param subPackageName 子包名(例如 controller、service.impl、api、views)
|
||||
* @param entityName 实体名(例如 User)
|
||||
* @return 生成文件路径
|
||||
*/
|
||||
private String getFilePath(String templateName, String moduleName, String packageName, String subPackageName, String entityName) {
|
||||
String path;
|
||||
@@ -183,6 +192,13 @@ public class CodegenServiceImpl implements CodegenService {
|
||||
+ File.separator + subPackageName
|
||||
+ File.separator + moduleName
|
||||
);
|
||||
} else if ("API_TYPES".equals(templateName)) {
|
||||
// path = "src/types/api";
|
||||
path = (codegenProperties.getFrontendAppName()
|
||||
+ File.separator + "src"
|
||||
+ File.separator + "types"
|
||||
+ File.separator + "api"
|
||||
);
|
||||
} else if ("VIEW".equals(templateName)) {
|
||||
// path = "src/views/system/user";
|
||||
path = (codegenProperties.getFrontendAppName()
|
||||
@@ -208,38 +224,47 @@ public class CodegenServiceImpl implements CodegenService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成代码内容
|
||||
* 渲染模板,生成代码内容。
|
||||
*
|
||||
* @param templateConfig 模板配置
|
||||
* @param genConfig 生成配置
|
||||
* @param genTable 表生成配置
|
||||
* @param fieldConfigs 字段配置
|
||||
* @return 代码内容
|
||||
* @param pageType 前端页面类型
|
||||
* @return 渲染后的代码内容
|
||||
*/
|
||||
private String getCodeContent(CodegenProperties.TemplateConfig templateConfig, GenConfig genConfig, List<GenFieldConfig> fieldConfigs, String pageType) {
|
||||
private String getCodeContent(CodegenProperties.TemplateConfig templateConfig, GenTable genTable, List<GenTableColumn> fieldConfigs, String pageType) {
|
||||
|
||||
Map<String, Object> bindMap = new HashMap<>();
|
||||
|
||||
String entityName = genConfig.getEntityName();
|
||||
String entityName = genTable.getEntityName();
|
||||
|
||||
bindMap.put("packageName", genConfig.getPackageName());
|
||||
bindMap.put("moduleName", genConfig.getModuleName());
|
||||
bindMap.put("packageName", genTable.getPackageName());
|
||||
bindMap.put("moduleName", genTable.getModuleName());
|
||||
bindMap.put("subpackageName", templateConfig.getSubpackageName());
|
||||
bindMap.put("date", DateUtil.format(new Date(), "yyyy-MM-dd HH:mm"));
|
||||
bindMap.put("entityName", entityName);
|
||||
bindMap.put("tableName", genConfig.getTableName());
|
||||
bindMap.put("author", genConfig.getAuthor());
|
||||
bindMap.put("lowerFirstEntityName", StrUtil.lowerFirst(entityName)); // UserTest → userTest
|
||||
bindMap.put("kebabCaseEntityName", StrUtil.toSymbolCase(entityName, '-')); // UserTest → user-test
|
||||
bindMap.put("businessName", genConfig.getBusinessName());
|
||||
bindMap.put("tableName", genTable.getTableName());
|
||||
bindMap.put("author", genTable.getAuthor());
|
||||
String entityLowerCamel = StrUtil.lowerFirst(entityName);
|
||||
String entityKebab = StrUtil.toSymbolCase(entityName, '-');
|
||||
String entityUpperSnake = StrUtil.toSymbolCase(entityName, '_').toUpperCase();
|
||||
bindMap.put("entityLowerCamel", entityLowerCamel);
|
||||
bindMap.put("entityKebab", entityKebab);
|
||||
bindMap.put("entityUpperSnake", entityUpperSnake);
|
||||
bindMap.put("businessName", genTable.getBusinessName());
|
||||
bindMap.put("fieldConfigs", fieldConfigs);
|
||||
|
||||
boolean hasLocalDateTime = false;
|
||||
boolean hasBigDecimal = false;
|
||||
boolean hasRequiredField = false;
|
||||
|
||||
for (GenFieldConfig fieldConfig : fieldConfigs) {
|
||||
for (GenTableColumn fieldConfig : fieldConfigs) {
|
||||
|
||||
if ("LocalDateTime".equals(fieldConfig.getFieldType())) {
|
||||
if (StrUtil.isBlank(fieldConfig.getFieldType())) {
|
||||
fieldConfig.setFieldType(JavaTypeEnum.getJavaTypeByColumnType(fieldConfig.getColumnType()));
|
||||
}
|
||||
|
||||
if ("LocalDateTime".equals(fieldConfig.getFieldType()) || "LocalDate".equals(fieldConfig.getFieldType())) {
|
||||
hasLocalDateTime = true;
|
||||
}
|
||||
if ("BigDecimal".equals(fieldConfig.getFieldType())) {
|
||||
@@ -267,10 +292,11 @@ public class CodegenServiceImpl implements CodegenService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载代码
|
||||
* 下载代码。
|
||||
*
|
||||
* @param tableNames 表名数组,支持多张表。
|
||||
* @return 压缩文件字节数组
|
||||
* @param tableNames 表名数组,支持多张表
|
||||
* @param ui 页面类型
|
||||
* @return zip 压缩文件字节数组
|
||||
*/
|
||||
@Override
|
||||
public byte[] downloadCode(String[] tableNames, String ui) {
|
||||
@@ -292,15 +318,16 @@ public class CodegenServiceImpl implements CodegenService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据表名生成代码并压缩到zip文件中
|
||||
* 根据表名生成代码并压缩到 zip 文件中。
|
||||
*
|
||||
* @param tableName 表名
|
||||
* @param zip 压缩文件输出流
|
||||
* @param ui 页面类型
|
||||
*/
|
||||
private void generateAndZipCode(String tableName, ZipOutputStream zip, String ui) {
|
||||
List<CodegenPreviewVO> codePreviewList = getCodegenPreviewData(tableName, ui);
|
||||
List<CodegenPreviewVo> codePreviewList = getCodegenPreviewData(tableName, ui);
|
||||
|
||||
for (CodegenPreviewVO codePreview : codePreviewList) {
|
||||
for (CodegenPreviewVo codePreview : codePreviewList) {
|
||||
String fileName = codePreview.getFileName();
|
||||
String content = codePreview.getContent();
|
||||
String path = codePreview.getPath();
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
package com.youlai.boot.platform.codegen.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.youlai.boot.platform.codegen.mapper.GenFieldConfigMapper;
|
||||
import com.youlai.boot.platform.codegen.model.entity.GenFieldConfig;
|
||||
import com.youlai.boot.platform.codegen.service.GenFieldConfigService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* 代码生成字段配置服务实现类
|
||||
*
|
||||
* @author Ray
|
||||
* @since 2.10.0
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class GenFieldConfigServiceImpl extends ServiceImpl<GenFieldConfigMapper, GenFieldConfig> implements GenFieldConfigService {
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.youlai.boot.platform.codegen.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.youlai.boot.platform.codegen.mapper.GenTableColumnMapper;
|
||||
import com.youlai.boot.platform.codegen.model.entity.GenTableColumn;
|
||||
import com.youlai.boot.platform.codegen.service.GenTableColumnService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* 代码生成字段配置服务实现类
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 2.10.0
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class GenTableColumnServiceImpl extends ServiceImpl<GenTableColumnMapper, GenTableColumn> implements GenTableColumnService {
|
||||
|
||||
|
||||
}
|
||||
@@ -14,14 +14,14 @@ import com.youlai.boot.core.exception.BusinessException;
|
||||
import com.youlai.boot.config.property.CodegenProperties;
|
||||
import com.youlai.boot.platform.codegen.converter.CodegenConverter;
|
||||
import com.youlai.boot.platform.codegen.mapper.DatabaseMapper;
|
||||
import com.youlai.boot.platform.codegen.mapper.GenConfigMapper;
|
||||
import com.youlai.boot.platform.codegen.mapper.GenTableMapper;
|
||||
import com.youlai.boot.platform.codegen.model.bo.ColumnMetaData;
|
||||
import com.youlai.boot.platform.codegen.model.bo.TableMetaData;
|
||||
import com.youlai.boot.platform.codegen.model.entity.GenConfig;
|
||||
import com.youlai.boot.platform.codegen.model.entity.GenFieldConfig;
|
||||
import com.youlai.boot.platform.codegen.model.entity.GenTable;
|
||||
import com.youlai.boot.platform.codegen.model.entity.GenTableColumn;
|
||||
import com.youlai.boot.platform.codegen.model.form.GenConfigForm;
|
||||
import com.youlai.boot.platform.codegen.service.GenConfigService;
|
||||
import com.youlai.boot.platform.codegen.service.GenFieldConfigService;
|
||||
import com.youlai.boot.platform.codegen.service.GenTableService;
|
||||
import com.youlai.boot.platform.codegen.service.GenTableColumnService;
|
||||
import com.youlai.boot.system.service.MenuService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
@@ -40,11 +40,11 @@ import java.util.Objects;
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class GenConfigServiceImpl extends ServiceImpl<GenConfigMapper, GenConfig> implements GenConfigService {
|
||||
public class GenTableServiceImpl extends ServiceImpl<GenTableMapper, GenTable> implements GenTableService {
|
||||
|
||||
private final DatabaseMapper databaseMapper;
|
||||
private final CodegenProperties codegenProperties;
|
||||
private final GenFieldConfigService genFieldConfigService;
|
||||
private final GenTableColumnService genTableColumnService;
|
||||
private final CodegenConverter codegenConverter;
|
||||
|
||||
@Value("${spring.profiles.active}")
|
||||
@@ -59,64 +59,64 @@ public class GenConfigServiceImpl extends ServiceImpl<GenConfigMapper, GenConfig
|
||||
* @return 代码生成配置
|
||||
*/
|
||||
@Override
|
||||
public GenConfigForm getGenConfigFormData(String tableName) {
|
||||
public GenConfigForm getGenTableFormData(String tableName) {
|
||||
// 查询表生成配置
|
||||
GenConfig genConfig = this.getOne(
|
||||
new LambdaQueryWrapper<>(GenConfig.class)
|
||||
.eq(GenConfig::getTableName, tableName)
|
||||
GenTable genTable = this.getOne(
|
||||
new LambdaQueryWrapper<>(GenTable.class)
|
||||
.eq(GenTable::getTableName, tableName)
|
||||
.last("LIMIT 1")
|
||||
);
|
||||
|
||||
// 是否有代码生成配置
|
||||
boolean hasGenConfig = genConfig != null;
|
||||
boolean hasGenTable = genTable != null;
|
||||
|
||||
// 如果没有代码生成配置,则根据表的元数据生成默认配置
|
||||
if (genConfig == null) {
|
||||
if (genTable == null) {
|
||||
TableMetaData tableMetadata = databaseMapper.getTableMetadata(tableName);
|
||||
Assert.isTrue(tableMetadata != null, "未找到表元数据");
|
||||
|
||||
genConfig = new GenConfig();
|
||||
genConfig.setTableName(tableName);
|
||||
genTable = new GenTable();
|
||||
genTable.setTableName(tableName);
|
||||
|
||||
// 表注释作为业务名称,去掉表字 例如:用户表 -> 用户
|
||||
String tableComment = tableMetadata.getTableComment();
|
||||
if (StrUtil.isNotBlank(tableComment)) {
|
||||
genConfig.setBusinessName(tableComment.replace("表", "").trim());
|
||||
genTable.setBusinessName(tableComment.replace("表", "").trim());
|
||||
}
|
||||
// 根据表名生成实体类名,支持去除前缀 例如:sys_user -> SysUser
|
||||
String removePrefix = genConfig.getRemoveTablePrefix();
|
||||
String removePrefix = genTable.getRemoveTablePrefix();
|
||||
String processedTable = tableName;
|
||||
if (StrUtil.isNotBlank(removePrefix) && StrUtil.startWith(tableName, removePrefix)) {
|
||||
processedTable = StrUtil.removePrefix(tableName, removePrefix);
|
||||
}
|
||||
genConfig.setEntityName(StrUtil.toCamelCase(StrUtil.upperFirst(StrUtil.toCamelCase(processedTable))));
|
||||
genTable.setEntityName(StrUtil.toCamelCase(StrUtil.upperFirst(StrUtil.toCamelCase(processedTable))));
|
||||
|
||||
genConfig.setPackageName(YouLaiBootApplication.class.getPackageName());
|
||||
genConfig.setModuleName(codegenProperties.getDefaultConfig().getModuleName()); // 默认模块名
|
||||
genConfig.setAuthor(codegenProperties.getDefaultConfig().getAuthor());
|
||||
genTable.setPackageName(YouLaiBootApplication.class.getPackageName());
|
||||
genTable.setModuleName(codegenProperties.getDefaultConfig().getModuleName()); // 默认模块名
|
||||
genTable.setAuthor(codegenProperties.getDefaultConfig().getAuthor());
|
||||
}
|
||||
|
||||
// 根据表的列 + 已经存在的字段生成配置 得到 组合后的字段生成配置
|
||||
List<GenFieldConfig> genFieldConfigs = new ArrayList<>();
|
||||
List<GenTableColumn> genTableColumns = new ArrayList<>();
|
||||
|
||||
// 获取表的列
|
||||
List<ColumnMetaData> tableColumns = databaseMapper.getTableColumns(tableName);
|
||||
if (CollectionUtil.isNotEmpty(tableColumns)) {
|
||||
// 查询字段生成配置
|
||||
List<GenFieldConfig> fieldConfigList = genFieldConfigService.list(
|
||||
new LambdaQueryWrapper<GenFieldConfig>()
|
||||
.eq(GenFieldConfig::getConfigId, genConfig.getId())
|
||||
.orderByAsc(GenFieldConfig::getFieldSort)
|
||||
List<GenTableColumn> fieldConfigList = genTableColumnService.list(
|
||||
new LambdaQueryWrapper<GenTableColumn>()
|
||||
.eq(GenTableColumn::getTableId, genTable.getId())
|
||||
.orderByAsc(GenTableColumn::getFieldSort)
|
||||
);
|
||||
Integer maxSort = fieldConfigList.stream()
|
||||
.map(GenFieldConfig::getFieldSort)
|
||||
.map(GenTableColumn::getFieldSort)
|
||||
.filter(Objects::nonNull) // 过滤掉空值
|
||||
.max(Integer::compareTo)
|
||||
.orElse(0);
|
||||
for (ColumnMetaData tableColumn : tableColumns) {
|
||||
// 根据列名获取字段生成配置
|
||||
String columnName = tableColumn.getColumnName();
|
||||
GenFieldConfig fieldConfig = fieldConfigList.stream()
|
||||
GenTableColumn fieldConfig = fieldConfigList.stream()
|
||||
.filter(item -> StrUtil.equals(item.getColumnName(), columnName))
|
||||
.findFirst()
|
||||
.orElseGet(() -> createDefaultFieldConfig(tableColumn));
|
||||
@@ -130,16 +130,16 @@ public class GenConfigServiceImpl extends ServiceImpl<GenConfigMapper, GenConfig
|
||||
fieldConfig.setFieldType(javaType);
|
||||
}
|
||||
// 如果没有代码生成配置,则默认展示在列表和表单
|
||||
if (!hasGenConfig) {
|
||||
if (!hasGenTable) {
|
||||
fieldConfig.setIsShowInList(1);
|
||||
fieldConfig.setIsShowInForm(1);
|
||||
}
|
||||
genFieldConfigs.add(fieldConfig);
|
||||
genTableColumns.add(fieldConfig);
|
||||
}
|
||||
}
|
||||
// 对 genFieldConfigs 按照 fieldSort 排序
|
||||
genFieldConfigs = genFieldConfigs.stream().sorted(Comparator.comparing(GenFieldConfig::getFieldSort)).toList();
|
||||
GenConfigForm genConfigForm = codegenConverter.toGenConfigForm(genConfig, genFieldConfigs);
|
||||
// 对 genTableColumns 按照 fieldSort 排序
|
||||
genTableColumns = genTableColumns.stream().sorted(Comparator.comparing(GenTableColumn::getFieldSort)).toList();
|
||||
GenConfigForm genConfigForm = codegenConverter.toGenConfigForm(genTable, genTableColumns);
|
||||
|
||||
genConfigForm.setFrontendAppName(codegenProperties.getFrontendAppName());
|
||||
genConfigForm.setBackendAppName(codegenProperties.getBackendAppName());
|
||||
@@ -153,17 +153,18 @@ public class GenConfigServiceImpl extends ServiceImpl<GenConfigMapper, GenConfig
|
||||
* @param columnMetaData 表字段元数据
|
||||
* @return
|
||||
*/
|
||||
private GenFieldConfig createDefaultFieldConfig(ColumnMetaData columnMetaData) {
|
||||
GenFieldConfig fieldConfig = new GenFieldConfig();
|
||||
private GenTableColumn createDefaultFieldConfig(ColumnMetaData columnMetaData) {
|
||||
GenTableColumn fieldConfig = new GenTableColumn();
|
||||
fieldConfig.setColumnName(columnMetaData.getColumnName());
|
||||
fieldConfig.setColumnType(columnMetaData.getDataType());
|
||||
fieldConfig.setFieldComment(columnMetaData.getColumnComment());
|
||||
fieldConfig.setFieldName(StrUtil.toCamelCase(columnMetaData.getColumnName()));
|
||||
fieldConfig.setIsRequired("YES".equals(columnMetaData.getIsNullable()) ? 0 : 1);
|
||||
|
||||
if (fieldConfig.getColumnType().equals("date")) {
|
||||
String columnType = StrUtil.blankToDefault(fieldConfig.getColumnType(), "").toLowerCase();
|
||||
if ("date".equals(columnType)) {
|
||||
fieldConfig.setFormType(FormTypeEnum.DATE);
|
||||
} else if (fieldConfig.getColumnType().equals("datetime")) {
|
||||
} else if ("datetime".equals(columnType) || "timestamp".equals(columnType)) {
|
||||
fieldConfig.setFormType(FormTypeEnum.DATE_TIME);
|
||||
} else {
|
||||
fieldConfig.setFormType(FormTypeEnum.INPUT);
|
||||
@@ -181,24 +182,24 @@ public class GenConfigServiceImpl extends ServiceImpl<GenConfigMapper, GenConfig
|
||||
*/
|
||||
@Override
|
||||
public void saveGenConfig(GenConfigForm formData) {
|
||||
GenConfig genConfig = codegenConverter.toGenConfig(formData);
|
||||
this.saveOrUpdate(genConfig);
|
||||
GenTable genTable = codegenConverter.toGenTable(formData);
|
||||
this.saveOrUpdate(genTable);
|
||||
|
||||
// 如果选择上级菜单且当前环境不是生产环境,则保存菜单
|
||||
Long parentMenuId = formData.getParentMenuId();
|
||||
if (parentMenuId != null && !EnvEnum.PROD.getValue().equals(springProfilesActive)) {
|
||||
menuService.addMenuForCodegen(parentMenuId, genConfig);
|
||||
menuService.addMenuForCodegen(parentMenuId, genTable);
|
||||
}
|
||||
|
||||
List<GenFieldConfig> genFieldConfigs = codegenConverter.toGenFieldConfig(formData.getFieldConfigs());
|
||||
List<GenTableColumn> genTableColumns = codegenConverter.toGenTableColumn(formData.getFieldConfigs());
|
||||
|
||||
if (CollectionUtil.isEmpty(genFieldConfigs)) {
|
||||
if (CollectionUtil.isEmpty(genTableColumns)) {
|
||||
throw new BusinessException("字段配置不能为空");
|
||||
}
|
||||
genFieldConfigs.forEach(genFieldConfig -> {
|
||||
genFieldConfig.setConfigId(genConfig.getId());
|
||||
genTableColumns.forEach(genTableColumn -> {
|
||||
genTableColumn.setTableId(genTable.getId());
|
||||
});
|
||||
genFieldConfigService.saveOrUpdateBatch(genFieldConfigs);
|
||||
genTableColumnService.saveOrUpdateBatch(genTableColumns);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -208,15 +209,15 @@ public class GenConfigServiceImpl extends ServiceImpl<GenConfigMapper, GenConfig
|
||||
*/
|
||||
@Override
|
||||
public void deleteGenConfig(String tableName) {
|
||||
GenConfig genConfig = this.getOne(new LambdaQueryWrapper<GenConfig>()
|
||||
.eq(GenConfig::getTableName, tableName));
|
||||
GenTable genTable = this.getOne(new LambdaQueryWrapper<GenTable>()
|
||||
.eq(GenTable::getTableName, tableName));
|
||||
|
||||
boolean result = this.remove(new LambdaQueryWrapper<GenConfig>()
|
||||
.eq(GenConfig::getTableName, tableName)
|
||||
boolean result = this.remove(new LambdaQueryWrapper<GenTable>()
|
||||
.eq(GenTable::getTableName, tableName)
|
||||
);
|
||||
if (result) {
|
||||
genFieldConfigService.remove(new LambdaQueryWrapper<GenFieldConfig>()
|
||||
.eq(GenFieldConfig::getConfigId, genConfig.getId())
|
||||
genTableColumnService.remove(new LambdaQueryWrapper<GenTableColumn>()
|
||||
.eq(GenTableColumn::getTableId, genTable.getId())
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,13 @@
|
||||
package com.youlai.boot.platform.websocket.controller;
|
||||
|
||||
import com.youlai.boot.platform.websocket.model.ChatMessage;
|
||||
import com.youlai.boot.platform.websocket.dto.TextMessage;
|
||||
import com.youlai.boot.platform.websocket.publisher.WebSocketPublisher;
|
||||
import com.youlai.boot.platform.websocket.topic.WebSocketTopics;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.messaging.handler.annotation.DestinationVariable;
|
||||
import org.springframework.messaging.handler.annotation.MessageMapping;
|
||||
import org.springframework.messaging.handler.annotation.SendTo;
|
||||
import org.springframework.messaging.simp.SimpMessagingTemplate;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@@ -26,7 +27,7 @@ import java.security.Principal;
|
||||
@Slf4j
|
||||
public class WebsocketController {
|
||||
|
||||
private final SimpMessagingTemplate messagingTemplate;
|
||||
private final WebSocketPublisher webSocketPublisher;
|
||||
|
||||
|
||||
/**
|
||||
@@ -58,7 +59,7 @@ public class WebsocketController {
|
||||
|
||||
log.info("发送人:{}; 接收人:{}", sender, receiver);
|
||||
// 发送消息给指定用户,拼接后路径 /user/{receiver}/queue/greeting
|
||||
messagingTemplate.convertAndSendToUser(receiver, "/queue/greeting", new ChatMessage(sender, message));
|
||||
webSocketPublisher.publishToUser(receiver, WebSocketTopics.USER_QUEUE_GREETING, new TextMessage(sender, message, System.currentTimeMillis()));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.youlai.boot.platform.websocket.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class DictChangeEvent {
|
||||
|
||||
private String dictCode;
|
||||
private long timestamp;
|
||||
|
||||
public DictChangeEvent(String dictCode) {
|
||||
this.dictCode = dictCode;
|
||||
this.timestamp = System.currentTimeMillis();
|
||||
}
|
||||
}
|
||||
@@ -1,25 +1,15 @@
|
||||
package com.youlai.boot.platform.websocket.model;
|
||||
package com.youlai.boot.platform.websocket.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 系统消息体
|
||||
*/
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class ChatMessage {
|
||||
public class TextMessage {
|
||||
|
||||
/**
|
||||
* 发送者
|
||||
*/
|
||||
private String sender;
|
||||
|
||||
/**
|
||||
* 消息内容
|
||||
*/
|
||||
private String content;
|
||||
|
||||
private Long timestamp;
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package com.youlai.boot.platform.websocket.publisher;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.messaging.simp.SimpMessagingTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class WebSocketPublisher {
|
||||
|
||||
private SimpMessagingTemplate messagingTemplate;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
@Autowired(required = false)
|
||||
public void setMessagingTemplate(SimpMessagingTemplate messagingTemplate) {
|
||||
this.messagingTemplate = messagingTemplate;
|
||||
}
|
||||
|
||||
public void publish(String destination, Object payload) {
|
||||
if (messagingTemplate == null) {
|
||||
log.warn("消息模板尚未初始化,无法发送消息: destination={}", destination);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
Object body = serializeIfNeeded(payload);
|
||||
messagingTemplate.convertAndSend(destination, body);
|
||||
} catch (Exception e) {
|
||||
log.error("发送消息失败: destination={}", destination, e);
|
||||
}
|
||||
}
|
||||
|
||||
public void publishToUser(String username, String destination, Object payload) {
|
||||
if (messagingTemplate == null) {
|
||||
log.warn("消息模板尚未初始化,无法发送用户消息: username={}, destination={}", username, destination);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
Object body = serializeIfNeeded(payload);
|
||||
messagingTemplate.convertAndSendToUser(username, destination, body);
|
||||
} catch (Exception e) {
|
||||
log.error("发送用户消息失败: username={}, destination={}", username, destination, e);
|
||||
}
|
||||
}
|
||||
|
||||
private Object serializeIfNeeded(Object payload) throws JsonProcessingException {
|
||||
if (payload == null) {
|
||||
return null;
|
||||
}
|
||||
if (payload instanceof String || payload instanceof Number || payload instanceof Boolean) {
|
||||
return payload;
|
||||
}
|
||||
return objectMapper.writeValueAsString(payload);
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,15 @@
|
||||
package com.youlai.boot.platform.websocket.service.impl;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.youlai.boot.system.model.dto.DictEventDTO;
|
||||
import com.youlai.boot.platform.websocket.dto.DictChangeEvent;
|
||||
import com.youlai.boot.platform.websocket.dto.TextMessage;
|
||||
import com.youlai.boot.platform.websocket.publisher.WebSocketPublisher;
|
||||
import com.youlai.boot.platform.websocket.session.UserSessionRegistry;
|
||||
import com.youlai.boot.platform.websocket.service.WebSocketService;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import com.youlai.boot.platform.websocket.topic.WebSocketTopics;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.messaging.simp.SimpMessagingTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* WebSocket 服务实现类
|
||||
@@ -33,39 +26,12 @@ import java.util.stream.Collectors;
|
||||
@Slf4j
|
||||
public class WebSocketServiceImpl implements WebSocketService {
|
||||
|
||||
// ==================== 在线用户管理 ====================
|
||||
|
||||
/**
|
||||
* 用户在线会话映射表
|
||||
* Key: 用户名
|
||||
* Value: 该用户的所有会话 ID 集合(支持多设备登录)
|
||||
*/
|
||||
private final Map<String, Set<String>> userSessionsMap = new ConcurrentHashMap<>();
|
||||
private final UserSessionRegistry userSessionRegistry;
|
||||
private final WebSocketPublisher webSocketPublisher;
|
||||
|
||||
/**
|
||||
* 会话详情映射表
|
||||
* Key: 会话 ID
|
||||
* Value: 会话详细信息
|
||||
*/
|
||||
private final Map<String, SessionInfo> sessionDetailsMap = new ConcurrentHashMap<>();
|
||||
|
||||
// ==================== 依赖注入 ====================
|
||||
|
||||
private SimpMessagingTemplate messagingTemplate;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
@Autowired
|
||||
public WebSocketServiceImpl(ObjectMapper objectMapper) {
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* 延迟注入 SimpMessagingTemplate,避免循环依赖
|
||||
*/
|
||||
@Autowired(required = false)
|
||||
public void setMessagingTemplate(SimpMessagingTemplate messagingTemplate) {
|
||||
this.messagingTemplate = messagingTemplate;
|
||||
log.info("✓ WebSocket 消息模板已初始化");
|
||||
public WebSocketServiceImpl(UserSessionRegistry userSessionRegistry, WebSocketPublisher webSocketPublisher) {
|
||||
this.userSessionRegistry = userSessionRegistry;
|
||||
this.webSocketPublisher = webSocketPublisher;
|
||||
}
|
||||
|
||||
// ==================== 用户在线状态管理 ====================
|
||||
@@ -88,16 +54,10 @@ public class WebSocketServiceImpl implements WebSocketService {
|
||||
return;
|
||||
}
|
||||
|
||||
// 添加会话到用户的会话集合中(支持多设备登录)
|
||||
userSessionsMap.computeIfAbsent(username, k -> ConcurrentHashMap.newKeySet())
|
||||
.add(sessionId);
|
||||
userSessionRegistry.userConnected(username, sessionId);
|
||||
|
||||
// 保存会话详情
|
||||
SessionInfo sessionInfo = new SessionInfo(username, sessionId, System.currentTimeMillis());
|
||||
sessionDetailsMap.put(sessionId, sessionInfo);
|
||||
|
||||
int sessionCount = userSessionsMap.get(username).size();
|
||||
int totalOnlineUsers = userSessionsMap.size();
|
||||
int sessionCount = userSessionRegistry.getUserSessionCount(username);
|
||||
int totalOnlineUsers = userSessionRegistry.getOnlineUserCount();
|
||||
|
||||
log.info("✓ 用户[{}]会话[{}]上线(该用户共 {} 个会话,系统总在线用户数:{})",
|
||||
username, sessionId, sessionCount, totalOnlineUsers);
|
||||
@@ -117,20 +77,9 @@ public class WebSocketServiceImpl implements WebSocketService {
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取该用户的所有会话
|
||||
Set<String> sessions = userSessionsMap.get(username);
|
||||
if (sessions == null || sessions.isEmpty()) {
|
||||
log.warn("用户[{}]下线:未找到会话记录", username);
|
||||
return;
|
||||
}
|
||||
userSessionRegistry.userDisconnected(username);
|
||||
|
||||
// 移除所有会话详情(通常一次只断开一个会话,但这里做全量清理)
|
||||
sessions.forEach(sessionDetailsMap::remove);
|
||||
|
||||
// 移除用户的会话记录
|
||||
userSessionsMap.remove(username);
|
||||
|
||||
int totalOnlineUsers = userSessionsMap.size();
|
||||
int totalOnlineUsers = userSessionRegistry.getOnlineUserCount();
|
||||
log.info("✓ 用户[{}]下线(系统总在线用户数:{})", username, totalOnlineUsers);
|
||||
|
||||
// 广播在线用户数变更
|
||||
@@ -143,29 +92,8 @@ public class WebSocketServiceImpl implements WebSocketService {
|
||||
* @param sessionId 会话 ID
|
||||
*/
|
||||
public void removeSession(String sessionId) {
|
||||
SessionInfo sessionInfo = sessionDetailsMap.remove(sessionId);
|
||||
if (sessionInfo == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
String username = sessionInfo.getUsername();
|
||||
Set<String> sessions = userSessionsMap.get(username);
|
||||
|
||||
if (sessions != null) {
|
||||
sessions.remove(sessionId);
|
||||
|
||||
// 如果该用户没有其他会话了,移除用户记录
|
||||
if (sessions.isEmpty()) {
|
||||
userSessionsMap.remove(username);
|
||||
log.info("✓ 用户[{}]最后一个会话[{}]下线", username, sessionId);
|
||||
} else {
|
||||
log.info("✓ 用户[{}]会话[{}]下线(还剩 {} 个会话)",
|
||||
username, sessionId, sessions.size());
|
||||
}
|
||||
|
||||
// 广播在线用户数变更
|
||||
broadcastOnlineUserCount();
|
||||
}
|
||||
userSessionRegistry.removeSession(sessionId);
|
||||
broadcastOnlineUserCount();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -173,23 +101,8 @@ public class WebSocketServiceImpl implements WebSocketService {
|
||||
*
|
||||
* @return 在线用户信息列表
|
||||
*/
|
||||
public List<OnlineUserDTO> getOnlineUsers() {
|
||||
return userSessionsMap.entrySet().stream()
|
||||
.map(entry -> {
|
||||
String username = entry.getKey();
|
||||
Set<String> sessions = entry.getValue();
|
||||
|
||||
// 获取该用户最早的登录时间
|
||||
long earliestLoginTime = sessions.stream()
|
||||
.map(sessionDetailsMap::get)
|
||||
.filter(info -> info != null)
|
||||
.mapToLong(SessionInfo::getConnectTime)
|
||||
.min()
|
||||
.orElse(System.currentTimeMillis());
|
||||
|
||||
return new OnlineUserDTO(username, sessions.size(), earliestLoginTime);
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
public List<UserSessionRegistry.OnlineUserDto> getOnlineUsers() {
|
||||
return userSessionRegistry.getOnlineUsers();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -198,7 +111,7 @@ public class WebSocketServiceImpl implements WebSocketService {
|
||||
* @return 在线用户数(不是会话数)
|
||||
*/
|
||||
public int getOnlineUserCount() {
|
||||
return userSessionsMap.size();
|
||||
return userSessionRegistry.getOnlineUserCount();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -207,7 +120,7 @@ public class WebSocketServiceImpl implements WebSocketService {
|
||||
* @return 所有在线会话的总数
|
||||
*/
|
||||
public int getTotalSessionCount() {
|
||||
return sessionDetailsMap.size();
|
||||
return userSessionRegistry.getTotalSessionCount();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -217,8 +130,7 @@ public class WebSocketServiceImpl implements WebSocketService {
|
||||
* @return 是否在线
|
||||
*/
|
||||
public boolean isUserOnline(String username) {
|
||||
Set<String> sessions = userSessionsMap.get(username);
|
||||
return sessions != null && !sessions.isEmpty();
|
||||
return userSessionRegistry.isUserOnline(username);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -228,8 +140,7 @@ public class WebSocketServiceImpl implements WebSocketService {
|
||||
* @return 会话数量
|
||||
*/
|
||||
public int getUserSessionCount(String username) {
|
||||
Set<String> sessions = userSessionsMap.get(username);
|
||||
return sessions != null ? sessions.size() : 0;
|
||||
return userSessionRegistry.getUserSessionCount(username);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -246,18 +157,9 @@ public class WebSocketServiceImpl implements WebSocketService {
|
||||
* 广播在线用户数量变更(内部方法)
|
||||
*/
|
||||
private void broadcastOnlineUserCount() {
|
||||
if (messagingTemplate == null) {
|
||||
log.warn("消息模板尚未初始化,无法发送在线用户数量");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
int count = getOnlineUserCount();
|
||||
messagingTemplate.convertAndSend("/topic/online-count", count);
|
||||
log.debug("✓ 已广播在线用户数量: {}", count);
|
||||
} catch (Exception e) {
|
||||
log.error("广播在线用户数量失败", e);
|
||||
}
|
||||
int count = getOnlineUserCount();
|
||||
webSocketPublisher.publish(WebSocketTopics.TOPIC_ONLINE_COUNT, count);
|
||||
log.debug("✓ 已广播在线用户数量: {}", count);
|
||||
}
|
||||
|
||||
// ==================== 消息推送功能 ====================
|
||||
@@ -274,30 +176,9 @@ public class WebSocketServiceImpl implements WebSocketService {
|
||||
return;
|
||||
}
|
||||
|
||||
DictEventDTO event = new DictEventDTO(dictCode);
|
||||
sendDictChangeEvent(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送字典变更事件
|
||||
*
|
||||
* @param event 字典事件
|
||||
*/
|
||||
private void sendDictChangeEvent(DictEventDTO event) {
|
||||
if (messagingTemplate == null) {
|
||||
log.warn("消息模板尚未初始化,无法发送字典更新通知");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
String message = objectMapper.writeValueAsString(event);
|
||||
messagingTemplate.convertAndSend("/topic/dict", message);
|
||||
log.info("✓ 已广播字典变更通知: dictCode={}", event.getDictCode());
|
||||
} catch (JsonProcessingException e) {
|
||||
log.error("字典事件序列化失败: dictCode={}", event.getDictCode(), e);
|
||||
} catch (Exception e) {
|
||||
log.error("发送字典变更通知失败: dictCode={}", event.getDictCode(), e);
|
||||
}
|
||||
DictChangeEvent event = new DictChangeEvent(dictCode);
|
||||
webSocketPublisher.publish(WebSocketTopics.TOPIC_DICT, event);
|
||||
log.info("✓ 已广播字典变更通知: dictCode={}", dictCode);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -318,20 +199,8 @@ public class WebSocketServiceImpl implements WebSocketService {
|
||||
return;
|
||||
}
|
||||
|
||||
if (messagingTemplate == null) {
|
||||
log.warn("消息模板尚未初始化,无法发送用户消息");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
String messageJson = objectMapper.writeValueAsString(message);
|
||||
messagingTemplate.convertAndSendToUser(username, "/queue/messages", messageJson);
|
||||
log.info("✓ 已向用户[{}]发送通知", username);
|
||||
} catch (JsonProcessingException e) {
|
||||
log.error("消息序列化失败: username={}", username, e);
|
||||
} catch (Exception e) {
|
||||
log.error("向用户[{}]发送通知失败", username, e);
|
||||
}
|
||||
webSocketPublisher.publishToUser(username, WebSocketTopics.USER_QUEUE_MESSAGES, message);
|
||||
log.info("✓ 已向用户[{}]发送通知", username);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -345,71 +214,8 @@ public class WebSocketServiceImpl implements WebSocketService {
|
||||
return;
|
||||
}
|
||||
|
||||
if (messagingTemplate == null) {
|
||||
log.warn("消息模板尚未初始化,无法发送广播消息");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
SystemMessage systemMessage = new SystemMessage(
|
||||
"系统通知",
|
||||
message,
|
||||
System.currentTimeMillis()
|
||||
);
|
||||
String messageJson = objectMapper.writeValueAsString(systemMessage);
|
||||
messagingTemplate.convertAndSend("/topic/public", messageJson);
|
||||
log.info("✓ 已广播系统消息: {}", message);
|
||||
} catch (JsonProcessingException e) {
|
||||
log.error("系统消息序列化失败", e);
|
||||
} catch (Exception e) {
|
||||
log.error("广播系统消息失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 内部数据类 ====================
|
||||
|
||||
/**
|
||||
* 会话信息
|
||||
*/
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
private static class SessionInfo {
|
||||
/** 用户名 */
|
||||
private String username;
|
||||
/** 会话 ID */
|
||||
private String sessionId;
|
||||
/** 连接时间戳 */
|
||||
private long connectTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* 在线用户 DTO
|
||||
*/
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public static class OnlineUserDTO {
|
||||
/** 用户名 */
|
||||
private String username;
|
||||
/** 会话数量 */
|
||||
private int sessionCount;
|
||||
/** 首次登录时间 */
|
||||
private long loginTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* 系统消息
|
||||
*/
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public static class SystemMessage {
|
||||
/** 发送者 */
|
||||
private String sender;
|
||||
/** 消息内容 */
|
||||
private String content;
|
||||
/** 时间戳 */
|
||||
private long timestamp;
|
||||
TextMessage systemMessage = new TextMessage("系统通知", message, System.currentTimeMillis());
|
||||
webSocketPublisher.publish(WebSocketTopics.TOPIC_PUBLIC, systemMessage);
|
||||
log.info("✓ 已广播系统消息: {}", message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
package com.youlai.boot.platform.websocket.session;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Component
|
||||
public class UserSessionRegistry {
|
||||
|
||||
private final Map<String, Set<String>> userSessionsMap = new ConcurrentHashMap<>();
|
||||
private final Map<String, SessionInfo> sessionDetailsMap = new ConcurrentHashMap<>();
|
||||
|
||||
public void userConnected(String username, String sessionId) {
|
||||
userSessionsMap.computeIfAbsent(username, k -> ConcurrentHashMap.newKeySet()).add(sessionId);
|
||||
sessionDetailsMap.put(sessionId, new SessionInfo(username, sessionId, System.currentTimeMillis()));
|
||||
}
|
||||
|
||||
public void userDisconnected(String username) {
|
||||
Set<String> sessions = userSessionsMap.remove(username);
|
||||
if (sessions == null) {
|
||||
return;
|
||||
}
|
||||
sessions.forEach(sessionDetailsMap::remove);
|
||||
}
|
||||
|
||||
public void removeSession(String sessionId) {
|
||||
SessionInfo sessionInfo = sessionDetailsMap.remove(sessionId);
|
||||
if (sessionInfo == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
String username = sessionInfo.getUsername();
|
||||
Set<String> sessions = userSessionsMap.get(username);
|
||||
if (sessions == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
sessions.remove(sessionId);
|
||||
if (sessions.isEmpty()) {
|
||||
userSessionsMap.remove(username);
|
||||
}
|
||||
}
|
||||
|
||||
public int getOnlineUserCount() {
|
||||
return userSessionsMap.size();
|
||||
}
|
||||
|
||||
public int getUserSessionCount(String username) {
|
||||
Set<String> sessions = userSessionsMap.get(username);
|
||||
return sessions != null ? sessions.size() : 0;
|
||||
}
|
||||
|
||||
public int getTotalSessionCount() {
|
||||
return sessionDetailsMap.size();
|
||||
}
|
||||
|
||||
public boolean isUserOnline(String username) {
|
||||
Set<String> sessions = userSessionsMap.get(username);
|
||||
return sessions != null && !sessions.isEmpty();
|
||||
}
|
||||
|
||||
public List<OnlineUserDto> getOnlineUsers() {
|
||||
return userSessionsMap.entrySet().stream()
|
||||
.map(entry -> {
|
||||
String username = entry.getKey();
|
||||
Set<String> sessions = entry.getValue();
|
||||
long earliestLoginTime = sessions.stream()
|
||||
.map(sessionDetailsMap::get)
|
||||
.filter(info -> info != null)
|
||||
.mapToLong(SessionInfo::getConnectTime)
|
||||
.min()
|
||||
.orElse(System.currentTimeMillis());
|
||||
|
||||
return new OnlineUserDto(username, sessions.size(), earliestLoginTime);
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
private static class SessionInfo {
|
||||
private String username;
|
||||
private String sessionId;
|
||||
private long connectTime;
|
||||
}
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public static class OnlineUserDto {
|
||||
private String username;
|
||||
private int sessionCount;
|
||||
private long loginTime;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.youlai.boot.platform.websocket.topic;
|
||||
|
||||
public final class WebSocketTopics {
|
||||
|
||||
private WebSocketTopics() {
|
||||
}
|
||||
|
||||
public static final String TOPIC_DICT = "/topic/dict";
|
||||
public static final String TOPIC_ONLINE_COUNT = "/topic/online-count";
|
||||
public static final String TOPIC_PUBLIC = "/topic/public";
|
||||
|
||||
public static final String USER_QUEUE_MESSAGES = "/queue/messages";
|
||||
public static final String USER_QUEUE_MESSAGE = "/queue/message";
|
||||
public static final String USER_QUEUE_GREETING = "/queue/greeting";
|
||||
}
|
||||
@@ -1,22 +1,28 @@
|
||||
package com.youlai.boot.plugin.mybatis;
|
||||
|
||||
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.ibatis.reflection.MetaObject;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* mybatis-plus 字段自动填充
|
||||
* <p>
|
||||
* 支持自动填充创建时间、更新时间
|
||||
* </p>
|
||||
*
|
||||
* @author haoxr
|
||||
* @author Ray.Hao
|
||||
* @since 2022/10/14
|
||||
*/
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class MyMetaObjectHandler implements MetaObjectHandler {
|
||||
|
||||
/**
|
||||
* 新增填充创建时间
|
||||
* 新增填充创建时间、更新时间
|
||||
*
|
||||
* @param metaObject 元数据
|
||||
*/
|
||||
|
||||
@@ -2,38 +2,45 @@ package com.youlai.boot.security.filter;
|
||||
|
||||
import cn.hutool.captcha.generator.CodeGenerator;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.json.JSONObject;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.youlai.boot.common.constant.RedisConstants;
|
||||
import com.youlai.boot.common.constant.SecurityConstants;
|
||||
import com.youlai.boot.core.web.ResultCode;
|
||||
import com.youlai.boot.core.web.WebResponseHelper;
|
||||
import com.youlai.boot.core.web.WebResponseWriter;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.ServletInputStream;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletRequestWrapper;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
import org.springframework.util.StreamUtils;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
import org.springframework.web.util.ContentCachingRequestWrapper;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
* 图形验证码校验过滤器
|
||||
*
|
||||
* @author haoxr
|
||||
* @since 2022/10/1
|
||||
*/
|
||||
public class CaptchaValidationFilter extends OncePerRequestFilter {
|
||||
|
||||
private static final RequestMatcher LOGIN_PATH_REQUEST_MATCHER = PathPatternRequestMatcher.withDefaults().matcher(HttpMethod.POST,SecurityConstants.LOGIN_PATH);
|
||||
private static final RequestMatcher LOGIN_PATH_REQUEST_MATCHER = PathPatternRequestMatcher.withDefaults()
|
||||
.matcher(HttpMethod.POST, SecurityConstants.LOGIN_PATH);
|
||||
|
||||
public static final String CAPTCHA_CODE_PARAM_NAME = "captchaCode";
|
||||
public static final String CAPTCHA_KEY_PARAM_NAME = "captchaKey";
|
||||
public static final String CAPTCHA_ID_PARAM_NAME = "captchaId";
|
||||
|
||||
private final RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
private final CodeGenerator codeGenerator;
|
||||
|
||||
public CaptchaValidationFilter(RedisTemplate<String, Object> redisTemplate, CodeGenerator codeGenerator) {
|
||||
@@ -41,37 +48,111 @@ public class CaptchaValidationFilter extends OncePerRequestFilter {
|
||||
this.codeGenerator = codeGenerator;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
|
||||
// 检验登录接口的验证码
|
||||
if (LOGIN_PATH_REQUEST_MATCHER.matches(request)) {
|
||||
// 请求中的验证码
|
||||
String captchaCode = request.getParameter(CAPTCHA_CODE_PARAM_NAME);
|
||||
// TODO 兼容没有验证码的版本(线上请移除这个判断)
|
||||
if (StrUtil.isBlank(captchaCode)) {
|
||||
chain.doFilter(request, response);
|
||||
return;
|
||||
}
|
||||
// 缓存中的验证码
|
||||
String verifyCodeKey = request.getParameter(CAPTCHA_KEY_PARAM_NAME);
|
||||
String cacheVerifyCode = (String) redisTemplate.opsForValue().get(
|
||||
StrUtil.format(RedisConstants.Captcha.IMAGE_CODE, verifyCodeKey)
|
||||
);
|
||||
if (cacheVerifyCode == null) {
|
||||
WebResponseHelper.writeError(response, ResultCode.USER_VERIFICATION_CODE_EXPIRED);
|
||||
} else {
|
||||
// 验证码比对
|
||||
if (codeGenerator.verify(cacheVerifyCode, captchaCode)) {
|
||||
chain.doFilter(request, response);
|
||||
} else {
|
||||
WebResponseHelper.writeError(response, ResultCode.USER_VERIFICATION_CODE_ERROR);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 非登录接口放行
|
||||
public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
|
||||
throws ServletException, IOException {
|
||||
|
||||
// 非登录接口直接放行
|
||||
if (!LOGIN_PATH_REQUEST_MATCHER.matches(request)) {
|
||||
chain.doFilter(request, response);
|
||||
return;
|
||||
}
|
||||
|
||||
// 仅支持 JSON 登录
|
||||
String contentType = request.getContentType();
|
||||
if (contentType == null || !contentType.contains(MediaType.APPLICATION_JSON_VALUE)) {
|
||||
WebResponseWriter.writeError(response, ResultCode.USER_VERIFICATION_CODE_ERROR);
|
||||
return;
|
||||
}
|
||||
|
||||
// 包装请求,确保下游还能读取 body
|
||||
ContentCachingRequestWrapper requestWrapper = new ContentCachingRequestWrapper(request);
|
||||
|
||||
byte[] bodyBytes = StreamUtils.copyToByteArray(requestWrapper.getInputStream());
|
||||
String body = new String(bodyBytes, StandardCharsets.UTF_8);
|
||||
String captchaCode = null;
|
||||
String captchaId = null;
|
||||
|
||||
if (StrUtil.isNotBlank(body)) {
|
||||
JSONObject jsonObject = JSONUtil.parseObj(body);
|
||||
captchaCode = jsonObject.getStr(CAPTCHA_CODE_PARAM_NAME);
|
||||
captchaId = jsonObject.getStr(CAPTCHA_ID_PARAM_NAME);
|
||||
}
|
||||
|
||||
if (StrUtil.isBlank(captchaCode) || StrUtil.isBlank(captchaId)) {
|
||||
WebResponseWriter.writeError(response, ResultCode.USER_VERIFICATION_CODE_ERROR);
|
||||
return;
|
||||
}
|
||||
|
||||
String cacheVerifyCode = (String) redisTemplate.opsForValue().get(
|
||||
StrUtil.format(RedisConstants.Captcha.IMAGE_CODE, captchaId)
|
||||
);
|
||||
if (cacheVerifyCode == null) {
|
||||
WebResponseWriter.writeError(response, ResultCode.USER_VERIFICATION_CODE_EXPIRED);
|
||||
return;
|
||||
}
|
||||
|
||||
if (codeGenerator.verify(cacheVerifyCode, captchaCode)) {
|
||||
HttpServletRequest repeatableRequest = new RepeatableReadRequestWrapper(requestWrapper, bodyBytes);
|
||||
chain.doFilter(repeatableRequest, response);
|
||||
} else {
|
||||
WebResponseWriter.writeError(response, ResultCode.USER_VERIFICATION_CODE_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple wrapper to allow repeated reads of the request body after we've parsed it here.
|
||||
*/
|
||||
private static class RepeatableReadRequestWrapper extends HttpServletRequestWrapper {
|
||||
|
||||
private final byte[] cachedBody;
|
||||
|
||||
RepeatableReadRequestWrapper(HttpServletRequest request, byte[] cachedBody) {
|
||||
super(request);
|
||||
this.cachedBody = cachedBody != null ? cachedBody : new byte[0];
|
||||
}
|
||||
|
||||
@Override
|
||||
public ServletInputStream getInputStream() {
|
||||
ByteArrayInputStream bais = new ByteArrayInputStream(cachedBody);
|
||||
return new ServletInputStream() {
|
||||
@Override
|
||||
public int read() {
|
||||
return bais.read();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFinished() {
|
||||
return bais.available() == 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isReady() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setReadListener(jakarta.servlet.ReadListener readListener) {
|
||||
// no-op
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public BufferedReader getReader() {
|
||||
return new BufferedReader(new InputStreamReader(getInputStream(), StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getContentLength() {
|
||||
return cachedBody.length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getContentLengthLong() {
|
||||
return cachedBody.length;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ package com.youlai.boot.security.filter;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.youlai.boot.common.constant.SecurityConstants;
|
||||
import com.youlai.boot.core.web.ResultCode;
|
||||
import com.youlai.boot.core.web.WebResponseHelper;
|
||||
import com.youlai.boot.core.web.WebResponseWriter;
|
||||
import com.youlai.boot.security.token.TokenManager;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
@@ -52,7 +52,7 @@ public class TokenAuthenticationFilter extends OncePerRequestFilter {
|
||||
// 执行令牌有效性检查(包含密码学验签和过期时间验证)
|
||||
boolean isValidToken = tokenManager.validateToken(rawToken);
|
||||
if (!isValidToken) {
|
||||
WebResponseHelper.writeError(response, ResultCode.ACCESS_TOKEN_INVALID);
|
||||
WebResponseWriter.writeError(response, ResultCode.ACCESS_TOKEN_INVALID);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ public class TokenAuthenticationFilter extends OncePerRequestFilter {
|
||||
} catch (Exception ex) {
|
||||
// 安全上下文清除保障(防止上下文残留)
|
||||
SecurityContextHolder.clearContext();
|
||||
WebResponseHelper.writeError(response, ResultCode.ACCESS_TOKEN_INVALID);
|
||||
WebResponseWriter.writeError(response, ResultCode.ACCESS_TOKEN_INVALID);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.youlai.boot.security.handler;
|
||||
|
||||
import com.youlai.boot.core.web.ResultCode;
|
||||
import com.youlai.boot.core.web.WebResponseHelper;
|
||||
import com.youlai.boot.core.web.WebResponseWriter;
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
import org.springframework.security.web.access.AccessDeniedHandler;
|
||||
import org.springframework.stereotype.Component;
|
||||
@@ -20,7 +20,7 @@ public class MyAccessDeniedHandler implements AccessDeniedHandler {
|
||||
|
||||
@Override
|
||||
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) {
|
||||
WebResponseHelper.writeError(response, ResultCode.ACCESS_UNAUTHORIZED);
|
||||
WebResponseWriter.writeError(response, ResultCode.ACCESS_UNAUTHORIZED);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.youlai.boot.security.handler;
|
||||
|
||||
import com.youlai.boot.core.web.ResultCode;
|
||||
import com.youlai.boot.core.web.WebResponseHelper;
|
||||
import com.youlai.boot.core.web.WebResponseWriter;
|
||||
import org.springframework.security.authentication.BadCredentialsException;
|
||||
import org.springframework.security.authentication.InsufficientAuthenticationException;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
@@ -32,13 +32,13 @@ public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {
|
||||
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
|
||||
if (authException instanceof BadCredentialsException) {
|
||||
// 用户名或密码错误
|
||||
WebResponseHelper.writeError(response, ResultCode.USER_PASSWORD_ERROR);
|
||||
WebResponseWriter.writeError(response, ResultCode.USER_PASSWORD_ERROR);
|
||||
} else if(authException instanceof InsufficientAuthenticationException){
|
||||
// 请求头缺失Authorization、Token格式错误、Token过期、签名验证失败
|
||||
WebResponseHelper.writeError(response, ResultCode.ACCESS_TOKEN_INVALID);
|
||||
WebResponseWriter.writeError(response, ResultCode.ACCESS_TOKEN_INVALID);
|
||||
} else {
|
||||
// 其他未明确处理的认证异常(如账户被锁定、账户禁用等)
|
||||
WebResponseHelper.writeError(response, ResultCode.USER_LOGIN_EXCEPTION, authException.getMessage());
|
||||
WebResponseWriter.writeError(response, ResultCode.USER_LOGIN_EXCEPTION, authException.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.youlai.boot.security.model;
|
||||
import cn.hutool.core.collection.CollectionUtil;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import com.youlai.boot.common.constant.SecurityConstants;
|
||||
import com.youlai.boot.security.model.UserAuthInfo;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
@@ -64,9 +65,9 @@ public class SysUserDetails implements UserDetails {
|
||||
/**
|
||||
* 构造函数:根据用户认证信息初始化用户详情对象
|
||||
*
|
||||
* @param user 用户认证信息对象 {@link UserAuthCredentials}
|
||||
* @param user 用户认证信息对象 {@link UserAuthInfo}
|
||||
*/
|
||||
public SysUserDetails(UserAuthCredentials user) {
|
||||
public SysUserDetails(UserAuthInfo user) {
|
||||
this.userId = user.getUserId();
|
||||
this.username = user.getUsername();
|
||||
this.password = user.getPassword();
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
package com.youlai.boot.security.model;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 用户认证凭证信息
|
||||
* 用户认证信息
|
||||
* <p>
|
||||
* 用于登录认证过程中的用户信息承载,包含用户名、密码、状态、角色等与认证/授权相关的数据。
|
||||
* </p>
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 2022/10/22
|
||||
* @since 2025/12/16
|
||||
*/
|
||||
@Data
|
||||
public class UserAuthCredentials {
|
||||
public class UserAuthInfo {
|
||||
|
||||
/**
|
||||
* 用户ID
|
||||
@@ -33,25 +37,22 @@ public class UserAuthCredentials {
|
||||
private Long deptId;
|
||||
|
||||
/**
|
||||
* 用户密码
|
||||
* 密码(加密后)
|
||||
*/
|
||||
private String password;
|
||||
|
||||
/**
|
||||
* 状态(1:启用;0:禁用)
|
||||
* 状态(1:启用 其它:禁用)
|
||||
*/
|
||||
private Integer status;
|
||||
|
||||
/**
|
||||
* 用户所属的角色集合
|
||||
* 角色集合
|
||||
*/
|
||||
private Set<String> roles;
|
||||
|
||||
/**
|
||||
* 数据权限范围,用于控制用户可以访问的数据级别
|
||||
*
|
||||
* @see com.youlai.boot.common.enums.DataScopeEnum
|
||||
* 数据权限范围
|
||||
*/
|
||||
private Integer dataScope;
|
||||
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import com.youlai.boot.common.constant.RedisConstants;
|
||||
import com.youlai.boot.security.exception.CaptchaValidationException;
|
||||
import com.youlai.boot.security.model.SmsAuthenticationToken;
|
||||
import com.youlai.boot.security.model.SysUserDetails;
|
||||
import com.youlai.boot.security.model.UserAuthCredentials;
|
||||
import com.youlai.boot.security.model.UserAuthInfo;
|
||||
import com.youlai.boot.system.service.UserService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
@@ -16,7 +16,6 @@ import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||
|
||||
|
||||
/**
|
||||
* 短信验证码认证 Provider
|
||||
*
|
||||
@@ -50,14 +49,14 @@ public class SmsAuthenticationProvider implements AuthenticationProvider {
|
||||
String inputVerifyCode = (String) authentication.getCredentials();
|
||||
|
||||
// 根据手机号获取用户信息
|
||||
UserAuthCredentials userAuthCredentials = userService.getAuthCredentialsByMobile(mobile);
|
||||
UserAuthInfo userAuthInfo = userService.getAuthInfoByMobile(mobile);
|
||||
|
||||
if (userAuthCredentials == null) {
|
||||
if (userAuthInfo == null) {
|
||||
throw new UsernameNotFoundException("用户不存在");
|
||||
}
|
||||
|
||||
// 检查用户状态是否有效
|
||||
if (ObjectUtil.notEqual(userAuthCredentials.getStatus(), 1)) {
|
||||
if (ObjectUtil.notEqual(userAuthInfo.getStatus(), 1)) {
|
||||
throw new DisabledException("用户已被禁用");
|
||||
}
|
||||
|
||||
@@ -73,7 +72,7 @@ public class SmsAuthenticationProvider implements AuthenticationProvider {
|
||||
}
|
||||
|
||||
// 构建认证后的用户详情信息
|
||||
SysUserDetails userDetails = new SysUserDetails(userAuthCredentials);
|
||||
SysUserDetails userDetails = new SysUserDetails(userAuthInfo);
|
||||
|
||||
// 创建已认证的 SmsAuthenticationToken
|
||||
return SmsAuthenticationToken.authenticated(
|
||||
|
||||
@@ -5,7 +5,7 @@ import cn.binarywang.wx.miniapp.bean.WxMaJscode2SessionResult;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.youlai.boot.security.model.SysUserDetails;
|
||||
import com.youlai.boot.security.model.UserAuthCredentials;
|
||||
import com.youlai.boot.security.model.UserAuthInfo;
|
||||
import com.youlai.boot.security.model.WxMiniAppCodeAuthenticationToken;
|
||||
import com.youlai.boot.system.service.UserService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -17,7 +17,6 @@ import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||
|
||||
|
||||
/**
|
||||
* 微信小程序Code认证Provider
|
||||
*
|
||||
@@ -30,13 +29,11 @@ public class WxMiniAppCodeAuthenticationProvider implements AuthenticationProvid
|
||||
private final UserService userService;
|
||||
private final WxMaService wxMaService;
|
||||
|
||||
|
||||
public WxMiniAppCodeAuthenticationProvider(UserService userService, WxMaService wxMaService) {
|
||||
this.userService = userService;
|
||||
this.wxMaService = wxMaService;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 微信认证逻辑,参考 Spring Security 认证密码校验流程
|
||||
*
|
||||
@@ -63,26 +60,26 @@ public class WxMiniAppCodeAuthenticationProvider implements AuthenticationProvid
|
||||
}
|
||||
|
||||
// 根据微信 OpenID 查询用户信息
|
||||
UserAuthCredentials userAuthCredentials = userService.getAuthCredentialsByOpenId(openId);
|
||||
UserAuthInfo userAuthInfo = userService.getAuthInfoByOpenId(openId);
|
||||
|
||||
if (userAuthCredentials == null) {
|
||||
if (userAuthInfo == null) {
|
||||
// 用户不存在则注册
|
||||
userService.registerOrBindWechatUser(openId);
|
||||
|
||||
// 再次查询用户信息,确保用户注册成功
|
||||
userAuthCredentials = userService.getAuthCredentialsByOpenId(openId);
|
||||
if (userAuthCredentials == null) {
|
||||
userAuthInfo = userService.getAuthInfoByOpenId(openId);
|
||||
if (userAuthInfo == null) {
|
||||
throw new UsernameNotFoundException("用户注册失败,请稍后重试");
|
||||
}
|
||||
}
|
||||
|
||||
// 检查用户状态是否有效
|
||||
if (ObjectUtil.notEqual(userAuthCredentials.getStatus(), 1)) {
|
||||
if (ObjectUtil.notEqual(userAuthInfo.getStatus(), 1)) {
|
||||
throw new DisabledException("用户已被禁用");
|
||||
}
|
||||
|
||||
// 构建认证后的用户详情信息
|
||||
SysUserDetails userDetails = new SysUserDetails(userAuthCredentials);
|
||||
SysUserDetails userDetails = new SysUserDetails(userAuthInfo);
|
||||
|
||||
// 创建已认证的Token
|
||||
return WxMiniAppCodeAuthenticationToken.authenticated(
|
||||
|
||||
@@ -6,7 +6,7 @@ import cn.binarywang.wx.miniapp.bean.WxMaPhoneNumberInfo;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.youlai.boot.security.model.SysUserDetails;
|
||||
import com.youlai.boot.security.model.UserAuthCredentials;
|
||||
import com.youlai.boot.security.model.UserAuthInfo;
|
||||
import com.youlai.boot.security.model.WxMiniAppPhoneAuthenticationToken;
|
||||
import com.youlai.boot.system.service.UserService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -78,28 +78,28 @@ public class WxMiniAppPhoneAuthenticationProvider implements AuthenticationProvi
|
||||
String phoneNumber = phoneNumberInfo.getPhoneNumber();
|
||||
|
||||
// 3. 根据手机号查询用户,不存在则创建新用户
|
||||
UserAuthCredentials userAuthCredentials = userService.getAuthCredentialsByMobile(phoneNumber);
|
||||
UserAuthInfo userAuthInfo = userService.getAuthInfoByMobile(phoneNumber);
|
||||
|
||||
if (userAuthCredentials == null) {
|
||||
if (userAuthInfo == null) {
|
||||
// 用户不存在,注册新用户
|
||||
boolean registered = userService.registerUserByMobileAndOpenId(phoneNumber, openId);
|
||||
if (!registered) {
|
||||
throw new UsernameNotFoundException("用户注册失败");
|
||||
}
|
||||
// 重新获取用户信息
|
||||
userAuthCredentials = userService.getAuthCredentialsByMobile(phoneNumber);
|
||||
userAuthInfo = userService.getAuthInfoByMobile(phoneNumber);
|
||||
} else {
|
||||
// 用户存在,绑定openId(如果未绑定)
|
||||
userService.bindUserOpenId(userAuthCredentials.getUserId(), openId);
|
||||
userService.bindUserOpenId(userAuthInfo.getUserId(), openId);
|
||||
}
|
||||
|
||||
// 4. 检查用户状态
|
||||
if (ObjectUtil.notEqual(userAuthCredentials.getStatus(), 1)) {
|
||||
if (ObjectUtil.notEqual(userAuthInfo.getStatus(), 1)) {
|
||||
throw new DisabledException("用户已被禁用");
|
||||
}
|
||||
|
||||
// 5. 构建认证后的用户详情
|
||||
SysUserDetails userDetails = new SysUserDetails(userAuthCredentials);
|
||||
SysUserDetails userDetails = new SysUserDetails(userAuthInfo);
|
||||
|
||||
// 6. 创建已认证的Token
|
||||
return WxMiniAppPhoneAuthenticationToken.authenticated(
|
||||
|
||||
@@ -15,8 +15,8 @@ import java.util.*;
|
||||
/**
|
||||
* SpringSecurity 权限校验
|
||||
*
|
||||
* @author haoxr
|
||||
* @since 2022/2/22
|
||||
* @author Ray.Hao
|
||||
* @since 0.0.1
|
||||
*/
|
||||
@Component("ss")
|
||||
@RequiredArgsConstructor
|
||||
@@ -73,15 +73,16 @@ public class PermissionService {
|
||||
* @return 角色权限列表
|
||||
*/
|
||||
public Set<String> getRolePermsFormCache(Set<String> roleCodes) {
|
||||
// 检查输入是否为空
|
||||
if (CollectionUtil.isEmpty(roleCodes)) {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
|
||||
// 构建缓存Key
|
||||
String cacheKey = RedisConstants.System.ROLE_PERMS;
|
||||
|
||||
Set<String> perms = new HashSet<>();
|
||||
// 从缓存中一次性获取所有角色的权限
|
||||
Collection<Object> roleCodesAsObjects = new ArrayList<>(roleCodes);
|
||||
List<Object> rolePermsList = redisTemplate.opsForHash().multiGet(RedisConstants.System.ROLE_PERMS, roleCodesAsObjects);
|
||||
List<Object> rolePermsList = redisTemplate.opsForHash().multiGet(cacheKey, roleCodesAsObjects);
|
||||
|
||||
for (Object rolePermsObj : rolePermsList) {
|
||||
if (rolePermsObj instanceof Set) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.youlai.boot.security.service;
|
||||
|
||||
import com.youlai.boot.security.model.SysUserDetails;
|
||||
import com.youlai.boot.security.model.UserAuthCredentials;
|
||||
import com.youlai.boot.security.model.UserAuthInfo;
|
||||
import com.youlai.boot.system.service.UserService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -33,11 +33,11 @@ public class SysUserDetailsService implements UserDetailsService {
|
||||
@Override
|
||||
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
|
||||
try {
|
||||
UserAuthCredentials userAuthCredentials = userService.getAuthCredentialsByUsername(username);
|
||||
if (userAuthCredentials == null) {
|
||||
UserAuthInfo userAuthInfo = userService.getAuthInfoByUsername(username);
|
||||
if (userAuthInfo == null) {
|
||||
throw new UsernameNotFoundException(username);
|
||||
}
|
||||
return new SysUserDetails(userAuthCredentials);
|
||||
return new SysUserDetails(userAuthInfo);
|
||||
} catch (Exception e) {
|
||||
// 记录异常日志
|
||||
log.error("认证异常:{}", e.getMessage());
|
||||
|
||||
@@ -132,49 +132,46 @@ public class JwtTokenManager implements TokenManager {
|
||||
* @return 是否有效
|
||||
*/
|
||||
private boolean validateToken(String token, boolean validateRefreshToken) {
|
||||
try {
|
||||
JWT jwt = JWTUtil.parseToken(token);
|
||||
// 检查 Token 是否有效(验签 + 是否过期)
|
||||
boolean isValid = jwt.setKey(secretKey).validate(0);
|
||||
JWT jwt = JWTUtil.parseToken(token);
|
||||
// 检查 Token 是否有效(验签 + 是否过期)
|
||||
boolean isValid = jwt.setKey(secretKey).validate(0);
|
||||
|
||||
if (isValid) {
|
||||
JSONObject payloads = jwt.getPayloads();
|
||||
// 1. 校验刷新令牌类型(仅在校验刷新令牌场景启用)
|
||||
String jti = payloads.getStr(JWTPayload.JWT_ID);
|
||||
if (validateRefreshToken) {
|
||||
//刷新token需要校验token类别
|
||||
boolean isRefreshToken = payloads.getBool(JwtClaimConstants.TOKEN_TYPE);
|
||||
if (!isRefreshToken) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// 2. 校验安全版本号(用于按用户维度失效历史 Token)
|
||||
Long userId = payloads.getLong(JwtClaimConstants.USER_ID);
|
||||
if (userId != null) {
|
||||
// 老版本 Token 可能没有 SECURITY_VERSION 声明,视为 0 版本
|
||||
Integer tokenVersionRaw = payloads.getInt(JwtClaimConstants.SECURITY_VERSION);
|
||||
int tokenVersion = tokenVersionRaw != null ? tokenVersionRaw : 0;
|
||||
|
||||
String versionKey = StrUtil.format(RedisConstants.Auth.USER_SECURITY_VERSION, userId);
|
||||
Integer currentVersionRaw = (Integer) redisTemplate.opsForValue().get(versionKey);
|
||||
int currentVersion = currentVersionRaw != null ? currentVersionRaw : 0;
|
||||
|
||||
// 如果当前版本号比 Token 携带的版本号新,则认为该 Token 已失效
|
||||
if (tokenVersion < currentVersion) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 判断是否在黑名单中,如果在,则返回 false 标识Token无效
|
||||
if (Boolean.TRUE.equals(redisTemplate.hasKey(StrUtil.format(RedisConstants.Auth.BLACKLIST_TOKEN, jti)))) {
|
||||
if (isValid) {
|
||||
JSONObject payloads = jwt.getPayloads();
|
||||
// 1. 校验刷新令牌类型(仅在校验刷新令牌场景启用)
|
||||
String jti = payloads.getStr(JWTPayload.JWT_ID);
|
||||
if (validateRefreshToken) {
|
||||
//刷新token需要校验token类别
|
||||
boolean isRefreshToken = payloads.getBool(JwtClaimConstants.TOKEN_TYPE);
|
||||
if (!isRefreshToken) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return isValid;
|
||||
} catch (Exception gitignore) {
|
||||
// token 验证
|
||||
// 2. 校验安全版本号(用于按用户维度失效历史 Token)
|
||||
// 场景示例:用户修改密码、被管理员强制下线、手动“踢所有端”后,将用户安全版本号 +1,旧版本 Token 全部失效
|
||||
Long userId = payloads.getLong(JwtClaimConstants.USER_ID);
|
||||
if (userId != null) {
|
||||
// 老版本 Token 可能没有 SECURITY_VERSION 声明,视为 0 版本
|
||||
Integer tokenVersionRaw = payloads.getInt(JwtClaimConstants.SECURITY_VERSION);
|
||||
int tokenVersion = tokenVersionRaw != null ? tokenVersionRaw : 0;
|
||||
|
||||
String versionKey = StrUtil.format(RedisConstants.Auth.USER_SECURITY_VERSION, userId);
|
||||
Integer currentVersionRaw = (Integer) redisTemplate.opsForValue().get(versionKey);
|
||||
int currentVersion = currentVersionRaw != null ? currentVersionRaw : 0;
|
||||
|
||||
// 如果当前版本号比 Token 携带的版本号新,则认为该 Token 已失效
|
||||
if (tokenVersion < currentVersion) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 判断是否在黑名单中,如果在,则返回 false 标识Token无效
|
||||
// 场景示例:单点退出登录、后台手动注销某个会话、封禁账号后立即阻断当前 Token 等
|
||||
if (Boolean.TRUE.equals(redisTemplate.hasKey(StrUtil.format(RedisConstants.Auth.BLACKLIST_TOKEN, jti)))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
return isValid;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -210,7 +207,6 @@ public class JwtTokenManager implements TokenManager {
|
||||
// 永不过期的Token永久加入黑名单
|
||||
redisTemplate.opsForValue().set(blacklistTokenKey, Boolean.TRUE);
|
||||
}
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -165,8 +165,8 @@ public class RedisTokenManager implements TokenManager {
|
||||
*/
|
||||
@Override
|
||||
public void invalidateToken(String token) {
|
||||
OnlineUser onlineUser = (OnlineUser) redisTemplate.opsForValue().get(formatTokenKey(token));
|
||||
if (onlineUser != null) {
|
||||
Object value = redisTemplate.opsForValue().get(formatTokenKey(token));
|
||||
if (value instanceof OnlineUser onlineUser) {
|
||||
Long userId = onlineUser.getUserId();
|
||||
invalidateUserSessions(userId);
|
||||
}
|
||||
@@ -186,20 +186,18 @@ public class RedisTokenManager implements TokenManager {
|
||||
// 1. 删除访问令牌相关
|
||||
String userAccessKey = StrUtil.format(RedisConstants.Auth.USER_ACCESS_TOKEN, userId);
|
||||
Object accessTokenValue = redisTemplate.opsForValue().get(userAccessKey);
|
||||
Optional.of(accessTokenValue)
|
||||
.map(String.class::cast)
|
||||
.ifPresent(accessToken -> redisTemplate.delete(formatTokenKey(accessToken)));
|
||||
if (accessTokenValue instanceof String accessToken) {
|
||||
redisTemplate.delete(formatTokenKey(accessToken));
|
||||
}
|
||||
// 无论是否存在访问令牌映射,都尝试删除 userAccessKey
|
||||
redisTemplate.delete(userAccessKey);
|
||||
|
||||
// 2. 删除刷新令牌相关
|
||||
String userRefreshKey = StrUtil.format(RedisConstants.Auth.USER_REFRESH_TOKEN, userId);
|
||||
Object refreshTokenValue = redisTemplate.opsForValue().get(userRefreshKey);
|
||||
Optional.of(refreshTokenValue)
|
||||
.map(String.class::cast)
|
||||
.ifPresent(refreshToken ->
|
||||
redisTemplate.delete(StrUtil.format(RedisConstants.Auth.REFRESH_TOKEN_USER, refreshToken))
|
||||
);
|
||||
if (refreshTokenValue instanceof String refreshToken) {
|
||||
redisTemplate.delete(StrUtil.format(RedisConstants.Auth.REFRESH_TOKEN_USER, refreshToken));
|
||||
}
|
||||
// 同样清理 userRefreshKey 本身
|
||||
redisTemplate.delete(userRefreshKey);
|
||||
}
|
||||
@@ -237,9 +235,9 @@ public class RedisTokenManager implements TokenManager {
|
||||
// 单设备登录控制,删除旧的访问令牌
|
||||
if (!allowMultiLogin) {
|
||||
Object oldAccessTokenValue = redisTemplate.opsForValue().get(userAccessKey);
|
||||
Optional.of(oldAccessTokenValue)
|
||||
.map(String.class::cast)
|
||||
.ifPresent(oldAccessToken -> redisTemplate.delete(formatTokenKey(oldAccessToken)));
|
||||
if (oldAccessTokenValue instanceof String oldAccessToken) {
|
||||
redisTemplate.delete(formatTokenKey(oldAccessToken));
|
||||
}
|
||||
}
|
||||
// 存储访问令牌映射(用户ID -> 访问令牌),用于单设备登录控制删除旧的访问令牌和刷新令牌时删除旧令牌
|
||||
setRedisValue(userAccessKey, accessToken, securityProperties.getSession().getAccessTokenTimeToLive());
|
||||
|
||||
@@ -113,7 +113,7 @@ public class SecurityUtils {
|
||||
*
|
||||
* @return Token 字符串
|
||||
*/
|
||||
public static String getTokenFromRequest() {
|
||||
public static String getAccessToken() {
|
||||
ServletRequestAttributes servletRequestAttributes = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes());
|
||||
if(Objects.isNull(servletRequestAttributes)) {
|
||||
return null;
|
||||
|
||||
@@ -7,7 +7,7 @@ import com.youlai.boot.core.web.Result;
|
||||
import com.youlai.boot.common.annotation.Log;
|
||||
import com.youlai.boot.system.model.form.ConfigForm;
|
||||
import com.youlai.boot.system.model.query.ConfigPageQuery;
|
||||
import com.youlai.boot.system.model.vo.ConfigVO;
|
||||
import com.youlai.boot.system.model.vo.ConfigVo;
|
||||
import com.youlai.boot.system.service.ConfigService;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import jakarta.validation.Valid;
|
||||
@@ -29,23 +29,23 @@ import org.springframework.security.access.prepost.PreAuthorize;
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@Tag(name = "08.系统配置")
|
||||
@RequestMapping("/api/v1/config")
|
||||
@RequestMapping("/api/v1/configs")
|
||||
public class ConfigController {
|
||||
|
||||
private final ConfigService configService;
|
||||
|
||||
@Operation(summary = "系统配置分页列表")
|
||||
@GetMapping("/page")
|
||||
@PreAuthorize("@ss.hasPerm('sys:config:query')")
|
||||
@PreAuthorize("@ss.hasPerm('sys:config:list')")
|
||||
@Log( value = "系统配置分页列表",module = LogModuleEnum.SETTING)
|
||||
public PageResult<ConfigVO> page(@ParameterObject ConfigPageQuery configPageQuery) {
|
||||
IPage<ConfigVO> result = configService.page(configPageQuery);
|
||||
public PageResult<ConfigVo> page(@ParameterObject ConfigPageQuery configPageQuery) {
|
||||
IPage<ConfigVo> result = configService.page(configPageQuery);
|
||||
return PageResult.success(result);
|
||||
}
|
||||
|
||||
@Operation(summary = "新增系统配置")
|
||||
@PostMapping
|
||||
@PreAuthorize("@ss.hasPerm('sys:config:add')")
|
||||
@PreAuthorize("@ss.hasPerm('sys:config:create')")
|
||||
@Log( value = "新增系统配置",module = LogModuleEnum.SETTING)
|
||||
public Result<?> save(@RequestBody @Valid ConfigForm configForm) {
|
||||
return Result.judge(configService.save(configForm));
|
||||
|
||||
@@ -6,7 +6,7 @@ import com.youlai.boot.common.model.Option;
|
||||
import com.youlai.boot.core.web.Result;
|
||||
import com.youlai.boot.system.model.form.DeptForm;
|
||||
import com.youlai.boot.system.model.query.DeptQuery;
|
||||
import com.youlai.boot.system.model.vo.DeptVO;
|
||||
import com.youlai.boot.system.model.vo.DeptVo;
|
||||
import com.youlai.boot.common.annotation.Log;
|
||||
import com.youlai.boot.system.service.DeptService;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
@@ -27,7 +27,7 @@ import java.util.List;
|
||||
*/
|
||||
@Tag(name = "05.部门接口")
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/dept")
|
||||
@RequestMapping("/api/v1/depts")
|
||||
@RequiredArgsConstructor
|
||||
public class DeptController {
|
||||
|
||||
@@ -36,10 +36,10 @@ public class DeptController {
|
||||
@Operation(summary = "部门列表")
|
||||
@GetMapping
|
||||
@Log( value = "部门列表",module = LogModuleEnum.DEPT)
|
||||
public Result<List<DeptVO>> getDeptList(
|
||||
public Result<List<DeptVo>> getDeptList(
|
||||
DeptQuery queryParams
|
||||
) {
|
||||
List<DeptVO> list = deptService.getDeptList(queryParams);
|
||||
List<DeptVo> list = deptService.getDeptList(queryParams);
|
||||
return Result.success(list);
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ public class DeptController {
|
||||
|
||||
@Operation(summary = "新增部门")
|
||||
@PostMapping
|
||||
@PreAuthorize("@ss.hasPerm('sys:dept:add')")
|
||||
@PreAuthorize("@ss.hasPerm('sys:dept:create')")
|
||||
@RepeatSubmit
|
||||
public Result<?> saveDept(
|
||||
@Valid @RequestBody DeptForm formData
|
||||
@@ -72,7 +72,7 @@ public class DeptController {
|
||||
|
||||
@Operation(summary = "修改部门")
|
||||
@PutMapping(value = "/{deptId}")
|
||||
@PreAuthorize("@ss.hasPerm('sys:dept:edit')")
|
||||
@PreAuthorize("@ss.hasPerm('sys:dept:update')")
|
||||
public Result<?> updateDept(
|
||||
@PathVariable Long deptId,
|
||||
@Valid @RequestBody DeptForm formData
|
||||
|
||||
@@ -8,9 +8,9 @@ import com.youlai.boot.common.enums.LogModuleEnum;
|
||||
import com.youlai.boot.system.model.form.DictItemForm;
|
||||
import com.youlai.boot.system.model.query.DictItemPageQuery;
|
||||
import com.youlai.boot.system.model.query.DictPageQuery;
|
||||
import com.youlai.boot.system.model.vo.DictItemOptionVO;
|
||||
import com.youlai.boot.system.model.vo.DictItemPageVO;
|
||||
import com.youlai.boot.system.model.vo.DictPageVO;
|
||||
import com.youlai.boot.system.model.vo.DictItemOptionVo;
|
||||
import com.youlai.boot.system.model.vo.DictItemPageVo;
|
||||
import com.youlai.boot.system.model.vo.DictPageVo;
|
||||
import com.youlai.boot.common.annotation.RepeatSubmit;
|
||||
import com.youlai.boot.system.model.form.DictForm;
|
||||
import com.youlai.boot.common.annotation.Log;
|
||||
@@ -36,7 +36,6 @@ import java.util.List;
|
||||
*/
|
||||
@Tag(name = "06.字典接口")
|
||||
@RestController
|
||||
@SuppressWarnings("SpellCheckingInspection")
|
||||
@RequestMapping("/api/v1/dicts")
|
||||
@RequiredArgsConstructor
|
||||
public class DictController {
|
||||
@@ -51,10 +50,10 @@ public class DictController {
|
||||
@Operation(summary = "字典分页列表")
|
||||
@GetMapping("/page")
|
||||
@Log( value = "字典分页列表",module = LogModuleEnum.DICT)
|
||||
public PageResult<DictPageVO> getDictPage(
|
||||
public PageResult<DictPageVo> getDictPage(
|
||||
DictPageQuery queryParams
|
||||
) {
|
||||
Page<DictPageVO> result = dictService.getDictPage(queryParams);
|
||||
Page<DictPageVo> result = dictService.getDictPage(queryParams);
|
||||
return PageResult.success(result);
|
||||
}
|
||||
|
||||
@@ -77,7 +76,7 @@ public class DictController {
|
||||
|
||||
@Operation(summary = "新增字典")
|
||||
@PostMapping
|
||||
@PreAuthorize("@ss.hasPerm('sys:dict:add')")
|
||||
@PreAuthorize("@ss.hasPerm('sys:dict:create')")
|
||||
@RepeatSubmit
|
||||
public Result<?> saveDict(@Valid @RequestBody DictForm formData) {
|
||||
boolean result = dictService.saveDict(formData);
|
||||
@@ -90,7 +89,7 @@ public class DictController {
|
||||
|
||||
@Operation(summary = "修改字典")
|
||||
@PutMapping("/{id}")
|
||||
@PreAuthorize("@ss.hasPerm('sys:dict:edit')")
|
||||
@PreAuthorize("@ss.hasPerm('sys:dict:update')")
|
||||
public Result<?> updateDict(
|
||||
@PathVariable Long id,
|
||||
@RequestBody DictForm dictForm
|
||||
@@ -128,27 +127,27 @@ public class DictController {
|
||||
//---------------------------------------------------
|
||||
@Operation(summary = "字典项分页列表")
|
||||
@GetMapping("/{dictCode}/items/page")
|
||||
public PageResult<DictItemPageVO> getDictItemPage(
|
||||
public PageResult<DictItemPageVo> getDictItemPage(
|
||||
@PathVariable String dictCode,
|
||||
DictItemPageQuery queryParams
|
||||
) {
|
||||
queryParams.setDictCode(dictCode);
|
||||
Page<DictItemPageVO> result = dictItemService.getDictItemPage(queryParams);
|
||||
Page<DictItemPageVo> result = dictItemService.getDictItemPage(queryParams);
|
||||
return PageResult.success(result);
|
||||
}
|
||||
|
||||
@Operation(summary = "字典项列表")
|
||||
@GetMapping("/{dictCode}/items")
|
||||
public Result<List<DictItemOptionVO>> getDictItems(
|
||||
public Result<List<DictItemOptionVo>> getDictItems(
|
||||
@Parameter(description = "字典编码") @PathVariable String dictCode
|
||||
) {
|
||||
List<DictItemOptionVO> list = dictItemService.getDictItems(dictCode);
|
||||
List<DictItemOptionVo> list = dictItemService.getDictItems(dictCode);
|
||||
return Result.success(list);
|
||||
}
|
||||
|
||||
@Operation(summary = "新增字典项")
|
||||
@PostMapping("/{dictCode}/items")
|
||||
@PreAuthorize("@ss.hasPerm('sys:dict-item:add')")
|
||||
@PreAuthorize("@ss.hasPerm('sys:dict-item:create')")
|
||||
@RepeatSubmit
|
||||
public Result<Void> saveDictItem(
|
||||
@PathVariable String dictCode,
|
||||
@@ -177,7 +176,7 @@ public class DictController {
|
||||
|
||||
@Operation(summary = "修改字典项")
|
||||
@PutMapping("/{dictCode}/items/{itemId}")
|
||||
@PreAuthorize("@ss.hasPerm('sys:dict-item:edit')")
|
||||
@PreAuthorize("@ss.hasPerm('sys:dict-item:update')")
|
||||
@RepeatSubmit
|
||||
public Result<?> updateDictItem(
|
||||
@PathVariable String dictCode,
|
||||
|
||||
@@ -2,20 +2,14 @@ package com.youlai.boot.system.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.youlai.boot.core.web.PageResult;
|
||||
import com.youlai.boot.core.web.Result;
|
||||
import com.youlai.boot.system.model.query.LogPageQuery;
|
||||
import com.youlai.boot.system.model.vo.LogPageVO;
|
||||
import com.youlai.boot.system.model.vo.VisitStatsVO;
|
||||
import com.youlai.boot.system.model.vo.VisitTrendVO;
|
||||
import com.youlai.boot.system.model.vo.LogPageVo;
|
||||
import com.youlai.boot.system.service.LogService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
||||
/**
|
||||
* 日志控制层
|
||||
*
|
||||
@@ -32,30 +26,11 @@ public class LogController {
|
||||
|
||||
@Operation(summary = "日志分页列表")
|
||||
@GetMapping("/page")
|
||||
public PageResult<LogPageVO> getLogPage(
|
||||
public PageResult<LogPageVo> getLogPage(
|
||||
LogPageQuery queryParams
|
||||
) {
|
||||
Page<LogPageVO> result = logService.getLogPage(queryParams);
|
||||
Page<LogPageVo> result = logService.getLogPage(queryParams);
|
||||
return PageResult.success(result);
|
||||
}
|
||||
|
||||
@Operation(summary = "获取访问趋势")
|
||||
@GetMapping("/visit-trend")
|
||||
public Result<VisitTrendVO> getVisitTrend(
|
||||
@Parameter(description = "开始时间", example = "yyyy-MM-dd") @RequestParam String startDate,
|
||||
@Parameter(description = "结束时间", example = "yyyy-MM-dd") @RequestParam String endDate
|
||||
) {
|
||||
LocalDate start = LocalDate.parse(startDate);
|
||||
LocalDate end = LocalDate.parse(endDate);
|
||||
VisitTrendVO data = logService.getVisitTrend(start, end);
|
||||
return Result.success(data);
|
||||
}
|
||||
|
||||
@Operation(summary = "获取访问统计")
|
||||
@GetMapping("/visit-stats")
|
||||
public Result<VisitStatsVO> getVisitStats() {
|
||||
VisitStatsVO result = logService.getVisitStats();
|
||||
return Result.success(result);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -7,8 +7,8 @@ import com.youlai.boot.common.model.Option;
|
||||
import com.youlai.boot.core.web.Result;
|
||||
import com.youlai.boot.system.model.form.MenuForm;
|
||||
import com.youlai.boot.system.model.query.MenuQuery;
|
||||
import com.youlai.boot.system.model.vo.MenuVO;
|
||||
import com.youlai.boot.system.model.vo.RouteVO;
|
||||
import com.youlai.boot.system.model.vo.MenuVo;
|
||||
import com.youlai.boot.system.model.vo.RouteVo;
|
||||
import com.youlai.boot.system.service.MenuService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
@@ -38,8 +38,8 @@ public class MenuController {
|
||||
@Operation(summary = "菜单列表")
|
||||
@GetMapping
|
||||
@Log(value = "菜单列表", module = LogModuleEnum.MENU)
|
||||
public Result<List<MenuVO>> getMenus(MenuQuery queryParams) {
|
||||
List<MenuVO> menuList = menuService.listMenus(queryParams);
|
||||
public Result<List<MenuVo>> getMenus(MenuQuery queryParams) {
|
||||
List<MenuVo> menuList = menuService.listMenus(queryParams);
|
||||
return Result.success(menuList);
|
||||
}
|
||||
|
||||
@@ -55,14 +55,14 @@ public class MenuController {
|
||||
|
||||
@Operation(summary = "当前用户菜单路由列表")
|
||||
@GetMapping("/routes")
|
||||
public Result<List<RouteVO>> getCurrentUserRoutes() {
|
||||
List<RouteVO> routeList = menuService.listCurrentUserRoutes();
|
||||
public Result<List<RouteVo>> getCurrentUserRoutes() {
|
||||
List<RouteVo> routeList = menuService.listCurrentUserRoutes();
|
||||
return Result.success(routeList);
|
||||
}
|
||||
|
||||
@Operation(summary = "菜单表单数据")
|
||||
@GetMapping("/{id}/form")
|
||||
@PreAuthorize("@ss.hasPerm('sys:menu:edit')")
|
||||
@PreAuthorize("@ss.hasPerm('sys:menu:update')")
|
||||
public Result<MenuForm> getMenuForm(
|
||||
@Parameter(description = "菜单ID") @PathVariable Long id
|
||||
) {
|
||||
@@ -72,7 +72,7 @@ public class MenuController {
|
||||
|
||||
@Operation(summary = "新增菜单")
|
||||
@PostMapping
|
||||
@PreAuthorize("@ss.hasPerm('sys:menu:add')")
|
||||
@PreAuthorize("@ss.hasPerm('sys:menu:create')")
|
||||
@RepeatSubmit
|
||||
public Result<?> addMenu(@RequestBody MenuForm menuForm) {
|
||||
boolean result = menuService.saveMenu(menuForm);
|
||||
@@ -81,7 +81,7 @@ public class MenuController {
|
||||
|
||||
@Operation(summary = "修改菜单")
|
||||
@PutMapping(value = "/{id}")
|
||||
@PreAuthorize("@ss.hasPerm('sys:menu:edit')")
|
||||
@PreAuthorize("@ss.hasPerm('sys:menu:update')")
|
||||
public Result<?> updateMenu(
|
||||
@RequestBody MenuForm menuForm
|
||||
) {
|
||||
@@ -101,7 +101,7 @@ public class MenuController {
|
||||
|
||||
@Operation(summary = "修改菜单显示状态")
|
||||
@PatchMapping("/{menuId}")
|
||||
@PreAuthorize("@ss.hasPerm('sys:menu:edit')")
|
||||
@PreAuthorize("@ss.hasPerm('sys:menu:update')")
|
||||
public Result<?> updateMenuVisible(
|
||||
@Parameter(description = "菜单ID") @PathVariable Long menuId,
|
||||
@Parameter(description = "显示状态(1:显示;0:隐藏)") Integer visible
|
||||
|
||||
@@ -5,9 +5,9 @@ import com.youlai.boot.core.web.PageResult;
|
||||
import com.youlai.boot.core.web.Result;
|
||||
import com.youlai.boot.system.model.form.NoticeForm;
|
||||
import com.youlai.boot.system.model.query.NoticePageQuery;
|
||||
import com.youlai.boot.system.model.vo.NoticeDetailVO;
|
||||
import com.youlai.boot.system.model.vo.NoticePageVO;
|
||||
import com.youlai.boot.system.model.vo.UserNoticePageVO;
|
||||
import com.youlai.boot.system.model.vo.NoticeDetailVo;
|
||||
import com.youlai.boot.system.model.vo.NoticePageVo;
|
||||
import com.youlai.boot.system.model.vo.UserNoticePageVo;
|
||||
import com.youlai.boot.system.service.NoticeService;
|
||||
import com.youlai.boot.system.service.UserNoticeService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
@@ -37,15 +37,15 @@ public class NoticeController {
|
||||
|
||||
@Operation(summary = "通知公告分页列表")
|
||||
@GetMapping("/page")
|
||||
@PreAuthorize("@ss.hasPerm('sys:notice:query')")
|
||||
public PageResult<NoticePageVO> getNoticePage(NoticePageQuery queryParams) {
|
||||
IPage<NoticePageVO> result = noticeService.getNoticePage(queryParams);
|
||||
@PreAuthorize("@ss.hasPerm('sys:notice:list')")
|
||||
public PageResult<NoticePageVo> getNoticePage(NoticePageQuery queryParams) {
|
||||
IPage<NoticePageVo> result = noticeService.getNoticePage(queryParams);
|
||||
return PageResult.success(result);
|
||||
}
|
||||
|
||||
@Operation(summary = "新增通知公告")
|
||||
@PostMapping
|
||||
@PreAuthorize("@ss.hasPerm('sys:notice:add')")
|
||||
@PreAuthorize("@ss.hasPerm('sys:notice:create')")
|
||||
public Result<?> saveNotice(@RequestBody @Valid NoticeForm formData) {
|
||||
boolean result = noticeService.saveNotice(formData);
|
||||
return Result.judge(result);
|
||||
@@ -53,7 +53,7 @@ public class NoticeController {
|
||||
|
||||
@Operation(summary = "获取通知公告表单数据")
|
||||
@GetMapping("/{id}/form")
|
||||
@PreAuthorize("@ss.hasPerm('sys:notice:edit')")
|
||||
@PreAuthorize("@ss.hasPerm('sys:notice:update')")
|
||||
public Result<NoticeForm> getNoticeForm(
|
||||
@Parameter(description = "通知公告ID") @PathVariable Long id
|
||||
) {
|
||||
@@ -63,16 +63,16 @@ public class NoticeController {
|
||||
|
||||
@Operation(summary = "阅读获取通知公告详情")
|
||||
@GetMapping("/{id}/detail")
|
||||
public Result<NoticeDetailVO> getNoticeDetail(
|
||||
public Result<NoticeDetailVo> getNoticeDetail(
|
||||
@Parameter(description = "通知公告ID") @PathVariable Long id
|
||||
) {
|
||||
NoticeDetailVO detailVO = noticeService.getNoticeDetail(id);
|
||||
return Result.success(detailVO);
|
||||
NoticeDetailVo detailVo = noticeService.getNoticeDetail(id);
|
||||
return Result.success(detailVo);
|
||||
}
|
||||
|
||||
@Operation(summary = "修改通知公告")
|
||||
@PutMapping(value = "/{id}")
|
||||
@PreAuthorize("@ss.hasPerm('sys:notice:edit')")
|
||||
@PreAuthorize("@ss.hasPerm('sys:notice:update')")
|
||||
public Result<Void> updateNotice(
|
||||
@Parameter(description = "通知公告ID") @PathVariable Long id,
|
||||
@RequestBody @Validated NoticeForm formData
|
||||
@@ -120,10 +120,10 @@ public class NoticeController {
|
||||
|
||||
@Operation(summary = "获取我的通知公告分页列表")
|
||||
@GetMapping("/my")
|
||||
public PageResult<UserNoticePageVO> getMyNoticePage(
|
||||
public PageResult<UserNoticePageVo> getMyNoticePage(
|
||||
NoticePageQuery queryParams
|
||||
) {
|
||||
IPage<UserNoticePageVO> result = noticeService.getMyNoticePage(queryParams);
|
||||
IPage<UserNoticePageVo> result = noticeService.getMyNoticePage(queryParams);
|
||||
return PageResult.success(result);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import com.youlai.boot.core.web.PageResult;
|
||||
import com.youlai.boot.core.web.Result;
|
||||
import com.youlai.boot.system.model.form.RoleForm;
|
||||
import com.youlai.boot.system.model.query.RolePageQuery;
|
||||
import com.youlai.boot.system.model.vo.RolePageVO;
|
||||
import com.youlai.boot.system.model.vo.RolePageVo;
|
||||
import com.youlai.boot.common.annotation.Log;
|
||||
import com.youlai.boot.system.service.RoleService;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
@@ -39,10 +39,10 @@ public class RoleController {
|
||||
@Operation(summary = "角色分页列表")
|
||||
@GetMapping("/page")
|
||||
@Log(value = "角色分页列表", module = LogModuleEnum.ROLE)
|
||||
public PageResult<RolePageVO> getRolePage(
|
||||
public PageResult<RolePageVo> getRolePage(
|
||||
RolePageQuery queryParams
|
||||
) {
|
||||
Page<RolePageVO> result = roleService.getRolePage(queryParams);
|
||||
Page<RolePageVo> result = roleService.getRolePage(queryParams);
|
||||
return PageResult.success(result);
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ public class RoleController {
|
||||
|
||||
@Operation(summary = "新增角色")
|
||||
@PostMapping
|
||||
@PreAuthorize("@ss.hasPerm('sys:role:add')")
|
||||
@PreAuthorize("@ss.hasPerm('sys:role:create')")
|
||||
@RepeatSubmit
|
||||
public Result<?> addRole(@Valid @RequestBody RoleForm roleForm) {
|
||||
boolean result = roleService.saveRole(roleForm);
|
||||
@@ -64,7 +64,7 @@ public class RoleController {
|
||||
|
||||
@Operation(summary = "获取角色表单数据")
|
||||
@GetMapping("/{roleId}/form")
|
||||
@PreAuthorize("@ss.hasPerm('sys:role:edit')")
|
||||
@PreAuthorize("@ss.hasPerm('sys:role:update')")
|
||||
public Result<RoleForm> getRoleForm(
|
||||
@Parameter(description = "角色ID") @PathVariable Long roleId
|
||||
) {
|
||||
@@ -74,7 +74,7 @@ public class RoleController {
|
||||
|
||||
@Operation(summary = "修改角色")
|
||||
@PutMapping(value = "/{id}")
|
||||
@PreAuthorize("@ss.hasPerm('sys:role:edit')")
|
||||
@PreAuthorize("@ss.hasPerm('sys:role:update')")
|
||||
public Result<?> updateRole(@Valid @RequestBody RoleForm roleForm) {
|
||||
boolean result = roleService.saveRole(roleForm);
|
||||
return Result.judge(result);
|
||||
@@ -92,7 +92,7 @@ public class RoleController {
|
||||
|
||||
@Operation(summary = "修改角色状态")
|
||||
@PutMapping(value = "/{roleId}/status")
|
||||
@PreAuthorize("@ss.hasPerm('sys:role:edit')")
|
||||
@PreAuthorize("@ss.hasPerm('sys:role:update')")
|
||||
public Result<?> updateRoleStatus(
|
||||
@Parameter(description = "角色ID") @PathVariable Long roleId,
|
||||
@Parameter(description = "状态(1:启用;0:禁用)") @RequestParam Integer status
|
||||
@@ -112,6 +112,7 @@ public class RoleController {
|
||||
|
||||
@Operation(summary = "角色分配菜单权限")
|
||||
@PutMapping("/{roleId}/menus")
|
||||
@PreAuthorize("@ss.hasPerm('sys:role:assign')")
|
||||
public Result<Void> assignMenusToRole(
|
||||
@PathVariable Long roleId,
|
||||
@RequestBody List<Long> menuIds
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.youlai.boot.system.controller;
|
||||
|
||||
import com.youlai.boot.core.web.Result;
|
||||
import com.youlai.boot.system.model.vo.VisitStatsVo;
|
||||
import com.youlai.boot.system.model.vo.VisitTrendVo;
|
||||
import com.youlai.boot.system.service.LogService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
||||
/**
|
||||
* 统计分析控制层
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 2025-12-15
|
||||
*/
|
||||
@Tag(name = "11.统计分析")
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/statistics")
|
||||
@RequiredArgsConstructor
|
||||
public class StatisticsController {
|
||||
|
||||
private final LogService logService;
|
||||
|
||||
@Operation(summary = "访问趋势统计")
|
||||
@GetMapping("/visits/trend")
|
||||
public Result<VisitTrendVo> getVisitTrend(
|
||||
@Parameter(description = "开始时间", example = "2024-01-01") @RequestParam String startDate,
|
||||
@Parameter(description = "结束时间", example = "2024-12-31") @RequestParam String endDate
|
||||
) {
|
||||
LocalDate start = LocalDate.parse(startDate);
|
||||
LocalDate end = LocalDate.parse(endDate);
|
||||
VisitTrendVo data = logService.getVisitTrend(start, end);
|
||||
return Result.success(data);
|
||||
}
|
||||
|
||||
@Operation(summary = "访问概览统计")
|
||||
@GetMapping("/visits/overview")
|
||||
public Result<VisitStatsVo> getVisitOverview() {
|
||||
VisitStatsVo result = logService.getVisitStats();
|
||||
return Result.success(result);
|
||||
}
|
||||
}
|
||||
@@ -14,14 +14,14 @@ import com.youlai.boot.core.web.Result;
|
||||
import com.youlai.boot.common.util.ExcelUtils;
|
||||
import com.youlai.boot.security.util.SecurityUtils;
|
||||
import com.youlai.boot.system.listener.UserImportListener;
|
||||
import com.youlai.boot.system.model.dto.UserExportDTO;
|
||||
import com.youlai.boot.system.model.dto.UserImportDTO;
|
||||
import com.youlai.boot.system.model.dto.UserExportDto;
|
||||
import com.youlai.boot.system.model.dto.UserImportDto;
|
||||
import com.youlai.boot.system.model.entity.User;
|
||||
import com.youlai.boot.system.model.form.*;
|
||||
import com.youlai.boot.system.model.query.UserPageQuery;
|
||||
import com.youlai.boot.system.model.dto.CurrentUserDTO;
|
||||
import com.youlai.boot.system.model.vo.UserPageVO;
|
||||
import com.youlai.boot.system.model.vo.UserProfileVO;
|
||||
import com.youlai.boot.system.model.dto.CurrentUserDto;
|
||||
import com.youlai.boot.system.model.vo.UserPageVo;
|
||||
import com.youlai.boot.system.model.vo.UserProfileVo;
|
||||
import com.youlai.boot.system.service.UserService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
@@ -59,16 +59,16 @@ public class UserController {
|
||||
@Operation(summary = "用户分页列表")
|
||||
@GetMapping("/page")
|
||||
@Log(value = "用户分页列表", module = LogModuleEnum.USER)
|
||||
public PageResult<UserPageVO> getUserPage(
|
||||
public PageResult<UserPageVo> getUserPage(
|
||||
@Valid UserPageQuery queryParams
|
||||
) {
|
||||
IPage<UserPageVO> result = userService.getUserPage(queryParams);
|
||||
IPage<UserPageVo> result = userService.getUserPage(queryParams);
|
||||
return PageResult.success(result);
|
||||
}
|
||||
|
||||
@Operation(summary = "新增用户")
|
||||
@PostMapping
|
||||
@PreAuthorize("@ss.hasPerm('sys:user:add')")
|
||||
@PreAuthorize("@ss.hasPerm('sys:user:create')")
|
||||
@RepeatSubmit
|
||||
@Log(value = "新增用户", module = LogModuleEnum.USER)
|
||||
public Result<?> saveUser(
|
||||
@@ -80,7 +80,7 @@ public class UserController {
|
||||
|
||||
@Operation(summary = "获取用户表单数据")
|
||||
@GetMapping("/{userId}/form")
|
||||
@PreAuthorize("@ss.hasPerm('sys:user:edit')")
|
||||
@PreAuthorize("@ss.hasPerm('sys:user:update')")
|
||||
@Log(value = "用户表单数据", module = LogModuleEnum.USER)
|
||||
public Result<UserForm> getUserForm(
|
||||
@Parameter(description = "用户ID") @PathVariable Long userId
|
||||
@@ -91,7 +91,7 @@ public class UserController {
|
||||
|
||||
@Operation(summary = "修改用户")
|
||||
@PutMapping(value = "/{userId}")
|
||||
@PreAuthorize("@ss.hasPerm('sys:user:edit')")
|
||||
@PreAuthorize("@ss.hasPerm('sys:user:update')")
|
||||
@Log(value = "修改用户", module = LogModuleEnum.USER)
|
||||
public Result<Void> updateUser(
|
||||
@Parameter(description = "用户ID") @PathVariable Long userId,
|
||||
@@ -114,7 +114,7 @@ public class UserController {
|
||||
|
||||
@Operation(summary = "修改用户状态")
|
||||
@PatchMapping(value = "/{userId}/status")
|
||||
@PreAuthorize("@ss.hasPerm('sys:user:edit')")
|
||||
@PreAuthorize("@ss.hasPerm('sys:user:update')")
|
||||
@Log(value = "修改用户状态", module = LogModuleEnum.USER)
|
||||
public Result<Void> updateUserStatus(
|
||||
@Parameter(description = "用户ID") @PathVariable Long userId,
|
||||
@@ -130,9 +130,9 @@ public class UserController {
|
||||
@Operation(summary = "获取当前登录用户信息")
|
||||
@GetMapping("/me")
|
||||
@Log(value = "获取当前登录用户信息", module = LogModuleEnum.USER)
|
||||
public Result<CurrentUserDTO> getCurrentUser() {
|
||||
CurrentUserDTO currentUserDTO = userService.getCurrentUserInfo();
|
||||
return Result.success(currentUserDTO);
|
||||
public Result<CurrentUserDto> getCurrentUser() {
|
||||
CurrentUserDto currentUserDto = userService.getCurrentUserInfo();
|
||||
return Result.success(currentUserDto);
|
||||
}
|
||||
|
||||
@Operation(summary = "用户导入模板下载")
|
||||
@@ -160,7 +160,7 @@ public class UserController {
|
||||
@Log(value = "导入用户", module = LogModuleEnum.USER)
|
||||
public Result<ExcelResult> importUsers(MultipartFile file) throws IOException {
|
||||
UserImportListener listener = new UserImportListener();
|
||||
ExcelUtils.importExcel(file.getInputStream(), UserImportDTO.class, listener);
|
||||
ExcelUtils.importExcel(file.getInputStream(), UserImportDto.class, listener);
|
||||
return Result.success(listener.getExcelResult());
|
||||
}
|
||||
|
||||
@@ -173,17 +173,17 @@ public class UserController {
|
||||
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
|
||||
response.setHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode(fileName, StandardCharsets.UTF_8));
|
||||
|
||||
List<UserExportDTO> exportUserList = userService.listExportUsers(queryParams);
|
||||
EasyExcel.write(response.getOutputStream(), UserExportDTO.class).sheet("用户列表")
|
||||
List<UserExportDto> exportUserList = userService.listExportUsers(queryParams);
|
||||
EasyExcel.write(response.getOutputStream(), UserExportDto.class).sheet("用户列表")
|
||||
.doWrite(exportUserList);
|
||||
}
|
||||
|
||||
@Operation(summary = "获取个人中心用户信息")
|
||||
@GetMapping("/profile")
|
||||
@Log(value = "获取个人中心用户信息", module = LogModuleEnum.USER)
|
||||
public Result<UserProfileVO> getUserProfile() {
|
||||
public Result<UserProfileVo> getUserProfile() {
|
||||
Long userId = SecurityUtils.getUserId();
|
||||
UserProfileVO userProfile = userService.getUserProfile(userId);
|
||||
UserProfileVo userProfile = userService.getUserProfile(userId);
|
||||
return Result.success(userProfile);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ package com.youlai.boot.system.converter;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.youlai.boot.system.model.entity.Config;
|
||||
import com.youlai.boot.system.model.vo.ConfigVO;
|
||||
import com.youlai.boot.system.model.vo.ConfigVo;
|
||||
import com.youlai.boot.system.model.form.ConfigForm;
|
||||
import org.mapstruct.Mapper;
|
||||
|
||||
@@ -15,7 +15,7 @@ import org.mapstruct.Mapper;
|
||||
@Mapper(componentModel = "spring")
|
||||
public interface ConfigConverter {
|
||||
|
||||
Page<ConfigVO> toPageVo(Page<Config> page);
|
||||
Page<ConfigVo> toPageVo(Page<Config> page);
|
||||
|
||||
Config toEntity(ConfigForm configForm);
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.youlai.boot.system.converter;
|
||||
|
||||
import com.youlai.boot.system.model.entity.Dept;
|
||||
import com.youlai.boot.system.model.vo.DeptVO;
|
||||
import com.youlai.boot.system.model.vo.DeptVo;
|
||||
import com.youlai.boot.system.model.form.DeptForm;
|
||||
import org.mapstruct.Mapper;
|
||||
|
||||
@@ -16,7 +16,7 @@ public interface DeptConverter {
|
||||
|
||||
DeptForm toForm(Dept entity);
|
||||
|
||||
DeptVO toVo(Dept entity);
|
||||
DeptVo toVo(Dept entity);
|
||||
|
||||
Dept toEntity(DeptForm deptForm);
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ package com.youlai.boot.system.converter;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.youlai.boot.system.model.entity.Dict;
|
||||
import com.youlai.boot.system.model.vo.DictPageVO;
|
||||
import com.youlai.boot.system.model.vo.DictPageVo;
|
||||
import com.youlai.boot.system.model.form.DictForm;
|
||||
import org.mapstruct.Mapper;
|
||||
|
||||
@@ -15,7 +15,7 @@ import org.mapstruct.Mapper;
|
||||
@Mapper(componentModel = "spring")
|
||||
public interface DictConverter {
|
||||
|
||||
Page<DictPageVO> toPageVo(Page<Dict> page);
|
||||
Page<DictPageVo> toPageVo(Page<Dict> page);
|
||||
|
||||
DictForm toForm(Dict entity);
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ package com.youlai.boot.system.converter;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.youlai.boot.system.model.entity.DictItem;
|
||||
import com.youlai.boot.system.model.form.DictItemForm;
|
||||
import com.youlai.boot.system.model.vo.DictPageVO;
|
||||
import com.youlai.boot.system.model.vo.DictPageVo;
|
||||
import com.youlai.boot.common.model.Option;
|
||||
import org.mapstruct.Mapper;
|
||||
|
||||
@@ -18,7 +18,7 @@ import java.util.List;
|
||||
@Mapper(componentModel = "spring")
|
||||
public interface DictItemConverter {
|
||||
|
||||
Page<DictPageVO> toPageVo(Page<DictItem> page);
|
||||
Page<DictPageVo> toPageVo(Page<DictItem> page);
|
||||
|
||||
DictItemForm toForm(DictItem entity);
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.youlai.boot.system.converter;
|
||||
|
||||
import com.youlai.boot.system.model.entity.Menu;
|
||||
import com.youlai.boot.system.model.vo.MenuVO;
|
||||
import com.youlai.boot.system.model.vo.MenuVo;
|
||||
import com.youlai.boot.system.model.form.MenuForm;
|
||||
import org.mapstruct.Mapper;
|
||||
import org.mapstruct.Mapping;
|
||||
@@ -15,7 +15,7 @@ import org.mapstruct.Mapping;
|
||||
@Mapper(componentModel = "spring")
|
||||
public interface MenuConverter {
|
||||
|
||||
MenuVO toVo(Menu entity);
|
||||
MenuVo toVo(Menu entity);
|
||||
|
||||
@Mapping(target = "params", ignore = true)
|
||||
MenuForm toForm(Menu entity);
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
package com.youlai.boot.system.converter;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.youlai.boot.system.model.bo.NoticeBO;
|
||||
import com.youlai.boot.system.model.bo.NoticeBo;
|
||||
import com.youlai.boot.system.model.entity.Notice;
|
||||
import com.youlai.boot.system.model.form.NoticeForm;
|
||||
import com.youlai.boot.system.model.vo.NoticeDetailVO;
|
||||
import com.youlai.boot.system.model.vo.NoticePageVO;
|
||||
import com.youlai.boot.system.model.vo.NoticeDetailVo;
|
||||
import com.youlai.boot.system.model.vo.NoticePageVo;
|
||||
import org.mapstruct.Mapper;
|
||||
import org.mapstruct.Mapping;
|
||||
import org.mapstruct.Mappings;
|
||||
@@ -30,9 +30,9 @@ public interface NoticeConverter{
|
||||
})
|
||||
Notice toEntity(NoticeForm formData);
|
||||
|
||||
NoticePageVO toPageVo(NoticeBO bo);
|
||||
NoticePageVo toPageVo(NoticeBo bo);
|
||||
|
||||
Page<NoticePageVO> toPageVo(Page<NoticeBO> noticePage);
|
||||
Page<NoticePageVo> toPageVo(Page<NoticeBo> noticePage);
|
||||
|
||||
NoticeDetailVO toDetailVO(NoticeBO noticeBO);
|
||||
NoticeDetailVo toDetailVo(NoticeBo noticeBo);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ package com.youlai.boot.system.converter;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.youlai.boot.system.model.entity.Role;
|
||||
import com.youlai.boot.system.model.vo.RolePageVO;
|
||||
import com.youlai.boot.system.model.vo.RolePageVo;
|
||||
import com.youlai.boot.common.model.Option;
|
||||
import com.youlai.boot.system.model.form.RoleForm;
|
||||
import org.mapstruct.Mapper;
|
||||
@@ -20,7 +20,7 @@ import java.util.List;
|
||||
@Mapper(componentModel = "spring")
|
||||
public interface RoleConverter {
|
||||
|
||||
Page<RolePageVO> toPageVo(Page<Role> page);
|
||||
Page<RolePageVo> toPageVo(Page<Role> page);
|
||||
|
||||
@Mappings({
|
||||
@Mapping(target = "value", source = "id"),
|
||||
|
||||
@@ -3,12 +3,12 @@ package com.youlai.boot.system.converter;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.youlai.boot.common.model.Option;
|
||||
import com.youlai.boot.system.model.entity.User;
|
||||
import com.youlai.boot.system.model.dto.CurrentUserDTO;
|
||||
import com.youlai.boot.system.model.vo.UserPageVO;
|
||||
import com.youlai.boot.system.model.vo.UserProfileVO;
|
||||
import com.youlai.boot.system.model.bo.UserBO;
|
||||
import com.youlai.boot.system.model.dto.CurrentUserDto;
|
||||
import com.youlai.boot.system.model.vo.UserPageVo;
|
||||
import com.youlai.boot.system.model.vo.UserProfileVo;
|
||||
import com.youlai.boot.system.model.bo.UserBo;
|
||||
import com.youlai.boot.system.model.form.UserForm;
|
||||
import com.youlai.boot.system.model.dto.UserImportDTO;
|
||||
import com.youlai.boot.system.model.dto.UserImportDto;
|
||||
import com.youlai.boot.system.model.form.UserProfileForm;
|
||||
import org.mapstruct.InheritInverseConfiguration;
|
||||
import org.mapstruct.Mapper;
|
||||
@@ -26,9 +26,9 @@ import java.util.List;
|
||||
@Mapper(componentModel = "spring")
|
||||
public interface UserConverter {
|
||||
|
||||
UserPageVO toPageVo(UserBO bo);
|
||||
UserPageVo toPageVo(UserBo bo);
|
||||
|
||||
Page<UserPageVO> toPageVo(Page<UserBO> bo);
|
||||
Page<UserPageVo> toPageVo(Page<UserBo> bo);
|
||||
|
||||
UserForm toForm(User entity);
|
||||
|
||||
@@ -38,12 +38,12 @@ public interface UserConverter {
|
||||
@Mappings({
|
||||
@Mapping(target = "userId", source = "id")
|
||||
})
|
||||
CurrentUserDTO toCurrentUserDto(User entity);
|
||||
CurrentUserDto toCurrentUserDto(User entity);
|
||||
|
||||
User toEntity(UserImportDTO vo);
|
||||
User toEntity(UserImportDto vo);
|
||||
|
||||
|
||||
UserProfileVO toProfileVo(UserBO bo);
|
||||
UserProfileVo toProfileVo(UserBo bo);
|
||||
|
||||
User toEntity(UserProfileForm formData);
|
||||
|
||||
|
||||
@@ -5,28 +5,31 @@ import com.youlai.boot.common.base.IBaseEnum;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 菜单类型枚举
|
||||
* 菜单类型枚举(char)
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 2022/4/23 9:36
|
||||
* C:目录
|
||||
* M:菜单
|
||||
* B:按钮
|
||||
*/
|
||||
@Getter
|
||||
public enum MenuTypeEnum implements IBaseEnum<Integer> {
|
||||
public enum MenuTypeEnum implements IBaseEnum<String> {
|
||||
|
||||
NULL(0, null),
|
||||
MENU(1, "菜单"),
|
||||
CATALOG(2, "目录"),
|
||||
EXTLINK(3, "外链"),
|
||||
BUTTON(4, "按钮");
|
||||
CATALOG("C", "目录"),
|
||||
MENU("M", "菜单"),
|
||||
BUTTON("B", "按钮");
|
||||
|
||||
// Mybatis-Plus 提供注解表示插入数据库时插入该值
|
||||
/**
|
||||
* 数据库存储值
|
||||
*/
|
||||
@EnumValue
|
||||
private final Integer value;
|
||||
private final String value;
|
||||
|
||||
// @JsonValue // 表示对枚举序列化时返回此字段
|
||||
/**
|
||||
* 友好名称
|
||||
*/
|
||||
private final String label;
|
||||
|
||||
MenuTypeEnum(Integer value, String label) {
|
||||
MenuTypeEnum(String value, String label) {
|
||||
this.value = value;
|
||||
this.label = label;
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ public enum NoticePublishStatusEnum implements IBaseEnum<Integer> {
|
||||
|
||||
UNPUBLISHED(0, "未发布"),
|
||||
PUBLISHED(1, "已发布"),
|
||||
REVOKED(-1, "已撤回");
|
||||
REVoKED(-1, "已撤回");
|
||||
|
||||
|
||||
private final Integer value;
|
||||
|
||||
@@ -2,9 +2,10 @@ package com.youlai.boot.system.handler;
|
||||
|
||||
|
||||
import com.youlai.boot.system.service.UserOnlineService;
|
||||
import com.youlai.boot.platform.websocket.publisher.WebSocketPublisher;
|
||||
import com.youlai.boot.platform.websocket.topic.WebSocketTopics;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.messaging.simp.SimpMessagingTemplate;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@@ -20,7 +21,7 @@ import org.springframework.stereotype.Component;
|
||||
public class OnlineUserJobHandler {
|
||||
|
||||
private final UserOnlineService userOnlineService;
|
||||
private final SimpMessagingTemplate messagingTemplate;
|
||||
private final WebSocketPublisher webSocketPublisher;
|
||||
|
||||
// 每3分钟统计一次在线用户数,减少服务器压力
|
||||
@Scheduled(cron = "0 */3 * * * ?")
|
||||
@@ -28,7 +29,7 @@ public class OnlineUserJobHandler {
|
||||
log.info("定时任务:统计在线用户数");
|
||||
// 推送在线用户数量到新主题
|
||||
int count = userOnlineService.getOnlineUserCount();
|
||||
messagingTemplate.convertAndSend("/topic/online-count", count);
|
||||
webSocketPublisher.publish(WebSocketTopics.TOPIC_ONLINE_COUNT, count);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import com.youlai.boot.common.enums.StatusEnum;
|
||||
import com.youlai.boot.core.web.ExcelResult;
|
||||
import com.youlai.boot.system.converter.UserConverter;
|
||||
import com.youlai.boot.system.enums.DictCodeEnum;
|
||||
import com.youlai.boot.system.model.dto.UserImportDTO;
|
||||
import com.youlai.boot.system.model.dto.UserImportDto;
|
||||
import com.youlai.boot.system.model.entity.*;
|
||||
import com.youlai.boot.system.service.*;
|
||||
import lombok.Getter;
|
||||
@@ -35,7 +35,7 @@ import java.util.stream.Collectors;
|
||||
* @since 2022/4/10
|
||||
*/
|
||||
@Slf4j
|
||||
public class UserImportListener extends AnalysisEventListener<UserImportDTO> {
|
||||
public class UserImportListener extends AnalysisEventListener<UserImportDto> {
|
||||
|
||||
/**
|
||||
* Excel 导入结果
|
||||
@@ -82,15 +82,15 @@ public class UserImportListener extends AnalysisEventListener<UserImportDTO> {
|
||||
* 1. 数据校验;全字段校验
|
||||
* 2. 数据持久化;
|
||||
*
|
||||
* @param userImportDTO 一行数据,类似于 {@link AnalysisContext#readRowHolder()}
|
||||
* @param userImportDto 一行数据,类似于 {@link AnalysisContext#readRowHolder()}
|
||||
*/
|
||||
@Override
|
||||
public void invoke(UserImportDTO userImportDTO, AnalysisContext analysisContext) {
|
||||
log.info("解析到一条用户数据:{}", JSONUtil.toJsonStr(userImportDTO));
|
||||
public void invoke(UserImportDto userImportDto, AnalysisContext analysisContext) {
|
||||
log.info("解析到一条用户数据:{}", JSONUtil.toJsonStr(userImportDto));
|
||||
|
||||
boolean validation = true;
|
||||
String errorMsg = "第" + currentRow + "行数据校验失败:";
|
||||
String username = userImportDTO.getUsername();
|
||||
String username = userImportDto.getUsername();
|
||||
if (StrUtil.isBlank(username)) {
|
||||
errorMsg += "用户名为空;";
|
||||
validation = false;
|
||||
@@ -102,13 +102,13 @@ public class UserImportListener extends AnalysisEventListener<UserImportDTO> {
|
||||
}
|
||||
}
|
||||
|
||||
String nickname = userImportDTO.getNickname();
|
||||
String nickname = userImportDto.getNickname();
|
||||
if (StrUtil.isBlank(nickname)) {
|
||||
errorMsg += "用户昵称为空;";
|
||||
validation = false;
|
||||
}
|
||||
|
||||
String mobile = userImportDTO.getMobile();
|
||||
String mobile = userImportDto.getMobile();
|
||||
if (StrUtil.isBlank(mobile)) {
|
||||
errorMsg += "手机号码为空;";
|
||||
validation = false;
|
||||
@@ -121,16 +121,16 @@ public class UserImportListener extends AnalysisEventListener<UserImportDTO> {
|
||||
|
||||
if (validation) {
|
||||
// 校验通过,持久化至数据库
|
||||
User entity = userConverter.toEntity(userImportDTO);
|
||||
User entity = userConverter.toEntity(userImportDto);
|
||||
entity.setPassword(passwordEncoder.encode(SystemConstants.DEFAULT_PASSWORD)); // 默认密码
|
||||
// 性别逆向翻译 根据字典标签得到字典值
|
||||
String genderLabel = userImportDTO.getGenderLabel();
|
||||
String genderLabel = userImportDto.getGenderLabel();
|
||||
entity.setGender(getGenderValue(genderLabel));
|
||||
// 角色解析
|
||||
String roleCodes = userImportDTO.getRoleCodes();
|
||||
String roleCodes = userImportDto.getRoleCodes();
|
||||
List<Long> roleIds = getRoleIds(roleCodes);
|
||||
// 部门解析
|
||||
String deptCode = userImportDTO.getDeptCode();
|
||||
String deptCode = userImportDto.getDeptCode();
|
||||
entity.setDeptId(getDeptId(deptCode));
|
||||
|
||||
boolean saveResult = userService.save(entity);
|
||||
|
||||
@@ -4,7 +4,7 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.youlai.boot.system.model.entity.DictItem;
|
||||
import com.youlai.boot.system.model.query.DictItemPageQuery;
|
||||
import com.youlai.boot.system.model.vo.DictItemPageVO;
|
||||
import com.youlai.boot.system.model.vo.DictItemPageVo;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
@@ -19,7 +19,7 @@ public interface DictItemMapper extends BaseMapper<DictItem> {
|
||||
/**
|
||||
* 字典项分页列表
|
||||
*/
|
||||
Page<DictItemPageVO> getDictItemPage(Page<DictItemPageVO> page, DictItemPageQuery queryParams);
|
||||
Page<DictItemPageVo> getDictItemPage(Page<DictItemPageVo> page, DictItemPageQuery queryParams);
|
||||
}
|
||||
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user