chore: 移除微信授权登录和AI 模块
This commit is contained in:
@@ -1,7 +1,5 @@
|
||||
package com.youlai.boot.auth.controller;
|
||||
|
||||
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;
|
||||
@@ -69,30 +67,6 @@ public class AuthController {
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@Operation(summary = "微信授权登录(Web)")
|
||||
@PostMapping("/login/wechat")
|
||||
@Log(value = "微信登录", module = LogModuleEnum.LOGIN)
|
||||
public Result<AuthenticationToken> loginByWechat(
|
||||
@Parameter(description = "微信授权码", example = "code") @RequestParam String code
|
||||
) {
|
||||
AuthenticationToken loginResult = authService.loginByWechat(code);
|
||||
return Result.success(loginResult);
|
||||
}
|
||||
|
||||
@Operation(summary = "微信小程序登录(Code)")
|
||||
@PostMapping("/wx/miniapp/code-login")
|
||||
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);
|
||||
return Result.success(token);
|
||||
}
|
||||
|
||||
@Operation(summary = "退出登录")
|
||||
@DeleteMapping("/logout")
|
||||
@Log(value = "退出登录", module = LogModuleEnum.LOGIN)
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
package com.youlai.boot.auth.model.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
|
||||
/**
|
||||
*微信小程序Code登录请求参数
|
||||
*/
|
||||
@Schema(description = "微信小程序Code登录请求参数")
|
||||
@Data
|
||||
public class WxMiniAppCodeLoginDTO {
|
||||
|
||||
@Schema(description = "微信小程序登录时获取的code", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@NotBlank(message = "code不能为空")
|
||||
private String code;
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
package com.youlai.boot.auth.model.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
|
||||
/**
|
||||
* 微信小程序手机号登录请求参数
|
||||
*/
|
||||
@Schema(description = "微信小程序手机号登录请求参数")
|
||||
@Data
|
||||
public class WxMiniAppPhoneLoginDTO {
|
||||
|
||||
@Schema(description = "微信小程序登录时获取的code", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@NotBlank(message = "code不能为空")
|
||||
private String code;
|
||||
|
||||
@Schema(description = "包括敏感数据在内的完整用户信息的加密数据")
|
||||
private String encryptedData;
|
||||
|
||||
@Schema(description = "加密算法的初始向量")
|
||||
private String iv;
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
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.security.model.AuthenticationToken;
|
||||
import com.youlai.boot.auth.model.dto.WxMiniAppCodeLoginDTO;
|
||||
|
||||
/**
|
||||
* 认证服务接口
|
||||
@@ -42,30 +40,6 @@ public interface AuthService {
|
||||
*/
|
||||
AuthenticationToken refreshToken(String refreshToken);
|
||||
|
||||
/**
|
||||
* 微信小程序登录
|
||||
*
|
||||
* @param code 微信登录code
|
||||
* @return 登录结果
|
||||
*/
|
||||
AuthenticationToken loginByWechat(String code);
|
||||
|
||||
/**
|
||||
* 微信小程序Code登录
|
||||
*
|
||||
* @param loginDto 登录参数
|
||||
* @return 访问令牌
|
||||
*/
|
||||
AuthenticationToken loginByWxMiniAppCode(WxMiniAppCodeLoginDTO loginDto);
|
||||
|
||||
/**
|
||||
* 微信小程序手机号登录
|
||||
*
|
||||
* @param loginDto 登录参数
|
||||
* @return 访问令牌
|
||||
*/
|
||||
AuthenticationToken loginByWxMiniAppPhone(WxMiniAppPhoneLoginDTO loginDto);
|
||||
|
||||
/**
|
||||
* 发送短信验证码
|
||||
*
|
||||
|
||||
@@ -5,8 +5,6 @@ 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.service.AuthService;
|
||||
import com.youlai.boot.common.constant.RedisConstants;
|
||||
@@ -17,8 +15,6 @@ import com.youlai.boot.platform.sms.enums.SmsTypeEnum;
|
||||
import com.youlai.boot.platform.sms.service.SmsService;
|
||||
import com.youlai.boot.security.model.AuthenticationToken;
|
||||
import com.youlai.boot.security.model.SmsAuthenticationToken;
|
||||
import com.youlai.boot.security.model.WxMiniAppCodeAuthenticationToken;
|
||||
import com.youlai.boot.security.model.WxMiniAppPhoneAuthenticationToken;
|
||||
import com.youlai.boot.security.token.TokenManager;
|
||||
import com.youlai.boot.security.util.SecurityUtils;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
@@ -84,27 +80,6 @@ public class AuthServiceImpl implements AuthService {
|
||||
return authenticationTokenResponse;
|
||||
}
|
||||
|
||||
/**
|
||||
* 微信一键授权登录
|
||||
*
|
||||
* @param code 微信登录code
|
||||
* @return 访问令牌
|
||||
*/
|
||||
@Override
|
||||
public AuthenticationToken loginByWechat(String code) {
|
||||
// 1. 创建用户微信认证的令牌(未认证)
|
||||
WxMiniAppCodeAuthenticationToken authenticationToken = new WxMiniAppCodeAuthenticationToken(code);
|
||||
|
||||
// 2. 执行认证(认证中)
|
||||
Authentication authentication = authenticationManager.authenticate(authenticationToken);
|
||||
|
||||
// 3. 认证成功后生成 JWT 令牌,并存入 Security 上下文,供登录日志 AOP 使用(已认证)
|
||||
AuthenticationToken token = tokenManager.generateToken(authentication);
|
||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
|
||||
return token;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送登录短信验证码
|
||||
*
|
||||
@@ -224,50 +199,4 @@ public class AuthServiceImpl implements AuthService {
|
||||
return tokenManager.refreshToken(refreshToken);
|
||||
}
|
||||
|
||||
/**
|
||||
* 微信小程序Code登录
|
||||
*
|
||||
* @param loginDto 登录参数
|
||||
* @return 访问令牌
|
||||
*/
|
||||
@Override
|
||||
public AuthenticationToken loginByWxMiniAppCode(WxMiniAppCodeLoginDTO loginDto) {
|
||||
// 1. 创建微信小程序认证令牌(未认证)
|
||||
WxMiniAppCodeAuthenticationToken authenticationToken = new WxMiniAppCodeAuthenticationToken(loginDto.getCode());
|
||||
|
||||
// 2. 执行认证(认证中)
|
||||
Authentication authentication = authenticationManager.authenticate(authenticationToken);
|
||||
|
||||
// 3. 认证成功后生成 JWT 令牌,并存入 Security 上下文,供登录日志 AOP 使用(已认证)
|
||||
AuthenticationToken token = tokenManager.generateToken(authentication);
|
||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
|
||||
return token;
|
||||
}
|
||||
|
||||
/**
|
||||
* 微信小程序手机号登录
|
||||
*
|
||||
* @param loginDto 登录参数
|
||||
* @return 访问令牌
|
||||
*/
|
||||
@Override
|
||||
public AuthenticationToken loginByWxMiniAppPhone(WxMiniAppPhoneLoginDTO loginDto) {
|
||||
// 创建微信小程序手机号认证Token
|
||||
WxMiniAppPhoneAuthenticationToken authenticationToken = new WxMiniAppPhoneAuthenticationToken(
|
||||
loginDto.getCode(),
|
||||
loginDto.getEncryptedData(),
|
||||
loginDto.getIv()
|
||||
);
|
||||
|
||||
// 执行认证
|
||||
Authentication authentication = authenticationManager.authenticate(authenticationToken);
|
||||
|
||||
// 认证成功后生成JWT令牌,并存入Security上下文
|
||||
AuthenticationToken token = tokenManager.generateToken(authentication);
|
||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
|
||||
return token;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.youlai.boot.config;
|
||||
|
||||
import cn.binarywang.wx.miniapp.api.WxMaService;
|
||||
import cn.hutool.captcha.generator.CodeGenerator;
|
||||
import cn.hutool.core.util.ArrayUtil;
|
||||
import com.youlai.boot.config.property.SecurityProperties;
|
||||
@@ -10,8 +9,6 @@ import com.youlai.boot.security.filter.TokenAuthenticationFilter;
|
||||
import com.youlai.boot.security.handler.MyAccessDeniedHandler;
|
||||
import com.youlai.boot.security.handler.MyAuthenticationEntryPoint;
|
||||
import com.youlai.boot.security.provider.SmsAuthenticationProvider;
|
||||
import com.youlai.boot.security.provider.WxMiniAppCodeAuthenticationProvider;
|
||||
import com.youlai.boot.security.provider.WxMiniAppPhoneAuthenticationProvider;
|
||||
import com.youlai.boot.security.token.TokenManager;
|
||||
import com.youlai.boot.security.service.SysUserDetailsService;
|
||||
import com.youlai.boot.system.service.ConfigService;
|
||||
@@ -50,7 +47,6 @@ public class SecurityConfig {
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
|
||||
private final TokenManager tokenManager;
|
||||
private final WxMaService wxMaService;
|
||||
private final UserService userService;
|
||||
private final SysUserDetailsService userDetailsService;
|
||||
|
||||
@@ -124,22 +120,6 @@ public class SecurityConfig {
|
||||
return daoAuthenticationProvider;
|
||||
}
|
||||
|
||||
/**
|
||||
* 微信小程序Code认证Provider
|
||||
*/
|
||||
@Bean
|
||||
public WxMiniAppCodeAuthenticationProvider wxMiniAppCodeAuthenticationProvider() {
|
||||
return new WxMiniAppCodeAuthenticationProvider(userService, wxMaService);
|
||||
}
|
||||
|
||||
/**
|
||||
* 微信小程序手机号认证Provider
|
||||
*/
|
||||
@Bean
|
||||
public WxMiniAppPhoneAuthenticationProvider wxMiniAppPhoneAuthenticationProvider() {
|
||||
return new WxMiniAppPhoneAuthenticationProvider(userService, wxMaService);
|
||||
}
|
||||
|
||||
/**
|
||||
* 短信验证码认证 Provider
|
||||
*/
|
||||
@@ -154,14 +134,10 @@ public class SecurityConfig {
|
||||
@Bean
|
||||
public AuthenticationManager authenticationManager(
|
||||
DaoAuthenticationProvider daoAuthenticationProvider,
|
||||
WxMiniAppCodeAuthenticationProvider wxMiniAppCodeAuthenticationProvider,
|
||||
WxMiniAppPhoneAuthenticationProvider wxMiniAppPhoneAuthenticationProvider,
|
||||
SmsAuthenticationProvider smsAuthenticationProvider
|
||||
) {
|
||||
return new ProviderManager(
|
||||
daoAuthenticationProvider,
|
||||
wxMiniAppCodeAuthenticationProvider,
|
||||
wxMiniAppPhoneAuthenticationProvider,
|
||||
smsAuthenticationProvider
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
package com.youlai.boot.config;
|
||||
|
||||
import cn.binarywang.wx.miniapp.api.WxMaService;
|
||||
import cn.binarywang.wx.miniapp.api.impl.WxMaServiceImpl;
|
||||
import cn.binarywang.wx.miniapp.config.WxMaConfig;
|
||||
import cn.binarywang.wx.miniapp.config.impl.WxMaDefaultConfigImpl;
|
||||
import lombok.Setter;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* 配置微信 appId 和 appSecret
|
||||
*
|
||||
* @author wangtao
|
||||
* @since 2024/11/26 17:28
|
||||
*/
|
||||
@Setter
|
||||
@ConfigurationProperties(prefix = "wx.miniapp")
|
||||
@Configuration
|
||||
public class WxMiniAppConfig {
|
||||
|
||||
private String appId;
|
||||
|
||||
private String appSecret;
|
||||
|
||||
@Bean
|
||||
public WxMaConfig wxMaConfig() {
|
||||
WxMaDefaultConfigImpl config = new WxMaDefaultConfigImpl();
|
||||
config.setAppid(appId);
|
||||
config.setSecret(appSecret);
|
||||
return config;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public WxMaService wxMaService(WxMaConfig wxMaConfig) {
|
||||
WxMaService service = new WxMaServiceImpl();
|
||||
service.setWxMaConfig(wxMaConfig);
|
||||
return service;
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
package com.youlai.boot.platform.ai.config;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.ai.chat.client.ChatClient;
|
||||
import org.springframework.ai.openai.OpenAiChatModel;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import com.youlai.boot.platform.ai.tools.UserTools;
|
||||
|
||||
/**
|
||||
* Spring AI 配置类
|
||||
*
|
||||
* 使用 Spring AI 自动配置,支持:
|
||||
* - OpenAI
|
||||
* - 通义千问(DashScope 兼容 OpenAI 协议)
|
||||
* - DeepSeek(兼容 OpenAI 协议)
|
||||
* - 其他兼容 OpenAI 协议的模型
|
||||
*
|
||||
* 配置方式:
|
||||
* spring.ai.openai.api-key: xxx
|
||||
* spring.ai.openai.base-url: xxx
|
||||
* spring.ai.openai.chat.options.model: xxx
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 3.0.0
|
||||
*/
|
||||
@Slf4j
|
||||
@Configuration
|
||||
@ConditionalOnProperty(prefix = "spring.ai.openai.chat", name = "enabled", havingValue = "true", matchIfMissing = false)
|
||||
public class SpringAiConfig {
|
||||
|
||||
/**
|
||||
* 创建 ChatClient(Spring AI 核心客户端)
|
||||
* <p>
|
||||
* OpenAiChatModel 由 Spring AI 自动配置创建
|
||||
* 根据 spring.ai.openai.* 配置自动初始化
|
||||
*/
|
||||
@Bean
|
||||
public ChatClient chatClient(OpenAiChatModel chatModel, UserTools userTools) {
|
||||
log.info("✅ Spring AI ChatClient 初始化成功");
|
||||
log.info("📋 当前配置 - 模型: {}", chatModel.getDefaultOptions().getModel());
|
||||
// 将 UserTools 注册为默认工具,所有调用都可使用
|
||||
return ChatClient.builder(chatModel)
|
||||
.defaultTools(userTools)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,79 +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.AiAssistantQuery;
|
||||
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.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
/**
|
||||
* AI 助手控制器
|
||||
* <p>
|
||||
* 负责 AI 命令的解析、执行与记录查询。
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 3.0.0
|
||||
*/
|
||||
@Tag(name = "13.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(AiAssistantQuery queryParams) {
|
||||
IPage<AiAssistantRecordVO> page = aiAssistantRecordService.getRecordPage(queryParams);
|
||||
return PageResult.success(page);
|
||||
}
|
||||
|
||||
// 记录类接口按需扩展,当前开放 parse/execute/records
|
||||
}
|
||||
@@ -1,22 +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.AiAssistantRecord;
|
||||
import com.youlai.boot.platform.ai.model.query.AiAssistantQuery;
|
||||
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, AiAssistantQuery queryParams);
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
package com.youlai.boot.platform.ai.model.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* AI 命令执行请求 Dto
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 3.0.0
|
||||
*/
|
||||
@Data
|
||||
public class AiExecuteRequestDTO {
|
||||
|
||||
/**
|
||||
* 关联的解析日志ID
|
||||
*/
|
||||
private String parseLogId;
|
||||
|
||||
/**
|
||||
* 原始命令(用于审计)
|
||||
*/
|
||||
private String originalCommand;
|
||||
|
||||
/**
|
||||
* 要执行的函数调用
|
||||
*/
|
||||
private AiFunctionCallDTO functionCall;
|
||||
|
||||
/**
|
||||
* 确认模式:auto=自动执行, manual=需要用户确认
|
||||
*/
|
||||
private String confirmMode;
|
||||
|
||||
/**
|
||||
* 用户确认标志
|
||||
*/
|
||||
private Boolean userConfirmed;
|
||||
|
||||
/**
|
||||
* 幂等性令牌(防止重复执行)
|
||||
*/
|
||||
private String idempotencyKey;
|
||||
|
||||
/**
|
||||
* 当前页面路由
|
||||
*/
|
||||
private String currentRoute;
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
package com.youlai.boot.platform.ai.model.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* AI 命令执行响应 Dto
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 3.0.0
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class AiExecuteResponseDTO {
|
||||
|
||||
/**
|
||||
* 是否执行成功
|
||||
*/
|
||||
private Boolean success;
|
||||
|
||||
/**
|
||||
* 执行结果数据
|
||||
*/
|
||||
private Object data;
|
||||
|
||||
/**
|
||||
* 执行结果说明
|
||||
*/
|
||||
private String message;
|
||||
|
||||
/**
|
||||
* 影响的记录数
|
||||
*/
|
||||
private Integer affectedRows;
|
||||
|
||||
/**
|
||||
* 错误信息
|
||||
*/
|
||||
private String error;
|
||||
|
||||
/**
|
||||
* 记录ID(用于追踪)
|
||||
*/
|
||||
private Long recordId;
|
||||
|
||||
/**
|
||||
* 需要用户确认
|
||||
*/
|
||||
private Boolean requiresConfirmation;
|
||||
|
||||
/**
|
||||
* 确认提示信息
|
||||
*/
|
||||
private String confirmationPrompt;
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
package com.youlai.boot.platform.ai.model.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* AI 函数调用 Dto
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 3.0.0
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class AiFunctionCallDTO {
|
||||
|
||||
/**
|
||||
* 函数名称
|
||||
*/
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* 函数描述
|
||||
*/
|
||||
private String description;
|
||||
|
||||
/**
|
||||
* 参数对象
|
||||
*/
|
||||
private Map<String, Object> arguments;
|
||||
}
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
package com.youlai.boot.platform.ai.model.dto;
|
||||
|
||||
import lombok.Data;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* AI 解析请求 Dto
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 3.0.0
|
||||
*/
|
||||
@Data
|
||||
public class AiParseRequestDTO {
|
||||
|
||||
/**
|
||||
* 用户输入的自然语言命令
|
||||
*/
|
||||
private String command;
|
||||
|
||||
/**
|
||||
* 当前页面路由(用于上下文)
|
||||
*/
|
||||
private String currentRoute;
|
||||
|
||||
/**
|
||||
* 当前激活的组件名称
|
||||
*/
|
||||
private String currentComponent;
|
||||
|
||||
/**
|
||||
* 额外上下文信息
|
||||
*/
|
||||
private Map<String, Object> context;
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
package com.youlai.boot.platform.ai.model.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* AI 解析响应 Dto
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 3.0.0
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class AiParseResponseDTO {
|
||||
|
||||
/**
|
||||
* 解析日志ID(用于关联执行记录)
|
||||
*/
|
||||
private Long parseLogId;
|
||||
|
||||
/**
|
||||
* 是否成功解析
|
||||
*/
|
||||
private Boolean success;
|
||||
|
||||
/**
|
||||
* 解析后的函数调用列表
|
||||
*/
|
||||
private List<AiFunctionCallDTO> functionCalls;
|
||||
|
||||
/**
|
||||
* AI 的理解和说明
|
||||
*/
|
||||
private String explanation;
|
||||
|
||||
/**
|
||||
* 置信度 (0-1)
|
||||
*/
|
||||
private Double confidence;
|
||||
|
||||
/**
|
||||
* 错误信息
|
||||
*/
|
||||
private String error;
|
||||
|
||||
/**
|
||||
* 原始 LLM 响应(用于调试)
|
||||
*/
|
||||
private String rawResponse;
|
||||
}
|
||||
@@ -1,80 +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_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,44 +0,0 @@
|
||||
package com.youlai.boot.platform.ai.model.query;
|
||||
|
||||
import com.youlai.boot.common.base.BaseQuery;
|
||||
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 BaseQuery {
|
||||
|
||||
@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,44 +0,0 @@
|
||||
package com.youlai.boot.platform.ai.model.query;
|
||||
|
||||
import com.youlai.boot.common.base.BaseQuery;
|
||||
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 AiAssistantQuery extends BaseQuery {
|
||||
|
||||
@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,91 +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
|
||||
*
|
||||
* @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,52 +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.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.AiAssistantQuery;
|
||||
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(AiAssistantQuery queryParams);
|
||||
|
||||
}
|
||||
@@ -1,302 +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.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.AiAssistantQuery;
|
||||
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(AiAssistantQuery queryParams) {
|
||||
Page<AiAssistantRecordVO> page = new Page<>(queryParams.getPageNum(), queryParams.getPageSize());
|
||||
return this.baseMapper.getRecordPage(page, queryParams);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
package com.youlai.boot.platform.ai.tools;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import com.youlai.boot.system.model.entity.User;
|
||||
import com.youlai.boot.system.service.UserService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.ai.tool.annotation.Tool;
|
||||
import org.springframework.ai.tool.annotation.ToolParam;
|
||||
|
||||
/**
|
||||
* 基于 Spring AI Tool 的用户管理工具
|
||||
*
|
||||
* 提供受控的 CRUD 能力,供 LLM 通过 Tool Calling 调用
|
||||
*/
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class UserTools {
|
||||
|
||||
private final UserService userService;
|
||||
|
||||
@Tool(description = "根据关键字在用户列表中筛选用户")
|
||||
public String queryUser(
|
||||
@ToolParam(description = "搜索关键字,用于在列表中搜索筛选") String keywords
|
||||
) {
|
||||
// 返回搜索关键字,前端会在用户列表页面进行筛选
|
||||
return "将在用户列表中搜索:" + keywords;
|
||||
}
|
||||
|
||||
@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 ? "用户昵称更新成功" : "用户昵称更新失败";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
package com.youlai.boot.security.model;
|
||||
|
||||
import org.springframework.security.authentication.AbstractAuthenticationToken;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.util.Collection;
|
||||
|
||||
/**
|
||||
* 微信小程序Code认证Token
|
||||
*
|
||||
* @author 有来技术团队
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public class WxMiniAppCodeAuthenticationToken extends AbstractAuthenticationToken {
|
||||
@Serial
|
||||
private static final long serialVersionUID = 621L;
|
||||
private final Object principal;
|
||||
|
||||
/**
|
||||
* 微信小程序Code认证Token (未认证)
|
||||
*
|
||||
* @param principal 微信code
|
||||
*/
|
||||
public WxMiniAppCodeAuthenticationToken(Object principal) {
|
||||
// 没有授权信息时,设置为 null
|
||||
super((Collection<? extends GrantedAuthority>) null);
|
||||
this.principal = principal;
|
||||
// 默认未认证
|
||||
this.setAuthenticated(false);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 微信小程序Code认证Token (已认证)
|
||||
*
|
||||
* @param principal 微信用户信息
|
||||
* @param authorities 授权信息
|
||||
*/
|
||||
public WxMiniAppCodeAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {
|
||||
super(authorities);
|
||||
this.principal = principal;
|
||||
// 认证通过
|
||||
super.setAuthenticated(true);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 认证通过
|
||||
*
|
||||
* @param principal 微信用户信息
|
||||
* @param authorities 授权信息
|
||||
* @return 已认证的Token
|
||||
*/
|
||||
public static WxMiniAppCodeAuthenticationToken authenticated(Object principal, Collection<? extends GrantedAuthority> authorities) {
|
||||
return new WxMiniAppCodeAuthenticationToken(principal, authorities);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getCredentials() {
|
||||
// 微信认证不需要密码
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getPrincipal() {
|
||||
return this.principal;
|
||||
}
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
package com.youlai.boot.security.model;
|
||||
|
||||
import org.springframework.security.authentication.AbstractAuthenticationToken;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.util.Collection;
|
||||
|
||||
/**
|
||||
* 微信小程序手机号认证Token
|
||||
*
|
||||
* @author 有来技术团队
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public class WxMiniAppPhoneAuthenticationToken extends AbstractAuthenticationToken {
|
||||
@Serial
|
||||
private static final long serialVersionUID = 622L;
|
||||
|
||||
private final Object principal; // code
|
||||
private String encryptedData;
|
||||
private String iv;
|
||||
|
||||
/**
|
||||
* 微信小程序手机号认证Token (未认证)
|
||||
*
|
||||
* @param code 微信登录code
|
||||
* @param encryptedData 加密数据
|
||||
* @param iv 初始向量
|
||||
*/
|
||||
public WxMiniAppPhoneAuthenticationToken(String code, String encryptedData, String iv) {
|
||||
super((Collection<? extends GrantedAuthority>) null);
|
||||
this.principal = code;
|
||||
this.encryptedData = encryptedData;
|
||||
this.iv = iv;
|
||||
this.setAuthenticated(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 微信小程序手机号认证Token (已认证)
|
||||
*
|
||||
* @param principal 用户信息
|
||||
* @param authorities 授权信息
|
||||
*/
|
||||
public WxMiniAppPhoneAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {
|
||||
super(authorities);
|
||||
this.principal = principal;
|
||||
super.setAuthenticated(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 认证通过
|
||||
*
|
||||
* @param principal 用户信息
|
||||
* @param authorities 授权信息
|
||||
* @return 认证通过的Token
|
||||
*/
|
||||
public static WxMiniAppPhoneAuthenticationToken authenticated(Object principal, Collection<? extends GrantedAuthority> authorities) {
|
||||
return new WxMiniAppPhoneAuthenticationToken(principal, authorities);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getCredentials() {
|
||||
// 微信小程序手机号认证不需要密码
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getPrincipal() {
|
||||
return this.principal;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取加密数据
|
||||
*
|
||||
* @return 加密数据
|
||||
*/
|
||||
public String getEncryptedData() {
|
||||
return encryptedData;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取初始向量
|
||||
*
|
||||
* @return 初始向量
|
||||
*/
|
||||
public String getIv() {
|
||||
return iv;
|
||||
}
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
package com.youlai.boot.security.provider;
|
||||
|
||||
import cn.binarywang.wx.miniapp.api.WxMaService;
|
||||
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.UserAuthInfo;
|
||||
import com.youlai.boot.security.model.WxMiniAppCodeAuthenticationToken;
|
||||
import com.youlai.boot.system.service.UserService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import me.chanjar.weixin.common.error.WxErrorException;
|
||||
import org.springframework.security.authentication.AuthenticationProvider;
|
||||
import org.springframework.security.authentication.CredentialsExpiredException;
|
||||
import org.springframework.security.authentication.DisabledException;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||
|
||||
/**
|
||||
* 微信小程序Code认证Provider
|
||||
*
|
||||
* @author 有来技术团队
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Slf4j
|
||||
public class WxMiniAppCodeAuthenticationProvider implements AuthenticationProvider {
|
||||
|
||||
private final UserService userService;
|
||||
private final WxMaService wxMaService;
|
||||
|
||||
public WxMiniAppCodeAuthenticationProvider(UserService userService, WxMaService wxMaService) {
|
||||
this.userService = userService;
|
||||
this.wxMaService = wxMaService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 微信认证逻辑,参考 Spring Security 认证密码校验流程
|
||||
*
|
||||
* @param authentication 认证对象
|
||||
* @return 认证后的 Authentication 对象
|
||||
* @throws AuthenticationException 认证异常
|
||||
* @see org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider#authenticate(Authentication)
|
||||
*/
|
||||
@Override
|
||||
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
|
||||
String code = (String) authentication.getPrincipal();
|
||||
|
||||
// 通过微信服务端验证 code 并获取用户会话信息
|
||||
WxMaJscode2SessionResult sessionInfo;
|
||||
try {
|
||||
sessionInfo = wxMaService.getUserService().getSessionInfo(code);
|
||||
} catch (WxErrorException e) {
|
||||
throw new CredentialsExpiredException("微信登录 code 无效或已失效,请重新获取");
|
||||
}
|
||||
|
||||
String openId = sessionInfo.getOpenid();
|
||||
if (StrUtil.isBlank(openId)) {
|
||||
throw new UsernameNotFoundException("未能获取到微信 OpenID,请稍后重试");
|
||||
}
|
||||
|
||||
// 根据微信 OpenID 查询用户信息
|
||||
UserAuthInfo userAuthInfo = userService.getAuthInfoByOpenId(openId);
|
||||
|
||||
if (userAuthInfo == null) {
|
||||
// 用户不存在则注册
|
||||
userService.registerOrBindWechatUser(openId);
|
||||
|
||||
// 再次查询用户信息,确保用户注册成功
|
||||
userAuthInfo = userService.getAuthInfoByOpenId(openId);
|
||||
if (userAuthInfo == null) {
|
||||
throw new UsernameNotFoundException("用户注册失败,请稍后重试");
|
||||
}
|
||||
}
|
||||
|
||||
// 检查用户状态是否有效
|
||||
if (ObjectUtil.notEqual(userAuthInfo.getStatus(), 1)) {
|
||||
throw new DisabledException("用户已被禁用");
|
||||
}
|
||||
|
||||
// 构建认证后的用户详情信息
|
||||
SysUserDetails userDetails = new SysUserDetails(userAuthInfo);
|
||||
|
||||
// 创建已认证的Token
|
||||
return WxMiniAppCodeAuthenticationToken.authenticated(
|
||||
userDetails,
|
||||
userDetails.getAuthorities()
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(Class<?> authentication) {
|
||||
return WxMiniAppCodeAuthenticationToken.class.isAssignableFrom(authentication);
|
||||
}
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
package com.youlai.boot.security.provider;
|
||||
|
||||
import cn.binarywang.wx.miniapp.api.WxMaService;
|
||||
import cn.binarywang.wx.miniapp.bean.WxMaJscode2SessionResult;
|
||||
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.UserAuthInfo;
|
||||
import com.youlai.boot.security.model.WxMiniAppPhoneAuthenticationToken;
|
||||
import com.youlai.boot.system.service.UserService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import me.chanjar.weixin.common.error.WxErrorException;
|
||||
import org.springframework.security.authentication.AuthenticationProvider;
|
||||
import org.springframework.security.authentication.CredentialsExpiredException;
|
||||
import org.springframework.security.authentication.DisabledException;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||
|
||||
/**
|
||||
* 微信小程序手机号认证Provider
|
||||
*
|
||||
* @author 有来技术团队
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Slf4j
|
||||
public class WxMiniAppPhoneAuthenticationProvider implements AuthenticationProvider {
|
||||
|
||||
private final UserService userService;
|
||||
private final WxMaService wxMaService;
|
||||
|
||||
public WxMiniAppPhoneAuthenticationProvider(UserService userService, WxMaService wxMaService) {
|
||||
this.userService = userService;
|
||||
this.wxMaService = wxMaService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
|
||||
WxMiniAppPhoneAuthenticationToken authenticationToken = (WxMiniAppPhoneAuthenticationToken) authentication;
|
||||
String code = (String) authenticationToken.getPrincipal();
|
||||
String encryptedData = authenticationToken.getEncryptedData();
|
||||
String iv = authenticationToken.getIv();
|
||||
|
||||
// 1. 通过code获取session_key
|
||||
WxMaJscode2SessionResult sessionInfo;
|
||||
try {
|
||||
sessionInfo = wxMaService.getUserService().getSessionInfo(code);
|
||||
} catch (WxErrorException e) {
|
||||
log.error("获取微信session_key失败", e);
|
||||
throw new CredentialsExpiredException("微信登录code无效或已过期");
|
||||
}
|
||||
|
||||
String sessionKey = sessionInfo.getSessionKey();
|
||||
String openId = sessionInfo.getOpenid();
|
||||
|
||||
if (StrUtil.isBlank(sessionKey) || StrUtil.isBlank(openId)) {
|
||||
throw new CredentialsExpiredException("获取微信会话信息失败");
|
||||
}
|
||||
|
||||
// 2. 解密手机号信息
|
||||
WxMaPhoneNumberInfo phoneNumberInfo;
|
||||
try {
|
||||
if (StrUtil.isNotBlank(encryptedData) && StrUtil.isNotBlank(iv)) {
|
||||
phoneNumberInfo = wxMaService.getUserService().getPhoneNoInfo(sessionKey, encryptedData, iv);
|
||||
} else {
|
||||
throw new IllegalArgumentException("缺少手机号加密数据");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("解密微信手机号失败", e);
|
||||
throw new CredentialsExpiredException("解密手机号信息失败");
|
||||
}
|
||||
|
||||
if (phoneNumberInfo == null || StrUtil.isBlank(phoneNumberInfo.getPhoneNumber())) {
|
||||
throw new CredentialsExpiredException("获取手机号失败");
|
||||
}
|
||||
|
||||
String phoneNumber = phoneNumberInfo.getPhoneNumber();
|
||||
|
||||
// 3. 根据手机号查询用户,不存在则创建新用户
|
||||
UserAuthInfo userAuthInfo = userService.getAuthInfoByMobile(phoneNumber);
|
||||
|
||||
if (userAuthInfo == null) {
|
||||
// 用户不存在,注册新用户
|
||||
boolean registered = userService.registerUserByMobileAndOpenId(phoneNumber, openId);
|
||||
if (!registered) {
|
||||
throw new UsernameNotFoundException("用户注册失败");
|
||||
}
|
||||
// 重新获取用户信息
|
||||
userAuthInfo = userService.getAuthInfoByMobile(phoneNumber);
|
||||
} else {
|
||||
// 用户存在,绑定openId(如果未绑定)
|
||||
userService.bindUserOpenId(userAuthInfo.getUserId(), openId);
|
||||
}
|
||||
|
||||
// 4. 检查用户状态
|
||||
if (ObjectUtil.notEqual(userAuthInfo.getStatus(), 1)) {
|
||||
throw new DisabledException("用户已被禁用");
|
||||
}
|
||||
|
||||
// 5. 构建认证后的用户详情
|
||||
SysUserDetails userDetails = new SysUserDetails(userAuthInfo);
|
||||
|
||||
// 6. 创建已认证的Token
|
||||
return WxMiniAppPhoneAuthenticationToken.authenticated(
|
||||
userDetails,
|
||||
userDetails.getAuthorities()
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(Class<?> authentication) {
|
||||
return WxMiniAppPhoneAuthenticationToken.class.isAssignableFrom(authentication);
|
||||
}
|
||||
}
|
||||
@@ -53,18 +53,6 @@ public interface UserMapper extends BaseMapper<User> {
|
||||
return getAuthInfoByUsername(username);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据微信openid获取用户认证信息
|
||||
*
|
||||
* @param openid 微信openid
|
||||
* @return 认证信息
|
||||
*/
|
||||
UserAuthInfo getAuthInfoByOpenId(String openid);
|
||||
|
||||
default UserAuthInfo getAuthCredentialsByOpenId(String openid) {
|
||||
return getAuthInfoByOpenId(openid);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据手机号获取用户认证信息
|
||||
*
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package com.youlai.boot.system.model.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.youlai.boot.common.base.BaseEntity;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
@@ -74,9 +72,4 @@ public class User extends BaseEntity {
|
||||
* 是否删除(0-否 1-是)
|
||||
*/
|
||||
private Integer isDeleted;
|
||||
|
||||
/**
|
||||
* 微信 OpenID
|
||||
*/
|
||||
private String openid;
|
||||
}
|
||||
@@ -56,7 +56,4 @@ public class UserForm {
|
||||
@NotEmpty(message = "用户角色不能为空")
|
||||
private List<Long> roleIds;
|
||||
|
||||
@Schema(description="微信openId")
|
||||
private String openId;
|
||||
|
||||
}
|
||||
|
||||
@@ -181,26 +181,6 @@ public interface UserService extends IService<User> {
|
||||
*/
|
||||
List<Option<String>> listUserOptions();
|
||||
|
||||
/**
|
||||
* 根据 openid 获取用户认证信息
|
||||
*
|
||||
* @param openId 用户名
|
||||
* @return {@link UserAuthInfo}
|
||||
*/
|
||||
|
||||
UserAuthInfo getAuthInfoByOpenId(String openId);
|
||||
|
||||
default UserAuthInfo getAuthCredentialsByOpenId(String openId) {
|
||||
return getAuthInfoByOpenId(openId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据微信 OpenID 注册或绑定用户
|
||||
*
|
||||
* @param openId 微信 OpenID
|
||||
*/
|
||||
boolean registerOrBindWechatUser(String openId);
|
||||
|
||||
/**
|
||||
* 根据手机号获取用户认证信息
|
||||
*
|
||||
@@ -213,22 +193,4 @@ public interface UserService extends IService<User> {
|
||||
return getAuthInfoByMobile(mobile);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据手机号和OpenID注册用户
|
||||
*
|
||||
* @param mobile 手机号
|
||||
* @param openId 微信OpenID
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean registerUserByMobileAndOpenId(String mobile, String openId);
|
||||
|
||||
/**
|
||||
* 绑定用户微信OpenID
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param openId 微信OpenID
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean bindUserOpenId(Long userId, String openId);
|
||||
|
||||
}
|
||||
|
||||
@@ -222,28 +222,6 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
|
||||
return userAuthInfo;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 根据OpenID获取用户认证信息
|
||||
*
|
||||
* @param openId 微信OpenID
|
||||
* @return 用户认证信息
|
||||
*/
|
||||
@Override
|
||||
public UserAuthInfo getAuthInfoByOpenId(String openId) {
|
||||
if (StrUtil.isBlank(openId)) {
|
||||
return null;
|
||||
}
|
||||
UserAuthInfo userAuthInfo = this.baseMapper.getAuthInfoByOpenId(openId);
|
||||
if (userAuthInfo != null) {
|
||||
Set<String> roles = userAuthInfo.getRoles();
|
||||
// 获取最大范围的数据权限
|
||||
Integer dataScope = roleService.getMaximumDataScope(roles);
|
||||
userAuthInfo.setDataScope(dataScope);
|
||||
}
|
||||
return userAuthInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据手机号获取用户认证信息
|
||||
*
|
||||
@@ -265,137 +243,6 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
|
||||
return userAuthInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册或绑定微信用户
|
||||
*
|
||||
* @param openId 微信OpenID
|
||||
* @return 是否成功
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean registerOrBindWechatUser(String openId) {
|
||||
if (StrUtil.isBlank(openId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 查询是否已存在该openId的用户
|
||||
User existUser = this.getOne(
|
||||
new LambdaQueryWrapper<User>()
|
||||
.eq(User::getOpenid, openId)
|
||||
);
|
||||
|
||||
if (existUser != null) {
|
||||
// 用户已存在,不需要注册
|
||||
return true;
|
||||
}
|
||||
|
||||
// 创建新用户
|
||||
User newUser = new User();
|
||||
newUser.setNickname("微信用户"); // 默认昵称
|
||||
newUser.setUsername(openId); // TODO 后续替换为手机号
|
||||
newUser.setOpenid(openId);
|
||||
newUser.setGender(0); // 保密
|
||||
newUser.setUpdateBy(SecurityUtils.getUserId());
|
||||
newUser.setPassword(SystemConstants.DEFAULT_PASSWORD);
|
||||
newUser.setCreateTime(LocalDateTime.now());
|
||||
newUser.setUpdateTime(LocalDateTime.now());
|
||||
this.save(newUser);
|
||||
// 为了默认系统管理员角色,这里按需调整,实际情况绑定已存在的系统用户,另一种情况是给默认游客角色,然后由系统管理员设置用户的角色
|
||||
UserRole userRole = new UserRole();
|
||||
userRole.setUserId(newUser.getId());
|
||||
userRole.setRoleId(1L); // TODO 系统管理员
|
||||
userRoleService.save(userRole);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据手机号和OpenID注册用户
|
||||
*
|
||||
* @param mobile 手机号
|
||||
* @param openId 微信OpenID
|
||||
* @return 是否成功
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean registerUserByMobileAndOpenId(String mobile, String openId) {
|
||||
if (StrUtil.isBlank(mobile) || StrUtil.isBlank(openId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 先查询是否已存在手机号对应的用户
|
||||
User existingUser = this.getOne(
|
||||
new LambdaQueryWrapper<User>()
|
||||
.eq(User::getMobile, mobile)
|
||||
);
|
||||
|
||||
if (existingUser != null) {
|
||||
// 如果存在用户但没绑定openId,则绑定openId
|
||||
if (StrUtil.isBlank(existingUser.getOpenid())) {
|
||||
return bindUserOpenId(existingUser.getId(), openId);
|
||||
}
|
||||
// 如果已经绑定了其他openId,则判断是否需要更新
|
||||
else if (!openId.equals(existingUser.getOpenid())) {
|
||||
return bindUserOpenId(existingUser.getId(), openId);
|
||||
}
|
||||
// 如果已经绑定了相同的openId,则不需要任何操作
|
||||
return true;
|
||||
}
|
||||
|
||||
// 不存在用户,创建新用户
|
||||
User newUser = new User();
|
||||
newUser.setMobile(mobile);
|
||||
newUser.setOpenid(openId);
|
||||
newUser.setUsername(mobile); // 使用手机号作为用户名
|
||||
newUser.setNickname("微信用户_" + mobile.substring(mobile.length() - 4)); // 使用手机号后4位作为昵称
|
||||
newUser.setPassword(SystemConstants.DEFAULT_PASSWORD); // 使用加密的openId作为初始密码
|
||||
newUser.setGender(0); // 保密
|
||||
newUser.setCreateTime(LocalDateTime.now());
|
||||
newUser.setUpdateTime(LocalDateTime.now());
|
||||
this.save(newUser);
|
||||
// 为了默认系统管理员角色,这里按需调整,实际情况绑定已存在的系统用户,另一种情况是给默认游客角色,然后由系统管理员设置用户的角色
|
||||
UserRole userRole = new UserRole();
|
||||
userRole.setUserId(newUser.getId());
|
||||
userRole.setRoleId(1L); // TODO 系统管理员
|
||||
userRoleService.save(userRole);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定用户微信OpenID
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param openId 微信OpenID
|
||||
* @return 是否成功
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean bindUserOpenId(Long userId, String openId) {
|
||||
if (userId == null || StrUtil.isBlank(openId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查是否已有其他用户绑定了此openId
|
||||
User existingUser = this.getOne(
|
||||
new LambdaQueryWrapper<User>()
|
||||
.eq(User::getOpenid, openId)
|
||||
.ne(User::getId, userId)
|
||||
);
|
||||
|
||||
if (existingUser != null) {
|
||||
log.warn("OpenID {} 已被用户 {} 绑定,无法为用户 {} 绑定", openId, existingUser.getId(), userId);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 更新用户openId
|
||||
boolean updated = this.update(
|
||||
new LambdaUpdateWrapper<User>()
|
||||
.eq(User::getId, userId)
|
||||
.set(User::getOpenid, openId)
|
||||
.set(User::getUpdateTime, LocalDateTime.now())
|
||||
);
|
||||
return updated ;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取导出用户列表
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user