From feb56c8a8d795ae40db77fbe3e5c14b4ff97a743 Mon Sep 17 00:00:00 2001 From: rosercode Date: Thu, 24 Sep 2026 12:30:00 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=99=BA=E8=83=BD=E6=9F=A5=E6=95=B0?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E6=8F=90=E9=97=AE=E5=8E=86=E5=8F=B2=E6=8C=81?= =?UTF-8?q?=E4=B9=85=E5=8C=96=E4=B8=8E=E4=BD=93=E9=AA=8C=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 为智能查数补充提问历史的后端持久化(含创建时间、可清空),并完善前端历史展示、会话缓存与若干体验问题。 变更内容: - 新增 t_ai_sql_history 表(MySQL/SQLite 双脚本)与 AiSqlHistory 实体、Mapper、Service、VO,记录提问内容与创建时间 - AiSqlQueryServiceImpl 在查询成功后写入提问历史,新增 GET/DELETE /api/ai/sql/history 接口,支持按创建时间倒序查询与清空 - 前端历史改为后端持久化:卡片式列表、展示创建时间、搜索过滤、一键复制提问、点击回填重查与清空 - 新增会话缓存(sessionStorage):切走再回或刷新后恢复上次输入、结果与视图模式,结果超过 500 行时不缓存结果,登出时清除 体验修复: - 修复柱状图与饼图切换时旧 series/坐标轴残留(VChart 采用 notMerge) - 修复进入 /smart-query 时短暂误显示「AI 未启用」(新增 aiEnabledLoaded 加载态) - 引入 JetBrains Mono 字体并修复生成的 SQL 字体未生效(内部 code 元素继承父级字体) 依赖: - 新增前端依赖 @fontsource/jetbrains-mono 说明: - 历史表由 SchemaInitializer 启动时幂等创建,无需手工执行 SQL --- frontend/package.json | 1 + frontend/src/api/ai.ts | 17 ++ frontend/src/composables/useAiEnabled.ts | 10 +- frontend/src/locales/messages.ts | 3 + frontend/src/store/user.ts | 5 +- frontend/src/utils/storage.ts | 28 +++ frontend/src/utils/storageKeys.ts | 4 +- frontend/src/views/SqlQuery.vue | 238 +++++++++++++++--- .../ledgerly/controller/AiController.java | 31 +++ .../ledgerly/entity/AiSqlHistory.java | 31 +++ .../ledgerly/mapper/AiSqlHistoryMapper.java | 15 ++ .../ledgerly/service/AiSqlHistoryService.java | 38 +++ .../service/impl/AiSqlHistoryServiceImpl.java | 98 ++++++++ .../service/impl/AiSqlQueryServiceImpl.java | 5 + .../ledgerly/util/sql/TextToSqlGuard.java | 1 - .../rosercode/ledgerly/vo/AiSqlHistoryVO.java | 23 ++ src/main/resources/sql/init.sql | 9 + src/main/resources/sql/schema-sqlite.sql | 9 + 18 files changed, 527 insertions(+), 39 deletions(-) create mode 100644 src/main/java/cn/rosercode/ledgerly/entity/AiSqlHistory.java create mode 100644 src/main/java/cn/rosercode/ledgerly/mapper/AiSqlHistoryMapper.java create mode 100644 src/main/java/cn/rosercode/ledgerly/service/AiSqlHistoryService.java create mode 100644 src/main/java/cn/rosercode/ledgerly/service/impl/AiSqlHistoryServiceImpl.java create mode 100644 src/main/java/cn/rosercode/ledgerly/vo/AiSqlHistoryVO.java diff --git a/frontend/package.json b/frontend/package.json index a906e28..d45477c 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -9,6 +9,7 @@ "preview": "vite preview" }, "dependencies": { + "@fontsource/jetbrains-mono": "^5.3.0", "@vicons/ionicons5": "^0.13.0", "axios": "^1.20.0", "echarts": "^6.1.0", diff --git a/frontend/src/api/ai.ts b/frontend/src/api/ai.ts index 92989f6..2ba0fd2 100644 --- a/frontend/src/api/ai.ts +++ b/frontend/src/api/ai.ts @@ -264,6 +264,23 @@ export function queryBySql(query: string) { return request.post('/ai/sql', { query }, { timeout: 120000 }) } +/** 智能查数历史记录 */ +export interface AiSqlHistoryItem { + id: number + query: string + createdAt: string +} + +/** 获取智能查数历史(按创建时间倒序) */ +export function getSqlHistory(limit = 100) { + return request.get('/ai/sql/history', { params: { limit } }) +} + +/** 清空智能查数历史 */ +export function clearSqlHistory() { + return request.delete('/ai/sql/history') +} + /** 自然语言记账解析结果 */ export interface AiRecordResult { summary: string diff --git a/frontend/src/composables/useAiEnabled.ts b/frontend/src/composables/useAiEnabled.ts index e548ed5..b783ea9 100644 --- a/frontend/src/composables/useAiEnabled.ts +++ b/frontend/src/composables/useAiEnabled.ts @@ -5,8 +5,8 @@ import { getAiConfig } from '../api/ai' /** 当前用户 AI 是否启用;读取失败一律按未启用处理 */ export const aiEnabled = ref(false) -/** 是否已成功加载过一次,避免重复请求 */ -let loaded = false +/** AI 启用状态是否已确定(首次读取完成前为 false,用于避免误显示「未启用」) */ +export const aiEnabledLoaded = ref(false) /** * 从后端刷新 AI 启用状态。 @@ -19,17 +19,17 @@ export async function refreshAiEnabled() { } catch (e) { aiEnabled.value = false } - loaded = true + aiEnabledLoaded.value = true } /** 确保至少加载过一次 AI 启用状态(已加载则不重复请求) */ export async function ensureAiEnabledLoaded() { - if (loaded) return + if (aiEnabledLoaded.value) return await refreshAiEnabled() } /** 重置为未启用(退出登录等场景使用) */ export function resetAiEnabled() { aiEnabled.value = false - loaded = false + aiEnabledLoaded.value = false } diff --git a/frontend/src/locales/messages.ts b/frontend/src/locales/messages.ts index 814cd45..afe4028 100644 --- a/frontend/src/locales/messages.ts +++ b/frontend/src/locales/messages.ts @@ -883,6 +883,9 @@ export default { examplesLabel: { zh: '示例', en: 'Examples', ja: '例' }, history: { zh: '历史提问', en: 'History', ja: '履歴' }, clearHistory: { zh: '清空历史', en: 'Clear history', ja: '履歴をクリア' }, + copyQuestion: { zh: '复制提问', en: 'Copy question', ja: '質問をコピー' }, + historySearch: { zh: '搜索历史提问', en: 'Search history', ja: '履歴を検索' }, + historyNoMatch: { zh: '无匹配结果', en: 'No matching history', ja: '一致する履歴がありません' }, needConfig: { zh: 'AI 未启用,请先在「设置」中开启 AI。', en: 'AI is not enabled. Please enable it in Settings first.', ja: 'AI が有効になっていません。「設定」で有効にしてください。' }, }, record: { diff --git a/frontend/src/store/user.ts b/frontend/src/store/user.ts index 27e56ab..1ae36dd 100644 --- a/frontend/src/store/user.ts +++ b/frontend/src/store/user.ts @@ -2,7 +2,8 @@ import { defineStore } from 'pinia' import { ref } from 'vue' import { getUserInfo } from '../api/user' -import { readString, removeKey, writeString } from '../utils/storage' +import { readString, removeKey, removeSessionKey, writeString } from '../utils/storage' +import { STORAGE_KEYS } from '../utils/storageKeys' /** 登录令牌存储键(认证凭据,不属于界面偏好,不纳入 ledgerly. 前缀) */ const TOKEN_KEY = 'token' @@ -41,6 +42,8 @@ export const useUserStore = defineStore('user', () => { nickname.value = '' userId.value = null removeKey(TOKEN_KEY) + // 清除智能查数上一次查询会话,避免同标签切换账号后看到上一用户的数据 + removeSessionKey(STORAGE_KEYS.sqlQuerySession) } return { token, nickname, userId, setToken, setUserInfo, fetchUserInfo, logout } diff --git a/frontend/src/utils/storage.ts b/frontend/src/utils/storage.ts index 91735da..ce0853c 100644 --- a/frontend/src/utils/storage.ts +++ b/frontend/src/utils/storage.ts @@ -64,6 +64,34 @@ export function writeSessionString(key: string, value: string): void { } } +/** 读取 sessionStorage JSON,解析失败或读取异常时回退 fallback */ +export function readSessionJson(key: string, fallback: T): T { + try { + const raw = sessionStorage.getItem(key) + return raw ? (JSON.parse(raw) as T) : fallback + } catch (e) { + return fallback + } +} + +/** 写入 sessionStorage JSON,忽略存储异常 */ +export function writeSessionJson(key: string, value: unknown): void { + try { + sessionStorage.setItem(key, JSON.stringify(value)) + } catch (e) { + // 忽略写入失败 + } +} + +/** 删除 sessionStorage 键,忽略存储异常 */ +export function removeSessionKey(key: string): void { + try { + sessionStorage.removeItem(key) + } catch (e) { + // 忽略删除失败 + } +} + /** * 迁移历史存储键:新键不存在且旧键存在时,将旧值搬运到新键并删除旧键。 * diff --git a/frontend/src/utils/storageKeys.ts b/frontend/src/utils/storageKeys.ts index b39fd3a..236e176 100644 --- a/frontend/src/utils/storageKeys.ts +++ b/frontend/src/utils/storageKeys.ts @@ -33,8 +33,8 @@ export const STORAGE_KEYS = { transactionAiQueryHistory: 'ledgerly.transaction.aiQueryHistory', // 收支记录 AI 查询浮窗位置尺寸 transactionAiQueryWindow: 'ledgerly.transaction.aiQueryWindow', - // 智能查数的历史提问 - sqlQueryHistory: 'ledgerly.sqlQuery.history', + // 智能查数上一次查询会话(sessionStorage,含输入与结果缓存) + sqlQuerySession: 'ledgerly.sqlQuery.session', // 内容区滚动位置(sessionStorage,按路由拼接后缀) scrollPrefix: 'ledgerly.scroll.', } as const diff --git a/frontend/src/views/SqlQuery.vue b/frontend/src/views/SqlQuery.vue index f3f9f6e..367c4d2 100644 --- a/frontend/src/views/SqlQuery.vue +++ b/frontend/src/views/SqlQuery.vue @@ -1,10 +1,10 @@