Browse Source

docs: 补充开发规范与踩坑记录

- 列表排序字段白名单、vector_store 物理删除、数据库初始化幂等自愈、Java 文本块拼 SQL 等规则
Spring-AI-1.1.2
wanghanlin 2 weeks ago
parent
commit
f06422d5e9
  1. 49
      CLAUDE.md
  2. 1
      frontend/UI-DEV-GUIDE.md

49
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」。
### 前端:弹窗实现统一模式
项目中存在两种弹窗模式,**不可混用**:

1
frontend/UI-DEV-GUIDE.md

@ -231,6 +231,7 @@ const columns = [
| --- | --- |
| 表格列定义 | 统一写在 `<script>``columns` 数组 + `:columns`;**禁用** `<t-table-column>` 标签写法 |
| 分页 | 统一用 `<t-table>` 内置 `:pagination` + `@page-change` + `showJumper`;**禁用**独立 `<t-pagination>` 组件 |
| 列排序 | 后端分页表格:列 `sorter: true` + 表格 `:sort="sortInfo"` + `@sort-change` 透传服务端 `sortField/sortOrder`;前端全量表格:仅列 `sorter: true`,数值列用 `sorter: (a,b)=>(a.x??0)-(b.x??0)` 函数 |
| 表格 loading | 必须绑定 `:loading`(现状有 4 个页面缺失) |
| pageSize | 默认统一 20(现状 10/20/30 三档不一) |
| 操作列 | `<t-space :size="4">` + `<t-button size="small" variant="text">`;危险操作 `theme="danger"`、主操作 `theme="primary"` |

Loading…
Cancel
Save