refactor(ai): 重构AI命令记录模块

- 将AiCommandRecord重命名为AiCommandLog
- 更新相关控制器、服务、映射器和实体类
This commit is contained in:
Ray.Hao
2025-12-10 11:52:55 +08:00
parent caf4f4e5c0
commit 1f650fb469
12 changed files with 262 additions and 383 deletions

View File

@@ -7,8 +7,8 @@ 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.model.vo.AiCommandLogVO;
import com.youlai.boot.platform.ai.service.AiCommandLogService;
import com.youlai.boot.platform.ai.service.AiCommandService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
@@ -32,7 +32,7 @@ import org.springframework.web.bind.annotation.*;
public class AiCommandController {
private final AiCommandService aiCommandService;
private final AiCommandRecordService recordService;
private final AiCommandLogService logService;
@Operation(summary = "解析自然语言命令")
@PostMapping("/parse")
@@ -72,17 +72,17 @@ public class AiCommandController {
@Operation(summary = "获取AI命令记录分页列表")
@GetMapping("/records")
public PageResult<AiCommandRecordVO> getRecordPage(AiCommandPageQuery queryParams) {
IPage<AiCommandRecordVO> page = recordService.getRecordPage(queryParams);
public PageResult<AiCommandLogVO> getLogPage(AiCommandPageQuery queryParams) {
IPage<AiCommandLogVO> page = logService.getLogPage(queryParams);
return PageResult.success(page);
}
@Operation(summary = "撤销命令执行")
@PostMapping("/rollback/{recordId}")
@PostMapping("/rollback/{logId}")
public Result<?> rollbackCommand(
@Parameter(description = "记录ID") @PathVariable String recordId
@Parameter(description = "记录ID") @PathVariable String logId
) {
recordService.rollbackCommand(recordId);
logService.rollbackCommand(logId);
return Result.success("撤销成功");
}

View File

@@ -3,21 +3,23 @@ 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.entity.AiCommandLog;
import com.youlai.boot.platform.ai.model.query.AiCommandPageQuery;
import com.youlai.boot.platform.ai.model.vo.AiCommandRecordVO;
import com.youlai.boot.platform.ai.model.vo.AiCommandLogVO;
import org.apache.ibatis.annotations.Mapper;
/**
* AI 命令记录 Mapper
*
* @author Ray.Hao
* @since 3.0.0
*/
@Mapper
public interface AiCommandRecordMapper extends BaseMapper<AiCommandRecord> {
public interface AiCommandLogMapper extends BaseMapper<AiCommandLog> {
/**
* 获取 AI 命令记录分页列表
*/
IPage<AiCommandRecordVO> getRecordPage(Page<AiCommandRecordVO> page, AiCommandPageQuery queryParams);
IPage<AiCommandLogVO> getLogPage(Page<AiCommandLogVO> page, AiCommandPageQuery queryParams);
}

View File

@@ -8,15 +8,15 @@ import lombok.EqualsAndHashCode;
import java.math.BigDecimal;
/**
* AI 命令记录实体合并解析和执行记录
* AI 命令记录实体
*
* @author Ray.Hao
* @since 3.0.0
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("ai_command_record")
public class AiCommandRecord extends BaseEntity {
@TableName("ai_command_log")
public class AiCommandLog extends BaseEntity {
/** 用户ID */
private Long userId;
@@ -30,13 +30,13 @@ public class AiCommandRecord extends BaseEntity {
// ==================== 解析相关字段 ====================
/** AI 供应商qwen/openai/deepseek等 */
private String provider;
private String aiProvider;
/** AI 模型qwen-plus/qwen-max/gpt-4-turbo等 */
private String model;
private String aiModel;
/** 解析是否成功 */
private Boolean parseSuccess;
/** 解析状态0-失败, 1-成功 */
private Integer parseStatus;
/** 解析出的函数调用列表JSON */
private String functionCalls;
@@ -56,11 +56,8 @@ public class AiCommandRecord extends BaseEntity {
/** 输出 Token 数量 */
private Integer outputTokens;
/** 总 Token 数量 */
private Integer totalTokens;
/** 解析耗时(毫秒) */
private Long parseTime;
private Integer parseDurationMs;
// ==================== 执行相关字段 ====================
@@ -70,46 +67,15 @@ public class AiCommandRecord extends BaseEntity {
/** 函数参数JSON */
private String functionArguments;
/** 执行状态pending, success, failed */
private String executeStatus;
/** 执行结果JSON */
private String executeResult;
/** 执行状态0-待执行, 1-成功, -1-失败) */
private Integer executeStatus;
/** 执行错误信息 */
private String executeErrorMessage;
/** 影响的记录数 */
private Integer affectedRows;
/** 是否危险操作 */
private Boolean isDangerous;
/** 是否需要确认 */
private Boolean requiresConfirmation;
/** 用户是否确认 */
private Boolean userConfirmed;
/** 幂等性令牌(防止重复执行) */
private String idempotencyKey;
/** 执行耗时(毫秒) */
private Long executionTime;
// ==================== 通用字段 ====================
/** IP 地址 */
private String ipAddress;
/** 用户代理 */
private String userAgent;
/** 当前页面路由 */
private String currentRoute;
/** 备注 */
private String remark;
}

View File

@@ -21,19 +21,25 @@ public class AiCommandPageQuery extends BasePageQuery {
@Schema(description = "关键字(原始命令/函数名称/用户名)")
private String keywords;
@Schema(description = "执行状态(pending-待执行, success-成功, failed-失败)")
private String executeStatus;
@Schema(description = "执行状态(0-待执行, 1-成功, -1-失败)")
private Integer executeStatus;
@Schema(description = "用户ID")
private Long userId;
@Schema(description = "是否危险操作")
private Boolean isDangerous;
@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

@@ -9,11 +9,14 @@ import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* AI命令记录VO合并解析和执行记录
* AI命令记录VO
*
* @author Ray.Hao
* @since 3.0.0
*/
@Data
@Schema(description = "AI命令记录VO")
public class AiCommandRecordVO implements Serializable {
public class AiCommandLogVO implements Serializable {
@Schema(description = "主键ID")
private String id;
@@ -30,13 +33,13 @@ public class AiCommandRecordVO implements Serializable {
// ==================== 解析相关字段 ====================
@Schema(description = "AI供应商")
private String provider;
private String aiProvider;
@Schema(description = "AI模型")
private String model;
private String aiModel;
@Schema(description = "解析是否成功")
private Boolean parseSuccess;
@Schema(description = "解析状态(0-失败, 1-成功)")
private Integer parseStatus;
@Schema(description = "解析出的函数调用列表(JSON)")
private String functionCalls;
@@ -56,11 +59,8 @@ public class AiCommandRecordVO implements Serializable {
@Schema(description = "输出Token数量")
private Integer outputTokens;
@Schema(description = "总Token数量")
private Integer totalTokens;
@Schema(description = "解析耗时(毫秒)")
private Long parseTime;
private Integer parseDurationMs;
// ==================== 执行相关字段 ====================
@@ -70,41 +70,17 @@ public class AiCommandRecordVO implements Serializable {
@Schema(description = "函数参数(JSON)")
private String functionArguments;
@Schema(description = "执行状态")
private String executeStatus;
@Schema(description = "执行结果(JSON)")
private String executeResult;
@Schema(description = "执行状态(0-待执行, 1-成功, -1-失败)")
private Integer executeStatus;
@Schema(description = "执行错误信息")
private String executeErrorMessage;
@Schema(description = "影响的记录数")
private Integer affectedRows;
@Schema(description = "是否危险操作")
private Boolean isDangerous;
@Schema(description = "是否需要确认")
private Boolean requiresConfirmation;
@Schema(description = "用户是否确认")
private Boolean userConfirmed;
@Schema(description = "执行耗时(毫秒)")
private Long executionTime;
// ==================== 通用字段 ====================
@Schema(description = "IP地址")
private String ipAddress;
@Schema(description = "用户代理")
private String userAgent;
@Schema(description = "当前页面路由")
private String currentRoute;
@Schema(description = "创建时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
@@ -112,9 +88,5 @@ public class AiCommandRecordVO implements Serializable {
@Schema(description = "更新时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime;
@Schema(description = "备注")
private String remark;
}

View File

@@ -2,14 +2,17 @@ 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.entity.AiCommandLog;
import com.youlai.boot.platform.ai.model.query.AiCommandPageQuery;
import com.youlai.boot.platform.ai.model.vo.AiCommandRecordVO;
import com.youlai.boot.platform.ai.model.vo.AiCommandLogVO;
/**
* AI 命令记录服务接口
*
* @author Ray.Hao
* @since 3.0.0
*/
public interface AiCommandRecordService extends IService<AiCommandRecord> {
public interface AiCommandLogService extends IService<AiCommandLog> {
/**
* 获取命令记录分页列表
@@ -17,14 +20,13 @@ public interface AiCommandRecordService extends IService<AiCommandRecord> {
* @param queryParams 查询参数
* @return 命令记录分页列表
*/
IPage<AiCommandRecordVO> getRecordPage(AiCommandPageQuery queryParams);
IPage<AiCommandLogVO> getLogPage(AiCommandPageQuery queryParams);
/**
* 撤销命令执行
*
* @param recordId 记录ID
* @param logId 记录ID
*/
void rollbackCommand(String recordId);
void rollbackCommand(String logId);
}

View File

@@ -0,0 +1,49 @@
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.AiCommandLogMapper;
import com.youlai.boot.platform.ai.model.entity.AiCommandLog;
import com.youlai.boot.platform.ai.model.query.AiCommandPageQuery;
import com.youlai.boot.platform.ai.model.vo.AiCommandLogVO;
import com.youlai.boot.platform.ai.service.AiCommandLogService;
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 AiCommandLogServiceImpl extends ServiceImpl<AiCommandLogMapper, AiCommandLog>
implements AiCommandLogService {
@Override
public IPage<AiCommandLogVO> getLogPage(AiCommandPageQuery queryParams) {
Page<AiCommandLogVO> page = new Page<>(queryParams.getPageNum(), queryParams.getPageSize());
return this.baseMapper.getLogPage(page, queryParams);
}
@Override
public void rollbackCommand(String logId) {
AiCommandLog log = this.getById(logId);
if (log == null) {
throw new RuntimeException("命令记录不存在");
}
if (log.getExecuteStatus() == null || log.getExecuteStatus() != 1) {
throw new RuntimeException("只能撤销成功执行的命令");
}
// TODO: 实现具体的回滚逻辑
log.info("撤销命令执行: logId={}, function={}", logId, log.getFunctionName());
throw new UnsupportedOperationException("回滚功能尚未实现");
}
}

View File

@@ -1,47 +0,0 @@
package com.youlai.boot.platform.ai.service.impl;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.youlai.boot.platform.ai.mapper.AiCommandRecordMapper;
import com.youlai.boot.platform.ai.model.entity.AiCommandRecord;
import com.youlai.boot.platform.ai.model.query.AiCommandPageQuery;
import com.youlai.boot.platform.ai.model.vo.AiCommandRecordVO;
import com.youlai.boot.platform.ai.service.AiCommandRecordService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
/**
* AI 命令记录服务实现类
*/
@Service
@Slf4j
@RequiredArgsConstructor
public class AiCommandRecordServiceImpl extends ServiceImpl<AiCommandRecordMapper, AiCommandRecord>
implements AiCommandRecordService {
@Override
public IPage<AiCommandRecordVO> getRecordPage(AiCommandPageQuery queryParams) {
Page<AiCommandRecordVO> page = new Page<>(queryParams.getPageNum(), queryParams.getPageSize());
return this.baseMapper.getRecordPage(page, queryParams);
}
@Override
public void rollbackCommand(String recordId) {
AiCommandRecord record = this.getById(recordId);
if (record == null) {
throw new RuntimeException("命令记录不存在");
}
if (!"success".equals(record.getExecuteStatus())) {
throw new RuntimeException("只能撤销成功执行的命令");
}
// TODO: 实现具体的回滚逻辑
log.info("撤销命令执行: recordId={}, function={}", recordId, record.getFunctionName());
throw new UnsupportedOperationException("回滚功能尚未实现");
}
}

View File

@@ -6,13 +6,12 @@ import cn.hutool.extra.servlet.JakartaServletUtil;
import cn.hutool.json.JSONUtil;
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.youlai.boot.platform.ai.model.dto.AiExecuteRequestDTO;
import com.youlai.boot.platform.ai.model.dto.AiFunctionCallDTO;
import com.youlai.boot.platform.ai.model.dto.AiParseRequestDTO;
import com.youlai.boot.platform.ai.model.dto.AiParseResponseDTO;
import com.youlai.boot.platform.ai.model.entity.AiCommandRecord;
import com.youlai.boot.platform.ai.service.AiCommandRecordService;
import com.youlai.boot.platform.ai.model.entity.AiCommandLog;
import com.youlai.boot.platform.ai.service.AiCommandLogService;
import com.youlai.boot.platform.ai.service.AiCommandService;
import com.youlai.boot.platform.ai.tools.UserTools;
import com.youlai.boot.security.util.SecurityUtils;
@@ -51,7 +50,7 @@ public class AiCommandServiceImpl implements AiCommandService {
当无法识别命令时success=false并给出 error。
""";
private final AiCommandRecordService recordService;
private final AiCommandLogService logService;
private final UserTools userTools;
private final ChatClient chatClient;
@@ -72,14 +71,13 @@ public class AiCommandServiceImpl implements AiCommandService {
String username = SecurityUtils.getUsername();
String ipAddress = JakartaServletUtil.getClientIP(httpRequest);
AiCommandRecord record = new AiCommandRecord();
record.setUserId(userId);
record.setUsername(username);
record.setOriginalCommand(command);
record.setIpAddress(ipAddress);
record.setCurrentRoute(request.getCurrentRoute());
record.setProvider("spring-ai");
record.setModel("auto");
AiCommandLog log = new AiCommandLog();
log.setUserId(userId);
log.setUsername(username);
log.setOriginalCommand(command);
log.setIpAddress(ipAddress);
log.setAiProvider("spring-ai");
log.setAiModel("auto");
String systemPrompt = buildSystemPrompt();
String userPrompt = buildUserPrompt(request);
@@ -97,19 +95,20 @@ public class AiCommandServiceImpl implements AiCommandService {
ParseResult parseResult = parseAiResponse(rawContent);
record.setProvider(StrUtil.emptyToDefault(parseResult.provider(), "spring-ai"));
record.setModel(StrUtil.emptyToDefault(parseResult.model(), "auto"));
record.setParseSuccess(parseResult.success());
record.setExplanation(parseResult.explanation());
record.setFunctionCalls(JSONUtil.toJsonStr(parseResult.functionCalls()));
record.setConfidence(parseResult.confidence() != null ? BigDecimal.valueOf(parseResult.confidence()) : null);
record.setParseErrorMessage(parseResult.success() ? null : StrUtil.emptyToDefault(parseResult.error(), "解析失败"));
record.setParseTime(System.currentTimeMillis() - startTime);
log.setAiProvider(StrUtil.emptyToDefault(parseResult.provider(), "spring-ai"));
log.setAiModel(StrUtil.emptyToDefault(parseResult.model(), "auto"));
log.setParseStatus(parseResult.success() ? 1 : 0);
log.setExplanation(parseResult.explanation());
log.setFunctionCalls(JSONUtil.toJsonStr(parseResult.functionCalls()));
log.setConfidence(parseResult.confidence() != null ? BigDecimal.valueOf(parseResult.confidence()) : null);
log.setParseErrorMessage(parseResult.success() ? null : StrUtil.emptyToDefault(parseResult.error(), "解析失败"));
long duration = System.currentTimeMillis() - startTime;
log.setParseDurationMs((int) duration);
recordService.save(record);
logService.save(log);
AiParseResponseDTO response = AiParseResponseDTO.builder()
.parseLogId(record.getId())
.parseLogId(log.getId())
.success(parseResult.success())
.functionCalls(parseResult.functionCalls())
.explanation(parseResult.explanation())
@@ -121,17 +120,17 @@ public class AiCommandServiceImpl implements AiCommandService {
if (!parseResult.success()) {
log.warn("❗️ AI 未能解析命令: {}", parseResult.error());
} else {
log.info("✅ 解析成功审计记录ID: {}", record.getId());
log.info("✅ 解析成功审计记录ID: {}", log.getId());
}
return response;
} catch (Exception e) {
long duration = System.currentTimeMillis() - startTime;
record.setParseSuccess(false);
record.setFunctionCalls(JSONUtil.toJsonStr(Collections.emptyList()));
record.setParseErrorMessage(e.getMessage());
record.setParseTime(duration);
recordService.save(record);
log.setParseStatus(0);
log.setFunctionCalls(JSONUtil.toJsonStr(Collections.emptyList()));
log.setParseErrorMessage(e.getMessage());
log.setParseDurationMs((int) duration);
logService.save(log);
log.error("❌ 解析命令失败: {}", e.getMessage(), e);
throw new RuntimeException("解析命令失败: " + e.getMessage(), e);
@@ -232,98 +231,59 @@ public class AiCommandServiceImpl implements AiCommandService {
AiFunctionCallDTO functionCall = request.getFunctionCall();
// 判断是否为危险操作
boolean isDangerous = isDangerousOperation(functionCall.getName());
// 根据解析日志ID获取审计记录如果不存在则创建新记录
AiCommandRecord record;
AiCommandLog log;
if (StrUtil.isNotBlank(request.getParseLogId())) {
// 更新已存在的审计记录(解析阶段已创建)
record = recordService.getById(request.getParseLogId());
if (record == null) {
log = logService.getById(request.getParseLogId());
if (log == null) {
throw new IllegalStateException("未找到对应的解析记录ID: " + request.getParseLogId());
}
} else {
// 如果没有解析日志ID创建新记录兼容直接执行的情况
record = new AiCommandRecord();
record.setUserId(userId);
record.setUsername(username);
record.setOriginalCommand(request.getOriginalCommand());
record.setIpAddress(ipAddress);
record.setCurrentRoute(request.getCurrentRoute());
recordService.save(record);
log = new AiCommandLog();
log.setUserId(userId);
log.setUsername(username);
log.setOriginalCommand(request.getOriginalCommand());
log.setIpAddress(ipAddress);
logService.save(log);
}
// 更新执行相关字段
record.setFunctionName(functionCall.getName());
record.setFunctionArguments(JSONUtil.toJsonStr(functionCall.getArguments()));
record.setIsDangerous(isDangerous);
record.setRequiresConfirmation(request.getConfirmMode() != null &&
"manual".equals(request.getConfirmMode()));
record.setUserConfirmed(request.getUserConfirmed());
record.setIdempotencyKey(request.getIdempotencyKey());
record.setUserAgent(httpRequest.getHeader("User-Agent"));
record.setExecuteStatus("pending");
log.setFunctionName(functionCall.getName());
log.setFunctionArguments(JSONUtil.toJsonStr(functionCall.getArguments()));
log.setExecuteStatus(0); // 0-待执行
try {
// 幂等性检查
if (StrUtil.isNotBlank(request.getIdempotencyKey())) {
AiCommandRecord existing = recordService.getOne(
new LambdaQueryWrapper<AiCommandRecord>()
.eq(AiCommandRecord::getIdempotencyKey, request.getIdempotencyKey())
.ne(AiCommandRecord::getId, record.getId()) // 排除当前记录
);
if (existing != null) {
log.warn("⚠️ 检测到重复执行,幂等性令牌: {}", request.getIdempotencyKey());
throw new IllegalStateException("该操作已执行,请勿重复提交");
}
}
// 🎯 执行具体的函数调用
Object result = executeFunctionCall(functionCall);
// 更新执行成功
record.setExecuteStatus("success");
record.setExecuteResult(JSONUtil.toJsonStr(result));
record.setExecutionTime(System.currentTimeMillis() - startTime);
log.setExecuteStatus(1); // 1-成功
log.setExecuteErrorMessage(null);
// 更新审计记录
recordService.updateById(record);
logService.updateById(log);
log.info("✅ 命令执行成功审计记录ID: {}", record.getId());
log.info("✅ 命令执行成功审计记录ID: {}", log.getId());
return result;
} catch (Exception e) {
// 更新执行失败
record.setExecuteStatus("failed");
record.setExecuteErrorMessage(e.getMessage());
record.setExecutionTime(System.currentTimeMillis() - startTime);
log.setExecuteStatus(-1); // -1-失败
log.setExecuteErrorMessage(e.getMessage());
// 更新审计记录
recordService.updateById(record);
logService.updateById(log);
log.error("❌ 命令执行失败审计记录ID: {}", record.getId(), e);
log.error("❌ 命令执行失败审计记录ID: {}", log.getId(), e);
// 抛出异常,由 Controller 统一处理
throw e;
}
}
/**
* 判断是否为危险操作
*/
private boolean isDangerousOperation(String functionName) {
String[] dangerousKeywords = {"delete", "remove", "drop", "truncate", "clear"};
String lowerName = functionName.toLowerCase();
for (String keyword : dangerousKeywords) {
if (lowerName.contains(keyword)) {
return true;
}
}
return false;
}
/**
* 执行具体的函数调用
*/