From f06422d5e9e0acb722ab5d830b707db4408f1629 Mon Sep 17 00:00:00 2001 From: wanghanlin <1533525126@qq.com> Date: Wed, 2 Sep 2026 15:21:05 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E8=A1=A5=E5=85=85=E5=BC=80=E5=8F=91?= =?UTF-8?q?=E8=A7=84=E8=8C=83=E4=B8=8E=E8=B8=A9=E5=9D=91=E8=AE=B0=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 列表排序字段白名单、vector_store 物理删除、数据库初始化幂等自愈、Java 文本块拼 SQL 等规则 --- CLAUDE.md | 49 ++++++++++++++++++++++++++++++++++++++++ frontend/UI-DEV-GUIDE.md | 1 + 2 files changed, 50 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index 5e961eb..ed869ee 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -148,6 +148,20 @@ AI 智能客服系统,基于 Spring AI Alibaba + 通义千问 + PGVector,支 **反面案例**: `knowledge_document.content_hash` 列仅在 `DatabaseInitConfig` 中迁移,未同步到 `knowledge-base.sql`,导致该脚本变为过时版本。 +### 后端:vector_store 物理删除(禁止在原生 SQL 中引用 is_delete) + +**规则**: `vector_store` 表由 Spring AI `PgVectorStore` 以 `initializeSchema(true)` 自动建表,默认只有 `id / content / metadata(json) / embedding` 四列;`DatabaseInitConfig` 仅额外补了 `content_tsvector`、`create_time` 两列。因此 **vector_store 只保证有六列**:`id / content / metadata / embedding / content_tsvector / create_time`,**没有 `is_delete`、没有 `update_time`**。 + +- vector_store 的向量删除走**物理删除**(`pgVectorVectorStore.delete(ids)`),不存在逻辑删除语义,原生 SQL 中**禁止**写 `AND is_delete = false`。 +- 凡是引用业务表(`knowledge_document` 等 MyBatis Plus 逻辑删除表)的 SQL 里常用的 `is_delete = false` 过滤,**不得照搬到 vector_store**。 +- 对 vector_store 写原生 SQL 前,先确认引用的列在上述六列白名单内。 + +**反面案例**: `HybridSearchService.keywordSearch()` 从 `knowledge_document` 表查询照搬了 `AND is_delete = false`,但 vector_store 无此列,PostgreSQL 报 `column "is_delete" does not exist`(SQLState 42703),被 Spring 统一翻译为 `BadSqlGrammarException`,前端显示「搜索失败: bad SQL grammar」,且仅关键词/混合模式触发(向量模式走 Spring AI similaritySearch 不执行该 SQL)。 + +### 后端:数据库初始化一律幂等自愈 + +**规则**: 新增列/索引/触发器的 init 方法(`DatabaseInitConfig` 内)**不要**「列已存在就 return」,应使用幂等 SQL 组合自愈:`ADD COLUMN IF NOT EXISTS` + 回填(`UPDATE ... WHERE xxx IS NULL`)+ `CREATE INDEX IF NOT EXISTS` + `CREATE OR REPLACE FUNCTION` + `DROP TRIGGER IF EXISTS` + `CREATE TRIGGER`。避免半途失败或对象被手动删除后留下静默缺口(如新文档 `content_tsvector` 恒为 NULL、检索静默返回空)。 + ### 后端:Long ID 序列化为字符串 **规则**: 所有雪花算法生成的 Long 类型 ID 字段,必须保证前端收到的是**字符串**而非数字,防止 JS 超过 `Number.MAX_SAFE_INTEGER`(2^53)精度丢失。 @@ -162,6 +176,41 @@ AI 智能客服系统,基于 Spring AI Alibaba + 通义千问 + PGVector,支 ### 后端:敏感字段脱敏 **规则**: API 返回用户对象前,必须将 `password` 等敏感字段置为 `null`。不得将 BCrypt 哈希暴露给前端。 +### 后端:列表排序字段白名单(防 SQL 注入) +**规则**: 任何用户可控的列表排序参数,必须经 `com.wok.supportbot.common.SortUtils` 的白名单映射解析,**严禁**将前端传入的 `sortField` 直接拼接进 `ORDER BY`。 + +| 场景 | 做法 | +|------|------| +| MyBatis Plus `QueryWrapper` | `String col = SortUtils.resolveColumn(sortField, 白名单Map, "create_time");` 再 `orderByAsc/orderByDesc(col)` | +| `LambdaQueryWrapper` | 其 `orderByAsc/orderByDesc` 只接受 `SFunction`、不接受字符串列名,需改用 `QueryWrapper` 做排序 | +| JdbcTemplate 原生 SQL | 同样用 `resolveColumn` 得到安全列名后拼 `ORDER BY col ASC/DESC` | +| `DISTINCT ON` 查询 | 排序需将原查询包成子查询,在外层按白名单列排序(见 `MessageFeedbackService.listConversationsByFeedback`) | + +白名单用 `Map.of(colKey, 列名)` 定义;`resolveColumn` 内部先判空(避免 `Map.of().get(null)` 抛 NPE),未知字段回退默认列(通常 `create_time`);`sortOrder` 仅 `asc` 视为升序,其余一律降序。 + +**反面案例**: 早期 `DocumentService.listDocuments` 手工写白名单,因未判 `sortField == null` 直接 `Map.of().getOrDefault(sortField, ...)` 触发 NPE,已由 `SortUtils.resolveColumn` 统一规避。 + +### 后端:Java 文本块拼 SQL 的边界空格陷阱 +**规则**: 用 Java 文本块(`"""`)拼 SQL 时,**严禁**让闭合定界符 `"""` 与内容行同尾、或让变量紧跟开启定界符 `"""` 拼接,否则空格/换行会被静默吞掉导致 SQL 粘连。Java 文本块有两个隐蔽行为: + +1. **闭合定界符前的尾随空格被剥离**:`ORDER BY """` 中 `BY` 后的空格会丢,`"ORDER BY "` 变成 `"ORDER BY"`。 +2. **开启定界符后的前导换行被消费**:`+ """` 后紧跟的下一文本块,其第一行前导换行不算内容,导致前一段末尾与后一段开头直接相连(如 `DESC` + `LIMIT` 变成 `DESCLIMIT`)。 + +**做法**: 涉及排序子句、动态变量拼接时,用普通字符串显式带空格拼接,不要依赖文本块边界: +```java +// ✅ 正确:显式空格 + 变量,不碰文本块边界 +""" ... ) t +""" + " ORDER BY " + orderBy + " LIMIT ? OFFSET ?"; +// ❌ 错误:ORDER BY 尾随空格被剥离、orderBy 与 LIMIT 粘连 +""" ... ) t +ORDER BY """ + orderBy + """ +LIMIT ? OFFSET ? +"""; +``` +`""" + whereClause` 这类「闭合定界符独立一行 + whereClause 自带前导空格」的写法是安全的,因为不依赖尾随空格。 + +**反面案例**: `MessageFeedbackService.listConversationsByFeedback` 用 `ORDER BY """ + orderBy + """` 拼接,编译后 `ORDER BY ` 尾随空格被剥离、`DESC` 与 `LIMIT` 粘连成 `ORDER BYfeedback_time DESCLIMIT`,PostgreSQL 报 `syntax error`(Spring 翻译为 `BadSqlGrammarException`),前端显示「查询失败:bad SQL grammar」。 + ### 前端:弹窗实现统一模式 项目中存在两种弹窗模式,**不可混用**: diff --git a/frontend/UI-DEV-GUIDE.md b/frontend/UI-DEV-GUIDE.md index c983066..ac4cdab 100644 --- a/frontend/UI-DEV-GUIDE.md +++ b/frontend/UI-DEV-GUIDE.md @@ -231,6 +231,7 @@ const columns = [ | --- | --- | | 表格列定义 | 统一写在 `