refactor: 实体命名规范调整,代码生成同步调整

This commit is contained in:
Ray.Hao
2025-12-18 09:43:36 +08:00
parent 5817826bbd
commit 8eaed3cfb7
165 changed files with 1885 additions and 2038 deletions

View File

@@ -2,7 +2,7 @@
FROM openjdk:17
# 维护者信息
MAINTAINER youlai <youlaitech@163.com>
LABEL maintainer="youlai <youlaitech@163.com>"
# 设置时区Debian直接使用环境变量
ENV TZ=Asia/Shanghai

View File

@@ -138,8 +138,8 @@ CREATE TABLE `sys_menu` (
-- ----------------------------
-- 顶级目录1-9系统/代码生成/AI助手/文档/接口文档/组件/演示/多级/路由
INSERT INTO `sys_menu` VALUES (1, 0, '0', '系统管理', 'C', '', '/system', 'Layout', NULL, NULL, NULL, 1, 1, 'system', '/system/user', now(), now(), NULL);
INSERT INTO `sys_menu` VALUES (2, 0, '0', '代码生成', 'C', '', '/gen', 'Layout', NULL, NULL, NULL, 1, 2, 'code', '/gen/index', now(), now(), NULL);
INSERT INTO `sys_menu` VALUES (3, 0, '0', 'AI助手', 'C', '', '/ai', 'Layout', NULL, NULL, NULL, 1, 3, 'platform', '/ai/command-record', now(), now(), NULL);
INSERT INTO `sys_menu` VALUES (2, 0, '0', '代码生成', 'C', '', '/codegen', 'Layout', NULL, NULL, NULL, 1, 2, 'code', '/codegen/index', now(), now(), NULL);
INSERT INTO `sys_menu` VALUES (3, 0, '0', 'AI助手', 'C', '', '/ai', 'Layout', NULL, NULL, NULL, 1, 3, 'ai', '/ai/command-record', now(), now(), NULL);
INSERT INTO `sys_menu` VALUES (4, 0, '0', '平台文档', 'C', '', '/doc', 'Layout', NULL, NULL, NULL, 1, 4, 'document', '', now(), now(), NULL);
INSERT INTO `sys_menu` VALUES (5, 0, '0', '接口文档', 'C', '', '/api', 'Layout', NULL, NULL, NULL, 1, 5, 'api', '', now(), now(), NULL);
INSERT INTO `sys_menu` VALUES (6, 0, '0', '组件封装', 'C', '', '/component', 'Layout', NULL, NULL, NULL, 1, 6, 'menu', '', now(), now(), NULL);
@@ -205,7 +205,7 @@ INSERT INTO `sys_menu` VALUES (2805, 280, '0,1,280', '通知发布', 'B', NULL,
INSERT INTO `sys_menu` VALUES (2806, 280, '0,1,280', '通知撤回', 'B', NULL, '', NULL, 'sys:notice:revoke', 0, 1, 1, 6, '', NULL, now(), now(), NULL);
-- 代码生成
INSERT INTO `sys_menu` VALUES (310, 2, '0,2', '代码生成', 'M', 'Gen', 'gen', 'gen/index', NULL, NULL, 1, 1, 1, 'code', NULL, now(), now(), NULL);
INSERT INTO `sys_menu` VALUES (310, 2, '0,2', '代码生成', 'M', 'Codegen', 'codegen', 'codegen/index', NULL, NULL, 1, 1, 1, 'code', NULL, now(), now(), NULL);
-- AI 助手
INSERT INTO `sys_menu` VALUES (401, 3, '0,3', 'AI命令记录', 'M', 'AiCommandRecord', 'command-record', 'ai/command-record/index', NULL, NULL, 1, 1, 1, 'document', NULL, now(), now(), NULL);
@@ -537,8 +537,8 @@ INSERT INTO `sys_user_notice` VALUES (10, 10, 2, 1, NULL, now(), now(), 0);
-- ----------------------------
-- AI 命令记录表
-- ----------------------------
DROP TABLE IF EXISTS `ai_command_record`;
CREATE TABLE `ai_command_record` (
DROP TABLE IF EXISTS `ai_assistant_record`;
CREATE TABLE `ai_assistant_record` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`user_id` bigint DEFAULT NULL COMMENT '用户ID',
`username` varchar(64) DEFAULT NULL COMMENT '用户名',
@@ -565,40 +565,6 @@ CREATE TABLE `ai_command_record` (
KEY `idx_create_time` (`create_time`),
KEY `idx_provider` (`ai_provider`),
KEY `idx_model` (`ai_model`),
KEY `idx_parse_success` (`parse_status`),
KEY `idx_parse_status` (`parse_status`),
KEY `idx_execute_status` (`execute_status`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='AI 命令记录表';
-- ----------------------------
-- 租户表(多租户模式)
-- ----------------------------
DROP TABLE IF EXISTS `sys_tenant`;
CREATE TABLE `sys_tenant` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '租户ID',
`name` varchar(100) NOT NULL COMMENT '租户名称',
`code` varchar(50) NOT NULL COMMENT '租户编码(唯一)',
`contact_name` varchar(50) DEFAULT NULL COMMENT '联系人姓名',
`contact_phone` varchar(20) DEFAULT NULL COMMENT '联系人电话',
`contact_email` varchar(100) DEFAULT NULL COMMENT '联系人邮箱',
`domain` varchar(100) DEFAULT NULL COMMENT '租户域名(用于域名识别)',
`logo` varchar(255) DEFAULT NULL COMMENT '租户Logo',
`status` tinyint DEFAULT '1' COMMENT '状态(1-正常 0-禁用)',
`remark` varchar(500) DEFAULT NULL COMMENT '备注',
`expire_time` datetime DEFAULT NULL COMMENT '过期时间NULL表示永不过期',
`create_time` datetime COMMENT '创建时间',
`update_time` datetime COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_code` (`code`),
UNIQUE KEY `uk_domain` (`domain`),
KEY `idx_status` (`status`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='系统租户表';
-- ----------------------------
-- Records of sys_tenant
-- ----------------------------
INSERT INTO `sys_tenant` VALUES (1, '默认租户', 'DEFAULT', '系统管理员', '18812345678', 'admin@youlai.tech', NULL, NULL, 1, '系统默认租户', NULL, now(), now());
INSERT INTO `sys_tenant` VALUES (2, '演示租户', 'DEMO', '演示用户', '18812345679', 'demo@youlai.tech', 'demo.youlai.tech', NULL, 1, '演示租户', NULL, now(), now());
SET FOREIGN_KEY_CHECKS = 1;
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='AI 助手行为记录表(解析、执行、审计)';

View File

@@ -1,12 +1,12 @@
package com.youlai.boot.auth.controller;
import com.youlai.boot.auth.model.vo.CaptchaVO;
import com.youlai.boot.auth.model.vo.CaptchaVo;
import com.youlai.boot.auth.model.dto.LoginRequest;
import com.youlai.boot.auth.model.dto.WxMiniAppPhoneLoginDTO;
import com.youlai.boot.auth.model.dto.WxMiniAppPhoneLoginDto;
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.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;
@@ -37,8 +37,8 @@ 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);
}
@@ -84,15 +84,15 @@ 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);
}

View File

@@ -13,7 +13,7 @@ import jakarta.validation.constraints.NotBlank;
*/
@Schema(description = "微信小程序Code登录请求参数")
@Data
public class WxMiniAppCodeLoginDTO {
public class WxMiniAppCodeLoginDto {
@Schema(description = "微信小程序登录时获取的code", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank(message = "code不能为空")

View File

@@ -13,7 +13,7 @@ import jakarta.validation.constraints.NotBlank;
*/
@Schema(description = "微信小程序手机号登录请求参数")
@Data
public class WxMiniAppPhoneLoginDTO {
public class WxMiniAppPhoneLoginDto {
@Schema(description = "微信小程序登录时获取的code", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank(message = "code不能为空")

View File

@@ -13,7 +13,7 @@ import lombok.Data;
@Schema(description = "验证码信息")
@Data
@Builder
public class CaptchaVO {
public class CaptchaVo {
@Schema(description = "验证码缓存 ID")
private String captchaId;

View File

@@ -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);
/**
* 发送短信验证码

View File

@@ -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 完成匹配)
// 认证通过后返回已认证的 Authenticationprincipal 为 SysUserDetailsauthorities 为角色/权限集合)。
Authentication authentication = authenticationManager.authenticate(authenticationToken);
// 3. 认证成功后生成 JWT 令牌,并存入 Security 上下文,供登录日志 AOP 使用(已认证)
@@ -166,7 +171,7 @@ public class AuthServiceImpl implements AuthService {
* @return 验证码
*/
@Override
public CaptchaVO getCaptcha() {
public CaptchaVo getCaptcha() {
String captchaType = captchaProperties.getType();
int width = captchaProperties.getWidth();
@@ -202,7 +207,7 @@ public class AuthServiceImpl implements AuthService {
TimeUnit.SECONDS
);
return CaptchaVO.builder()
return CaptchaVo.builder()
.captchaId(captchaId)
.captchaBase64(imageBase64Data)
.build();
@@ -222,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);
@@ -243,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()
);
// 执行认证

View File

@@ -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;
}

View File

@@ -43,7 +43,7 @@ public enum ResultCode implements IResultCode, Serializable {
VERIFICATION_CODE_INPUT_ERROR("A0130", "校验码输入错误"),
SMS_VERIFICATION_CODE_INPUT_ERROR("A0131", "短信校验码输入错误"),
EMAIL_VERIFICATION_CODE_INPUT_ERROR("A0132", "邮件校验码输入错误"),
VOICE_VERIFICATION_CODE_INPUT_ERROR("A0133", "语音校验码输入错误"),
VoICE_VERIFICATION_CODE_INPUT_ERROR("A0133", "语音校验码输入错误"),
USER_CERTIFICATE_EXCEPTION("A0140", "用户证件异常"),
USER_CERTIFICATE_TYPE_NOT_SELECTED("A0141", "用户证件类型未选择"),
@@ -262,7 +262,7 @@ public enum ResultCode implements IResultCode, Serializable {
/** 二级宏观错误码 */
NOTIFICATION_SERVICE_ERROR("C0500", "通知服务出错"),
SMS_REMINDER_SERVICE_FAILED("C0501", "短信提醒服务失败"),
VOICE_REMINDER_SERVICE_FAILED("C0502", "语音提醒服务失败"),
VoICE_REMINDER_SERVICE_FAILED("C0502", "语音提醒服务失败"),
EMAIL_REMINDER_SERVICE_FAILED("C0503", "邮件提醒服务失败");

View File

@@ -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();
}
}

View File

@@ -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("撤销成功");
}
}

View File

@@ -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);
}

View File

@@ -1,26 +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
*
* @author Ray.Hao
* @since 3.0.0
*/
@Mapper
public interface AiCommandRecordMapper extends BaseMapper<AiCommandRecord> {
/**
* 获取 AI 命令记录分页列表
*/
IPage<AiCommandRecordVO> getRecordPage(Page<AiCommandRecordVO> page, AiCommandPageQuery queryParams);
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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 {
/**
* 函数名称

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -1,82 +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 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;
}

View File

@@ -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;
}

View File

@@ -1,45 +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 = "执行状态(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;
}

View File

@@ -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;
}

View File

@@ -1,92 +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 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 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;
}

View File

@@ -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);
}

View File

@@ -1,33 +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 命令记录服务接口
*
* @author Ray.Hao
* @since 3.0.0
*/
public interface AiCommandRecordService extends IService<AiCommandRecord> {
/**
* 获取命令记录分页列表
*
* @param queryParams 查询参数
* @return 命令记录分页列表
*/
IPage<AiCommandRecordVO> getRecordPage(AiCommandPageQuery queryParams);
/**
* 撤销命令执行
*
* @param logId 记录ID
*/
void rollbackCommand(String logId);
}

View File

@@ -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;
}

View File

@@ -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("回滚功能尚未实现");
}
}

View File

@@ -1,50 +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 命令记录服务实现类
*
* @author Ray.Hao
* @since 3.0.0
*/
@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 logId) {
AiCommandRecord commandRecord = this.getById(logId);
if (commandRecord == null) {
throw new RuntimeException("命令记录不存在");
}
if (commandRecord.getExecuteStatus() == null || commandRecord.getExecuteStatus() != 1) {
throw new RuntimeException("只能撤销成功执行的命令");
}
// TODO: 实现具体的回滚逻辑
log.info("撤销命令执行: logId={}, function={}", logId, commandRecord.getFunctionName());
throw new UnsupportedOperationException("回滚功能尚未实现");
}
}

View File

@@ -1,324 +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.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 commandRecord = new AiCommandRecord();
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);
recordService.save(commandRecord);
AiParseResponseDTO response = AiParseResponseDTO.builder()
.parseLogId(commandRecord.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: {}", commandRecord.getId());
}
return response;
} 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);
recordService.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 startTime = System.currentTimeMillis();
// 获取用户信息
Long userId = SecurityUtils.getUserId();
String username = SecurityUtils.getUsername();
String ipAddress = JakartaServletUtil.getClientIP(httpRequest);
AiFunctionCallDTO functionCall = request.getFunctionCall();
// 根据解析日志ID获取审计记录如果不存在则创建新记录
AiCommandRecord commandRecord ;
if (StrUtil.isNotBlank(request.getParseLogId())) {
// 更新已存在的审计记录(解析阶段已创建)
commandRecord = recordService.getById(request.getParseLogId());
if (commandRecord == null) {
throw new IllegalStateException("未找到对应的解析记录ID: " + request.getParseLogId());
}
} else {
// 如果没有解析日志ID创建新记录兼容直接执行的情况
commandRecord = new AiCommandRecord();
commandRecord.setUserId(userId);
commandRecord.setUsername(username);
commandRecord.setOriginalCommand(request.getOriginalCommand());
commandRecord.setIpAddress(ipAddress);
recordService.save(commandRecord);
}
// 更新执行相关字段
commandRecord.setFunctionName(functionCall.getName());
commandRecord.setFunctionArguments(JSONUtil.toJsonStr(functionCall.getArguments()));
commandRecord.setExecuteStatus(0); // 0-待执行
try {
// 🎯 执行具体的函数调用
Object result = executeFunctionCall(functionCall);
// 更新执行成功
commandRecord.setExecuteStatus(1); // 1-成功
commandRecord.setExecuteErrorMessage(null);
// 更新审计记录
recordService.updateById(commandRecord);
log.info("✅ 命令执行成功审计记录ID: {}", commandRecord.getId());
return result;
} catch (Exception e) {
// 更新执行失败
commandRecord.setExecuteStatus(-1); // -1-失败
commandRecord.setExecuteErrorMessage(e.getMessage());
// 更新审计记录
recordService.updateById(commandRecord);
log.error("❌ 命令执行失败审计记录ID: {}", commandRecord.getId(), e);
// 抛出异常,由 Controller 统一处理
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);
}
}
/**
* 使用 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);
}
}

View File

@@ -8,8 +8,8 @@ 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.GenTableService;
import io.swagger.v3.oas.annotations.Operation;
@@ -46,10 +46,10 @@ public class CodegenController {
@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);
}
@@ -82,9 +82,9 @@ public class CodegenController {
@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);
}

View File

@@ -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;
}
}

View File

@@ -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);
/**
* 获取表字段列表

View File

@@ -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 {

View File

@@ -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;

View File

@@ -6,7 +6,7 @@ import lombok.Data;
@Schema(description = "表视图对象")
@Data
public class TablePageVO {
public class TablePageVo {
@Schema(description = "表名称", example = "sys_user")
private String tableName;

View File

@@ -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);
/**
* 下载代码

View File

@@ -21,8 +21,8 @@ import com.youlai.boot.platform.codegen.mapper.DatabaseMapper;
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
@@ -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,9 +77,9 @@ 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<>();
GenTable genTable = genTableService.getOne(new LambdaQueryWrapper<GenTable>()
.eq(GenTable::getTableName, tableName)
@@ -96,7 +100,7 @@ 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();
@@ -110,7 +114,7 @@ public class CodegenServiceImpl implements CodegenService {
// 文件名 UserController.java
String fileName = getFileName(entityName, templateName, extension);
previewVO.setFileName(fileName);
previewVo.setFileName(fileName);
/* 2. 生成文件路径 */
// 包名com.youlai.boot
@@ -121,26 +125,28 @@ public class CodegenServiceImpl implements CodegenService {
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(genTable.getPageType(), pageType);
String content = getCodeContent(templateConfig, genTable, fieldConfigs, finalType);
previewVO.setContent(content);
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,12 +224,13 @@ public class CodegenServiceImpl implements CodegenService {
}
/**
* 生成代码内容
* 渲染模板,生成代码内容
*
* @param templateConfig 模板配置
* @param genConfig 生成配置
* @param genTable 生成配置
* @param fieldConfigs 字段配置
* @return 代码内容
* @param pageType 前端页面类型
* @return 渲染后的代码内容
*/
private String getCodeContent(CodegenProperties.TemplateConfig templateConfig, GenTable genTable, List<GenTableColumn> fieldConfigs, String pageType) {
@@ -228,8 +245,12 @@ public class CodegenServiceImpl implements CodegenService {
bindMap.put("entityName", entityName);
bindMap.put("tableName", genTable.getTableName());
bindMap.put("author", genTable.getAuthor());
bindMap.put("lowerFirstEntityName", StrUtil.lowerFirst(entityName)); // UserTest → userTest
bindMap.put("kebabCaseEntityName", StrUtil.toSymbolCase(entityName, '-')); // UserTest → user-test
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);
@@ -239,7 +260,11 @@ public class CodegenServiceImpl implements CodegenService {
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();

View File

@@ -161,9 +161,10 @@ public class GenTableServiceImpl extends ServiceImpl<GenTableMapper, GenTable> i
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);

View File

@@ -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()));
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}

View File

@@ -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);
}
}

View File

@@ -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 {
// ==================== 在线用户管理 ====================
private final UserSessionRegistry userSessionRegistry;
private final WebSocketPublisher webSocketPublisher;
/**
* 用户在线会话映射表
* Key: 用户名
* Value: 该用户的所有会话 ID 集合(支持多设备登录)
*/
private final Map<String, Set<String>> userSessionsMap = new ConcurrentHashMap<>();
/**
* 会话详情映射表
* 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,53 +92,17 @@ 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());
}
// 广播在线用户数变更
userSessionRegistry.removeSession(sessionId);
broadcastOnlineUserCount();
}
}
/**
* 获取在线用户列表
*
* @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);
webSocketPublisher.publish(WebSocketTopics.TOPIC_ONLINE_COUNT, count);
log.debug("✓ 已广播在线用户数量: {}", count);
} catch (Exception e) {
log.error("广播在线用户数量失败", e);
}
}
// ==================== 消息推送功能 ====================
@@ -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);
webSocketPublisher.publishToUser(username, WebSocketTopics.USER_QUEUE_MESSAGES, message);
log.info("✓ 已向用户[{}]发送通知", username);
} catch (JsonProcessingException e) {
log.error("消息序列化失败: username={}", username, e);
} catch (Exception e) {
log.error("向用户[{}]发送通知失败", username, e);
}
}
/**
@@ -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);
TextMessage systemMessage = new TextMessage("系统通知", message, System.currentTimeMillis());
webSocketPublisher.publish(WebSocketTopics.TOPIC_PUBLIC, systemMessage);
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;
}
}

View File

@@ -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;
}
}

View File

@@ -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";
}

View File

@@ -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();

View File

@@ -1,7 +1,6 @@
package com.youlai.boot.security.model;
import lombok.Data;
import java.util.Set;
/**
* 用户认证凭证信息
@@ -10,48 +9,6 @@ import java.util.Set;
* @since 2022/10/22
*/
@Data
public class UserAuthCredentials {
/**
* 用户ID
*/
private Long userId;
/**
* 用户名
*/
private String username;
/**
* 昵称
*/
private String nickname;
/**
* 部门ID
*/
private Long deptId;
/**
* 用户密码
*/
private String password;
/**
* 状态1:启用0:禁用)
*/
private Integer status;
/**
* 用户所属的角色集合
*/
private Set<String> roles;
/**
* 数据权限范围,用于控制用户可以访问的数据级别
*
* @see com.youlai.boot.common.enums.DataScopeEnum
*/
private Integer dataScope;
public class UserAuthCredentials extends UserAuthInfo {
}

View File

@@ -0,0 +1,58 @@
package com.youlai.boot.security.model;
import lombok.Data;
import java.util.Set;
/**
* 用户认证信息
* <p>
* 用于登录认证过程中的用户信息承载,包含用户名、密码、状态、角色等与认证/授权相关的数据。
* </p>
*
* @author Ray.Hao
* @since 2025/12/16
*/
@Data
public class UserAuthInfo {
/**
* 用户ID
*/
private Long userId;
/**
* 用户名
*/
private String username;
/**
* 昵称
*/
private String nickname;
/**
* 部门ID
*/
private Long deptId;
/**
* 密码(加密后)
*/
private String password;
/**
* 状态1:启用 其它:禁用)
*/
private Integer status;
/**
* 角色集合
*/
private Set<String> roles;
/**
* 数据权限范围
*/
private Integer dataScope;
}

View File

@@ -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(

View File

@@ -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(

View File

@@ -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(

View File

@@ -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());

View File

@@ -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;
@@ -38,8 +38,8 @@ public class ConfigController {
@GetMapping("/page")
@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);
}

View File

@@ -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;
@@ -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);
}

View File

@@ -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;
@@ -51,10 +51,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);
}
@@ -128,21 +128,21 @@ 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);
}

View File

@@ -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);
}
}

View File

@@ -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,8 +55,8 @@ 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);
}

View File

@@ -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;
@@ -38,8 +38,8 @@ public class NoticeController {
@Operation(summary = "通知公告分页列表")
@GetMapping("/page")
@PreAuthorize("@ss.hasPerm('sys:notice:list')")
public PageResult<NoticePageVO> getNoticePage(NoticePageQuery queryParams) {
IPage<NoticePageVO> result = noticeService.getNoticePage(queryParams);
public PageResult<NoticePageVo> getNoticePage(NoticePageQuery queryParams) {
IPage<NoticePageVo> result = noticeService.getNoticePage(queryParams);
return PageResult.success(result);
}
@@ -63,11 +63,11 @@ 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 = "修改通知公告")
@@ -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);
}
}

View File

@@ -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);
}

View File

@@ -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 haoxr
* @since 2024-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);
}
}

View File

@@ -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,10 +59,10 @@ 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);
}
@@ -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);
}

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);
}

View File

@@ -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"),

View File

@@ -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);

View File

@@ -16,7 +16,7 @@ public enum NoticePublishStatusEnum implements IBaseEnum<Integer> {
UNPUBLISHED(0, "未发布"),
PUBLISHED(1, "已发布"),
REVOKED(-1, "已撤回");
REVoKED(-1, "已撤回");
private final Integer value;

View File

@@ -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);
}
}

View File

@@ -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);

View File

@@ -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);
}

View File

@@ -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.Dict;
import com.youlai.boot.system.model.query.DictPageQuery;
import com.youlai.boot.system.model.vo.DictPageVO;
import com.youlai.boot.system.model.vo.DictPageVo;
import org.apache.ibatis.annotations.Mapper;
/**
@@ -23,7 +23,7 @@ public interface DictMapper extends BaseMapper<Dict> {
* @param queryParams 查询参数
* @return 字典分页列表
*/
Page<DictPageVO> getDictPage(Page<DictPageVO> page, DictPageQuery queryParams);
Page<DictPageVo> getDictPage(Page<DictPageVo> page, DictPageQuery queryParams);
}

View File

@@ -2,11 +2,11 @@ package com.youlai.boot.system.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.youlai.boot.system.model.bo.VisitCount;
import com.youlai.boot.system.model.bo.VisitStatsBO;
import com.youlai.boot.system.model.bo.VisitCountBo;
import com.youlai.boot.system.model.bo.VisitStatsBo;
import com.youlai.boot.system.model.entity.Log;
import com.youlai.boot.system.model.query.LogPageQuery;
import com.youlai.boot.system.model.vo.LogPageVO;
import com.youlai.boot.system.model.vo.LogPageVo;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@@ -24,7 +24,7 @@ public interface LogMapper extends BaseMapper<Log> {
/**
* 获取日志分页列表
*/
Page<LogPageVO> getLogPage(Page<LogPageVO> page, LogPageQuery queryParams);
Page<LogPageVo> getLogPage(Page<LogPageVo> page, LogPageQuery queryParams);
/**
* 统计浏览数(PV)
@@ -32,7 +32,7 @@ public interface LogMapper extends BaseMapper<Log> {
* @param startDate 开始日期 yyyy-MM-dd
* @param endDate 结束日期 yyyy-MM-dd
*/
List<VisitCount> getPvCounts(String startDate, String endDate);
List<VisitCountBo> getPvCounts(String startDate, String endDate);
/**
* 统计IP数
@@ -40,17 +40,17 @@ public interface LogMapper extends BaseMapper<Log> {
* @param startDate 开始日期 yyyy-MM-dd
* @param endDate 结束日期 yyyy-MM-dd
*/
List<VisitCount> getIpCounts(String startDate, String endDate);
List<VisitCountBo> getIpCounts(String startDate, String endDate);
/**
* 获取浏览量(PV)统计
*/
VisitStatsBO getPvStats();
VisitStatsBo getPvStats();
/**
* 获取访问IP统计
*/
VisitStatsBO getUvStats();
VisitStatsBo getUvStats();
}

View File

@@ -2,10 +2,10 @@ package com.youlai.boot.system.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
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.query.NoticePageQuery;
import com.youlai.boot.system.model.vo.NoticePageVO;
import com.youlai.boot.system.model.vo.NoticePageVo;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
@@ -25,7 +25,7 @@ public interface NoticeMapper extends BaseMapper<Notice> {
* @param queryParams 查询参数
* @return 通知公告分页数据
*/
Page<NoticeBO> getNoticePage(Page<NoticePageVO> page, NoticePageQuery queryParams);
Page<NoticeBo> getNoticePage(Page<NoticePageVo> page, NoticePageQuery queryParams);
/**
* 获取阅读时通知公告详情
@@ -33,5 +33,5 @@ public interface NoticeMapper extends BaseMapper<Notice> {
* @param id 通知公告ID
* @return 通知公告详情
*/
NoticeBO getNoticeDetail(@Param("id") Long id);
NoticeBo getNoticeDetail(@Param("id") Long id);
}

View File

@@ -1,7 +1,7 @@
package com.youlai.boot.system.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.youlai.boot.system.model.bo.RolePermsBO;
import com.youlai.boot.system.model.bo.RolePermsBo;
import com.youlai.boot.system.model.entity.RoleMenu;
import org.apache.ibatis.annotations.Mapper;
@@ -28,7 +28,7 @@ public interface RoleMenuMapper extends BaseMapper<RoleMenu> {
/**
* 获取权限和拥有权限的角色列表
*/
List<RolePermsBO> getRolePermsList(String roleCode);
List<RolePermsBo> getRolePermsList(String roleCode);
/**

View File

@@ -2,13 +2,13 @@ package com.youlai.boot.system.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.youlai.boot.system.model.bo.UserBO;
import com.youlai.boot.system.model.bo.UserBo;
import com.youlai.boot.system.model.entity.User;
import com.youlai.boot.system.model.query.UserPageQuery;
import com.youlai.boot.system.model.form.UserForm;
import com.youlai.boot.common.annotation.DataPermission;
import com.youlai.boot.security.model.UserAuthCredentials;
import com.youlai.boot.system.model.dto.UserExportDTO;
import com.youlai.boot.security.model.UserAuthInfo;
import com.youlai.boot.system.model.dto.UserExportDto;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@@ -30,7 +30,7 @@ public interface UserMapper extends BaseMapper<User> {
* @return 用户分页列表
*/
@DataPermission(deptAlias = "u", userAlias = "u")
Page<UserBO> getUserPage(Page<UserBO> page, UserPageQuery queryParams);
Page<UserBo> getUserPage(Page<UserBo> page, UserPageQuery queryParams);
/**
* 获取用户表单详情
@@ -46,7 +46,11 @@ public interface UserMapper extends BaseMapper<User> {
* @param username 用户名
* @return 认证信息
*/
UserAuthCredentials getAuthCredentialsByUsername(String username);
UserAuthInfo getAuthInfoByUsername(String username);
default UserAuthInfo getAuthCredentialsByUsername(String username) {
return getAuthInfoByUsername(username);
}
/**
* 根据微信openid获取用户认证信息
@@ -54,7 +58,11 @@ public interface UserMapper extends BaseMapper<User> {
* @param openid 微信openid
* @return 认证信息
*/
UserAuthCredentials getAuthCredentialsByOpenId(String openid);
UserAuthInfo getAuthInfoByOpenId(String openid);
default UserAuthInfo getAuthCredentialsByOpenId(String openid) {
return getAuthInfoByOpenId(openid);
}
/**
* 根据手机号获取用户认证信息
@@ -62,7 +70,11 @@ public interface UserMapper extends BaseMapper<User> {
* @param mobile 手机号
* @return 认证信息
*/
UserAuthCredentials getAuthCredentialsByMobile(String mobile);
UserAuthInfo getAuthInfoByMobile(String mobile);
default UserAuthInfo getAuthCredentialsByMobile(String mobile) {
return getAuthInfoByMobile(mobile);
}
/**
* 获取导出用户列表
@@ -71,7 +83,7 @@ public interface UserMapper extends BaseMapper<User> {
* @return 导出用户列表
*/
@DataPermission(deptAlias = "u", userAlias = "u")
List<UserExportDTO> listExportUsers(UserPageQuery queryParams);
List<UserExportDto> listExportUsers(UserPageQuery queryParams);
/**
* 获取用户个人中心信息
@@ -79,6 +91,6 @@ public interface UserMapper extends BaseMapper<User> {
* @param userId 用户ID
* @return 用户个人中心信息
*/
UserBO getUserProfile(Long userId);
UserBo getUserProfile(Long userId);
}

View File

@@ -5,8 +5,8 @@ import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.youlai.boot.system.model.entity.UserNotice;
import com.youlai.boot.system.model.query.NoticePageQuery;
import com.youlai.boot.system.model.vo.NoticePageVO;
import com.youlai.boot.system.model.vo.UserNoticePageVO;
import com.youlai.boot.system.model.vo.NoticePageVo;
import com.youlai.boot.system.model.vo.UserNoticePageVo;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
@@ -26,5 +26,5 @@ public interface UserNoticeMapper extends BaseMapper<UserNotice> {
* @param queryParams 查询参数
* @return 通知公告分页列表
*/
IPage<UserNoticePageVO> getMyNoticePage(Page<NoticePageVO> page, @Param("queryParams") NoticePageQuery queryParams);
IPage<UserNoticePageVo> getMyNoticePage(Page<NoticePageVo> page, @Param("queryParams") NoticePageQuery queryParams);
}

View File

@@ -18,5 +18,5 @@ public interface UserRoleMapper extends BaseMapper<UserRole> {
*
* @param roleId 角色ID
*/
int countUsersForRole(Long roleId);
int countUsersByRoleId(Long roleId);
}

View File

@@ -11,7 +11,7 @@ import java.time.LocalDateTime;
* @since 2024-09-01 10:31
*/
@Data
public class NoticeBO {
public class NoticeBo {
/**
* 通知ID

View File

@@ -11,7 +11,7 @@ import java.util.Set;
* @since 2023/11/29
*/
@Data
public class RolePermsBO {
public class RolePermsBo {
/**
* 角色编码

View File

@@ -7,11 +7,11 @@ import java.time.LocalDateTime;
/**
* 用户持久化对象
*
* @author haoxr
* @author Ray.Hao
* @since 2022/6/10
*/
@Data
public class UserBO {
public class UserBo {
/**
* 用户ID

View File

@@ -9,7 +9,7 @@ import lombok.Data;
* @since 2.10.0
*/
@Data
public class VisitCount {
public class VisitCountBo {
/**
* 日期 yyyy-MM-dd

View File

@@ -14,7 +14,7 @@ import java.math.BigDecimal;
*/
@Getter
@Setter
public class VisitStatsBO {
public class VisitStatsBo {
@Schema(description = "今日访问量 (PV)")
private Integer todayCount;

View File

@@ -13,7 +13,7 @@ import java.util.Set;
*/
@Schema(description ="当前登录用户对象")
@Data
public class CurrentUserDTO {
public class CurrentUserDto {
@Schema(description="用户ID")
private Long userId;

View File

@@ -1,28 +0,0 @@
package com.youlai.boot.system.model.dto;
import lombok.Data;
/**
* 字典更新事件消息
*
* @author Ray.Hao
* @since 3.0.0
*/
@Data
public class DictEventDTO {
/**
* 字典编码
*/
private String dictCode;
/**
* 时间戳
*/
private long timestamp;
public DictEventDTO(String dictCode) {
this.dictCode = dictCode;
this.timestamp = System.currentTimeMillis();
}
}

View File

@@ -13,7 +13,7 @@ import java.time.LocalDateTime;
* @since 2024-9-2 14:32:58
*/
@Data
public class NoticeDTO {
public class NoticeDto {
@Schema(description = "通知ID")
private Long id;
@@ -28,5 +28,4 @@ public class NoticeDTO {
@JsonFormat(pattern = "yyyy-MM-dd HH:mm")
private LocalDateTime publishTime;
}

View File

@@ -16,7 +16,7 @@ import java.time.LocalDateTime;
@Data
@ColumnWidth(20)
public class UserExportDTO {
public class UserExportDto {
@ExcelProperty(value = "用户名")
private String username;

View File

@@ -10,7 +10,7 @@ import lombok.Data;
* @since 2022/4/10
*/
@Data
public class UserImportDTO {
public class UserImportDto {
@ExcelProperty(value = "用户名")
private String username;

View File

@@ -6,13 +6,13 @@ import java.util.HashSet;
import java.util.Set;
/**
* 用户会话DTO
* 用户会话Dto
*
* @author Ray.Hao
* @since 3.0.0
*/
@Data
public class UserSessionDTO {
public class UserSessionDto {
/**
* 用户名
@@ -29,7 +29,7 @@ public class UserSessionDTO {
*/
private long lastActiveTime;
public UserSessionDTO(String username) {
public UserSessionDto(String username) {
this.username = username;
this.sessionIds = new HashSet<>();
this.lastActiveTime = System.currentTimeMillis();

View File

@@ -18,8 +18,8 @@ import java.io.Serializable;
@Data
@Builder
@EqualsAndHashCode(callSuper = false)
@Schema(description = "系统配置VO")
public class ConfigVO {
@Schema(description = "系统配置Vo")
public class ConfigVo {
@Schema(description = "主键")
private Long id;

View File

@@ -9,7 +9,7 @@ import java.util.List;
@Schema(description = "部门视图对象")
@Data
public class DeptVO {
public class DeptVo {
@Schema(description = "部门ID")
private Long id;
@@ -30,7 +30,7 @@ public class DeptVO {
private Integer status;
@Schema(description = "子部门")
private List<DeptVO> children;
private List<DeptVo> children;
@Schema(description = "创建时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm")

View File

@@ -13,7 +13,7 @@ import lombok.Setter;
@Schema(description = "字典项键值对象")
@Getter
@Setter
public class DictItemOptionVO {
public class DictItemOptionVo {
@Schema(description = "字典项值")
private String value;

View File

@@ -14,7 +14,7 @@ import lombok.Setter;
@Schema(description = "字典项分页对象")
@Getter
@Setter
public class DictItemPageVO {
public class DictItemPageVo {
@Schema(description = "字典项ID")
private Long id;

View File

@@ -9,7 +9,7 @@ import java.util.List;
/**
* 字典分页VO
* 字典分页Vo
*
* @author Ray
* @since 0.0.1
@@ -17,7 +17,7 @@ import java.util.List;
@Schema(description = "字典分页对象")
@Getter
@Setter
public class DictPageVO {
public class DictPageVo {
@Schema(description = "字典ID")
private Long id;

View File

@@ -9,14 +9,14 @@ import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 系统日志分页VO
* 系统日志分页Vo
*
* @author Ray
* @since 2.10.0
*/
@Data
@Schema(description = "系统日志分页VO")
public class LogPageVO implements Serializable {
@Schema(description = "系统日志分页Vo")
public class LogPageVo implements Serializable {
@Schema(description = "主键")
private Long id;

View File

@@ -8,7 +8,7 @@ import java.util.List;
@Schema(description ="菜单视图对象")
@Data
public class MenuVO {
public class MenuVo {
@Schema(description = "菜单ID")
private Long id;
@@ -48,6 +48,6 @@ public class MenuVO {
@Schema(description = "子菜单")
@JsonInclude(value = JsonInclude.Include.NON_NULL)
private List<MenuVO> children;
private List<MenuVo> children;
}

Some files were not shown because too many files have changed in this diff Show More