Merge pull request #318 from ageerle/feat/register-default-role-and-misc

feat: 注册默认角色绑定 + 短剧流式超时兜底 + trace 测试适配
This commit is contained in:
ageerle
2026-07-28 11:25:37 +08:00
committed by GitHub
5 changed files with 164 additions and 3 deletions

View File

@@ -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 id2xxxxxxxxxxxxxxxxx不冲突。
-- ============================================================
-- 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. 新建 普通用户 角色(租户 000000data_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';

View File

@@ -1864,10 +1864,20 @@ public class ShortDramaServiceImpl implements IShortDramaService {
}); });
heartbeat.scheduleAtFixedRate(() -> emit(emitter, streamPhase, "running", "模型仍在处理中,请稍候..."), heartbeat.scheduleAtFixedRate(() -> emit(emitter, streamPhase, "running", "模型仍在处理中,请稍候..."),
15, 15, TimeUnit.SECONDS); 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<ChatMessage> messages = List.of(UserMessage.from(prompt)); List<ChatMessage> messages = List.of(UserMessage.from(prompt));
streamingModel.chat(messages, new StreamingChatResponseHandler() { streamingModel.chat(messages, new StreamingChatResponseHandler() {
@Override @Override
public void onPartialResponse(String text) { public void onPartialResponse(String text) {
firstTokenReceived.set(true);
buf.append(text); buf.append(text);
emitStream(emitter, streamPhase, text); emitStream(emitter, streamPhase, text);
if (onPartial != null) { if (onPartial != null) {

View File

@@ -2,6 +2,8 @@ package org.ruoyi.service.retrieval.impl;
import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test; 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.bo.vector.QueryVectorBo;
import org.ruoyi.domain.vo.knowledge.KnowledgeRetrievalVo; import org.ruoyi.domain.vo.knowledge.KnowledgeRetrievalVo;
import org.ruoyi.factory.RerankModelFactory; import org.ruoyi.factory.RerankModelFactory;
@@ -21,7 +23,8 @@ class KnowledgeRetrievalServiceRegressionTest {
@Test @Test
void rrfMergesSameStableIdAndKeepsDistinctItems() throws Exception { void rrfMergesSameStableIdAndKeepsDistinctItems() throws Exception {
KnowledgeRetrievalServiceImpl service = new KnowledgeRetrievalServiceImpl( 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 vectorA = item("fid-a", "A", 0.9);
KnowledgeRetrievalVo vectorB = item("fid-b", "B", 0.8); KnowledgeRetrievalVo vectorB = item("fid-b", "B", 0.8);
KnowledgeRetrievalVo keywordA = item("fid-a", "A", 10.0); KnowledgeRetrievalVo keywordA = item("fid-a", "A", 10.0);
@@ -44,7 +47,8 @@ class KnowledgeRetrievalServiceRegressionTest {
VectorStoreService vectorStore = mock(VectorStoreService.class); VectorStoreService vectorStore = mock(VectorStoreService.class);
when(vectorStore.search(org.mockito.ArgumentMatchers.any())).thenReturn(List.of(item("fid-a", "A", 0.75))); when(vectorStore.search(org.mockito.ArgumentMatchers.any())).thenReturn(List.of(item("fid-a", "A", 0.75)));
KnowledgeRetrievalServiceImpl service = new KnowledgeRetrievalServiceImpl( 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(); QueryVectorBo query = new QueryVectorBo();
query.setKid("1"); query.setKid("1");
query.setQuery("test"); query.setQuery("test");

View File

@@ -1,5 +1,6 @@
package org.ruoyi.system.service; package org.ruoyi.system.service;
import cn.hutool.core.convert.Convert;
import cn.hutool.crypto.digest.BCrypt; import cn.hutool.crypto.digest.BCrypt;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
@@ -35,6 +36,12 @@ public class SysRegisterService {
private final ISysUserService userService; private final ISysUserService userService;
private final SysUserMapper userMapper; private final SysUserMapper userMapper;
private final CaptchaProperties captchaProperties; 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) { if (!regFlag) {
throw new UserException("user.register.error"); throw new UserException("user.register.error");
} }
// 绑定默认角色(未配置则跳过,不影响注册流程)
bindDefaultRole(tenantId, sysUser.getUserId());
recordLogininfor(tenantId, username, Constants.REGISTER, MessageUtils.message("user.register.success")); 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});
}
/** /**
* 校验验证码 * 校验验证码
* *

View File

@@ -333,7 +333,10 @@ public class SysUserServiceImpl implements ISysUserService, UserService {
user.setUpdateBy(0L); user.setUpdateBy(0L);
SysUser sysUser = MapstructUtils.convert(user, SysUser.class); SysUser sysUser = MapstructUtils.convert(user, SysUser.class);
sysUser.setTenantId(tenantId); sysUser.setTenantId(tenantId);
return baseMapper.insert(sysUser) > 0; boolean rows = baseMapper.insert(sysUser) > 0;
// 回写主键调用方注册服务、mpLogin需要用 userId 绑定默认角色或登录
user.setUserId(sysUser.getUserId());
return rows;
} }
/** /**