docs: 修复第三轮 spec review 问题

- Service 返回 PageResult,内部做 Page 转换
- 修正展开行预览截断逻辑(lastIndexOf)
- PageResult 类型移至 types/common.ts
- 明确旧 GET 接口保留策略
- 弹窗增加响应式、ESC/遮罩关闭、错误信息格式
- 补充 keyword SQL 注入安全和大小写说明

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-24 11:37:13 +08:00
parent 90fca4922e
commit 03dfdd13fa
@@ -87,8 +87,9 @@ purpose: AI 调用日志详情查看功能设计文档
**弹窗特性:** **弹窗特性:**
- 入参/出参使用代码高亮 + 等宽字体,支持纵向滚动 - 入参/出参使用代码高亮 + 等宽字体,支持纵向滚动
- 每个代码块右上角有「复制」按钮,点击复制原始 JSON 到剪贴板 - 每个代码块右上角有「复制」按钮,点击复制原始 JSON 到剪贴板
- 错误信息区域仅在 `status === 'failed'` 时显示 - 错误信息区域仅在 `status === 'failed'` 时显示,格式为 `{errorCode}: {errorMessage}`(如 `AI_STREAM_INTERRUPTED: 连接超时`
- 弹窗宽度 800px - 弹窗宽度 800px,响应式限制 `max-width: 90vw`,防止窄屏溢出
- 支持 ESC 关闭和点击遮罩关闭(Element Plus Dialog 默认行为)
## 前端组件设计 ## 前端组件设计
@@ -153,16 +154,17 @@ interface LogQueryParams {
function previewText(jsonStr: string, maxLen = 200): string { function previewText(jsonStr: string, maxLen = 200): string {
if (!jsonStr) return '-' if (!jsonStr) return '-'
if (jsonStr.length <= maxLen) return jsonStr if (jsonStr.length <= maxLen) return jsonStr
// 优先在空白字符处截断,避免截断在转义序列中间
const truncated = jsonStr.slice(0, maxLen) const truncated = jsonStr.slice(0, maxLen)
const lastSpace = truncated.search(/\s+(?!.*\s)/) // 优先在空白字符处截断;无空白字符时直接截断到 maxLen
return lastSpace > maxLen * 0.5 ? truncated.slice(0, lastSpace) + '...' : truncated + '...' const lastSpace = truncated.lastIndexOf(' ')
const cutAt = lastSpace > maxLen * 0.5 ? lastSpace : maxLen
return truncated.slice(0, cutAt) + '...'
} }
``` ```
### 类型定义 ### 类型定义
`web-admin/src/types/aiconfig.ts` 中新增: `web-admin/src/types/common.ts` 中新增通用分页类型(如文件不存在则创建)
```typescript ```typescript
export interface LogQueryParams { export interface LogQueryParams {
status?: string status?: string
@@ -198,7 +200,7 @@ export function queryAiCallLogs(params: LogQueryParams) {
} }
``` ```
**兼容性说明:** 经检查,`listAiCallLogs` 仅在 `AiRoutingList.vue` 的调用日志 Tab 中使用,无其他调用方。新接口使用 POST,旧接口使用 GET,路径相同方法不同,技术上可共存。实现时直接替换前端调用即可,旧 GET 接口保留或后续清理 **兼容性说明:** 经检查,`listAiCallLogs` 仅在 `AiRoutingList.vue` 的调用日志 Tab 中使用,无其他调用方。新接口使用 POST,旧接口使用 GET,路径相同方法不同,技术上可共存。实现时直接替换前端调用为 POST 新接口,旧 GET 接口保留不删除(避免影响其他潜在调用方)
## 后端 API 设计 ## 后端 API 设计
@@ -264,8 +266,8 @@ PageResult<AiCallLog> query(AiCallLogQueryRequest request);
```java ```java
@Override @Override
public Page<AiCallLog> query(AiCallLogQueryRequest request) { public PageResult<AiCallLog> query(AiCallLogQueryRequest request) {
Page<AiCallLog> page = new Page<>(request.getPageNum(), request.getPageSize()); Page<AiCallLog> pageParam = new Page<>(request.getPageNum(), request.getPageSize());
LambdaQueryWrapper<AiCallLog> wrapper = new LambdaQueryWrapper<>(); LambdaQueryWrapper<AiCallLog> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(AiCallLog::getIsDeleted, 0) wrapper.eq(AiCallLog::getIsDeleted, 0)
@@ -278,13 +280,22 @@ public Page<AiCallLog> query(AiCallLogQueryRequest request) {
.orderByDesc(AiCallLog::getCreateTime); .orderByDesc(AiCallLog::getCreateTime);
// keyword 为空字符串或纯空格时不执行 LIKE 搜索(isNotBlank 已处理) // keyword 为空字符串或纯空格时不执行 LIKE 搜索(isNotBlank 已处理)
// MyBatis-Plus like 默认使用预编译语句,无 SQL 注入风险
// LIKE 是否区分大小写取决于数据库字符集(项目使用 utf8mb4,默认不区分大小写)
if (StringUtils.isNotBlank(request.getKeyword())) { if (StringUtils.isNotBlank(request.getKeyword())) {
wrapper.and(w -> w.like(AiCallLog::getInputText, request.getKeyword()) wrapper.and(w -> w.like(AiCallLog::getInputText, request.getKeyword())
.or() .or()
.like(AiCallLog::getOutputText, request.getKeyword())); .like(AiCallLog::getOutputText, request.getKeyword()));
} }
return page(page, wrapper); Page<AiCallLog> page = page(pageParam, wrapper);
// 转换为 PageResult,字段名与前端对齐
PageResult<AiCallLog> result = new PageResult<>();
result.setRecords(page.getRecords());
result.setTotal(page.getTotal());
result.setPageNum((int) page.getCurrent());
result.setPageSize((int) page.getSize());
return result;
} }
``` ```