Compare commits

...

77 Commits

Author SHA1 Message Date
chenhao 6107e611f4 feat: 添加屏保管理页面和相关功能
- 在前端添加 `ScreenSaverManagement` 页面,支持屏保的创建、编辑、删除和状态切换
- 在 `AiModelController` 中添加 Swagger 注解以描述 API 操作
- 在 `pom.xml` 中添加 `springdoc-openapi-starter-webmvc-ui` 依赖
- 更新 `role-permission` 和 `tenants` 页面的分页逻辑
- 在 `sys-params` 页面中使用 `ListTable` 组件并优化分页显示
2026-04-20 11:30:26 +08:00
alanpaine b1fd9de87d 更新分享页面基础信息显示与隐藏 2026-04-17 11:30:22 +08:00
alanpaine 9480d0e4bf feat(会议详情): 改进音频播放和转写交互体验
- 将会议预览页面的基本信息卡片合并到会议概览卡片,优化布局
- 为转写列表添加当前播放高亮和自动滚动功能
- 实现浮动音频播放器,随滚动保持可见
- 调整转写列表的响应式网格布局
2026-04-17 11:02:03 +08:00
chenhao ddd97e0514 补充提交 2026-04-17 10:09:18 +08:00
chenhao 27ae0a3def feat: 添加用户提示支持和优化会议访问逻辑
- 在 `MeetingCommandService` 和 `MeetingDomainSupport` 中添加 `userPrompt` 参数
- 在 `MeetingAccessService` 和 `MeetingQueryService` 中添加忽略租户的会议查询方法
- 更新前端API和组件,支持用户提示功能
- 优化会议访问逻辑,包括预览密码验证和角色管理页面
- 添加相关单元测试以验证新功能的正确性
2026-04-17 10:08:40 +08:00
chenhao d4424a157b feat: 增加会议上传超时配置并优化分页选项
- 在 `meeting.ts` 中添加 `MEETING_UPLOAD_FLOW_TIMEOUT` 配置,并应用于 `createMeeting` 和 `uploadMeeting` 请求
- 在 `AppPagination` 组件中增加 `8` 作为分页选项之一
- 优化 `Meetings.tsx` 和 `MeetingDetail.tsx` 的导入和状态初始化逻辑
2026-04-16 14:18:48 +08:00
alanpaine 0960e625f9 feat(ui): 优化页面标题组件和业务管理页面的视觉设计
重构 PageHeader 组件,移除冗余的 Space 导入,使用内联样式替代 Tailwind 类以提供更灵活的布局控制。同时,为外部应用管理和客户端管理页面引入全新的卡片设计,包括圆角、渐变背景、毛玻璃效果和自定义间距,以提升视觉层次感和现代感。调整按钮样式,统一圆角和高度,增强整体 UI 的一致性。
2026-04-16 14:17:12 +08:00
alanpaine 48dd7d1150 style(MeetingPreview): 调整分析标签页布局为弹性等宽
将标签页容器设为 display: flex 并移除滚动条,为每个标签页添加 flex: 1 以实现等宽分布,同时优化内边距和文本对齐
2026-04-16 13:42:43 +08:00
alanpaine 29a4be912d style(ui): 替换分析标签组件为自定义样式实现
移除对 Ant Design Segmented 组件的依赖,改用自定义 div 实现标签切换。这提供了更精细的样式控制,优化了视觉外观和交互反馈,并消除了不必要的第三方组件依赖。
2026-04-16 13:39:25 +08:00
alanpaine 4194b9afe6 style(frontend): 统一会议中心页面样式与布局
调整会议中心页面的卡片与布局结构,使用一致的样式类名并优化间距。
移除 List.Item 的底部边距,使卡片间距完全由网格控制。
将分页组件移至内容卡片内部,提升整体布局一致性。
2026-04-16 13:32:00 +08:00
alanpaine 276f9aa188 style(layout): 优化页脚样式,移除冗余装饰元素
- 调整页脚内边距,移除背景透明设置,使用卡片背景色并添加顶部边框
- 简化页脚内容容器样式,移除圆角、边框和毛玻璃效果
- 调整文字颜色为次要文本色,减少间距,优化视觉层次
- 更新分隔符颜色以匹配边框颜色变量
2026-04-16 13:15:39 +08:00
alanpaine 5a5f71d465 feat(会议预览): 增强分享弹窗样式并添加音频播放器
- 为会议分享弹窗添加渐变背景和卡片样式
- 在会议预览页面添加自定义音频播放器,支持播放/暂停、进度控制和倍速切换
- 优化转录文本的显示布局,将发言人头像和时间信息整合到内容区域
- 更新免责声明文本,明确AI生成内容的性质
- 调整会议详情页的关键词选择交互,使用勾选图标替代复选框
- 统一多个页面的配色方案,使用主色调变量
2026-04-16 13:05:50 +08:00
alanpaine 7c64cdf7a2 更新加载动画 2026-04-16 11:26:21 +08:00
chenhao 712d31d911 feat: 添加会议访问密码功能和相关UI组件
- 在 `MeetingCommandServiceImpl` 中添加 `normalizeAccessPassword` 方法,处理访问密码
- 在 `MeetingVO` 和 `UpdateMeetingBasicCommand` 中添加 `accessPassword` 字段
- 在前端 `MeetingPreview.tsx` 和 `MeetingDetail.tsx` 中添加访问密码输入和预览链接生成逻辑
- 在 `MeetingPublicPreviewController` 中移除预览响应中的访问密码
- 在 `LegacyAuthController` 中添加异常处理,返回错误信息
- 更新相关前端API和组件,支持访问密码功能
2026-04-16 10:55:10 +08:00
alanpaine caf2e22df0 chore: 更新代理配置并替换分页组件
- 更新 Vite 代理配置中的后端服务器地址和端口
- 将 ExternalAppManagement 和 ClientManagement 页面中的自定义分页逻辑替换为统一的 AppPagination 组件
- 移除表格内嵌的滚动样式,使分页组件独立于表格布局
2026-04-16 09:52:29 +08:00
chenhao 017e1d2ded feat: 添加旧版Android API支持和会议预览功能
- 添加 `LegacyLoginResponse`, `LegacyRefreshTokenResponse`, `LegacyLoginUserResponse` 和 `LegacyMeetingTagResponse` DTO
- 添加 `LegacyAuthController` 以处理旧版登录和刷新令牌请求
- 更新 `MeetingAccessServiceImpl` 中的异常信息为英文
- 在前端 `MeetingPreview.tsx` 中添加多语言文本和分享功能
2026-04-16 09:41:22 +08:00
chenhao db310fc803 feat: 添加会议预览功能和相关UI组件
- 在 `MeetingAccessService` 中添加 `isPreviewPasswordRequired` 和 `assertCanPreviewMeeting` 方法
- 在 `PromptTemplateVO` 和 `PromptTemplateDTO` 中添加 `description` 字段
- 在前端添加 `meetingAnalysis.ts` 以处理会议分析数据
- 在 `PromptTemplates.tsx` 中显示模板描述
- 添加 `MeetingPreview.tsx` 组件,支持会议预览和密码验证
2026-04-15 17:55:57 +08:00
alanpaine 2b30744d2e feat(ui): 统一全局分页组件并优化布局样式
- 新增 AppPagination 组件,统一分页样式与行为
- 将多个页面的自定义分页替换为标准分页组件
- 修复布局容器高度计算问题,确保内容区域正确滚动
- 调整表格容器样式,支持响应式布局和水平滚动
- 优化国际化配置,支持 Ant Design 组件多语言
- 统一分页边距和边框样式,提升视觉一致性
2026-04-15 17:52:19 +08:00
chenhao 6d46998abe feat: 增强旧版会议控制器功能和相关测试
- 添加 `StringRedisTemplate` 和 `ObjectMapper` 依赖
- 更新 `buildListItem` 方法以构建会议列表项
- 增加实时进度解析和处理逻辑
- 优化会议预览数据响应构建
- 添加并更新相关单元测试以验证新功能的正确性
2026-04-15 15:21:00 +08:00
alanpaine 658b7e6b59 新增拖拉排序 2026-04-15 13:49:50 +08:00
alanpaine 5972182519 新增拖拉排序 2026-04-15 13:49:30 +08:00
chenhao b2430abe73 refactor: 统一使用全局租户ID并优化客户端和外部应用管理页面
- 在 `ClientDownloadServiceImpl` 和 `ExternalAppServiceImpl` 中使用 `GLOBAL_TENANT_ID`
- 移除 `requireOwned` 方法中的租户ID检查,更新为 `requireExisting`
- 更新前端 `ClientManagement` 和 `ExternalAppManagement` 页面,优化平台选项和状态过滤
- 添加数据字典驱动的平台分组和选项加载逻辑
- 修复前端组件中的字段名称和代理配置问题
2026-04-14 11:17:25 +08:00
chenhao 3b7ba2c47a feat: 添加旧版Android API支持和相关测试
- 添加 `LegacyMeetingAttendeeResponse`, `LegacyMeetingPreviewDataResponse`, `LegacyExternalAppItemResponse`, `LegacyClientDownloadResponse` 等DTO
- 添加 `LegacyCatalogAdapterService` 接口及其实现
- 添加 `MeetingAuthorizationServiceImplTest`, `RealtimeMeetingSessionStateServiceImplTest`, `RealtimeMeetingGrpcServiceTest`, `LegacyMeetingAdapterServiceImplTest` 单元测试
- 添加 `ClientDownload` 和 `ExternalApp` 实体类
- 添加 `ApkManifestParser` 工具类
2026-04-13 20:21:08 +08:00
chenhao dffd33206a feat: 添加旧版Android API支持和日志配置
- 添加 `ApiResponseSuccessCodeAdvice` 以处理旧版成功代码
- 添加 `LegacyMeetingCreateRequest`, `LegacyApiResponse` 和相关控制器
- 添加 `GrpcExceptionLoggingInterceptor` 以增强gRPC异常日志记录
- 更新 `application.yml`,移除 `/api/android/**` 的安全配置
- 更新前端API和组件,修复字段名称和代理配置
- 添加日志配置文件 `logback-spring.xml` 以支持日志滚动和格式化
2026-04-13 10:32:56 +08:00
alanpaine 5f895bfe26 feat: 增加音频上传超时和进度显示
为音频文件上传接口增加超时设置,防止大文件上传时请求超时。在 speaker 注册接口设置 2 分钟超时,在会议音频上传接口设置 5 分钟超时。同时为上传音频功能添加真实的进度回调,替换模拟进度,提升用户体验。

在首页点击创建会议按钮时,改为直接打开创建抽屉,而不是跳转到会议页面再打开,简化用户操作流程。
2026-04-10 17:38:23 +08:00
chenhao 53ff2292a8 feat: 添加重新识别会议转录功能
- 在 `MeetingCommandServiceImpl` 中添加 `retryTranscription` 方法,用于重新提交转录任务
- 更新 `MeetingCommandService` 接口,添加 `retryTranscription` 方法
- 在 `MeetingController` 中添加 `retryTranscription` API 端点
- 更新前端API和组件,支持重新识别会议转录
- 添加相关单元测试以验证新功能的正确性
2026-04-10 15:18:05 +08:00
chenhao b9593324a5 feat: 增强会议配置解析和日志级别设置
- 在 `MeetingCommandServiceImpl` 中引入 `MeetingRuntimeProfileResolver`,解析并应用运行时配置
- 更新 `application-dev.yml` 和 `application.yml`,添加日志级别配置和新的数据库表
2026-04-10 09:14:00 +08:00
chenhao 1c82365e97 feat: 增强实时会议gRPC服务和会话状态管理
- 在 `MeetingCommandServiceImpl` 中更新 `saveRealtimeTranscriptSnapshot` 方法,仅保存最终结果
- 在 `GrpcServerLifecycle` 中添加 `GrpcExceptionLoggingInterceptor`
- 在 `RealtimeMeetingSessionStateServiceImpl` 中添加终端状态处理逻辑
- 在 `RealtimeMeetingGrpcService` 中增强错误处理和流关闭逻辑
- 添加 `saveRealtimeTranscriptSnapshotShouldIgnoreNonFinalTranscript` 测试用例
- 在 `MeetingAuthorizationServiceImpl` 中添加匿名访问支持
- 在 `RealtimeMeetingGrpcSessionServiceImpl` 中添加异常处理和清理逻辑
2026-04-08 19:45:50 +08:00
chenhao b2e2f2c46a feat: 增强实时会议gRPC服务和会话状态管理
- 在 `MeetingCommandServiceImpl` 中更新 `saveRealtimeTranscriptSnapshot` 方法,仅保存最终结果
- 在 `GrpcServerLifecycle` 中添加 `GrpcExceptionLoggingInterceptor`
- 在 `RealtimeMeetingSessionStateServiceImpl` 中添加终端状态处理逻辑
- 在 `RealtimeMeetingGrpcService` 中增强错误处理和流关闭逻辑
- 添加 `saveRealtimeTranscriptSnapshotShouldIgnoreNonFinalTranscript` 测试用例
- 在 `MeetingAuthorizationServiceImpl` 中添加匿名访问支持
- 在 `RealtimeMeetingGrpcSessionServiceImpl` 中添加异常处理和清理逻辑
2026-04-08 19:15:03 +08:00
alanpaine e83a0ece32 feat(首页): 优化最近会议布局并调整创建会议表单
- 将最近会议网格布局从3列改为4列,并添加响应式设计
- 调整首页标题为"最近会议",优化查看全部按钮样式和布局
- 移除热词选择功能,简化创建会议表单
- 默认启用声纹区分功能,优化高级设置面板视觉样式
- 修复创建会议时表单初始化逻辑
2026-04-08 17:30:31 +08:00
alanpaine c802f63ada fix(frontend): 修复多个抽屉和模态框组件渲染问题
为多个抽屉和模态框组件添加 forceRender 属性,确保表单字段在隐藏后重新显示时能正确渲染
修复会议详情页转录时间线显示问题,移除多余的时间戳元素
优化实时ASR会话页面的UI布局和交互设计
2026-04-08 16:16:21 +08:00
alanpaine 21c38355c3 feat(frontend): 添加Antd App组件和配置更新
- 将全局message替换为App.useApp以支持Antd 5.x静态方法
- 更新vite代理配置指向新后端地址
- 添加baseUrl到tsconfig.json支持路径别名
- 统一Card组件使用variant="borderless"替代bordered={false}
- 移除AppLayout中的菜单loading属性
- 优化热词表格显示,添加文本省略
- 更新Drawer组件的destroyOnClose为destroyOnHidden
- 添加前端.gitignore文件
- 更新分页组件size配置为default
- 修复会议详情页面总结更新参数传递问题
- 移除实时识别创建页面路由,整合到会议列表
- 添加生产环境配置文件到target目录
- 更新字体文件到资源目录
更新首页样式
2026-04-08 15:13:53 +08:00
chenhao 135203b9f6 feat: 添加实时会议创建和验证测试
- 添加 `MeetingCreateCommandValidationTest` 以验证创建会议命令的必填字段
- 添加 `MeetingRuntimeProfileResolverImplTest` 以测试运行时配置解析
- 添加 `MeetingCommandServiceImplTest` 以测试会议命令服务的逻辑
- 添加 `AndroidAuthServiceImplTest` 以测试 Android 认证服务
- 更新 `MeetingCommandService` 接口,添加 `saveRealtimeTranscriptSnapshot` 和更新 `completeRealtimeMeeting` 方法
- 在 `AndroidMeetingRealtimeController` 中添加创建实时会议的 API 端点
- 定义 `AndroidCreateRealtimeMeetingCommand` 和 `AndroidCreateRealtimeMeetingVO` 数据传输对象
2026-04-08 09:15:26 +08:00
chenhao 24c3835b79 feat: 添加实时会议配置选项和会话状态批量查询功能
- 在 `CreateRealtimeMeetingCommand` 中添加 `mode`, `language`, `enablePunctuation`, `enableItn`, `enableTextRefine`, 和 `saveAudio` 字段
- 更新 `MeetingCommandServiceImpl` 以支持新的实时会议配置选项
- 添加 `getRealtimeSessionStatuses` 接口,支持批量查询实时会议会话状态
- 更新前端API和组件,支持新的配置选项和批量查询功能
2026-04-03 14:38:36 +08:00
chenhao ff47c34349 refactor: 优化首页布局和样式
- 更新 `RightVisual.less` 和 `index.less`,调整响应式布局和样式
- 改进背景视觉元素的自适应性和动画效果
- 调整颜色和字体以提升UI/UX体验
2026-04-03 10:58:20 +08:00
chenhao 3cd1c48bce refactor: 优化首页布局和样式
- 简化首页布局,移除不必要的视觉元素
- 更新 `RightVisual.less` 和 `index.less`,调整样式和动画
- 在 `index.html` 中添加 Google Fonts 链接
- 更新 `index.tsx`,简化组件结构并优化内容展示
2026-04-03 10:40:34 +08:00
chenhao d780278da4 refactor: 移除Android API和WebSocket安全配置,更新应用配置
- 删除 `AndroidApiSecurityConfig` 和 `WebSocketSecurityConfig` 类
- 更新 `application.yml`,添加 `/api/android/**` 和 `/ws/**` 到 `permit-all-urls`
2026-04-02 17:05:41 +08:00
chenhao 4f42fb50ad feat: 添加分页查询和前端搜索功能
- 在 `SpeakerController` 中添加分页查询接口
- 在 `SpeakerServiceImpl` 中实现分页查询逻辑
- 更新前端API和组件,支持分页查询和按名称搜索声纹记录
2026-04-02 15:52:54 +08:00
chenhao 5da9a97d55 feat: 添加声纹库管理功能和相关API
- 在 `Speaker` 实体中添加 `creatorId` 和 `externalSpeakerId` 字段
- 更新数据库表 `biz_speakers`,添加 `creator_id` 和 `external_speaker_id` 字段,并创建相应索引
- 在 `SpeakerService` 中添加 `listByCreator` 和 `deleteSpeaker` 方法
- 更新前端API和组件,支持声纹注册、删除和列表查询
- 优化声纹注册逻辑,支持第三方声纹服务的调用和状态更新
2026-04-01 17:20:42 +08:00
chenhao 578359a0d3 feat: 添加声纹库管理功能和相关API
- 在 `Speaker` 实体中添加 `creatorId` 和 `externalSpeakerId` 字段
- 更新数据库表 `biz_speakers`,添加 `creator_id` 和 `external_speaker_id` 字段,并创建相应索引
- 在 `SpeakerService` 中添加 `listByCreator` 和 `deleteSpeaker` 方法
- 更新前端API和组件,支持声纹注册、删除和列表查询
- 优化声纹注册逻辑,支持第三方声纹服务的调用和状态更新
2026-04-01 14:31:12 +08:00
chenhao f0d63c97a3 feat: 添加Android实时会议gRPC服务和相关实现
- 定义 `common.proto` 和 `realtime_meeting.proto`,包含客户端认证、错误事件和实时会议相关的gRPC消息
- 实现 `AndroidGatewayPushServiceImpl`,处理设备注册、注销和消息推送
- 实现 `AndroidDeviceSessionServiceImpl`,管理设备会话状态和心跳更新
- 实现 `AsrUpstreamBridgeServiceImpl`,桥接ASR上游服务并处理音频流
- 实现 `AndroidRealtimeSessionTicketServiceImpl`,创建和获取实时会议会话票据
- 定义 `gateway.proto`,包含网关服务的消息定义
2026-04-01 09:27:30 +08:00
chenhao 3a7baa0341 feat: 添加实时会议会话状态管理和WebSocket控制功能
- 创建 `RealtimeMeetingSessionStateService` 及其实现类,管理实时会议会话状态
- 添加 `RealtimeMeetingSessionExpirationListener` 监听器,处理会话过期事件
- 更新前端API和组件,支持获取和控制实时会议会话状态
- 在 `RealtimeAsrSession` 组件中添加暂停和恢复识别的功能
2026-03-31 14:44:36 +08:00
chenhao a611ac2b61 feat: 添加文本修正功能和相关配置选项
- 在 `CreateMeetingCommand` 和 `meeting.ts` 中添加 `enableTextRefine` 字段
- 更新 `AiTaskServiceImpl` 和 `MeetingCommandServiceImpl` 以支持文本修正配置
- 在 `Meetings.tsx` 中添加文本修正的表单选项和默认值
2026-03-31 10:11:56 +08:00
chenhao 552e2255bd feat: 添加文本修正功能和相关配置选项
- 在 `CreateMeetingCommand` 和 `meeting.ts` 中添加 `enableTextRefine` 字段
- 更新 `AiTaskServiceImpl` 和 `MeetingCommandServiceImpl` 以支持文本修正配置
- 在 `Meetings.tsx` 中添加文本修正的表单选项和默认值
2026-03-31 10:02:42 +08:00
chenhao f9c0d31b87 feat: 添加实时会议WebSocket支持和相关服务
- 创建 `RealtimeMeetingSocketSessionService` 及其实现类,用于创建和获取实时会议会话
- 添加 `WebSocketSecurityConfig` 以配置WebSocket安全
- 创建 `RealtimeMeetingProxyWebSocketHandler` 处理WebSocket消息代理
- 配置 `RealtimeMeetingWebSocketConfig` 注
2026-03-31 09:54:08 +08:00
chenhao 9d1a8710af feat: 增加实时会议配置选项和WebSocket支持
- 在 `RealtimeAsr` 组件中添加语言、标点、ITN、文本修正和音频保存等配置选项
- 添加构建WebSocket URL的函数 `buildRealtimeProxyPreviewUrl`
- 更新 `meeting.ts` API,增加 `openRealtimeMeetingSocketSession` 接口
- 更新 `vite.config.ts`,添加WebSocket代理配置
- 优化 `RealtimeAsrSession` 组件,处理WebSocket消息并支持新的配置选项
2026-03-30 17:56:30 +08:00
chenhao 60754bbd26 feat: 添加会议转录编辑组件和优化关键词处理
- 在 `MeetingDetail` 页面中添加 `ActiveTranscriptRow` 组件,支持会议转录的编辑和保存
- 优化关键词处理逻辑,移除重复代码并
2026-03-27 14:33:32 +08:00
chenhao ffc19fa572 feat: 添加关键词管理功能和会议转录编辑功能
- 在 `MeetingDetail` 页面中添加关键词选择和一键加入热词的功能
- 增加会议转录的编辑和保存功能
- 更新后端接口和相关服务,支持更新会议转录内容
2026-03-27 13:50:01 +08:00
chenhao 8dbed4c8e6 refactor: 优化MeetingDetail页面和后端时间处理逻辑
- 优化 `MeetingDetail` 页面的待办事项和侧边栏样式
- 更新 `AiTaskServiceImpl` 和 `MeetingSummaryFileServiceImpl`
2026-03-27 10:52:01 +08:00
chenhao 12c79cdf26 refactor: 重构MeetingDetail页面,优化会议详情和智能分析展示
- 重构 `MeetingDetail` 组件,优化会议详情和智能分析的展示
- 增加关键词、全文概要、章节速览、发言总结、要点回顾和待办事项的展示逻辑
- 优化音频播放器和进度条功能
- 更新表单验证和数据处理逻辑
- 修复部分样式和布局问题
2026-03-27 10:30:48 +08:00
chenhao 4ee7a620b9 refactor: 删除MeetingCreate页面并更新主页和AI模型服务
- 删除 `MeetingCreate` 页面及其相关代码
- 更新主页组件,替换静态视觉元素为动态 `RightVisual` 组件
- 在 `AiModelService` 和 `AiModelController` 中添加本地连通性测试功能
- 重构 `AiModelServiceImpl`,增加验证和配置更新逻辑
2026-03-26 17:42:29 +08:00
chenhao 92e6b9fd4d feat: 添加角色数据权限管理功能
- 在 `zh-CN.json` 中新增与Bot凭证相关的国际化字符串
- 在 `index.tsx` 中添加数据权限管理标签页,支持自定义部门选择
- 更新API接口,新增获取和保存角色数据权限的方法
- 重构角色选择逻辑,加载角色时同时获取权限和数据权限信息
- 优化用户绑定和权限保存操作的提示信息
2026-03-26 13:44:57 +08:00
chenhao 653a9f7ef4 feat: 添加实时会议和主页组件
- 新增 `RealtimeAsr` 组件,用于创建和配置实时会议
- 新增 `HomePage` 组件,展示最近的会议记录和快速入口
- 新增 `RealtimeAsrSession` 组件,用于实时会议的会中识别和转录
2026-03-26 11:18:44 +08:00
chenhao 2b1d7b8a2e feat: 集成jar 集成前端 2026-03-17 15:31:09 +08:00
chenhao af5c658bd9 feat: 修改文件 2026-03-17 10:24:50 +08:00
chenhao d8bfdb21fa feat: 增加实时会议功能并优化数据库和API
- 增加实时会议的创建、追加转录和完成接口
- 更新 `MeetingDTO` 和 `MeetingVO`,添加可选字段 `summaryModelId`, `useSpkId`, `hotWords`
- 增加 `RealtimeTranscriptItemDTO` 接口
- 延长HTTP客户端连接超时时间至300秒
- 优化数据库表结构,新增 `biz_prompt_template_user_config` 表
- 更新系统参数和权限配置
2026-03-12 20:39:49 +08:00
chenhao eaed89c9ec feat: 修改页面 2026-03-10 17:43:33 +08:00
chenhao 364e49b3df feat: 重构AI模型控制器和服务,支持ASR和LLM模型类型
- 更新 `AiModelController` 中的保存、更新和删除方法,增加模型类型校验
- 重构 `AiModelServiceImpl`,分离ASR和LLM模型处理逻辑
- 更新数据库表结构,新增 `biz_asr_models` 和 `biz_llm_models` 表
- 添加前端API接口,支持按类型删除AI模型
2026-03-09 16:10:48 +08:00
chenhao 86b3616786 feat: 重构会议和任务模型,优化ASR和总结任务处理逻辑
- 移除 `Meeting` 实体中的 `asrModelId`, `summaryModelId`, `promptContent`, `useSpkId`, `hotWords`, `summaryContent` 字段,新增 `latestSummaryTaskId` 字段
- 更新数据库表结构,移除相关字段并添加 `latest_summary_task_id` 字段
- 在 `AiTask` 实体中新增 `task_config` 和 `result_file_path` 字段
- 重构 `AiTaskServiceImpl` 中的 ASR 和总结任务处理逻辑,支持任务配置和结果文件路径
- 重构 `MeetingServiceImpl` 中的会议创建和更新逻辑,分离 ASR 和总结任务
- 优化 `PromptTemplateServiceImpl`,新增用户模板状态管理功能
2026-03-09 14:44:24 +08:00
chenhao d554849e8e feat: 添加声纹注册路由并优化用户菜单项 2026-03-06 15:27:08 +08:00
chenhao c60db64578 feat:导出pdf优化 2026-03-06 13:45:56 +08:00
chenhao 430459c331 feat:导出pdf优化 2026-03-06 09:59:29 +08:00
chenhao 61da050438 feat:导出pdf 2026-03-05 17:52:08 +08:00
chenhao 0ccf0aa87d feat: 优化ASR任务接管逻辑和前端多语言支持
- 在后端实现ASR任务接管逻辑,支持恢复未完成的任务
- 添加前端多语言支持,引入 `react-i18next` 库
-
2026-03-05 09:36:41 +08:00
chenhao 11ab76f2ed feat: 优化会议创建表单和卡片显示
- 重构会议创建表单,增加录音上传、AI分析配置和参会人员选择
- 更新会议卡片组件,集成进度背景和状态标签,优化样式和交互体验
- 增加分页功能和多语言支持
- 修复和优化多处代码逻辑和样式问题
2026-03-04 20:59:49 +08:00
chenhao 423327c61d feat: 优化会议创建表单和卡片显示
- 重构会议创建表单,增加录音上传、AI分析配置和参会人员选择
- 更新会议卡片组件,集成进度背景和状态标签,优化样式和交互体验
- 增加分页功能和多语言支持
- 修复和优化多处代码逻辑和样式问题
2026-03-04 20:49:32 +08:00
chenhao 5c47b07ec6 feat:sql修改 2026-03-04 19:28:07 +08:00
chenhao afff8a8d07 feat: 添加会议总结测试用例和优化前端会议卡片显示
- 在后端添加 `SummaryTest` 测试类,实现会议总结功能的分步测试
- 重构前端会议卡片组件,集成进度背景和状态标签
- 优化会议卡片样式和交互体验,增加呼吸灯效果和详细信息展示
2026-03-04 19:25:21 +08:00
chenhao 35396104a0 Merge branch 'refs/heads/master' into dev_na
# Conflicts:
#	backend/src/main/java/com/imeeting/mapper/SysUserRoleMapper.java
2026-03-04 18:57:43 +08:00
chenhao 37025d3f02 feat: 添加会议进度显示和优化任务处理逻辑
- 在前端添加会议进度显示组件
- 优化后端任务调度逻辑,增加轮询锁防止并发执行
- 更新ASR和LLM任务处理流程,同步进度到Redis
- 重构会议详情页,展示AI分析进度和状态
- 修复和优化多处代码逻辑和样式问题
2026-03-04 17:19:41 +08:00
chenhao 80a4682757 feat: 添加声纹识别开关和优化模板管理权限
- 在会议创建页面添加声纹识别开关
- 优化提示模板的权限管理,区分平台级、租户级和个人级
- 更新后端逻辑处理不同层级的模板权限
- 重构前端模板选择界面,增强用户体验
- 添加热词权重显示并更新数据库表结构
- 修复和优化多处代码逻辑和样式问题
2026-03-04 15:19:40 +08:00
chenhao eaadc4ee51 feat: 添加会议创建事件和任务调度监听器
- 新增 `MeetingCreatedEvent` 事件类
- 实现 `MeetingTaskDispatchListener` 监听器,处理会议创建后的任务调度
- 更新 `MeetingServiceImpl` 发布会议创建事件
- 新增 `DashboardController` 提供仪表板统计和最近会议接口
- 更新 `SpeakerController` 和 `SpeakerServiceImpl` 支持声纹注册调用外部接口
- 添加测试配置文件 `application-test.yml`
- 优化 `WebConfig` 配置上传路径
- 更新前端 API 封装 `dashboard.ts`
2026-03-03 15:51:17 +08:00
chenhao 5e4a2aa2d1 feat(layout): 添加声纹注册菜单项并重构仪表板页面
- 在应用布局的导航菜单中添加声纹注册功能入口
- 重构仪表板页面实现动态数据加载和实时任务状态显示
- 实现会议任务的分步处理进度可视化展示
- 优化声纹注册页面的用户体验和交互流程
- 完善会议管理的权限控制逻辑
- 实现仪表板统计数据的实时轮询更新
2026-03-03 11:25:03 +08:00
chenhao 70e739818a feat(hotword): 实现热词权限管理和搜索功能
- 添加管理员权限判断方法支持平台管理员和租户管理员
- 实现公开热词仅管理员可创建、修改、删除的权限控制
- 支持私有热词本人或管理员可操作的权限验证
- 添加前端搜索类型筛选功能支持公开/私有分类
- 更新前端权限检查逻辑统一使用管理员身份判断
- 修复后端查询构造器逻辑确保权限过滤正确执行
- 添加租户管理员身份到JWT认证信息中
- 配置数据访问层忽略租户隔离注解优化查询性能
2026-03-03 10:08:07 +08:00
chenhao f6ba04ebd8 Merge branch 'refs/heads/master' into dev_na
# Conflicts:
#	backend/src/main/java/com/imeeting/auth/JwtAuthenticationFilter.java
#	backend/src/main/java/com/imeeting/controller/DictItemController.java
#	backend/src/main/java/com/imeeting/service/impl/AuthServiceImpl.java
2026-03-03 09:23:39 +08:00
chenhao 21b3ab3afc feat(business): 添加AI模型配置功能实现
- 新增AiModel实体类定义数据库表结构
- 实现AI模型的增删改查REST API接口
- 添加前端AI模型管理页面支持配置展示
- 实现ASR和LLM两种模型类型的区分管理
- 添加模型远程列表获取和验证功能
- 实现默认模型设置和租户权限控制
- 新增AiTask实体用于AI任务调度管理
- 实现AI任务异步处理服务逻辑
- 添加会议转录和总结的完整处理流程
2026-03-02 19:59:47 +08:00
chenhao 1a392d96b9 feat(layout): 添加音频、热词和提示词菜单项并优化布局结构
- 在AppLayout中添加AudioOutlined、TagsOutlined和BulbOutlined图标
- 为audio、hotword和prompt路由配置对应的菜单图标映射
- 重构菜单项生成逻辑,使用useMemo优化性能并修复TDZ错误
- 在菜单树查找函数中添加数组验证防止运行时错误
- 添加新业务模块数据库表:声纹发言人表、热词管理表和提示词模板表
- 更新租户ID字段的自动填充逻辑和权限过滤配置
- 在认证流程中添加displayName和pwdResetRequired字段支持
- 添加React Markdown依赖用于内容渲染功能
2026-03-02 09:09:53 +08:00
433 changed files with 41783 additions and 10567 deletions

11
.gitignore vendored 100644
View File

@ -0,0 +1,11 @@
# Local environment files
.env
.env.*
backend/.env
backend/.env.*
backend/src/main/resources/application-local.yml
# Logs
*.log
!backend/.env.example
.omx/

View File

