From 176cc67a5c4ca507b875ea03d493d4f7402fd53f Mon Sep 17 00:00:00 2001 From: ageerle Date: Tue, 28 Jul 2026 11:22:12 +0800 Subject: [PATCH 1/3] =?UTF-8?q?feat(register):=20=E6=96=B0=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E6=B3=A8=E5=86=8C=E6=88=90=E5=8A=9F=E5=90=8E=E7=BB=91?= =?UTF-8?q?=E5=AE=9A=E9=BB=98=E8=AE=A4=E8=A7=92=E8=89=B2=E5=B9=B6=E8=A1=A5?= =?UTF-8?q?=E9=BD=90=E7=BC=BA=E5=A4=B1=E8=8F=9C=E5=8D=95=E6=9D=83=E9=99=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - SysRegisterService: 注册成功后读取 sys.register.defaultRoleId 配置, 为新用户绑定默认角色;未配置则静默跳过,不影响注册流程 - SysUserServiceImpl.registerUser: insert 后回写 userId 主键, 供调用方绑定默认角色或登录使用 - SQL: 修正 知识管理 父菜单 perms(knowledge:info:list -> system:info:list), 新增 system:session:* / system:attach:* / system:fragment:* 菜单, 新建 普通用户 角色(租户 000000,data_scope=5 仅本人)及角色-菜单关联, 写入 sys.register.defaultRoleId 配置项 解决注册用户无默认角色导致无权限访问 AI 对话界面、以及菜单权限标识 与控制器不一致无法授权的问题。 Co-Authored-By: Claude --- ...egister-default-role-and-missing-menus.sql | 119 ++++++++++++++++++ .../system/service/SysRegisterService.java | 25 ++++ .../service/impl/SysUserServiceImpl.java | 5 +- 3 files changed, 148 insertions(+), 1 deletion(-) create mode 100644 docs/script/sql/update/2026-07-24-register-default-role-and-missing-menus.sql diff --git a/docs/script/sql/update/2026-07-24-register-default-role-and-missing-menus.sql b/docs/script/sql/update/2026-07-24-register-default-role-and-missing-menus.sql new file mode 100644 index 00000000..fcaa1fbc --- /dev/null +++ b/docs/script/sql/update/2026-07-24-register-default-role-and-missing-menus.sql @@ -0,0 +1,119 @@ +-- 注册链路修复:补齐缺失的菜单权限标识 + 新建注册默认角色 +-- 关联问题: +-- #3 注册用户无默认角色 -> 无权限访问 AI 对话界面 +-- #4 sys_menu 缺 system:session:* / system:attach:* / system:fragment:*, +-- 且 知识管理 父菜单 perms 为 knowledge:info:list,与控制器 +-- KnowledgeInfoController 用的 system:info:list 对不上,:list 无法授权。 +-- 本脚本幂等,可重复执行(INSERT IGNORE + UPDATE 天然幂等)。 +-- 新增 menu_id / role_id / config_id 统一使用 2099010100000000xxx 段, +-- 与现有 snowflake id(2xxxxxxxxxxxxxxxxx)不冲突。 + +-- ============================================================ +-- 1. 修正 知识管理 父菜单权限标识:knowledge:info:list -> system:info:list +-- ============================================================ +UPDATE `sys_menu` +SET `perms` = 'system:info:list' +WHERE `menu_id` = 2006681261898813441 AND `perms` = 'knowledge:info:list'; + +-- ============================================================ +-- 2. 新增 会话管理 菜单组(system:session:*) +-- 父菜单=对话管理(2000209300188356609)。 +-- 管理端暂无 chat/session 页面,故 C 菜单设为隐藏(visible=1), +-- 权限标识仍可在角色管理中授权给用户端使用。 +-- ============================================================ +INSERT IGNORE INTO `sys_menu` VALUES +(2099010100000000001, '会话管理', 2000209300188356609, 6, 'session', NULL, NULL, 1, 0, 'C', '1', '0', 'system:session:list', '#', 103, 1, '2026-07-24 00:00:00', NULL, NULL, '会话管理菜单'), +(2099010100000000002, '会话管理查询', 2099010100000000001, 1, '#', '', NULL, 1, 0, 'F', '0', '0', 'system:session:query', '#', 103, 1, '2026-07-24 00:00:00', NULL, NULL, ''), +(2099010100000000003, '会话管理新增', 2099010100000000001, 2, '#', '', NULL, 1, 0, 'F', '0', '0', 'system:session:add', '#', 103, 1, '2026-07-24 00:00:00', NULL, NULL, ''), +(2099010100000000004, '会话管理修改', 2099010100000000001, 3, '#', '', NULL, 1, 0, 'F', '0', '0', 'system:session:edit', '#', 103, 1, '2026-07-24 00:00:00', NULL, NULL, ''), +(2099010100000000005, '会话管理删除', 2099010100000000001, 4, '#', '', NULL, 1, 0, 'F', '0', '0', 'system:session:remove', '#', 103, 1, '2026-07-24 00:00:00', NULL, NULL, ''), +(2099010100000000006, '会话管理导出', 2099010100000000001, 5, '#', '', NULL, 1, 0, 'F', '0', '0', 'system:session:export', '#', 103, 1, '2026-07-24 00:00:00', NULL, NULL, ''); + +-- ============================================================ +-- 3. 新增 知识附件 菜单组(system:attach:*) +-- 父菜单=对话管理(2000209300188356609),组件对应管理端 knowledge/attach/index。 +-- ============================================================ +INSERT IGNORE INTO `sys_menu` VALUES +(2099010100000000010, '知识附件', 2000209300188356609, 7, 'attach', 'knowledge/attach/index', NULL, 1, 0, 'C', '0', '0', 'system:attach:list', 'ant-design:paper-clip-outlined', 103, 1, '2026-07-24 00:00:00', NULL, NULL, '知识附件菜单'), +(2099010100000000011, '知识附件查询', 2099010100000000010, 1, '#', '', NULL, 1, 0, 'F', '0', '0', 'system:attach:query', '#', 103, 1, '2026-07-24 00:00:00', NULL, NULL, ''), +(2099010100000000012, '知识附件新增', 2099010100000000010, 2, '#', '', NULL, 1, 0, 'F', '0', '0', 'system:attach:add', '#', 103, 1, '2026-07-24 00:00:00', NULL, NULL, ''), +(2099010100000000013, '知识附件修改', 2099010100000000010, 3, '#', '', NULL, 1, 0, 'F', '0', '0', 'system:attach:edit', '#', 103, 1, '2026-07-24 00:00:00', NULL, NULL, ''), +(2099010100000000014, '知识附件删除', 2099010100000000010, 4, '#', '', NULL, 1, 0, 'F', '0', '0', 'system:attach:remove', '#', 103, 1, '2026-07-24 00:00:00', NULL, NULL, ''), +(2099010100000000015, '知识附件导出', 2099010100000000010, 5, '#', '', NULL, 1, 0, 'F', '0', '0', 'system:attach:export', '#', 103, 1, '2026-07-24 00:00:00', NULL, NULL, ''); + +-- ============================================================ +-- 4. 新增 知识片段 菜单组(system:fragment:*) +-- 父菜单=对话管理(2000209300188356609),组件对应管理端 knowledge/fragment/index。 +-- ============================================================ +INSERT IGNORE INTO `sys_menu` VALUES +(2099010100000000020, '知识片段', 2000209300188356609, 8, 'fragment', 'knowledge/fragment/index', NULL, 1, 0, 'C', '0', '0', 'system:fragment:list', 'ant-design:file-text-outlined', 103, 1, '2026-07-24 00:00:00', NULL, NULL, '知识片段菜单'), +(2099010100000000021, '知识片段查询', 2099010100000000020, 1, '#', '', NULL, 1, 0, 'F', '0', '0', 'system:fragment:query', '#', 103, 1, '2026-07-24 00:00:00', NULL, NULL, ''), +(2099010100000000022, '知识片段新增', 2099010100000000020, 2, '#', '', NULL, 1, 0, 'F', '0', '0', 'system:fragment:add', '#', 103, 1, '2026-07-24 00:00:00', NULL, NULL, ''), +(2099010100000000023, '知识片段修改', 2099010100000000020, 3, '#', '', NULL, 1, 0, 'F', '0', '0', 'system:fragment:edit', '#', 103, 1, '2026-07-24 00:00:00', NULL, NULL, ''), +(2099010100000000024, '知识片段删除', 2099010100000000020, 4, '#', '', NULL, 1, 0, 'F', '0', '0', 'system:fragment:remove', '#', 103, 1, '2026-07-24 00:00:00', NULL, NULL, ''), +(2099010100000000025, '知识片段导出', 2099010100000000020, 5, '#', '', NULL, 1, 0, 'F', '0', '0', 'system:fragment:export', '#', 103, 1, '2026-07-24 00:00:00', NULL, NULL, ''); + +-- ============================================================ +-- 5. 新建 普通用户 角色(租户 000000,data_scope=5 仅本人) +-- ============================================================ +INSERT IGNORE INTO `sys_role` VALUES +(2099010100000000030, '000000', '普通用户', 'user', 2, '5', 1, 1, '0', '0', 103, 1, '2026-07-24 00:00:00', NULL, NULL, '注册用户默认角色'); + +-- ============================================================ +-- 6. 角色-菜单关联:普通用户 拥有用户端 AI 对话所需权限 +-- 会话管理(001-006) + 聊天消息(2000210914680823809 及 810-814) +-- + 知识管理(2006681261898813441 及 442-446) +-- + 知识附件(010-015) + 知识片段(020-025) +-- ============================================================ +INSERT IGNORE INTO `sys_role_menu` (`role_id`, `menu_id`) VALUES +-- 会话管理 +(2099010100000000030, 2099010100000000001), +(2099010100000000030, 2099010100000000002), +(2099010100000000030, 2099010100000000003), +(2099010100000000030, 2099010100000000004), +(2099010100000000030, 2099010100000000005), +(2099010100000000030, 2099010100000000006), +-- 聊天消息 +(2099010100000000030, 2000210914680823809), +(2099010100000000030, 2000210914680823810), +(2099010100000000030, 2000210914680823811), +(2099010100000000030, 2000210914680823812), +(2099010100000000030, 2000210914680823813), +(2099010100000000030, 2000210914680823814), +-- 知识管理 +(2099010100000000030, 2006681261898813441), +(2099010100000000030, 2006681261898813442), +(2099010100000000030, 2006681261898813443), +(2099010100000000030, 2006681261898813444), +(2099010100000000030, 2006681261898813445), +(2099010100000000030, 2006681261898813446), +-- 知识附件 +(2099010100000000030, 2099010100000000010), +(2099010100000000030, 2099010100000000011), +(2099010100000000030, 2099010100000000012), +(2099010100000000030, 2099010100000000013), +(2099010100000000030, 2099010100000000014), +(2099010100000000030, 2099010100000000015), +-- 知识片段 +(2099010100000000030, 2099010100000000020), +(2099010100000000030, 2099010100000000021), +(2099010100000000030, 2099010100000000022), +(2099010100000000030, 2099010100000000023), +(2099010100000000030, 2099010100000000024), +(2099010100000000030, 2099010100000000025); + +-- ============================================================ +-- 7. 注册默认角色配置项 sys.register.defaultRoleId +-- 后端 SysRegisterService 注册成功后读取此配置为新用户绑定角色; +-- 值为空字符串时不绑定(保留旧行为)。 +-- ============================================================ +INSERT IGNORE INTO `sys_config` VALUES +(2099010100000000031, '000000', '注册-默认角色ID', 'sys.register.defaultRoleId', '2099010100000000030', 'Y', 103, 1, '2026-07-24 00:00:00', NULL, NULL, '新注册用户绑定的默认角色ID,留空则不绑定'); + +-- ============================================================ +-- 8. 前置条件(运营项,默认不执行):开启用户注册 +-- sys.account.registerUser 默认 false,后端 AuthController.register +-- 会据此返回「当前系统没有开启注册功能」。若要开放注册,取消注释执行。 +-- ============================================================ +-- UPDATE `sys_config` SET `config_value` = 'true' +-- WHERE `config_key` = 'sys.account.registerUser' AND `tenant_id` = '000000'; diff --git a/ruoyi-modules/ruoyi-system/src/main/java/org/ruoyi/system/service/SysRegisterService.java b/ruoyi-modules/ruoyi-system/src/main/java/org/ruoyi/system/service/SysRegisterService.java index c2912fb7..0a65ce14 100644 --- a/ruoyi-modules/ruoyi-system/src/main/java/org/ruoyi/system/service/SysRegisterService.java +++ b/ruoyi-modules/ruoyi-system/src/main/java/org/ruoyi/system/service/SysRegisterService.java @@ -1,5 +1,6 @@ package org.ruoyi.system.service; +import cn.hutool.core.convert.Convert; import cn.hutool.crypto.digest.BCrypt; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import lombok.RequiredArgsConstructor; @@ -35,6 +36,12 @@ public class SysRegisterService { private final ISysUserService userService; private final SysUserMapper userMapper; private final CaptchaProperties captchaProperties; + private final ISysConfigService configService; + + /** + * 注册默认角色配置 key,值为角色 ID;未配置或为空则不绑定角色。 + */ + private static final String DEFAULT_ROLE_CONFIG_KEY = "sys.register.defaultRoleId"; /** * 注册 @@ -68,9 +75,27 @@ public class SysRegisterService { if (!regFlag) { throw new UserException("user.register.error"); } + // 绑定默认角色(未配置则跳过,不影响注册流程) + bindDefaultRole(tenantId, sysUser.getUserId()); recordLogininfor(tenantId, username, Constants.REGISTER, MessageUtils.message("user.register.success")); } + /** + * 读取配置 sys.register.defaultRoleId 并为新用户绑定默认角色。 + * 配置为空或角色 ID 无效时静默跳过。 + */ + private void bindDefaultRole(String tenantId, Long userId) { + if (userId == null) { + return; + } + Long defaultRoleId = TenantHelper.dynamic(tenantId, () -> + Convert.toLong(configService.selectConfigByKey(DEFAULT_ROLE_CONFIG_KEY), null)); + if (defaultRoleId == null) { + return; + } + userService.insertUserAuth(userId, new Long[]{defaultRoleId}); + } + /** * 校验验证码 * diff --git a/ruoyi-modules/ruoyi-system/src/main/java/org/ruoyi/system/service/impl/SysUserServiceImpl.java b/ruoyi-modules/ruoyi-system/src/main/java/org/ruoyi/system/service/impl/SysUserServiceImpl.java index df494d61..a71a30a9 100644 --- a/ruoyi-modules/ruoyi-system/src/main/java/org/ruoyi/system/service/impl/SysUserServiceImpl.java +++ b/ruoyi-modules/ruoyi-system/src/main/java/org/ruoyi/system/service/impl/SysUserServiceImpl.java @@ -333,7 +333,10 @@ public class SysUserServiceImpl implements ISysUserService, UserService { user.setUpdateBy(0L); SysUser sysUser = MapstructUtils.convert(user, SysUser.class); sysUser.setTenantId(tenantId); - return baseMapper.insert(sysUser) > 0; + boolean rows = baseMapper.insert(sysUser) > 0; + // 回写主键,调用方(注册服务、mpLogin)需要用 userId 绑定默认角色或登录 + user.setUserId(sysUser.getUserId()); + return rows; } /** From b80dc6e23fa9acc5ffa9b3189a9cf1556f77a0ce Mon Sep 17 00:00:00 2001 From: ageerle Date: Tue, 28 Jul 2026 11:22:19 +0800 Subject: [PATCH 2/3] =?UTF-8?q?fix(shortdrama):=20=E6=B5=81=E5=BC=8F?= =?UTF-8?q?=E7=94=9F=E6=88=90=E5=A2=9E=E5=8A=A0=E9=A6=96=20token=2090=20?= =?UTF-8?q?=E7=A7=92=E8=B6=85=E6=97=B6=E5=85=9C=E5=BA=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 部分聚合站对 stream=true 既不返回内容也不报错,外层 30 分钟总超时 才会失败,前端会长时间卡死。90 秒内未收到任何 token 时快速失败, 提示更换模型或使用同步生成。收到首 token 后取消兜底计时。 Co-Authored-By: Claude --- .../service/shortdrama/impl/ShortDramaServiceImpl.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/shortdrama/impl/ShortDramaServiceImpl.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/shortdrama/impl/ShortDramaServiceImpl.java index 219dfb08..49e0cdad 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/shortdrama/impl/ShortDramaServiceImpl.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/shortdrama/impl/ShortDramaServiceImpl.java @@ -1864,10 +1864,20 @@ public class ShortDramaServiceImpl implements IShortDramaService { }); heartbeat.scheduleAtFixedRate(() -> emit(emitter, streamPhase, "running", "模型仍在处理中,请稍候..."), 15, 15, TimeUnit.SECONDS); + // 首 token 超时兜底:部分聚合站对 stream=true 既不返回内容也不报错, + // 外层 30 分钟总超时才会失败,前端会长时间卡死。90 秒内未收到任何 token 时快速失败。 + final AtomicBoolean firstTokenReceived = new AtomicBoolean(false); + heartbeat.schedule(() -> { + if (firstTokenReceived.compareAndSet(false, true)) { + done.completeExceptionally(new RuntimeException( + "模型在90秒内未返回任何流式内容,可能该模型或服务不支持流式输出,请更换模型或使用同步生成")); + } + }, 90, TimeUnit.SECONDS); List messages = List.of(UserMessage.from(prompt)); streamingModel.chat(messages, new StreamingChatResponseHandler() { @Override public void onPartialResponse(String text) { + firstTokenReceived.set(true); buf.append(text); emitStream(emitter, streamPhase, text); if (onPartial != null) { From 77846601ee4c208fd13c52cc13a5401e0a81ff75 Mon Sep 17 00:00:00 2001 From: ageerle Date: Tue, 28 Jul 2026 11:22:26 +0800 Subject: [PATCH 3/3] =?UTF-8?q?test(retrieval):=20=E9=80=82=E9=85=8D=20Kno?= =?UTF-8?q?wledgeRetrievalServiceImpl=20=E6=96=B0=E5=A2=9E=20trace=20?= =?UTF-8?q?=E4=BE=9D=E8=B5=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 构造测试实例时补上 TraceRecordService 与 TraceProperties 两个新参数, 修复编译/构造失败。 Co-Authored-By: Claude --- .../impl/KnowledgeRetrievalServiceRegressionTest.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/ruoyi-modules/ruoyi-chat/src/test/java/org/ruoyi/service/retrieval/impl/KnowledgeRetrievalServiceRegressionTest.java b/ruoyi-modules/ruoyi-chat/src/test/java/org/ruoyi/service/retrieval/impl/KnowledgeRetrievalServiceRegressionTest.java index 0841af78..4c21ded9 100644 --- a/ruoyi-modules/ruoyi-chat/src/test/java/org/ruoyi/service/retrieval/impl/KnowledgeRetrievalServiceRegressionTest.java +++ b/ruoyi-modules/ruoyi-chat/src/test/java/org/ruoyi/service/retrieval/impl/KnowledgeRetrievalServiceRegressionTest.java @@ -2,6 +2,8 @@ package org.ruoyi.service.retrieval.impl; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; +import org.ruoyi.common.trace.config.TraceProperties; +import org.ruoyi.common.trace.service.TraceRecordService; import org.ruoyi.domain.bo.vector.QueryVectorBo; import org.ruoyi.domain.vo.knowledge.KnowledgeRetrievalVo; import org.ruoyi.factory.RerankModelFactory; @@ -21,7 +23,8 @@ class KnowledgeRetrievalServiceRegressionTest { @Test void rrfMergesSameStableIdAndKeepsDistinctItems() throws Exception { KnowledgeRetrievalServiceImpl service = new KnowledgeRetrievalServiceImpl( - mock(VectorStoreService.class), mock(RerankModelFactory.class), mock(KnowledgeFragmentMapper.class)); + mock(VectorStoreService.class), mock(RerankModelFactory.class), mock(KnowledgeFragmentMapper.class), + mock(TraceRecordService.class), new TraceProperties()); KnowledgeRetrievalVo vectorA = item("fid-a", "A", 0.9); KnowledgeRetrievalVo vectorB = item("fid-b", "B", 0.8); KnowledgeRetrievalVo keywordA = item("fid-a", "A", 10.0); @@ -44,7 +47,8 @@ class KnowledgeRetrievalServiceRegressionTest { VectorStoreService vectorStore = mock(VectorStoreService.class); when(vectorStore.search(org.mockito.ArgumentMatchers.any())).thenReturn(List.of(item("fid-a", "A", 0.75))); KnowledgeRetrievalServiceImpl service = new KnowledgeRetrievalServiceImpl( - vectorStore, mock(RerankModelFactory.class), mock(KnowledgeFragmentMapper.class)); + vectorStore, mock(RerankModelFactory.class), mock(KnowledgeFragmentMapper.class), + mock(TraceRecordService.class), new TraceProperties()); QueryVectorBo query = new QueryVectorBo(); query.setKid("1"); query.setQuery("test");