diff --git a/CLAUDE.md b/CLAUDE.md index 0323227..033ddaf 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -115,6 +115,41 @@ AI 智能客服系统,基于 Spring AI Alibaba + 通义千问 + PGVector,支 - **SSE 流式**: `js/utils.js` 中 `readSSEStream()` 统一处理三种 SSE 接口 - **添加新功能**: 在 `components/` 下新建 JS 组件文件,在 `app.js` 中导入注册即可 +## 开发规范与踩坑记录 + +### 后端:Long ID 序列化为字符串 +**规则**: 所有雪花算法生成的 Long 类型 ID 字段,必须保证前端收到的是**字符串**而非数字,防止 JS 超过 `Number.MAX_SAFE_INTEGER`(2^53)精度丢失。 + +| 场景 | 做法 | +|------|------| +| Entity 字段 | `@JsonSerialize(using = ToStringSerializer.class)` + `@TableId(type = IdType.ASSIGN_ID)` | +| 原生 JDBC SQL | `SELECT u.id::TEXT AS id`(CAST 为 TEXT),不能直接 `SELECT u.id` | +| Controller Map 返回 | `Map.of("id", user.getId().toString())` | + +**反面案例**: 用户管理 `listUsers` 使用原生 SQL 未 CAST,导致新建的用户(雪花 ID 约 19 位)在前端编辑时 ID 被截断,报"用户不存在"。 + +### 后端:敏感字段脱敏 +**规则**: API 返回用户对象前,必须将 `password` 等敏感字段置为 `null`。不得将 BCrypt 哈希暴露给前端。 + +### 前端:弹窗实现统一模式 +项目中存在两种弹窗模式,**不可混用**: + +| 模式 | 实现方式 | 适用 CSS 类 | +|------|----------|------------| +| **A: CSS 类切换** | `:class="{ active: xxx.visible }"` | `.modal-overlay`(CSS 定义 `display:none` + `.active { display:flex }`) | +| **B: 内联样式 + v-if** | `v-if="xxx" style="...display:flex..."` | 无 `.modal-overlay`,用内联样式 | + +**禁止**: 将 `.modal-overlay` 与 `v-if` 搭配使用 — `v-if` 控制 DOM 存在性,但不添加 `.active` 类,导致弹窗渲染后被 CSS `display:none` 隐藏,按钮点击无反应。 + +### 前端:错误处理显示服务器信息 +**规则**: `catch` 块必须透传服务器错误信息,禁止吞掉错误只显示泛化提示: +```javascript +// ✅ 正确 +catch (e) { toast(e.message || '操作失败', 'error') } +// ❌ 错误 — 用户和开发者都无法排查 +catch (e) { toast('操作失败', 'error') } +``` + ## API 路由约定 - AI 对话: `/ai/*`(`AiController`) diff --git a/src/main/java/com/wok/supportbot/service/SysUserService.java b/src/main/java/com/wok/supportbot/service/SysUserService.java index c9b9ca7..8f84b5b 100644 --- a/src/main/java/com/wok/supportbot/service/SysUserService.java +++ b/src/main/java/com/wok/supportbot/service/SysUserService.java @@ -119,9 +119,11 @@ public class SysUserService implements UserDetailsService { Long total = jdbcTemplate.queryForObject(countSql, Long.class, params.toArray()); // 分页数据(联查角色名称) + // 注意:id 必须 CAST 为 TEXT,否则 Jackson 将 BIGINT 序列化为 JSON number, + // 前端 JS 解析大雪花 ID 时会丢失精度(超过 Number.MAX_SAFE_INTEGER) int offset = (page - 1) * size; String dataSql = """ - SELECT u.id, u.username, u.nickname, u.email, u.phone, u.enabled, + SELECT u.id::TEXT AS id, u.username, u.nickname, u.email, u.phone, u.enabled, u.last_login_time, u.create_time, STRING_AGG(r.name, ',') AS role_names, STRING_AGG(r.role_key, ',') AS role_keys @@ -153,9 +155,11 @@ public class SysUserService implements UserDetailsService { @Transactional(rollbackFor = Exception.class) public SysUser createUser(String username, String password, String nickname, String email, String phone, List roleIds) { - // 检查用户名是否已存在 + // 检查用户名是否已存在(显式过滤已删除用户,不依赖 @TableLogic 隐式行为) Long exists = userMapper.selectCount( - new LambdaQueryWrapper().eq(SysUser::getUsername, username) + new LambdaQueryWrapper() + .eq(SysUser::getUsername, username) + .eq(SysUser::getIsDelete, false) ); if (exists > 0) { throw new IllegalArgumentException("用户名已存在: " + username); @@ -242,12 +246,14 @@ public class SysUserService implements UserDetailsService { } /** - * 根据ID获取用户详情(含角色) + * 根据ID获取用户详情(含角色,密码脱敏) */ public SysUser getUserById(Long id) { SysUser user = userMapper.selectById(id); if (user != null) { user.setRoles(getUserRoles(user.getId())); + // 安全脱敏:不返回密码哈希 + user.setPassword(null); } return user; } diff --git a/src/main/resources/static/components/ModelConfigManager.js b/src/main/resources/static/components/ModelConfigManager.js index f113712..cfd4966 100644 --- a/src/main/resources/static/components/ModelConfigManager.js +++ b/src/main/resources/static/components/ModelConfigManager.js @@ -334,11 +334,14 @@ export default {
+ :placeholder="editModal.mode === 'edit' ? '留空则保留原值不变' : '请输入 API Key'" style="flex:1;">
+
+ 💡 出于安全考虑,API Key 已脱敏显示。留空表示保留原有 Key 不变;如需更换请输入新的完整 Key。 +
@@ -751,9 +754,11 @@ export default { function openEditModal(config) { let extraConfig = {} - if (config.extraConfig) { + // 列表 API 返回 snake_case (extra_config),详情/复制 API 返回 camelCase (extraConfig),兼容两者 + const rawExtra = config.extra_config || config.extraConfig + if (rawExtra) { try { - extraConfig = typeof config.extraConfig === 'string' ? JSON.parse(config.extraConfig) : config.extraConfig + extraConfig = typeof rawExtra === 'string' ? JSON.parse(rawExtra) : rawExtra } catch (e) {} } editModal.value = { diff --git a/src/main/resources/static/components/UserManager.js b/src/main/resources/static/components/UserManager.js index a562a71..6e47c99 100644 --- a/src/main/resources/static/components/UserManager.js +++ b/src/main/resources/static/components/UserManager.js @@ -83,7 +83,7 @@ export default { -