@ -1,223 +1,368 @@
# 数据库结构文档PostgreSQL
# 鏁版嵁搴撶粨鏋勬枃妗紙PostgreSQL锛?
本文档根据 `backend/design/db_schema_pgsql.sql` 生成,描述当前核心表结构、字段、约束与索引。
鏈枃妗f牴鎹?`backend/design/db_schema_pgsql.sql` 鐢熸垚锛屾弿杩板綋鍓嶆牳蹇冭〃缁撴瀯銆佸瓧娈点€佺害鏉熶笌绱㈠紩銆?
## 0. 租户与组织
## 0. 绉熸埛涓庣粍缁?
### 0.1 `sys_tenant`(租户表)
| 字段 | 类型 | 约束 | 说明 |
### 0.1 `sys_tenant`锛堢鎴疯〃锛?
| 瀛楁 | 绫诲瀷 | 绾︽潫 | 璇存槑 |
| --- | --- | --- | --- |
| id | BIGSERIAL | PK | 租户ID |
| tenant_code | VARCHAR(64) | NOT NULL, UNIQUE | 租户编码 |
| tenant_name | VARCHAR(128) | NOT NULL | 租户名称 |
| status | SMALLINT | NOT NULL, DEFAULT 1 | 状态 |
| expire_time | TIMESTAMP(6) | | 过期时间 |
| contact_name | VARCHAR(64) | | 联系人 |
| contact_phone | VARCHAR(32) | | 联系电话 |
| remark | VARCHAR(255) | | 备注 |
| created_at | TIMESTAMP(6) | NOT NULL, DEFAULT CURRENT_TIMESTAMP | 创建时间 |
| updated_at | TIMESTAMP(6) | NOT NULL, DEFAULT CURRENT_TIMESTAMP | 更新时间 |
| is_deleted | SMALLINT | DEFAULT 0 | 逻辑删除标记 |
| id | BIGSERIAL | PK | 绉熸埛ID |
| tenant_code | VARCHAR(64) | NOT NULL, UNIQUE | 绉熸埛缂栫爜 |
| tenant_name | VARCHAR(128) | NOT NULL | 绉熸埛鍚嶇О |
| status | SMALLINT | NOT NULL, DEFAULT 1 | 鐘舵€?|
| expire_time | TIMESTAMP(6) | | 杩囨湡鏃堕棿 |
| contact_name | VARCHAR(64) | | 鑱旂郴浜?|
| contact_phone | VARCHAR(32) | | 鑱旂郴鐢佃瘽 |
| remark | VARCHAR(255) | | 澶囨敞 |
| created_at | TIMESTAMP(6) | NOT NULL, DEFAULT CURRENT_TIMESTAMP | 鍒涘缓鏃堕棿 |
| updated_at | TIMESTAMP(6) | NOT NULL, DEFAULT CURRENT_TIMESTAMP | 鏇存柊鏃堕棿 |
| is_deleted | SMALLINT | DEFAULT 0 | 閫昏緫鍒犻櫎鏍囪 |
索引:
- `uk_tenant_code``UNIQUE (tenant_code) WHERE is_deleted = FALSE`
绱㈠紩锛?
- `uk_tenant_code`锛歚UNIQUE (tenant_code) WHERE is_deleted = FALSE`
### 0.2 `sys_org`(组织架构表)
| 字段 | 类型 | 约束 | 说明 |
### 0.2 `sys_org`锛堢粍缁囨灦鏋勮〃锛?
| 瀛楁 | 绫诲瀷 | 绾︽潫 | 璇存槑 |
| --- | --- | --- | --- |
| id | BIGSERIAL | PK | 组织ID |
| tenant_id | BIGINT | NOT NULL | 租户ID |
| parent_id | BIGINT | | 父级组织ID |
| org_name | VARCHAR(128) | NOT NULL | 组织名称 |
| org_code | VARCHAR(64) | | 组织编码 |
| org_path | VARCHAR(512) | | 组织路径 |
| sort_order | INTEGER | DEFAULT 0 | 排序 |
| status | SMALLINT | DEFAULT 1 | 状态 |
| created_at | TIMESTAMP(6) | NOT NULL, DEFAULT CURRENT_TIMESTAMP | 创建时间 |
| updated_at | TIMESTAMP(6) | NOT NULL, DEFAULT CURRENT_TIMESTAMP | 更新时间 |
| is_deleted | SMALLINT | DEFAULT 0 | 逻辑删除标记 |
| id | BIGSERIAL | PK | 缁勭粐ID |
| tenant_id | BIGINT | NOT NULL | 绉熸埛ID |
| parent_id | BIGINT | | 鐖剁骇缁勭粐ID |
| org_name | VARCHAR(128) | NOT NULL | 缁勭粐鍚嶇О |
| org_code | VARCHAR(64) | | 缁勭粐缂栫爜 |
| org_path | VARCHAR(512) | | 缁勭粐璺緞 |
| sort_order | INTEGER | DEFAULT 0 | 鎺掑簭 |
| status | SMALLINT | DEFAULT 1 | 鐘舵€?|
| created_at | TIMESTAMP(6) | NOT NULL, DEFAULT CURRENT_TIMESTAMP | 鍒涘缓鏃堕棿 |
| updated_at | TIMESTAMP(6) | NOT NULL, DEFAULT CURRENT_TIMESTAMP | 鏇存柊鏃堕棿 |
| is_deleted | SMALLINT | DEFAULT 0 | 閫昏緫鍒犻櫎鏍囪 |
外键:
- `fk_org_parent``parent_id -> sys_org(id)`
- `fk_org_tenant``tenant_id -> sys_tenant(id)`
澶栭敭锛?
- `fk_org_parent`锛歚parent_id -> sys_org(id)`
- `fk_org_tenant`锛歚tenant_id -> sys_tenant(id)`
索引:
- `idx_org_tenant``(tenant_id)`
绱㈠紩锛?
- `idx_org_tenant`锛歚(tenant_id)`
## 1. 用户与角色
## 1. 鐢ㄦ埛涓庤鑹?
### 1.1 `sys_user`(用户表)
| 字段 | 类型 | 约束 | 说明 |
### 1.1 `sys_user`锛堢敤鎴疯〃锛?
| 瀛楁 | 绫诲瀷 | 绾︽潫 | 璇存槑 |
| --- | --- | --- | --- |
| user_id | BIGSERIAL | PK | 用户ID |
| username | VARCHAR(50) | NOT NULL, UNIQUE | 登录名 |
| display_name | VARCHAR(50) | NOT NULL | 显示名 |
| email | VARCHAR(100) | | 邮箱 |
| phone | VARCHAR(30) | UNIQUE | 手机号 |
| password_hash | VARCHAR(255) | NOT NULL | 密码哈希 |
| status | SMALLINT | NOT NULL, DEFAULT 1 | 状态 |
| pwd_reset_required | SMALLINT | DEFAULT 1 | 首次登录是否需改密 |
| is_deleted | SMALLINT | NOT NULL, DEFAULT 0 | 逻辑删除标记 |
| created_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 创建时间 |
| updated_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 更新时间 |
| is_platform_admin | BOOLEAN | DEFAULT false | 是否平台管理员 |
| user_id | BIGSERIAL | PK | 鐢ㄦ埛ID |
| username | VARCHAR(50) | NOT NULL, UNIQUE | 鐧诲綍鍚?|
| display_name | VARCHAR(50) | NOT NULL | 鏄剧ず鍚?|
| email | VARCHAR(100) | | 閭 |
| phone | VARCHAR(30) | UNIQUE | 鎵嬫満鍙?|
| password_hash | VARCHAR(255) | NOT NULL | 瀵嗙爜鍝堝笇 |
| status | SMALLINT | NOT NULL, DEFAULT 1 | 鐘舵€?|
| pwd_reset_required | SMALLINT | DEFAULT 1 | 棣栨鐧诲綍鏄惁闇€鏀瑰瘑 |
| is_deleted | SMALLINT | NOT NULL, DEFAULT 0 | 閫昏緫鍒犻櫎鏍囪 |
| created_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 鍒涘缓鏃堕棿 |
| updated_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 鏇存柊鏃堕棿 |
| is_platform_admin | BOOLEAN | DEFAULT false | 鏄惁骞冲彴绠$悊鍛?|
索引:
- `uk_user_username``UNIQUE (username) WHERE is_deleted = FALSE`
绱㈠紩锛?
- `uk_user_username`锛歚UNIQUE (username) WHERE is_deleted = FALSE`
### 1.2 `sys_role`(角色表)
| 字段 | 类型 | 约束 | 说明 |
### 1.2 `sys_role`锛堣鑹茶〃锛?
| 瀛楁 | 绫诲瀷 | 绾︽潫 | 璇存槑 |
| --- | --- | --- | --- |
| role_id | BIGSERIAL | PK | 角色ID |
| tenant_id | BIGINT | NOT NULL | 租户ID |
| role_code | VARCHAR(50) | NOT NULL | 角色编码(租户内唯一) |
| role_name | VARCHAR(50) | NOT NULL | 角色名称 |
| status | SMALLINT | NOT NULL, DEFAULT 1 | 状态 |
| remark | TEXT | | 备注 |
| is_deleted | SMALLINT | NOT NULL, DEFAULT 0 | 逻辑删除标记 |
| created_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 创建时间 |
| updated_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 更新时间 |
| role_id | BIGSERIAL | PK | 瑙掕壊ID |
| tenant_id | BIGINT | NOT NULL | 绉熸埛ID |
| role_code | VARCHAR(50) | NOT NULL | 瑙掕壊缂栫爜锛堢鎴峰唴鍞竴锛?|
| role_name | VARCHAR(50) | NOT NULL | 瑙掕壊鍚嶇О |
| status | SMALLINT | NOT NULL, DEFAULT 1 | 鐘舵€?|
| remark | TEXT | | 澶囨敞 |
| is_deleted | SMALLINT | NOT NULL, DEFAULT 0 | 閫昏緫鍒犻櫎鏍囪 |
| created_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 鍒涘缓鏃堕棿 |
| updated_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 鏇存柊鏃堕棿 |
索引:
- `idx_sys_role_tenant``(tenant_id)`
- `uk_role_code``UNIQUE (tenant_id, role_code) WHERE is_deleted = FALSE`
绱㈠紩锛?
- `idx_sys_role_tenant`锛歚(tenant_id)`
- `uk_role_code`锛歚UNIQUE (tenant_id, role_code) WHERE is_deleted = FALSE`
### 1.3 `sys_user_role`(用户-角色关联表,租户强约束)
| 字段 | 类型 | 约束 | 说明 |
### 1.3 `sys_user_role`锛堢敤鎴?瑙掕壊鍏宠仈琛紝绉熸埛寮虹害鏉燂級
| 瀛楁 | 绫诲瀷 | 绾︽潫 | 璇存槑 |
| --- | --- | --- | --- |
| id | BIGSERIAL | PK | 关联ID |
| tenant_id | BIGINT | NOT NULL | 租户ID |
| user_id | BIGINT | NOT NULL | 用户ID |
| role_id | BIGINT | NOT NULL | 角色ID |
| is_deleted | SMALLINT | NOT NULL, DEFAULT 0 | 逻辑删除标记 |
| created_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 创建时间 |
| updated_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 更新时间 |
| id | BIGSERIAL | PK | 鍏宠仈ID |
| tenant_id | BIGINT | NOT NULL | 绉熸埛ID |
| user_id | BIGINT | NOT NULL | 鐢ㄦ埛ID |
| role_id | BIGINT | NOT NULL | 瑙掕壊ID |
| is_deleted | SMALLINT | NOT NULL, DEFAULT 0 | 閫昏緫鍒犻櫎鏍囪 |
| created_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 鍒涘缓鏃堕棿 |
| updated_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 鏇存柊鏃堕棿 |
唯一约束:
鍞竴绾︽潫锛?
- `UNIQUE (tenant_id, user_id, role_id) WHERE is_deleted = 0`
### 1.4 `sys_tenant_user`(租户成员关联表)
| 字段 | 类型 | 约束 | 说明 |
### 1.4 `sys_tenant_user`锛堢鎴锋垚鍛樺叧鑱旇〃锛?
| 瀛楁 | 绫诲瀷 | 绾︽潫 | 璇存槑 |
| --- | --- | --- | --- |
| id | BIGSERIAL | PK | 关联ID |
| user_id | BIGINT | NOT NULL | 用户ID |
| tenant_id | BIGINT | NOT NULL | 租户ID |
| org_id | BIGINT | | 组织ID |
| status | SMALLINT | DEFAULT 1 | 状态 |
| is_deleted | SMALLINT | DEFAULT 0 | 逻辑删除标记 |
| created_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 创建时间 |
| updated_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 更新时间 |
| id | BIGSERIAL | PK | 鍏宠仈ID |
| user_id | BIGINT | NOT NULL | 鐢ㄦ埛ID |
| tenant_id | BIGINT | NOT NULL | 绉熸埛ID |
| org_id | BIGINT | | 缁勭粐ID |
| status | SMALLINT | DEFAULT 1 | 鐘舵€?|
| is_deleted | SMALLINT | DEFAULT 0 | 閫昏緫鍒犻櫎鏍囪 |
| created_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 鍒涘缓鏃堕棿 |
| updated_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 鏇存柊鏃堕棿 |
索引:
- `uk_tenant_user``UNIQUE (user_id, tenant_id) WHERE is_deleted = 0`
绱㈠紩锛?
- `uk_tenant_user`锛歚UNIQUE (user_id, tenant_id) WHERE is_deleted = 0`
## 2. 权限/字典/参数(全局共享)
## 2. 鏉冮檺/瀛楀吀/鍙傛暟锛堝叏灞€鍏变韩锛?
### 2.1 `sys_permission`(权限表)
| 字段 | 类型 | 约束 | 说明 |
### 2.1 `sys_permission`锛堟潈闄愯〃锛?
| 瀛楁 | 绫诲瀷 | 绾︽潫 | 璇存槑 |
| --- | --- | --- | --- |
| perm_id | BIGSERIAL | PK | 权限ID |
| parent_id | BIGINT | | 父级权限ID |
| name | VARCHAR(100) | NOT NULL | 权限名称 |
| code | VARCHAR(100) | NOT NULL, UNIQUE | 权限编码 |
| perm_type | VARCHAR(20) | NOT NULL | 权限类型 |
| level | INTEGER | NOT NULL | 层级 |
| path | VARCHAR(255) | | 路径 |
| component | VARCHAR(255) | | 组件 |
| icon | VARCHAR(100) | | 图标 |
| sort_order | INTEGER | NOT NULL, DEFAULT 0 | 排序 |
| is_visible | SMALLINT | NOT NULL, DEFAULT 1 | 是否可见 |
| status | SMALLINT | NOT NULL, DEFAULT 1 | 状态 |
| description | TEXT | | 描述 |
| meta | JSONB | | 扩展信息 |
| is_deleted | SMALLINT | NOT NULL, DEFAULT 0 | 逻辑删除标记 |
| created_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 创建时间 |
| updated_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 更新时间 |
| perm_id | BIGSERIAL | PK | 鏉冮檺ID |
| parent_id | BIGINT | | 鐖剁骇鏉冮檺ID |
| name | VARCHAR(100) | NOT NULL | 鏉冮檺鍚嶇О |
| code | VARCHAR(100) | NOT NULL, UNIQUE | 鏉冮檺缂栫爜 |
| perm_type | VARCHAR(20) | NOT NULL | 鏉冮檺绫诲瀷 |
| level | INTEGER | NOT NULL | 灞傜骇 |
| path | VARCHAR(255) | | 璺緞 |
| component | VARCHAR(255) | | 缁勪欢 |
| icon | VARCHAR(100) | | 鍥炬爣 |
| sort_order | INTEGER | NOT NULL, DEFAULT 0 | 鎺掑簭 |
| is_visible | SMALLINT | NOT NULL, DEFAULT 1 | 鏄惁鍙 |
| status | SMALLINT | NOT NULL, DEFAULT 1 | 鐘舵€?|
| description | TEXT | | 鎻忚堪 |
| meta | JSONB | | 鎵╁睍淇℃伅 |
| is_deleted | SMALLINT | NOT NULL, DEFAULT 0 | 閫昏緫鍒犻櫎鏍囪 |
| created_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 鍒涘缓鏃堕棿 |
| updated_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 鏇存柊鏃堕棿 |
### 2.2 `sys_dict_type`(字典类型表)
| 字段 | 类型 | 约束 | 说明 |
### 2.2 `sys_dict_type`锛堝瓧鍏哥被鍨嬭〃锛?
| 瀛楁 | 绫诲瀷 | 绾︽潫 | 璇存槑 |
| --- | --- | --- | --- |
| dict_type_id | BIGSERIAL | PK | 类型ID |
| type_code | VARCHAR(50) | NOT NULL, UNIQUE | 类型编码 |
| type_name | VARCHAR(50) | NOT NULL | 类型名称 |
| status | SMALLINT | DEFAULT 1 | 状态 |
| remark | TEXT | | 备注 |
| created_at | TIMESTAMP | NOT NULL, DEFAULT NOW() | 创建时间 |
| updated_at | TIMESTAMP | NOT NULL, DEFAULT NOW() | 更新时间 |
| is_deleted | SMALLINT | DEFAULT 0 | 逻辑删除标记 |
| dict_type_id | BIGSERIAL | PK | 绫诲瀷ID |
| type_code | VARCHAR(50) | NOT NULL, UNIQUE | 绫诲瀷缂栫爜 |
| type_name | VARCHAR(50) | NOT NULL | 绫诲瀷鍚嶇О |
| status | SMALLINT | DEFAULT 1 | 鐘舵€?|
| remark | TEXT | | 澶囨敞 |
| created_at | TIMESTAMP | NOT NULL, DEFAULT NOW() | 鍒涘缓鏃堕棿 |
| updated_at | TIMESTAMP | NOT NULL, DEFAULT NOW() | 鏇存柊鏃堕棿 |
| is_deleted | SMALLINT | DEFAULT 0 | 閫昏緫鍒犻櫎鏍囪 |
**初始化数据:**
- `sys_common_status`: 通用状态 (启用/禁用)
- `sys_permission_type`: 权限类型 (目录/菜单/按钮)
- `sys_common_visibility`: 可见性 (显示/隐藏)
- `sys_permission_level`: 权限层级 (1, 2, 3)
- `sys_log_type`: 日志类型 (LOGIN/OPERATION)
- `sys_param_type`: 参数类型 (String/Number/Boolean/JSON)
- `sys_log_status`: 操作状态 (成功/失败)
**鍒濆鍖栨暟鎹細**
- `sys_common_status`: 閫氱敤鐘舵€?(鍚敤/绂佺敤)
- `sys_permission_type`: 鏉冮檺绫诲瀷 (鐩綍/鑿滃崟/鎸夐挳)
- `sys_common_visibility`: 鍙鎬?(鏄剧ず/闅愯棌)
- `sys_permission_level`: 鏉冮檺灞傜骇 (1, 2, 3)
- `sys_log_type`: 鏃ュ織绫诲瀷 (LOGIN/OPERATION)
- `sys_param_type`: 鍙傛暟绫诲瀷 (String/Number/Boolean/JSON)
- `sys_log_status`: 鎿嶄綔鐘舵€?(鎴愬姛/澶辫触)
### 2.3 `sys_dict_item`(字典项表)
| 字段 | 类型 | 约束 | 说明 |
### 2.3 `sys_dict_item`锛堝瓧鍏搁」琛級
| 瀛楁 | 绫诲瀷 | 绾︽潫 | 璇存槑 |
| --- | --- | --- | --- |
| dict_item_id | BIGSERIAL | PK | 字典项ID |
| type_code | VARCHAR(50) | NOT NULL | 字典类型编码 |
| item_label | VARCHAR(100) | NOT NULL | 展示文本 |
| item_value | VARCHAR(100) | NOT NULL | 存储值 |
| sort_order | INT | DEFAULT 0 | 排序 |
| status | SMALLINT | DEFAULT 1 | 状态 |
| created_at | TIMESTAMP | NOT NULL, DEFAULT NOW() | 创建时间 |
| updated_at | TIMESTAMP | NOT NULL, DEFAULT NOW() | 更新时间 |
| is_deleted | SMALLINT | DEFAULT 0 | 逻辑删除标记 |
| dict_item_id | BIGSERIAL | PK | 瀛楀吀椤笽D |
| type_code | VARCHAR(50) | NOT NULL | 瀛楀吀绫诲瀷缂栫爜 |
| item_label | VARCHAR(100) | NOT NULL | 灞曠ず鏂囨湰 |
| item_value | VARCHAR(100) | NOT NULL | 瀛樺偍鍊?|
| sort_order | INT | DEFAULT 0 | 鎺掑簭 |
| status | SMALLINT | DEFAULT 1 | 鐘舵€?|
| created_at | TIMESTAMP | NOT NULL, DEFAULT NOW() | 鍒涘缓鏃堕棿 |
| updated_at | TIMESTAMP | NOT NULL, DEFAULT NOW() | 鏇存柊鏃堕棿 |
| is_deleted | SMALLINT | DEFAULT 0 | 閫昏緫鍒犻櫎鏍囪 |
索引:
- `idx_dict_item_type``(type_code)`
- `uk_dict_item_value``UNIQUE (type_code, item_value)`
绱㈠紩锛?
- `idx_dict_item_type`锛歚(type_code)`
- `uk_dict_item_value`锛歚UNIQUE (type_code, item_value)`
### 2.4 `sys_param`(系统参数表)
| 字段 | 类型 | 约束 | 说明 |
### 2.4 `sys_param`锛堢郴缁熷弬鏁拌〃锛?
| 瀛楁 | 绫诲瀷 | 绾︽潫 | 璇存槑 |
| --- | --- | --- | --- |
| id | BIGSERIAL | PK | 参数ID |
| param_key | VARCHAR(100) | NOT NULL, UNIQUE | 参数键 |
| param_value | TEXT | NOT NULL | 参数值 |
| param_type | VARCHAR(20) | NOT NULL | 参数类型 |
| is_system | SMALLINT | DEFAULT 0 | 是否系统内置 |
| status | SMALLINT | DEFAULT 1 | 状态 |
| description | TEXT | | 描述 |
| created_at | TIMESTAMP | NOT NULL, DEFAULT NOW() | 创建时间 |
| id | BIGSERIAL | PK | 鍙傛暟ID |
| param_key | VARCHAR(100) | NOT NULL, UNIQUE | 鍙傛暟閿?|
| param_value | TEXT | NOT NULL | 鍙傛暟鍊?|
| param_type | VARCHAR(20) | NOT NULL | 鍙傛暟绫诲瀷 |
| is_system | SMALLINT | DEFAULT 0 | 鏄惁绯荤粺鍐呯疆 |
| status | SMALLINT | DEFAULT 1 | 鐘舵€?|
| description | TEXT | | 鎻忚堪 |
| created_at | TIMESTAMP | NOT NULL, DEFAULT NOW() | 鍒涘缓鏃堕棿 |
## 3. 日志(租户隔离)
## 3. 鏃ュ織锛堢鎴烽殧绂伙級
### 3.1 `sys_log`(系统日志表)
| 字段 | 类型 | 约束 | 说明 |
### 3.1 `sys_log`锛堢郴缁熸棩蹇楄〃锛?
| 瀛楁 | 绫诲瀷 | 绾︽潫 | 璇存槑 |
| --- | --- | --- | --- |
| id | BIGSERIAL | PK | 日志ID |
| tenant_id | BIGINT | NOT NULL, DEFAULT 0 | 租户ID |
| user_id | BIGINT | | 用户ID |
| username | VARCHAR(50) | | 用户名 |
| log_type | VARCHAR(20) | | 日志类型(如 LOGIN、OPERATION |
| operation | VARCHAR(100) | NOT NULL | 操作描述 |
| method | VARCHAR(200) | | 方法 |
| params | TEXT | | 请求参数 |
| status | SMALLINT | DEFAULT 1 | 状态 |
| id | BIGSERIAL | PK | 鏃ュ織ID |
| tenant_id | BIGINT | NOT NULL, DEFAULT 0 | 绉熸埛ID |
| user_id | BIGINT | | 鐢ㄦ埛ID |
| username | VARCHAR(50) | | 鐢ㄦ埛鍚?|
| log_type | VARCHAR(20) | | 鏃ュ織绫诲瀷锛堝 LOGIN銆丱PERATION锛?|
| operation | VARCHAR(100) | NOT NULL | 鎿嶄綔鎻忚堪 |
| method | VARCHAR(200) | | 鏂规硶 |
| params | TEXT | | 璇锋眰鍙傛暟 |
| status | SMALLINT | DEFAULT 1 | 鐘舵€?|
| ip | VARCHAR(50) | | IP |
| duration | BIGINT | | 耗时ms |
| created_at | TIMESTAMP | NOT NULL, DEFAULT NOW() | 创建时间 |
| duration | BIGINT | | 鑰楁椂锛坢s锛?|
| created_at | TIMESTAMP | NOT NULL, DEFAULT NOW() | 鍒涘缓鏃堕棿 |
索引:
- `idx_log_tenant_type``(tenant_id, log_type, created_at)`
绱㈠紩锛?
- `idx_log_tenant_type`锛歚(tenant_id, log_type, created_at)`
## 4. 平台配置
## 4. 骞冲彴閰嶇疆
### 4.1 `sys_platform_config`(平台管理表)
| 字段 | 类型 | 约束 | 说明 |
### 4.1 `sys_platform_config`锛堝钩鍙扮鐞嗚〃锛?
| 瀛楁 | 绫诲瀷 | 绾︽潫 | 璇存槑 |
| --- | --- | --- | --- |
| id | BIGINT | PK | 固定为 1 |
| project_name | VARCHAR(128) | NOT NULL | 项目名称 |
| id | BIGINT | PK | 鍥哄畾涓?1 |
| project_name | VARCHAR(128) | NOT NULL | 椤圭洰鍚嶇О |
| logo_url | VARCHAR(512) | | Logo URL |
| icon_url | VARCHAR(512) | | Icon URL |
| login_bg_url | VARCHAR(512) | | 登录页背景 |
| icp_info | VARCHAR(128) | | 备案信息 |
| copyright_info | VARCHAR(255) | | 版权信息 |
| system_description | TEXT | | 系统描述 |
| created_at | TIMESTAMP | NOT NULL | 创建时间 |
| updated_at | TIMESTAMP | NOT NULL | 更新时间 |
| login_bg_url | VARCHAR(512) | | 鐧诲綍椤佃儗鏅?|
| icp_info | VARCHAR(128) | | 澶囨淇℃伅 |
| copyright_info | VARCHAR(255) | | 鐗堟潈淇℃伅 |
| system_description | TEXT | | 绯荤粺鎻忚堪 |
| created_at | TIMESTAMP | NOT NULL | 鍒涘缓鏃堕棿 |
| updated_at | TIMESTAMP | NOT NULL | 鏇存柊鏃堕棿 |
## 5. 涓氬姟妯″潡
### 5.1 `biz_speakers`锛堝0绾瑰彂瑷€浜鸿〃锛?
| 瀛楁 | 绫诲瀷 | 绾︽潫 | 璇存槑 |
| --- | --- | --- | --- |
| id | BIGSERIAL | PK | 涓婚敭ID |
| tenant_id | BIGINT | NOT NULL | 绉熸埛ID |
| user_id | BIGINT | | 鍏宠仈绯荤粺鐢ㄦ埛ID |
| name | VARCHAR(100) | NOT NULL | 鍙戣█浜哄鍚?|
| voice_path | VARCHAR(512) | | 鍘熷鏂囦欢璺緞 |
| voice_ext | VARCHAR(10) | | 鏂囦欢鍚庣紑 |
| voice_size | BIGINT | | 鏂囦欢澶у皬 |
| status | SMALLINT | DEFAULT 1 | 鐘舵€?(1:宸蹭繚瀛? 2:娉ㄥ唽涓? 3:宸叉敞鍐? |
| embedding | VECTOR | | 澹扮汗鐗瑰緛鍚戦噺 |
| remark | TEXT | | 澶囨敞 |
| created_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 鍒涘缓鏃堕棿 |
| updated_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 鏇存柊鏃堕棿 |
| is_deleted | SMALLINT | DEFAULT 0 | 閫昏緫鍒犻櫎 |
绱㈠紩锛?
- `idx_speaker_tenant`: `(tenant_id)`
- `idx_speaker_user`: `(user_id) WHERE is_deleted = 0`
### 5.2 `biz_hot_words`锛堢儹璇嶇鐞嗚〃锛?
| 瀛楁 | 绫诲瀷 | 绾︽潫 | 璇存槑 |
| --- | --- | --- | --- |
| id | BIGSERIAL | PK | 涓婚敭ID |
| tenant_id | BIGINT | NOT NULL | 绉熸埛ID |
| word | VARCHAR(100) | NOT NULL | 鐑瘝鍘熸枃 |
| pinyin_list | JSONB | | 鎷奸煶鏁扮粍 |
| match_strategy | SMALLINT | DEFAULT 1 | 鍖归厤绛栫暐 (1:绮剧‘, 2:妯$硦) |
| category | VARCHAR(50) | | 绫诲埆 (浜哄悕銆佹湳璇瓑) |
| weight | INTEGER | DEFAULT 10 | 鏉冮噸 (1-100) |
| status | SMALLINT | DEFAULT 1 | 鐘舵€?(1:鍚敤, 0:绂佺敤) |
| is_synced | SMALLINT | DEFAULT 0 | 宸插悓姝ョ涓夋柟鏍囪 |
| remark | TEXT | | 澶囨敞 |
| created_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 鍒涘缓鏃堕棿 |
| updated_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 鏇存柊鏃堕棿 |
| is_deleted | SMALLINT | DEFAULT 0 | 閫昏緫鍒犻櫎 |
绱㈠紩锛?
- `idx_hotword_tenant`: `(tenant_id)`
- `idx_hotword_word`: `(word) WHERE is_deleted = 0`
### 5.3 `biz_prompt_templates`锛堟彁绀鸿瘝妯℃澘琛級
| 瀛楁 | 绫诲瀷 | 绾︽潫 | 璇存槑 |
| --- | --- | --- | --- |
| id | BIGSERIAL | PK | 涓婚敭ID |
| tenant_id | BIGINT | NOT NULL | 绉熸埛ID |
| template_name | VARCHAR(100) | NOT NULL | 妯℃澘鍚嶇О |
| category | VARCHAR(20) | | 鍒嗙被 (瀛楀吀: biz_prompt_category) |
| is_system | SMALLINT | DEFAULT 0 | 鏄惁棰勭疆 (1:鏄? 0:鍚? |
| creator_id | BIGINT | | 鍒涘缓浜篒D |
| tags | JSONB | | 鏍囩鏁扮粍 |
| usage_count | INTEGER | DEFAULT 0 | 浣跨敤娆℃暟 |
| prompt_content | TEXT | NOT NULL | 鎻愮ず璇嶅唴瀹?|
| status | SMALLINT | DEFAULT 1 | 鐘舵€?(1:鍚敤, 0:绂佺敤) |
| remark | VARCHAR(255) | | 澶囨敞 |
| created_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 鍒涘缓鏃堕棿 |
| updated_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 鏇存柊鏃堕棿 |
| is_deleted | SMALLINT | DEFAULT 0 | 閫昏緫鍒犻櫎 |
绱㈠紩锛?
- `idx_prompt_tenant`: `(tenant_id)`
- `idx_prompt_system`: `(is_system) WHERE is_deleted = 0`
### 5.4 `biz_asr_models`ASR 模型管理表)
| 字段 | 类型 | 约束 | 说明 |
| --- | --- | --- | --- |
| id | BIGSERIAL | PK | 主键ID |
| tenant_id | BIGINT | NOT NULL | 租户ID |
| model_name | VARCHAR(100) | NOT NULL | 模型显示名称 |
| provider | VARCHAR(50) | | 提供商 |
| base_url | VARCHAR(255) | | 接口基础地址 |
| api_key | VARCHAR(255) | | API 密钥 |
| model_code | VARCHAR(100) | | 模型代码 |
| ws_url | VARCHAR(255) | | WebSocket 地址 |
| media_config | JSON/TEXT | | 媒体参数 |
| is_default | SMALLINT | DEFAULT 0 | 默认模型标记 |
| status | SMALLINT | DEFAULT 1 | 状态 |
| remark | VARCHAR(255) | | 备注 |
| created_at | TIMESTAMP(6) | NOT NULL | 创建时间 |
| updated_at | TIMESTAMP(6) | NOT NULL | 更新时间 |
| is_deleted | SMALLINT | DEFAULT 0 | 逻辑删除 |
索引:
- `idx_asr_model_tenant`: `(tenant_id)`
- `idx_asr_model_default`: `(is_default) WHERE is_deleted = 0`
### 5.5 `biz_llm_models`LLM 模型管理表)
| 字段 | 类型 | 约束 | 说明 |
| --- | --- | --- | --- |
| id | BIGSERIAL | PK | 主键ID |
| tenant_id | BIGINT | NOT NULL | 租户ID |
| model_name | VARCHAR(100) | NOT NULL | 模型显示名称 |
| provider | VARCHAR(50) | | 提供商 |
| base_url | VARCHAR(255) | | 接口基础地址 |
| api_path | VARCHAR(100) | | API 路径 |
| api_key | VARCHAR(255) | | API 密钥 |
| model_code | VARCHAR(100) | | 模型代码 |
| temperature | DECIMAL | DEFAULT 0.7 | 随机性 |
| top_p | DECIMAL | DEFAULT 0.9 | 核采样 |
| is_default | SMALLINT | DEFAULT 0 | 默认模型标记 |
| status | SMALLINT | DEFAULT 1 | 状态 |
| remark | VARCHAR(255) | | 备注 |
| created_at | TIMESTAMP(6) | NOT NULL | 创建时间 |
| updated_at | TIMESTAMP(6) | NOT NULL | 更新时间 |
| is_deleted | SMALLINT | DEFAULT 0 | 逻辑删除 |
索引:
- `idx_llm_model_tenant`: `(tenant_id)`
- `idx_llm_model_default`: `(is_default) WHERE is_deleted = 0`
### 5.6 `biz_meetings`锛堜細璁富琛級
| 瀛楁 | 绫诲瀷 | 绾︽潫 | 璇存槑 |
| --- | --- | --- | --- |
| id | BIGSERIAL | PK | 涓婚敭ID |
| tenant_id | BIGINT | NOT NULL | 绉熸埛ID |
| title | VARCHAR(200) | NOT NULL | 浼氳鏍囬 |
| audio_url | VARCHAR(500) | | 涓撳睘闊抽璺緞 |
| latest_summary_task_id | BIGINT | | 鏈€鏂版垚鍔熺殑鎬荤粨浠诲姟ID |
| status | SMALLINT | DEFAULT 0 | 0:寰呭鐞? 1:璇嗗埆涓? 2:鎬荤粨涓? 3:宸插畬鎴? 4:澶辫触 |
### 5.6 `biz_meeting_transcripts`锛堣浆褰曟槑缁嗚〃锛?
| 瀛楁 | 绫诲瀷 | 绾︽潫 | 璇存槑 |
| --- | --- | --- | --- |
| id | BIGSERIAL | PK | 涓婚敭ID |
| meeting_id | BIGINT | NOT NULL | 鍏宠仈浼氳ID |
| speaker_label | VARCHAR(50) | | 鍙戣█浜烘爣绛?|
| content | TEXT | | 杞綍鏂囧瓧 |
| start_time | INTEGER | | 寮€濮嬫椂闂?(ms) |
### 5.7 `biz_ai_tasks`锛圓I 浠诲姟娴佹按琛級
| 瀛楁 | 绫诲瀷 | 绾︽潫 | 璇存槑 |
| --- | --- | --- | --- |
| id | BIGSERIAL | PK | 涓婚敭ID |
| meeting_id | BIGINT | NOT NULL | 鍏宠仈浼氳ID |
| task_type | VARCHAR(20) | | ASR / SUMMARY |
| request_data | JSONB | | 璇锋眰鍘熷鏁版嵁 |
| response_data | JSONB | | 鍝嶅簲鍘熷鏁版嵁 |
| task_config | TEXT | | **[蹇収]** 浠诲姟閰嶇疆(妯″瀷ID銆佹彁绀鸿瘝妯℃澘绛? |
| result_file_path | VARCHAR(500) | | 缁撴灉鏂囦欢鐩稿璺緞 (濡侻D鎬荤粨鏂囦欢) |
| status | SMALLINT | | 0:鎺掗槦, 1:澶勭悊涓? 2:鎴愬姛, 3:澶辫触 |

View File

@ -1,10 +1,10 @@
-- PostgreSQL Database Schema for iMeeting (Multi-tenant)
-- PostgreSQL Database Schema for iMeeting (Multi-tenant)
-- 0 为系统预留租户 ID
-- ----------------------------
-- 0. 租户与组织
-- ----------------------------
CREATE EXTENSION IF NOT EXISTS vector;
-- 租户表
CREATE TABLE sys_tenant (
id BIGSERIAL PRIMARY KEY,
@ -19,7 +19,7 @@ CREATE TABLE sys_tenant (
updated_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
is_deleted SMALLINT DEFAULT 0
);
CREATE UNIQUE INDEX uk_tenant_code ON sys_tenant (tenant_code) WHERE is_deleted = 0;
CREATE INDEX uk_tenant_code ON sys_tenant (tenant_code) WHERE is_deleted = 0;
-- 组织架构表
DROP TABLE IF EXISTS sys_org CASCADE;
@ -83,7 +83,7 @@ CREATE TABLE sys_role (
);
CREATE INDEX idx_sys_role_tenant ON sys_role (tenant_id);
CREATE UNIQUE INDEX uk_role_code ON sys_role (tenant_id, role_code) WHERE is_deleted = 0;
CREATE INDEX uk_role_code ON sys_role (tenant_id, role_code) WHERE is_deleted = 0;
-- 用户-角色关联表 (按 tenant_id 强约束,避免跨租户角色污染)
DROP TABLE IF EXISTS sys_user_role CASCADE;
@ -109,7 +109,7 @@ CREATE TABLE sys_permission (
perm_id BIGSERIAL PRIMARY KEY,
parent_id BIGINT,
name VARCHAR(100) NOT NULL,
code VARCHAR(100) NOT NULL UNIQUE,
code VARCHAR(100) NOT NULL ,
perm_type VARCHAR(20) NOT NULL,
level INTEGER NOT NULL,
path VARCHAR(255),
@ -138,7 +138,7 @@ CREATE TABLE sys_tenant_user (
updated_at TIMESTAMP(6) NOT NULL DEFAULT now()
);
CREATE UNIQUE INDEX uk_tenant_user
CREATE INDEX uk_tenant_user
ON sys_tenant_user (user_id, tenant_id)
WHERE is_deleted = 0;
CREATE TABLE sys_dict_type (
@ -161,13 +161,14 @@ CREATE TABLE sys_dict_item (
status SMALLINT DEFAULT 1,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
is_deleted SMALLINT DEFAULT 0
is_deleted SMALLINT DEFAULT 0,
remark varchar(255)
);
CREATE INDEX idx_dict_item_type ON sys_dict_item (type_code);
CREATE UNIQUE INDEX uk_dict_item_value ON sys_dict_item (type_code, item_value);
CREATE INDEX uk_dict_item_value ON sys_dict_item (type_code, item_value);
CREATE TABLE sys_param (
id BIGSERIAL PRIMARY KEY,
param_id BIGSERIAL PRIMARY KEY,
param_key VARCHAR(100) UNIQUE NOT NULL,
param_value TEXT NOT NULL,
param_type VARCHAR(20) NOT NULL,
@ -179,6 +180,14 @@ CREATE TABLE sys_param (
is_deleted SMALLINT DEFAULT 0
);
CREATE TABLE sys_role_permission (
"id" BIGSERIAL PRIMARY KEY,
"role_id" int8 NOT NULL,
"perm_id" int8 NOT NULL,
"is_deleted" int2 NOT NULL DEFAULT 0,
"created_at" timestamp(6) NOT NULL DEFAULT now(),
"updated_at" timestamp(6) NOT NULL DEFAULT now()
);
-- ----------------------------
-- 3. 日志 (租户隔离)
-- ----------------------------
@ -220,6 +229,216 @@ CREATE TABLE sys_platform_config (
INSERT INTO sys_platform_config (id, project_name, copyright_info)
VALUES (1, 'iMeeting 智能会议系统', '© 2026 iMeeting Team. All rights reserved.');
-- ----------------------------
-- 6. 业务模块 - 声纹管理
-- ----------------------------
DROP TABLE IF EXISTS biz_speakers CASCADE;
CREATE TABLE biz_speakers (
id BIGSERIAL PRIMARY KEY,
tenant_id BIGINT NOT NULL, -- 租户ID
creator_id BIGINT NOT NULL, -- 创建人ID用于声纹库管理归属
user_id BIGINT, -- 关联系统用户ID可为空
external_speaker_id VARCHAR(100), -- 第三方声纹库中的人员ID
name VARCHAR(100) NOT NULL, -- 发言人姓名
voice_path VARCHAR(512), -- 原始声纹文件存储路径
voice_ext VARCHAR(10), -- 文件后缀
voice_size BIGINT, -- 文件大小
status SMALLINT DEFAULT 1, -- 状态: 1=已保存, 2=注册中, 3=已注册, 4=失败
embedding VECTOR(512), -- 声纹特征向量 (预留 pgvector 字段)
remark TEXT, -- 备注
created_at TIMESTAMP(6) NOT NULL DEFAULT now(),
updated_at TIMESTAMP(6) NOT NULL DEFAULT now(),
is_deleted SMALLINT NOT NULL DEFAULT 0
);
CREATE INDEX idx_speaker_tenant ON biz_speakers (tenant_id) WHERE is_deleted = 0;
CREATE INDEX idx_speaker_creator ON biz_speakers (creator_id) WHERE is_deleted = 0;
CREATE INDEX idx_speaker_user ON biz_speakers (user_id) WHERE is_deleted = 0;
CREATE INDEX idx_speaker_external ON biz_speakers (external_speaker_id) WHERE is_deleted = 0;
CREATE UNIQUE INDEX uk_speaker_tenant_name ON biz_speakers (tenant_id, name) WHERE is_deleted = 0;
COMMENT ON TABLE biz_speakers IS '声纹发言人基础信息表 (声纹库资源)';
-- ----------------------------
-- 7. 业务模块 - 热词管理
-- ----------------------------
DROP TABLE IF EXISTS biz_hot_words CASCADE;
CREATE TABLE biz_hot_words (
id BIGSERIAL PRIMARY KEY,
tenant_id BIGINT NOT NULL, -- 租户ID (强制隔离)
word VARCHAR(100) NOT NULL, -- 热词原文
is_public SMALLINT DEFAULT 0, -- 1:租户公开, 0:个人私有
creator_id BIGINT, -- 创建者ID
pinyin_list text, -- 拼音数组(支持多音字, 如 ["i mi ting", "i mei ting"])
match_strategy SMALLINT DEFAULT 1, -- 匹配策略: 1:精确匹配, 2:拼音模糊匹配
category VARCHAR(50), -- 类别 (人名、术语、地名)
weight INTEGER DEFAULT 10, -- 权重 (1-100)
status SMALLINT DEFAULT 1, -- 状态: 1:启用, 0:禁用
is_synced SMALLINT DEFAULT 0, -- 是否已同步至第三方引擎: 0:未同步, 1:已同步
remark TEXT, -- 备注
created_at TIMESTAMP(6) NOT NULL DEFAULT now(),
updated_at TIMESTAMP(6) NOT NULL DEFAULT now(),
is_deleted SMALLINT NOT NULL DEFAULT 0
);
CREATE INDEX idx_hotword_tenant ON biz_hot_words (tenant_id);
CREATE INDEX idx_hotword_word ON biz_hot_words (word) WHERE is_deleted = 0;
COMMENT ON TABLE biz_hot_words IS '语音识别热词表';
-- ----------------------------
-- 8. 业务模块 - 提示词模板
-- ----------------------------
DROP TABLE IF EXISTS biz_prompt_templates CASCADE;
CREATE TABLE biz_prompt_templates (
id BIGSERIAL PRIMARY KEY,
tenant_id BIGINT NOT NULL DEFAULT 0, -- 租户ID (0为系统级)
template_name VARCHAR(100) NOT NULL, -- 模板名称
description VARCHAR(255), -- 模板描述
category VARCHAR(20), -- 分类 (字典: biz_prompt_category)
is_system SMALLINT DEFAULT 0, -- 是否系统预置 (1:是, 0:否)
creator_id BIGINT, -- 创建人ID
tags text, -- 标签数组 (JSONB)
usage_count INTEGER DEFAULT 0, -- 使用次数
prompt_content TEXT NOT NULL, -- 提示词内容
status SMALLINT DEFAULT 1, -- 状态: 1:启用, 0:禁用
remark VARCHAR(255), -- 备注
created_at TIMESTAMP(6) NOT NULL DEFAULT now(),
updated_at TIMESTAMP(6) NOT NULL DEFAULT now(),
is_deleted SMALLINT NOT NULL DEFAULT 0
);
CREATE INDEX idx_prompt_tenant ON biz_prompt_templates (tenant_id);
CREATE INDEX idx_prompt_system ON biz_prompt_templates (is_system) WHERE is_deleted = 0;
COMMENT ON TABLE biz_prompt_templates IS '会议总结提示词模板表';
-- ----------------------------
-- 9. 业务模块 - AI 模型管理
-- ----------------------------
DROP TABLE IF EXISTS biz_asr_models CASCADE;
CREATE TABLE biz_asr_models (
id BIGSERIAL PRIMARY KEY,
tenant_id BIGINT NOT NULL DEFAULT 0,
model_name VARCHAR(100) NOT NULL,
provider VARCHAR(50),
base_url VARCHAR(255),
api_key VARCHAR(255),
model_code VARCHAR(100),
ws_url VARCHAR(255),
media_config text,
is_default SMALLINT DEFAULT 0,
status SMALLINT DEFAULT 1,
remark VARCHAR(255),
created_at TIMESTAMP(6) NOT NULL DEFAULT now(),
updated_at TIMESTAMP(6) NOT NULL DEFAULT now(),
is_deleted SMALLINT NOT NULL DEFAULT 0
);
DROP TABLE IF EXISTS biz_llm_models CASCADE;
CREATE TABLE biz_llm_models (
id BIGSERIAL PRIMARY KEY,
tenant_id BIGINT NOT NULL DEFAULT 0,
model_name VARCHAR(100) NOT NULL,
provider VARCHAR(50),
base_url VARCHAR(255),
api_path VARCHAR(100),
api_key VARCHAR(255),
model_code VARCHAR(100),
temperature DECIMAL(3,2) DEFAULT 0.7,
top_p DECIMAL(3,2) DEFAULT 0.9,
is_default SMALLINT DEFAULT 0,
status SMALLINT DEFAULT 1,
remark VARCHAR(255),
created_at TIMESTAMP(6) NOT NULL DEFAULT now(),
updated_at TIMESTAMP(6) NOT NULL DEFAULT now(),
is_deleted SMALLINT NOT NULL DEFAULT 0
);
CREATE INDEX idx_asr_model_tenant ON biz_asr_models (tenant_id);
CREATE INDEX idx_asr_model_default ON biz_asr_models (is_default) WHERE is_deleted = 0;
CREATE INDEX idx_llm_model_tenant ON biz_llm_models (tenant_id);
CREATE INDEX idx_llm_model_default ON biz_llm_models (is_default) WHERE is_deleted = 0;
COMMENT ON TABLE biz_asr_models IS 'ASR 模型配置表';
COMMENT ON TABLE biz_llm_models IS 'LLM 模型配置表';
-- ----------------------------
-- 10. 业务模块 - 会议主表
-- ----------------------------
DROP TABLE IF EXISTS biz_meetings CASCADE;
CREATE TABLE biz_meetings (
id BIGSERIAL PRIMARY KEY,
tenant_id BIGINT NOT NULL DEFAULT 0,
title VARCHAR(200) NOT NULL,
meeting_time TIMESTAMP(6),
participants TEXT,
tags VARCHAR(255),
audio_url VARCHAR(500),
creator_id BIGINT, -- 发起人ID
creator_name VARCHAR(100), -- 发起人姓名
latest_summary_task_id BIGINT, -- 最新成功总结任务ID
status SMALLINT DEFAULT 0, -- 0:待处理, 1:处理中, 2:成功, 3:失败
created_at TIMESTAMP(6) NOT NULL DEFAULT now(),
updated_at TIMESTAMP(6) NOT NULL DEFAULT now(),
is_deleted SMALLINT NOT NULL DEFAULT 0
);
-- ----------------------------
-- 11. 业务模块 - 转录明细表
-- ----------------------------
DROP TABLE IF EXISTS biz_meeting_transcripts CASCADE;
CREATE TABLE biz_meeting_transcripts (
id BIGSERIAL PRIMARY KEY,
meeting_id BIGINT NOT NULL,
speaker_id VARCHAR(50), -- ASR返回的发言人标识
speaker_name VARCHAR(100), -- 修改后的发言人姓名
speaker_label VARCHAR(50), -- 发言人标签
content TEXT, -- 转录内容
start_time INTEGER, -- 开始时间(ms)
end_time INTEGER, -- 结束时间(ms)
sort_order INTEGER,
created_at TIMESTAMP(6) NOT NULL DEFAULT now()
);
-- ----------------------------
-- 12. 业务模块 - AI 异步任务日志表
-- ----------------------------
DROP TABLE IF EXISTS biz_ai_tasks CASCADE;
CREATE TABLE biz_ai_tasks (
id BIGSERIAL PRIMARY KEY,
meeting_id BIGINT NOT NULL,
task_type VARCHAR(20), -- ASR / SUMMARY
status SMALLINT DEFAULT 0, -- 0:排队, 1:执行中, 2:成功, 3:失败
request_data text, -- 请求三方原始JSON
response_data text, -- 三方返回原始JSON
task_config text, -- 任务配置参数快照
result_file_path VARCHAR(500), -- 结果文件路径
error_msg TEXT, -- 错误堆栈
started_at TIMESTAMP(6),
completed_at TIMESTAMP(6)
);
CREATE INDEX idx_meeting_tenant ON biz_meetings (tenant_id);
CREATE INDEX idx_transcript_meeting ON biz_meeting_transcripts (meeting_id);
CREATE INDEX idx_aitask_meeting ON biz_ai_tasks (meeting_id);
COMMENT ON TABLE biz_meetings IS '会议管理主表';
COMMENT ON TABLE biz_meeting_transcripts IS '会议转录明细表';
COMMENT ON TABLE biz_ai_tasks IS 'AI 任务流水日志表';
DROP TABLE IF EXISTS "biz_prompt_template_user_config";
CREATE TABLE "biz_prompt_template_user_config" (
"id" BIGSERIAL PRIMARY KEY,
"tenant_id" int8 NOT NULL DEFAULT 0,
"user_id" int8 NOT NULL,
"template_id" int8 NOT NULL,
"status" int2 DEFAULT 1,
"created_at" timestamp(6) NOT NULL DEFAULT now(),
"updated_at" timestamp(6) NOT NULL DEFAULT now(),
"is_deleted" int2 NOT NULL DEFAULT 0
);
-- ----------------------------
-- 5. 基础初始化数据
-- ----------------------------
@ -263,3 +482,84 @@ INSERT INTO sys_dict_item (type_code, item_label, item_value, sort_order) VALUES
INSERT INTO sys_dict_type (type_code, type_name, remark) VALUES ('sys_log_status', '操作状态', '1=成功, 0=失败');
INSERT INTO sys_dict_item (type_code, item_label, item_value, sort_order) VALUES ('sys_log_status', '成功', '1', 1);
INSERT INTO sys_dict_item (type_code, item_label, item_value, sort_order) VALUES ('sys_log_status', '失败', '0', 2);
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (21, 18, '查询租户', 'sys_tenant:query', 'button', 3, NULL, NULL, NULL, 1, 1, 1, NULL, NULL, 0, '2026-02-25 10:09:32.647439', '2026-02-25 10:09:32.647439');
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (28, 19, '删除组织', 'sys:org:delete', 'button', 3, NULL, NULL, NULL, 4, 1, 1, NULL, NULL, 0, '2026-02-25 10:09:32.65871', '2026-02-25 10:09:32.65871');
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (22, 18, '新增租户', 'sys_tenant:create', 'button', 3, NULL, NULL, NULL, 2, 1, 1, NULL, NULL, 0, '2026-02-25 10:09:32.647439', '2026-02-25 10:09:32.647439');
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (23, 18, '修改租户', 'sys_tenant:update', 'button', 3, NULL, NULL, NULL, 3, 1, 1, NULL, NULL, 0, '2026-02-25 10:09:32.647439', '2026-02-25 10:09:32.647439');
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (24, 18, '删除租户', 'sys_tenant:delete', 'button', 3, NULL, NULL, NULL, 4, 1, 1, NULL, NULL, 0, '2026-02-25 10:09:32.647439', '2026-02-25 10:09:32.647439');
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (48, 3, '权限查询', 'sys:permission:list', 'button', 3, NULL, NULL, NULL, 0, 1, 1, NULL, NULL, 0, '2026-02-26 16:07:04.002702', '2026-02-26 16:07:04.003701');
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (13, 12, '角色权限', 'menu:role:permission', 'menu', 2, '/role-permissions', NULL, NULL, 0, 1, 1, NULL, NULL, 1, '2026-02-10 18:01:32.999774', '2026-02-11 09:41:31.952294');
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (39, 12, '平台管理', 'platform', 'menu', 2, '/platform-settings', NULL, NULL, 0, 1, 1, NULL, NULL, 0, '2026-02-26 14:54:49.406968', '2026-02-26 14:54:49.407968');
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (25, 19, '查询组织', 'sys:org:query', 'button', 3, NULL, NULL, NULL, 1, 1, 1, NULL, NULL, 0, '2026-02-25 10:09:32.65871', '2026-02-25 10:09:32.65871');
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (26, 19, '新增组织', 'sys:org:create', 'button', 3, NULL, NULL, NULL, 2, 1, 1, NULL, NULL, 0, '2026-02-25 10:09:32.65871', '2026-02-25 10:09:32.65871');
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (27, 19, '修改组织', 'sys:org:update', 'button', 3, NULL, NULL, NULL, 3, 1, 1, NULL, NULL, 0, '2026-02-25 10:09:32.65871', '2026-02-25 10:09:32.65871');
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (3, 12, '角色管理', 'sys:role:list', 'menu', 2, '/roles', NULL, NULL, 3, 1, 1, '角色管理菜单', NULL, 0, '2026-02-10 07:24:30.148186', '2026-02-10 17:24:07.484806');
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (40, 3, '角色查询', 'sys:role:query', 'button', 3, NULL, NULL, NULL, 0, 1, 1, NULL, NULL, 0, '2026-02-26 15:35:35.692367', '2026-02-26 15:35:35.693366');
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (41, 3, '角色创建', 'sys:role:create', 'button', 3, NULL, NULL, NULL, 0, 1, 1, NULL, NULL, 0, '2026-02-26 15:35:50.081581', '2026-02-26 15:35:50.081581');
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (42, 3, '角色更新', 'sys:role:update', 'button', 3, NULL, NULL, NULL, 0, 1, 1, NULL, NULL, 0, '2026-02-26 15:36:03.385343', '2026-02-26 15:36:03.385343');
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (43, 3, '角色删除', 'sys:role:delete', 'button', 3, NULL, NULL, NULL, 0, 1, 1, NULL, NULL, 0, '2026-02-26 15:36:20.451039', '2026-02-26 15:36:20.451039');
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (44, 3, '角色权限列表', 'sys:role:permission:list', 'button', 3, NULL, NULL, NULL, 0, 1, 1, NULL, NULL, 0, '2026-02-26 15:36:39.902216', '2026-02-26 15:36:39.902216');
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (45, 3, '角色权限更新', 'sys:role:permission:save', 'button', 3, NULL, NULL, NULL, 0, 1, 1, NULL, NULL, 0, '2026-02-26 15:36:53.595974', '2026-02-26 15:36:53.595974');
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (2, 12, '用户管理', 'sys:user:list', 'menu', 2, '/users', NULL, NULL, 2, 1, 1, '用户管理菜单', NULL, 0, '2026-02-10 07:24:30.148186', '2026-02-26 15:43:21.037142');
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (29, 2, '查询用户', 'sys:user:query', 'button', 3, NULL, NULL, NULL, 1, 1, 1, NULL, NULL, 0, '2026-02-25 10:09:32.669947', '2026-02-25 10:09:32.669947');
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (30, 2, '新增用户', 'sys:user:create', 'button', 3, NULL, NULL, NULL, 2, 1, 1, NULL, NULL, 0, '2026-02-25 10:09:32.669947', '2026-02-25 10:09:32.669947');
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (31, 2, '修改用户', 'sys:user:update', 'button', 3, NULL, NULL, NULL, 3, 1, 1, NULL, NULL, 0, '2026-02-25 10:09:32.669947', '2026-02-25 10:09:32.669947');
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (32, 2, '删除用户', 'sys:user:delete', 'button', 3, NULL, NULL, NULL, 4, 1, 1, NULL, NULL, 0, '2026-02-25 10:09:32.669947', '2026-02-25 10:09:32.669947');
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (33, 2, '分配角色', 'sys:user:role:save', 'button', 3, NULL, NULL, NULL, 5, 1, 1, NULL, NULL, 0, '2026-02-25 10:09:32.669947', '2026-02-25 10:09:32.669947');
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (46, 2, '用户角色查询', 'sys:user:role:list', 'button', 3, NULL, NULL, NULL, 0, 1, 1, NULL, NULL, 0, '2026-02-26 15:45:14.890567', '2026-02-26 15:45:28.553231');
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (54, NULL, '热词管理', 'menu:hotword', 'menu', 1, '/hotwords', NULL, 'hotword', 11, 1, 1, NULL, NULL, 0, '2026-02-28 16:51:49.158997', '2026-02-28 16:51:49.158997');
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (55, NULL, '总结模板', 'menu:prompt', 'menu', 1, '/prompts', NULL, 'prompt', 12, 1, 1, NULL, NULL, 0, '2026-02-28 17:47:51.015282', '2026-02-28 17:47:51.015282');
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (56, NULL, '模型配置', 'menu:aimodel', 'menu', 1, '/aimodels', NULL, 'aimodel', 13, 1, 1, NULL, NULL, 0, '2026-03-02 09:48:27.179055', '2026-03-02 09:48:27.179055');
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (57, NULL, '会议中心', 'menu:meeting', 'menu', 1, '/meetings', NULL, 'meeting', 20, 1, 1, NULL, NULL, 0, '2026-03-02 11:02:58.089065', '2026-03-02 11:02:58.089065');
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (52, NULL, '测试菜单', 'test', 'directory', 1, '/role-permissions', NULL, NULL, 0, 1, 1, NULL, NULL, 1, '2026-02-27 10:39:04.576329', '2026-03-03 10:03:45.999369');
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (59, NULL, '声纹注册', 'speaker', 'menu', 1, '/speaker-reg', NULL, NULL, 0, 1, 1, NULL, NULL, 0, '2026-03-06 15:23:09.314321', '2026-03-06 15:23:51.715481');
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (18, 12, '租户管理', 'menu:tenant', 'menu', 2, '/tenants', NULL, NULL, 1, 1, 1, NULL, NULL, 0, '2026-02-12 14:06:13.672548', '2026-03-06 16:31:45.006699');
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (58, NULL, '发起会议', 'menu:meeting:create', 'menu', 1, '/meeting-create', NULL, 'audio', 19, 1, 1, NULL, NULL, 1, '2026-03-02 16:21:47.326202', '2026-03-05 09:05:49.301092');
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (4, 12, '菜单管理', 'sys:permission:list', 'menu', 2, '/permissions', NULL, NULL, 4, 1, 1, '权限管理菜单', NULL, 0, '2026-02-10 07:24:30.148186', '2026-03-05 17:10:30.891258');
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (5, 12, '设备管理', 'menu:devices', 'menu', 2, '/devices', NULL, NULL, 5, 0, 1, '设备管理菜单', NULL, 0, '2026-02-10 07:24:30.148186', '2026-03-05 17:11:48.867451');
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (15, 12, '字典管理', 'menu:dict', 'menu', 2, '/dictionaries', NULL, NULL, 1, 1, 1, NULL, NULL, 0, '2026-02-11 13:54:56.100838', '2026-03-05 17:12:28.223844');
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (17, 12, '日志管理', 'menu:log', 'menu', 2, '/logs', NULL, NULL, 1, 1, 1, NULL, NULL, 0, '2026-02-12 09:49:02.814427', '2026-03-05 17:12:57.94561');
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (38, 12, '参数管理', 'params', 'menu', 2, '/params', NULL, NULL, 2, 1, 1, NULL, NULL, 0, '2026-02-26 14:34:16.903552', '2026-03-06 16:31:51.714937');
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (1, NULL, '任务监控', 'menu:dashboard', 'menu', 1, '/', NULL, NULL, 1, 1, 1, 'Dashboard 菜单', NULL, 0, '2026-02-10 07:24:30.148186', '2026-03-05 18:01:31.515477');
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (19, 12, '组织管理', 'sys:org:list', 'menu', 2, '/orgs', NULL, NULL, 3, 1, 1, NULL, NULL, 0, '2026-02-12 14:09:01.818807', '2026-03-06 16:32:00.114277');
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (12, NULL, '系统管理', 'system', 'directory', 1, NULL, NULL, NULL, 110, 1, 1, NULL, NULL, 0, '2026-02-10 17:23:52.877017', '2026-03-06 14:00:20.182181');
INSERT INTO sys_param ("param_id", "param_key", "param_value", "param_type", "status", "is_system", "description", "is_deleted", "created_at", "updated_at") VALUES (2, 'security.token.refresh_ttl_days', '7', 'int', 1, 1, 'Refresh Token 有效期(天)', 0, '2026-02-09 09:54:21.893832', '2026-02-09 09:54:21.893832');
INSERT INTO sys_param ("param_id", "param_key", "param_value", "param_type", "status", "is_system", "description", "is_deleted", "created_at", "updated_at") VALUES (4, 'tenant.init.default.menu.codes', 'sys:user:list,sys:user:create,sys:user:query,sys:role:create,sys:user:role:save,sys:org:delete,sys:org:query,sys:role:permission:list,sys:org:update,sys:role:permission:save,sys:role:update,system,sys:user:delete,sys:user:role:list,sys:org:list,sys:role:delete,sys:role:list,sys:org:create,sys:user:update,sys:permission:list,sys:role:query', 'String', 1, 1, '新建租户时角色权限', 0, '2026-02-26 16:46:20.392789', '2026-02-26 16:46:38.137264');
INSERT INTO sys_param ("param_id", "param_key", "param_value", "param_type", "status", "is_system", "description", "is_deleted", "created_at", "updated_at") VALUES (5, 'tenant.init.default.password', '123456', 'String', 1, 1, NULL, 0, '2026-02-26 16:46:52.124755', '2026-02-26 16:46:52.124755');
INSERT INTO sys_param ("param_id", "param_key", "param_value", "param_type", "status", "is_system", "description", "is_deleted", "created_at", "updated_at") VALUES (3, 'security.captcha.enabled', 'false', 'boolean', 1, 1, '是否开启验证码', 0, '2026-02-11 02:45:31.097324', '2026-03-10 09:40:33.084368');
INSERT INTO sys_param ("param_id", "param_key", "param_value", "param_type", "status", "is_system", "description", "is_deleted", "created_at", "updated_at") VALUES (1, 'security.token.access_ttl_minutes', '120', 'int', 1, 1, 'Access Token 有效期(分钟)', 0, '2026-02-09 09:54:21.888052', '2026-03-10 10:15:39.55035');
INSERT INTO sys_user ( "username", "display_name", "email", "phone", "password_hash", "status", "is_deleted", "created_at", "updated_at", "is_platform_admin", "pwd_reset_required")
VALUES ( 'admin', '管理员', 'admin', NULL, '$2a$10$BOm1iCFj3ObfBeyQxOvjVO659vXvIRGOd4YR62r0TUHqSusWW5bFS', 1, 0, '2026-02-09 09:54:21.880637', '2026-02-28 17:57:32.63338', 't', NULL);
INSERT INTO "sys_dict_type" ("dict_type_id", "type_code", "type_name", "status", "remark", "created_at", "updated_at") VALUES (9, 'biz_hotword_category', '热词类别', 1, '语音识别纠错分类', '2026-02-28 17:08:52.362532', '2026-02-28 17:08:52.362532');
INSERT INTO "sys_dict_type" ("dict_type_id", "type_code", "type_name", "status", "remark", "created_at", "updated_at") VALUES (10, 'biz_prompt_category', '提示词分类', 1, '会议总结模板分类', '2026-02-28 17:47:50.999655', '2026-02-28 17:47:50.999655');
INSERT INTO "sys_dict_type" ("dict_type_id", "type_code", "type_name", "status", "remark", "created_at", "updated_at") VALUES (11, 'biz_ai_provider', '模型提供商', 1, 'AI 模型服务商分类', '2026-03-02 10:10:16.653182', '2026-03-02 10:10:16.653182');
INSERT INTO "sys_dict_type" ("dict_type_id", "type_code", "type_name", "status", "remark", "created_at", "updated_at") VALUES (12, 'biz_speaker_label', '发言人角色', 1, '会议发言人的身份标签', '2026-03-02 16:15:58.193117', '2026-03-02 16:15:58.193117');
INSERT INTO "sys_dict_type" ("dict_type_id", "type_code", "type_name", "status", "remark", "created_at", "updated_at") VALUES (13, 'biz_prompt_level', '提示词模板属性', 1, '用于定义提示词模板的层级属性1-预置模板系统或租户级0-个人模板', '2026-03-04 10:54:30.49116', '2026-03-04 10:54:30.49116');
INSERT INTO "sys_dict_item" ("dict_item_id", "type_code", "item_label", "item_value", "sort_order", "status", "remark", "created_at", "updated_at") VALUES (22, 'biz_hotword_category', '人名', 'person', 1, 1, NULL, '2026-02-28 17:08:52.374667', '2026-02-28 17:08:52.374667');
INSERT INTO "sys_dict_item" ("dict_item_id", "type_code", "item_label", "item_value", "sort_order", "status", "remark", "created_at", "updated_at") VALUES (23, 'biz_hotword_category', '术语', 'term', 2, 1, NULL, '2026-02-28 17:08:52.374667', '2026-02-28 17:08:52.374667');
INSERT INTO "sys_dict_item" ("dict_item_id", "type_code", "item_label", "item_value", "sort_order", "status", "remark", "created_at", "updated_at") VALUES (24, 'biz_hotword_category', '地名', 'location', 3, 1, NULL, '2026-02-28 17:08:52.374667', '2026-02-28 17:08:52.374667');
INSERT INTO "sys_dict_item" ("dict_item_id", "type_code", "item_label", "item_value", "sort_order", "status", "remark", "created_at", "updated_at") VALUES (25, 'biz_hotword_category', '通用', 'general', 4, 1, NULL, '2026-02-28 17:08:52.374667', '2026-02-28 17:08:52.374667');
INSERT INTO "sys_dict_item" ("dict_item_id", "type_code", "item_label", "item_value", "sort_order", "status", "remark", "created_at", "updated_at") VALUES (26, 'biz_prompt_category', '全文纪要', 'summary', 1, 1, NULL, '2026-02-28 17:47:51.013288', '2026-02-28 17:47:51.013288');
INSERT INTO "sys_dict_item" ("dict_item_id", "type_code", "item_label", "item_value", "sort_order", "status", "remark", "created_at", "updated_at") VALUES (27, 'biz_prompt_category', '待办提取', 'todo', 2, 1, NULL, '2026-02-28 17:47:51.013288', '2026-02-28 17:47:51.013288');
INSERT INTO "sys_dict_item" ("dict_item_id", "type_code", "item_label", "item_value", "sort_order", "status", "remark", "created_at", "updated_at") VALUES (28, 'biz_prompt_category', '访谈整理', 'interview', 3, 1, NULL, '2026-02-28 17:47:51.013288', '2026-02-28 17:47:51.013288');
INSERT INTO "sys_dict_item" ("dict_item_id", "type_code", "item_label", "item_value", "sort_order", "status", "remark", "created_at", "updated_at") VALUES (29, 'biz_prompt_category', '创意构思', 'creative', 4, 1, NULL, '2026-02-28 17:47:51.013288', '2026-02-28 17:47:51.013288');
INSERT INTO "sys_dict_item" ("dict_item_id", "type_code", "item_label", "item_value", "sort_order", "status", "remark", "created_at", "updated_at") VALUES (30, 'biz_ai_provider', '阿里云', 'Aliyun', 1, 1, NULL, '2026-03-02 10:10:16.665646', '2026-03-02 10:10:16.665646');
INSERT INTO "sys_dict_item" ("dict_item_id", "type_code", "item_label", "item_value", "sort_order", "status", "remark", "created_at", "updated_at") VALUES (31, 'biz_ai_provider', 'OpenAI', 'OpenAI', 2, 1, NULL, '2026-03-02 10:10:16.665646', '2026-03-02 10:10:16.665646');
INSERT INTO "sys_dict_item" ("dict_item_id", "type_code", "item_label", "item_value", "sort_order", "status", "remark", "created_at", "updated_at") VALUES (32, 'biz_ai_provider', 'Gemini', 'Gemini', 3, 1, NULL, '2026-03-02 10:10:16.665646', '2026-03-02 10:10:16.665646');
INSERT INTO "sys_dict_item" ("dict_item_id", "type_code", "item_label", "item_value", "sort_order", "status", "remark", "created_at", "updated_at") VALUES (33, 'biz_ai_provider', 'DeepSeek', 'DeepSeek', 4, 1, NULL, '2026-03-02 10:10:16.665646', '2026-03-02 10:10:16.665646');
INSERT INTO "sys_dict_item" ("dict_item_id", "type_code", "item_label", "item_value", "sort_order", "status", "remark", "created_at", "updated_at") VALUES (34, 'biz_ai_provider', 'Kimi', 'Kimi', 5, 1, NULL, '2026-03-02 10:10:16.665646', '2026-03-02 10:10:16.665646');
INSERT INTO "sys_dict_item" ("dict_item_id", "type_code", "item_label", "item_value", "sort_order", "status", "remark", "created_at", "updated_at") VALUES (35, 'biz_ai_provider', '自定义/本地', 'Custom', 6, 1, NULL, '2026-03-02 10:10:16.665646', '2026-03-02 10:10:16.665646');
INSERT INTO "sys_dict_item" ("dict_item_id", "type_code", "item_label", "item_value", "sort_order", "status", "remark", "created_at", "updated_at") VALUES (36, 'biz_speaker_label', '主持人', 'host', 1, 1, NULL, '2026-03-02 16:15:58.205277', '2026-03-02 16:15:58.205277');
INSERT INTO "sys_dict_item" ("dict_item_id", "type_code", "item_label", "item_value", "sort_order", "status", "remark", "created_at", "updated_at") VALUES (37, 'biz_speaker_label', '汇报人', 'speaker', 2, 1, NULL, '2026-03-02 16:15:58.205277', '2026-03-02 16:15:58.205277');
INSERT INTO "sys_dict_item" ("dict_item_id", "type_code", "item_label", "item_value", "sort_order", "status", "remark", "created_at", "updated_at") VALUES (38, 'biz_speaker_label', '技术专家', 'expert', 3, 1, NULL, '2026-03-02 16:15:58.205277', '2026-03-02 16:15:58.205277');
INSERT INTO "sys_dict_item" ("dict_item_id", "type_code", "item_label", "item_value", "sort_order", "status", "remark", "created_at", "updated_at") VALUES (39, 'biz_speaker_label', '客户代表', 'customer', 4, 1, NULL, '2026-03-02 16:15:58.205277', '2026-03-02 16:15:58.205277');
INSERT INTO "sys_dict_item" ("dict_item_id", "type_code", "item_label", "item_value", "sort_order", "status", "remark", "created_at", "updated_at") VALUES (40, 'biz_prompt_level', '预置模板', '1', 1, 1, '平台系统预置或租户共享预置', '2026-03-04 10:55:42.163768', '2026-03-04 10:55:42.163768');
INSERT INTO "sys_dict_item" ("dict_item_id", "type_code", "item_label", "item_value", "sort_order", "status", "remark", "created_at", "updated_at") VALUES (41, 'biz_prompt_level', '个人模板', '0', 2, 1, '个人私有模板', '2026-03-04 10:55:42.175269', '2026-03-04 10:55:42.175269');

View File

@ -1,4 +1,4 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
@ -20,6 +20,10 @@
<mybatis-plus.version>3.5.6</mybatis-plus.version>
<jjwt.version>0.11.5</jjwt.version>
<easycaptcha.version>1.6.2</easycaptcha.version>
<grpc.version>1.76.1</grpc.version>
<protobuf.version>3.25.8</protobuf.version>
<protobuf.plugin.version>0.6.1</protobuf.plugin.version>
<os.maven.plugin.version>1.7.1</os.maven.plugin.version>
</properties>
<dependencies>
@ -27,6 +31,10 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
@ -79,6 +87,31 @@
<artifactId>easy-captcha</artifactId>
<version>${easycaptcha.version}</version>
</dependency>
<dependency>
<groupId>com.belerweb</groupId>
<artifactId>pinyin4j</artifactId>
<version>2.5.1</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-netty-shaded</artifactId>
<version>${grpc.version}</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-protobuf</artifactId>
<version>${grpc.version}</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-stub</artifactId>
<version>${grpc.version}</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-services</artifactId>
<version>${grpc.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
@ -89,10 +122,79 @@
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.pdfbox</groupId>
<artifactId>pdfbox</artifactId>
<version>2.0.30</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>5.2.5</version>
</dependency>
<dependency>
<groupId>org.commonmark</groupId>
<artifactId>commonmark</artifactId>
<version>0.21.0</version>
</dependency>
<dependency>
<groupId>com.openhtmltopdf</groupId>
<artifactId>openhtmltopdf-core</artifactId>
<version>1.0.10</version>
</dependency>
<dependency>
<groupId>com.openhtmltopdf</groupId>
<artifactId>openhtmltopdf-pdfbox</artifactId>
<version>1.0.10</version>
</dependency>
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.17.2</version>
</dependency>
<dependency>
<groupId>com.unisbase</groupId>
<artifactId>unisbase-spring-boot-starter</artifactId>
<version>0.1.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.3.0</version>
</dependency>
</dependencies>
<build>
<extensions>
<extension>
<groupId>kr.motd.maven</groupId>
<artifactId>os-maven-plugin</artifactId>
<version>${os.maven.plugin.version}</version>
</extension>
</extensions>
<plugins>
<plugin>
<groupId>org.xolstice.maven.plugins</groupId>
<artifactId>protobuf-maven-plugin</artifactId>
<version>${protobuf.plugin.version}</version>
<configuration>
<protocArtifact>com.google.protobuf:protoc:${protobuf.version}:exe:${os.detected.classifier}</protocArtifact>
<pluginId>grpc-java</pluginId>
<pluginArtifact>io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier}</pluginArtifact>
</configuration>
<executions>
<execution>
<goals>
<goal>compile</goal>
<goal>compile-custom</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>

View File

@ -1,167 +0,0 @@
package com.imeeting.auth;
import com.imeeting.common.RedisKeys;
import com.imeeting.entity.SysTenant;
import com.imeeting.entity.SysUser;
import com.imeeting.security.LoginUser;
import com.imeeting.service.AuthScopeService;
import com.imeeting.service.AuthVersionService;
import com.imeeting.service.SysParamService;
import com.imeeting.service.SysPermissionService;
import com.imeeting.mapper.SysTenantMapper;
import com.imeeting.mapper.SysUserMapper;
import io.jsonwebtoken.Claims;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.context.annotation.Lazy;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.Set;
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtTokenProvider jwtTokenProvider;
private final SysPermissionService sysPermissionService;
private final SysTenantMapper sysTenantMapper;
private final SysUserMapper sysUserMapper;
private final SysParamService sysParamService;
private final StringRedisTemplate redisTemplate;
private final AuthScopeService authScopeService;
private final AuthVersionService authVersionService;
public JwtAuthenticationFilter(JwtTokenProvider jwtTokenProvider,
@Lazy SysPermissionService sysPermissionService,
SysTenantMapper sysTenantMapper,
SysUserMapper sysUserMapper,
@Lazy SysParamService sysParamService,
StringRedisTemplate redisTemplate,
AuthScopeService authScopeService,
AuthVersionService authVersionService) {
this.jwtTokenProvider = jwtTokenProvider;
this.sysPermissionService = sysPermissionService;
this.sysTenantMapper = sysTenantMapper;
this.sysUserMapper = sysUserMapper;
this.sysParamService = sysParamService;
this.redisTemplate = redisTemplate;
this.authScopeService = authScopeService;
this.authVersionService = authVersionService;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String uri = request.getRequestURI();
// Skip filter for public endpoints
if (uri.startsWith("/auth/") || uri.equals("/api/params/value")) {
filterChain.doFilter(request, response);
return;
}
String authHeader = request.getHeader("Authorization");
if (authHeader != null && authHeader.startsWith("Bearer ")) {
String token = authHeader.substring(7);
try {
Claims claims = jwtTokenProvider.parseToken(token);
String username = claims.get("username", String.class);
Long userId = claims.get("userId", Long.class);
Long tenantId = claims.get("tenantId", Long.class);
Number tokenAuthVersionNum = claims.get("authVersion", Number.class);
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
// 1. Validate User Status (Ignore Tenant isolation here)
SysUser user = sysUserMapper.selectByIdIgnoreTenant(userId);
if (user == null || user.getStatus() != 1 || user.getIsDeleted() != 0) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":\"401\",\"msg\":\"User account is disabled or deleted\"}");
return;
}
// 2. Validate Tenant Status & Grace Period
// Skip validation for system platform tenant (ID=0)
Long activeTenantId = tenantId;
if (activeTenantId != null && !Long.valueOf(0).equals(activeTenantId)) {
SysTenant tenant = sysTenantMapper.selectByIdIgnoreTenant(activeTenantId);
if (tenant == null || tenant.getStatus() != 1) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":\"401\",\"msg\":\"Tenant is disabled\"}");
return;
}
if (tenant.getExpireTime() != null) {
LocalDateTime now = LocalDateTime.now();
if (now.isAfter(tenant.getExpireTime())) {
String graceDaysStr = sysParamService.getParamValue("sys.tenant.grace_period_days", "0");
int graceDays = Integer.parseInt(graceDaysStr);
if (now.isAfter(tenant.getExpireTime().plusDays(graceDays))) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":\"401\",\"msg\":\"Tenant subscription expired\"}");
return;
}
}
}
}
long currentAuthVersion = authVersionService.getVersion(userId, activeTenantId);
long requestAuthVersion = tokenAuthVersionNum == null ? 0L : tokenAuthVersionNum.longValue();
if (currentAuthVersion != requestAuthVersion) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":\"401\",\"msg\":\"Token revoked\"}");
return;
}
// 3. Get Permissions (With Redis Cache, Key must include tenantId)
String permKey = RedisKeys.authPermKey(userId, activeTenantId, currentAuthVersion);
Set<String> permissions;
String cachedPerms = redisTemplate.opsForValue().get(permKey);
if (cachedPerms != null && !cachedPerms.trim().isEmpty()) {
permissions = Set.of(cachedPerms.split(","));
} else {
permissions = sysPermissionService.listPermissionCodesByUserId(userId, activeTenantId);
if (permissions != null && !permissions.isEmpty()) {
redisTemplate.opsForValue().set(permKey, String.join(",", permissions), java.time.Duration.ofHours(2));
} else {
permissions = Collections.emptySet();
}
}
boolean isTenantAdmin = authScopeService.isTenantAdmin(userId, activeTenantId);
LoginUser loginUser = new LoginUser(userId, activeTenantId, username, user.getIsPlatformAdmin(), isTenantAdmin, permissions);
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (io.jsonwebtoken.ExpiredJwtException e) {
SecurityContextHolder.clearContext();
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":\"401\",\"msg\":\"Token expired\"}");
return;
} catch (io.jsonwebtoken.JwtException e) {
SecurityContextHolder.clearContext();
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":\"401\",\"msg\":\"Invalid token\"}");
return;
} catch (Exception ignored) {
SecurityContextHolder.clearContext();
}
}
filterChain.doFilter(request, response);
}
}

View File

@ -1,37 +0,0 @@
package com.imeeting.auth;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.nio.charset.StandardCharsets;
import java.security.Key;
import java.util.Date;
import java.util.Map;
@Component
public class JwtTokenProvider {
private final Key key;
public JwtTokenProvider(@Value("${security.jwt.secret}") String secret) {
this.key = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
}
public String createToken(Map<String, Object> claims, long ttlMillis) {
Date now = new Date();
Date exp = new Date(now.getTime() + ttlMillis);
return Jwts.builder()
.setClaims(claims)
.setIssuedAt(now)
.setExpiration(exp)
.signWith(key, SignatureAlgorithm.HS256)
.compact();
}
public Claims parseToken(String token) {
return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
}
}

View File

@ -1,11 +0,0 @@
package com.imeeting.auth.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
public class CaptchaResponse {
private String captchaId;
private String imageBase64;
}

View File

@ -1,15 +0,0 @@
package com.imeeting.auth.dto;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
@Data
public class DeviceCodeRequest {
@NotBlank
private String username;
@NotBlank
private String password;
private String captchaId;
private String captchaCode;
private String deviceName;
}

View File

@ -1,16 +0,0 @@
package com.imeeting.auth.dto;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
@Data
public class LoginRequest {
private String tenantCode;
@NotBlank
private String username;
@NotBlank
private String password;
private String captchaId;
private String captchaCode;
private String deviceCode;
}

View File

@ -1,10 +0,0 @@
package com.imeeting.auth.dto;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
@Data
public class RefreshRequest {
@NotBlank
private String refreshToken;
}

View File

@ -1,23 +0,0 @@
package com.imeeting.auth.dto;
import lombok.Builder;
import lombok.Data;
import java.util.List;
@Data
@Builder
public class TokenResponse {
private String accessToken;
private String refreshToken;
private long accessExpiresInMinutes;
private long refreshExpiresInDays;
private List<TenantInfo> availableTenants;
@Data
@Builder
public static class TenantInfo {
private Long tenantId;
private String tenantCode;
private String tenantName;
}
}

View File

@ -1,22 +0,0 @@
package com.imeeting.common;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ApiResponse<T> {
private String code;
private String msg;
private T data;
public static <T> ApiResponse<T> ok(T data) {
return new ApiResponse<>("0", "OK", data);
}
public static <T> ApiResponse<T> error(String msg) {
return new ApiResponse<>("-1", msg, null);
}
}

View File

@ -1,29 +0,0 @@
package com.imeeting.common;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class GlobalExceptionHandler {
private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
@ExceptionHandler(IllegalArgumentException.class)
public ApiResponse<Void> handleIllegalArgument(IllegalArgumentException ex) {
log.warn("Business error: {}", ex.getMessage());
return ApiResponse.error(ex.getMessage());
}
@ExceptionHandler(org.springframework.security.access.AccessDeniedException.class)
public ApiResponse<Void> handleAccessDenied(org.springframework.security.access.AccessDeniedException ex) {
log.warn("Access denied: {}", ex.getMessage());
return ApiResponse.error("无权限操作");
}
@ExceptionHandler(Exception.class)
public ApiResponse<Void> handleGeneric(Exception ex) {
log.error("Unhandled exception", ex);
return ApiResponse.error("系统异常");
}
}

View File

@ -1,9 +0,0 @@
package com.imeeting.common;
import lombok.Data;
@Data
public class PageResult<T> {
private long total;
private T records;
}

View File

@ -35,6 +35,78 @@ public final class RedisKeys {
return "sys:platform:config";
}
public static String meetingProgressKey(Long meetingId) {
return "biz:meeting:progress:" + meetingId;
}
public static String meetingPollingLockKey(Long meetingId) {
return "biz:meeting:polling:lock:" + meetingId;
}
public static String realtimeMeetingSocketSessionKey(String sessionToken) {
return "biz:meeting:realtime:socket:" + sessionToken;
}
public static String realtimeMeetingSessionStateKey(Long meetingId) {
return "biz:meeting:realtime:state:" + meetingId;
}
public static String realtimeMeetingResumeTimeoutKey(Long meetingId) {
return realtimeMeetingResumeTimeoutPrefix() + meetingId;
}
public static String realtimeMeetingEmptyTimeoutKey(Long meetingId) {
return realtimeMeetingEmptyTimeoutPrefix() + meetingId;
}
public static String realtimeMeetingTimeoutLockKey(Long meetingId) {
return "biz:meeting:realtime:timeout:lock:" + meetingId;
}
public static String realtimeMeetingResumeTimeoutPrefix() {
return "biz:meeting:realtime:resume-timeout:";
}
public static String realtimeMeetingEmptyTimeoutPrefix() {
return "biz:meeting:realtime:empty-timeout:";
}
public static String androidDeviceOnlineKey(String deviceId) {
return "biz:android:device:online:" + deviceId;
}
public static String androidDeviceActiveConnectionKey(String deviceId) {
return "biz:android:device:active-conn:" + deviceId;
}
public static String androidDeviceConnectionKey(String connectionId) {
return "biz:android:device:conn:" + connectionId;
}
public static String androidDeviceTopicsKey(String deviceId) {
return "biz:android:device:topics:" + deviceId;
}
public static String androidDeviceOutboxKey(String deviceId) {
return "biz:android:device:outbox:" + deviceId;
}
public static String androidDeviceMessageSeqKey(String deviceId) {
return "biz:android:device:message-seq:" + deviceId;
}
public static String realtimeMeetingGrpcSessionKey(String streamToken) {
return "biz:meeting:realtime:grpc-session:" + streamToken;
}
public static String realtimeMeetingGrpcConnectionKey(String connectionId) {
return "biz:meeting:realtime:grpc-conn:" + connectionId;
}
public static String realtimeMeetingEventSeqKey(Long meetingId) {
return "biz:meeting:realtime:event-seq:" + meetingId;
}
public static final String CACHE_EMPTY_MARKER = "EMPTY_MARKER";
public static final String SYS_PARAM_FIELD_VALUE = "value";
public static final String SYS_PARAM_FIELD_TYPE = "type";

View File

@ -4,4 +4,5 @@ public final class SysParamKeys {
private SysParamKeys() {}
public static final String CAPTCHA_ENABLED = "security.captcha.enabled";
public static final String MEETING_SUMMARY_SYSTEM_PROMPT = "meeting.summary.system_prompt";
}

View File

@ -1,11 +0,0 @@
package com.imeeting.common.annotation;
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Log {
String value() default ""; // 操作描述
String type() default ""; // 资源类型/模块名
}

View File

@ -1,111 +0,0 @@
package com.imeeting.common.aspect;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.imeeting.common.annotation.Log;
import com.imeeting.entity.SysLog;
import com.imeeting.security.LoginUser;
import com.imeeting.service.SysLogService;
import jakarta.servlet.http.HttpServletRequest;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import java.lang.reflect.Method;
import java.time.LocalDateTime;
@Aspect
@Component
public class LogAspect {
private final SysLogService sysLogService;
private final ObjectMapper objectMapper;
public LogAspect(SysLogService sysLogService, ObjectMapper objectMapper) {
this.sysLogService = sysLogService;
this.objectMapper = objectMapper;
}
@Around("@annotation(com.imeeting.common.annotation.Log)")
public Object around(ProceedingJoinPoint point) throws Throwable {
long start = System.currentTimeMillis();
Object result = null;
Exception exception = null;
try {
result = point.proceed();
return result;
} catch (Exception e) {
exception = e;
throw e;
} finally {
saveLog(point, result, exception, System.currentTimeMillis() - start);
}
}
private void saveLog(ProceedingJoinPoint joinPoint, Object result, Exception e, long duration) {
try {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes == null) return;
HttpServletRequest request = attributes.getRequest();
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
Log logAnnotation = method.getAnnotation(Log.class);
SysLog sysLog = new SysLog();
sysLog.setLogType("OPERATION");
sysLog.setOperation(logAnnotation.value());
sysLog.setMethod(request.getMethod() + " " + request.getRequestURI());
sysLog.setDuration(duration);
sysLog.setIp(request.getRemoteAddr());
sysLog.setCreatedAt(LocalDateTime.now());
// 仅保留请求参数,移除响应结果
sysLog.setParams(getArgsJson(joinPoint));
// 获取当前租户和用户信息
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null && auth.getPrincipal() instanceof LoginUser) {
LoginUser user = (LoginUser) auth.getPrincipal();
sysLog.setUserId(user.getUserId());
sysLog.setTenantId(user.getTenantId());
sysLog.setUsername(user.getUsername());
}
sysLog.setStatus(e != null ? 0 : 1);
sysLogService.recordLog(sysLog);
} catch (Exception ex) {
ex.printStackTrace();
}
}
private String getArgsJson(ProceedingJoinPoint joinPoint) {
try {
Object[] args = joinPoint.getArgs();
if (args == null || args.length == 0) return null;
StringBuilder sb = new StringBuilder();
for (Object arg : args) {
if (arg instanceof jakarta.servlet.ServletRequest
|| arg instanceof jakarta.servlet.ServletResponse
|| arg instanceof org.springframework.web.multipart.MultipartFile) {
continue;
}
try {
sb.append(objectMapper.writeValueAsString(arg)).append(" ");
} catch (Exception e) {
sb.append("[Unserializable Argument] ");
}
}
return sb.toString().trim();
} catch (Exception e) {
return "[Error capturing params]";
}
}
}

View File

@ -0,0 +1,35 @@
package com.imeeting.config;
import com.unisbase.common.ApiResponse;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
@RestControllerAdvice
public class ApiResponseSuccessCodeAdvice implements ResponseBodyAdvice<Object> {
private static final String LEGACY_SUCCESS_CODE = "0";
private static final String SUCCESS_CODE = "200";
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
return true;
}
@Override
public Object beforeBodyWrite(Object body,
MethodParameter returnType,
MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> selectedConverterType,
ServerHttpRequest request,
ServerHttpResponse response) {
if (body instanceof ApiResponse<?> apiResponse && LEGACY_SUCCESS_CODE.equals(apiResponse.getCode())) {
apiResponse.setCode(SUCCESS_CODE);
}
return body;
}
}

View File

@ -1,29 +0,0 @@
package com.imeeting.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.time.Duration;
@Configuration
public class CacheConfig {
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofHours(1)) // Default TTL
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()))
.disableCachingNullValues();
return RedisCacheManager.builder(connectionFactory)
.cacheDefaults(config)
.build();
}
}

View File

@ -1,82 +0,0 @@
package com.imeeting.config;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
import com.imeeting.security.LoginUser;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.LongValue;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import java.time.LocalDateTime;
import java.util.List;
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(new TenantLineHandler() {
@Override
public Expression getTenantId() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null && auth.getPrincipal() instanceof LoginUser) {
LoginUser user = (LoginUser) auth.getPrincipal();
if (user.getTenantId() != null) {
return new LongValue(user.getTenantId());
}
}
// If no tenant context (e.g. system task or error), return 0
return new LongValue(0);
}
@Override
public String getTenantIdColumn() {
return "tenant_id";
}
@Override
public boolean ignoreTable(String tableName) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null && auth.getPrincipal() instanceof LoginUser) {
LoginUser user = (LoginUser) auth.getPrincipal();
// 只有当平台管理员处于系统租户(0)时,才忽略所有过滤。
// 如果他切换到了具体租户(>0),则必须接受过滤,确保只能看到当前租户数据。
if (Boolean.TRUE.equals(user.getIsPlatformAdmin()) && Long.valueOf(0).equals(user.getTenantId())) {
return true;
}
}
// 公共表始终忽略过滤
return List.of("sys_tenant","sys_platform_config", "sys_user", "sys_tenant_user", "sys_permission", "sys_role_permission", "sys_user_role", "sys_dict_type", "sys_dict_item", "sys_param").contains(tableName.toLowerCase());
}
}));
interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return interceptor;
}
@Bean
public MetaObjectHandler metaObjectHandler() {
return new MetaObjectHandler() {
@Override
public void insertFill(MetaObject metaObject) {
strictInsertFill(metaObject, "createdAt", LocalDateTime::now, LocalDateTime.class);
strictInsertFill(metaObject, "updatedAt", LocalDateTime::now, LocalDateTime.class);
strictInsertFill(metaObject, "status", () -> 1, Integer.class);
strictInsertFill(metaObject, "isDeleted", () -> 0, Integer.class);
}
@Override
public void updateFill(MetaObject metaObject) {
strictUpdateFill(metaObject, "updatedAt", LocalDateTime::now, LocalDateTime.class);
}
};
}
}

View File

@ -0,0 +1,40 @@
package com.imeeting.config;
import io.swagger.v3.oas.annotations.enums.SecuritySchemeIn;
import io.swagger.v3.oas.annotations.enums.SecuritySchemeType;
import io.swagger.v3.oas.annotations.security.SecurityScheme;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme.In;
import io.swagger.v3.oas.models.security.SecurityScheme.Type;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@SecurityScheme(
name = OpenApiConfig.BEARER_AUTH_SCHEME,
type = SecuritySchemeType.HTTP,
scheme = "bearer",
bearerFormat = "JWT",
in = SecuritySchemeIn.HEADER
)
public class OpenApiConfig {
public static final String BEARER_AUTH_SCHEME = "bearerAuth";
@Bean
public OpenAPI imeetingOpenApi() {
return new OpenAPI()
.info(new Info()
.title("iMeeting 接口文档")
.description("iMeeting 后端 REST 接口与兼容接口文档")
.version("v0.1.0"))
.addSecurityItem(new SecurityRequirement().addList(BEARER_AUTH_SCHEME))
.schemaRequirement(BEARER_AUTH_SCHEME, new io.swagger.v3.oas.models.security.SecurityScheme()
.type(Type.HTTP)
.scheme("bearer")
.bearerFormat("JWT")
.in(In.HEADER));
}
}

View File

@ -0,0 +1,22 @@
package com.imeeting.config;
import com.imeeting.websocket.RealtimeMeetingProxyWebSocketHandler;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
@Configuration
@EnableWebSocket
@RequiredArgsConstructor
public class RealtimeMeetingWebSocketConfig implements WebSocketConfigurer {
private final RealtimeMeetingProxyWebSocketHandler realtimeMeetingProxyWebSocketHandler;
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(realtimeMeetingProxyWebSocketHandler, "/ws/meeting/realtime")
.setAllowedOriginPatterns("*");
}
}

View File

@ -0,0 +1,17 @@
package com.imeeting.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
@Configuration
public class RedisKeyExpirationConfig {
@Bean
public RedisMessageListenerContainer redisMessageListenerContainer(RedisConnectionFactory connectionFactory) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
return container;
}
}

View File

@ -1,62 +0,0 @@
package com.imeeting.config;
import com.imeeting.auth.JwtAuthenticationFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.util.List;
@Configuration
@EnableMethodSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http, JwtAuthenticationFilter jwtAuthenticationFilter) throws Exception {
http.csrf(csrf -> csrf.disable())
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/auth/**").permitAll()
.requestMatchers("/api/open/**").permitAll()
.requestMatchers("/api/static/**").permitAll()
.requestMatchers("/api/params/value").permitAll()
.anyRequest().authenticated()
)
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
return configuration.getAuthenticationManager();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOriginPatterns(List.of("*"));
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
config.setAllowedHeaders(List.of("*"));
config.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return source;
}
}

View File

@ -1,20 +0,0 @@
package com.imeeting.config;
import com.imeeting.service.SysParamService;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;
@Component
public class SysParamCacheInitializer implements ApplicationRunner {
private final SysParamService sysParamService;
public SysParamCacheInitializer(SysParamService sysParamService) {
this.sysParamService = sysParamService;
}
@Override
public void run(ApplicationArguments args) {
sysParamService.syncAllToCache();
}
}

View File

@ -10,10 +10,10 @@ import java.io.File;
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Value("${app.upload-path}")
@Value("${unisbase.app.upload-path}")
private String uploadPath;
@Value("${app.resource-prefix}")
@Value("${unisbase.app.resource-prefix}")
private String resourcePrefix;
@Override

View File

@ -0,0 +1,12 @@
package com.imeeting.config.grpc;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
@Data
@ConfigurationProperties(prefix = "imeeting.grpc.auth")
public class AndroidGrpcAuthProperties {
private boolean enabled = false;
private boolean allowAnonymous = true;
}

View File

@ -0,0 +1,77 @@
package com.imeeting.config.grpc;
import io.grpc.ForwardingServerCallListener;
import io.grpc.Metadata;
import io.grpc.ServerCall;
import io.grpc.ServerCallHandler;
import io.grpc.ServerInterceptor;
import io.grpc.Status;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.concurrent.atomic.AtomicBoolean;
@Slf4j
@Component
public class GrpcExceptionLoggingInterceptor implements ServerInterceptor {
@Override
public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(ServerCall<ReqT, RespT> call,
Metadata headers,
ServerCallHandler<ReqT, RespT> next) {
String methodName = call.getMethodDescriptor().getFullMethodName();
AtomicBoolean closed = new AtomicBoolean(false);
ServerCall.Listener<ReqT> delegate;
try {
delegate = next.startCall(call, headers);
} catch (RuntimeException ex) {
log.error("gRPC startCall failed, method={}", methodName, ex);
closeCall(call, closed, ex);
return new ServerCall.Listener<>() {
};
}
return new ForwardingServerCallListener.SimpleForwardingServerCallListener<>(delegate) {
@Override
public void onMessage(ReqT message) {
runSafely("onMessage", () -> super.onMessage(message));
}
@Override
public void onHalfClose() {
runSafely("onHalfClose", super::onHalfClose);
}
@Override
public void onCancel() {
runSafely("onCancel", super::onCancel);
}
@Override
public void onComplete() {
runSafely("onComplete", super::onComplete);
}
@Override
public void onReady() {
runSafely("onReady", super::onReady);
}
private void runSafely(String phase, Runnable action) {
try {
action.run();
} catch (RuntimeException ex) {
log.error("gRPC request handling failed, method={}, phase={}", methodName, phase, ex);
closeCall(call, closed, ex);
}
}
};
}
private <ReqT, RespT> void closeCall(ServerCall<ReqT, RespT> call, AtomicBoolean closed, RuntimeException ex) {
if (!closed.compareAndSet(false, true)) {
return;
}
call.close(Status.UNKNOWN.withDescription("Application error processing RPC").withCause(ex), new Metadata());
}
}

View File

@ -0,0 +1,56 @@
package com.imeeting.config.grpc;
import io.grpc.BindableService;
import io.grpc.Server;
import io.grpc.protobuf.services.ProtoReflectionService;
import io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.List;
@Slf4j
@Component
@RequiredArgsConstructor
@EnableConfigurationProperties({GrpcServerProperties.class, AndroidGrpcAuthProperties.class})
public class GrpcServerLifecycle {
private final GrpcServerProperties properties;
private final GrpcExceptionLoggingInterceptor grpcExceptionLoggingInterceptor;
private final List<BindableService> bindableServices;
private Server server;
@PostConstruct
public void start() throws IOException {
if (!properties.isEnabled()) {
log.info("gRPC server is disabled by configuration");
return;
}
NettyServerBuilder builder = NettyServerBuilder.forPort(properties.getPort())
.maxInboundMessageSize(properties.getMaxInboundMessageSize())
.intercept(grpcExceptionLoggingInterceptor);
bindableServices.forEach(builder::addService);
if (properties.isReflectionEnabled()) {
builder.addService(ProtoReflectionService.newInstance());
}
server = builder.build();
server.start();
log.info("gRPC server started on port {} with {} services", properties.getPort(), bindableServices.size());
}
@PreDestroy
public void stop() {
if (server == null) {
return;
}
log.info("Stopping gRPC server");
server.shutdown();
}
}

View File

@ -0,0 +1,31 @@
package com.imeeting.config.grpc;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
@Data
@ConfigurationProperties(prefix = "imeeting.grpc")
public class GrpcServerProperties {
private boolean enabled = true;
private int port = 19090;
private int maxInboundMessageSize = 4194304;
private boolean reflectionEnabled = true;
private Gateway gateway = new Gateway();
private Realtime realtime = new Realtime();
@Data
public static class Gateway {
private long heartbeatIntervalSeconds = 15;
private long heartbeatTimeoutSeconds = 45;
}
@Data
public static class Realtime {
private long sessionTtlSeconds = 600;
private int sampleRate = 16000;
private int channels = 1;
private String encoding = "PCM16LE";
private long connectionTtlSeconds = 1800;
}
}

View File

@ -1,100 +0,0 @@
package com.imeeting.controller;
import com.imeeting.auth.JwtTokenProvider;
import com.imeeting.auth.dto.CaptchaResponse;
import com.imeeting.auth.dto.DeviceCodeRequest;
import com.imeeting.auth.dto.LoginRequest;
import com.imeeting.auth.dto.RefreshRequest;
import com.imeeting.auth.dto.TokenResponse;
import com.imeeting.common.ApiResponse;
import com.imeeting.common.RedisKeys;
import com.imeeting.common.SysParamKeys;
import com.imeeting.service.SysParamService;
import com.imeeting.service.AuthService;
import com.wf.captcha.SpecCaptcha;
import jakarta.validation.Valid;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.*;
import java.time.Duration;
import java.util.UUID;
@RestController
@RequestMapping("/auth")
public class AuthController {
private final AuthService authService;
private final StringRedisTemplate stringRedisTemplate;
private final JwtTokenProvider jwtTokenProvider;
private final SysParamService sysParamService;
@Value("${app.captcha.ttl-seconds:120}")
private long captchaTtlSeconds;
public AuthController(AuthService authService, StringRedisTemplate stringRedisTemplate,
JwtTokenProvider jwtTokenProvider, SysParamService sysParamService) {
this.authService = authService;
this.stringRedisTemplate = stringRedisTemplate;
this.jwtTokenProvider = jwtTokenProvider;
this.sysParamService = sysParamService;
}
@GetMapping("/captcha")
public ApiResponse<CaptchaResponse> captcha() {
if (!isCaptchaEnabled()) {
return ApiResponse.error("Captcha disabled");
}
SpecCaptcha captcha = new SpecCaptcha(130, 48, 4);
String code = captcha.text();
String imageBase64 = captcha.toBase64();
String captchaId = UUID.randomUUID().toString().replace("-", "");
stringRedisTemplate.opsForValue().set(RedisKeys.captchaKey(captchaId), code, Duration.ofSeconds(captchaTtlSeconds));
return ApiResponse.ok(new CaptchaResponse(captchaId, imageBase64));
}
@PostMapping("/device-code")
public ApiResponse<String> deviceCode(@Valid @RequestBody DeviceCodeRequest request) {
LoginRequest loginRequest = new LoginRequest();
loginRequest.setUsername(request.getUsername());
loginRequest.setPassword(request.getPassword());
loginRequest.setCaptchaId(request.getCaptchaId());
loginRequest.setCaptchaCode(request.getCaptchaCode());
String deviceCode = authService.createDeviceCode(loginRequest, request.getDeviceName());
return ApiResponse.ok(deviceCode);
}
@PostMapping("/login")
public ApiResponse<TokenResponse> login(@Valid @RequestBody LoginRequest request) {
return ApiResponse.ok(authService.login(request));
}
@PostMapping("/refresh")
public ApiResponse<TokenResponse> refresh(@Valid @RequestBody RefreshRequest request) {
return ApiResponse.ok(authService.refresh(request.getRefreshToken()));
}
@PostMapping("/switch-tenant")
public ApiResponse<TokenResponse> switchTenant(@RequestParam Long tenantId, @RequestHeader("Authorization") String authorization) {
String token = authorization.replace("Bearer ", "");
var claims = jwtTokenProvider.parseToken(token);
Long userId = claims.get("userId", Long.class);
String deviceCode = claims.get("deviceCode", String.class);
return ApiResponse.ok(authService.switchTenant(userId, tenantId, deviceCode));
}
@PostMapping("/logout")
public ApiResponse<Void> logout(@RequestHeader("Authorization") String authorization) {
String token = authorization.replace("Bearer ", "");
var claims = jwtTokenProvider.parseToken(token);
Long userId = claims.get("userId", Long.class);
String deviceCode = claims.get("deviceCode", String.class);
authService.logout(userId, deviceCode);
return ApiResponse.ok(null);
}
private boolean isCaptchaEnabled() {
String value = sysParamService.getCachedParamValue(SysParamKeys.CAPTCHA_ENABLED, "true");
return Boolean.parseBoolean(value);
}
}

View File

@ -1,44 +0,0 @@
package com.imeeting.controller;
import com.imeeting.common.ApiResponse;
import com.imeeting.entity.Device;
import com.imeeting.service.DeviceService;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/devices")
public class DeviceController {
private final DeviceService deviceService;
public DeviceController(DeviceService deviceService) {
this.deviceService = deviceService;
}
@GetMapping
public ApiResponse<List<Device>> list() {
return ApiResponse.ok(deviceService.list());
}
@GetMapping("/{id}")
public ApiResponse<Device> get(@PathVariable Long id) {
return ApiResponse.ok(deviceService.getById(id));
}
@PostMapping
public ApiResponse<Boolean> create(@RequestBody Device device) {
return ApiResponse.ok(deviceService.save(device));
}
@PutMapping("/{id}")
public ApiResponse<Boolean> update(@PathVariable Long id, @RequestBody Device device) {
device.setDeviceId(id);
return ApiResponse.ok(deviceService.updateById(device));
}
@DeleteMapping("/{id}")
public ApiResponse<Boolean> delete(@PathVariable Long id) {
return ApiResponse.ok(deviceService.removeById(id));
}
}

View File

@ -1,62 +0,0 @@
package com.imeeting.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.imeeting.common.ApiResponse;
import com.imeeting.entity.SysDictItem;
import com.imeeting.service.SysDictItemService;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/dict-items")
public class DictItemController {
private final SysDictItemService sysDictItemService;
public DictItemController(SysDictItemService sysDictItemService) {
this.sysDictItemService = sysDictItemService;
}
@GetMapping
@PreAuthorize("@ss.hasPermi('sys_dict:list')")
public ApiResponse<List<SysDictItem>> list(@RequestParam(required = false) String typeCode) {
LambdaQueryWrapper<SysDictItem> queryWrapper = new LambdaQueryWrapper<>();
if (typeCode != null && !typeCode.isEmpty()) {
queryWrapper.eq(SysDictItem::getTypeCode, typeCode);
}
queryWrapper.orderByAsc(SysDictItem::getSortOrder);
return ApiResponse.ok(sysDictItemService.list(queryWrapper));
}
@GetMapping("/{id}")
@PreAuthorize("@ss.hasPermi('sys_dict:query')")
public ApiResponse<SysDictItem> get(@PathVariable Long id) {
return ApiResponse.ok(sysDictItemService.getById(id));
}
@PostMapping
@PreAuthorize("@ss.hasPermi('sys_dict:create')")
public ApiResponse<Boolean> create(@RequestBody SysDictItem dictItem) {
return ApiResponse.ok(sysDictItemService.save(dictItem));
}
@PutMapping("/{id}")
@PreAuthorize("@ss.hasPermi('sys_dict:update')")
public ApiResponse<Boolean> update(@PathVariable Long id, @RequestBody SysDictItem dictItem) {
dictItem.setDictItemId(id);
return ApiResponse.ok(sysDictItemService.updateById(dictItem));
}
@DeleteMapping("/{id}")
@PreAuthorize("@ss.hasPermi('sys_dict:delete')")
public ApiResponse<Boolean> delete(@PathVariable Long id) {
return ApiResponse.ok(sysDictItemService.removeById(id));
}
@GetMapping("/type/{typeCode}")
// @PreAuthorize("@ss.hasPermi('sys_dict:query')")
public ApiResponse<List<SysDictItem>> getByType(@PathVariable String typeCode) {
return ApiResponse.ok(sysDictItemService.getItemsByTypeCode(typeCode));
}
}

View File

@ -1,63 +0,0 @@
package com.imeeting.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.imeeting.common.ApiResponse;
import com.imeeting.entity.SysDictType;
import com.imeeting.service.SysDictTypeService;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/dict-types")
public class DictTypeController {
private final SysDictTypeService sysDictTypeService;
public DictTypeController(SysDictTypeService sysDictTypeService) {
this.sysDictTypeService = sysDictTypeService;
}
@GetMapping
@PreAuthorize("@ss.hasPermi('sys_dict:list')")
public ApiResponse<Page<SysDictType>> list(
@RequestParam(defaultValue = "1") Integer current,
@RequestParam(defaultValue = "10") Integer size,
@RequestParam(required = false) String typeCode,
@RequestParam(required = false) String typeName) {
Page<SysDictType> page = new Page<>(current, size);
LambdaQueryWrapper<SysDictType> queryWrapper = new LambdaQueryWrapper<>();
if (typeCode != null && !typeCode.isEmpty()) {
queryWrapper.like(SysDictType::getTypeCode, typeCode);
}
if (typeName != null && !typeName.isEmpty()) {
queryWrapper.like(SysDictType::getTypeName, typeName);
}
queryWrapper.orderByAsc(SysDictType::getTypeCode);
return ApiResponse.ok(sysDictTypeService.page(page, queryWrapper));
}
@GetMapping("/{id}")
@PreAuthorize("@ss.hasPermi('sys_dict:query')")
public ApiResponse<SysDictType> get(@PathVariable Long id) {
return ApiResponse.ok(sysDictTypeService.getById(id));
}
@PostMapping
@PreAuthorize("@ss.hasPermi('sys_dict:create')")
public ApiResponse<Boolean> create(@RequestBody SysDictType dictType) {
return ApiResponse.ok(sysDictTypeService.save(dictType));
}
@PutMapping("/{id}")
@PreAuthorize("@ss.hasPermi('sys_dict:update')")
public ApiResponse<Boolean> update(@PathVariable Long id, @RequestBody SysDictType dictType) {
dictType.setDictTypeId(id);
return ApiResponse.ok(sysDictTypeService.updateById(dictType));
}
@DeleteMapping("/{id}")
@PreAuthorize("@ss.hasPermi('sys_dict:delete')")
public ApiResponse<Boolean> delete(@PathVariable Long id) {
return ApiResponse.ok(sysDictTypeService.removeById(id));
}
}

View File

@ -1,231 +0,0 @@
package com.imeeting.controller;
import com.imeeting.common.ApiResponse;
import com.imeeting.dto.PermissionNode;
import com.imeeting.entity.SysPermission;
import com.imeeting.entity.SysRole;
import com.imeeting.mapper.SysRolePermissionMapper;
import com.imeeting.mapper.SysUserRoleMapper;
import com.imeeting.service.AuthVersionService;
import com.imeeting.service.SysPermissionService;
import com.imeeting.service.SysRoleService;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/api/permissions")
public class PermissionController {
private final SysPermissionService sysPermissionService;
private final SysRolePermissionMapper sysRolePermissionMapper;
private final SysUserRoleMapper sysUserRoleMapper;
private final SysRoleService sysRoleService;
private final AuthVersionService authVersionService;
public PermissionController(SysPermissionService sysPermissionService,
SysRolePermissionMapper sysRolePermissionMapper, SysUserRoleMapper sysUserRoleMapper,
SysRoleService sysRoleService, AuthVersionService authVersionService) {
this.sysPermissionService = sysPermissionService;
this.sysRolePermissionMapper = sysRolePermissionMapper;
this.sysUserRoleMapper = sysUserRoleMapper;
this.sysRoleService = sysRoleService;
this.authVersionService = authVersionService;
}
@GetMapping
@PreAuthorize("@ss.hasPermi('sys:permission:list')")
public ApiResponse<List<SysPermission>> list() {
Long tenantId = getCurrentTenantId();
// 平台管理员查询所有
if (Long.valueOf(0).equals(tenantId)) {
return ApiResponse.ok(sysPermissionService.list());
}
// 非平台管理员只能查询自己拥有的权限
return ApiResponse.ok(sysPermissionService.listByUserId(getCurrentUserId(), tenantId));
}
@GetMapping("/me")
public ApiResponse<List<SysPermission>> myPermissions() {
return ApiResponse.ok(sysPermissionService.listByUserId(getCurrentUserId(), getCurrentTenantId()));
}
@GetMapping("/tree")
@PreAuthorize("@ss.hasPermi('sys:permission:list')")
public ApiResponse<List<PermissionNode>> tree() {
Long tenantId = getCurrentTenantId();
List<SysPermission> list;
if (Long.valueOf(0).equals(tenantId)) {
list = sysPermissionService.list();
} else {
list = sysPermissionService.listByUserId(getCurrentUserId(), tenantId);
}
return ApiResponse.ok(buildTree(list));
}
@GetMapping("/tree/me")
public ApiResponse<List<PermissionNode>> myTree() {
return ApiResponse.ok(buildTree(sysPermissionService.listByUserId(getCurrentUserId(), getCurrentTenantId())));
}
@GetMapping("/{id}")
@PreAuthorize("@ss.hasPermi('sys:permission:query')")
public ApiResponse<SysPermission> get(@PathVariable Long id) {
return ApiResponse.ok(sysPermissionService.getById(id));
}
@PostMapping
@PreAuthorize("@ss.hasPermi('sys:permission:create')")
public ApiResponse<Boolean> create(@RequestBody SysPermission perm) {
String error = validateParent(perm);
if (error != null) {
return ApiResponse.error(error);
}
return ApiResponse.ok(sysPermissionService.save(perm));
}
@PutMapping("/{id}")
@PreAuthorize("@ss.hasPermi('sys:permission:update')")
public ApiResponse<Boolean> update(@PathVariable Long id, @RequestBody SysPermission perm) {
List<Long> roleIds = sysRolePermissionMapper.selectRoleIdsByPermId(id);
perm.setPermId(id);
String error = validateParent(perm);
if (error != null) {
return ApiResponse.error(error);
}
boolean updated = sysPermissionService.updateById(perm);
if (perm.getLevel() != null && perm.getLevel() == 1) {
sysPermissionService.lambdaUpdate()
.set(SysPermission::getParentId, null)
.eq(SysPermission::getPermId, id)
.update();
}
if (updated) {
invalidateRoleUsers(roleIds);
}
return ApiResponse.ok(updated);
}
@DeleteMapping("/{id}")
@PreAuthorize("@ss.hasPermi('sys:permission:delete')")
public ApiResponse<Boolean> delete(@PathVariable Long id) {
List<Long> roleIds = sysRolePermissionMapper.selectRoleIdsByPermId(id);
boolean removed = sysPermissionService.removeById(id);
if (removed) {
invalidateRoleUsers(roleIds);
}
return ApiResponse.ok(removed);
}
private Long getCurrentUserId() {
org.springframework.security.core.Authentication authentication = org.springframework.security.core.context.SecurityContextHolder.getContext().getAuthentication();
if (authentication != null && authentication.getPrincipal() instanceof com.imeeting.security.LoginUser) {
return ((com.imeeting.security.LoginUser) authentication.getPrincipal()).getUserId();
}
return null;
}
private Long getCurrentTenantId() {
org.springframework.security.core.Authentication authentication = org.springframework.security.core.context.SecurityContextHolder.getContext().getAuthentication();
if (authentication != null && authentication.getPrincipal() instanceof com.imeeting.security.LoginUser) {
return ((com.imeeting.security.LoginUser) authentication.getPrincipal()).getTenantId();
}
return null;
}
private String validateParent(SysPermission perm) {
if (perm.getLevel() == null) {
return null;
}
if (perm.getPermType() != null && "button".equalsIgnoreCase(perm.getPermType())) {
if (perm.getCode() == null || perm.getCode().trim().isEmpty()) {
return "Code required for button permission";
}
}
if (perm.getLevel() == 1) {
perm.setParentId(null);
return null;
}
if (perm.getLevel() == 2) {
if (perm.getParentId() == null) {
return "ParentId required for level 2";
}
SysPermission parent = sysPermissionService.getById(perm.getParentId());
if (parent == null) {
return "Parent not found";
}
if (parent.getLevel() == null || parent.getLevel() != 1) {
return "Parent must be level 1";
}
}
return null;
}
private List<PermissionNode> buildTree(List<SysPermission> list) {
Map<Long, PermissionNode> map = new HashMap<>();
List<PermissionNode> roots = new ArrayList<>();
for (SysPermission p : list) {
PermissionNode node = toNode(p);
map.put(node.getPermId(), node);
}
for (PermissionNode node : map.values()) {
Long parentId = node.getParentId();
if (parentId != null && map.containsKey(parentId)) {
map.get(parentId).getChildren().add(node);
} else {
roots.add(node);
}
}
sortTree(roots);
return roots;
}
private void sortTree(List<PermissionNode> nodes) {
nodes.sort(Comparator.comparingInt(n -> n.getSortOrder() == null ? 0 : n.getSortOrder()));
for (PermissionNode node : nodes) {
if (node.getChildren() != null && !node.getChildren().isEmpty()) {
sortTree(node.getChildren());
}
}
}
private PermissionNode toNode(SysPermission p) {
PermissionNode node = new PermissionNode();
node.setPermId(p.getPermId());
node.setParentId(p.getParentId());
node.setName(p.getName());
node.setCode(p.getCode());
node.setPermType(p.getPermType());
node.setLevel(p.getLevel());
node.setPath(p.getPath());
node.setComponent(p.getComponent());
node.setIcon(p.getIcon());
node.setSortOrder(p.getSortOrder());
node.setIsVisible(p.getIsVisible());
node.setStatus(p.getStatus());
node.setDescription(p.getDescription());
node.setMeta(p.getMeta());
return node;
}
private void invalidateRoleUsers(List<Long> roleIds) {
if (roleIds == null || roleIds.isEmpty()) {
return;
}
for (Long roleId : roleIds) {
if (roleId == null) {
continue;
}
SysRole role = sysRoleService.getById(roleId);
if (role == null || role.getTenantId() == null) {
continue;
}
List<Long> userIds = sysUserRoleMapper.selectUserIdsByRoleId(roleId);
authVersionService.invalidateUsersTenantAuth(userIds, role.getTenantId());
}
}
}

View File

@ -1,55 +0,0 @@
package com.imeeting.controller;
import com.imeeting.common.ApiResponse;
import com.imeeting.dto.PlatformConfigVO;
import com.imeeting.entity.SysPlatformConfig;
import com.imeeting.service.SysPlatformConfigService;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
@RestController
@RequestMapping("/api")
public class PlatformConfigController {
private final SysPlatformConfigService platformConfigService;
public PlatformConfigController(SysPlatformConfigService platformConfigService) {
this.platformConfigService = platformConfigService;
}
/**
* (favicon)
*/
@GetMapping("/open/platform/config")
public ApiResponse<PlatformConfigVO> getOpenConfig() {
return ApiResponse.ok(platformConfigService.getConfig());
}
/**
* ()
*/
@GetMapping("/admin/platform/config")
@PreAuthorize("isAuthenticated()")
public ApiResponse<PlatformConfigVO> getAdminConfig() {
return ApiResponse.ok(platformConfigService.getConfig());
}
/**
* ()
*/
@PutMapping("/admin/platform/config")
@PreAuthorize("hasRole('ADMIN') or @ss.hasPermi('sys_platform:config:update')")
public ApiResponse<Boolean> updateConfig(@RequestBody SysPlatformConfig config) {
return ApiResponse.ok(platformConfigService.updateConfig(config));
}
/**
* ()
*/
@PostMapping("/admin/platform/config/upload")
@PreAuthorize("hasRole('ADMIN') or @ss.hasPermi('sys_platform:config:update')")
public ApiResponse<String> upload(@RequestParam("file") MultipartFile file) {
return ApiResponse.ok(platformConfigService.uploadAsset(file));
}
}

View File

@ -1,344 +0,0 @@
package com.imeeting.controller;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.imeeting.common.ApiResponse;
import com.imeeting.common.annotation.Log;
import com.imeeting.entity.SysRole;
import com.imeeting.entity.SysRolePermission;
import com.imeeting.entity.SysUser;
import com.imeeting.entity.SysUserRole;
import com.imeeting.mapper.SysRolePermissionMapper;
import com.imeeting.mapper.SysUserRoleMapper;
import com.imeeting.service.AuthScopeService;
import com.imeeting.service.AuthVersionService;
import com.imeeting.service.SysRoleService;
import com.imeeting.service.SysUserService;
import com.imeeting.service.SysPermissionService;
import com.imeeting.service.SysTenantUserService;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.*;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import java.util.Set;
@RestController
@RequestMapping("/api/roles")
public class RoleController {
private final SysRoleService sysRoleService;
private final SysUserService sysUserService;
private final SysRolePermissionMapper sysRolePermissionMapper;
private final SysUserRoleMapper sysUserRoleMapper;
private final SysPermissionService sysPermissionService;
private final AuthScopeService authScopeService;
private final AuthVersionService authVersionService;
private final SysTenantUserService sysTenantUserService;
public RoleController(SysRoleService sysRoleService, SysUserService sysUserService,
SysRolePermissionMapper sysRolePermissionMapper, SysUserRoleMapper sysUserRoleMapper,
SysPermissionService sysPermissionService,
AuthScopeService authScopeService,
AuthVersionService authVersionService,
SysTenantUserService sysTenantUserService) {
this.sysRoleService = sysRoleService;
this.sysUserService = sysUserService;
this.sysRolePermissionMapper = sysRolePermissionMapper;
this.sysUserRoleMapper = sysUserRoleMapper;
this.sysPermissionService = sysPermissionService;
this.authScopeService = authScopeService;
this.authVersionService = authVersionService;
this.sysTenantUserService = sysTenantUserService;
}
@GetMapping
@PreAuthorize("@ss.hasPermi('sys:role:list')")
public ApiResponse<List<SysRole>> list(@RequestParam(required = false) Long tenantId) {
QueryWrapper<SysRole> wrapper = new QueryWrapper<>();
if (authScopeService.isCurrentPlatformAdmin()) {
if (tenantId != null) {
wrapper.eq("tenant_id", tenantId);
}
} else {
Long currentTenantId = getCurrentTenantId();
wrapper.eq("tenant_id", currentTenantId);
}
return ApiResponse.ok(sysRoleService.list(wrapper));
}
@GetMapping("/{id}/users")
@PreAuthorize("@ss.hasPermi('sys:role:query')")
public ApiResponse<List<SysUser>> listUsers(@PathVariable Long id) {
SysRole role = sysRoleService.getById(id);
if (role == null) {
return ApiResponse.error("角色不存在");
}
if (!canAccessTenant(role.getTenantId())) {
return ApiResponse.error("禁止跨租户查看角色用户");
}
return ApiResponse.ok(sysUserService.listUsersByRoleId(id));
}
@GetMapping("/{id}")
@PreAuthorize("@ss.hasPermi('sys:role:query')")
public ApiResponse<SysRole> get(@PathVariable Long id) {
SysRole role = sysRoleService.getById(id);
if (role == null) {
return ApiResponse.error("角色不存在");
}
if (!canAccessTenant(role.getTenantId())) {
return ApiResponse.error("禁止跨租户查看角色");
}
return ApiResponse.ok(role);
}
@PostMapping
@PreAuthorize("@ss.hasPermi('sys:role:create')")
@Log(value = "新增角色", type = "角色管理")
public ApiResponse<Boolean> create(@RequestBody SysRole role) {
Long currentTenantId = getCurrentTenantId();
if (currentTenantId == null) {
return ApiResponse.error("Tenant ID required");
}
if (!authScopeService.isCurrentPlatformAdmin()) {
role.setTenantId(currentTenantId);
} else if (role.getTenantId() == null) {
return ApiResponse.error("tenantId required for platform role creation");
}
return ApiResponse.ok(sysRoleService.save(role));
}
@PutMapping("/{id}")
@PreAuthorize("@ss.hasPermi('sys:role:update')")
@Log(value = "修改角色", type = "角色管理")
public ApiResponse<Boolean> update(@PathVariable Long id, @RequestBody SysRole role) {
SysRole existing = sysRoleService.getById(id);
if (existing == null) {
return ApiResponse.error("角色不存在");
}
if (!canAccessTenant(existing.getTenantId())) {
return ApiResponse.error("禁止跨租户修改角色");
}
role.setRoleId(id);
role.setTenantId(existing.getTenantId());
return ApiResponse.ok(sysRoleService.updateById(role));
}
@DeleteMapping("/{id}")
@PreAuthorize("@ss.hasPermi('sys:role:delete')")
@Log(value = "删除角色", type = "角色管理")
public ApiResponse<Boolean> delete(@PathVariable Long id) {
SysRole existing = sysRoleService.getById(id);
if (existing == null) {
return ApiResponse.error("角色不存在");
}
if (!canAccessTenant(existing.getTenantId())) {
return ApiResponse.error("禁止跨租户删除角色");
}
if ("TENANT_ADMIN".equalsIgnoreCase(existing.getRoleCode()) && !authScopeService.isCurrentPlatformAdmin()) {
return ApiResponse.error("租户管理员角色只能由平台管理员删除");
}
List<Long> userIds = sysUserRoleMapper.selectUserIdsByRoleId(id);
boolean removed = sysRoleService.removeById(id);
if (removed) {
authVersionService.invalidateUsersTenantAuth(userIds, existing.getTenantId());
}
return ApiResponse.ok(removed);
}
@GetMapping("/{id}/permissions")
@PreAuthorize("@ss.hasPermi('sys:role:permission:list')")
public ApiResponse<List<Long>> listRolePermissions(@PathVariable Long id) {
SysRole targetRole = sysRoleService.getById(id);
if (targetRole == null) {
return ApiResponse.error("角色不存在");
}
if (!canAccessTenant(targetRole.getTenantId())) {
return ApiResponse.error("禁止跨租户查看角色权限");
}
List<SysRolePermission> rows = sysRolePermissionMapper.selectList(
new QueryWrapper<SysRolePermission>().eq("role_id", id)
);
List<Long> permIds = new ArrayList<>();
for (SysRolePermission row : rows) {
if (row.getPermId() != null) {
permIds.add(row.getPermId());
}
}
return ApiResponse.ok(permIds);
}
@PostMapping("/{id}/permissions")
@PreAuthorize("@ss.hasPermi('sys:role:permission:save')")
@Transactional(rollbackFor = Exception.class)
public ApiResponse<Boolean> saveRolePermissions(@PathVariable Long id, @RequestBody PermissionBindingPayload payload) {
List<Long> permIds = payload == null ? null : payload.getPermIds();
// 权限越权校验
Long currentTenantId = getCurrentTenantId();
if (currentTenantId == null) {
return ApiResponse.error("Tenant ID required");
}
SysRole targetRole = sysRoleService.getById(id);
if (targetRole == null) {
return ApiResponse.error("角色不存在");
}
// 关键校验:只有平台管理员可以修改 TENANT_ADMIN 角色的权限
if ("TENANT_ADMIN".equalsIgnoreCase(targetRole.getRoleCode())) {
if (!authScopeService.isCurrentPlatformAdmin()) {
return ApiResponse.error("租户管理员角色的权限只能由平台管理员修改");
}
}
if (!canAccessTenant(targetRole.getTenantId())) {
return ApiResponse.error("禁止跨租户修改角色权限");
}
if (!authScopeService.isCurrentPlatformAdmin()) {
List<com.imeeting.entity.SysPermission> myPerms = sysPermissionService.listByUserId(getCurrentUserId(), currentTenantId);
Set<Long> myPermIds = myPerms.stream()
.map(com.imeeting.entity.SysPermission::getPermId)
.collect(Collectors.toSet());
if (permIds != null) {
for (Long pId : permIds) {
if (!myPermIds.contains(pId)) {
return ApiResponse.error("越权分配权限:" + pId);
}
}
}
}
sysRolePermissionMapper.delete(new QueryWrapper<SysRolePermission>().eq("role_id", id));
if (permIds == null || permIds.isEmpty()) {
authVersionService.invalidateUsersTenantAuth(sysUserRoleMapper.selectUserIdsByRoleId(id), targetRole.getTenantId());
return ApiResponse.ok(true);
}
for (Long permId : permIds) {
if (permId == null) {
continue;
}
SysRolePermission item = new SysRolePermission();
item.setRoleId(id);
item.setPermId(permId);
sysRolePermissionMapper.insert(item);
}
authVersionService.invalidateUsersTenantAuth(sysUserRoleMapper.selectUserIdsByRoleId(id), targetRole.getTenantId());
return ApiResponse.ok(true);
}
@PostMapping("/{id}/users")
@PreAuthorize("@ss.hasPermi('sys:role:update')")
@Log(value = "角色关联用户", type = "角色管理")
@Transactional(rollbackFor = Exception.class)
public ApiResponse<Boolean> bindUsers(@PathVariable Long id, @RequestBody UserBindingPayload payload) {
if (payload == null || payload.getUserIds() == null) {
return ApiResponse.ok(true);
}
SysRole role = sysRoleService.getById(id);
if (role == null || role.getRoleId() == null || role.getTenantId() == null) {
return ApiResponse.error("角色不存在");
}
if (!canAccessTenant(role.getTenantId())) {
return ApiResponse.error("禁止跨租户绑定用户");
}
List<Long> toInsertUserIds = new ArrayList<>();
for (Long userId : payload.getUserIds()) {
if (userId == null) {
continue;
}
// 修复:处理逻辑删除导致的唯一键冲突
// 执行物理删除,彻底清除旧记录(包括已逻辑删除的)
sysUserRoleMapper.physicalDelete(id, userId, role.getTenantId());
// 确保该用户属于该租户
boolean hasMembership = sysTenantUserService.count(
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<com.imeeting.entity.SysTenantUser>()
.eq(com.imeeting.entity.SysTenantUser::getUserId, userId)
.eq(com.imeeting.entity.SysTenantUser::getTenantId, role.getTenantId())
) > 0;
if (!hasMembership) {
return ApiResponse.error("用户不属于角色所在租户:" + role.getTenantId());
}
toInsertUserIds.add(userId);
}
for (Long userId : toInsertUserIds) {
SysUserRole ur = new SysUserRole();
ur.setTenantId(role.getTenantId());
ur.setRoleId(id);
ur.setUserId(userId);
sysUserRoleMapper.insert(ur);
authVersionService.invalidateUserTenantAuth(userId, role.getTenantId());
}
return ApiResponse.ok(true);
}
@DeleteMapping("/{id}/users/{userId}")
@PreAuthorize("@ss.hasPermi('sys:role:update')")
@Log(value = "角色取消关联用户", type = "角色管理")
@Transactional(rollbackFor = Exception.class)
public ApiResponse<Boolean> unbindUser(@PathVariable Long id, @PathVariable Long userId) {
SysRole role = sysRoleService.getById(id);
if (role == null || role.getRoleId() == null || role.getTenantId() == null) {
return ApiResponse.error("角色不存在");
}
if (!canAccessTenant(role.getTenantId())) {
return ApiResponse.error("禁止跨租户解绑用户");
}
sysUserRoleMapper.physicalDelete(id, userId, role.getTenantId());
authVersionService.invalidateUserTenantAuth(userId, role.getTenantId());
return ApiResponse.ok(true);
}
private Long getCurrentUserId() {
org.springframework.security.core.Authentication authentication = org.springframework.security.core.context.SecurityContextHolder.getContext().getAuthentication();
if (authentication != null && authentication.getPrincipal() instanceof com.imeeting.security.LoginUser) {
return ((com.imeeting.security.LoginUser) authentication.getPrincipal()).getUserId();
}
return null;
}
private Long getCurrentTenantId() {
org.springframework.security.core.Authentication authentication = org.springframework.security.core.context.SecurityContextHolder.getContext().getAuthentication();
if (authentication != null && authentication.getPrincipal() instanceof com.imeeting.security.LoginUser) {
return ((com.imeeting.security.LoginUser) authentication.getPrincipal()).getTenantId();
}
return null;
}
private boolean canAccessTenant(Long targetTenantId) {
if (targetTenantId == null) {
return false;
}
if (authScopeService.isCurrentPlatformAdmin()) {
return true;
}
Long currentTenantId = getCurrentTenantId();
return currentTenantId != null && currentTenantId.equals(targetTenantId);
}
public static class UserBindingPayload {
private List<Long> userIds;
public List<Long> getUserIds() { return userIds; }
public void setUserIds(List<Long> userIds) { this.userIds = userIds; }
}
public static class PermissionBindingPayload {
private List<Long> permIds;
public List<Long> getPermIds() {
return permIds;
}
public void setPermIds(List<Long> permIds) {
this.permIds = permIds;
}
}
}

View File

@ -1,87 +0,0 @@
package com.imeeting.controller;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.imeeting.common.ApiResponse;
import com.imeeting.entity.SysLog;
import com.imeeting.security.LoginUser;
import com.imeeting.service.SysLogService;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/logs")
public class SysLogController {
private final SysLogService sysLogService;
public SysLogController(SysLogService sysLogService) {
this.sysLogService = sysLogService;
}
@GetMapping
@PreAuthorize("@ss.hasPermi('sys_log:list')")
public ApiResponse<IPage<SysLog>> list(
@RequestParam(defaultValue = "1") Integer current,
@RequestParam(defaultValue = "10") Integer size,
@RequestParam(required = false) String username,
@RequestParam(required = false) String logType,
@RequestParam(required = false) String operation,
@RequestParam(required = false) Integer status,
@RequestParam(required = false) String startDate,
@RequestParam(required = false) String endDate,
@RequestParam(required = false) String sortField,
@RequestParam(required = false) String sortOrder
) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
LoginUser loginUser = (LoginUser) auth.getPrincipal();
// 判定平台管理员: isPlatformAdmin=true 且 tenantId=0
boolean isPlatformAdmin = Boolean.TRUE.equals(loginUser.getIsPlatformAdmin()) && Long.valueOf(0).equals(loginUser.getTenantId());
QueryWrapper<SysLog> query = new QueryWrapper<>();
// 只有联表查询才需要前缀 'l.'
String prefix = isPlatformAdmin ? "l." : "";
if (logType != null && !logType.isEmpty()) {
query.eq(prefix + "log_type", logType);
}
if (username != null && !username.isEmpty()) {
query.like(prefix + "username", username);
}
if (operation != null && !operation.isEmpty()) {
query.like(prefix + "operation", operation);
}
if (status != null) {
query.eq(prefix + "status", status);
}
if (startDate != null && !startDate.isEmpty()) {
query.ge(prefix + "created_at", startDate + " 00:00:00");
}
if (endDate != null && !endDate.isEmpty()) {
query.le(prefix + "created_at", endDate + " 23:59:59");
}
// 动态排序逻辑
if (sortField != null && !sortField.isEmpty()) {
String column = "created_at";
if ("duration".equals(sortField)) column = "duration";
if ("ascend".equals(sortOrder)) {
query.orderByAsc(prefix + column);
} else {
query.orderByDesc(prefix + column);
}
} else {
query.orderByDesc(prefix + "created_at");
}
if (isPlatformAdmin) {
return ApiResponse.ok(sysLogService.selectPageWithTenant(new Page<>(current, size), query));
} else {
return ApiResponse.ok(sysLogService.page(new Page<>(current, size), query));
}
}
}

View File

@ -1,60 +0,0 @@
package com.imeeting.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.imeeting.common.ApiResponse;
import com.imeeting.common.annotation.Log;
import com.imeeting.entity.SysOrg;
import com.imeeting.service.SysOrgService;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/orgs")
public class SysOrgController {
private final SysOrgService sysOrgService;
public SysOrgController(SysOrgService sysOrgService) {
this.sysOrgService = sysOrgService;
}
@GetMapping
@PreAuthorize("@ss.hasPermi('sys:org:list')")
public ApiResponse<List<SysOrg>> list(@RequestParam(required = false) Long tenantId) {
return ApiResponse.ok(sysOrgService.listTree(tenantId));
}
@GetMapping("/{id}")
@PreAuthorize("@ss.hasPermi('sys:org:query')")
public ApiResponse<SysOrg> get(@PathVariable Long id) {
return ApiResponse.ok(sysOrgService.getById(id));
}
@PostMapping
@PreAuthorize("@ss.hasPermi('sys:org:create')")
@Log(value = "新增组织", type = "组织管理")
public ApiResponse<Boolean> create(@RequestBody SysOrg org) {
return ApiResponse.ok(sysOrgService.save(org));
}
@PutMapping("/{id}")
@PreAuthorize("@ss.hasPermi('sys:org:update')")
@Log(value = "修改组织", type = "组织管理")
public ApiResponse<Boolean> update(@PathVariable Long id, @RequestBody SysOrg org) {
org.setId(id);
return ApiResponse.ok(sysOrgService.updateById(org));
}
@DeleteMapping("/{id}")
@PreAuthorize("@ss.hasPermi('sys:org:delete')")
@Log(value = "删除组织", type = "组织管理")
public ApiResponse<Boolean> delete(@PathVariable Long id) {
// Check if has children
long count = sysOrgService.count(new LambdaQueryWrapper<SysOrg>().eq(SysOrg::getParentId, id));
if (count > 0) {
return ApiResponse.error("存在下级组织,无法删除");
}
return ApiResponse.ok(sysOrgService.removeById(id));
}
}

View File

@ -1,94 +0,0 @@
package com.imeeting.controller;
import com.imeeting.common.ApiResponse;
import com.imeeting.common.PageResult;
import com.imeeting.dto.SysParamQueryDTO;
import com.imeeting.dto.SysParamVO;
import com.imeeting.entity.SysParam;
import com.imeeting.service.SysParamService;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.stream.Collectors;
@RestController
@RequestMapping("/api/params")
public class SysParamController {
private final SysParamService sysParamService;
public SysParamController(SysParamService sysParamService) {
this.sysParamService = sysParamService;
}
@GetMapping("/page")
@PreAuthorize("@ss.hasPermi('sys_param:list')")
public ApiResponse<PageResult<List<SysParamVO>>> page(SysParamQueryDTO query) {
return ApiResponse.ok(sysParamService.page(query));
}
@GetMapping
@PreAuthorize("@ss.hasPermi('sys_param:list')")
public ApiResponse<List<SysParamVO>> list() {
return ApiResponse.ok(sysParamService.list().stream().map(this::toVO).collect(Collectors.toList()));
}
@GetMapping("/{id}")
@PreAuthorize("@ss.hasPermi('sys_param:query')")
public ApiResponse<SysParamVO> get(@PathVariable Long id) {
return ApiResponse.ok(toVO(sysParamService.getById(id)));
}
@PostMapping
@PreAuthorize("@ss.hasPermi('sys_param:create')")
public ApiResponse<Boolean> create(@RequestBody SysParam param) {
boolean saved = sysParamService.save(param);
if (saved) {
sysParamService.syncParamToCache(param);
}
return ApiResponse.ok(saved);
}
@PutMapping("/{id}")
@PreAuthorize("@ss.hasPermi('sys_param:update')")
public ApiResponse<Boolean> update(@PathVariable Long id, @RequestBody SysParam param) {
param.setParamId(id);
boolean updated = sysParamService.updateById(param);
if (updated) {
sysParamService.syncParamToCache(param);
}
return ApiResponse.ok(updated);
}
@DeleteMapping("/{id}")
@PreAuthorize("@ss.hasPermi('sys_param:delete')")
public ApiResponse<Boolean> delete(@PathVariable Long id) {
SysParam param = sysParamService.getById(id);
boolean removed = sysParamService.removeById(id);
if (removed && param != null) {
sysParamService.deleteParamCache(param.getParamKey());
}
return ApiResponse.ok(removed);
}
@GetMapping("/value")
public ApiResponse<String> getValue(@RequestParam("key") String key,
@RequestParam(value = "defaultValue", required = false) String defaultValue) {
return ApiResponse.ok(sysParamService.getCachedParamValue(key, defaultValue));
}
private SysParamVO toVO(SysParam entity) {
if (entity == null) return null;
SysParamVO vo = new SysParamVO();
vo.setParamId(entity.getParamId());
vo.setParamKey(entity.getParamKey());
vo.setParamValue(entity.getParamValue());
vo.setParamType(entity.getParamType());
vo.setIsSystem(entity.getIsSystem());
vo.setDescription(entity.getDescription());
vo.setStatus(entity.getStatus());
vo.setCreatedAt(entity.getCreatedAt());
vo.setUpdatedAt(entity.getUpdatedAt());
return vo;
}
}

View File

@ -1,69 +0,0 @@
package com.imeeting.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.imeeting.common.ApiResponse;
import com.imeeting.common.annotation.Log;
import com.imeeting.entity.SysTenant;
import com.imeeting.service.SysTenantService;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/tenants")
public class SysTenantController {
private final SysTenantService sysTenantService;
public SysTenantController(SysTenantService sysTenantService) {
this.sysTenantService = sysTenantService;
}
@GetMapping
@PreAuthorize("@ss.hasPermi('sys_tenant:list')")
public ApiResponse<Page<SysTenant>> list(
@RequestParam(defaultValue = "1") Integer current,
@RequestParam(defaultValue = "10") Integer size,
@RequestParam(required = false) String name,
@RequestParam(required = false) String code
) {
LambdaQueryWrapper<SysTenant> query = new LambdaQueryWrapper<>();
if (name != null && !name.isEmpty()) {
query.like(SysTenant::getTenantName, name);
}
if (code != null && !code.isEmpty()) {
query.like(SysTenant::getTenantCode, code);
}
query.orderByDesc(SysTenant::getCreatedAt);
return ApiResponse.ok(sysTenantService.page(new Page<>(current, size), query));
}
@GetMapping("/{id}")
@PreAuthorize("@ss.hasPermi('sys_tenant:query')")
public ApiResponse<SysTenant> get(@PathVariable Long id) {
return ApiResponse.ok(sysTenantService.getById(id));
}
@PostMapping
@PreAuthorize("@ss.hasPermi('sys_tenant:create')")
@Log(value = "新增租户", type = "租户管理")
public ApiResponse<Long> create(@RequestBody com.imeeting.dto.CreateTenantDTO tenantDto) {
return ApiResponse.ok(sysTenantService.createTenantWithAdmin(tenantDto));
}
@PutMapping("/{id}")
@PreAuthorize("@ss.hasPermi('sys_tenant:update')")
@Log(value = "修改租户", type = "租户管理")
public ApiResponse<Boolean> update(@PathVariable Long id, @RequestBody SysTenant tenant) {
tenant.setId(id);
return ApiResponse.ok(sysTenantService.updateById(tenant));
}
@DeleteMapping("/{id}")
@PreAuthorize("@ss.hasPermi('sys_tenant:delete')")
@Log(value = "删除租户", type = "租户管理")
public ApiResponse<Boolean> delete(@PathVariable Long id) {
return ApiResponse.ok(sysTenantService.removeById(id));
}
}

View File

@ -1,371 +0,0 @@
package com.imeeting.controller;
import com.imeeting.common.ApiResponse;
import com.imeeting.dto.PasswordUpdateDTO;
import com.imeeting.dto.UserProfile;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.imeeting.security.LoginUser;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.imeeting.entity.SysUser;
import com.imeeting.entity.SysUserRole;
import com.imeeting.mapper.SysUserRoleMapper;
import com.imeeting.service.AuthScopeService;
import com.imeeting.service.AuthVersionService;
import com.imeeting.service.SysUserService;
import com.imeeting.common.annotation.Log;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.*;
import java.util.ArrayList;
import java.util.List;
@RestController
@RequestMapping("/api/users")
public class UserController {
private final SysUserService sysUserService;
private final PasswordEncoder passwordEncoder;
private final SysUserRoleMapper sysUserRoleMapper;
private final com.imeeting.service.SysTenantUserService sysTenantUserService;
private final com.imeeting.service.SysRoleService sysRoleService;
private final AuthScopeService authScopeService;
private final AuthVersionService authVersionService;
public UserController(SysUserService sysUserService, PasswordEncoder passwordEncoder,
SysUserRoleMapper sysUserRoleMapper,
com.imeeting.service.SysTenantUserService sysTenantUserService,
com.imeeting.service.SysRoleService sysRoleService,
AuthScopeService authScopeService,
AuthVersionService authVersionService) {
this.sysUserService = sysUserService;
this.passwordEncoder = passwordEncoder;
this.sysUserRoleMapper = sysUserRoleMapper;
this.sysTenantUserService = sysTenantUserService;
this.sysRoleService = sysRoleService;
this.authScopeService = authScopeService;
this.authVersionService = authVersionService;
}
@GetMapping
@PreAuthorize("@ss.hasPermi('sys:user:list')")
public ApiResponse<List<SysUser>> list(@RequestParam(required = false) Long tenantId, @RequestParam(required = false) Long orgId) {
Long currentTenantId = getCurrentTenantId();
List<SysUser> users;
Long targetTenantId = null;
if (Long.valueOf(0).equals(currentTenantId) && tenantId == null) {
users = sysUserService.list();
} else {
targetTenantId = tenantId != null ? tenantId : currentTenantId;
if (targetTenantId == null) {
return ApiResponse.error("Tenant ID required");
}
users = sysUserService.listUsersByTenant(targetTenantId, orgId);
}
if (users != null && !users.isEmpty()) {
for (SysUser user : users) {
// 加载租户关系
user.setMemberships(sysTenantUserService.listByUserId(user.getUserId()));
// 加载角色信息
QueryWrapper<SysUserRole> roleQuery = new QueryWrapper<SysUserRole>().eq("user_id", user.getUserId());
if (targetTenantId != null) {
roleQuery.eq("tenant_id", targetTenantId);
}
List<SysUserRole> userRoles = sysUserRoleMapper.selectList(roleQuery);
if (userRoles != null && !userRoles.isEmpty()) {
List<Long> roleIds = userRoles.stream()
.map(SysUserRole::getRoleId)
.collect(java.util.stream.Collectors.toList());
user.setRoles(sysRoleService.listByIds(roleIds));
}
}
}
return ApiResponse.ok(users);
}
@GetMapping("/me")
public ApiResponse<UserProfile> me() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null || !(authentication.getPrincipal() instanceof LoginUser)) {
return ApiResponse.error("Unauthorized");
}
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
Long userId = loginUser.getUserId();
SysUser user = sysUserService.getByIdIgnoreTenant(userId);
if (user == null) {
return ApiResponse.error("User not found");
}
UserProfile profile = new UserProfile();
profile.setUserId(user.getUserId());
profile.setUsername(user.getUsername());
profile.setDisplayName(user.getDisplayName());
profile.setEmail(user.getEmail());
profile.setPhone(user.getPhone());
profile.setStatus(user.getStatus());
profile.setAdmin(userId == 1L);
profile.setIsPlatformAdmin(user.getIsPlatformAdmin());
profile.setIsTenantAdmin(loginUser.getIsTenantAdmin());
profile.setPwdResetRequired(user.getPwdResetRequired());
return ApiResponse.ok(profile);
}
@GetMapping("/{id}")
@PreAuthorize("@ss.hasPermi('sys:user:query')")
public ApiResponse<SysUser> get(@PathVariable Long id) {
Long currentTenantId = getCurrentTenantId();
if (currentTenantId == null) {
return ApiResponse.error("Tenant ID required");
}
if (!authScopeService.isCurrentPlatformAdmin() && !isUserInTenant(id, currentTenantId)) {
return ApiResponse.error("禁止跨租户查看用户");
}
SysUser user = sysUserService.getByIdIgnoreTenant(id);
if (user != null) {
user.setMemberships(sysTenantUserService.listByUserId(id));
}
return ApiResponse.ok(user);
}
private Long getCurrentTenantId() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null && auth.getPrincipal() instanceof LoginUser) {
return ((LoginUser) auth.getPrincipal()).getTenantId();
}
return null;
}
@PostMapping
@PreAuthorize("@ss.hasPermi('sys:user:create')")
@Log(value = "新增用户", type = "用户管理")
public ApiResponse<Boolean> create(@RequestBody SysUser user) {
Long currentTenantId = getCurrentTenantId();
if (currentTenantId == null) {
return ApiResponse.error("Tenant ID required");
}
// 非平台管理员强制设置为当前租户
if (!Long.valueOf(0).equals(currentTenantId)) {
if (user.getMemberships() != null && !user.getMemberships().isEmpty()) {
user.getMemberships().forEach(m -> m.setTenantId(currentTenantId));
} else {
// 如果没传身份,补齐当前租户身份
List<com.imeeting.entity.SysTenantUser> memberships = new java.util.ArrayList<>();
com.imeeting.entity.SysTenantUser m = new com.imeeting.entity.SysTenantUser();
m.setTenantId(currentTenantId);
memberships.add(m);
user.setMemberships(memberships);
}
}
if (user.getPasswordHash() != null && !user.getPasswordHash().isEmpty()) {
user.setPasswordHash(passwordEncoder.encode(user.getPasswordHash()));
}
boolean saved = sysUserService.save(user);
if (saved) {
sysTenantUserService.syncMemberships(user.getUserId(), user.getMemberships());
}
return ApiResponse.ok(saved);
}
@PutMapping("/{id}")
@PreAuthorize("@ss.hasPermi('sys:user:update')")
@Log(value = "修改用户", type = "用户管理")
public ApiResponse<Boolean> update(@PathVariable Long id, @RequestBody SysUser user) {
Long currentTenantId = getCurrentTenantId();
if (currentTenantId == null) {
return ApiResponse.error("Tenant ID required");
}
user.setUserId(id);
if (!authScopeService.isCurrentPlatformAdmin() && !isUserInTenant(id, currentTenantId)) {
return ApiResponse.error("禁止跨租户修改用户");
}
// 非平台管理员强制约束租户身份
if (!Long.valueOf(0).equals(currentTenantId)) {
if (user.getMemberships() != null) {
user.getMemberships().forEach(m -> m.setTenantId(currentTenantId));
}
}
if (user.getPasswordHash() != null && !user.getPasswordHash().isEmpty()) {
user.setPasswordHash(passwordEncoder.encode(user.getPasswordHash()));
}
boolean updated = sysUserService.updateById(user);
if (updated) {
sysTenantUserService.syncMemberships(id, user.getMemberships());
}
return ApiResponse.ok(updated);
}
@DeleteMapping("/{id}")
@PreAuthorize("@ss.hasPermi('sys:user:delete')")
@Log(value = "删除用户", type = "用户管理")
public ApiResponse<Boolean> delete(@PathVariable Long id) {
Long currentTenantId = getCurrentTenantId();
if (currentTenantId == null) {
return ApiResponse.error("Tenant ID required");
}
if (!authScopeService.isCurrentPlatformAdmin() && !isUserInTenant(id, currentTenantId)) {
return ApiResponse.error("禁止跨租户删除用户");
}
return ApiResponse.ok(sysUserService.removeById(id));
}
@PutMapping("/profile")
public ApiResponse<Boolean> updateProfile(@RequestBody SysUser user) {
Long userId = getCurrentUserId();
SysUser existing = sysUserService.getByIdIgnoreTenant(userId);
if (existing == null) return ApiResponse.error("用户不存在");
existing.setDisplayName(user.getDisplayName());
existing.setEmail(user.getEmail());
existing.setPhone(user.getPhone());
return ApiResponse.ok(sysUserService.updateById(existing));
}
@PutMapping("/password")
public ApiResponse<Boolean> updatePassword(@RequestBody PasswordUpdateDTO dto) {
Long userId = getCurrentUserId();
SysUser user = sysUserService.getByIdIgnoreTenant(userId);
if (user == null) return ApiResponse.error("用户不存在");
if (!passwordEncoder.matches(dto.getOldPassword(), user.getPasswordHash())) {
return ApiResponse.error("旧密码不正确");
}
user.setPasswordHash(passwordEncoder.encode(dto.getNewPassword()));
user.setPwdResetRequired(0); // 重置标志位
return ApiResponse.ok(sysUserService.updateById(user));
}
private Long getCurrentUserId() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null && auth.getPrincipal() instanceof LoginUser) {
return ((LoginUser) auth.getPrincipal()).getUserId();
}
return null;
}
@GetMapping("/{id}/roles")
@PreAuthorize("@ss.hasPermi('sys:user:role:list')")
public ApiResponse<List<Long>> listUserRoles(@PathVariable Long id) {
Long currentTenantId = getCurrentTenantId();
if (currentTenantId == null) {
return ApiResponse.error("Tenant ID required");
}
if (!authScopeService.isCurrentPlatformAdmin() && !isUserInTenant(id, currentTenantId)) {
return ApiResponse.error("禁止跨租户查看用户角色");
}
QueryWrapper<SysUserRole> query = new QueryWrapper<SysUserRole>().eq("user_id", id);
if (!authScopeService.isCurrentPlatformAdmin()) {
query.eq("tenant_id", currentTenantId);
}
List<SysUserRole> rows = sysUserRoleMapper.selectList(query);
List<Long> roleIds = new ArrayList<>();
for (SysUserRole row : rows) {
if (row.getRoleId() != null) {
roleIds.add(row.getRoleId());
}
}
return ApiResponse.ok(roleIds);
}
@PostMapping("/{id}/roles")
@PreAuthorize("@ss.hasPermi('sys:user:role:save')")
@Transactional(rollbackFor = Exception.class)
public ApiResponse<Boolean> saveUserRoles(@PathVariable Long id, @RequestBody RoleBindingPayload payload) {
Long currentTenantId = getCurrentTenantId();
if (currentTenantId == null) {
return ApiResponse.error("Tenant ID required");
}
if (!authScopeService.isCurrentPlatformAdmin() && !isUserInTenant(id, currentTenantId)) {
return ApiResponse.error("禁止跨租户分配角色");
}
List<Long> roleIds = payload == null ? null : payload.getRoleIds();
List<com.imeeting.entity.SysRole> rolesToBind = new ArrayList<>();
if (roleIds != null) {
for (Long roleId : roleIds) {
if (roleId == null) {
continue;
}
com.imeeting.entity.SysRole role = sysRoleService.getById(roleId);
if (role == null || role.getRoleId() == null || role.getTenantId() == null) {
return ApiResponse.error("角色不存在:" + roleId);
}
Long roleTenantId = role.getTenantId();
if (!authScopeService.isCurrentPlatformAdmin() && !currentTenantId.equals(roleTenantId)) {
return ApiResponse.error("禁止跨租户分配角色:" + roleId);
}
boolean hasMembership = sysTenantUserService.count(
new LambdaQueryWrapper<com.imeeting.entity.SysTenantUser>()
.eq(com.imeeting.entity.SysTenantUser::getUserId, id)
.eq(com.imeeting.entity.SysTenantUser::getTenantId, roleTenantId)
) > 0;
if (!hasMembership) {
return ApiResponse.error("用户不属于角色所在租户:" + roleTenantId);
}
rolesToBind.add(role);
}
}
QueryWrapper<SysUserRole> scopeQuery = new QueryWrapper<SysUserRole>().eq("user_id", id);
if (!authScopeService.isCurrentPlatformAdmin()) {
scopeQuery.eq("tenant_id", currentTenantId);
}
List<SysUserRole> existingRows = sysUserRoleMapper.selectList(scopeQuery);
java.util.Set<Long> affectedTenantIds = new java.util.HashSet<>();
for (SysUserRole row : existingRows) {
if (row.getTenantId() != null) {
affectedTenantIds.add(row.getTenantId());
}
}
for (com.imeeting.entity.SysRole role : rolesToBind) {
if (role.getTenantId() != null) {
affectedTenantIds.add(role.getTenantId());
}
}
sysUserRoleMapper.delete(scopeQuery);
for (com.imeeting.entity.SysRole role : rolesToBind) {
SysUserRole item = new SysUserRole();
item.setTenantId(role.getTenantId());
item.setUserId(id);
item.setRoleId(role.getRoleId());
sysUserRoleMapper.insert(item);
}
for (Long tenantId : affectedTenantIds) {
authVersionService.invalidateUserTenantAuth(id, tenantId);
}
return ApiResponse.ok(true);
}
private boolean isUserInTenant(Long userId, Long tenantId) {
if (userId == null || tenantId == null) {
return false;
}
return sysTenantUserService.count(
new LambdaQueryWrapper<com.imeeting.entity.SysTenantUser>()
.eq(com.imeeting.entity.SysTenantUser::getUserId, userId)
.eq(com.imeeting.entity.SysTenantUser::getTenantId, tenantId)
) > 0;
}
public static class RoleBindingPayload {
private List<Long> roleIds;
public List<Long> getRoleIds() {
return roleIds;
}
public void setRoleIds(List<Long> roleIds) {
this.roleIds = roleIds;
}
}
}

View File

@ -0,0 +1,236 @@
package com.imeeting.controller.android;
import com.imeeting.config.grpc.GrpcServerProperties;
import com.imeeting.dto.android.AndroidAuthContext;
import com.imeeting.dto.android.AndroidCreateRealtimeMeetingCommand;
import com.imeeting.dto.android.AndroidCreateRealtimeMeetingVO;
import com.imeeting.dto.android.AndroidOpenRealtimeGrpcSessionCommand;
import com.imeeting.dto.android.AndroidRealtimeGrpcSessionVO;
import com.imeeting.dto.biz.CreateRealtimeMeetingCommand;
import com.imeeting.dto.biz.MeetingTranscriptVO;
import com.imeeting.dto.biz.RealtimeMeetingRuntimeProfile;
import com.imeeting.dto.biz.MeetingVO;
import com.imeeting.dto.biz.RealtimeMeetingCompleteDTO;
import com.imeeting.dto.biz.RealtimeMeetingSessionStatusVO;
import com.imeeting.entity.biz.Meeting;
import com.imeeting.service.android.AndroidAuthService;
import com.imeeting.service.biz.MeetingAccessService;
import com.imeeting.service.biz.MeetingAuthorizationService;
import com.imeeting.service.biz.MeetingCommandService;
import com.imeeting.service.biz.MeetingQueryService;
import com.imeeting.service.biz.MeetingRuntimeProfileResolver;
import com.imeeting.service.biz.RealtimeMeetingSessionStateService;
import com.imeeting.service.realtime.AndroidRealtimeSessionTicketService;
import com.unisbase.common.ApiResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
@Tag(name = "Android实时会议")
@RestController
@RequestMapping("/api/android/meeting")
@RequiredArgsConstructor
public class AndroidMeetingRealtimeController {
private static final DateTimeFormatter TITLE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
private final AndroidAuthService androidAuthService;
private final MeetingAccessService meetingAccessService;
private final MeetingAuthorizationService meetingAuthorizationService;
private final MeetingQueryService meetingQueryService;
private final MeetingCommandService meetingCommandService;
private final RealtimeMeetingSessionStateService realtimeMeetingSessionStateService;
private final AndroidRealtimeSessionTicketService androidRealtimeSessionTicketService;
private final MeetingRuntimeProfileResolver meetingRuntimeProfileResolver;
private final GrpcServerProperties grpcServerProperties;
@Operation(summary = "创建Android实时会议")
@PostMapping("/realtime/create")
public ApiResponse<AndroidCreateRealtimeMeetingVO> createRealtimeMeeting(HttpServletRequest request,
@RequestBody(required = false) AndroidCreateRealtimeMeetingCommand command) {
AndroidAuthContext authContext = androidAuthService.authenticateHttp(request);
meetingAuthorizationService.assertCanCreateMeeting(authContext);
RealtimeMeetingRuntimeProfile runtimeProfile = meetingRuntimeProfileResolver.resolve(
authContext.getTenantId(),
command == null ? null : command.getAsrModelId(),
command == null ? null : command.getSummaryModelId(),
command == null ? null : command.getPromptId(),
command == null ? null : command.getMode(),
command == null ? null : command.getLanguage(),
command == null ? null : command.getUseSpkId(),
command == null ? null : command.getEnablePunctuation(),
command == null ? null : command.getEnableItn(),
command == null ? null : command.getEnableTextRefine(),
command == null ? null : command.getSaveAudio(),
command == null ? null : command.getHotWords()
);
CreateRealtimeMeetingCommand createCommand = buildCreateCommand(command, authContext, runtimeProfile);
MeetingVO meeting = meetingCommandService.createRealtimeMeeting(
createCommand,
authContext.getTenantId(),
authContext.getUserId(),
resolveCreatorName(authContext)
);
RealtimeMeetingSessionStatusVO status = realtimeMeetingSessionStateService.getStatus(meeting.getId());
AndroidCreateRealtimeMeetingVO vo = new AndroidCreateRealtimeMeetingVO();
vo.setMeetingId(meeting.getId());
vo.setTitle(meeting.getTitle());
vo.setHostUserId(meeting.getHostUserId());
vo.setHostName(meeting.getHostName());
vo.setSampleRate(grpcServerProperties.getRealtime().getSampleRate());
vo.setChannels(grpcServerProperties.getRealtime().getChannels());
vo.setEncoding(grpcServerProperties.getRealtime().getEncoding());
vo.setResolvedAsrModelId(runtimeProfile.getResolvedAsrModelId());
vo.setResolvedAsrModelName(runtimeProfile.getResolvedAsrModelName());
vo.setResolvedSummaryModelId(runtimeProfile.getResolvedSummaryModelId());
vo.setResolvedSummaryModelName(runtimeProfile.getResolvedSummaryModelName());
vo.setResolvedPromptId(runtimeProfile.getResolvedPromptId());
vo.setResolvedPromptName(runtimeProfile.getResolvedPromptName());
vo.setResumeConfig(status == null ? null : status.getResumeConfig());
vo.setStatus(status);
return ApiResponse.ok(vo);
}
@Operation(summary = "查询Android实时会议状态")
@GetMapping("/{id}/realtime/session-status")
public ApiResponse<RealtimeMeetingSessionStatusVO> getRealtimeSessionStatus(@PathVariable Long id, HttpServletRequest request) {
AndroidAuthContext authContext = androidAuthService.authenticateHttp(request);
Meeting meeting = meetingAccessService.requireMeeting(id);
meetingAuthorizationService.assertCanManageRealtimeMeeting(meeting, authContext);
return ApiResponse.ok(realtimeMeetingSessionStateService.getStatus(id));
}
@Operation(summary = "查询Android会议转写")
@GetMapping("/{id}/transcripts")
public ApiResponse<List<MeetingTranscriptVO>> getTranscripts(@PathVariable Long id, HttpServletRequest request) {
AndroidAuthContext authContext = androidAuthService.authenticateHttp(request);
Meeting meeting = meetingAccessService.requireMeeting(id);
meetingAuthorizationService.assertCanViewMeeting(meeting, authContext);
return ApiResponse.ok(meetingQueryService.getTranscripts(id));
}
@Operation(summary = "暂停Android实时会议")
@PostMapping("/{id}/realtime/pause")
public ApiResponse<RealtimeMeetingSessionStatusVO> pauseRealtimeMeeting(@PathVariable Long id, HttpServletRequest request) {
AndroidAuthContext authContext = androidAuthService.authenticateHttp(request);
Meeting meeting = meetingAccessService.requireMeeting(id);
meetingAuthorizationService.assertCanManageRealtimeMeeting(meeting, authContext);
return ApiResponse.ok(realtimeMeetingSessionStateService.pause(id));
}
@Operation(summary = "完成Android实时会议")
@PostMapping("/{id}/realtime/complete")
public ApiResponse<Boolean> completeRealtimeMeeting(@PathVariable Long id,
HttpServletRequest request,
@RequestBody(required = false) RealtimeMeetingCompleteDTO dto) {
AndroidAuthContext authContext = androidAuthService.authenticateHttp(request);
Meeting meeting = meetingAccessService.requireMeeting(id);
meetingAuthorizationService.assertCanManageRealtimeMeeting(meeting, authContext);
meetingCommandService.completeRealtimeMeeting(
id,
dto != null ? dto.getAudioUrl() : null,
dto != null && Boolean.TRUE.equals(dto.getOverwriteAudio())
);
return ApiResponse.ok(true);
}
@Operation(summary = "打开Android实时会议gRPC会话")
@PostMapping("/{id}/realtime/grpc-session")
public ApiResponse<AndroidRealtimeGrpcSessionVO> openRealtimeGrpcSession(@PathVariable Long id,
HttpServletRequest request,
@RequestBody(required = false) AndroidOpenRealtimeGrpcSessionCommand command) {
AndroidAuthContext authContext = androidAuthService.authenticateHttp(request);
Meeting meeting = meetingAccessService.requireMeeting(id);
meetingAuthorizationService.assertCanManageRealtimeMeeting(meeting, authContext);
return ApiResponse.ok(androidRealtimeSessionTicketService.createSession(id, command, authContext));
}
private CreateRealtimeMeetingCommand buildCreateCommand(AndroidCreateRealtimeMeetingCommand command,
AndroidAuthContext authContext,
RealtimeMeetingRuntimeProfile runtimeProfile) {
CreateRealtimeMeetingCommand createCommand = new CreateRealtimeMeetingCommand();
LocalDateTime meetingTime = command != null && command.getMeetingTime() != null ? command.getMeetingTime() : LocalDateTime.now();
createCommand.setTitle(resolveMeetingTitle(command, meetingTime));
createCommand.setMeetingTime(meetingTime);
createCommand.setParticipants(command == null ? "" : normalize(command.getParticipants(), ""));
createCommand.setTags(command == null ? "" : normalize(command.getTags()));
createCommand.setHostUserId(resolveHostUserId(command, authContext));
createCommand.setHostName(resolveHostName(command, authContext, createCommand.getHostUserId()));
createCommand.setAsrModelId(runtimeProfile.getResolvedAsrModelId());
createCommand.setSummaryModelId(runtimeProfile.getResolvedSummaryModelId());
createCommand.setPromptId(runtimeProfile.getResolvedPromptId());
createCommand.setMode(runtimeProfile.getResolvedMode());
createCommand.setLanguage(runtimeProfile.getResolvedLanguage());
createCommand.setUseSpkId(runtimeProfile.getResolvedUseSpkId());
createCommand.setEnablePunctuation(runtimeProfile.getResolvedEnablePunctuation());
createCommand.setEnableItn(runtimeProfile.getResolvedEnableItn());
createCommand.setEnableTextRefine(runtimeProfile.getResolvedEnableTextRefine());
createCommand.setSaveAudio(runtimeProfile.getResolvedSaveAudio());
createCommand.setHotWords(runtimeProfile.getResolvedHotWords());
return createCommand;
}
private String resolveMeetingTitle(AndroidCreateRealtimeMeetingCommand command, LocalDateTime meetingTime) {
String title = command == null ? null : normalize(command.getTitle());
if (title != null && !title.isBlank()) {
return title;
}
return "Android-Realtime-Meeting-" + TITLE_TIME_FORMATTER.format(meetingTime);
}
private Long resolveHostUserId(AndroidCreateRealtimeMeetingCommand command, AndroidAuthContext authContext) {
if (command != null && command.getHostUserId() != null) {
return command.getHostUserId();
}
return authContext.getUserId();
}
private String resolveHostName(AndroidCreateRealtimeMeetingCommand command, AndroidAuthContext authContext, Long hostUserId) {
if (command != null && command.getHostName() != null && !command.getHostName().isBlank()) {
return command.getHostName().trim();
}
if (hostUserId != null && hostUserId.equals(authContext.getUserId())) {
return resolveCreatorName(authContext);
}
return null;
}
private String resolveCreatorName(AndroidAuthContext authContext) {
if (authContext == null) {
return "android";
}
if (authContext.getDisplayName() != null && !authContext.getDisplayName().isBlank()) {
return authContext.getDisplayName().trim();
}
if (authContext.getUsername() != null && !authContext.getUsername().isBlank()) {
return authContext.getUsername().trim();
}
return authContext.getDeviceId() == null || authContext.getDeviceId().isBlank()
? "android"
: "android:" + authContext.getDeviceId().trim();
}
private String normalize(String value) {
return normalize(value, null);
}
private String normalize(String value, String defaultValue) {
if (value == null) {
return defaultValue;
}
String normalized = value.trim();
return normalized.isEmpty() ? defaultValue : normalized;
}
}

View File

@ -0,0 +1,49 @@
package com.imeeting.controller.android;
import com.imeeting.dto.android.AndroidAuthContext;
import com.imeeting.dto.android.AndroidScreenSaverCatalogVO;
import com.imeeting.dto.android.AndroidScreenSaverItemVO;
import com.imeeting.dto.biz.ScreenSaverSelectionResult;
import com.imeeting.service.android.AndroidAuthService;
import com.imeeting.service.biz.ScreenSaverService;
import com.unisbase.common.ApiResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Tag(name = "Android屏保")
@RestController
@RequestMapping("/api/android/screensavers")
@RequiredArgsConstructor
public class AndroidScreenSaverController {
private final AndroidAuthService androidAuthService;
private final ScreenSaverService screenSaverService;
@Operation(summary = "获取当前生效屏保")
@GetMapping("/active")
public ApiResponse<AndroidScreenSaverCatalogVO> active(HttpServletRequest request) {
AndroidAuthContext authContext = androidAuthService.authenticateHttp(request);
ScreenSaverSelectionResult selection = screenSaverService.getActiveSelection(authContext == null ? null : authContext.getUserId());
AndroidScreenSaverCatalogVO vo = new AndroidScreenSaverCatalogVO();
vo.setRefreshIntervalSec(300);
vo.setPlayMode("SEQUENTIAL");
vo.setSourceScope(selection.getSourceScope());
vo.setItems(selection.getItems().stream().map(item -> {
AndroidScreenSaverItemVO child = new AndroidScreenSaverItemVO();
child.setId(item.getId());
child.setName(item.getName());
child.setImageUrl(item.getImageUrl());
child.setDescription(item.getDescription());
child.setDisplayDurationSec(item.getDisplayDurationSec());
child.setSortOrder(item.getSortOrder());
child.setUpdatedAt(item.getUpdatedAt());
return child;
}).toList());
return ApiResponse.ok(vo);
}
}

View File

@ -0,0 +1,111 @@
package com.imeeting.controller.android.legacy;
import com.imeeting.dto.android.legacy.LegacyApiResponse;
import com.imeeting.dto.android.legacy.LegacyLoginResponse;
import com.imeeting.dto.android.legacy.LegacyLoginUserResponse;
import com.imeeting.dto.android.legacy.LegacyRefreshTokenResponse;
import com.unisbase.dto.LoginRequest;
import com.unisbase.dto.RefreshRequest;
import com.unisbase.dto.SysRoleDTO;
import com.unisbase.dto.SysUserDTO;
import com.unisbase.dto.TokenResponse;
import com.unisbase.service.AuthService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.util.StringUtils;
import java.util.List;
@Tag(name = "兼容认证接口")
@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
public class LegacyAuthController {
private final AuthService authService;
@Operation(summary = "兼容登录")
@PostMapping("/login")
public LegacyApiResponse<LegacyLoginResponse> login(@Valid @RequestBody LoginRequest request) {
TokenResponse tokenResponse = null;
try {
tokenResponse = authService.login(request, true);
} catch (Exception e) {
return LegacyApiResponse.error("400",e.getMessage());
}
return LegacyApiResponse.ok(new LegacyLoginResponse(
tokenResponse.getAccessToken(),
tokenResponse.getRefreshToken(),
toLegacyUser(tokenResponse.getUser())
));
}
@Operation(summary = "兼容刷新令牌")
@PostMapping("/refresh")
public LegacyApiResponse<LegacyRefreshTokenResponse> refresh(@RequestBody(required = false) RefreshRequest request,
@RequestHeader(value = "Authorization", required = false) String authorization,
@RequestHeader(value = "X-Android-Access-Token", required = false) String androidAccessToken) {
TokenResponse tokenResponse = null;
try {
tokenResponse = authService.refresh(resolveRefreshToken(request, authorization, androidAccessToken));
} catch (Exception e) {
return LegacyApiResponse.error("400",e.getMessage());
}
return LegacyApiResponse.ok(new LegacyRefreshTokenResponse(tokenResponse.getAccessToken(),tokenResponse.getRefreshToken()));
}
private LegacyLoginUserResponse toLegacyUser(SysUserDTO user) {
if (user == null) {
return null;
}
SysRoleDTO primaryRole = resolvePrimaryRole(user);
return new LegacyLoginUserResponse(
user.getUserId(),
user.getUsername(),
user.getDisplayName(),
user.getAvatarUrl(),
user.getEmail(),
primaryRole == null ? null : primaryRole.getRoleId(),
primaryRole == null ? null : primaryRole.getRoleName(),
user.getCreatedAt()
);
}
private SysRoleDTO resolvePrimaryRole(SysUserDTO user) {
List<SysRoleDTO> roles = user.getRoles();
if (roles != null && !roles.isEmpty()) {
return roles.get(0);
}
List<Long> roleIds = user.getRoleIds();
if (roleIds != null && !roleIds.isEmpty()) {
SysRoleDTO role = new SysRoleDTO();
role.setRoleId(roleIds.get(0));
return role;
}
return null;
}
private String resolveRefreshToken(RefreshRequest request, String authorization, String androidAccessToken) {
if (request != null && StringUtils.hasText(request.getRefreshToken())) {
return request.getRefreshToken().trim();
}
if (StringUtils.hasText(androidAccessToken)) {
return androidAccessToken.trim();
}
if (StringUtils.hasText(authorization)) {
String value = authorization.trim();
if (value.startsWith("Bearer ")) {
return value.substring(7).trim();
}
return value;
}
throw new IllegalArgumentException("refreshToken不能为空");
}
}

View File

@ -0,0 +1,38 @@
package com.imeeting.controller.android.legacy;
import com.imeeting.dto.android.legacy.LegacyApiResponse;
import com.imeeting.dto.android.legacy.LegacyClientDownloadResponse;
import com.imeeting.service.android.legacy.LegacyCatalogAdapterService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@Tag(name = "兼容客户端下载接口")
@RestController
@RequestMapping("/api/clients")
@RequiredArgsConstructor
public class LegacyClientController {
private final LegacyCatalogAdapterService legacyCatalogAdapterService;
@Operation(summary = "查询平台最新客户端")
@GetMapping("/latest/by-platform")
public LegacyApiResponse<LegacyClientDownloadResponse> latestByPlatform(@RequestParam(value = "platform_code", required = false) String platformCode,
@RequestParam(value = "platform_type", required = false) String platformType,
@RequestParam(value = "platform_name", required = false) String platformName) {
if ((platformCode == null || platformCode.isBlank())
&& ((platformType == null || platformType.isBlank()) || (platformName == null || platformName.isBlank()))) {
return LegacyApiResponse.error("400", "请提供 platform_code 参数");
}
LegacyClientDownloadResponse response = legacyCatalogAdapterService.getLatestClient(platformCode, platformType, platformName);
if (response == null) {
return LegacyApiResponse.error("404", "暂无最新客户端");
}
return LegacyApiResponse.ok(response);
}
}

View File

@ -0,0 +1,29 @@
package com.imeeting.controller.android.legacy;
import com.imeeting.dto.android.legacy.LegacyApiResponse;
import com.imeeting.dto.android.legacy.LegacyExternalAppItemResponse;
import com.imeeting.service.android.legacy.LegacyCatalogAdapterService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@Tag(name = "兼容外部应用接口")
@RestController
@RequestMapping("/api/external-apps")
@RequiredArgsConstructor
public class LegacyExternalAppController {
private final LegacyCatalogAdapterService legacyCatalogAdapterService;
@Operation(summary = "查询启用的外部应用")
@GetMapping("/active")
public LegacyApiResponse<List<LegacyExternalAppItemResponse>> active(@RequestParam(value = "is_active", required = false) Integer ignoredIsActive) {
return LegacyApiResponse.ok(legacyCatalogAdapterService.listActiveExternalApps());
}
}

View File

@ -0,0 +1,55 @@
package com.imeeting.controller.android.legacy;
import com.imeeting.dto.android.legacy.LegacyApiResponse;
import com.imeeting.dto.android.legacy.LegacyLlmModelItemResponse;
import com.imeeting.dto.biz.AiModelVO;
import com.imeeting.service.biz.AiModelService;
import com.unisbase.dto.PageResult;
import com.unisbase.security.LoginUser;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import java.util.Objects;
@Tag(name = "兼容模型接口")
@RestController
@RequestMapping("/api/llm-models")
@RequiredArgsConstructor
public class LegacyLlmModelController {
private final AiModelService aiModelService;
@Operation(summary = "查询启用的大模型列表")
@GetMapping("/active")
@PreAuthorize("isAuthenticated()")
public LegacyApiResponse<List<LegacyLlmModelItemResponse>> activeModels() {
LoginUser loginUser = currentLoginUser();
PageResult<List<AiModelVO>> result = aiModelService.pageModels(1, 1000, null, "LLM", loginUser.getTenantId());
List<AiModelVO> enabledModels = result.getRecords() == null
? List.of()
: result.getRecords().stream()
.filter(item -> Integer.valueOf(1).equals(item.getStatus()))
.toList();
boolean hasExplicitDefault = enabledModels.stream().anyMatch(item -> Integer.valueOf(1).equals(item.getIsDefault()));
Long fallbackDefaultId = enabledModels.isEmpty() ? null : enabledModels.get(0).getId();
List<LegacyLlmModelItemResponse> models = enabledModels.stream()
.map(item -> LegacyLlmModelItemResponse.from(
item,
Integer.valueOf(1).equals(item.getIsDefault())
|| (!hasExplicitDefault && Objects.equals(item.getId(), fallbackDefaultId))
))
.toList();
return LegacyApiResponse.ok(models);
}
private LoginUser currentLoginUser() {
return (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
}
}

View File

@ -0,0 +1,599 @@
package com.imeeting.controller.android.legacy;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.imeeting.common.RedisKeys;
import com.imeeting.dto.android.legacy.LegacyApiResponse;
import com.imeeting.dto.android.legacy.LegacyMeetingAccessPasswordRequest;
import com.imeeting.dto.android.legacy.LegacyMeetingAccessPasswordResponse;
import com.imeeting.dto.android.legacy.LegacyMeetingAttendeeResponse;
import com.imeeting.dto.android.legacy.LegacyMeetingCreateRequest;
import com.imeeting.dto.android.legacy.LegacyMeetingCreateResponse;
import com.imeeting.dto.android.legacy.LegacyMeetingItemResponse;
import com.imeeting.dto.android.legacy.LegacyMeetingListResponse;
import com.imeeting.dto.android.legacy.LegacyMeetingPreviewDataResponse;
import com.imeeting.dto.android.legacy.LegacyMeetingPreviewResult;
import com.imeeting.dto.android.legacy.LegacyMeetingProcessingStatusResponse;
import com.imeeting.dto.android.legacy.LegacyMeetingTagResponse;
import com.imeeting.dto.biz.MeetingVO;
import com.imeeting.entity.biz.AiTask;
import com.imeeting.entity.biz.Meeting;
import com.imeeting.mapper.biz.MeetingTranscriptMapper;
import com.imeeting.entity.biz.PromptTemplate;
import com.imeeting.service.android.legacy.LegacyMeetingAdapterService;
import com.imeeting.service.biz.AiTaskService;
import com.imeeting.service.biz.MeetingAccessService;
import com.imeeting.service.biz.MeetingCommandService;
import com.imeeting.service.biz.MeetingQueryService;
import com.imeeting.service.biz.MeetingService;
import com.imeeting.service.biz.PromptTemplateService;
import com.unisbase.dto.PageResult;
import com.unisbase.entity.SysUser;
import com.unisbase.mapper.SysUserMapper;
import com.unisbase.security.LoginUser;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
@Tag(name = "兼容会议接口")
@RestController
@RequestMapping("/api/meetings")
public class LegacyMeetingController {
private static final String STAGE_DATA_INITIALIZATION = "data_initialization";
private static final String STAGE_AUDIO_TRANSCRIPTION = "audio_transcription";
private static final String STAGE_SUMMARY_GENERATION = "summary_generation";
private static final String STAGE_COMPLETED = "completed";
private final LegacyMeetingAdapterService legacyMeetingAdapterService;
private final MeetingQueryService meetingQueryService;
private final MeetingAccessService meetingAccessService;
private final MeetingCommandService meetingCommandService;
private final MeetingService meetingService;
private final AiTaskService aiTaskService;
private final PromptTemplateService promptTemplateService;
private final MeetingTranscriptMapper meetingTranscriptMapper;
private final SysUserMapper sysUserMapper;
private final StringRedisTemplate redisTemplate;
private final ObjectMapper objectMapper;
public LegacyMeetingController(LegacyMeetingAdapterService legacyMeetingAdapterService,
MeetingQueryService meetingQueryService,
MeetingAccessService meetingAccessService,
MeetingCommandService meetingCommandService,
MeetingService meetingService,
AiTaskService aiTaskService,
PromptTemplateService promptTemplateService,
MeetingTranscriptMapper meetingTranscriptMapper,
SysUserMapper sysUserMapper) {
this(legacyMeetingAdapterService,
meetingQueryService,
meetingAccessService,
meetingCommandService,
meetingService,
aiTaskService,
promptTemplateService,
meetingTranscriptMapper,
sysUserMapper,
null,
new ObjectMapper());
}
@Autowired
public LegacyMeetingController(LegacyMeetingAdapterService legacyMeetingAdapterService,
MeetingQueryService meetingQueryService,
MeetingAccessService meetingAccessService,
MeetingCommandService meetingCommandService,
MeetingService meetingService,
AiTaskService aiTaskService,
PromptTemplateService promptTemplateService,
MeetingTranscriptMapper meetingTranscriptMapper,
SysUserMapper sysUserMapper,
StringRedisTemplate redisTemplate,
ObjectMapper objectMapper) {
this.legacyMeetingAdapterService = legacyMeetingAdapterService;
this.meetingQueryService = meetingQueryService;
this.meetingAccessService = meetingAccessService;
this.meetingCommandService = meetingCommandService;
this.meetingService = meetingService;
this.aiTaskService = aiTaskService;
this.promptTemplateService = promptTemplateService;
this.meetingTranscriptMapper = meetingTranscriptMapper;
this.sysUserMapper = sysUserMapper;
this.redisTemplate = redisTemplate;
this.objectMapper = objectMapper;
}
@Operation(summary = "兼容创建会议")
@PostMapping
@PreAuthorize("isAuthenticated()")
public LegacyApiResponse<LegacyMeetingCreateResponse> create(@RequestBody LegacyMeetingCreateRequest request) {
MeetingVO meeting = legacyMeetingAdapterService.createMeeting(request, currentLoginUser());
return LegacyApiResponse.ok(new LegacyMeetingCreateResponse(meeting.getId()));
}
@Operation(summary = "兼容上传会议音频")
@PostMapping("/upload-audio")
@PreAuthorize("isAuthenticated()")
public LegacyApiResponse<Void> uploadAudio(@RequestParam("meeting_id") Long meetingId,
@RequestParam(value = "prompt_id", required = false) Long promptId,
@RequestParam(value = "model_code", required = false) String modelCode,
@RequestParam(value = "force_replace", defaultValue = "false") boolean forceReplace,
@RequestParam("audio_file") MultipartFile audioFile) throws IOException {
legacyMeetingAdapterService.uploadAndTriggerOfflineProcess(
meetingId,
promptId,
modelCode,
forceReplace,
audioFile,
currentLoginUser()
);
return LegacyApiResponse.ok("上传成功", null);
}
@Operation(summary = "兼容分页查询会议")
@GetMapping
@PreAuthorize("isAuthenticated()")
public LegacyApiResponse<LegacyMeetingListResponse> list(@RequestParam(value = "user_id", required = false) Long ignoredUserId,
@RequestParam(defaultValue = "1") Integer page,
@RequestParam(value = "page_size", defaultValue = "10") Integer pageSize,
@RequestParam(required = false) String title) {
LoginUser loginUser = currentLoginUser();
boolean isAdmin = Boolean.TRUE.equals(loginUser.getIsPlatformAdmin()) || Boolean.TRUE.equals(loginUser.getIsTenantAdmin());
PageResult<List<MeetingVO>> result = meetingQueryService.pageMeetings(
page,
pageSize,
title,
loginUser.getTenantId(),
loginUser.getUserId(),
resolveCreatorName(loginUser),
"all",
isAdmin
);
LegacyMeetingListResponse data = new LegacyMeetingListResponse();
data.setPage(page);
data.setPageSize(pageSize);
data.setTotal(result.getTotal());
data.setTotalPages(pageSize == null || pageSize <= 0 ? 0 : (result.getTotal() + pageSize - 1) / pageSize);
data.setHasMore(page != null && page < data.getTotalPages());
data.setMeetings(result.getRecords() == null
? List.of()
: result.getRecords().stream().map(this::buildListItem).toList());
return LegacyApiResponse.ok(data);
}
@Operation(summary = "兼容查询会议预览数据")
@GetMapping("/{meetingId}/preview-data")
public LegacyApiResponse<?> previewData(@PathVariable Long meetingId) {
LegacyMeetingPreviewResult result = buildPreviewResult(meetingId);
return new LegacyApiResponse<>(result.getCode(), result.getMessage(), result.getData());
}
@Operation(summary = "兼容更新会议访问密码")
@PutMapping("/{meetingId}/access-password")
@PreAuthorize("isAuthenticated()")
public LegacyApiResponse<LegacyMeetingAccessPasswordResponse> updateAccessPassword(@PathVariable Long meetingId,
@RequestBody(required = false) LegacyMeetingAccessPasswordRequest request) {
LoginUser loginUser = currentLoginUser();
Meeting meeting = meetingAccessService.requireMeeting(meetingId);
if (!Objects.equals(meeting.getCreatorId(), loginUser.getUserId())) {
return LegacyApiResponse.error("403", "仅会议创建人可设置访问密码");
}
String password = normalizePassword(request == null ? null : request.getPassword());
meeting.setAccessPassword(password);
meetingService.updateById(meeting);
return LegacyApiResponse.ok(new LegacyMeetingAccessPasswordResponse(password));
}
@Operation(summary = "兼容删除会议")
@DeleteMapping("/{meetingId}")
@PreAuthorize("isAuthenticated()")
public LegacyApiResponse<Void> delete(@PathVariable Long meetingId) {
LoginUser loginUser = currentLoginUser();
Meeting meeting = meetingAccessService.requireMeeting(meetingId);
meetingAccessService.assertCanEditMeeting(meeting, loginUser);
meetingCommandService.deleteMeeting(meetingId);
return LegacyApiResponse.ok("删除成功", null);
}
private LegacyMeetingPreviewResult buildPreviewResult(Long meetingId) {
Meeting meeting = meetingService.getById(meetingId);
if (meeting == null) {
return new LegacyMeetingPreviewResult("404", "会议不存在", null);
}
AiTask asrTask = findLatestTask(meetingId, "ASR");
AiTask summaryTask = findLatestTask(meetingId, "SUMMARY");
boolean summaryCompleted = summaryTask != null && Integer.valueOf(2).equals(summaryTask.getStatus());
MeetingVO detail = (Integer.valueOf(3).equals(meeting.getStatus()) || summaryCompleted)
? meetingQueryService.getDetail(meetingId)
: null;
boolean hasSummary = detail != null && detail.getSummaryContent() != null && !detail.getSummaryContent().isBlank();
if (hasSummary) {
return new LegacyMeetingPreviewResult("200", "success", buildCompletedPreview(meeting, detail, summaryTask));
}
if (summaryCompleted) {
return new LegacyMeetingPreviewResult(
"504",
"处理已完成,但摘要尚未同步,请稍后重试",
buildProcessingPreview(meeting, summaryTask, processingStatus("摘要已生成,可扫码查看", 100, STAGE_COMPLETED))
);
}
if (isFailed(asrTask) || Integer.valueOf(4).equals(meeting.getStatus())) {
return new LegacyMeetingPreviewResult(
"503",
buildFailureMessage(asrTask, "转译"),
buildProcessingPreview(meeting, summaryTask, processingStatus("转译或总结失败", 50, STAGE_AUDIO_TRANSCRIPTION))
);
}
if (isFailed(summaryTask)) {
return new LegacyMeetingPreviewResult(
"503",
buildFailureMessage(summaryTask, "总结"),
buildProcessingPreview(meeting, summaryTask, processingStatus("转译或总结失败", 75, STAGE_SUMMARY_GENERATION))
);
}
Integer realtimeProgress = resolveRealtimeProgress(meetingId);
if (realtimeProgress != null) {
if (realtimeProgress < 90) {
return new LegacyMeetingPreviewResult(
"400",
"浼氳姝e湪澶勭悊涓?",
buildProcessingPreview(meeting, summaryTask, processingStatus("姝e湪杞瘧闊抽", 50, STAGE_AUDIO_TRANSCRIPTION))
);
}
if (realtimeProgress == 90) {
return new LegacyMeetingPreviewResult(
"400",
"浼氳姝e湪澶勭悊涓?",
buildProcessingPreview(meeting, summaryTask, processingStatus("姝e湪鐢熸垚鎬荤粨", 75, STAGE_SUMMARY_GENERATION))
);
}
}
boolean isSummaryStage = isSummaryStage(meeting.getStatus(), summaryTask);
boolean isAsrStage = isAsrStage(meeting.getStatus(), asrTask, hasAudio(meeting), isSummaryStage);
if (!isAsrStage && !isSummaryStage) {
return new LegacyMeetingPreviewResult(
"400",
"会议正在处理中",
buildProcessingPreview(meeting, summaryTask, processingStatus("会议数据准备中", 25, STAGE_DATA_INITIALIZATION))
);
}
if (!isSummaryStage) {
return new LegacyMeetingPreviewResult(
"400",
"会议正在处理中",
buildProcessingPreview(meeting, summaryTask, processingStatus("正在转译音频", 50, STAGE_AUDIO_TRANSCRIPTION))
);
}
return new LegacyMeetingPreviewResult(
"400",
"会议正在处理中",
buildProcessingPreview(meeting, summaryTask, processingStatus("正在生成总结", 75, STAGE_SUMMARY_GENERATION))
);
}
private LegacyMeetingPreviewDataResponse buildCompletedPreview(Meeting meeting, MeetingVO detail, AiTask summaryTask) {
LegacyMeetingPreviewDataResponse data = new LegacyMeetingPreviewDataResponse();
data.setMeetingId(meeting.getId());
data.setTitle(meeting.getTitle());
data.setMeetingTime(formatDateTime(meeting.getMeetingTime()));
data.setSummary(detail.getSummaryContent());
data.setCreatorUsername(resolveCreatorDisplayName(meeting.getCreatorId(), meeting.getCreatorName()));
Long promptId = resolvePromptId(summaryTask);
data.setPromptId(promptId);
data.setPromptName(resolvePromptName(promptId));
List<LegacyMeetingAttendeeResponse> attendees = buildAttendees(meeting.getParticipants());
data.setAttendees(attendees);
data.setAttendeesCount(attendees.size());
data.setHasPassword(meeting.getAccessPassword() != null && !meeting.getAccessPassword().isBlank());
data.setProcessingStatus(processingStatus("摘要已生成,可扫码查看", 100, STAGE_COMPLETED));
return data;
}
private LegacyMeetingItemResponse buildListItem(MeetingVO meeting) {
LegacyMeetingItemResponse item = new LegacyMeetingItemResponse();
item.setMeetingId(meeting.getId());
item.setTitle(meeting.getTitle());
item.setMeetingTime(formatDateTime(meeting.getMeetingTime()));
item.setCreatedAt(formatDateTime(meeting.getCreatedAt()));
item.setCreatorId(meeting.getCreatorId());
item.setCreatorUsername(resolveCreatorDisplayName(meeting.getCreatorId(), meeting.getCreatorName()));
item.setAudioFilePath(meeting.getAudioUrl());
item.setAudioDuration(meeting.getDuration());
item.setAccessPassword(resolveAccessPassword(meeting.getId()));
List<Long> attendeeIds = meeting.getParticipantIds() == null ? List.of() : meeting.getParticipantIds();
item.setAttendeeIds(attendeeIds);
item.setAttendees(buildAttendees(attendeeIds));
item.setTags(buildTags(meeting.getTags()));
item.setSummary(resolveListSummary(meeting.getId()));
LegacyMeetingProcessingStatusResponse status = buildListStatus(meeting);
item.setOverallStatus(status.getOverallStatus());
item.setOverallProgress(status.getOverallProgress());
item.setCurrentStage(translateListStage(status.getCurrentStage()));
return item;
}
private LegacyMeetingPreviewDataResponse buildProcessingPreview(Meeting meeting,
AiTask summaryTask,
LegacyMeetingProcessingStatusResponse status) {
LegacyMeetingPreviewDataResponse data = new LegacyMeetingPreviewDataResponse();
data.setMeetingId(meeting.getId());
data.setTitle(meeting.getTitle());
data.setMeetingTime(formatDateTime(meeting.getMeetingTime()));
data.setCreatorUsername(resolveCreatorDisplayName(meeting.getCreatorId(), meeting.getCreatorName()));
Long promptId = resolvePromptId(summaryTask);
data.setPromptId(promptId);
data.setPromptName(resolvePromptName(promptId));
data.setHasPassword(meeting.getAccessPassword() != null && !meeting.getAccessPassword().isBlank());
data.setProcessingStatus(status);
return data;
}
private LegacyMeetingProcessingStatusResponse processingStatus(String overallStatus, int overallProgress, String currentStage) {
return new LegacyMeetingProcessingStatusResponse(overallStatus, overallProgress, currentStage);
}
private Integer resolveRealtimeProgress(Long meetingId) {
if (redisTemplate == null) {
return null;
}
String rawProgress = redisTemplate.opsForValue().get(RedisKeys.meetingProgressKey(meetingId));
if (rawProgress == null || rawProgress.isBlank()) {
return null;
}
try {
JsonNode progress = objectMapper.readTree(rawProgress);
return progress.hasNonNull("percent") ? progress.path("percent").asInt() : null;
} catch (Exception ignored) {
return null;
}
}
private LegacyMeetingProcessingStatusResponse buildListStatus(MeetingVO meeting) {
Long meetingId = meeting.getId();
AiTask asrTask = findLatestTask(meetingId, "ASR");
AiTask summaryTask = findLatestTask(meetingId, "SUMMARY");
boolean summaryCompleted = summaryTask != null && Integer.valueOf(2).equals(summaryTask.getStatus());
if (Integer.valueOf(3).equals(meeting.getStatus()) || summaryCompleted) {
return new LegacyMeetingProcessingStatusResponse("completed", 100, STAGE_COMPLETED);
}
if (isFailed(asrTask) || Integer.valueOf(4).equals(meeting.getStatus())) {
return new LegacyMeetingProcessingStatusResponse("failed", 50, STAGE_AUDIO_TRANSCRIPTION);
}
if (isFailed(summaryTask)) {
return new LegacyMeetingProcessingStatusResponse("failed", 75, STAGE_SUMMARY_GENERATION);
}
boolean isSummaryStage = isSummaryStage(meeting.getStatus(), summaryTask);
boolean isAsrStage = isAsrStage(meeting.getStatus(), asrTask, hasAudio(meeting), isSummaryStage);
if (!isAsrStage && !isSummaryStage) {
return new LegacyMeetingProcessingStatusResponse("pending", 0, STAGE_DATA_INITIALIZATION);
}
if (isSummaryStage) {
return new LegacyMeetingProcessingStatusResponse("summarizing", 75, STAGE_SUMMARY_GENERATION);
}
return new LegacyMeetingProcessingStatusResponse("transcribing", 50, STAGE_AUDIO_TRANSCRIPTION);
}
private String buildFailureMessage(AiTask failedTask, String stageName) {
String error = failedTask == null || failedTask.getErrorMsg() == null || failedTask.getErrorMsg().isBlank()
? "处理失败"
: failedTask.getErrorMsg();
return "会议" + stageName + "失败: " + error;
}
private boolean isRunningAsr(AiTask task) {
return task != null && (Integer.valueOf(0).equals(task.getStatus()) || Integer.valueOf(1).equals(task.getStatus()));
}
private boolean isRunningSummary(AiTask task) {
return task != null && (Integer.valueOf(0).equals(task.getStatus()) || Integer.valueOf(1).equals(task.getStatus()));
}
private boolean isFailed(AiTask task) {
return task != null && Integer.valueOf(3).equals(task.getStatus());
}
private AiTask findLatestTask(Long meetingId, String taskType) {
return aiTaskService.getOne(new LambdaQueryWrapper<AiTask>()
.eq(AiTask::getMeetingId, meetingId)
.eq(AiTask::getTaskType, taskType)
.orderByDesc(AiTask::getId)
.last("LIMIT 1"));
}
private Long resolvePromptId(AiTask summaryTask) {
if (summaryTask == null || summaryTask.getTaskConfig() == null) {
return null;
}
Object rawPromptId = summaryTask.getTaskConfig().get("promptId");
if (rawPromptId == null) {
return null;
}
if (rawPromptId instanceof Number number) {
return number.longValue();
}
String value = String.valueOf(rawPromptId).trim();
if (value.isEmpty()) {
return null;
}
try {
return Long.parseLong(value);
} catch (NumberFormatException ignored) {
return null;
}
}
private String resolvePromptName(Long promptId) {
if (promptId == null) {
return null;
}
PromptTemplate template = promptTemplateService.getById(promptId);
return template == null ? null : template.getTemplateName();
}
private List<LegacyMeetingAttendeeResponse> buildAttendees(String participants) {
return buildAttendees(parseParticipantIds(participants));
}
private List<LegacyMeetingAttendeeResponse> buildAttendees(List<Long> participantIds) {
if (participantIds == null || participantIds.isEmpty()) {
return List.of();
}
Map<Long, SysUser> userMap = sysUserMapper.selectBatchIds(participantIds).stream()
.collect(Collectors.toMap(SysUser::getUserId, user -> user, (left, right) -> left, LinkedHashMap::new));
return participantIds.stream()
.map(userId -> {
SysUser user = userMap.get(userId);
String caption = user == null
? String.valueOf(userId)
: (user.getDisplayName() != null ? user.getDisplayName() : user.getUsername());
String username = user == null ? null : user.getUsername();
return new LegacyMeetingAttendeeResponse(userId, username, caption);
})
.toList();
}
private List<LegacyMeetingTagResponse> buildTags(String rawTags) {
if (rawTags == null || rawTags.isBlank()) {
return List.of();
}
return Arrays.stream(rawTags.split(","))
.map(String::trim)
.filter(value -> !value.isEmpty())
.map(value -> new LegacyMeetingTagResponse(null, value))
.toList();
}
private List<Long> parseParticipantIds(String participants) {
if (participants == null || participants.isBlank()) {
return List.of();
}
return Arrays.stream(participants.split(","))
.map(String::trim)
.filter(value -> !value.isEmpty())
.map(value -> {
try {
return Long.parseLong(value);
} catch (NumberFormatException ignored) {
return null;
}
})
.filter(Objects::nonNull)
.toList();
}
private String normalizePassword(String password) {
if (password == null) {
return null;
}
String normalized = password.trim();
return normalized.isEmpty() ? null : normalized;
}
private String resolveListSummary(Long meetingId) {
MeetingVO detail = meetingQueryService.getDetail(meetingId);
if (detail == null || detail.getSummaryContent() == null || detail.getSummaryContent().isBlank()) {
return null;
}
String summary = detail.getSummaryContent().trim();
return summary.length() <= 240 ? summary : summary.substring(0, 240);
}
private String resolveAccessPassword(Long meetingId) {
Meeting meeting = meetingService.getById(meetingId);
return meeting == null ? null : normalizePassword(meeting.getAccessPassword());
}
private String resolveCreatorDisplayName(Long creatorId, String fallbackName) {
if (creatorId == null) {
return fallbackName;
}
SysUser creator = sysUserMapper.selectById(creatorId);
if (creator == null) {
return fallbackName;
}
if (creator.getDisplayName() != null && !creator.getDisplayName().isBlank()) {
return creator.getDisplayName();
}
if (creator.getUsername() != null && !creator.getUsername().isBlank()) {
return creator.getUsername();
}
return fallbackName;
}
private boolean hasAudio(Meeting meeting) {
return meeting.getAudioUrl() != null && !meeting.getAudioUrl().isBlank();
}
private boolean hasAudio(MeetingVO meeting) {
return meeting.getAudioUrl() != null && !meeting.getAudioUrl().isBlank();
}
private boolean isSummaryStage(Integer meetingStatus, AiTask summaryTask) {
return Integer.valueOf(2).equals(meetingStatus) || isRunningSummary(summaryTask);
}
private boolean isAsrStage(Integer meetingStatus, AiTask asrTask, boolean hasAudio, boolean isSummaryStage) {
return Integer.valueOf(1).equals(meetingStatus)
|| isRunningAsr(asrTask)
|| (hasAudio && !isSummaryStage);
}
private String formatDateTime(LocalDateTime value) {
return value == null ? null : value.toString();
}
private String translateListStage(String stage) {
if (STAGE_SUMMARY_GENERATION.equals(stage)) {
return "llm";
}
if (STAGE_COMPLETED.equals(stage)) {
return "completed";
}
return "transcription";
}
private LoginUser currentLoginUser() {
return (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
}
private String resolveCreatorName(LoginUser loginUser) {
return loginUser.getDisplayName() != null ? loginUser.getDisplayName() : loginUser.getUsername();
}
}

View File

@ -0,0 +1,67 @@
package com.imeeting.controller.android.legacy;
import com.imeeting.dto.android.legacy.LegacyApiResponse;
import com.imeeting.dto.android.legacy.LegacyPromptItemResponse;
import com.imeeting.dto.android.legacy.LegacyPromptListResponse;
import com.imeeting.dto.biz.PromptTemplateVO;
import com.imeeting.service.biz.PromptTemplateService;
import com.unisbase.dto.PageResult;
import com.unisbase.security.LoginUser;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import java.util.Objects;
@Tag(name = "兼容提示词接口")
@RestController
@RequestMapping("/api/prompts")
@RequiredArgsConstructor
public class LegacyPromptController {
private static final String LEGACY_MEETING_SCENE = "MEETING_TASK";
private final PromptTemplateService promptTemplateService;
@Operation(summary = "查询场景提示词")
@GetMapping("/active/{scene}")
@PreAuthorize("isAuthenticated()")
public LegacyApiResponse<LegacyPromptListResponse> activePrompts(@PathVariable String scene) {
if (!LEGACY_MEETING_SCENE.equals(scene)) {
return LegacyApiResponse.error("400", "scene only supports MEETING_TASK");
}
LoginUser loginUser = currentLoginUser();
PageResult<List<PromptTemplateVO>> result = promptTemplateService.pageTemplates(
1,
1000,
null,
null,
loginUser.getTenantId(),
loginUser.getUserId(),
loginUser.getIsPlatformAdmin(),
loginUser.getIsTenantAdmin()
);
List<PromptTemplateVO> enabledTemplates = result.getRecords() == null
? List.of()
: result.getRecords().stream()
.filter(item -> Integer.valueOf(1).equals(item.getStatus()))
.toList();
Long defaultTemplateId = enabledTemplates.isEmpty() ? null : enabledTemplates.get(0).getId();
List<LegacyPromptItemResponse> prompts = enabledTemplates.stream()
.map(item -> LegacyPromptItemResponse.from(item, Objects.equals(item.getId(), defaultTemplateId)))
.toList();
return LegacyApiResponse.ok(new LegacyPromptListResponse(prompts));
}
private LoginUser currentLoginUser() {
return (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
}
}

View File

@ -0,0 +1,28 @@
package com.imeeting.controller.android.legacy;
import com.imeeting.dto.android.legacy.LegacyApiResponse;
import com.imeeting.dto.android.legacy.LegacyScreenSaverItemResponse;
import com.imeeting.service.android.legacy.LegacyScreenSaverAdapterService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@Tag(name = "兼容屏保接口")
@RestController
@RequestMapping("/api/screensavers")
@RequiredArgsConstructor
public class LegacyScreenSaverController {
private final LegacyScreenSaverAdapterService legacyScreenSaverAdapterService;
@Operation(summary = "查询启用的屏保列表")
@GetMapping("/active")
public LegacyApiResponse<List<LegacyScreenSaverItemResponse>> active() {
return LegacyApiResponse.ok(legacyScreenSaverAdapterService.listActiveScreenSavers());
}
}

View File

@ -0,0 +1,124 @@
package com.imeeting.controller.biz;
import com.imeeting.dto.biz.AiModelDTO;
import com.imeeting.dto.biz.AiLocalProfileVO;
import com.imeeting.dto.biz.AiModelVO;
import com.imeeting.service.biz.AiModelService;
import com.unisbase.common.ApiResponse;
import com.unisbase.dto.PageResult;
import com.unisbase.security.LoginUser;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@Tag(name = "AI模型管理")
@RestController
@RequestMapping("/api/biz/aimodel")
public class AiModelController {
private final AiModelService aiModelService;
public AiModelController(AiModelService aiModelService) {
this.aiModelService = aiModelService;
}
@Operation(summary = "新增AI模型")
@PostMapping
@PreAuthorize("isAuthenticated()")
public ApiResponse<AiModelVO> save(@RequestBody AiModelDTO dto) {
return ApiResponse.ok(aiModelService.saveModel(dto));
}
@Operation(summary = "更新AI模型")
@PutMapping
@PreAuthorize("isAuthenticated()")
public ApiResponse<AiModelVO> update(@RequestBody AiModelDTO dto) {
if (dto.getId() == null) {
return ApiResponse.error("模型ID不能为空");
}
if (dto.getModelType() == null || dto.getModelType().isBlank()) {
return ApiResponse.error("模型类型不能为空");
}
LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
AiModelVO existing = aiModelService.getModelById(dto.getId(), dto.getModelType());
if (existing == null) {
return ApiResponse.error("模型不存在");
}
if (Long.valueOf(0L).equals(existing.getTenantId()) && !Boolean.TRUE.equals(loginUser.getIsPlatformAdmin())) {
return ApiResponse.error("无权修改系统级模型");
}
return ApiResponse.ok(aiModelService.updateModel(dto));
}
@Operation(summary = "删除AI模型")
@DeleteMapping("/{id}")
@PreAuthorize("isAuthenticated()")
public ApiResponse<Boolean> delete(@PathVariable Long id, @RequestParam String type) {
if (type == null || type.isBlank()) {
return ApiResponse.error("模型类型不能为空");
}
LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
AiModelVO existing = aiModelService.getModelById(id, type);
if (existing == null) {
return ApiResponse.ok(true);
}
if (Long.valueOf(0L).equals(existing.getTenantId()) && !Boolean.TRUE.equals(loginUser.getIsPlatformAdmin())) {
return ApiResponse.error("无权删除系统级模型");
}
return ApiResponse.ok(aiModelService.removeModelById(id, type));
}
@Operation(summary = "分页查询AI模型")
@GetMapping("/page")
@PreAuthorize("isAuthenticated()")
public ApiResponse<PageResult<List<AiModelVO>>> page(
@RequestParam(defaultValue = "1") Integer current,
@RequestParam(defaultValue = "10") Integer size,
@RequestParam(required = false) String name,
@RequestParam(required = false) String type) {
LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
return ApiResponse.ok(aiModelService.pageModels(current, size, name, type, loginUser.getTenantId()));
}
@Operation(summary = "拉取远程模型列表")
@GetMapping("/remote-list")
@PreAuthorize("isAuthenticated()")
public ApiResponse<List<String>> remoteList(
@RequestParam String provider,
@RequestParam String baseUrl,
@RequestParam(required = false) String apiKey) {
return ApiResponse.ok(aiModelService.fetchRemoteModels(provider, baseUrl, apiKey));
}
@Operation(summary = "测试本地模型连通性")
@PostMapping("/local-connectivity-test")
@PreAuthorize("isAuthenticated()")
public ApiResponse<AiLocalProfileVO> testLocalConnectivity(@RequestBody AiModelDTO dto) {
if (dto.getBaseUrl() == null || dto.getBaseUrl().isBlank()) {
return ApiResponse.error("Base URL不能为空");
}
if (dto.getApiKey() == null || dto.getApiKey().isBlank()) {
return ApiResponse.error("API Key不能为空");
}
return ApiResponse.ok(aiModelService.testLocalConnectivity(dto.getBaseUrl(), dto.getApiKey()));
}
@Operation(summary = "获取默认AI模型")
@GetMapping("/default")
@PreAuthorize("isAuthenticated()")
public ApiResponse<AiModelVO> getDefault(@RequestParam String type) {
LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
return ApiResponse.ok(aiModelService.getDefaultModel(type, loginUser.getTenantId()));
}
}

View File

@ -0,0 +1,86 @@
package com.imeeting.controller.biz;
import com.imeeting.dto.biz.ClientDownloadDTO;
import com.imeeting.entity.biz.ClientDownload;
import com.imeeting.service.biz.ClientDownloadService;
import com.unisbase.common.ApiResponse;
import com.unisbase.security.LoginUser;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Tag(name = "客户端下载管理")
@RestController
@RequestMapping("/api/clients")
@RequiredArgsConstructor
public class ClientDownloadController {
private final ClientDownloadService clientDownloadService;
@Operation(summary = "查询客户端下载包列表")
@GetMapping
@PreAuthorize("isAuthenticated()")
public ApiResponse<Map<String, Object>> list(@RequestParam(value = "platformCode", required = false) String platformCode,
@RequestParam(value = "status", required = false) Integer status,
@RequestParam(value = "page", defaultValue = "1") Integer page,
@RequestParam(value = "size", defaultValue = "50") Integer size) {
List<ClientDownload> clients = clientDownloadService.listForAdmin(currentLoginUser(), platformCode, status);
Map<String, Object> data = new HashMap<>();
data.put("clients", clients);
data.put("total", clients.size());
data.put("page", page);
data.put("size", size);
return ApiResponse.ok(data);
}
@Operation(summary = "新增客户端下载包")
@PostMapping
@PreAuthorize("isAuthenticated()")
public ApiResponse<ClientDownload> create(@RequestBody ClientDownloadDTO dto) {
return ApiResponse.ok(clientDownloadService.create(dto, currentLoginUser()));
}
@Operation(summary = "修改客户端下载包")
@PutMapping("/{id}")
@PreAuthorize("isAuthenticated()")
public ApiResponse<ClientDownload> update(@PathVariable Long id, @RequestBody ClientDownloadDTO dto) {
return ApiResponse.ok(clientDownloadService.update(id, dto, currentLoginUser()));
}
@Operation(summary = "删除客户端下载包")
@DeleteMapping("/{id}")
@PreAuthorize("isAuthenticated()")
public ApiResponse<Boolean> delete(@PathVariable Long id) {
clientDownloadService.removeClient(id, currentLoginUser());
return ApiResponse.ok(true);
}
@Operation(summary = "上传客户端安装包")
@PostMapping("/upload")
@PreAuthorize("isAuthenticated()")
public ApiResponse<Map<String, Object>> upload(@RequestParam("platformCode") String platformCode,
@RequestParam("file") MultipartFile file) throws IOException {
return ApiResponse.ok(clientDownloadService.uploadPackage(platformCode, file));
}
private LoginUser currentLoginUser() {
return (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
}
}

View File

@ -0,0 +1,45 @@
package com.imeeting.controller.biz;
import com.imeeting.dto.biz.MeetingVO;
import com.imeeting.service.biz.MeetingQueryService;
import com.unisbase.common.ApiResponse;
import com.unisbase.security.LoginUser;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import java.util.Map;
@Tag(name = "工作台")
@RestController
@RequestMapping("/api/biz/dashboard")
public class DashboardController {
private final MeetingQueryService meetingQueryService;
public DashboardController(MeetingQueryService meetingQueryService) {
this.meetingQueryService = meetingQueryService;
}
@Operation(summary = "获取工作台统计数据")
@GetMapping("/stats")
@PreAuthorize("isAuthenticated()")
public ApiResponse<Map<String, Object>> getStats() {
LoginUser user = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
boolean isAdmin = Boolean.TRUE.equals(user.getIsPlatformAdmin()) || Boolean.TRUE.equals(user.getIsTenantAdmin());
return ApiResponse.ok(meetingQueryService.getDashboardStats(user.getTenantId(), user.getUserId(), isAdmin));
}
@Operation(summary = "获取最近会议列表")
@GetMapping("/recent")
@PreAuthorize("isAuthenticated()")
public ApiResponse<List<MeetingVO>> getRecent() {
LoginUser user = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
boolean isAdmin = Boolean.TRUE.equals(user.getIsPlatformAdmin()) || Boolean.TRUE.equals(user.getIsTenantAdmin());
return ApiResponse.ok(meetingQueryService.getRecentMeetings(user.getTenantId(), user.getUserId(), isAdmin, 10));
}
}

View File

@ -0,0 +1,83 @@
package com.imeeting.controller.biz;
import com.imeeting.dto.biz.ExternalAppDTO;
import com.imeeting.entity.biz.ExternalApp;
import com.imeeting.service.biz.ExternalAppService;
import com.unisbase.common.ApiResponse;
import com.unisbase.security.LoginUser;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.util.List;
import java.util.Map;
@Tag(name = "外部应用管理")
@RestController
@RequestMapping("/api/external-apps")
@RequiredArgsConstructor
public class ExternalAppController {
private final ExternalAppService externalAppService;
@Operation(summary = "查询外部应用列表")
@GetMapping
@PreAuthorize("isAuthenticated()")
public ApiResponse<List<Map<String, Object>>> list(@RequestParam(value = "appType", required = false) String appType,
@RequestParam(value = "status", required = false) Integer status) {
return ApiResponse.ok(externalAppService.listForAdmin(currentLoginUser(), appType, status));
}
@Operation(summary = "新增外部应用")
@PostMapping
@PreAuthorize("isAuthenticated()")
public ApiResponse<ExternalApp> create(@RequestBody ExternalAppDTO dto) {
return ApiResponse.ok(externalAppService.create(dto, currentLoginUser()));
}
@Operation(summary = "修改外部应用")
@PutMapping("/{id}")
@PreAuthorize("isAuthenticated()")
public ApiResponse<ExternalApp> update(@PathVariable Long id, @RequestBody ExternalAppDTO dto) {
return ApiResponse.ok(externalAppService.update(id, dto, currentLoginUser()));
}
@Operation(summary = "删除外部应用")
@DeleteMapping("/{id}")
@PreAuthorize("isAuthenticated()")
public ApiResponse<Boolean> delete(@PathVariable Long id) {
externalAppService.removeApp(id, currentLoginUser());
return ApiResponse.ok(true);
}
@Operation(summary = "上传外部应用APK")
@PostMapping("/upload-apk")
@PreAuthorize("isAuthenticated()")
public ApiResponse<Map<String, Object>> uploadApk(@RequestParam("apkFile") MultipartFile apkFile) throws IOException {
return ApiResponse.ok(externalAppService.uploadApk(apkFile));
}
@Operation(summary = "上传外部应用图标")
@PostMapping("/upload-icon")
@PreAuthorize("isAuthenticated()")
public ApiResponse<Map<String, Object>> uploadIcon(@RequestParam("iconFile") MultipartFile iconFile) throws IOException {
return ApiResponse.ok(externalAppService.uploadIcon(iconFile));
}
private LoginUser currentLoginUser() {
return (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
}
}

View File

@ -0,0 +1,170 @@
package com.imeeting.controller.biz;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.imeeting.dto.biz.HotWordDTO;
import com.imeeting.dto.biz.HotWordVO;
import com.imeeting.entity.biz.HotWord;
import com.imeeting.service.biz.HotWordService;
import com.unisbase.common.ApiResponse;
import com.unisbase.dto.PageResult;
import com.unisbase.security.LoginUser;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.stream.Collectors;
@Tag(name = "热词管理")
@RestController
@RequestMapping("/api/biz/hotword")
public class HotWordController {
private final HotWordService hotWordService;
public HotWordController(HotWordService hotWordService) {
this.hotWordService = hotWordService;
}
/**
* ()
*/
private boolean isCurrentUserAdmin(LoginUser user) {
return Boolean.TRUE.equals(user.getIsPlatformAdmin()) || Boolean.TRUE.equals(user.getIsTenantAdmin());
}
@Operation(summary = "新增热词")
@PostMapping
@PreAuthorize("isAuthenticated()")
public ApiResponse<HotWordVO> save(@RequestBody HotWordDTO hotWordDTO) {
LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
// 核心校验:只有管理员可以创建公开热词
if (Integer.valueOf(1).equals(hotWordDTO.getIsPublic()) && !isCurrentUserAdmin(loginUser)) {
return ApiResponse.error("无权创建租户公开热词,请设为个人私有");
}
return ApiResponse.ok(hotWordService.saveHotWord(hotWordDTO, loginUser.getUserId()));
}
@Operation(summary = "修改热词")
@PutMapping
@PreAuthorize("isAuthenticated()")
public ApiResponse<HotWordVO> update(@RequestBody HotWordDTO hotWordDTO) {
LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
HotWord existing = hotWordService.getById(hotWordDTO.getId());
if (existing == null) return ApiResponse.error("热词不存在");
boolean isAdmin = isCurrentUserAdmin(loginUser);
// 核心校验逻辑:
// 1. 如果用户尝试将热词设为公开,必须具备管理员权限
if (Integer.valueOf(1).equals(hotWordDTO.getIsPublic()) && !isAdmin) {
return ApiResponse.error("无权将热词设为公开");
}
// 2. 如果是公开热词,只有管理员能改
if (Integer.valueOf(1).equals(existing.getIsPublic())) {
if (!isAdmin) return ApiResponse.error("无权修改公开热词");
} else {
// 3. 如果是私有热词,本人或管理员能改
if (!existing.getCreatorId().equals(loginUser.getUserId()) && !isAdmin) {
return ApiResponse.error("无权修改他人私有热词");
}
}
return ApiResponse.ok(hotWordService.updateHotWord(hotWordDTO));
}
@Operation(summary = "删除热词")
@DeleteMapping("/{id}")
@PreAuthorize("isAuthenticated()")
public ApiResponse<Boolean> delete(@PathVariable Long id) {
LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
HotWord existing = hotWordService.getById(id);
if (existing == null) return ApiResponse.ok(true);
boolean isAdmin = isCurrentUserAdmin(loginUser);
// 权限校验:公开热词管理员可删,私有热词本人或管理员可删
if (Integer.valueOf(1).equals(existing.getIsPublic())) {
if (!isAdmin) return ApiResponse.error("无权删除公开热词");
} else {
if (!existing.getCreatorId().equals(loginUser.getUserId()) && !isAdmin) {
return ApiResponse.error("无权删除他人私有热词");
}
}
return ApiResponse.ok(hotWordService.removeById(id));
}
@Operation(summary = "分页查询热词")
@GetMapping("/page")
@PreAuthorize("isAuthenticated()")
public ApiResponse<PageResult<List<HotWordVO>>> page(
@RequestParam(defaultValue = "1") Integer current,
@RequestParam(defaultValue = "10") Integer size,
@RequestParam(required = false) String word,
@RequestParam(required = false) String category,
@RequestParam(required = false) Integer isPublic) {
LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
boolean isAdmin = isCurrentUserAdmin(loginUser);
LambdaQueryWrapper<HotWord> wrapper = new LambdaQueryWrapper<HotWord>()
.eq(HotWord::getTenantId, loginUser.getTenantId());
if (!isAdmin) {
// 普通用户:只能看到“已公开”的,或者“自己创建”的
wrapper.and(w -> w.eq(HotWord::getIsPublic, 1).or().eq(HotWord::getCreatorId, loginUser.getUserId()));
}
// 增加类型过滤
if (isPublic != null) {
wrapper.eq(HotWord::getIsPublic, isPublic);
}
wrapper.like(word != null && !word.isEmpty(), HotWord::getWord, word)
.eq(category != null && !category.isEmpty(), HotWord::getCategory, category)
.orderByDesc(HotWord::getIsPublic)
.orderByDesc(HotWord::getCreatedAt);
Page<HotWord> page = hotWordService.page(new Page<>(current, size), wrapper);
List<HotWordVO> vos = page.getRecords().stream().map(this::toVO).collect(Collectors.toList());
PageResult<List<HotWordVO>> result = new PageResult<>();
result.setTotal(page.getTotal());
result.setRecords(vos);
return ApiResponse.ok(result);
}
@Operation(summary = "生成热词拼音")
@GetMapping("/pinyin")
@PreAuthorize("isAuthenticated()")
public ApiResponse<List<String>> getPinyin(@RequestParam String word) {
return ApiResponse.ok(hotWordService.generatePinyin(word));
}
private HotWordVO toVO(HotWord entity) {
HotWordVO vo = new HotWordVO();
vo.setId(entity.getId());
vo.setWord(entity.getWord());
vo.setPinyinList(entity.getPinyinList());
vo.setMatchStrategy(entity.getMatchStrategy());
vo.setCategory(entity.getCategory());
vo.setWeight(entity.getWeight());
vo.setStatus(entity.getStatus());
vo.setIsPublic(entity.getIsPublic());
vo.setCreatorId(entity.getCreatorId());
vo.setIsSynced(entity.getIsSynced());
vo.setRemark(entity.getRemark());
vo.setCreatedAt(entity.getCreatedAt());
vo.setUpdatedAt(entity.getUpdatedAt());
return vo;
}
}

View File

@ -0,0 +1,459 @@
package com.imeeting.controller.biz;
import com.imeeting.common.RedisKeys;
import com.imeeting.dto.biz.CreateMeetingCommand;
import com.imeeting.dto.biz.CreateRealtimeMeetingCommand;
import com.imeeting.dto.biz.MeetingResummaryDTO;
import com.imeeting.dto.biz.MeetingSpeakerUpdateDTO;
import com.imeeting.dto.biz.MeetingSummaryExportResult;
import com.imeeting.dto.biz.MeetingTranscriptVO;
import com.imeeting.dto.biz.MeetingVO;
import com.imeeting.dto.biz.OpenRealtimeSocketSessionCommand;
import com.imeeting.dto.biz.RealtimeMeetingCompleteDTO;
import com.imeeting.dto.biz.RealtimeMeetingSessionStatusVO;
import com.imeeting.dto.biz.RealtimeSocketSessionVO;
import com.imeeting.dto.biz.RealtimeTranscriptItemDTO;
import com.imeeting.dto.biz.UpdateMeetingBasicCommand;
import com.imeeting.dto.biz.UpdateMeetingParticipantsCommand;
import com.imeeting.dto.biz.UpdateMeetingSummaryCommand;
import com.imeeting.dto.biz.UpdateMeetingTranscriptCommand;
import com.imeeting.entity.biz.Meeting;
import com.imeeting.service.biz.MeetingAccessService;
import com.imeeting.service.biz.MeetingCommandService;
import com.imeeting.service.biz.MeetingExportService;
import com.imeeting.service.biz.MeetingQueryService;
import com.imeeting.service.biz.PromptTemplateService;
import com.imeeting.service.biz.RealtimeMeetingSessionStateService;
import com.imeeting.service.biz.RealtimeMeetingSocketSessionService;
import com.unisbase.common.ApiResponse;
import com.unisbase.dto.PageResult;
import com.unisbase.security.LoginUser;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.LinkedHashMap;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
@Tag(name = "会议管理")
@RestController
@RequestMapping("/api/biz/meeting")
public class MeetingController {
private final MeetingQueryService meetingQueryService;
private final MeetingCommandService meetingCommandService;
private final MeetingAccessService meetingAccessService;
private final MeetingExportService meetingExportService;
private final PromptTemplateService promptTemplateService;
private final RealtimeMeetingSocketSessionService realtimeMeetingSocketSessionService;
private final RealtimeMeetingSessionStateService realtimeMeetingSessionStateService;
private final StringRedisTemplate redisTemplate;
private final String uploadPath;
private final String resourcePrefix;
public MeetingController(MeetingQueryService meetingQueryService,
MeetingCommandService meetingCommandService,
MeetingAccessService meetingAccessService,
MeetingExportService meetingExportService,
PromptTemplateService promptTemplateService,
RealtimeMeetingSocketSessionService realtimeMeetingSocketSessionService,
RealtimeMeetingSessionStateService realtimeMeetingSessionStateService,
StringRedisTemplate redisTemplate,
@Value("${unisbase.app.upload-path}") String uploadPath,
@Value("${unisbase.app.resource-prefix}") String resourcePrefix) {
this.meetingQueryService = meetingQueryService;
this.meetingCommandService = meetingCommandService;
this.meetingAccessService = meetingAccessService;
this.meetingExportService = meetingExportService;
this.promptTemplateService = promptTemplateService;
this.realtimeMeetingSocketSessionService = realtimeMeetingSocketSessionService;
this.realtimeMeetingSessionStateService = realtimeMeetingSessionStateService;
this.redisTemplate = redisTemplate;
this.uploadPath = uploadPath;
this.resourcePrefix = resourcePrefix;
}
@Operation(summary = "查询会议处理进度")
@GetMapping("/{id}/progress")
@PreAuthorize("isAuthenticated()")
public ApiResponse<Map<String, Object>> getProgress(@PathVariable Long id) {
LoginUser loginUser = currentLoginUser();
Meeting meeting = meetingAccessService.requireMeeting(id);
meetingAccessService.assertCanViewMeeting(meeting, loginUser);
String key = RedisKeys.meetingProgressKey(id);
String json = redisTemplate.opsForValue().get(key);
if (json != null) {
try {
return ApiResponse.ok(new com.fasterxml.jackson.databind.ObjectMapper().readValue(json, Map.class));
} catch (Exception ex) {
return ApiResponse.error("Progress parse failed");
}
}
Map<String, Object> fallback = new HashMap<>();
if (meeting.getStatus() == 3) {
fallback.put("percent", 100);
fallback.put("message", "Completed");
} else if (meeting.getStatus() == 4) {
fallback.put("percent", -1);
fallback.put("message", "Failed");
} else {
fallback.put("percent", 0);
fallback.put("message", "Waiting...");
}
return ApiResponse.ok(fallback);
}
@Operation(summary = "上传会议音频")
@PostMapping("/upload")
@PreAuthorize("isAuthenticated()")
public ApiResponse<String> upload(@RequestParam("file") MultipartFile file) throws IOException {
String basePath = uploadPath.endsWith("/") ? uploadPath : uploadPath + "/";
String uploadDir = basePath + "audio/";
File dir = new File(uploadDir);
if (!dir.exists()) {
dir.mkdirs();
}
String fileName = UUID.randomUUID() + "_" + file.getOriginalFilename();
file.transferTo(new File(uploadDir + fileName));
String baseResourcePrefix = resourcePrefix.endsWith("/") ? resourcePrefix : resourcePrefix + "/";
return ApiResponse.ok(baseResourcePrefix + "audio/" + fileName);
}
@Operation(summary = "创建离线会议")
@PostMapping
@PreAuthorize("isAuthenticated()")
public ApiResponse<MeetingVO> create(@Valid @RequestBody CreateMeetingCommand command) {
LoginUser loginUser = currentLoginUser();
assertPromptAvailable(command.getPromptId(), loginUser);
return ApiResponse.ok(meetingCommandService.createMeeting(
command,
loginUser.getTenantId(),
loginUser.getUserId(),
resolveCreatorName(loginUser)
));
}
@Operation(summary = "创建实时会议")
@PostMapping("/realtime/start")
@PreAuthorize("isAuthenticated()")
public ApiResponse<MeetingVO> createRealtime(@Valid @RequestBody CreateRealtimeMeetingCommand command) {
LoginUser loginUser = currentLoginUser();
assertPromptAvailable(command.getPromptId(), loginUser);
return ApiResponse.ok(meetingCommandService.createRealtimeMeeting(
command,
loginUser.getTenantId(),
loginUser.getUserId(),
resolveCreatorName(loginUser)
));
}
@Operation(summary = "分页查询会议")
@GetMapping("/page")
@PreAuthorize("isAuthenticated()")
public ApiResponse<PageResult<List<MeetingVO>>> page(
@RequestParam(defaultValue = "1") Integer current,
@RequestParam(defaultValue = "10") Integer size,
@RequestParam(required = false) String title,
@RequestParam(defaultValue = "all") String viewType) {
LoginUser loginUser = currentLoginUser();
boolean isAdmin = Boolean.TRUE.equals(loginUser.getIsPlatformAdmin()) || Boolean.TRUE.equals(loginUser.getIsTenantAdmin());
return ApiResponse.ok(meetingQueryService.pageMeetings(
current,
size,
title,
loginUser.getTenantId(),
loginUser.getUserId(),
resolveCreatorName(loginUser),
viewType,
isAdmin
));
}
@Operation(summary = "查询会议详情")
@GetMapping("/{id}")
@PreAuthorize("isAuthenticated()")
public ApiResponse<MeetingVO> getDetail(@PathVariable Long id) {
LoginUser loginUser = currentLoginUser();
Meeting meeting = meetingAccessService.requireMeeting(id);
meetingAccessService.assertCanViewMeeting(meeting, loginUser);
return ApiResponse.ok(meetingQueryService.getDetail(id));
}
@Operation(summary = "导出会议摘要")
@GetMapping("/{id}/summary/export")
@PreAuthorize("isAuthenticated()")
public ResponseEntity<byte[]> exportSummary(@PathVariable Long id, @RequestParam(defaultValue = "pdf") String format) {
LoginUser loginUser = currentLoginUser();
Meeting meeting = meetingAccessService.requireMeeting(id);
meetingAccessService.assertCanExportMeeting(meeting, loginUser);
MeetingVO meetingDetail = meetingQueryService.getDetail(id);
if (meetingDetail == null) {
throw new RuntimeException("Meeting not found");
}
MeetingSummaryExportResult exportResult = meetingExportService.exportSummary(meeting, meetingDetail, format);
String encodedFilename = URLEncoder.encode(exportResult.getFileName(), StandardCharsets.UTF_8).replace("+", "%20");
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename*=UTF-8''" + encodedFilename)
.contentType(MediaType.parseMediaType(exportResult.getContentType()))
.body(exportResult.getContent());
}
@Operation(summary = "查询会议转写记录")
@GetMapping("/{id}/transcripts")
@PreAuthorize("isAuthenticated()")
public ApiResponse<List<MeetingTranscriptVO>> getTranscripts(@PathVariable Long id) {
LoginUser loginUser = currentLoginUser();
Meeting meeting = meetingAccessService.requireMeeting(id);
meetingAccessService.assertCanViewMeeting(meeting, loginUser);
return ApiResponse.ok(meetingQueryService.getTranscripts(id));
}
@Operation(summary = "查询实时会议状态")
@GetMapping("/{id}/realtime/session-status")
@PreAuthorize("isAuthenticated()")
public ApiResponse<RealtimeMeetingSessionStatusVO> getRealtimeSessionStatus(@PathVariable Long id) {
LoginUser loginUser = currentLoginUser();
Meeting meeting = meetingAccessService.requireMeeting(id);
meetingAccessService.assertCanManageRealtimeMeeting(meeting, loginUser);
return ApiResponse.ok(realtimeMeetingSessionStateService.getStatus(id));
}
@Operation(summary = "批量查询实时会议状态")
@PostMapping("/realtime/session-status/batch")
@PreAuthorize("isAuthenticated()")
public ApiResponse<Map<Long, RealtimeMeetingSessionStatusVO>> getRealtimeSessionStatuses(@RequestBody List<Long> ids) {
LoginUser loginUser = currentLoginUser();
Map<Long, RealtimeMeetingSessionStatusVO> result = new LinkedHashMap<>();
if (ids == null || ids.isEmpty()) {
return ApiResponse.ok(result);
}
Map<Long, RealtimeMeetingSessionStatusVO> statuses = realtimeMeetingSessionStateService.getStatuses(ids);
for (Long id : ids) {
if (id == null) {
continue;
}
try {
Meeting meeting = meetingAccessService.requireMeeting(id);
meetingAccessService.assertCanManageRealtimeMeeting(meeting, loginUser);
RealtimeMeetingSessionStatusVO status = statuses.get(id);
if (status != null) {
result.put(id, status);
}
} catch (RuntimeException ignored) {
// Preserve previous per-item fallback behavior for inaccessible meetings.
}
}
return ApiResponse.ok(result);
}
@Operation(summary = "追加实时转写片段")
@PostMapping("/{id}/realtime/transcripts")
@PreAuthorize("isAuthenticated()")
public ApiResponse<Boolean> appendRealtimeTranscripts(@PathVariable Long id, @RequestBody List<RealtimeTranscriptItemDTO> items) {
LoginUser loginUser = currentLoginUser();
Meeting meeting = meetingAccessService.requireMeeting(id);
meetingAccessService.assertCanManageRealtimeMeeting(meeting, loginUser);
meetingCommandService.appendRealtimeTranscripts(id, items);
return ApiResponse.ok(true);
}
@Operation(summary = "暂停实时会议")
@PostMapping("/{id}/realtime/pause")
@PreAuthorize("isAuthenticated()")
public ApiResponse<RealtimeMeetingSessionStatusVO> pauseRealtimeMeeting(@PathVariable Long id) {
LoginUser loginUser = currentLoginUser();
Meeting meeting = meetingAccessService.requireMeeting(id);
meetingAccessService.assertCanManageRealtimeMeeting(meeting, loginUser);
return ApiResponse.ok(realtimeMeetingSessionStateService.pause(id));
}
@Operation(summary = "打开实时会议Socket会话")
@PostMapping("/{id}/realtime/socket-session")
@PreAuthorize("isAuthenticated()")
public ApiResponse<RealtimeSocketSessionVO> openRealtimeSocketSession(@PathVariable Long id,
@RequestBody OpenRealtimeSocketSessionCommand command) {
LoginUser loginUser = currentLoginUser();
return ApiResponse.ok(realtimeMeetingSocketSessionService.createSession(
id,
command.getAsrModelId(),
command.getMode(),
command.getLanguage(),
command.getUseSpkId(),
command.getEnablePunctuation(),
command.getEnableItn(),
command.getEnableTextRefine(),
command.getSaveAudio(),
command.getHotwords(),
loginUser
));
}
@Operation(summary = "完成实时会议")
@PostMapping("/{id}/realtime/complete")
@PreAuthorize("isAuthenticated()")
public ApiResponse<Boolean> completeRealtimeMeeting(@PathVariable Long id, @RequestBody(required = false) RealtimeMeetingCompleteDTO dto) {
LoginUser loginUser = currentLoginUser();
Meeting meeting = meetingAccessService.requireMeeting(id);
meetingAccessService.assertCanManageRealtimeMeeting(meeting, loginUser);
meetingCommandService.completeRealtimeMeeting(
id,
dto != null ? dto.getAudioUrl() : null,
dto != null && Boolean.TRUE.equals(dto.getOverwriteAudio())
);
return ApiResponse.ok(true);
}
@Operation(summary = "更新会议讲话人")
@PutMapping("/speaker")
@PreAuthorize("isAuthenticated()")
public ApiResponse<Boolean> updateSpeaker(@RequestBody MeetingSpeakerUpdateDTO dto) {
LoginUser loginUser = currentLoginUser();
Meeting meeting = meetingAccessService.requireMeeting(dto.getMeetingId());
meetingAccessService.assertCanEditMeeting(meeting, loginUser);
meetingCommandService.updateSpeakerInfo(dto.getMeetingId(), dto.getSpeakerId(), dto.getNewName(), dto.getLabel());
return ApiResponse.ok(true);
}
@Operation(summary = "更新会议转写")
@PutMapping("/{id}/transcripts/{transcriptId}")
@PreAuthorize("isAuthenticated()")
public ApiResponse<Boolean> updateTranscript(@PathVariable Long id,
@PathVariable Long transcriptId,
@RequestBody UpdateMeetingTranscriptCommand command) {
LoginUser loginUser = currentLoginUser();
Meeting meeting = meetingAccessService.requireMeeting(id);
meetingAccessService.assertCanEditMeeting(meeting, loginUser);
command.setMeetingId(id);
command.setTranscriptId(transcriptId);
meetingCommandService.updateMeetingTranscript(command);
return ApiResponse.ok(true);
}
@Operation(summary = "更新参会人员")
@PutMapping("/{id}/participants")
@PreAuthorize("isAuthenticated()")
public ApiResponse<Boolean> updateParticipants(@PathVariable Long id,
@RequestBody UpdateMeetingParticipantsCommand command) {
LoginUser loginUser = currentLoginUser();
Meeting meeting = meetingAccessService.requireMeeting(id);
meetingAccessService.assertCanEditMeeting(meeting, loginUser);
command.setMeetingId(id);
meetingCommandService.updateMeetingParticipants(command.getMeetingId(), command.getParticipants());
return ApiResponse.ok(true);
}
@Operation(summary = "重新生成会议摘要")
@PostMapping("/{id}/summary/regenerate")
@PreAuthorize("isAuthenticated()")
public ApiResponse<Boolean> reSummary(@PathVariable Long id, @Valid @RequestBody MeetingResummaryDTO dto) {
LoginUser loginUser = currentLoginUser();
Meeting meeting = meetingAccessService.requireMeeting(id);
meetingAccessService.assertCanEditMeeting(meeting, loginUser);
dto.setMeetingId(id);
assertPromptAvailable(dto.getPromptId(), loginUser);
meetingCommandService.reSummary(dto.getMeetingId(), dto.getSummaryModelId(), dto.getPromptId(), dto.getUserPrompt());
return ApiResponse.ok(true);
}
@Operation(summary = "重试音频转写")
@PostMapping("/{id}/transcripts/regenerate")
@PreAuthorize("isAuthenticated()")
public ApiResponse<Boolean> retryTranscription(@PathVariable Long id) {
LoginUser loginUser = currentLoginUser();
Meeting meeting = meetingAccessService.requireMeeting(id);
meetingAccessService.assertCanEditMeeting(meeting, loginUser);
meetingCommandService.retryTranscription(id);
return ApiResponse.ok(true);
}
@Operation(summary = "更新会议基础信息")
@PutMapping("/{id}/basic")
@PreAuthorize("isAuthenticated()")
public ApiResponse<Boolean> updateBasic(@PathVariable Long id, @RequestBody UpdateMeetingBasicCommand command) {
LoginUser loginUser = currentLoginUser();
Meeting meeting = meetingAccessService.requireMeeting(id);
meetingAccessService.assertCanEditMeeting(meeting, loginUser);
command.setMeetingId(id);
meetingCommandService.updateMeetingBasic(command);
return ApiResponse.ok(true);
}
@Operation(summary = "更新会议摘要")
@PutMapping("/{id}/summary")
@PreAuthorize("isAuthenticated()")
public ApiResponse<Boolean> updateSummary(@PathVariable Long id, @RequestBody UpdateMeetingSummaryCommand command) {
LoginUser loginUser = currentLoginUser();
Meeting meeting = meetingAccessService.requireMeeting(id);
meetingAccessService.assertCanEditMeeting(meeting, loginUser);
command.setMeetingId(id);
meetingCommandService.updateSummaryContent(command.getMeetingId(), command.getSummaryContent());
return ApiResponse.ok(true);
}
@Operation(summary = "删除会议")
@DeleteMapping("/{id}")
@PreAuthorize("isAuthenticated()")
public ApiResponse<Boolean> delete(@PathVariable Long id) {
LoginUser loginUser = currentLoginUser();
Meeting meeting = meetingAccessService.requireMeeting(id);
meetingAccessService.assertCanEditMeeting(meeting, loginUser);
meetingCommandService.deleteMeeting(id);
return ApiResponse.ok(true);
}
private LoginUser currentLoginUser() {
return (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
}
private void assertPromptAvailable(Long promptId, LoginUser loginUser) {
if (promptId == null) {
return;
}
boolean enabled = promptTemplateService.isTemplateEnabledForUser(
promptId,
loginUser.getTenantId(),
loginUser.getUserId(),
loginUser.getIsPlatformAdmin(),
loginUser.getIsTenantAdmin()
);
if (!enabled) {
throw new RuntimeException("Summary template unavailable");
}
}
private String resolveCreatorName(LoginUser loginUser) {
return loginUser.getDisplayName() != null ? loginUser.getDisplayName() : loginUser.getUsername();
}
}

View File

@ -0,0 +1,61 @@
package com.imeeting.controller.biz;
import com.imeeting.dto.biz.MeetingPreviewAccessVO;
import com.imeeting.dto.biz.PublicMeetingPreviewVO;
import com.imeeting.entity.biz.Meeting;
import com.imeeting.service.biz.MeetingAccessService;
import com.imeeting.service.biz.MeetingQueryService;
import com.unisbase.common.ApiResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@Tag(name = "会议公开预览")
@RestController
@RequestMapping("/api/public/meetings")
public class MeetingPublicPreviewController {
private final MeetingQueryService meetingQueryService;
private final MeetingAccessService meetingAccessService;
public MeetingPublicPreviewController(MeetingQueryService meetingQueryService,
MeetingAccessService meetingAccessService) {
this.meetingQueryService = meetingQueryService;
this.meetingAccessService = meetingAccessService;
}
@Operation(summary = "查询会议预览访问要求")
@GetMapping("/{id}/preview/access")
public ApiResponse<MeetingPreviewAccessVO> getPreviewAccess(@PathVariable Long id) {
try {
Meeting meeting = meetingAccessService.requireMeetingIgnoreTenant(id);
return ApiResponse.ok(new MeetingPreviewAccessVO(meetingAccessService.isPreviewPasswordRequired(meeting)));
} catch (RuntimeException ex) {
return ApiResponse.error(ex.getMessage());
}
}
@Operation(summary = "获取会议公开预览内容")
@GetMapping("/{id}/preview")
public ApiResponse<PublicMeetingPreviewVO> getPreview(@PathVariable Long id,
@RequestParam(required = false) String accessPassword) {
try {
Meeting meeting = meetingAccessService.requireMeetingIgnoreTenant(id);
meetingAccessService.assertCanPreviewMeeting(meeting, accessPassword);
PublicMeetingPreviewVO data = new PublicMeetingPreviewVO();
data.setMeeting(meetingQueryService.getDetailIgnoreTenant(id));
if (data.getMeeting() != null) {
data.getMeeting().setAccessPassword(null);
}
data.setTranscripts(meetingQueryService.getTranscripts(id));
return ApiResponse.ok(data);
} catch (RuntimeException ex) {
return ApiResponse.error(ex.getMessage());
}
}
}

View File

@ -0,0 +1,166 @@
package com.imeeting.controller.biz;
import com.imeeting.dto.biz.PromptTemplateDTO;
import com.imeeting.dto.biz.PromptTemplateVO;
import com.imeeting.entity.biz.PromptTemplate;
import com.imeeting.service.biz.PromptTemplateService;
import com.unisbase.common.ApiResponse;
import com.unisbase.dto.PageResult;
import com.unisbase.security.LoginUser;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@Tag(name = "提示词模板管理")
@RestController
@RequestMapping("/api/biz/prompt")
public class PromptTemplateController {
private final PromptTemplateService promptTemplateService;
public PromptTemplateController(PromptTemplateService promptTemplateService) {
this.promptTemplateService = promptTemplateService;
}
@Operation(summary = "新增提示词模板")
@PostMapping
@PreAuthorize("isAuthenticated()")
public ApiResponse<PromptTemplateVO> save(@RequestBody PromptTemplateDTO dto) {
LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (Integer.valueOf(1).equals(dto.getIsSystem())) {
if (!Boolean.TRUE.equals(loginUser.getIsPlatformAdmin()) && !Boolean.TRUE.equals(loginUser.getIsTenantAdmin())) {
return ApiResponse.error("No permission to create public template");
}
if (!Boolean.TRUE.equals(loginUser.getIsPlatformAdmin())) {
dto.setTenantId(loginUser.getTenantId());
} else if (dto.getTenantId() == null) {
dto.setTenantId(0L);
}
} else {
dto.setTenantId(loginUser.getTenantId());
}
return ApiResponse.ok(promptTemplateService.saveTemplate(dto, loginUser.getUserId(), loginUser.getTenantId()));
}
@Operation(summary = "修改提示词模板")
@PutMapping
@PreAuthorize("isAuthenticated()")
public ApiResponse<PromptTemplateVO> update(@RequestBody PromptTemplateDTO dto) {
LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
PromptTemplate existing = promptTemplateService.getById(dto.getId());
if (existing == null) {
return ApiResponse.error("Template not found");
}
boolean canModify = false;
if (Integer.valueOf(0).equals(existing.getIsSystem())) {
canModify = existing.getCreatorId().equals(loginUser.getUserId());
} else {
if (Boolean.TRUE.equals(loginUser.getIsPlatformAdmin())) {
canModify = existing.getTenantId() == 0L;
} else if (Boolean.TRUE.equals(loginUser.getIsTenantAdmin())) {
canModify = existing.getTenantId().equals(loginUser.getTenantId());
}
}
if (!canModify) {
return ApiResponse.error("No permission to modify this template");
}
return ApiResponse.ok(promptTemplateService.updateTemplate(dto));
}
@Operation(summary = "更新提示词模板状态")
@PutMapping("/{id}/status")
@PreAuthorize("isAuthenticated()")
public ApiResponse<Boolean> updateStatus(@PathVariable Long id, @RequestParam Integer status) {
LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
PromptTemplate existing = promptTemplateService.getById(id);
if (existing == null) {
return ApiResponse.error("Template not found");
}
boolean canGlobalModify = false;
if (Integer.valueOf(1).equals(existing.getIsSystem())) {
if (Boolean.TRUE.equals(loginUser.getIsPlatformAdmin()) && Long.valueOf(0L).equals(existing.getTenantId())) {
canGlobalModify = true;
} else if (Boolean.TRUE.equals(loginUser.getIsTenantAdmin()) && existing.getTenantId().equals(loginUser.getTenantId())) {
canGlobalModify = true;
}
}
if (canGlobalModify) {
existing.setStatus(status);
return ApiResponse.ok(promptTemplateService.updateById(existing));
}
boolean success = promptTemplateService.updateUserTemplateStatus(
id,
status,
loginUser.getTenantId(),
loginUser.getUserId(),
loginUser.getIsPlatformAdmin(),
loginUser.getIsTenantAdmin()
);
if (!success) {
return ApiResponse.error("Template not found or no permission");
}
return ApiResponse.ok(true);
}
@Operation(summary = "删除提示词模板")
@DeleteMapping("/{id}")
@PreAuthorize("isAuthenticated()")
public ApiResponse<Boolean> delete(@PathVariable Long id) {
LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
PromptTemplate existing = promptTemplateService.getById(id);
if (existing == null) {
return ApiResponse.ok(true);
}
boolean canModify = false;
if (Integer.valueOf(0).equals(existing.getIsSystem())) {
canModify = existing.getCreatorId().equals(loginUser.getUserId());
} else {
if (Boolean.TRUE.equals(loginUser.getIsPlatformAdmin())) {
canModify = existing.getTenantId() == 0L;
} else if (Boolean.TRUE.equals(loginUser.getIsTenantAdmin())) {
canModify = existing.getTenantId().equals(loginUser.getTenantId());
}
}
if (!canModify) {
return ApiResponse.error("No permission to delete this template");
}
return ApiResponse.ok(promptTemplateService.removeById(id));
}
@Operation(summary = "分页查询提示词模板")
@GetMapping("/page")
@PreAuthorize("isAuthenticated()")
public ApiResponse<PageResult<List<PromptTemplateVO>>> page(
@RequestParam(defaultValue = "1") Integer current,
@RequestParam(defaultValue = "10") Integer size,
@RequestParam(required = false) String name,
@RequestParam(required = false) String category) {
LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
return ApiResponse.ok(promptTemplateService.pageTemplates(
current,
size,
name,
category,
loginUser.getTenantId(),
loginUser.getUserId(),
loginUser.getIsPlatformAdmin(),
loginUser.getIsTenantAdmin()));
}
}

View File

@ -0,0 +1,79 @@
package com.imeeting.controller.biz;
import com.imeeting.dto.biz.ScreenSaverAdminVO;
import com.imeeting.dto.biz.ScreenSaverDTO;
import com.imeeting.dto.biz.ScreenSaverImageUploadVO;
import com.imeeting.entity.biz.ScreenSaver;
import com.imeeting.service.biz.ScreenSaverService;
import com.unisbase.common.ApiResponse;
import com.unisbase.security.LoginUser;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.util.List;
@Tag(name = "屏保管理")
@RestController
@RequestMapping("/api/screen-savers")
@RequiredArgsConstructor
public class ScreenSaverController {
private final ScreenSaverService screenSaverService;
@Operation(summary = "查询屏保列表")
@GetMapping
@PreAuthorize("isAuthenticated()")
public ApiResponse<List<ScreenSaverAdminVO>> list(@RequestParam(value = "keyword", required = false) String keyword,
@RequestParam(value = "status", required = false) Integer status,
@RequestParam(value = "scopeType", required = false) String scopeType,
@RequestParam(value = "ownerUserId", required = false) Long ownerUserId) {
return ApiResponse.ok(screenSaverService.listForAdmin(currentLoginUser(), keyword, status, scopeType, ownerUserId));
}
@Operation(summary = "新增屏保")
@PostMapping
@PreAuthorize("isAuthenticated()")
public ApiResponse<ScreenSaver> create(@RequestBody ScreenSaverDTO dto) {
return ApiResponse.ok(screenSaverService.create(dto, currentLoginUser()));
}
@Operation(summary = "修改屏保")
@PutMapping("/{id}")
@PreAuthorize("isAuthenticated()")
public ApiResponse<ScreenSaver> update(@PathVariable Long id, @RequestBody ScreenSaverDTO dto) {
return ApiResponse.ok(screenSaverService.update(id, dto, currentLoginUser()));
}
@Operation(summary = "删除屏保")
@DeleteMapping("/{id}")
@PreAuthorize("isAuthenticated()")
public ApiResponse<Boolean> delete(@PathVariable Long id) {
screenSaverService.removeScreenSaver(id, currentLoginUser());
return ApiResponse.ok(true);
}
@Operation(summary = "上传屏保图片")
@PostMapping("/upload-image")
@PreAuthorize("isAuthenticated()")
public ApiResponse<ScreenSaverImageUploadVO> uploadImage(@RequestParam("imageFile") MultipartFile imageFile) throws IOException {
return ApiResponse.ok(screenSaverService.uploadImage(imageFile));
}
private LoginUser currentLoginUser() {
return (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
}
}

View File

@ -0,0 +1,84 @@
package com.imeeting.controller.biz;
import com.imeeting.dto.biz.SpeakerRegisterDTO;
import com.imeeting.dto.biz.SpeakerVO;
import com.imeeting.service.biz.SpeakerService;
import com.unisbase.common.ApiResponse;
import com.unisbase.dto.PageResult;
import com.unisbase.security.LoginUser;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@Tag(name = "讲话人管理")
@RestController
@RequestMapping("/api/biz/speaker")
public class SpeakerController {
private final SpeakerService speakerService;
public SpeakerController(SpeakerService speakerService) {
this.speakerService = speakerService;
}
@Operation(summary = "注册讲话人样本")
@PostMapping("/register")
@PreAuthorize("isAuthenticated()")
public ApiResponse<SpeakerVO> register(@ModelAttribute SpeakerRegisterDTO registerDTO) {
LoginUser loginUser = getLoginUser();
if (loginUser == null || loginUser.getUserId() == null) {
return ApiResponse.error("未获取到用户信息");
}
registerDTO.setCreatorId(loginUser.getUserId());
return ApiResponse.ok(speakerService.register(registerDTO, loginUser));
}
@Operation(summary = "分页查询讲话人")
@GetMapping("/page")
@PreAuthorize("isAuthenticated()")
public ApiResponse<PageResult<List<SpeakerVO>>> page(
@RequestParam(defaultValue = "1") Integer current,
@RequestParam(defaultValue = "8") Integer size,
@RequestParam(required = false) String name) {
LoginUser loginUser = getLoginUser();
if (loginUser == null || loginUser.getUserId() == null) {
return ApiResponse.error("未获取到用户信息");
}
return ApiResponse.ok(speakerService.pageVisible(current, size, name, loginUser));
}
@Operation(summary = "查询可见讲话人")
@GetMapping("/list")
@PreAuthorize("isAuthenticated()")
public ApiResponse<List<SpeakerVO>> list() {
LoginUser loginUser = getLoginUser();
if (loginUser == null || loginUser.getUserId() == null) {
return ApiResponse.error("未获取到用户信息");
}
return ApiResponse.ok(speakerService.listVisible(loginUser));
}
@Operation(summary = "删除讲话人")
@DeleteMapping("/{id}")
@PreAuthorize("isAuthenticated()")
public ApiResponse<Boolean> delete(@PathVariable Long id) {
LoginUser loginUser = getLoginUser();
if (loginUser == null || loginUser.getUserId() == null) {
return ApiResponse.error("未获取到用户信息");
}
speakerService.deleteSpeaker(id, loginUser);
return ApiResponse.ok(Boolean.TRUE);
}
private LoginUser getLoginUser() {
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (principal instanceof LoginUser loginUser) {
return loginUser;
}
return null;
}
}

View File

@ -1,14 +0,0 @@
package com.imeeting.dto;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class CreateTenantDTO {
private String tenantCode;
private String tenantName;
private String contactName;
private String contactPhone;
private String remark;
private LocalDateTime expireTime;
}

View File

@ -1,9 +0,0 @@
package com.imeeting.dto;
import lombok.Data;
@Data
public class PasswordUpdateDTO {
private String oldPassword;
private String newPassword;
}

View File

@ -1,25 +0,0 @@
package com.imeeting.dto;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
@Data
public class PermissionNode {
private Long permId;
private Long parentId;
private String name;
private String code;
private String permType;
private Integer level;
private String path;
private String component;
private String icon;
private Integer sortOrder;
private Integer isVisible;
private Integer status;
private String description;
private String meta;
private List<PermissionNode> children = new ArrayList<>();
}

View File

@ -1,15 +0,0 @@
package com.imeeting.dto;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class PlatformConfigVO {
private String projectName;
private String logoUrl;
private String iconUrl;
private String loginBgUrl;
private String icpInfo;
private String copyrightInfo;
private String systemDescription;
}

View File

@ -1,12 +0,0 @@
package com.imeeting.dto;
import lombok.Data;
@Data
public class SysParamQueryDTO {
private String paramKey;
private String paramType;
private String description;
private Integer pageNum = 1;
private Integer pageSize = 10;
}

View File

@ -1,17 +0,0 @@
package com.imeeting.dto;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class SysParamVO {
private Long paramId;
private String paramKey;
private String paramValue;
private String paramType;
private Integer isSystem;
private String description;
private Integer status;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}

View File

@ -1,19 +0,0 @@
package com.imeeting.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
@Data
public class UserProfile {
private Long userId;
private String username;
private String displayName;
private String email;
private String phone;
private Integer status;
@JsonProperty("isAdmin")
private boolean isAdmin;
private Boolean isPlatformAdmin;
private Boolean isTenantAdmin;
private Integer pwdResetRequired;
}

View File

@ -0,0 +1,24 @@
package com.imeeting.dto.android;
import lombok.Data;
import java.util.Set;
@Data
public class AndroidAuthContext {
private String authMode;
private String deviceId;
private Long tenantId;
private String tenantCode;
private Long userId;
private String username;
private String displayName;
private Boolean platformAdmin;
private Boolean tenantAdmin;
private Set<String> permissions;
private String appId;
private String appVersion;
private String platform;
private String accessToken;
private boolean anonymous;
}

View File

@ -0,0 +1,31 @@
package com.imeeting.dto.android;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.List;
@Data
public class AndroidCreateRealtimeMeetingCommand {
private String title;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime meetingTime;
private String participants;
private String tags;
private Long hostUserId;
private String hostName;
private Long asrModelId;
private Long summaryModelId;
private Long promptId;
private String mode;
private String language;
private Integer useSpkId;
private Boolean enablePunctuation;
private Boolean enableItn;
private Boolean enableTextRefine;
private Boolean saveAudio;
private List<String> hotWords;
}

View File

@ -0,0 +1,24 @@
package com.imeeting.dto.android;
import com.imeeting.dto.biz.RealtimeMeetingResumeConfig;
import com.imeeting.dto.biz.RealtimeMeetingSessionStatusVO;
import lombok.Data;
@Data
public class AndroidCreateRealtimeMeetingVO {
private Long meetingId;
private String title;
private Long hostUserId;
private String hostName;
private Integer sampleRate;
private Integer channels;
private String encoding;
private Long resolvedAsrModelId;
private String resolvedAsrModelName;
private Long resolvedSummaryModelId;
private String resolvedSummaryModelName;
private Long resolvedPromptId;
private String resolvedPromptName;
private RealtimeMeetingResumeConfig resumeConfig;
private RealtimeMeetingSessionStatusVO status;
}

View File

@ -0,0 +1,14 @@
package com.imeeting.dto.android;
import lombok.Data;
@Data
public class AndroidDeviceSessionState {
private String connectionId;
private String deviceId;
private String status;
private Long lastSeenAt;
private String appVersion;
private String platform;
private String tenantCode;
}

View File

@ -0,0 +1,19 @@
package com.imeeting.dto.android;
import lombok.Data;
import java.util.List;
import java.util.Map;
@Data
public class AndroidOpenRealtimeGrpcSessionCommand {
private Long asrModelId;
private String mode;
private String language;
private Integer useSpkId;
private Boolean enablePunctuation;
private Boolean enableItn;
private Boolean enableTextRefine;
private Boolean saveAudio;
private List<Map<String, Object>> hotwords;
}

View File

@ -0,0 +1,16 @@
package com.imeeting.dto.android;
import com.imeeting.dto.biz.RealtimeMeetingResumeConfig;
import lombok.Data;
@Data
public class AndroidRealtimeGrpcSessionData {
private Long meetingId;
private Long tenantId;
private Long userId;
private String deviceId;
private Long asrModelId;
private String targetWsUrl;
private String startMessageJson;
private RealtimeMeetingResumeConfig resumeConfig;
}

View File

@ -0,0 +1,17 @@
package com.imeeting.dto.android;
import com.imeeting.dto.biz.RealtimeMeetingResumeConfig;
import com.imeeting.dto.biz.RealtimeMeetingSessionStatusVO;
import lombok.Data;
@Data
public class AndroidRealtimeGrpcSessionVO {
private Long meetingId;
private String streamToken;
private Long expiresInSeconds;
private Integer sampleRate;
private Integer channels;
private String encoding;
private RealtimeMeetingResumeConfig resumeConfig;
private RealtimeMeetingSessionStatusVO status;
}

View File

@ -0,0 +1,13 @@
package com.imeeting.dto.android;
import lombok.Data;
import java.util.List;
@Data
public class AndroidScreenSaverCatalogVO {
private Integer refreshIntervalSec;
private String playMode;
private String sourceScope;
private List<AndroidScreenSaverItemVO> items;
}

View File

@ -0,0 +1,14 @@
package com.imeeting.dto.android;
import lombok.Data;
@Data
public class AndroidScreenSaverItemVO {
private Long id;
private String name;
private String imageUrl;
private String description;
private Integer displayDurationSec;
private Integer sortOrder;
private String updatedAt;
}

View File

@ -0,0 +1,26 @@
package com.imeeting.dto.android.legacy;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LegacyApiResponse<T> {
private String code;
private String message;
private T data;
public static <T> LegacyApiResponse<T> ok(T data) {
return new LegacyApiResponse<>("200", "success", data);
}
public static <T> LegacyApiResponse<T> ok(String message, T data) {
return new LegacyApiResponse<>("200", message, data);
}
public static <T> LegacyApiResponse<T> error(String code, String message) {
return new LegacyApiResponse<>(code, message, null);
}
}

View File

@ -0,0 +1,67 @@
package com.imeeting.dto.android.legacy;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.imeeting.entity.biz.ClientDownload;
import lombok.Data;
@Data
public class LegacyClientDownloadResponse {
private String id;
@JsonProperty("platform_type")
private String platformType;
@JsonProperty("platform_name")
private String platformName;
private String version;
@JsonProperty("version_code")
private String versionCode;
@JsonProperty("download_url")
private String downloadUrl;
@JsonProperty("file_size")
private Long fileSize;
@JsonProperty("release_notes")
private String releaseNotes;
@JsonProperty("is_active")
private Integer isActive;
@JsonProperty("is_latest")
private Integer isLatest;
@JsonProperty("min_system_version")
private String minSystemVersion;
@JsonProperty("created_at")
private String createdAt;
@JsonProperty("updated_at")
private String updatedAt;
@JsonProperty("created_by")
private Long createdBy;
public static LegacyClientDownloadResponse from(ClientDownload source) {
LegacyClientDownloadResponse response = new LegacyClientDownloadResponse();
response.setId(source.getId() == null ? null : String.valueOf(source.getId()));
response.setPlatformType(source.getPlatformType());
response.setPlatformName(source.getPlatformName());
response.setVersion(source.getVersion());
response.setVersionCode(source.getVersionCode() == null ? null : String.valueOf(source.getVersionCode()));
response.setDownloadUrl(source.getDownloadUrl());
response.setFileSize(source.getFileSize());
response.setReleaseNotes(source.getReleaseNotes());
response.setIsActive(Integer.valueOf(1).equals(source.getStatus()) ? 1 : 0);
response.setIsLatest(Integer.valueOf(1).equals(source.getIsLatest()) ? 1 : 0);
response.setMinSystemVersion(source.getMinSystemVersion());
response.setCreatedAt(source.getCreatedAt() == null ? null : source.getCreatedAt().toString());
response.setUpdatedAt(source.getUpdatedAt() == null ? null : source.getUpdatedAt().toString());
response.setCreatedBy(source.getCreatedBy());
return response;
}
}

View File

@ -0,0 +1,101 @@
package com.imeeting.dto.android.legacy;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.imeeting.entity.biz.ExternalApp;
import lombok.Data;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Locale;
@Data
public class LegacyExternalAppItemResponse {
private Long id;
@JsonProperty("app_name")
private String appName;
@JsonProperty("app_type")
private String appType;
@JsonProperty("app_info")
private Map<String, Object> appInfo;
@JsonProperty("icon_url")
private String iconUrl;
private String description;
@JsonProperty("sort_order")
private Integer sortOrder;
@JsonProperty("is_active")
private Integer isActive;
@JsonProperty("created_at")
private String createdAt;
@JsonProperty("updated_at")
private String updatedAt;
@JsonProperty("created_by")
private Long createdBy;
@JsonProperty("creator_username")
private String creatorUsername;
public static LegacyExternalAppItemResponse from(ExternalApp source, String creatorUsername) {
LegacyExternalAppItemResponse response = new LegacyExternalAppItemResponse();
response.setId(source.getId());
response.setAppName(source.getAppName());
response.setAppType(source.getAppType());
response.setAppInfo(normalizeAppInfo(source.getAppInfo()));
response.setIconUrl(source.getIconUrl());
response.setDescription(source.getDescription());
response.setSortOrder(source.getSortOrder());
response.setIsActive(Integer.valueOf(1).equals(source.getStatus()) ? 1 : 0);
response.setCreatedAt(source.getCreatedAt() == null ? null : source.getCreatedAt().toString());
response.setUpdatedAt(source.getUpdatedAt() == null ? null : source.getUpdatedAt().toString());
response.setCreatedBy(source.getCreatedBy());
response.setCreatorUsername(creatorUsername);
return response;
}
private static Map<String, Object> normalizeAppInfo(Map<String, Object> appInfo) {
if (appInfo == null || appInfo.isEmpty()) {
return appInfo;
}
Map<String, Object> normalized = new LinkedHashMap<>();
appInfo.forEach((key, value) -> normalized.put(toSnakeCase(key), normalizeValue(value)));
return normalized;
}
private static Object normalizeValue(Object value) {
if (value instanceof Map<?, ?> nestedMap) {
Map<String, Object> normalized = new LinkedHashMap<>();
nestedMap.forEach((key, nestedValue) -> normalized.put(toSnakeCase(String.valueOf(key)), normalizeValue(nestedValue)));
return normalized;
}
if (value instanceof List<?> list) {
List<Object> normalized = new ArrayList<>(list.size());
list.forEach(item -> normalized.add(normalizeValue(item)));
return normalized;
}
return value;
}
private static String toSnakeCase(String value) {
if (value == null || value.isBlank()) {
return value;
}
return value
.replace('-', '_')
.replace(' ', '_')
.replaceAll("([A-Z]+)([A-Z][a-z])", "$1_$2")
.replaceAll("([a-z0-9])([A-Z])", "$1_$2")
.replaceAll("_+", "_")
.toLowerCase(Locale.ROOT);
}
}

View File

@ -0,0 +1,28 @@
package com.imeeting.dto.android.legacy;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.imeeting.dto.biz.AiModelVO;
import lombok.Data;
@Data
public class LegacyLlmModelItemResponse {
@JsonProperty("model_code")
private String modelCode;
@JsonProperty("model_name")
private String modelName;
private String provider;
@JsonProperty("is_default")
private Integer isDefault;
public static LegacyLlmModelItemResponse from(AiModelVO source, boolean defaultItem) {
LegacyLlmModelItemResponse response = new LegacyLlmModelItemResponse();
response.setModelCode(source.getModelCode());
response.setModelName(source.getModelName());
response.setProvider(source.getProvider());
response.setIsDefault(defaultItem ? 1 : 0);
return response;
}
}

View File

@ -0,0 +1,14 @@
package com.imeeting.dto.android.legacy;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LegacyLoginResponse {
private String token;
private String refreshToken;
private LegacyLoginUserResponse user;
}

View File

@ -0,0 +1,21 @@
package com.imeeting.dto.android.legacy;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LegacyLoginUserResponse {
private Long user_id;
private String username;
private String caption;
private String avatar_url;
private String email;
private Long role_id;
private String role_name;
private LocalDateTime created_at;
}

View File

@ -0,0 +1,8 @@
package com.imeeting.dto.android.legacy;
import lombok.Data;
@Data
public class LegacyMeetingAccessPasswordRequest {
private String password;
}

View File

@ -0,0 +1,12 @@
package com.imeeting.dto.android.legacy;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LegacyMeetingAccessPasswordResponse {
private String password;
}

View File

@ -0,0 +1,18 @@
package com.imeeting.dto.android.legacy;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LegacyMeetingAttendeeResponse {
@JsonProperty("user_id")
private Long userId;
private String username;
private String caption;
}

View File

@ -0,0 +1,22 @@
package com.imeeting.dto.android.legacy;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import java.util.List;
@Data
public class LegacyMeetingCreateRequest {
@JsonProperty("user_id")
private Long userId;
private String title;
@JsonProperty("meeting_time")
private String meetingTime;
private Object tags;
@JsonProperty("attendee_ids")
private List<Long> attendeeIds;
}

View File

@ -0,0 +1,14 @@
package com.imeeting.dto.android.legacy;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LegacyMeetingCreateResponse {
@JsonProperty("meeting_id")
private Long meetingId;
}

View File

@ -0,0 +1,53 @@
package com.imeeting.dto.android.legacy;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import java.util.List;
@Data
public class LegacyMeetingItemResponse {
@JsonProperty("meeting_id")
private Long meetingId;
private String title;
@JsonProperty("meeting_time")
private String meetingTime;
private String summary;
@JsonProperty("created_at")
private String createdAt;
@JsonProperty("creator_id")
private Long creatorId;
@JsonProperty("creator_username")
private String creatorUsername;
private List<LegacyMeetingAttendeeResponse> attendees;
@JsonProperty("attendee_ids")
private List<Long> attendeeIds;
private List<LegacyMeetingTagResponse> tags;
@JsonProperty("audio_file_path")
private String audioFilePath;
@JsonProperty("audio_duration")
private Integer audioDuration;
@JsonProperty("overall_status")
private String overallStatus;
@JsonProperty("overall_progress")
private Integer overallProgress;
@JsonProperty("current_stage")
private String currentStage;
@JsonProperty("access_password")
private String accessPassword;
}

View File

@ -0,0 +1,22 @@
package com.imeeting.dto.android.legacy;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import java.util.List;
@Data
public class LegacyMeetingListResponse {
private List<LegacyMeetingItemResponse> meetings;
private long total;
private int page;
@JsonProperty("page_size")
private int pageSize;
@JsonProperty("total_pages")
private long totalPages;
@JsonProperty("has_more")
private boolean hasMore;
}

View File

@ -0,0 +1,39 @@
package com.imeeting.dto.android.legacy;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import java.util.List;
@Data
public class LegacyMeetingPreviewDataResponse {
@JsonProperty("meeting_id")
private Long meetingId;
private String title;
@JsonProperty("meeting_time")
private String meetingTime;
private String summary;
@JsonProperty("creator_username")
private String creatorUsername;
@JsonProperty("prompt_id")
private Long promptId;
@JsonProperty("prompt_name")
private String promptName;
private List<LegacyMeetingAttendeeResponse> attendees;
@JsonProperty("attendees_count")
private Integer attendeesCount;
@JsonProperty("has_password")
private Boolean hasPassword;
@JsonProperty("processing_status")
private LegacyMeetingProcessingStatusResponse processingStatus;
}

View File

@ -0,0 +1,12 @@
package com.imeeting.dto.android.legacy;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
public class LegacyMeetingPreviewResult {
private String code;
private String message;
private LegacyMeetingPreviewDataResponse data;
}

View File

@ -0,0 +1,20 @@
package com.imeeting.dto.android.legacy;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LegacyMeetingProcessingStatusResponse {
@JsonProperty("overall_status")
private String overallStatus;
@JsonProperty("overall_progress")
private Integer overallProgress;
@JsonProperty("current_stage")
private String currentStage;
}

View File

@ -0,0 +1,13 @@
package com.imeeting.dto.android.legacy;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LegacyMeetingTagResponse {
private Long id;
private String name;
}

View File

@ -0,0 +1,24 @@
package com.imeeting.dto.android.legacy;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.imeeting.dto.biz.PromptTemplateVO;
import lombok.Data;
@Data
public class LegacyPromptItemResponse {
private Long id;
private String name;
private String description;
@JsonProperty("is_default")
private Integer isDefault;
public static LegacyPromptItemResponse from(PromptTemplateVO source, boolean defaultItem) {
LegacyPromptItemResponse response = new LegacyPromptItemResponse();
response.setId(source.getId());
response.setName(source.getTemplateName());
response.setDescription(source.getDescription());
response.setIsDefault(defaultItem ? 1 : 0);
return response;
}
}

View File

@ -0,0 +1,14 @@
package com.imeeting.dto.android.legacy;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LegacyPromptListResponse {
private List<LegacyPromptItemResponse> prompts;
}

View File

@ -0,0 +1,13 @@
package com.imeeting.dto.android.legacy;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LegacyRefreshTokenResponse {
private String token;
private String refreshToken;
}

View File

@ -0,0 +1,54 @@
package com.imeeting.dto.android.legacy;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.imeeting.dto.biz.ScreenSaverAdminVO;
import lombok.Data;
@Data
public class LegacyScreenSaverItemResponse {
private Long id;
private String name;
@JsonProperty("image_url")
private String imageUrl;
private String description;
@JsonProperty("display_duration_sec")
private Integer displayDurationSec;
@JsonProperty("sort_order")
private Integer sortOrder;
@JsonProperty("is_active")
private Integer isActive;
@JsonProperty("created_at")
private String createdAt;
@JsonProperty("updated_at")
private String updatedAt;
@JsonProperty("created_by")
private Long createdBy;
@JsonProperty("creator_username")
private String creatorUsername;
public static LegacyScreenSaverItemResponse from(ScreenSaverAdminVO source) {
LegacyScreenSaverItemResponse response = new LegacyScreenSaverItemResponse();
response.setId(source.getId());
response.setName(source.getName());
response.setImageUrl(source.getImageUrl());
response.setDescription(source.getDescription());
response.setDisplayDurationSec(source.getDisplayDurationSec());
response.setSortOrder(source.getSortOrder());
response.setIsActive(Integer.valueOf(1).equals(source.getStatus()) ? 1 : 0);
response.setCreatedAt(source.getCreatedAt());
response.setUpdatedAt(source.getUpdatedAt());
response.setCreatedBy(source.getCreatedBy());
response.setCreatorUsername(source.getCreatorUsername());
return response;
}
}

View File

@ -0,0 +1,17 @@
package com.imeeting.dto.android.legacy;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LegacyUploadAudioResponse {
@JsonProperty("meeting_id")
private Long meetingId;
@JsonProperty("audio_url")
private String audioUrl;
}

Some files were not shown because too many files have changed in this diff Show More