feat: 增加实时会议功能并优化数据库和API

- 增加实时会议的创建、追加转录和完成接口
- 更新 `MeetingDTO` 和 `MeetingVO`,添加可选字段 `summaryModelId`, `useSpkId`, `hotWords`
- 增加 `RealtimeTranscriptItemDTO` 接口
- 延长HTTP客户端连接超时时间至300秒
- 优化数据库表结构,新增 `biz_prompt_template_user_config` 表
- 更新系统参数和权限配置
dev_na
chenhao 2026-03-12 20:39:49 +08:00
parent eaed89c9ec
commit d8bfdb21fa
14 changed files with 512 additions and 159 deletions

View File

@ -4,7 +4,7 @@
-- ---------------------------- -- ----------------------------
-- 0. 租户与组织 -- 0. 租户与组织
-- ---------------------------- -- ----------------------------
CREATE EXTENSION IF NOT EXISTS vector;
-- 租户表 -- 租户表
CREATE TABLE sys_tenant ( CREATE TABLE sys_tenant (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
@ -109,7 +109,7 @@ CREATE TABLE sys_permission (
perm_id BIGSERIAL PRIMARY KEY, perm_id BIGSERIAL PRIMARY KEY,
parent_id BIGINT, parent_id BIGINT,
name VARCHAR(100) NOT NULL, name VARCHAR(100) NOT NULL,
code VARCHAR(100) NOT NULL UNIQUE, code VARCHAR(100) NOT NULL ,
perm_type VARCHAR(20) NOT NULL, perm_type VARCHAR(20) NOT NULL,
level INTEGER NOT NULL, level INTEGER NOT NULL,
path VARCHAR(255), path VARCHAR(255),
@ -161,13 +161,14 @@ CREATE TABLE sys_dict_item (
status SMALLINT DEFAULT 1, status SMALLINT DEFAULT 1,
created_at TIMESTAMP NOT NULL DEFAULT NOW(), created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_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 INDEX idx_dict_item_type ON sys_dict_item (type_code);
CREATE 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 ( CREATE TABLE sys_param (
id BIGSERIAL PRIMARY KEY, param_id BIGSERIAL PRIMARY KEY,
param_key VARCHAR(100) UNIQUE NOT NULL, param_key VARCHAR(100) UNIQUE NOT NULL,
param_value TEXT NOT NULL, param_value TEXT NOT NULL,
param_type VARCHAR(20) NOT NULL, param_type VARCHAR(20) NOT NULL,
@ -179,6 +180,14 @@ CREATE TABLE sys_param (
is_deleted SMALLINT DEFAULT 0 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. 日志 (租户隔离) -- 3. 日志 (租户隔离)
-- ---------------------------- -- ----------------------------
@ -409,6 +418,17 @@ CREATE INDEX idx_aitask_meeting ON biz_ai_tasks (meeting_id);
COMMENT ON TABLE biz_meetings IS '会议管理主表'; COMMENT ON TABLE biz_meetings IS '会议管理主表';
COMMENT ON TABLE biz_meeting_transcripts IS '会议转录明细表'; COMMENT ON TABLE biz_meeting_transcripts IS '会议转录明细表';
COMMENT ON TABLE biz_ai_tasks IS 'AI 任务流水日志表'; 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
);
-- ---------------------------- -- ----------------------------
@ -454,3 +474,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_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', '成功', '1', 1);
INSERT INTO sys_dict_item (type_code, item_label, item_value, sort_order) VALUES ('sys_log_status', '失败', '0', 2); 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 ("user_id", "username", "display_name", "email", "phone", "password_hash", "status", "is_deleted", "created_at", "updated_at", "is_platform_admin", "pwd_reset_required")
VALUES (1, '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

@ -6,6 +6,8 @@ import com.imeeting.common.RedisKeys;
import com.imeeting.dto.biz.MeetingDTO; import com.imeeting.dto.biz.MeetingDTO;
import com.imeeting.dto.biz.MeetingTranscriptVO; import com.imeeting.dto.biz.MeetingTranscriptVO;
import com.imeeting.dto.biz.MeetingVO; import com.imeeting.dto.biz.MeetingVO;
import com.imeeting.dto.biz.RealtimeMeetingCompleteDTO;
import com.imeeting.dto.biz.RealtimeTranscriptItemDTO;
import com.imeeting.entity.biz.AiTask; import com.imeeting.entity.biz.AiTask;
import com.imeeting.entity.biz.Meeting; import com.imeeting.entity.biz.Meeting;
import com.imeeting.security.LoginUser; import com.imeeting.security.LoginUser;
@ -144,6 +146,28 @@ public class MeetingController {
return ApiResponse.ok(meetingService.createMeeting(dto)); return ApiResponse.ok(meetingService.createMeeting(dto));
} }
@PostMapping("/realtime/start")
@PreAuthorize("isAuthenticated()")
public ApiResponse<MeetingVO> createRealtime(@RequestBody MeetingDTO dto) {
LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (dto.getPromptId() != null) {
boolean enabled = promptTemplateService.isTemplateEnabledForUser(
dto.getPromptId(),
loginUser.getTenantId(),
loginUser.getUserId(),
loginUser.getIsPlatformAdmin(),
loginUser.getIsTenantAdmin()
);
if (!enabled) {
return ApiResponse.error("总结模板不可用或已被你禁用");
}
}
dto.setTenantId(loginUser.getTenantId());
dto.setCreatorId(loginUser.getUserId());
dto.setCreatorName(loginUser.getDisplayName() != null ? loginUser.getDisplayName() : loginUser.getUsername());
return ApiResponse.ok(meetingService.createRealtimeMeeting(dto));
}
@GetMapping("/page") @GetMapping("/page")
@PreAuthorize("isAuthenticated()") @PreAuthorize("isAuthenticated()")
public ApiResponse<PageResult<List<MeetingVO>>> page( public ApiResponse<PageResult<List<MeetingVO>>> page(
@ -296,6 +320,40 @@ public class MeetingController {
return ApiResponse.ok(meetingService.getTranscripts(id)); return ApiResponse.ok(meetingService.getTranscripts(id));
} }
@PostMapping("/{id}/realtime/transcripts")
@PreAuthorize("isAuthenticated()")
public ApiResponse<Boolean> appendRealtimeTranscripts(@PathVariable Long id, @RequestBody List<RealtimeTranscriptItemDTO> items) {
LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
Meeting existing = meetingService.getById(id);
if (existing == null) {
return ApiResponse.error("会议不存在");
}
if (!existing.getCreatorId().equals(loginUser.getUserId())
&& !Boolean.TRUE.equals(loginUser.getIsPlatformAdmin())
&& !Boolean.TRUE.equals(loginUser.getIsTenantAdmin())) {
return ApiResponse.error("无权写入此会议的实时转录");
}
meetingService.appendRealtimeTranscripts(id, items);
return ApiResponse.ok(true);
}
@PostMapping("/{id}/realtime/complete")
@PreAuthorize("isAuthenticated()")
public ApiResponse<Boolean> completeRealtimeMeeting(@PathVariable Long id, @RequestBody(required = false) RealtimeMeetingCompleteDTO dto) {
LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
Meeting existing = meetingService.getById(id);
if (existing == null) {
return ApiResponse.error("会议不存在");
}
if (!existing.getCreatorId().equals(loginUser.getUserId())
&& !Boolean.TRUE.equals(loginUser.getIsPlatformAdmin())
&& !Boolean.TRUE.equals(loginUser.getIsTenantAdmin())) {
return ApiResponse.error("无权结束此实时会议");
}
meetingService.completeRealtimeMeeting(id, dto != null ? dto.getAudioUrl() : null);
return ApiResponse.ok(true);
}
@PutMapping("/speaker") @PutMapping("/speaker")
@PreAuthorize("isAuthenticated()") @PreAuthorize("isAuthenticated()")
public ApiResponse<Boolean> updateSpeaker(@RequestBody Map<String, Object> params) { public ApiResponse<Boolean> updateSpeaker(@RequestBody Map<String, Object> params) {

View File

@ -20,6 +20,7 @@ public class MeetingVO {
private List<Long> participantIds; private List<Long> participantIds;
private String tags; private String tags;
private String audioUrl; private String audioUrl;
private Integer duration;
private String summaryContent; private String summaryContent;
private Integer status; private Integer status;

View File

@ -3,19 +3,22 @@ package com.imeeting.service.biz;
import com.baomidou.mybatisplus.extension.service.IService; import com.baomidou.mybatisplus.extension.service.IService;
import com.imeeting.common.PageResult; import com.imeeting.common.PageResult;
import com.imeeting.dto.biz.MeetingDTO; import com.imeeting.dto.biz.MeetingDTO;
import com.imeeting.dto.biz.RealtimeTranscriptItemDTO;
import com.imeeting.dto.biz.MeetingTranscriptVO;
import com.imeeting.dto.biz.MeetingVO; import com.imeeting.dto.biz.MeetingVO;
import com.imeeting.entity.biz.Meeting; import com.imeeting.entity.biz.Meeting;
import com.imeeting.dto.biz.MeetingTranscriptVO;
import java.util.List; import java.util.List;
public interface MeetingService extends IService<Meeting> { public interface MeetingService extends IService<Meeting> {
MeetingVO createMeeting(MeetingDTO dto); MeetingVO createMeeting(MeetingDTO dto);
MeetingVO createRealtimeMeeting(MeetingDTO dto);
PageResult<List<MeetingVO>> pageMeetings(Integer current, Integer size, String title, Long tenantId, Long userId, String userName, String viewType, boolean isAdmin); PageResult<List<MeetingVO>> pageMeetings(Integer current, Integer size, String title, Long tenantId, Long userId, String userName, String viewType, boolean isAdmin);
void deleteMeeting(Long id); void deleteMeeting(Long id);
MeetingVO getDetail(Long id); MeetingVO getDetail(Long id);
List<MeetingTranscriptVO> getTranscripts(Long meetingId); List<MeetingTranscriptVO> getTranscripts(Long meetingId);
void appendRealtimeTranscripts(Long meetingId, List<RealtimeTranscriptItemDTO> items);
void completeRealtimeMeeting(Long meetingId, String audioUrl);
void updateSpeakerInfo(Long meetingId, String speakerId, String newName, String label); void updateSpeakerInfo(Long meetingId, String speakerId, String newName, String label);
void updateMeetingParticipants(Long meetingId, String participants); void updateMeetingParticipants(Long meetingId, String participants);
void reSummary(Long meetingId, Long summaryModelId, Long promptId); void reSummary(Long meetingId, Long summaryModelId, Long promptId);

View File

@ -47,7 +47,7 @@ public class AiModelServiceImpl implements AiModelService {
private final LlmModelMapper llmModelMapper; private final LlmModelMapper llmModelMapper;
private final HttpClient httpClient = HttpClient.newBuilder() private final HttpClient httpClient = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(10)) .connectTimeout(Duration.ofSeconds(300))
.build(); .build();
@Override @Override
@ -232,8 +232,8 @@ public class AiModelServiceImpl implements AiModelService {
} }
private String resolveModelListUrl(String providerKey, String baseUrl, String apiKey) { private String resolveModelListUrl(String providerKey, String baseUrl, String apiKey) {
if (baseUrl.contains("3050")) { if ("Custom".equalsIgnoreCase(providerKey)) {
return "http://10.100.51.199:3050/api/asrconfig"; return baseUrl+"/api/asrconfig";
} }
if ("gemini".equals(providerKey) || "google".equals(providerKey)) { if ("gemini".equals(providerKey) || "google".equals(providerKey)) {
if (apiKey == null || apiKey.isBlank()) { if (apiKey == null || apiKey.isBlank()) {

View File

@ -61,7 +61,8 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
private String uploadPath; private String uploadPath;
private final HttpClient httpClient = HttpClient.newBuilder() private final HttpClient httpClient = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(10)) .connectTimeout(Duration.ofSeconds(300))
.version(HttpClient.Version.HTTP_1_1)
.build(); .build();
@Override @Override
@ -206,7 +207,7 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
updateProgress(meeting.getId(), (int)(currentPercent * 0.85), data.path("message").asText(), eta); updateProgress(meeting.getId(), (int)(currentPercent * 0.85), data.path("message").asText(), eta);
if (currentPercent > 0 && currentPercent == lastPercent) { if (currentPercent > 0 && currentPercent == lastPercent) {
if (++unchangedCount > 45) throw new RuntimeException("识别任务长时间无进度增长,自动强制超时"); if (++unchangedCount > 300) throw new RuntimeException("识别任务长时间无进度增长,自动强制超时");
} else { } else {
unchangedCount = 0; unchangedCount = 0;
} }
@ -316,15 +317,19 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
this.updateById(taskRecord); this.updateById(taskRecord);
String url = llmModel.getBaseUrl() + (llmModel.getApiPath() != null ? llmModel.getApiPath() : "/v1/chat/completions"); String url = llmModel.getBaseUrl() + (llmModel.getApiPath() != null ? llmModel.getApiPath() : "/v1/chat/completions");
String requestBody = objectMapper.writeValueAsString(req);
log.info("Sending LLM summary request to url={}, body={}", url, requestBody);
HttpRequest request = HttpRequest.newBuilder() HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(url)) .uri(URI.create(url))
.header("Content-Type", "application/json") .header("Content-Type", "application/json; charset=UTF-8")
.header("Accept", "application/json")
.header("Authorization", "Bearer " + llmModel.getApiKey()) .header("Authorization", "Bearer " + llmModel.getApiKey())
.POST(HttpRequest.BodyPublishers.ofString(objectMapper.writeValueAsString(req))) .POST(HttpRequest.BodyPublishers.ofString(requestBody, StandardCharsets.UTF_8))
.build(); .build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
log.info("LLM summary response status={}, body={}", response.statusCode(), response.body());
JsonNode respNode = objectMapper.readTree(response.body()); JsonNode respNode = objectMapper.readTree(response.body());
if (response.statusCode() == 200 && respNode.has("choices")) { if (response.statusCode() == 200 && respNode.has("choices")) {

View File

@ -6,43 +6,43 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.imeeting.common.PageResult; import com.imeeting.common.PageResult;
import com.imeeting.dto.biz.MeetingDTO; import com.imeeting.dto.biz.MeetingDTO;
import com.imeeting.dto.biz.MeetingVO;
import com.imeeting.dto.biz.MeetingTranscriptVO; import com.imeeting.dto.biz.MeetingTranscriptVO;
import com.imeeting.entity.biz.Meeting; import com.imeeting.dto.biz.MeetingVO;
import com.imeeting.entity.biz.AiTask; import com.imeeting.dto.biz.RealtimeTranscriptItemDTO;
import com.imeeting.entity.biz.PromptTemplate;
import com.imeeting.entity.biz.MeetingTranscript;
import com.imeeting.entity.biz.HotWord;
import com.imeeting.entity.SysUser; import com.imeeting.entity.SysUser;
import com.imeeting.entity.biz.AiTask;
import com.imeeting.entity.biz.HotWord;
import com.imeeting.entity.biz.Meeting;
import com.imeeting.entity.biz.MeetingTranscript;
import com.imeeting.entity.biz.PromptTemplate;
import com.imeeting.event.MeetingCreatedEvent;
import com.imeeting.mapper.SysUserMapper;
import com.imeeting.mapper.biz.AiTaskMapper; import com.imeeting.mapper.biz.AiTaskMapper;
import com.imeeting.mapper.biz.MeetingMapper; import com.imeeting.mapper.biz.MeetingMapper;
import com.imeeting.mapper.biz.MeetingTranscriptMapper; import com.imeeting.mapper.biz.MeetingTranscriptMapper;
import com.imeeting.mapper.SysUserMapper;
import com.imeeting.event.MeetingCreatedEvent;
import com.imeeting.service.biz.MeetingService;
import com.imeeting.service.biz.AiModelService; import com.imeeting.service.biz.AiModelService;
import com.imeeting.service.biz.PromptTemplateService;
import com.imeeting.service.biz.AiTaskService; import com.imeeting.service.biz.AiTaskService;
import com.imeeting.service.biz.HotWordService; import com.imeeting.service.biz.HotWordService;
import com.imeeting.service.biz.MeetingService;
import com.imeeting.service.biz.PromptTemplateService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.FileSystemUtils;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.nio.file.StandardCopyOption; import java.nio.file.StandardCopyOption;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@Slf4j @Slf4j
@ -65,48 +65,10 @@ public class MeetingServiceImpl extends ServiceImpl<MeetingMapper, Meeting> impl
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public MeetingVO createMeeting(MeetingDTO dto) { public MeetingVO createMeeting(MeetingDTO dto) {
Meeting meeting = new Meeting(); Meeting meeting = initMeeting(dto, 0);
meeting.setTitle(dto.getTitle()); meeting.setAudioUrl(relocateAudioUrl(meeting.getId(), dto.getAudioUrl()));
meeting.setMeetingTime(dto.getMeetingTime());
meeting.setParticipants(dto.getParticipants());
meeting.setTags(dto.getTags());
meeting.setCreatorId(dto.getCreatorId());
meeting.setCreatorName(dto.getCreatorName());
meeting.setTenantId(dto.getTenantId() != null ? dto.getTenantId() : 0L);
meeting.setStatus(0);
this.save(meeting); // Save to get meeting ID
// File moving logic
String audioUrl = dto.getAudioUrl();
if (audioUrl != null && audioUrl.startsWith("/api/static/audio/")) {
try {
String fileName = audioUrl.substring(audioUrl.lastIndexOf("/") + 1);
String basePath = uploadPath.endsWith("/") ? uploadPath : uploadPath + "/";
Path sourcePath = Paths.get(basePath, "audio", fileName);
if (Files.exists(sourcePath)) {
String ext = "";
int dotIdx = fileName.lastIndexOf('.');
if (dotIdx > 0) {
ext = fileName.substring(dotIdx);
}
String targetDir = basePath + "meetings/" + meeting.getId();
Files.createDirectories(Paths.get(targetDir));
Path targetPath = Paths.get(targetDir, "source_audio" + ext);
Files.move(sourcePath, targetPath, StandardCopyOption.REPLACE_EXISTING);
audioUrl = "/api/static/meetings/" + meeting.getId() + "/source_audio" + ext;
}
} catch (Exception e) {
log.error("Failed to move audio file for meeting {}", meeting.getId(), e);
throw new RuntimeException("文件处理失败: " + e.getMessage());
}
}
meeting.setAudioUrl(audioUrl);
this.updateById(meeting); this.updateById(meeting);
// ASR Task
AiTask asrTask = new AiTask(); AiTask asrTask = new AiTask();
asrTask.setMeetingId(meeting.getId()); asrTask.setMeetingId(meeting.getId());
asrTask.setTaskType("ASR"); asrTask.setTaskType("ASR");
@ -119,49 +81,41 @@ public class MeetingServiceImpl extends ServiceImpl<MeetingMapper, Meeting> impl
List<String> finalHotWords = dto.getHotWords(); List<String> finalHotWords = dto.getHotWords();
if (finalHotWords == null || finalHotWords.isEmpty()) { if (finalHotWords == null || finalHotWords.isEmpty()) {
finalHotWords = hotWordService.list(new LambdaQueryWrapper<HotWord>() finalHotWords = hotWordService.list(new LambdaQueryWrapper<HotWord>()
.eq(HotWord::getTenantId, meeting.getTenantId()) .eq(HotWord::getTenantId, meeting.getTenantId())
.eq(HotWord::getStatus, 1)) .eq(HotWord::getStatus, 1))
.stream().map(HotWord::getWord).collect(Collectors.toList()); .stream()
.map(HotWord::getWord)
.collect(Collectors.toList());
} }
asrConfig.put("hotWords", finalHotWords); asrConfig.put("hotWords", finalHotWords);
asrTask.setTaskConfig(asrConfig); asrTask.setTaskConfig(asrConfig);
aiTaskService.save(asrTask); aiTaskService.save(asrTask);
// SUMMARY Task createSummaryTask(meeting.getId(), dto.getSummaryModelId(), dto.getPromptId());
AiTask sumTask = new AiTask();
sumTask.setMeetingId(meeting.getId());
sumTask.setTaskType("SUMMARY");
sumTask.setStatus(0);
Map<String, Object> sumConfig = new HashMap<>();
sumConfig.put("summaryModelId", dto.getSummaryModelId());
if (dto.getPromptId() != null) {
PromptTemplate template = promptTemplateService.getById(dto.getPromptId());
if (template != null) {
sumConfig.put("promptContent", template.getPromptContent());
}
}
sumTask.setTaskConfig(sumConfig);
aiTaskService.save(sumTask);
eventPublisher.publishEvent(new MeetingCreatedEvent(meeting.getId())); eventPublisher.publishEvent(new MeetingCreatedEvent(meeting.getId()));
return toVO(meeting, false); return toVO(meeting, false);
} }
@Override
@Transactional(rollbackFor = Exception.class)
public MeetingVO createRealtimeMeeting(MeetingDTO dto) {
Meeting meeting = initMeeting(dto, 1);
createSummaryTask(meeting.getId(), dto.getSummaryModelId(), dto.getPromptId());
return toVO(meeting, false);
}
@Override @Override
public PageResult<List<MeetingVO>> pageMeetings(Integer current, Integer size, String title, Long tenantId, Long userId, String userName, String viewType, boolean isAdmin) { public PageResult<List<MeetingVO>> pageMeetings(Integer current, Integer size, String title, Long tenantId, Long userId, String userName, String viewType, boolean isAdmin) {
LambdaQueryWrapper<Meeting> wrapper = new LambdaQueryWrapper<Meeting>() LambdaQueryWrapper<Meeting> wrapper = new LambdaQueryWrapper<Meeting>()
.eq(Meeting::getTenantId, tenantId); .eq(Meeting::getTenantId, tenantId);
if (isAdmin && "all".equals(viewType)) { if (!isAdmin || !"all".equals(viewType)) {
// 管理员全局可见
} else {
String userIdStr = String.valueOf(userId); String userIdStr = String.valueOf(userId);
if ("created".equals(viewType)) { if ("created".equals(viewType)) {
wrapper.eq(Meeting::getCreatorId, userId); wrapper.eq(Meeting::getCreatorId, userId);
} else if ("involved".equals(viewType)) { } else if ("involved".equals(viewType)) {
wrapper.and(w -> w.apply("',' || participants || ',' LIKE '%,' || {0} || ',%'", userIdStr)) wrapper.and(w -> w.apply("',' || participants || ',' LIKE '%,' || {0} || ',%'", userIdStr))
.ne(Meeting::getCreatorId, userId); .ne(Meeting::getCreatorId, userId);
} else { } else {
wrapper.and(w -> w.eq(Meeting::getCreatorId, userId) wrapper.and(w -> w.eq(Meeting::getCreatorId, userId)
.or() .or()
@ -199,9 +153,10 @@ public class MeetingServiceImpl extends ServiceImpl<MeetingMapper, Meeting> impl
@Override @Override
public List<MeetingTranscriptVO> getTranscripts(Long meetingId) { public List<MeetingTranscriptVO> getTranscripts(Long meetingId) {
return transcriptMapper.selectList(new LambdaQueryWrapper<MeetingTranscript>() return transcriptMapper.selectList(new LambdaQueryWrapper<MeetingTranscript>()
.eq(MeetingTranscript::getMeetingId, meetingId) .eq(MeetingTranscript::getMeetingId, meetingId)
.orderByAsc(MeetingTranscript::getStartTime)) .orderByAsc(MeetingTranscript::getStartTime))
.stream().map(t -> { .stream()
.map(t -> {
MeetingTranscriptVO vo = new MeetingTranscriptVO(); MeetingTranscriptVO vo = new MeetingTranscriptVO();
vo.setId(t.getId()); vo.setId(t.getId());
vo.setSpeakerId(t.getSpeakerId()); vo.setSpeakerId(t.getSpeakerId());
@ -214,6 +169,75 @@ public class MeetingServiceImpl extends ServiceImpl<MeetingMapper, Meeting> impl
}).collect(Collectors.toList()); }).collect(Collectors.toList());
} }
@Override
@Transactional(rollbackFor = Exception.class)
public void appendRealtimeTranscripts(Long meetingId, List<RealtimeTranscriptItemDTO> items) {
if (items == null || items.isEmpty()) {
return;
}
Integer maxSortOrder = transcriptMapper.selectList(new LambdaQueryWrapper<MeetingTranscript>()
.eq(MeetingTranscript::getMeetingId, meetingId)
.orderByDesc(MeetingTranscript::getSortOrder)
.last("LIMIT 1"))
.stream()
.findFirst()
.map(MeetingTranscript::getSortOrder)
.orElse(0);
int nextSortOrder = maxSortOrder == null ? 0 : maxSortOrder + 1;
for (RealtimeTranscriptItemDTO item : items) {
if (item.getContent() == null || item.getContent().isBlank()) {
continue;
}
MeetingTranscript existing = transcriptMapper.selectOne(new LambdaQueryWrapper<MeetingTranscript>()
.eq(MeetingTranscript::getMeetingId, meetingId)
.eq(MeetingTranscript::getContent, item.getContent().trim())
.eq(item.getSpeakerId() != null && !item.getSpeakerId().isBlank(), MeetingTranscript::getSpeakerId, item.getSpeakerId())
.eq(item.getStartTime() != null, MeetingTranscript::getStartTime, item.getStartTime())
.eq(item.getEndTime() != null, MeetingTranscript::getEndTime, item.getEndTime())
.last("LIMIT 1"));
if (existing != null) {
continue;
}
MeetingTranscript transcript = new MeetingTranscript();
transcript.setMeetingId(meetingId);
transcript.setSpeakerId(resolveSpeakerId(item));
transcript.setSpeakerName(resolveSpeakerName(item));
transcript.setContent(item.getContent().trim());
transcript.setStartTime(item.getStartTime());
transcript.setEndTime(item.getEndTime());
transcript.setSortOrder(nextSortOrder++);
transcriptMapper.insert(transcript);
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public void completeRealtimeMeeting(Long meetingId, String audioUrl) {
Meeting meeting = this.getById(meetingId);
if (meeting == null) {
throw new RuntimeException("Meeting not found");
}
if (audioUrl != null && !audioUrl.isBlank()) {
meeting.setAudioUrl(relocateAudioUrl(meetingId, audioUrl));
this.updateById(meeting);
}
long transcriptCount = transcriptMapper.selectCount(new LambdaQueryWrapper<MeetingTranscript>()
.eq(MeetingTranscript::getMeetingId, meetingId));
if (transcriptCount <= 0) {
meeting.setStatus(4);
this.updateById(meeting);
throw new RuntimeException("未接收到可用的实时转录内容");
}
aiTaskService.dispatchSummaryTask(meetingId);
}
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public void updateSpeakerInfo(Long meetingId, String speakerId, String newName, String label) { public void updateSpeakerInfo(Long meetingId, String speakerId, String newName, String label) {
@ -236,32 +260,19 @@ public class MeetingServiceImpl extends ServiceImpl<MeetingMapper, Meeting> impl
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public void reSummary(Long meetingId, Long summaryModelId, Long promptId) { public void reSummary(Long meetingId, Long summaryModelId, Long promptId) {
Meeting meeting = this.getById(meetingId); Meeting meeting = this.getById(meetingId);
if (meeting == null) throw new RuntimeException("Meeting not found"); if (meeting == null) {
throw new RuntimeException("Meeting not found");
AiTask sumTask = new AiTask();
sumTask.setMeetingId(meetingId);
sumTask.setTaskType("SUMMARY");
sumTask.setStatus(0);
Map<String, Object> sumConfig = new HashMap<>();
sumConfig.put("summaryModelId", summaryModelId);
if (promptId != null) {
PromptTemplate template = promptTemplateService.getById(promptId);
if (template != null) {
sumConfig.put("promptContent", template.getPromptContent());
}
} }
sumTask.setTaskConfig(sumConfig);
aiTaskService.save(sumTask);
createSummaryTask(meetingId, summaryModelId, promptId);
meeting.setStatus(2); meeting.setStatus(2);
this.updateById(meeting); this.updateById(meeting);
aiTaskService.dispatchSummaryTask(meetingId); aiTaskService.dispatchSummaryTask(meetingId);
} }
@Override @Override
public java.util.Map<String, Object> getDashboardStats(Long tenantId, Long userId, boolean isAdmin) { public Map<String, Object> getDashboardStats(Long tenantId, Long userId, boolean isAdmin) {
java.util.Map<String, Object> stats = new java.util.HashMap<>(); Map<String, Object> stats = new HashMap<>();
LambdaQueryWrapper<Meeting> baseWrapper = new LambdaQueryWrapper<Meeting>().eq(Meeting::getTenantId, tenantId); LambdaQueryWrapper<Meeting> baseWrapper = new LambdaQueryWrapper<Meeting>().eq(Meeting::getTenantId, tenantId);
if (!isAdmin) { if (!isAdmin) {
String userIdStr = String.valueOf(userId); String userIdStr = String.valueOf(userId);
@ -277,7 +288,7 @@ public class MeetingServiceImpl extends ServiceImpl<MeetingMapper, Meeting> impl
long totalFinished = this.count(baseWrapper.clone().in(Meeting::getStatus, 3, 4)); long totalFinished = this.count(baseWrapper.clone().in(Meeting::getStatus, 3, 4));
long success = this.count(baseWrapper.clone().eq(Meeting::getStatus, 3)); long success = this.count(baseWrapper.clone().eq(Meeting::getStatus, 3));
stats.put("successRate", totalFinished == 0 ? 100 : (int)((double)success / totalFinished * 100)); stats.put("successRate", totalFinished == 0 ? 100 : (int) ((double) success / totalFinished * 100));
return stats; return stats;
} }
@ -294,6 +305,92 @@ public class MeetingServiceImpl extends ServiceImpl<MeetingMapper, Meeting> impl
return this.list(wrapper).stream().map(m -> toVO(m, false)).collect(Collectors.toList()); return this.list(wrapper).stream().map(m -> toVO(m, false)).collect(Collectors.toList());
} }
private void createSummaryTask(Long meetingId, Long summaryModelId, Long promptId) {
AiTask sumTask = new AiTask();
sumTask.setMeetingId(meetingId);
sumTask.setTaskType("SUMMARY");
sumTask.setStatus(0);
Map<String, Object> sumConfig = new HashMap<>();
sumConfig.put("summaryModelId", summaryModelId);
if (promptId != null) {
PromptTemplate template = promptTemplateService.getById(promptId);
if (template != null) {
sumConfig.put("promptContent", template.getPromptContent());
}
}
sumTask.setTaskConfig(sumConfig);
aiTaskService.save(sumTask);
}
private Meeting initMeeting(MeetingDTO dto, int status) {
Meeting meeting = new Meeting();
meeting.setTitle(dto.getTitle());
meeting.setMeetingTime(dto.getMeetingTime());
meeting.setParticipants(dto.getParticipants());
meeting.setTags(dto.getTags());
meeting.setCreatorId(dto.getCreatorId());
meeting.setCreatorName(dto.getCreatorName());
meeting.setTenantId(dto.getTenantId() != null ? dto.getTenantId() : 0L);
meeting.setAudioUrl(dto.getAudioUrl());
meeting.setStatus(status);
this.save(meeting);
return meeting;
}
private String relocateAudioUrl(Long meetingId, String audioUrl) {
if (audioUrl == null || audioUrl.isBlank()) {
return audioUrl;
}
if (!audioUrl.startsWith("/api/static/audio/")) {
return audioUrl;
}
try {
String fileName = audioUrl.substring(audioUrl.lastIndexOf("/") + 1);
String basePath = uploadPath.endsWith("/") ? uploadPath : uploadPath + "/";
Path sourcePath = Paths.get(basePath, "audio", fileName);
if (!Files.exists(sourcePath)) {
return audioUrl;
}
String ext = "";
int dotIdx = fileName.lastIndexOf('.');
if (dotIdx > 0) {
ext = fileName.substring(dotIdx);
}
Path targetDir = Paths.get(basePath, "meetings", String.valueOf(meetingId));
Files.createDirectories(targetDir);
Path targetPath = targetDir.resolve("source_audio" + ext);
Files.move(sourcePath, targetPath, StandardCopyOption.REPLACE_EXISTING);
return "/api/static/meetings/" + meetingId + "/source_audio" + ext;
} catch (Exception e) {
log.error("Failed to move audio file for meeting {}", meetingId, e);
throw new RuntimeException("文件处理失败: " + e.getMessage());
}
}
private String resolveSpeakerId(RealtimeTranscriptItemDTO item) {
if (item.getSpeakerId() != null && !item.getSpeakerId().isBlank()) {
return item.getSpeakerId();
}
return "spk_0";
}
private String resolveSpeakerName(RealtimeTranscriptItemDTO item) {
if (item.getSpeakerName() != null && !item.getSpeakerName().isBlank()) {
return item.getSpeakerName();
}
String speakerId = resolveSpeakerId(item);
if (speakerId.matches("\\d+")) {
SysUser user = sysUserMapper.selectById(Long.parseLong(speakerId));
if (user != null) {
return user.getDisplayName() != null ? user.getDisplayName() : user.getUsername();
}
}
return speakerId;
}
private MeetingVO toVO(Meeting meeting, boolean includeSummary) { private MeetingVO toVO(Meeting meeting, boolean includeSummary) {
MeetingVO vo = new MeetingVO(); MeetingVO vo = new MeetingVO();
vo.setId(meeting.getId()); vo.setId(meeting.getId());
@ -304,17 +401,23 @@ public class MeetingServiceImpl extends ServiceImpl<MeetingMapper, Meeting> impl
vo.setMeetingTime(meeting.getMeetingTime()); vo.setMeetingTime(meeting.getMeetingTime());
vo.setTags(meeting.getTags()); vo.setTags(meeting.getTags());
vo.setAudioUrl(meeting.getAudioUrl()); vo.setAudioUrl(meeting.getAudioUrl());
vo.setDuration(resolveMeetingDuration(meeting.getId()));
vo.setStatus(meeting.getStatus()); vo.setStatus(meeting.getStatus());
vo.setCreatedAt(meeting.getCreatedAt()); vo.setCreatedAt(meeting.getCreatedAt());
if (meeting.getParticipants() != null && !meeting.getParticipants().isEmpty()) { if (meeting.getParticipants() != null && !meeting.getParticipants().isEmpty()) {
try { try {
List<Long> userIds = Arrays.stream(meeting.getParticipants().split(",")) List<Long> userIds = Arrays.stream(meeting.getParticipants().split(","))
.map(String::trim).filter(s -> !s.isEmpty()).map(Long::valueOf).collect(Collectors.toList()); .map(String::trim)
.filter(s -> !s.isEmpty())
.map(Long::valueOf)
.collect(Collectors.toList());
vo.setParticipantIds(userIds); vo.setParticipantIds(userIds);
if (!userIds.isEmpty()) { if (!userIds.isEmpty()) {
List<SysUser> users = sysUserMapper.selectBatchIds(userIds); List<SysUser> users = sysUserMapper.selectBatchIds(userIds);
String names = users.stream().map(u -> u.getDisplayName() != null ? u.getDisplayName() : u.getUsername()).collect(Collectors.joining(", ")); String names = users.stream()
.map(u -> u.getDisplayName() != null ? u.getDisplayName() : u.getUsername())
.collect(Collectors.joining(", "));
vo.setParticipants(names); vo.setParticipants(names);
} }
} catch (Exception e) { } catch (Exception e) {
@ -363,6 +466,27 @@ public class MeetingServiceImpl extends ServiceImpl<MeetingMapper, Meeting> impl
} }
} }
private Integer resolveMeetingDuration(Long meetingId) {
MeetingTranscript latestTranscript = transcriptMapper.selectOne(new LambdaQueryWrapper<MeetingTranscript>()
.eq(MeetingTranscript::getMeetingId, meetingId)
.isNotNull(MeetingTranscript::getEndTime)
.orderByDesc(MeetingTranscript::getEndTime)
.last("LIMIT 1"));
if (latestTranscript != null && latestTranscript.getEndTime() != null && latestTranscript.getEndTime() > 0) {
return latestTranscript.getEndTime();
}
latestTranscript = transcriptMapper.selectOne(new LambdaQueryWrapper<MeetingTranscript>()
.eq(MeetingTranscript::getMeetingId, meetingId)
.isNotNull(MeetingTranscript::getStartTime)
.orderByDesc(MeetingTranscript::getStartTime)
.last("LIMIT 1"));
if (latestTranscript != null && latestTranscript.getStartTime() != null && latestTranscript.getStartTime() > 0) {
return latestTranscript.getStartTime();
}
return null;
}
private String stripFrontMatter(String markdown) { private String stripFrontMatter(String markdown) {
if (markdown == null || markdown.isBlank()) { if (markdown == null || markdown.isBlank()) {
return markdown; return markdown;

View File

@ -1,14 +1,14 @@
server: server:
port: 8080 port: 8081
spring: spring:
datasource: datasource:
url: jdbc:postgresql://10.100.51.199:5432/imeeting_db url: jdbc:postgresql://192.168.1.55:5432/imeeting_db
username: postgres username: postgres
password: postgres password: postgres
data: data:
redis: redis:
host: 10.100.51.199 host: 192.168.1.55
port: 6379 port: 6379
password: unis@123 password: unis@123
database: 15 database: 15
@ -40,7 +40,7 @@ security:
secret: change-me-please-change-me-32bytes secret: change-me-please-change-me-32bytes
app: app:
server-base-url: http://10.100.52.13:8080 # 本地应用对外暴露的 IP 和端口 server-base-url: http://10.100.52.13:${server.port} # 本地应用对外暴露的 IP 和端口
upload-path: D:/data/imeeting/uploads/ upload-path: D:/data/imeeting/uploads/
resource-prefix: /api/static/ resource-prefix: /api/static/
captcha: captcha:

View File

@ -23,9 +23,12 @@ export interface MeetingDTO {
meetingTime: string; meetingTime: string;
participants: string; participants: string;
tags: string; tags: string;
audioUrl: string; audioUrl?: string;
asrModelId: number; asrModelId: number;
summaryModelId?: number;
promptId: number; promptId: number;
useSpkId?: number;
hotWords?: string[];
} }
export const getMeetingPage = (params: { export const getMeetingPage = (params: {
@ -47,6 +50,35 @@ export const createMeeting = (data: MeetingDTO) => {
); );
}; };
export interface RealtimeTranscriptItemDTO {
speakerId?: string;
speakerName?: string;
content: string;
startTime?: number;
endTime?: number;
}
export const createRealtimeMeeting = (data: MeetingDTO) => {
return http.post<any, { code: string; data: MeetingVO; msg: string }>(
"/api/biz/meeting/realtime/start",
data
);
};
export const appendRealtimeTranscripts = (meetingId: number, data: RealtimeTranscriptItemDTO[]) => {
return http.post<any, { code: string; data: boolean; msg: string }>(
`/api/biz/meeting/${meetingId}/realtime/transcripts`,
data
);
};
export const completeRealtimeMeeting = (meetingId: number, data?: { audioUrl?: string }) => {
return http.post<any, { code: string; data: boolean; msg: string }>(
`/api/biz/meeting/${meetingId}/realtime/complete`,
data || {}
);
};
export const deleteMeeting = (id: number) => { export const deleteMeeting = (id: number) => {
return http.delete<any, { code: string; data: boolean; msg: string }>( return http.delete<any, { code: string; data: boolean; msg: string }>(
`/api/biz/meeting/${id}` `/api/biz/meeting/${id}`

View File

@ -1,5 +1,6 @@
import React, { useEffect, useMemo, useRef, useState } from "react"; import React, { useEffect, useMemo, useRef, useState } from "react";
import { import {
AutoComplete,
Button, Button,
Card, Card,
Col, Col,
@ -82,19 +83,21 @@ const AiModels: React.FC = () => {
if (!profileStr) { if (!profileStr) {
return false; return false;
} }
const profile = JSON.parse(profileStr); const profile = JSON.parse(profileStr);
return profile.isPlatformAdmin === true; return profile.isPlatformAdmin === true;
}, []); }, []);
useEffect(() => { useEffect(() => {
fetchData(); void fetchData();
}, [current, size, searchName, activeType]); }, [current, size, searchName, activeType]);
useEffect(() => { useEffect(() => {
if (!drawerVisible || !provider) { if (!drawerVisible || !provider) {
return; return;
} }
const providerItem = providers.find((i) => i.itemValue === provider);
const providerItem = providers.find((item) => item.itemValue === provider);
const providerLabel = providerItem?.itemLabel || provider; const providerLabel = providerItem?.itemLabel || provider;
const currentDisplayName = form.getFieldValue("modelName"); const currentDisplayName = form.getFieldValue("modelName");
if (!editingId && (!currentDisplayName || modelNameAutoFilledRef.current)) { if (!editingId && (!currentDisplayName || modelNameAutoFilledRef.current)) {
@ -130,6 +133,7 @@ const AiModels: React.FC = () => {
const openDrawer = (record?: AiModelVO) => { const openDrawer = (record?: AiModelVO) => {
setRemoteModels([]); setRemoteModels([]);
modelNameAutoFilledRef.current = false; modelNameAutoFilledRef.current = false;
if (record) { if (record) {
setEditingId(record.id); setEditingId(record.id);
form.setFieldsValue({ form.setFieldsValue({
@ -153,18 +157,20 @@ const AiModels: React.FC = () => {
apiPath: "/v1/chat/completions", apiPath: "/v1/chat/completions",
}); });
} }
setDrawerVisible(true); setDrawerVisible(true);
}; };
const handleFetchRemote = async () => { const handleFetchRemote = async () => {
const vals = form.getFieldsValue(["provider", "baseUrl", "apiKey"]); const values = form.getFieldsValue(["provider", "baseUrl", "apiKey"]);
if (!vals.provider || !vals.baseUrl) { if (!values.provider || !values.baseUrl) {
message.warning("请先填写提供商和 Base URL"); message.warning("请先填写提供商和 Base URL");
return; return;
} }
setFetchLoading(true); setFetchLoading(true);
try { try {
const res = await getRemoteModelList(vals); const res = await getRemoteModelList(values);
const rawModels = (res as any)?.data?.data ?? (Array.isArray(res) ? res : []); const rawModels = (res as any)?.data?.data ?? (Array.isArray(res) ? res : []);
const models = Array.isArray(rawModels) ? rawModels : []; const models = Array.isArray(rawModels) ? rawModels : [];
setRemoteModels(models); setRemoteModels(models);
@ -203,7 +209,7 @@ const AiModels: React.FC = () => {
message.success("新增成功"); message.success("新增成功");
} }
setDrawerVisible(false); setDrawerVisible(false);
fetchData(); void fetchData();
} finally { } finally {
setSubmitLoading(false); setSubmitLoading(false);
} }
@ -212,7 +218,7 @@ const AiModels: React.FC = () => {
const handleDelete = async (record: AiModelVO) => { const handleDelete = async (record: AiModelVO) => {
await deleteAiModelByType(record.id, record.modelType); await deleteAiModelByType(record.id, record.modelType);
message.success("删除成功"); message.success("删除成功");
fetchData(); void fetchData();
}; };
const columns = [ const columns = [
@ -236,9 +242,9 @@ const AiModels: React.FC = () => {
title: "提供商", title: "提供商",
dataIndex: "provider", dataIndex: "provider",
key: "provider", key: "provider",
render: (val: string) => { render: (value: string) => {
const item = providers.find((i) => i.itemValue === val); const item = providers.find((providerItem) => providerItem.itemValue === value);
return item ? <Tag>{item.itemLabel}</Tag> : val; return item ? <Tag>{item.itemLabel}</Tag> : value;
}, },
}, },
{ title: "模型名称(code)", dataIndex: "modelCode", key: "modelCode" }, { title: "模型名称(code)", dataIndex: "modelCode", key: "modelCode" },
@ -284,7 +290,7 @@ const AiModels: React.FC = () => {
placeholder="搜索模型名称" placeholder="搜索模型名称"
prefix={<SearchOutlined />} prefix={<SearchOutlined />}
allowClear allowClear
onPressEnter={(e) => setSearchName((e.target as HTMLInputElement).value)} onPressEnter={(event) => setSearchName((event.target as HTMLInputElement).value)}
style={{ width: 220 }} style={{ width: 220 }}
/> />
<Button type="primary" icon={<PlusOutlined />} onClick={() => openDrawer()}> <Button type="primary" icon={<PlusOutlined />} onClick={() => openDrawer()}>
@ -314,9 +320,9 @@ const AiModels: React.FC = () => {
current, current,
pageSize: size, pageSize: size,
total, total,
onChange: (p, s) => { onChange: (page, pageSize) => {
setCurrent(p); setCurrent(page);
setSize(s); setSize(pageSize);
}, },
}} }}
/> />
@ -349,7 +355,11 @@ const AiModels: React.FC = () => {
<Row gutter={16}> <Row gutter={16}>
<Col span={12}> <Col span={12}>
<Form.Item name="modelName" label="显示名称" rules={[{ required: true, message: "请输入显示名称" }]}> <Form.Item
name="modelName"
label="显示名称"
rules={[{ required: true, message: "请输入显示名称" }]}
>
<Input <Input
onChange={() => { onChange={() => {
modelNameAutoFilledRef.current = false; modelNameAutoFilledRef.current = false;
@ -358,7 +368,11 @@ const AiModels: React.FC = () => {
</Form.Item> </Form.Item>
</Col> </Col>
<Col span={12}> <Col span={12}>
<Form.Item name="provider" label="提供商" rules={[{ required: true, message: "请选择提供商" }]}> <Form.Item
name="provider"
label="提供商"
rules={[{ required: true, message: "请选择提供商" }]}
>
<Select allowClear placeholder="请选择"> <Select allowClear placeholder="请选择">
{providers.map((item) => ( {providers.map((item) => (
<Option key={item.itemValue} value={item.itemValue}> <Option key={item.itemValue} value={item.itemValue}>
@ -384,22 +398,28 @@ const AiModels: React.FC = () => {
</Divider> </Divider>
<Form.Item label="模型名称" required tooltip="单选,值将作为模型 code 传给后端"> <Form.Item
label="模型名称"
required
tooltip="可从远程列表选择,也可手动输入;值将作为模型 code 传给后端"
>
<Space.Compact style={{ width: "100%" }}> <Space.Compact style={{ width: "100%" }}>
<Form.Item name="modelCode" noStyle rules={[{ required: true, message: "请选择模型名称" }]}> <Form.Item
<Select name="modelCode"
noStyle
rules={[{ required: true, message: "请输入或选择模型名称" }]}
>
<AutoComplete
allowClear allowClear
showSearch
style={{ width: "calc(100% - 100px)" }} style={{ width: "calc(100% - 100px)" }}
placeholder="选择模型名称" placeholder="可选择或自定义输入模型名称"
optionFilterProp="children" options={remoteModels.map((model) => ({ value: model }))}
filterOption={(inputValue, option) =>
String(option?.value || "").toLowerCase().includes(inputValue.toLowerCase())
}
> >
{remoteModels.map((m) => ( <Input />
<Option key={m} value={m}> </AutoComplete>
{m}
</Option>
))}
</Select>
</Form.Item> </Form.Item>
<Button icon={<SyncOutlined spin={fetchLoading} />} onClick={handleFetchRemote} style={{ width: 100 }}> <Button icon={<SyncOutlined spin={fetchLoading} />} onClick={handleFetchRemote} style={{ width: 100 }}>

View File

@ -247,6 +247,7 @@ const MeetingDetail: React.FC = () => {
} }
return false; return false;
}, [meeting]); }, [meeting]);
const canRetrySummary = isOwner && transcripts.length > 0 && meeting?.status !== 1 && meeting?.status !== 2;
useEffect(() => { useEffect(() => {
if (id) { if (id) {
@ -458,7 +459,7 @@ const MeetingDetail: React.FC = () => {
</Col> </Col>
<Col> <Col>
<Space> <Space>
{isOwner && meeting.status === 3 && ( {canRetrySummary && (
<Button icon={<SyncOutlined />} type="primary" ghost onClick={() => setSummaryVisible(true)} disabled={actionLoading}> <Button icon={<SyncOutlined />} type="primary" ghost onClick={() => setSummaryVisible(true)} disabled={actionLoading}>
</Button> </Button>

View File

@ -9,6 +9,7 @@ import {
AudioOutlined AudioOutlined
} from '@ant-design/icons'; } from '@ant-design/icons';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { usePermission } from '../../hooks/usePermission';
import { getMeetingPage, deleteMeeting, MeetingVO, getMeetingProgress, MeetingProgress, createMeeting, uploadAudio, updateMeetingParticipants } from '../../api/business/meeting'; import { getMeetingPage, deleteMeeting, MeetingVO, getMeetingProgress, MeetingProgress, createMeeting, uploadAudio, updateMeetingParticipants } from '../../api/business/meeting';
import { getAiModelPage, getAiModelDefault, AiModelVO } from '../../api/business/aimodel'; import { getAiModelPage, getAiModelDefault, AiModelVO } from '../../api/business/aimodel';
import { getPromptPage, PromptTemplateVO } from '../../api/business/prompt'; import { getPromptPage, PromptTemplateVO } from '../../api/business/prompt';
@ -340,6 +341,7 @@ const MeetingCardItem: React.FC<{ item: MeetingVO, config: any, fetchData: () =>
const Meetings: React.FC = () => { const Meetings: React.FC = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const navigate = useNavigate(); const navigate = useNavigate();
const { can } = usePermission();
const [form] = Form.useForm(); const [form] = Form.useForm();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [submitLoading, setSubmitLoading] = useState(false); const [submitLoading, setSubmitLoading] = useState(false);
@ -456,7 +458,8 @@ const Meetings: React.FC = () => {
setUploadProgress(0); setUploadProgress(0);
setFileList([]); setFileList([]);
setCreateDrawerVisible(true); setCreateDrawerVisible(true);
}} style={{ borderRadius: 8, height: 36, fontWeight: 500 }}></Button> }} style={{ borderRadius: 8, height: 36, fontWeight: 500 }}></Button>
{can("meeting:create:realtime") && <Button icon={<AudioOutlined />} onClick={() => navigate('/meeting-live-create')} style={{ borderRadius: 8, height: 36, fontWeight: 500 }}></Button>}
</Space> </Space>
</Col> </Col>
</Row> </Row>

View File

@ -13,6 +13,8 @@ import SysParams from "../pages/SysParams";
import PlatformSettings from "../pages/PlatformSettings"; import PlatformSettings from "../pages/PlatformSettings";
import Profile from "../pages/Profile"; import Profile from "../pages/Profile";
import SpeakerReg from "../pages/business/SpeakerReg"; import SpeakerReg from "../pages/business/SpeakerReg";
import RealtimeAsr from "../pages/business/RealtimeAsr";
import RealtimeAsrSession from "../pages/business/RealtimeAsrSession";
import HotWords from "../pages/business/HotWords"; import HotWords from "../pages/business/HotWords";
import PromptTemplates from "../pages/business/PromptTemplates"; import PromptTemplates from "../pages/business/PromptTemplates";
import AiModels from "../pages/business/AiModels"; import AiModels from "../pages/business/AiModels";
@ -25,6 +27,7 @@ import type { MenuRoute } from "../types";
export const menuRoutes: MenuRoute[] = [ export const menuRoutes: MenuRoute[] = [
{ path: "/", label: "总览", element: <Dashboard />, perm: "menu:dashboard" }, { path: "/", label: "总览", element: <Dashboard />, perm: "menu:dashboard" },
{ path: "/profile", label: "个人中心", element: <Profile /> }, { path: "/profile", label: "个人中心", element: <Profile /> },
{ path: "/realtime-asr", label: "实时识别", element: <RealtimeAsr />, perm: "menu:meeting" },
{ path: "/speaker-reg", label: "声纹注册", element: <SpeakerReg />, perm: "menu:speaker" }, { path: "/speaker-reg", label: "声纹注册", element: <SpeakerReg />, perm: "menu:speaker" },
{ path: "/tenants", label: "租户管理", element: <Tenants />, perm: "menu:tenants" }, { path: "/tenants", label: "租户管理", element: <Tenants />, perm: "menu:tenants" },
{ path: "/orgs", label: "组织管理", element: <Orgs />, perm: "menu:orgs" }, { path: "/orgs", label: "组织管理", element: <Orgs />, perm: "menu:orgs" },
@ -46,6 +49,8 @@ export const menuRoutes: MenuRoute[] = [
]; ];
export const extraRoutes = [ export const extraRoutes = [
{ path: "/meetings/:id", element: <MeetingDetail />, perm: "menu:meeting" } { path: "/meetings/:id", element: <MeetingDetail />, perm: "menu:meeting" },
{ path: "/meeting-live-create", element: <RealtimeAsr />, perm: "menu:meeting" },
{ path: "/meeting-live-session/:id", element: <RealtimeAsrSession />, perm: "menu:meeting" }
]; ];

View File

@ -4,10 +4,10 @@ import react from "@vitejs/plugin-react";
export default defineConfig({ export default defineConfig({
plugins: [react()], plugins: [react()],
server: { server: {
port: 5173, port: 5174,
proxy: { proxy: {
"/auth": "http://localhost:8080", "/auth": "http://localhost:8081",
"/api": "http://localhost:8080" "/api": "http://localhost:8081"
} }
} }
}); });