jisti-meet服务开启
This commit is contained in:
@@ -121,17 +121,20 @@ CREATE TABLE workcase.tb_video_meeting(
|
||||
workcase_id VARCHAR(50) NOT NULL, -- 关联工单ID
|
||||
meeting_name VARCHAR(200) NOT NULL, -- 会议名称
|
||||
meeting_password VARCHAR(50) DEFAULT NULL, -- 会议密码(可选)
|
||||
description VARCHAR(500) DEFAULT NULL, -- 会议模式
|
||||
jwt_token TEXT DEFAULT NULL, -- JWT Token(用于身份验证)
|
||||
jitsi_room_name VARCHAR(200) NOT NULL, -- Jitsi房间名(格式:workcase_{workcase_id}_{timestamp})
|
||||
jitsi_server_url VARCHAR(500) NOT NULL DEFAULT 'https://meet.jit.si', -- Jitsi服务器地址
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'scheduled', -- 状态:scheduled-已安排 ongoing-进行中 ended-已结束 cancelled-已取消
|
||||
creator_id VARCHAR(50) NOT NULL, -- 创建者ID
|
||||
creator_type VARCHAR(20) NOT NULL, -- 创建者类型:guest-来客 agent-客服
|
||||
creator_name VARCHAR(100) NOT NULL, -- 创建者名称
|
||||
participant_count INTEGER NOT NULL DEFAULT 0, -- 参与人数
|
||||
max_participants INTEGER DEFAULT 10, -- 最大参与人数
|
||||
start_time TIMESTAMPTZ DEFAULT NULL, -- 实际开始时间
|
||||
end_time TIMESTAMPTZ DEFAULT NULL, -- 实际结束时间
|
||||
start_time TIMESTAMPTZ NOT NULL, -- 定义会议开始时间
|
||||
end_time TIMESTAMPTZ NOT NULL, -- 定义会议结束时间
|
||||
advance INTEGER DEFAULT 5, -- 提前入会时间(分钟)
|
||||
actual_start_time TIMESTAMPTZ DEFAULT NULL, -- 真正会议开始时间
|
||||
actual_end_time TIMESTAMPTZ DEFAULT NULL, -- 真正会议结束时间
|
||||
duration_seconds INTEGER DEFAULT 0, -- 会议时长(秒)
|
||||
iframe_url TEXT DEFAULT NULL, -- iframe嵌入URL(生成后存储)
|
||||
config JSONB DEFAULT NULL, -- Jitsi配置项(自定义配置)
|
||||
|
||||
@@ -47,7 +47,8 @@ INSERT INTO sys.tb_sys_module (
|
||||
('MODULE-0008', 'module_agent', '智能体', '智能体管理', 'system', NULL, now(), false),
|
||||
('MODULE-0005', 'module_knowledge', '知识库', '知识文档管理', 'system', NULL, now(), false),
|
||||
('MODULE-0006', 'module_bidding', '招投标', '招投标业务管理', 'system', NULL, now(), false),
|
||||
('MODULE-0007', 'module_workcase', '智能客服', '客服工单管理', 'system', NULL, now(), false);
|
||||
('MODULE-0007', 'module_workcase', '智能客服', '客服工单管理', 'system', NULL, now(), false),
|
||||
('MODULE-0009', 'module_meeting', '视频会议', 'Jitsi Meet视频会议管理', 'system', NULL, now(), false);
|
||||
|
||||
-- =============================
|
||||
-- 4. 初始化系统权限
|
||||
@@ -161,7 +162,14 @@ INSERT INTO sys.tb_sys_permission (
|
||||
('PERM-0722', 'perm_workcase_ticket_update', '工单更新', 'workcase:ticket:update', '更新工单', 'module_workcase', true, 'system', NULL, now(), false),
|
||||
('PERM-0723', 'perm_workcase_ticket_view', '工单查看', 'workcase:ticket:view', '查看工单详情和列表', 'module_workcase', true, 'system', NULL, now(), false),
|
||||
('PERM-0724', 'perm_workcase_ticket_process', '工单处理', 'workcase:ticket:process', '工单处理过程管理', 'module_workcase', true, 'system', NULL, now(), false),
|
||||
('PERM-0725', 'perm_workcase_ticket_device', '工单设备', 'workcase:ticket:device', '工单设备管理', 'module_workcase', true, 'system', NULL, now(), false);
|
||||
('PERM-0725', 'perm_workcase_ticket_device', '工单设备', 'workcase:ticket:device', '工单设备管理', 'module_workcase', true, 'system', NULL, now(), false),
|
||||
|
||||
-- 视频会议模块权限(Jitsi Meet)
|
||||
('PERM-0730', 'perm_meeting_create', '创建会议', 'meeting:create:own', '创建视频会议', 'module_meeting', true, 'system', NULL, now(), false),
|
||||
('PERM-0731', 'perm_meeting_join', '加入会议', 'meeting:join:any', '加入视频会议', 'module_meeting', true, 'system', NULL, now(), false),
|
||||
('PERM-0732', 'perm_meeting_url', '获取会议链接', 'meeting:url:any', '获取会议加入链接', 'module_meeting', true, 'system', NULL, now(), false),
|
||||
('PERM-0733', 'perm_meeting_token', '获取会议令牌', 'meeting:token:any', '获取会议参与令牌', 'module_meeting', true, 'system', NULL, now(), false);
|
||||
|
||||
-- =============================
|
||||
-- 5. 初始化视图(菜单)
|
||||
-- =============================
|
||||
@@ -332,7 +340,12 @@ INSERT INTO sys.tb_sys_role_permission (
|
||||
('RP-U-0013', 'role_user', 'perm_file_upload', 'system', NULL, now(), false),
|
||||
('RP-U-0014', 'role_user', 'perm_file_download', 'system', NULL, now(), false),
|
||||
('RP-U-0015', 'role_user', 'perm_message_view', 'system', NULL, now(), false),
|
||||
('RP-U-0016', 'role_user', 'perm_config_view', 'system', NULL, now(), false);
|
||||
('RP-U-0016', 'role_user', 'perm_config_view', 'system', NULL, now(), false),
|
||||
--- 视频会议权限
|
||||
('RP-U-0050', 'role_user', 'perm_meeting_create', 'system', NULL, now(), false),
|
||||
('RP-U-0051', 'role_user', 'perm_meeting_join', 'system', NULL, now(), false),
|
||||
('RP-U-0052', 'role_user', 'perm_meeting_url', 'system', NULL, now(), false),
|
||||
('RP-U-0053', 'role_user', 'perm_meeting_token', 'system', NULL, now(), false);
|
||||
|
||||
-- 访客权限(基础菜单 + workcase聊天和工单全部接口权限)
|
||||
INSERT INTO sys.tb_sys_role_permission (
|
||||
@@ -366,7 +379,12 @@ INSERT INTO sys.tb_sys_role_permission (
|
||||
('RP-G-0042', 'role_guest', 'perm_workcase_ticket_update', 'system', NULL, now(), false),
|
||||
('RP-G-0043', 'role_guest', 'perm_workcase_ticket_view', 'system', NULL, now(), false),
|
||||
('RP-G-0044', 'role_guest', 'perm_workcase_ticket_process', 'system', NULL, now(), false),
|
||||
('RP-G-0045', 'role_guest', 'perm_workcase_ticket_device', 'system', NULL, now(), false);
|
||||
('RP-G-0045', 'role_guest', 'perm_workcase_ticket_device', 'system', NULL, now(), false),
|
||||
--- 视频会议权限
|
||||
('RP-G-0050', 'role_guest', 'perm_meeting_create', 'system', NULL, now(), false),
|
||||
('RP-G-0051', 'role_guest', 'perm_meeting_join', 'system', NULL, now(), false),
|
||||
('RP-G-0052', 'role_guest', 'perm_meeting_url', 'system', NULL, now(), false),
|
||||
('RP-G-0053', 'role_guest', 'perm_meeting_token', 'system', NULL, now(), false);
|
||||
|
||||
-- =============================
|
||||
-- 7. 视图权限关联
|
||||
|
||||
@@ -74,24 +74,249 @@ services:
|
||||
# 管理员账户配置
|
||||
MINIO_ROOT_USER: minioadmin
|
||||
MINIO_ROOT_PASSWORD: minioadmin123
|
||||
|
||||
|
||||
# Console 地址配置
|
||||
MINIO_CONSOLE_ADDRESS: ":9001"
|
||||
MINIO_ADDRESS: ":9000"
|
||||
|
||||
|
||||
# 时区设置
|
||||
TZ: Asia/Shanghai
|
||||
|
||||
|
||||
volumes:
|
||||
# 数据持久化到主机目录
|
||||
- ../../../.data/docker/minio/data:/data
|
||||
- ../../../.data/docker/minio/config:/root/.minio
|
||||
|
||||
|
||||
command: server /data --console-address ":9001"
|
||||
|
||||
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
|
||||
interval: 30s
|
||||
timeout: 20s
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
|
||||
# ====================== Jitsi Meet 视频会议服务 ======================
|
||||
|
||||
jitsi-web:
|
||||
image: jitsi/web:stable-9584
|
||||
container_name: urban-lifeline-jitsi-web
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- urban-lifeline
|
||||
ports:
|
||||
- "8280:80" # 保留原 HTTP 端口
|
||||
- "8443:443" # 保留原 HTTPS 端口(仅保留映射,实际禁用 HTTPS)
|
||||
environment:
|
||||
# 基础配置(局域网访问)
|
||||
TZ: Asia/Shanghai
|
||||
PUBLIC_URL: http://192.168.0.253:8280
|
||||
|
||||
# 核心修复:解决 WebSocket 协议/URL 错误(局域网 IP)
|
||||
BOSH_URL_BASE: http://jitsi-prosody:5280
|
||||
WEBSOCKET_URL: ws://192.168.0.253:8280/xmpp-websocket
|
||||
|
||||
# XMPP 配置(完全保留原设置)
|
||||
XMPP_DOMAIN: meet.jitsi
|
||||
XMPP_AUTH_DOMAIN: auth.meet.jitsi
|
||||
XMPP_BOSH_URL_BASE: http://jitsi-prosody:5280
|
||||
XMPP_MUC_DOMAIN: muc.meet.jitsi
|
||||
XMPP_INTERNAL_MUC_DOMAIN: internal-muc.meet.jitsi
|
||||
XMPP_GUEST_DOMAIN: guest.meet.jitsi
|
||||
|
||||
# Jicofo 配置(完全保留原设置)
|
||||
JICOFO_COMPONENT_SECRET: jicofo-secret
|
||||
JICOFO_AUTH_USER: focus
|
||||
|
||||
# JVB 配置(完全保留原设置)
|
||||
JVB_AUTH_USER: jvb
|
||||
JVB_AUTH_PASSWORD: jvb-password
|
||||
|
||||
# JWT 认证配置(完全保留原设置)
|
||||
ENABLE_AUTH: 1
|
||||
ENABLE_GUESTS: 1
|
||||
AUTH_TYPE: jwt
|
||||
JWT_APP_ID: urbanLifeline
|
||||
JWT_APP_SECRET: urbanLifelinejitsi
|
||||
JWT_ACCEPTED_ISSUERS: urbanLifeline
|
||||
JWT_ACCEPTED_AUDIENCES: jitsi
|
||||
JWT_ASAP_KEYSERVER: https://192.168.0.253:8280/
|
||||
JWT_ALLOW_EMPTY: 0
|
||||
JWT_AUTH_TYPE: token
|
||||
JWT_TOKEN_AUTH_MODULE: token_verification
|
||||
|
||||
# 界面/功能配置(完全保留原设置)
|
||||
ENABLE_RECORDING: 0
|
||||
ENABLE_TRANSCRIPTIONS: 0
|
||||
ENABLE_SUBDOMAINS: 0
|
||||
ENABLE_XMPP_WEBSOCKET: 1
|
||||
ENABLE_SCTP: 1
|
||||
DISABLE_HTTPS: 1
|
||||
|
||||
# 日志/HTTPS 配置
|
||||
ENABLE_LETSENCRYPT: 0
|
||||
LETSENCRYPT_DOMAIN: 192.168.0.253
|
||||
|
||||
volumes:
|
||||
- ../../../.data/docker/jitsi/web:/config
|
||||
- ../../../.data/docker/jitsi/web/crontabs:/var/spool/cron/crontabs
|
||||
- ../../../.data/docker/jitsi/transcripts:/usr/share/jitsi-meet/transcripts
|
||||
depends_on:
|
||||
- jitsi-prosody
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:80/"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 60s
|
||||
|
||||
# XMPP 服务(Prosody)- 完全保留原配置
|
||||
jitsi-prosody:
|
||||
image: jitsi/prosody:stable-9584
|
||||
container_name: urban-lifeline-jitsi-prosody
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- urban-lifeline
|
||||
expose:
|
||||
- "5222" # XMPP客户端连接(内部)
|
||||
- "5347" # XMPP组件连接(内部)
|
||||
- "5280" # BOSH/WebSocket(内部)
|
||||
environment:
|
||||
TZ: Asia/Shanghai
|
||||
|
||||
# XMPP域配置(完全保留)
|
||||
XMPP_DOMAIN: meet.jitsi
|
||||
XMPP_AUTH_DOMAIN: auth.meet.jitsi
|
||||
XMPP_MUC_DOMAIN: muc.meet.jitsi
|
||||
XMPP_INTERNAL_MUC_DOMAIN: internal-muc.meet.jitsi
|
||||
XMPP_GUEST_DOMAIN: guest.meet.jitsi
|
||||
|
||||
# Jicofo组件密钥(完全保留)
|
||||
JICOFO_COMPONENT_SECRET: jicofo-secret
|
||||
JICOFO_AUTH_USER: focus
|
||||
JICOFO_AUTH_PASSWORD: focus-password
|
||||
|
||||
# JVB认证(完全保留)
|
||||
JVB_AUTH_USER: jvb
|
||||
JVB_AUTH_PASSWORD: jvb-password
|
||||
|
||||
# JWT认证(完全保留)
|
||||
ENABLE_AUTH: 1
|
||||
ENABLE_GUESTS: 1
|
||||
AUTH_TYPE: jwt
|
||||
JWT_APP_ID: urbanLifeline
|
||||
JWT_APP_SECRET: urbanLifelinejitsi
|
||||
JWT_ACCEPTED_ISSUERS: urbanLifeline
|
||||
JWT_ACCEPTED_AUDIENCES: jitsi
|
||||
JWT_ALLOW_EMPTY: 0
|
||||
JWT_AUTH_TYPE: token
|
||||
JWT_TOKEN_AUTH_MODULE: token_verification
|
||||
|
||||
# 日志配置(完全保留)
|
||||
LOG_LEVEL: info
|
||||
|
||||
# 公共URL(局域网访问)
|
||||
PUBLIC_URL: http://192.168.0.253:8280
|
||||
|
||||
volumes:
|
||||
- ../../../.data/docker/jitsi/prosody/config:/config
|
||||
- ../../../.data/docker/jitsi/prosody/prosody-plugins-custom:/prosody-plugins-custom
|
||||
healthcheck:
|
||||
test: ["CMD", "prosodyctl", "status"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 90s
|
||||
|
||||
# 会议焦点控制器(Jicofo)- 完全保留原配置
|
||||
jitsi-jicofo:
|
||||
image: jitsi/jicofo:stable-9584
|
||||
container_name: urban-lifeline-jitsi-jicofo
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- urban-lifeline
|
||||
environment:
|
||||
TZ: Asia/Shanghai
|
||||
|
||||
# XMPP配置(完全保留)
|
||||
XMPP_DOMAIN: meet.jitsi
|
||||
XMPP_AUTH_DOMAIN: auth.meet.jitsi
|
||||
XMPP_MUC_DOMAIN: muc.meet.jitsi
|
||||
XMPP_INTERNAL_MUC_DOMAIN: internal-muc.meet.jitsi
|
||||
XMPP_SERVER: jitsi-prosody
|
||||
|
||||
# Jicofo认证(完全保留)
|
||||
JICOFO_COMPONENT_SECRET: jicofo-secret
|
||||
JICOFO_AUTH_USER: focus
|
||||
JICOFO_AUTH_PASSWORD: focus-password
|
||||
|
||||
# JWT配置(完全保留)
|
||||
AUTH_TYPE: jwt
|
||||
|
||||
# JVB配置(完全保留)
|
||||
JVB_BREWERY_MUC: jvbbrewery
|
||||
|
||||
# 日志级别(完全保留)
|
||||
JICOFO_ENABLE_HEALTH_CHECKS: "true"
|
||||
|
||||
volumes:
|
||||
- ../../../.data/docker/jitsi/jicofo:/config
|
||||
depends_on:
|
||||
- jitsi-prosody
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8888/about/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 90s
|
||||
|
||||
# 视频桥接服务(JVB)- 仅修复 WebSocket 相关,保留IP/端口
|
||||
jitsi-jvb:
|
||||
image: jitsi/jvb:stable-9584
|
||||
container_name: urban-lifeline-jitsi-jvb
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- urban-lifeline
|
||||
ports:
|
||||
- "10000:10000/udp" # 保留原 UDP 端口
|
||||
- "4443:4443/tcp" # 保留原 TCP 端口
|
||||
environment:
|
||||
TZ: Asia/Shanghai
|
||||
|
||||
# XMPP配置(完全保留)
|
||||
XMPP_DOMAIN: meet.jitsi
|
||||
XMPP_AUTH_DOMAIN: auth.meet.jitsi
|
||||
XMPP_INTERNAL_MUC_DOMAIN: internal-muc.meet.jitsi
|
||||
XMPP_SERVER: jitsi-prosody
|
||||
|
||||
# JVB认证(完全保留)
|
||||
JVB_AUTH_USER: jvb
|
||||
JVB_AUTH_PASSWORD: jvb-password
|
||||
|
||||
# JVB配置(完全保留)
|
||||
JVB_BREWERY_MUC: jvbbrewery
|
||||
JVB_PORT: 10000
|
||||
JVB_STUN_SERVERS: stun.l.google.com:19302,stun1.l.google.com:19302
|
||||
|
||||
# 本地IP配置(局域网IP - 关键配置!)
|
||||
DOCKER_HOST_ADDRESS: 192.168.0.253
|
||||
JVB_ADVERTISE_IPS: 192.168.0.253
|
||||
|
||||
# 启用统计(完全保留)
|
||||
JVB_ENABLE_APIS: rest,colibri
|
||||
|
||||
# 性能优化(完全保留)
|
||||
JVB_TCP_HARVESTER_DISABLED: "false"
|
||||
JVB_TCP_PORT: 4443
|
||||
JVB_TCP_MAPPED_PORT: 4443
|
||||
|
||||
volumes:
|
||||
- ../../../.data/docker/jitsi/jvb:/config
|
||||
depends_on:
|
||||
- jitsi-prosody
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8080/about/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 90s
|
||||
|
||||
@@ -34,6 +34,9 @@ public class TbVideoMeetingDTO extends BaseDTO {
|
||||
@Schema(description = "会议密码")
|
||||
private String meetingPassword;
|
||||
|
||||
@Schema(description = "会议模式")
|
||||
private String description;
|
||||
|
||||
@Schema(description = "JWT Token")
|
||||
private String jwtToken;
|
||||
|
||||
@@ -46,9 +49,6 @@ public class TbVideoMeetingDTO extends BaseDTO {
|
||||
@Schema(description = "状态:scheduled-已安排 ongoing-进行中 ended-已结束 cancelled-已取消")
|
||||
private String status;
|
||||
|
||||
@Schema(description = "创建者ID")
|
||||
private String creatorId;
|
||||
|
||||
@Schema(description = "创建者类型:guest-来客 agent-客服")
|
||||
private String creatorType;
|
||||
|
||||
@@ -61,6 +61,15 @@ public class TbVideoMeetingDTO extends BaseDTO {
|
||||
@Schema(description = "最大参与人数")
|
||||
private Integer maxParticipants;
|
||||
|
||||
@Schema(description = "定义会议开始时间")
|
||||
private Date startTime;
|
||||
|
||||
@Schema(description = "定义会议结束时间")
|
||||
private Date endTime;
|
||||
|
||||
@Schema(description = "提前入会时间(分钟)")
|
||||
private Integer advance;
|
||||
|
||||
@Schema(description = "实际开始时间")
|
||||
private Date actualStartTime;
|
||||
|
||||
|
||||
@@ -1,186 +0,0 @@
|
||||
package org.xyzh.api.workcase.service;
|
||||
|
||||
import org.xyzh.api.workcase.dto.TbVideoMeetingDTO;
|
||||
import org.xyzh.api.workcase.dto.TbMeetingParticipantDTO;
|
||||
import org.xyzh.api.workcase.dto.TbMeetingTranscriptionDTO;
|
||||
import org.xyzh.api.workcase.vo.VideoMeetingVO;
|
||||
import org.xyzh.api.workcase.vo.MeetingParticipantVO;
|
||||
import org.xyzh.api.workcase.vo.MeetingTranscriptionVO;
|
||||
import org.xyzh.common.core.domain.ResultDomain;
|
||||
import org.xyzh.common.core.page.PageRequest;
|
||||
|
||||
/**
|
||||
* @description 视频会议服务接口,管理Jitsi Meet会议、参与者和转录
|
||||
* @filename MeetService.java
|
||||
* @author cascade
|
||||
* @copyright xyzh
|
||||
* @since 2025-12-22
|
||||
*/
|
||||
public interface MeetService {
|
||||
|
||||
// ========================= 会议管理 ==========================
|
||||
|
||||
/**
|
||||
* @description 创建视频会议
|
||||
* @param meeting 会议信息
|
||||
* @author cascade
|
||||
* @since 2025-12-22
|
||||
*/
|
||||
ResultDomain<TbVideoMeetingDTO> createMeeting(TbVideoMeetingDTO meeting);
|
||||
|
||||
/**
|
||||
* @description 更新会议信息
|
||||
* @param meeting 会议信息
|
||||
* @author cascade
|
||||
* @since 2025-12-22
|
||||
*/
|
||||
ResultDomain<TbVideoMeetingDTO> updateMeeting(TbVideoMeetingDTO meeting);
|
||||
|
||||
/**
|
||||
* @description 开始会议
|
||||
* @param meetingId 会议ID
|
||||
* @author cascade
|
||||
* @since 2025-12-22
|
||||
*/
|
||||
ResultDomain<TbVideoMeetingDTO> startMeeting(String meetingId);
|
||||
|
||||
/**
|
||||
* @description 结束会议
|
||||
* @param meetingId 会议ID
|
||||
* @author cascade
|
||||
* @since 2025-12-22
|
||||
*/
|
||||
ResultDomain<TbVideoMeetingDTO> endMeeting(String meetingId);
|
||||
|
||||
/**
|
||||
* @description 删除会议
|
||||
* @param meetingId 会议ID
|
||||
* @author cascade
|
||||
* @since 2025-12-22
|
||||
*/
|
||||
ResultDomain<Boolean> deleteMeeting(String meetingId);
|
||||
|
||||
/**
|
||||
* @description 根据ID获取会议
|
||||
* @param meetingId 会议ID
|
||||
* @author cascade
|
||||
* @since 2025-12-22
|
||||
*/
|
||||
ResultDomain<TbVideoMeetingDTO> getMeetingById(String meetingId);
|
||||
|
||||
/**
|
||||
* @description 获取会议列表/分页
|
||||
* @param pageRequest 分页请求
|
||||
* @author cascade
|
||||
* @since 2025-12-22
|
||||
*/
|
||||
ResultDomain<VideoMeetingVO> getMeetingPage(PageRequest<TbVideoMeetingDTO> pageRequest);
|
||||
|
||||
/**
|
||||
* @description 生成会议加入链接/iframe URL
|
||||
* @param meetingId 会议ID
|
||||
* @param userId 用户ID
|
||||
* @param userName 用户名称
|
||||
* @author cascade
|
||||
* @since 2025-12-22
|
||||
*/
|
||||
ResultDomain<String> generateMeetingJoinUrl(String meetingId, String userId, String userName);
|
||||
|
||||
/**
|
||||
* @description 生成会议JWT Token
|
||||
* @param meetingId 会议ID
|
||||
* @param userId 用户ID
|
||||
* @param isModerator 是否主持人
|
||||
* @author cascade
|
||||
* @since 2025-12-22
|
||||
*/
|
||||
ResultDomain<String> generateMeetingToken(String meetingId, String userId, boolean isModerator);
|
||||
|
||||
// ========================= 参与者管理 ==========================
|
||||
|
||||
/**
|
||||
* @description 参与者加入会议
|
||||
* @param participant 参与者信息
|
||||
* @author cascade
|
||||
* @since 2025-12-22
|
||||
*/
|
||||
ResultDomain<TbMeetingParticipantDTO> joinMeeting(TbMeetingParticipantDTO participant);
|
||||
|
||||
/**
|
||||
* @description 参与者离开会议
|
||||
* @param participantId 参与者ID
|
||||
* @author cascade
|
||||
* @since 2025-12-22
|
||||
*/
|
||||
ResultDomain<Boolean> leaveMeeting(String participantId);
|
||||
|
||||
/**
|
||||
* @description 获取会议参与者列表
|
||||
* @param meetingId 会议ID
|
||||
* @author cascade
|
||||
* @since 2025-12-22
|
||||
*/
|
||||
ResultDomain<MeetingParticipantVO> getMeetingParticipantList(String meetingId);
|
||||
|
||||
/**
|
||||
* @description 更新参与者信息
|
||||
* @param participant 参与者信息
|
||||
* @author cascade
|
||||
* @since 2025-12-22
|
||||
*/
|
||||
ResultDomain<TbMeetingParticipantDTO> updateParticipant(TbMeetingParticipantDTO participant);
|
||||
|
||||
/**
|
||||
* @description 设置参与者为主持人
|
||||
* @param participantId 参与者ID
|
||||
* @param isModerator 是否主持人
|
||||
* @author cascade
|
||||
* @since 2025-12-22
|
||||
*/
|
||||
ResultDomain<Boolean> setModerator(String participantId, boolean isModerator);
|
||||
|
||||
// ========================= 转录管理 ==========================
|
||||
|
||||
/**
|
||||
* @description 添加转录记录
|
||||
* @param transcription 转录内容
|
||||
* @author cascade
|
||||
* @since 2025-12-22
|
||||
*/
|
||||
ResultDomain<TbMeetingTranscriptionDTO> addTranscription(TbMeetingTranscriptionDTO transcription);
|
||||
|
||||
/**
|
||||
* @description 获取会议转录列表/分页
|
||||
* @param pageRequest 分页请求
|
||||
* @author cascade
|
||||
* @since 2025-12-22
|
||||
*/
|
||||
ResultDomain<MeetingTranscriptionVO> getTranscriptionPage(PageRequest<TbMeetingTranscriptionDTO> pageRequest);
|
||||
|
||||
/**
|
||||
* @description 获取会议完整转录文本
|
||||
* @param meetingId 会议ID
|
||||
* @author cascade
|
||||
* @since 2025-12-22
|
||||
*/
|
||||
ResultDomain<String> getFullTranscriptionText(String meetingId);
|
||||
|
||||
/**
|
||||
* @description 删除转录记录
|
||||
* @param transcriptionId 转录ID
|
||||
* @author cascade
|
||||
* @since 2025-12-22
|
||||
*/
|
||||
ResultDomain<Boolean> deleteTranscription(String transcriptionId);
|
||||
|
||||
// ========================= 会议统计 ==========================
|
||||
|
||||
/**
|
||||
* @description 获取会议统计信息(参与人数、时长等)
|
||||
* @param meetingId 会议ID
|
||||
* @author cascade
|
||||
* @since 2025-12-22
|
||||
*/
|
||||
ResultDomain<VideoMeetingVO> getMeetingStatistics(String meetingId);
|
||||
|
||||
}
|
||||
@@ -33,6 +33,10 @@ public class VideoMeetingVO extends BaseVO {
|
||||
@Schema(description = "会议密码")
|
||||
private String meetingPassword;
|
||||
|
||||
@Schema(description = "会议模式")
|
||||
private String description;
|
||||
|
||||
|
||||
@Schema(description = "JWT Token(用于身份验证)")
|
||||
private String jwtToken;
|
||||
|
||||
@@ -45,9 +49,6 @@ public class VideoMeetingVO extends BaseVO {
|
||||
@Schema(description = "状态:scheduled-已安排 ongoing-进行中 ended-已结束 cancelled-已取消")
|
||||
private String status;
|
||||
|
||||
@Schema(description = "创建者ID")
|
||||
private String creatorId;
|
||||
|
||||
@Schema(description = "创建者类型:guest-来客 agent-客服")
|
||||
private String creatorType;
|
||||
|
||||
@@ -59,6 +60,17 @@ public class VideoMeetingVO extends BaseVO {
|
||||
|
||||
@Schema(description = "最大参与人数")
|
||||
private Integer maxParticipants;
|
||||
|
||||
@Schema(description = "定义会议开始时间", format = "date-time")
|
||||
@JSONField(format = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date startTime;
|
||||
|
||||
@Schema(description = "定义会议结束时间", format = "date-time")
|
||||
@JSONField(format = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date endTime;
|
||||
|
||||
@Schema(description = "提前入会时间(分钟)")
|
||||
private Integer advance;
|
||||
|
||||
@Schema(description = "实际开始时间", format = "date-time")
|
||||
@JSONField(format = "yyyy-MM-dd HH:mm:ss")
|
||||
|
||||
@@ -33,6 +33,11 @@
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-security</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
|
||||
@@ -2,8 +2,11 @@ package org.xyzh.common.exception.handler;
|
||||
|
||||
import jakarta.validation.ConstraintViolation;
|
||||
import jakarta.validation.ConstraintViolationException;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.security.authorization.AuthorizationDeniedException;
|
||||
import org.springframework.validation.BindException;
|
||||
import org.springframework.validation.FieldError;
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
@@ -26,17 +29,16 @@ import java.util.stream.Collectors;
|
||||
* @copyright yslg
|
||||
* @since 2025-12-17
|
||||
*/
|
||||
@Slf4j
|
||||
@RestControllerAdvice
|
||||
public class GlobalExceptionHandler {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
|
||||
/**
|
||||
* 业务异常
|
||||
*/
|
||||
@ExceptionHandler(BusinessException.class)
|
||||
@ResponseStatus(HttpStatus.OK)
|
||||
public ResultDomain<?> handleBusinessException(BusinessException e) {
|
||||
log.warn("业务异常: {}", e.getMessage());
|
||||
logger.warn("业务异常: {}", e.getMessage());
|
||||
return ResultDomain.failure(e.getCode(), e.getMessage());
|
||||
}
|
||||
|
||||
@@ -49,7 +51,7 @@ public class GlobalExceptionHandler {
|
||||
String message = e.getBindingResult().getFieldErrors().stream()
|
||||
.map(FieldError::getDefaultMessage)
|
||||
.collect(Collectors.joining("; "));
|
||||
log.warn("参数校验失败: {}", message);
|
||||
logger.warn("参数校验失败: {}", message);
|
||||
return ResultDomain.failure(HttpStatus.BAD_REQUEST.value(), message);
|
||||
}
|
||||
|
||||
@@ -63,7 +65,7 @@ public class GlobalExceptionHandler {
|
||||
String message = violations.stream()
|
||||
.map(ConstraintViolation::getMessage)
|
||||
.collect(Collectors.joining("; "));
|
||||
log.warn("参数校验失败: {}", message);
|
||||
logger.warn("参数校验失败: {}", message);
|
||||
return ResultDomain.failure(HttpStatus.BAD_REQUEST.value(), message);
|
||||
}
|
||||
|
||||
@@ -76,7 +78,7 @@ public class GlobalExceptionHandler {
|
||||
String message = e.getFieldErrors().stream()
|
||||
.map(FieldError::getDefaultMessage)
|
||||
.collect(Collectors.joining("; "));
|
||||
log.warn("参数绑定失败: {}", message);
|
||||
logger.warn("参数绑定失败: {}", message);
|
||||
return ResultDomain.failure(HttpStatus.BAD_REQUEST.value(), message);
|
||||
}
|
||||
|
||||
@@ -87,7 +89,7 @@ public class GlobalExceptionHandler {
|
||||
@ResponseStatus(HttpStatus.OK)
|
||||
public ResultDomain<?> handleMissingServletRequestParameterException(MissingServletRequestParameterException e) {
|
||||
String message = "缺少必要参数: " + e.getParameterName();
|
||||
log.warn(message);
|
||||
logger.warn(message);
|
||||
return ResultDomain.failure(HttpStatus.BAD_REQUEST.value(), message);
|
||||
}
|
||||
|
||||
@@ -98,7 +100,7 @@ public class GlobalExceptionHandler {
|
||||
@ResponseStatus(HttpStatus.OK)
|
||||
public ResultDomain<?> handleMissingServletRequestPartException(MissingServletRequestPartException e) {
|
||||
String message = "缺少必要参数: " + e.getRequestPartName();
|
||||
log.warn(message);
|
||||
logger.warn(message);
|
||||
return ResultDomain.failure(HttpStatus.BAD_REQUEST.value(), message);
|
||||
}
|
||||
|
||||
@@ -108,17 +110,27 @@ public class GlobalExceptionHandler {
|
||||
@ExceptionHandler(MaxUploadSizeExceededException.class)
|
||||
@ResponseStatus(HttpStatus.OK)
|
||||
public ResultDomain<?> handleMaxUploadSizeExceededException(MaxUploadSizeExceededException e) {
|
||||
log.warn("文件上传大小超限: {}", e.getMessage());
|
||||
logger.warn("文件上传大小超限: {}", e.getMessage());
|
||||
return ResultDomain.failure(HttpStatus.BAD_REQUEST.value(), "上传文件大小超过限制");
|
||||
}
|
||||
|
||||
/**
|
||||
* 权限不足异常
|
||||
*/
|
||||
@ExceptionHandler(AuthorizationDeniedException.class)
|
||||
@ResponseStatus(HttpStatus.OK)
|
||||
public ResultDomain<?> handleAuthorizationDeniedException(AuthorizationDeniedException e) {
|
||||
logger.warn("权限不足: {}", e.getMessage());
|
||||
return ResultDomain.failure(HttpStatus.FORBIDDEN.value(), "权限不足");
|
||||
}
|
||||
|
||||
/**
|
||||
* 其他未捕获异常
|
||||
*/
|
||||
@ExceptionHandler(Exception.class)
|
||||
@ResponseStatus(HttpStatus.OK)
|
||||
public ResultDomain<?> handleException(Exception e) {
|
||||
log.error("系统异常: ", e);
|
||||
logger.error("系统异常: ", e);
|
||||
return ResultDomain.failure(HttpStatus.INTERNAL_SERVER_ERROR.value(), "系统异常,请联系管理员");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.xyzh.common.utils.validation;
|
||||
|
||||
import org.xyzh.common.utils.validation.method.FieldCompareValidateMethod;
|
||||
import org.xyzh.common.utils.validation.method.InSetValidateMethod;
|
||||
import org.xyzh.common.utils.validation.method.MinFieldsValidateMethod;
|
||||
import org.xyzh.common.utils.validation.method.ObjectValidateMethod;
|
||||
@@ -10,6 +11,7 @@ import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
@@ -380,4 +382,32 @@ public class ValidationUtils {
|
||||
.validateMethod(new InSetValidateMethod(fieldLabel, allowedValues))
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 创建字段比较校验参数(比较对象中的两个字段)
|
||||
* @param field1Name 第一个字段名称
|
||||
* @param field2Name 第二个字段名称
|
||||
* @param fieldLabel 字段标签
|
||||
* @param compareFunction 比较函数:(field1Value, field2Value) -> Boolean,返回true表示通过
|
||||
* @param errorMessage 自定义错误消息
|
||||
* @return ValidationParam
|
||||
*/
|
||||
public static ValidationParam fieldCompare(String field1Name,
|
||||
String field2Name,
|
||||
String fieldLabel,
|
||||
BiFunction<Object, Object, Boolean> compareFunction,
|
||||
String errorMessage) {
|
||||
return ValidationParam.builder()
|
||||
.fieldName(field1Name)
|
||||
.fieldLabel(fieldLabel)
|
||||
.required(false)
|
||||
.validateMethod(new FieldCompareValidateMethod(
|
||||
field1Name,
|
||||
field2Name,
|
||||
fieldLabel,
|
||||
compareFunction,
|
||||
errorMessage
|
||||
))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
package org.xyzh.common.utils.validation.method;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.function.BiFunction;
|
||||
|
||||
/**
|
||||
* @description 字段比较校验方法,用于比较对象中的两个字段
|
||||
* @filename FieldCompareValidateMethod.java
|
||||
* @author Claude Code
|
||||
* @copyright xyzh
|
||||
* @since 2025-12-26
|
||||
*/
|
||||
public class FieldCompareValidateMethod implements ObjectValidateMethod {
|
||||
|
||||
/**
|
||||
* 第一个字段名称
|
||||
*/
|
||||
private final String field1Name;
|
||||
|
||||
/**
|
||||
* 第二个字段名称
|
||||
*/
|
||||
private final String field2Name;
|
||||
|
||||
/**
|
||||
* 字段标签(用于错误消息)
|
||||
*/
|
||||
private final String fieldLabel;
|
||||
|
||||
/**
|
||||
* 比较函数:(field1Value, field2Value) -> Boolean
|
||||
* 返回 true 表示校验通过,false 表示校验失败
|
||||
*/
|
||||
private final BiFunction<Object, Object, Boolean> compareFunction;
|
||||
|
||||
/**
|
||||
* 自定义错误消息
|
||||
*/
|
||||
private final String customErrorMessage;
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*
|
||||
* @param field1Name 第一个字段名称
|
||||
* @param field2Name 第二个字段名称
|
||||
* @param fieldLabel 字段标签
|
||||
* @param compareFunction 比较函数
|
||||
* @param customErrorMessage 自定义错误消息
|
||||
*/
|
||||
public FieldCompareValidateMethod(String field1Name,
|
||||
String field2Name,
|
||||
String fieldLabel,
|
||||
BiFunction<Object, Object, Boolean> compareFunction,
|
||||
String customErrorMessage) {
|
||||
this.field1Name = field1Name;
|
||||
this.field2Name = field2Name;
|
||||
this.fieldLabel = fieldLabel;
|
||||
this.compareFunction = compareFunction;
|
||||
this.customErrorMessage = customErrorMessage;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean validate(Object targetObject) {
|
||||
if (targetObject == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
// 获取第一个字段的值
|
||||
Object field1Value = getFieldValue(targetObject, field1Name);
|
||||
|
||||
// 获取第二个字段的值
|
||||
Object field2Value = getFieldValue(targetObject, field2Name);
|
||||
|
||||
// 如果任意字段为null,跳过校验
|
||||
if (field1Value == null || field2Value == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 执行比较函数
|
||||
return compareFunction.apply(field1Value, field2Value);
|
||||
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("字段比较校验失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getErrorMessage() {
|
||||
if (customErrorMessage != null && !customErrorMessage.trim().isEmpty()) {
|
||||
return customErrorMessage;
|
||||
}
|
||||
return fieldLabel + "校验失败";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "字段比较校验";
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取字段值(支持嵌套字段)
|
||||
*
|
||||
* @param obj 对象
|
||||
* @param fieldName 字段名称(支持 "field" 或 "nested.field")
|
||||
* @return 字段值
|
||||
*/
|
||||
private Object getFieldValue(Object obj, String fieldName) throws Exception {
|
||||
if (fieldName == null || fieldName.trim().isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 支持嵌套字段访问(如 "user.name")
|
||||
String[] fieldParts = fieldName.split("\\.");
|
||||
Object currentObj = obj;
|
||||
|
||||
for (String part : fieldParts) {
|
||||
if (currentObj == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 获取字段
|
||||
Field field = findField(currentObj.getClass(), part);
|
||||
if (field == null) {
|
||||
throw new NoSuchFieldException("字段不存在: " + part);
|
||||
}
|
||||
|
||||
// 设置可访问
|
||||
field.setAccessible(true);
|
||||
|
||||
// 获取字段值
|
||||
currentObj = field.get(currentObj);
|
||||
}
|
||||
|
||||
return currentObj;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找字段(包括父类)
|
||||
*
|
||||
* @param clazz 类
|
||||
* @param fieldName 字段名称
|
||||
* @return 字段
|
||||
*/
|
||||
private Field findField(Class<?> clazz, String fieldName) {
|
||||
Class<?> currentClass = clazz;
|
||||
while (currentClass != null) {
|
||||
try {
|
||||
return currentClass.getDeclaredField(fieldName);
|
||||
} catch (NoSuchFieldException e) {
|
||||
currentClass = currentClass.getSuperclass();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import org.xyzh.api.workcase.dto.TbChatRoomDTO;
|
||||
import org.xyzh.api.workcase.dto.TbChatRoomMemberDTO;
|
||||
import org.xyzh.api.workcase.dto.TbChatRoomMessageDTO;
|
||||
import org.xyzh.api.workcase.dto.TbCustomerServiceDTO;
|
||||
import org.xyzh.api.workcase.dto.TbVideoMeetingDTO;
|
||||
import org.xyzh.api.workcase.dto.TbWordCloudDTO;
|
||||
import org.xyzh.api.workcase.service.ChatRoomService;
|
||||
import org.xyzh.api.workcase.service.WorkcaseChatService;
|
||||
@@ -24,16 +25,20 @@ import org.xyzh.api.workcase.vo.ChatMemberVO;
|
||||
import org.xyzh.api.workcase.vo.ChatRoomMessageVO;
|
||||
import org.xyzh.api.workcase.vo.ChatRoomVO;
|
||||
import org.xyzh.api.workcase.vo.CustomerServiceVO;
|
||||
import org.xyzh.api.workcase.vo.VideoMeetingVO;
|
||||
import org.xyzh.common.auth.utils.JwtTokenUtil;
|
||||
import org.xyzh.common.auth.utils.LoginUtil;
|
||||
import org.xyzh.common.core.domain.ResultDomain;
|
||||
import org.xyzh.common.core.page.PageRequest;
|
||||
import org.xyzh.common.utils.validation.ValidationParam;
|
||||
import org.xyzh.common.utils.validation.ValidationResult;
|
||||
import org.xyzh.common.utils.validation.ValidationUtils;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
/**
|
||||
@@ -274,12 +279,38 @@ public class WorkcaseChatContorller {
|
||||
private org.xyzh.api.workcase.service.VideoMeetingService videoMeetingService;
|
||||
|
||||
@Operation(summary = "创建视频会议")
|
||||
@PreAuthorize("hasAuthority('workcase:room:meeting')")
|
||||
@PreAuthorize("hasAuthority('meeting:create:own')")
|
||||
@PostMapping("/meeting/create")
|
||||
public ResultDomain<org.xyzh.api.workcase.vo.VideoMeetingVO> createVideoMeeting(
|
||||
@RequestBody org.xyzh.api.workcase.dto.TbVideoMeetingDTO meetingDTO) {
|
||||
public ResultDomain<VideoMeetingVO> createVideoMeeting(
|
||||
@RequestBody TbVideoMeetingDTO meetingDTO) {
|
||||
ValidationResult vr = ValidationUtils.validate(meetingDTO, Arrays.asList(
|
||||
ValidationUtils.requiredString("roomId", "聊天室ID")
|
||||
ValidationUtils.requiredString("roomId", "聊天室ID"),
|
||||
ValidationUtils.requiredString("meetingName", "会议名称"),
|
||||
// 校验开始时间不为空
|
||||
ValidationParam.builder()
|
||||
.fieldName("startTime")
|
||||
.fieldLabel("会议开始时间")
|
||||
.required()
|
||||
.build(),
|
||||
// 校验结束时间不为空
|
||||
ValidationParam.builder()
|
||||
.fieldName("endTime")
|
||||
.fieldLabel("会议结束时间")
|
||||
.required()
|
||||
.build(),
|
||||
// 校验开始时间小于结束时间(使用 fieldCompare 比较两个字段)
|
||||
ValidationUtils.fieldCompare(
|
||||
"startTime",
|
||||
"endTime",
|
||||
"会议时间",
|
||||
(startTime, endTime) -> {
|
||||
if (startTime instanceof Date && endTime instanceof Date) {
|
||||
return ((Date) startTime).before((Date) endTime);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
"会议开始时间不能晚于结束时间"
|
||||
)
|
||||
));
|
||||
if (!vr.isValid()) {
|
||||
return ResultDomain.failure(vr.getAllErrors());
|
||||
@@ -293,9 +324,9 @@ public class WorkcaseChatContorller {
|
||||
}
|
||||
|
||||
@Operation(summary = "获取会议信息")
|
||||
@PreAuthorize("hasAuthority('workcase:room:meeting')")
|
||||
@PreAuthorize("hasAuthority('meeting:url:any')")
|
||||
@GetMapping("/meeting/{meetingId}")
|
||||
public ResultDomain<org.xyzh.api.workcase.vo.VideoMeetingVO> getMeetingInfo(
|
||||
public ResultDomain<VideoMeetingVO> getMeetingInfo(
|
||||
@PathVariable(value = "meetingId") String meetingId) {
|
||||
String userId = LoginUtil.getCurrentUserId();
|
||||
|
||||
@@ -307,9 +338,9 @@ public class WorkcaseChatContorller {
|
||||
}
|
||||
|
||||
@Operation(summary = "加入会议(生成用户专属JWT)")
|
||||
@PreAuthorize("hasAuthority('workcase:room:meeting')")
|
||||
@PreAuthorize("hasAuthority('meeting:join:any')")
|
||||
@PostMapping("/meeting/{meetingId}/join")
|
||||
public ResultDomain<org.xyzh.api.workcase.vo.VideoMeetingVO> joinMeeting(
|
||||
public ResultDomain<VideoMeetingVO> joinMeeting(
|
||||
@PathVariable(value = "meetingId") String meetingId) {
|
||||
String userId = LoginUtil.getCurrentUserId();
|
||||
|
||||
@@ -328,7 +359,7 @@ public class WorkcaseChatContorller {
|
||||
}
|
||||
|
||||
@Operation(summary = "开始会议")
|
||||
@PreAuthorize("hasAuthority('workcase:room:meeting')")
|
||||
@PreAuthorize("hasAuthority('meeting:create:own')")
|
||||
@PostMapping("/meeting/{meetingId}/start")
|
||||
public ResultDomain<Boolean> startMeeting(@PathVariable(value = "meetingId") String meetingId) {
|
||||
try {
|
||||
@@ -339,9 +370,9 @@ public class WorkcaseChatContorller {
|
||||
}
|
||||
|
||||
@Operation(summary = "结束会议")
|
||||
@PreAuthorize("hasAuthority('workcase:room:meeting')")
|
||||
@PreAuthorize("hasAuthority('meeting:create:own')")
|
||||
@PostMapping("/meeting/{meetingId}/end")
|
||||
public ResultDomain<org.xyzh.api.workcase.vo.VideoMeetingVO> endMeeting(
|
||||
public ResultDomain<VideoMeetingVO> endMeeting(
|
||||
@PathVariable(value = "meetingId") String meetingId) {
|
||||
try {
|
||||
return videoMeetingService.endMeeting(meetingId);
|
||||
@@ -351,9 +382,9 @@ public class WorkcaseChatContorller {
|
||||
}
|
||||
|
||||
@Operation(summary = "获取聊天室当前活跃会议")
|
||||
@PreAuthorize("hasAuthority('workcase:room:meeting')")
|
||||
@PreAuthorize("hasAuthority('meeting:url:any')")
|
||||
@GetMapping("/meeting/room/{roomId}/active")
|
||||
public ResultDomain<org.xyzh.api.workcase.vo.VideoMeetingVO> getActiveMeetingByRoom(
|
||||
public ResultDomain<VideoMeetingVO> getActiveMeetingByRoom(
|
||||
@PathVariable(value = "roomId") String roomId) {
|
||||
try {
|
||||
return videoMeetingService.getActiveMeetingByRoom(roomId);
|
||||
|
||||
@@ -1,450 +0,0 @@
|
||||
package org.xyzh.workcase.service;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.dubbo.config.annotation.DubboService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.xyzh.api.workcase.dto.TbVideoMeetingDTO;
|
||||
import org.xyzh.api.workcase.dto.TbMeetingParticipantDTO;
|
||||
import org.xyzh.api.workcase.dto.TbMeetingTranscriptionDTO;
|
||||
import org.xyzh.api.workcase.service.MeetService;
|
||||
import org.xyzh.api.workcase.vo.VideoMeetingVO;
|
||||
import org.xyzh.api.workcase.vo.MeetingParticipantVO;
|
||||
import org.xyzh.api.workcase.vo.MeetingTranscriptionVO;
|
||||
import org.xyzh.common.core.domain.ResultDomain;
|
||||
import org.xyzh.common.core.page.PageDomain;
|
||||
import org.xyzh.common.core.page.PageParam;
|
||||
import org.xyzh.common.core.page.PageRequest;
|
||||
import org.xyzh.common.utils.id.IdUtil;
|
||||
import org.xyzh.workcase.mapper.TbVideoMeetingMapper;
|
||||
import org.xyzh.workcase.mapper.TbMeetingParticipantMapper;
|
||||
import org.xyzh.workcase.mapper.TbMeetingTranscriptionMapper;
|
||||
|
||||
/**
|
||||
* @description 视频会议服务实现类(伪代码)
|
||||
* @filename MeetServiceImpl.java
|
||||
* @author cascade
|
||||
* @copyright xyzh
|
||||
* @since 2025-12-22
|
||||
*/
|
||||
@DubboService(version = "1.0.0", group = "workcase", timeout = 30000, retries = 0)
|
||||
public class MeetServiceImpl implements MeetService {
|
||||
private static final Logger logger = LoggerFactory.getLogger(MeetServiceImpl.class);
|
||||
|
||||
@Autowired
|
||||
private TbVideoMeetingMapper videoMeetingMapper;
|
||||
|
||||
@Autowired
|
||||
private TbMeetingParticipantMapper meetingParticipantMapper;
|
||||
|
||||
@Autowired
|
||||
private TbMeetingTranscriptionMapper meetingTranscriptionMapper;
|
||||
|
||||
// TODO: 注入Jitsi配置和JWT工具类
|
||||
// @Autowired
|
||||
// private JitsiConfig jitsiConfig;
|
||||
// @Autowired
|
||||
// private JwtTokenUtil jwtTokenUtil;
|
||||
|
||||
// ========================= 会议管理 ==========================
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public ResultDomain<TbVideoMeetingDTO> createMeeting(TbVideoMeetingDTO meeting) {
|
||||
logger.info("创建会议: roomId={}, meetingName={}", meeting.getRoomId(), meeting.getMeetingName());
|
||||
|
||||
// TODO: 生成唯一的Jitsi房间名
|
||||
// String jitsiRoomName = "meet_" + IdUtil.generateUUID().replace("-", "");
|
||||
|
||||
if (meeting.getMeetingId() == null || meeting.getMeetingId().isEmpty()) {
|
||||
meeting.setMeetingId(IdUtil.generateUUID());
|
||||
}
|
||||
if (meeting.getOptsn() == null || meeting.getOptsn().isEmpty()) {
|
||||
meeting.setOptsn(IdUtil.getOptsn());
|
||||
}
|
||||
if (meeting.getStatus() == null || meeting.getStatus().isEmpty()) {
|
||||
meeting.setStatus("scheduled");
|
||||
}
|
||||
// TODO: 设置Jitsi相关配置
|
||||
// meeting.setJitsiRoomName(jitsiRoomName);
|
||||
// meeting.setJitsiServerUrl(jitsiConfig.getServerUrl());
|
||||
|
||||
int rows = videoMeetingMapper.insertVideoMeeting(meeting);
|
||||
if (rows > 0) {
|
||||
logger.info("会议创建成功: meetingId={}", meeting.getMeetingId());
|
||||
return ResultDomain.success("创建成功", meeting);
|
||||
}
|
||||
return ResultDomain.failure("创建失败");
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResultDomain<TbVideoMeetingDTO> updateMeeting(TbVideoMeetingDTO meeting) {
|
||||
logger.info("更新会议: meetingId={}", meeting.getMeetingId());
|
||||
|
||||
TbVideoMeetingDTO existing = videoMeetingMapper.selectVideoMeetingById(meeting.getMeetingId());
|
||||
if (existing == null) {
|
||||
return ResultDomain.failure("会议不存在");
|
||||
}
|
||||
|
||||
int rows = videoMeetingMapper.updateVideoMeeting(meeting);
|
||||
if (rows > 0) {
|
||||
TbVideoMeetingDTO updated = videoMeetingMapper.selectVideoMeetingById(meeting.getMeetingId());
|
||||
return ResultDomain.success("更新成功", updated);
|
||||
}
|
||||
return ResultDomain.failure("更新失败");
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResultDomain<TbVideoMeetingDTO> startMeeting(String meetingId) {
|
||||
logger.info("开始会议: meetingId={}", meetingId);
|
||||
|
||||
TbVideoMeetingDTO existing = videoMeetingMapper.selectVideoMeetingById(meetingId);
|
||||
if (existing == null) {
|
||||
return ResultDomain.failure("会议不存在");
|
||||
}
|
||||
if ("ongoing".equals(existing.getStatus())) {
|
||||
return ResultDomain.failure("会议已在进行中");
|
||||
}
|
||||
|
||||
TbVideoMeetingDTO meeting = new TbVideoMeetingDTO();
|
||||
meeting.setMeetingId(meetingId);
|
||||
meeting.setStatus("ongoing");
|
||||
meeting.setActualStartTime(new Date());
|
||||
|
||||
int rows = videoMeetingMapper.updateVideoMeeting(meeting);
|
||||
if (rows > 0) {
|
||||
TbVideoMeetingDTO updated = videoMeetingMapper.selectVideoMeetingById(meetingId);
|
||||
return ResultDomain.success("会议已开始", updated);
|
||||
}
|
||||
return ResultDomain.failure("开始会议失败");
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResultDomain<TbVideoMeetingDTO> endMeeting(String meetingId) {
|
||||
logger.info("结束会议: meetingId={}", meetingId);
|
||||
|
||||
TbVideoMeetingDTO existing = videoMeetingMapper.selectVideoMeetingById(meetingId);
|
||||
if (existing == null) {
|
||||
return ResultDomain.failure("会议不存在");
|
||||
}
|
||||
|
||||
TbVideoMeetingDTO meeting = new TbVideoMeetingDTO();
|
||||
meeting.setMeetingId(meetingId);
|
||||
meeting.setStatus("ended");
|
||||
meeting.setActualEndTime(new Date());
|
||||
|
||||
// TODO: 计算会议时长
|
||||
// if (existing.getActualStartTime() != null) {
|
||||
// long durationMs = new Date().getTime() - existing.getActualStartTime().getTime();
|
||||
// meeting.setDurationSeconds((int)(durationMs / 1000));
|
||||
// }
|
||||
|
||||
int rows = videoMeetingMapper.updateVideoMeeting(meeting);
|
||||
if (rows > 0) {
|
||||
// TODO: 更新所有参与者离开时间
|
||||
// updateAllParticipantsLeaveTime(meetingId);
|
||||
|
||||
TbVideoMeetingDTO updated = videoMeetingMapper.selectVideoMeetingById(meetingId);
|
||||
return ResultDomain.success("会议已结束", updated);
|
||||
}
|
||||
return ResultDomain.failure("结束会议失败");
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResultDomain<Boolean> deleteMeeting(String meetingId) {
|
||||
logger.info("删除会议: meetingId={}", meetingId);
|
||||
|
||||
TbVideoMeetingDTO existing = videoMeetingMapper.selectVideoMeetingById(meetingId);
|
||||
if (existing == null) {
|
||||
return ResultDomain.failure("会议不存在");
|
||||
}
|
||||
|
||||
TbVideoMeetingDTO meeting = new TbVideoMeetingDTO();
|
||||
meeting.setMeetingId(meetingId);
|
||||
int rows = videoMeetingMapper.deleteVideoMeeting(meeting);
|
||||
if (rows > 0) {
|
||||
return ResultDomain.success("删除成功", true);
|
||||
}
|
||||
return ResultDomain.failure("删除失败");
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResultDomain<TbVideoMeetingDTO> getMeetingById(String meetingId) {
|
||||
TbVideoMeetingDTO meeting = videoMeetingMapper.selectVideoMeetingById(meetingId);
|
||||
if (meeting != null) {
|
||||
return ResultDomain.success("查询成功", meeting);
|
||||
}
|
||||
return ResultDomain.failure("会议不存在");
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResultDomain<VideoMeetingVO> getMeetingPage(PageRequest<TbVideoMeetingDTO> pageRequest) {
|
||||
TbVideoMeetingDTO filter = pageRequest.getFilter();
|
||||
if (filter == null) {
|
||||
filter = new TbVideoMeetingDTO();
|
||||
}
|
||||
|
||||
PageParam pageParam = pageRequest.getPageParam();
|
||||
List<VideoMeetingVO> list = videoMeetingMapper.selectVideoMeetingPage(filter, pageParam);
|
||||
long total = videoMeetingMapper.countVideoMeetings(filter);
|
||||
pageParam.setTotal((int) total);
|
||||
|
||||
PageDomain<VideoMeetingVO> pageDomain = new PageDomain<>(pageParam, list);
|
||||
return ResultDomain.success("查询成功", pageDomain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResultDomain<String> generateMeetingJoinUrl(String meetingId, String userId, String userName) {
|
||||
logger.info("生成会议加入链接: meetingId={}, userId={}", meetingId, userId);
|
||||
|
||||
TbVideoMeetingDTO meeting = videoMeetingMapper.selectVideoMeetingById(meetingId);
|
||||
if (meeting == null) {
|
||||
return ResultDomain.failure("会议不存在");
|
||||
}
|
||||
|
||||
// TODO: 生成Jitsi iframe URL
|
||||
// String jwtToken = generateMeetingToken(meetingId, userId, false).getData();
|
||||
// String baseUrl = meeting.getJitsiServerUrl();
|
||||
// String roomName = meeting.getJitsiRoomName();
|
||||
// String iframeUrl = String.format("%s/%s?jwt=%s#userInfo.displayName=%s",
|
||||
// baseUrl, roomName, jwtToken, URLEncoder.encode(userName, "UTF-8"));
|
||||
|
||||
String iframeUrl = "TODO: 生成Jitsi iframe URL";
|
||||
return ResultDomain.success("生成成功", iframeUrl);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResultDomain<String> generateMeetingToken(String meetingId, String userId, boolean isModerator) {
|
||||
logger.info("生成会议JWT: meetingId={}, userId={}, isModerator={}", meetingId, userId, isModerator);
|
||||
|
||||
TbVideoMeetingDTO meeting = videoMeetingMapper.selectVideoMeetingById(meetingId);
|
||||
if (meeting == null) {
|
||||
return ResultDomain.failure("会议不存在");
|
||||
}
|
||||
|
||||
// TODO: 使用Jitsi JWT规范生成Token
|
||||
// JitsiTokenPayload payload = new JitsiTokenPayload();
|
||||
// payload.setRoom(meeting.getJitsiRoomName());
|
||||
// payload.setModerator(isModerator);
|
||||
// payload.setUserId(userId);
|
||||
// String token = jwtTokenUtil.generateJitsiToken(payload);
|
||||
|
||||
String token = "TODO: 生成Jitsi JWT Token";
|
||||
return ResultDomain.success("生成成功", token);
|
||||
}
|
||||
|
||||
// ========================= 参与者管理 ==========================
|
||||
|
||||
@Override
|
||||
public ResultDomain<TbMeetingParticipantDTO> joinMeeting(TbMeetingParticipantDTO participant) {
|
||||
logger.info("参与者加入会议: meetingId={}, userId={}", participant.getMeetingId(), participant.getUserId());
|
||||
|
||||
// 检查会议是否存在
|
||||
TbVideoMeetingDTO meeting = videoMeetingMapper.selectVideoMeetingById(participant.getMeetingId());
|
||||
if (meeting == null) {
|
||||
return ResultDomain.failure("会议不存在");
|
||||
}
|
||||
|
||||
if (participant.getParticipantId() == null || participant.getParticipantId().isEmpty()) {
|
||||
participant.setParticipantId(IdUtil.generateUUID());
|
||||
}
|
||||
if (participant.getOptsn() == null || participant.getOptsn().isEmpty()) {
|
||||
participant.setOptsn(IdUtil.getOptsn());
|
||||
}
|
||||
participant.setJoinTime(new Date());
|
||||
|
||||
int rows = meetingParticipantMapper.insertMeetingParticipant(participant);
|
||||
if (rows > 0) {
|
||||
// 更新会议参与人数
|
||||
TbVideoMeetingDTO updateMeeting = new TbVideoMeetingDTO();
|
||||
updateMeeting.setMeetingId(participant.getMeetingId());
|
||||
updateMeeting.setParticipantCount(meeting.getParticipantCount() != null ? meeting.getParticipantCount() + 1 : 1);
|
||||
videoMeetingMapper.updateVideoMeeting(updateMeeting);
|
||||
|
||||
return ResultDomain.success("加入成功", participant);
|
||||
}
|
||||
return ResultDomain.failure("加入失败");
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResultDomain<Boolean> leaveMeeting(String participantId) {
|
||||
logger.info("参与者离开会议: participantId={}", participantId);
|
||||
|
||||
TbMeetingParticipantDTO existing = meetingParticipantMapper.selectMeetingParticipantById(participantId);
|
||||
if (existing == null) {
|
||||
return ResultDomain.failure("参与者不存在");
|
||||
}
|
||||
|
||||
TbMeetingParticipantDTO participant = new TbMeetingParticipantDTO();
|
||||
participant.setParticipantId(participantId);
|
||||
participant.setLeaveTime(new Date());
|
||||
|
||||
// TODO: 计算参与时长
|
||||
// if (existing.getJoinTime() != null) {
|
||||
// long durationMs = new Date().getTime() - existing.getJoinTime().getTime();
|
||||
// participant.setDurationSeconds((int)(durationMs / 1000));
|
||||
// }
|
||||
|
||||
int rows = meetingParticipantMapper.updateMeetingParticipant(participant);
|
||||
if (rows > 0) {
|
||||
return ResultDomain.success("离开成功", true);
|
||||
}
|
||||
return ResultDomain.failure("离开失败");
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResultDomain<MeetingParticipantVO> getMeetingParticipantList(String meetingId) {
|
||||
TbMeetingParticipantDTO filter = new TbMeetingParticipantDTO();
|
||||
filter.setMeetingId(meetingId);
|
||||
List<MeetingParticipantVO> list = meetingParticipantMapper.selectMeetingParticipantList(filter);
|
||||
return ResultDomain.success("查询成功", list);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResultDomain<TbMeetingParticipantDTO> updateParticipant(TbMeetingParticipantDTO participant) {
|
||||
logger.info("更新参与者: participantId={}", participant.getParticipantId());
|
||||
|
||||
TbMeetingParticipantDTO existing = meetingParticipantMapper.selectMeetingParticipantById(participant.getParticipantId());
|
||||
if (existing == null) {
|
||||
return ResultDomain.failure("参与者不存在");
|
||||
}
|
||||
|
||||
int rows = meetingParticipantMapper.updateMeetingParticipant(participant);
|
||||
if (rows > 0) {
|
||||
TbMeetingParticipantDTO updated = meetingParticipantMapper.selectMeetingParticipantById(participant.getParticipantId());
|
||||
return ResultDomain.success("更新成功", updated);
|
||||
}
|
||||
return ResultDomain.failure("更新失败");
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResultDomain<Boolean> setModerator(String participantId, boolean isModerator) {
|
||||
logger.info("设置主持人: participantId={}, isModerator={}", participantId, isModerator);
|
||||
|
||||
TbMeetingParticipantDTO participant = new TbMeetingParticipantDTO();
|
||||
participant.setParticipantId(participantId);
|
||||
participant.setIsModerator(isModerator);
|
||||
|
||||
int rows = meetingParticipantMapper.updateMeetingParticipant(participant);
|
||||
if (rows > 0) {
|
||||
return ResultDomain.success("设置成功", true);
|
||||
}
|
||||
return ResultDomain.failure("设置失败");
|
||||
}
|
||||
|
||||
// ========================= 转录管理 ==========================
|
||||
|
||||
@Override
|
||||
public ResultDomain<TbMeetingTranscriptionDTO> addTranscription(TbMeetingTranscriptionDTO transcription) {
|
||||
logger.info("添加转录记录: meetingId={}, speakerId={}", transcription.getMeetingId(), transcription.getSpeakerId());
|
||||
|
||||
if (transcription.getTranscriptionId() == null || transcription.getTranscriptionId().isEmpty()) {
|
||||
transcription.setTranscriptionId(IdUtil.generateUUID());
|
||||
}
|
||||
if (transcription.getOptsn() == null || transcription.getOptsn().isEmpty()) {
|
||||
transcription.setOptsn(IdUtil.getOptsn());
|
||||
}
|
||||
|
||||
int rows = meetingTranscriptionMapper.insertMeetingTranscription(transcription);
|
||||
if (rows > 0) {
|
||||
return ResultDomain.success("添加成功", transcription);
|
||||
}
|
||||
return ResultDomain.failure("添加失败");
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResultDomain<MeetingTranscriptionVO> getTranscriptionPage(PageRequest<TbMeetingTranscriptionDTO> pageRequest) {
|
||||
TbMeetingTranscriptionDTO filter = pageRequest.getFilter();
|
||||
if (filter == null) {
|
||||
filter = new TbMeetingTranscriptionDTO();
|
||||
}
|
||||
|
||||
PageParam pageParam = pageRequest.getPageParam();
|
||||
List<MeetingTranscriptionVO> list = meetingTranscriptionMapper.selectMeetingTranscriptionPage(filter, pageParam);
|
||||
long total = meetingTranscriptionMapper.countMeetingTranscriptions(filter);
|
||||
pageParam.setTotal((int) total);
|
||||
|
||||
PageDomain<MeetingTranscriptionVO> pageDomain = new PageDomain<>(pageParam, list);
|
||||
return ResultDomain.success("查询成功", pageDomain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResultDomain<String> getFullTranscriptionText(String meetingId) {
|
||||
logger.info("获取完整转录文本: meetingId={}", meetingId);
|
||||
|
||||
TbMeetingTranscriptionDTO filter = new TbMeetingTranscriptionDTO();
|
||||
filter.setMeetingId(meetingId);
|
||||
filter.setIsFinal(true);
|
||||
List<MeetingTranscriptionVO> list = meetingTranscriptionMapper.selectMeetingTranscriptionList(filter);
|
||||
|
||||
// TODO: 拼接转录文本
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (MeetingTranscriptionVO transcription : list) {
|
||||
// 格式:[说话人名称] 内容
|
||||
sb.append("[").append(transcription.getSpeakerName()).append("] ");
|
||||
sb.append(transcription.getContent()).append("\n");
|
||||
}
|
||||
|
||||
return ResultDomain.success("查询成功", sb.toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResultDomain<Boolean> deleteTranscription(String transcriptionId) {
|
||||
logger.info("删除转录记录: transcriptionId={}", transcriptionId);
|
||||
|
||||
TbMeetingTranscriptionDTO existing = meetingTranscriptionMapper.selectMeetingTranscriptionById(transcriptionId);
|
||||
if (existing == null) {
|
||||
return ResultDomain.failure("转录记录不存在");
|
||||
}
|
||||
|
||||
TbMeetingTranscriptionDTO transcription = new TbMeetingTranscriptionDTO();
|
||||
transcription.setTranscriptionId(transcriptionId);
|
||||
int rows = meetingTranscriptionMapper.deleteMeetingTranscription(transcription);
|
||||
if (rows > 0) {
|
||||
return ResultDomain.success("删除成功", true);
|
||||
}
|
||||
return ResultDomain.failure("删除失败");
|
||||
}
|
||||
|
||||
// ========================= 会议统计 ==========================
|
||||
|
||||
@Override
|
||||
public ResultDomain<VideoMeetingVO> getMeetingStatistics(String meetingId) {
|
||||
logger.info("获取会议统计: meetingId={}", meetingId);
|
||||
|
||||
TbVideoMeetingDTO meeting = videoMeetingMapper.selectVideoMeetingById(meetingId);
|
||||
if (meeting == null) {
|
||||
return ResultDomain.failure("会议不存在");
|
||||
}
|
||||
|
||||
// TODO: 查询并组装统计信息
|
||||
// - 参与人数
|
||||
// - 会议时长
|
||||
// - 转录记录数
|
||||
// - 各参与者参与时长等
|
||||
|
||||
TbMeetingParticipantDTO participantFilter = new TbMeetingParticipantDTO();
|
||||
participantFilter.setMeetingId(meetingId);
|
||||
List<MeetingParticipantVO> participants = meetingParticipantMapper.selectMeetingParticipantList(participantFilter);
|
||||
|
||||
VideoMeetingVO vo = new VideoMeetingVO();
|
||||
vo.setMeetingId(meeting.getMeetingId());
|
||||
vo.setMeetingName(meeting.getMeetingName());
|
||||
vo.setStatus(meeting.getStatus());
|
||||
vo.setParticipantCount(participants.size());
|
||||
vo.setActualStartTime(meeting.getActualStartTime());
|
||||
vo.setActualEndTime(meeting.getActualEndTime());
|
||||
vo.setDurationSeconds(meeting.getDurationSeconds());
|
||||
|
||||
// TODO: 格式化时长
|
||||
// if (meeting.getDurationSeconds() != null) {
|
||||
// vo.setDurationFormatted(formatDuration(meeting.getDurationSeconds()));
|
||||
// }
|
||||
|
||||
return ResultDomain.success("查询成功", vo);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.xyzh.workcase.service;
|
||||
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import org.apache.dubbo.config.annotation.DubboService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -7,19 +8,25 @@ import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.xyzh.api.workcase.dto.TbChatRoomMemberDTO;
|
||||
import org.xyzh.api.workcase.dto.TbChatRoomMessageDTO;
|
||||
import org.xyzh.api.workcase.dto.TbVideoMeetingDTO;
|
||||
import org.xyzh.api.workcase.service.ChatRoomService;
|
||||
import org.xyzh.api.workcase.service.JitsiTokenService;
|
||||
import org.xyzh.api.workcase.service.VideoMeetingService;
|
||||
import org.xyzh.api.workcase.vo.ChatMemberVO;
|
||||
import org.xyzh.api.workcase.vo.VideoMeetingVO;
|
||||
import org.xyzh.common.auth.utils.LoginUtil;
|
||||
import org.xyzh.common.core.domain.LoginDomain;
|
||||
import org.xyzh.common.core.domain.ResultDomain;
|
||||
import org.xyzh.common.utils.id.IdUtil;
|
||||
import org.xyzh.workcase.mapper.TbChatRoomMemberMapper;
|
||||
import org.xyzh.workcase.mapper.TbVideoMeetingMapper;
|
||||
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
/**
|
||||
* @description 视频会议服务实现类
|
||||
@@ -41,13 +48,21 @@ public class VideoMeetingServiceImpl implements VideoMeetingService {
|
||||
@Autowired
|
||||
private JitsiTokenService jitsiTokenService;
|
||||
|
||||
@Autowired
|
||||
private ChatRoomService chatRoomService;
|
||||
|
||||
// 会议创建锁映射表:每个meetingId对应一个ReentrantLock
|
||||
private final ConcurrentHashMap<String, ReentrantLock> meetingLocks = new ConcurrentHashMap<>();
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public ResultDomain<VideoMeetingVO> createMeeting(TbVideoMeetingDTO meetingDTO) {
|
||||
// 获取当前用户ID
|
||||
String userId = LoginUtil.getCurrentUserId();
|
||||
logger.info("创建视频会议: roomId={}, workcaseId={}, userId={}",
|
||||
meetingDTO.getRoomId(), meetingDTO.getWorkcaseId(), userId);
|
||||
LoginDomain loginDomain = LoginUtil.getCurrentLogin();
|
||||
String userId = loginDomain.getUser().getUserId();
|
||||
logger.info("创建视频会议预约: roomId={}, workcaseId={}, userId={}, startTime={}, endTime={}",
|
||||
meetingDTO.getRoomId(), meetingDTO.getWorkcaseId(), userId,
|
||||
meetingDTO.getStartTime(), meetingDTO.getEndTime());
|
||||
|
||||
try {
|
||||
// 1. 验证用户是否为聊天室成员
|
||||
@@ -57,18 +72,25 @@ public class VideoMeetingServiceImpl implements VideoMeetingService {
|
||||
return ResultDomain.failure("您不是聊天室成员,无法创建会议");
|
||||
}
|
||||
|
||||
// 2. 检查聊天室是否已有进行中的会议
|
||||
TbVideoMeetingDTO existingMeetingFilter = new TbVideoMeetingDTO();
|
||||
existingMeetingFilter.setRoomId(meetingDTO.getRoomId());
|
||||
existingMeetingFilter.setStatus("ongoing");
|
||||
List<VideoMeetingVO> existingMeetings = videoMeetingMapper.selectVideoMeetingList(existingMeetingFilter);
|
||||
// 2. 检查聊天室是否已有时间冲突的会议
|
||||
TbVideoMeetingDTO conflictFilter = new TbVideoMeetingDTO();
|
||||
conflictFilter.setRoomId(meetingDTO.getRoomId());
|
||||
conflictFilter.setStatus("scheduled"); // 只检查已安排的会议
|
||||
List<VideoMeetingVO> existingMeetings = videoMeetingMapper.selectVideoMeetingList(conflictFilter);
|
||||
|
||||
if (existingMeetings != null && !existingMeetings.isEmpty()) {
|
||||
logger.warn("聊天室已有进行中的会议: roomId={}", meetingDTO.getRoomId());
|
||||
return ResultDomain.failure("聊天室已有进行中的会议,请稍后再试");
|
||||
for (VideoMeetingVO existing : existingMeetings) {
|
||||
// 检查时间是否冲突
|
||||
if (isTimeConflict(meetingDTO.getStartTime(), meetingDTO.getEndTime(),
|
||||
existing.getStartTime(), existing.getEndTime())) {
|
||||
logger.warn("会议时间冲突: roomId={}, existingMeetingId={}",
|
||||
meetingDTO.getRoomId(), existing.getMeetingId());
|
||||
return ResultDomain.failure("该时间段已有会议安排,请选择其他时间");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 生成会议ID和房间名
|
||||
// 3. 生成会议ID和房间名(房间名暂时生成,真正创建Jitsi时会重新生成)
|
||||
String meetingId = IdUtil.generateUUID();
|
||||
String jitsiRoomName = jitsiTokenService.generateRoomName(meetingDTO.getWorkcaseId());
|
||||
|
||||
@@ -78,32 +100,21 @@ public class VideoMeetingServiceImpl implements VideoMeetingService {
|
||||
memberFilter.setUserId(userId);
|
||||
List<ChatMemberVO> members = chatRoomMemberMapper.selectChatRoomMemberList(memberFilter);
|
||||
|
||||
String userName = "用户";
|
||||
String userType = "guest";
|
||||
String userName = loginDomain.getUserInfo().getUsername();
|
||||
String userType = "guest".equals(loginDomain.getUser().getStatus())?"guest":"user";
|
||||
if (members != null && !members.isEmpty()) {
|
||||
ChatMemberVO member = members.get(0);
|
||||
userName = member.getUserName();
|
||||
userType = member.getUserType();
|
||||
}
|
||||
|
||||
// 5. 生成创建者的JWT Token(创建者默认为主持人)
|
||||
String jwtToken = jitsiTokenService.generateJwtToken(
|
||||
jitsiRoomName,
|
||||
userId,
|
||||
userName,
|
||||
true // 创建者为主持人
|
||||
);
|
||||
|
||||
// 6. 构建iframe URL
|
||||
String iframeUrl = jitsiTokenService.buildIframeUrl(jitsiRoomName, jwtToken, meetingDTO.getConfig());
|
||||
|
||||
// 7. 填充会议信息
|
||||
// 5. 填充会议预约信息(不生成JWT Token和iframe URL)
|
||||
meetingDTO.setMeetingId(meetingId);
|
||||
meetingDTO.setJitsiRoomName(jitsiRoomName);
|
||||
meetingDTO.setJwtToken(jwtToken); // 存储创建者的token(可选)
|
||||
meetingDTO.setIframeUrl(iframeUrl);
|
||||
meetingDTO.setStatus("scheduled");
|
||||
meetingDTO.setCreatorId(userId);
|
||||
meetingDTO.setJwtToken(null); // 预约阶段不生成token
|
||||
meetingDTO.setIframeUrl(null); // 预约阶段不生成URL
|
||||
meetingDTO.setStatus("scheduled"); // 状态为已安排
|
||||
meetingDTO.setCreator(userId);
|
||||
meetingDTO.setCreatorType(userType);
|
||||
meetingDTO.setCreatorName(userName);
|
||||
meetingDTO.setParticipantCount(0);
|
||||
@@ -113,24 +124,31 @@ public class VideoMeetingServiceImpl implements VideoMeetingService {
|
||||
meetingDTO.setMaxParticipants(10);
|
||||
}
|
||||
|
||||
// 8. 插入数据库
|
||||
if (meetingDTO.getAdvance() == null) {
|
||||
meetingDTO.setAdvance(5); // 默认提前5分钟可入会
|
||||
}
|
||||
|
||||
// 6. 插入数据库
|
||||
int rows = videoMeetingMapper.insertVideoMeeting(meetingDTO);
|
||||
if (rows > 0) {
|
||||
logger.info("视频会议创建成功: meetingId={}, jitsiRoomName={}",
|
||||
meetingId, jitsiRoomName);
|
||||
logger.info("视频会议预约创建成功: meetingId={}, jitsiRoomName={}, startTime={}, endTime={}",
|
||||
meetingId, jitsiRoomName, meetingDTO.getStartTime(), meetingDTO.getEndTime());
|
||||
|
||||
// 9. 返回VO
|
||||
// 7. 发送会议通知消息到聊天室(内容为meetingId而非URL)
|
||||
sendMeetingNotification(meetingDTO, userName);
|
||||
|
||||
// 8. 返回VO
|
||||
VideoMeetingVO meetingVO = new VideoMeetingVO();
|
||||
BeanUtils.copyProperties(meetingDTO, meetingVO);
|
||||
return ResultDomain.success("创建会议成功", meetingVO);
|
||||
return ResultDomain.success("创建会议预约成功", meetingVO);
|
||||
} else {
|
||||
logger.error("插入会议记录失败: meetingId={}", meetingId);
|
||||
return ResultDomain.failure("创建会议失败");
|
||||
return ResultDomain.failure("创建会议预约失败");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("创建视频会议异常: roomId={}, error={}",
|
||||
logger.error("创建视频会议预约异常: roomId={}, error={}",
|
||||
meetingDTO.getRoomId(), e.getMessage(), e);
|
||||
return ResultDomain.failure("创建会议失败: " + e.getMessage());
|
||||
return ResultDomain.failure("创建会议预约失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,6 +213,7 @@ public class VideoMeetingServiceImpl implements VideoMeetingService {
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public ResultDomain<VideoMeetingVO> generateUserMeetingUrl(String meetingId, String userId) {
|
||||
logger.info("生成用户专属会议URL: meetingId={}, userId={}", meetingId, userId);
|
||||
|
||||
@@ -218,23 +237,96 @@ public class VideoMeetingServiceImpl implements VideoMeetingService {
|
||||
return ResultDomain.failure("您无权访问此会议");
|
||||
}
|
||||
|
||||
// 3. 获取用户信息
|
||||
// 3. 检查会议时间窗口(仅对scheduled状态的会议检查)
|
||||
if ("scheduled".equals(meeting.getStatus())) {
|
||||
Date now = new Date();
|
||||
|
||||
// 计算提前入会时间点(开始时间 - advance 分钟)
|
||||
Calendar calendar = Calendar.getInstance();
|
||||
calendar.setTime(meeting.getStartTime());
|
||||
calendar.add(Calendar.MINUTE, -meeting.getAdvance());
|
||||
Date advanceTime = calendar.getTime();
|
||||
|
||||
// 检查当前时间是否在允许入会的时间窗口内
|
||||
if (now.before(advanceTime)) {
|
||||
logger.warn("会议未到入会时间: meetingId={}, 当前时间={}, 提前入会时间={}",
|
||||
meetingId, now, advanceTime);
|
||||
return ResultDomain.failure("会议未到入会时间,请在 " + advanceTime + " 之后加入");
|
||||
}
|
||||
|
||||
if (now.after(meeting.getEndTime())) {
|
||||
logger.warn("会议已结束: meetingId={}, 当前时间={}, 结束时间={}",
|
||||
meetingId, now, meeting.getEndTime());
|
||||
return ResultDomain.failure("会议已结束");
|
||||
}
|
||||
|
||||
// 4. 使用ReentrantLock进行双检锁:首次用户入会时创建Jitsi会议室
|
||||
// 获取或创建该会议的锁对象
|
||||
ReentrantLock lock = meetingLocks.computeIfAbsent(meetingId, k -> new ReentrantLock());
|
||||
|
||||
logger.info("尝试获取会议创建锁: meetingId={}", meetingId);
|
||||
lock.lock(); // 阻塞等待获取锁
|
||||
try {
|
||||
logger.info("成功获取会议创建锁: meetingId={}, userId={}", meetingId, userId);
|
||||
|
||||
// 双重检查:再次查询数据库确认会议状态(防止其他线程已创建)
|
||||
List<VideoMeetingVO> recheck = videoMeetingMapper.selectVideoMeetingList(filter);
|
||||
if (recheck != null && !recheck.isEmpty()) {
|
||||
VideoMeetingVO recheckMeeting = recheck.get(0);
|
||||
|
||||
if ("scheduled".equals(recheckMeeting.getStatus())) {
|
||||
logger.info("首次创建Jitsi会议室: meetingId={}", meetingId);
|
||||
|
||||
// 更新会议状态为进行中
|
||||
TbVideoMeetingDTO updateDTO = new TbVideoMeetingDTO();
|
||||
updateDTO.setMeetingId(meetingId);
|
||||
updateDTO.setStatus("ongoing");
|
||||
updateDTO.setActualStartTime(new Date());
|
||||
|
||||
int rows = videoMeetingMapper.updateVideoMeeting(updateDTO);
|
||||
if (rows > 0) {
|
||||
logger.info("Jitsi会议室创建成功,会议状态已更新: meetingId={}", meetingId);
|
||||
meeting.setStatus("ongoing");
|
||||
meeting.setActualStartTime(new Date());
|
||||
} else {
|
||||
logger.error("更新会议状态失败: meetingId={}", meetingId);
|
||||
return ResultDomain.failure("创建会议失败");
|
||||
}
|
||||
} else {
|
||||
logger.info("会议已被其他用户创建: meetingId={}, status={}",
|
||||
meetingId, recheckMeeting.getStatus());
|
||||
meeting.setStatus(recheckMeeting.getStatus());
|
||||
meeting.setActualStartTime(recheckMeeting.getActualStartTime());
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
lock.unlock();
|
||||
logger.info("释放会议创建锁: meetingId={}", meetingId);
|
||||
|
||||
// 清理锁对象:如果没有其他线程在等待,则移除锁
|
||||
if (!lock.hasQueuedThreads()) {
|
||||
meetingLocks.remove(meetingId);
|
||||
logger.debug("清理会议锁对象: meetingId={}", meetingId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 5. 获取用户信息
|
||||
TbChatRoomMemberDTO memberFilter = new TbChatRoomMemberDTO();
|
||||
memberFilter.setRoomId(meeting.getRoomId());
|
||||
memberFilter.setUserId(userId);
|
||||
List<ChatMemberVO> members = chatRoomMemberMapper.selectChatRoomMemberList(memberFilter);
|
||||
|
||||
String userName = "用户";
|
||||
boolean isModerator = false;
|
||||
// 会议创建人才是主持人
|
||||
boolean isModerator = userId.equals(meeting.getCreator());
|
||||
|
||||
if (members != null && !members.isEmpty()) {
|
||||
ChatMemberVO member = members.get(0);
|
||||
userName = member.getUserName();
|
||||
// 客服人员设为主持人
|
||||
isModerator = "agent".equals(member.getUserType());
|
||||
}
|
||||
|
||||
// 4. 生成用户专属JWT Token
|
||||
// 6. 生成用户专属JWT Token
|
||||
String userJwtToken = jitsiTokenService.generateJwtToken(
|
||||
meeting.getJitsiRoomName(),
|
||||
userId,
|
||||
@@ -242,18 +334,19 @@ public class VideoMeetingServiceImpl implements VideoMeetingService {
|
||||
isModerator
|
||||
);
|
||||
|
||||
// 5. 构建用户专属iframe URL
|
||||
// 7. 构建用户专属iframe URL
|
||||
String userIframeUrl = jitsiTokenService.buildIframeUrl(
|
||||
meeting.getJitsiRoomName(),
|
||||
userJwtToken,
|
||||
meeting.getConfig()
|
||||
);
|
||||
|
||||
// 6. 更新VO
|
||||
// 8. 更新VO
|
||||
meeting.setJwtToken(userJwtToken);
|
||||
meeting.setIframeUrl(userIframeUrl);
|
||||
|
||||
logger.info("生成用户专属会议URL成功: meetingId={}, userId={}", meetingId, userId);
|
||||
logger.info("生成用户专属会议URL成功: meetingId={}, userId={}, status={}",
|
||||
meetingId, userId, meeting.getStatus());
|
||||
return ResultDomain.success("生成用户专属会议URL成功", meeting);
|
||||
} catch (Exception e) {
|
||||
logger.error("生成用户专属会议URL异常: meetingId={}, error={}", meetingId, e.getMessage(), e);
|
||||
@@ -383,4 +476,62 @@ public class VideoMeetingServiceImpl implements VideoMeetingService {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送会议通知消息到聊天室
|
||||
* @param meetingDTO 会议信息
|
||||
* @param creatorName 创建者名称
|
||||
*/
|
||||
private void sendMeetingNotification(TbVideoMeetingDTO meetingDTO, String creatorName) {
|
||||
logger.info("发送会议通知消息: roomId={}, meetingId={}",
|
||||
meetingDTO.getRoomId(), meetingDTO.getMeetingId());
|
||||
|
||||
// 构建消息内容
|
||||
TbChatRoomMessageDTO message = new TbChatRoomMessageDTO();
|
||||
message.setMessageId(IdUtil.generateUUID());
|
||||
message.setRoomId(meetingDTO.getRoomId());
|
||||
message.setSenderId(meetingDTO.getCreator());
|
||||
message.setSenderType(meetingDTO.getCreatorType());
|
||||
message.setSenderName(creatorName);
|
||||
message.setMessageType("meet"); // 会议类型消息
|
||||
message.setContent(meetingDTO.getMeetingId()); // 会议ID作为内容(前端根据ID查询会议详情)
|
||||
message.setStatus("sent");
|
||||
message.setReadCount(0);
|
||||
message.setSendTime(new Date());
|
||||
|
||||
// 构建扩展信息(会议详情)
|
||||
JSONObject contentExtra = new JSONObject();
|
||||
contentExtra.put("meetingId", meetingDTO.getMeetingId());
|
||||
contentExtra.put("meetingName", meetingDTO.getMeetingName());
|
||||
contentExtra.put("jitsiRoomName", meetingDTO.getJitsiRoomName());
|
||||
contentExtra.put("startTime", meetingDTO.getStartTime());
|
||||
contentExtra.put("endTime", meetingDTO.getEndTime());
|
||||
contentExtra.put("advance", meetingDTO.getAdvance());
|
||||
contentExtra.put("maxParticipants", meetingDTO.getMaxParticipants());
|
||||
contentExtra.put("creatorName", creatorName);
|
||||
contentExtra.put("workcaseId", meetingDTO.getWorkcaseId());
|
||||
contentExtra.put("status", meetingDTO.getStatus());
|
||||
message.setContentExtra(contentExtra);
|
||||
|
||||
// 发送消息
|
||||
ResultDomain<TbChatRoomMessageDTO> sendResult = chatRoomService.sendMessage(message);
|
||||
if (sendResult.getSuccess()) {
|
||||
logger.info("会议通知消息发送成功: messageId={}", message.getMessageId());
|
||||
} else {
|
||||
logger.warn("会议通知消息发送失败: {}", sendResult.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查时间是否冲突
|
||||
* @param start1 时间段1开始时间
|
||||
* @param end1 时间段1结束时间
|
||||
* @param start2 时间段2开始时间
|
||||
* @param end2 时间段2结束时间
|
||||
* @return 是否冲突
|
||||
*/
|
||||
private boolean isTimeConflict(Date start1, Date end1, Date start2, Date end2) {
|
||||
// 时间段1的结束时间 > 时间段2的开始时间 AND 时间段1的开始时间 < 时间段2的结束时间
|
||||
return end1.after(start2) && start1.before(end2);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,4 +92,20 @@ logging:
|
||||
console: UTF-8
|
||||
file: UTF-8
|
||||
level:
|
||||
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping: TRACE
|
||||
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping: TRACE
|
||||
|
||||
# ================== Jitsi Meet 视频会议配置 ==================
|
||||
jitsi:
|
||||
app:
|
||||
# 应用ID(必须与Docker配置中的JWT_APP_ID一致)
|
||||
id: urbanLifeline
|
||||
# JWT密钥(必须与Docker配置中的JWT_APP_SECRET一致)
|
||||
# 警告:生产环境请修改为强随机字符串,并妥善保管
|
||||
# 注意:HS256算法要求密钥长度至少32字节(256 bits)
|
||||
secret: urbanLifeline-jitsi-secret-key-2025-production-safe-hs256
|
||||
server:
|
||||
# Jitsi Meet服务器地址(Docker部署在本地8280端口)
|
||||
url: http://192.168.0.253:8280
|
||||
token:
|
||||
# JWT Token有效期(毫秒)- 默认2小时
|
||||
expiration: 7200000
|
||||
@@ -12,7 +12,7 @@
|
||||
<result column="message_type" property="messageType" jdbcType="VARCHAR"/>
|
||||
<result column="content" property="content" jdbcType="VARCHAR"/>
|
||||
<result column="files" property="files" jdbcType="ARRAY" typeHandler="org.xyzh.common.jdbc.handler.StringArrayTypeHandler"/>
|
||||
<result column="content_extra" property="contentExtra" jdbcType="OTHER" typeHandler="com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler"/>
|
||||
<result column="content_extra" property="contentExtra" jdbcType="OTHER" typeHandler="org.xyzh.common.jdbc.handler.FastJson2TypeHandler"/>
|
||||
<result column="reply_to_msg_id" property="replyToMsgId" jdbcType="VARCHAR"/>
|
||||
<result column="is_ai_message" property="isAiMessage" jdbcType="BOOLEAN"/>
|
||||
<result column="ai_message_id" property="aiMessageId" jdbcType="VARCHAR"/>
|
||||
@@ -34,7 +34,7 @@
|
||||
<result column="message_type" property="messageType" jdbcType="VARCHAR"/>
|
||||
<result column="content" property="content" jdbcType="VARCHAR"/>
|
||||
<result column="files" property="files" jdbcType="ARRAY" typeHandler="org.xyzh.common.jdbc.handler.StringArrayTypeHandler"/>
|
||||
<result column="content_extra" property="contentExtra" jdbcType="OTHER" typeHandler="com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler"/>
|
||||
<result column="content_extra" property="contentExtra" jdbcType="OTHER" typeHandler="org.xyzh.common.jdbc.handler.FastJson2TypeHandler"/>
|
||||
<result column="reply_to_msg_id" property="replyToMsgId" jdbcType="VARCHAR"/>
|
||||
<result column="is_ai_message" property="isAiMessage" jdbcType="BOOLEAN"/>
|
||||
<result column="ai_message_id" property="aiMessageId" jdbcType="VARCHAR"/>
|
||||
@@ -66,7 +66,7 @@
|
||||
#{optsn}, #{messageId}, #{roomId}, #{senderId}, #{senderType}, #{senderName}, #{content}, #{creator}
|
||||
<if test="messageType != null">, #{messageType}</if>
|
||||
<if test="files != null">, #{files, typeHandler=org.xyzh.common.jdbc.handler.StringArrayTypeHandler}</if>
|
||||
<if test="contentExtra != null">, #{contentExtra, typeHandler=com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler}</if>
|
||||
<if test="contentExtra != null">, #{contentExtra, typeHandler=org.xyzh.common.jdbc.handler.FastJson2TypeHandler}::jsonb</if>
|
||||
<if test="replyToMsgId != null">, #{replyToMsgId}</if>
|
||||
<if test="isAiMessage != null">, #{isAiMessage}</if>
|
||||
<if test="aiMessageId != null">, #{aiMessageId}</if>
|
||||
|
||||
@@ -9,21 +9,24 @@
|
||||
<result column="workcase_id" property="workcaseId" jdbcType="VARCHAR"/>
|
||||
<result column="meeting_name" property="meetingName" jdbcType="VARCHAR"/>
|
||||
<result column="meeting_password" property="meetingPassword" jdbcType="VARCHAR"/>
|
||||
<result column="description" property="description" jdbcType="VARCHAR"/>
|
||||
<result column="jwt_token" property="jwtToken" jdbcType="VARCHAR"/>
|
||||
<result column="jitsi_room_name" property="jitsiRoomName" jdbcType="VARCHAR"/>
|
||||
<result column="jitsi_server_url" property="jitsiServerUrl" jdbcType="VARCHAR"/>
|
||||
<result column="status" property="status" jdbcType="VARCHAR"/>
|
||||
<result column="creator_id" property="creatorId" jdbcType="VARCHAR"/>
|
||||
<result column="creator" property="creator" jdbcType="VARCHAR"/>
|
||||
<result column="creator_type" property="creatorType" jdbcType="VARCHAR"/>
|
||||
<result column="creator_name" property="creatorName" jdbcType="VARCHAR"/>
|
||||
<result column="participant_count" property="participantCount" jdbcType="INTEGER"/>
|
||||
<result column="max_participants" property="maxParticipants" jdbcType="INTEGER"/>
|
||||
<result column="start_time" property="actualStartTime" jdbcType="TIMESTAMP"/>
|
||||
<result column="end_time" property="actualEndTime" jdbcType="TIMESTAMP"/>
|
||||
<result column="start_time" property="startTime" jdbcType="TIMESTAMP"/>
|
||||
<result column="end_time" property="endTime" jdbcType="TIMESTAMP"/>
|
||||
<result column="advance" property="advance" jdbcType="INTEGER"/>
|
||||
<result column="actual_start_time" property="actualStartTime" jdbcType="TIMESTAMP"/>
|
||||
<result column="actual_end_time" property="actualEndTime" jdbcType="TIMESTAMP"/>
|
||||
<result column="duration_seconds" property="durationSeconds" jdbcType="INTEGER"/>
|
||||
<result column="iframe_url" property="iframeUrl" jdbcType="VARCHAR"/>
|
||||
<result column="config" property="config" jdbcType="OTHER" typeHandler="com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler"/>
|
||||
<result column="creator" property="creator" jdbcType="VARCHAR"/>
|
||||
<result column="create_time" property="createTime" jdbcType="TIMESTAMP"/>
|
||||
<result column="update_time" property="updateTime" jdbcType="TIMESTAMP"/>
|
||||
<result column="delete_time" property="deleteTime" jdbcType="TIMESTAMP"/>
|
||||
@@ -37,17 +40,20 @@
|
||||
<result column="workcase_id" property="workcaseId" jdbcType="VARCHAR"/>
|
||||
<result column="meeting_name" property="meetingName" jdbcType="VARCHAR"/>
|
||||
<result column="meeting_password" property="meetingPassword" jdbcType="VARCHAR"/>
|
||||
<result column="description" property="description" jdbcType="VARCHAR"/>
|
||||
<result column="jwt_token" property="jwtToken" jdbcType="VARCHAR"/>
|
||||
<result column="jitsi_room_name" property="jitsiRoomName" jdbcType="VARCHAR"/>
|
||||
<result column="jitsi_server_url" property="jitsiServerUrl" jdbcType="VARCHAR"/>
|
||||
<result column="status" property="status" jdbcType="VARCHAR"/>
|
||||
<result column="creator_id" property="creatorId" jdbcType="VARCHAR"/>
|
||||
<result column="creator_type" property="creatorType" jdbcType="VARCHAR"/>
|
||||
<result column="creator_name" property="creatorName" jdbcType="VARCHAR"/>
|
||||
<result column="participant_count" property="participantCount" jdbcType="INTEGER"/>
|
||||
<result column="max_participants" property="maxParticipants" jdbcType="INTEGER"/>
|
||||
<result column="start_time" property="actualStartTime" jdbcType="TIMESTAMP"/>
|
||||
<result column="end_time" property="actualEndTime" jdbcType="TIMESTAMP"/>
|
||||
<result column="start_time" property="startTime" jdbcType="TIMESTAMP"/>
|
||||
<result column="end_time" property="endTime" jdbcType="TIMESTAMP"/>
|
||||
<result column="advance" property="advance" jdbcType="INTEGER"/>
|
||||
<result column="actual_start_time" property="actualStartTime" jdbcType="TIMESTAMP"/>
|
||||
<result column="actual_end_time" property="actualEndTime" jdbcType="TIMESTAMP"/>
|
||||
<result column="duration_seconds" property="durationSeconds" jdbcType="INTEGER"/>
|
||||
<result column="iframe_url" property="iframeUrl" jdbcType="VARCHAR"/>
|
||||
<result column="config" property="config" jdbcType="OTHER" typeHandler="com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler"/>
|
||||
@@ -59,29 +65,38 @@
|
||||
</resultMap>
|
||||
|
||||
<sql id="Base_Column_List">
|
||||
meeting_id, optsn, room_id, workcase_id, meeting_name, meeting_password, jwt_token,
|
||||
jitsi_room_name, jitsi_server_url, status, creator_id, creator_type, creator_name,
|
||||
participant_count, max_participants, start_time, end_time, duration_seconds, iframe_url,
|
||||
meeting_id, optsn, room_id, workcase_id, meeting_name, meeting_password, description, jwt_token,
|
||||
jitsi_room_name, jitsi_server_url, status, creator_type, creator_name,
|
||||
participant_count, max_participants, start_time, end_time, advance,
|
||||
actual_start_time, actual_end_time, duration_seconds, iframe_url,
|
||||
config, creator, create_time, update_time, delete_time, deleted
|
||||
</sql>
|
||||
|
||||
<insert id="insertVideoMeeting" parameterType="org.xyzh.api.workcase.dto.TbVideoMeetingDTO">
|
||||
INSERT INTO workcase.tb_video_meeting (
|
||||
optsn, meeting_id, room_id, workcase_id, meeting_name, jitsi_room_name, creator_id, creator_type, creator_name, creator
|
||||
optsn, meeting_id, room_id, workcase_id, meeting_name, jitsi_room_name, creator_type, creator_name, creator
|
||||
<if test="meetingPassword != null">, meeting_password</if>
|
||||
<if test="description != null">, description</if>
|
||||
<if test="jwtToken != null">, jwt_token</if>
|
||||
<if test="jitsiServerUrl != null">, jitsi_server_url</if>
|
||||
<if test="status != null">, status</if>
|
||||
<if test="maxParticipants != null">, max_participants</if>
|
||||
<if test="startTime != null">, start_time</if>
|
||||
<if test="endTime != null">, end_time</if>
|
||||
<if test="advance != null">, advance</if>
|
||||
<if test="iframeUrl != null">, iframe_url</if>
|
||||
<if test="config != null">, config</if>
|
||||
) VALUES (
|
||||
#{optsn}, #{meetingId}, #{roomId}, #{workcaseId}, #{meetingName}, #{jitsiRoomName}, #{creatorId}, #{creatorType}, #{creatorName}, #{creator}
|
||||
#{optsn}, #{meetingId}, #{roomId}, #{workcaseId}, #{meetingName}, #{jitsiRoomName}, #{creatorType}, #{creatorName}, #{creator}
|
||||
<if test="meetingPassword != null">, #{meetingPassword}</if>
|
||||
<if test="description != null">, #{description}</if>
|
||||
<if test="jwtToken != null">, #{jwtToken}</if>
|
||||
<if test="jitsiServerUrl != null">, #{jitsiServerUrl}</if>
|
||||
<if test="status != null">, #{status}</if>
|
||||
<if test="maxParticipants != null">, #{maxParticipants}</if>
|
||||
<if test="startTime != null">, #{startTime}</if>
|
||||
<if test="endTime != null">, #{endTime}</if>
|
||||
<if test="advance != null">, #{advance}</if>
|
||||
<if test="iframeUrl != null">, #{iframeUrl}</if>
|
||||
<if test="config != null">, #{config, typeHandler=com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler}</if>
|
||||
)
|
||||
@@ -92,11 +107,15 @@
|
||||
<set>
|
||||
<if test="meetingName != null and meetingName != ''">meeting_name = #{meetingName},</if>
|
||||
<if test="meetingPassword != null">meeting_password = #{meetingPassword},</if>
|
||||
<if test="description != null">description = #{description},</if>
|
||||
<if test="jwtToken != null">jwt_token = #{jwtToken},</if>
|
||||
<if test="status != null and status != ''">status = #{status},</if>
|
||||
<if test="participantCount != null">participant_count = #{participantCount},</if>
|
||||
<if test="actualStartTime != null">start_time = #{actualStartTime},</if>
|
||||
<if test="actualEndTime != null">end_time = #{actualEndTime},</if>
|
||||
<if test="startTime != null">start_time = #{startTime},</if>
|
||||
<if test="endTime != null">end_time = #{endTime},</if>
|
||||
<if test="advance != null">advance = #{advance},</if>
|
||||
<if test="actualStartTime != null">actual_start_time = #{actualStartTime},</if>
|
||||
<if test="actualEndTime != null">actual_end_time = #{actualEndTime},</if>
|
||||
<if test="durationSeconds != null">duration_seconds = #{durationSeconds},</if>
|
||||
<if test="iframeUrl != null">iframe_url = #{iframeUrl},</if>
|
||||
<if test="config != null">config = #{config, typeHandler=com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler},</if>
|
||||
@@ -126,7 +145,7 @@
|
||||
<if test="filter.workcaseId != null and filter.workcaseId != ''">AND workcase_id = #{filter.workcaseId}</if>
|
||||
<if test="filter.meetingName != null and filter.meetingName != ''">AND meeting_name LIKE CONCAT('%', #{filter.meetingName}, '%')</if>
|
||||
<if test="filter.status != null and filter.status != ''">AND status = #{filter.status}</if>
|
||||
<if test="filter.creatorId != null and filter.creatorId != ''">AND creator_id = #{filter.creatorId}</if>
|
||||
<if test="filter.creator != null and filter.creator != ''">AND creator = #{filter.creator}</if>
|
||||
<if test="filter.creatorType != null and filter.creatorType != ''">AND creator_type = #{filter.creatorType}</if>
|
||||
AND deleted = false
|
||||
</where>
|
||||
@@ -142,7 +161,7 @@
|
||||
<if test="filter.workcaseId != null and filter.workcaseId != ''">AND workcase_id = #{filter.workcaseId}</if>
|
||||
<if test="filter.meetingName != null and filter.meetingName != ''">AND meeting_name LIKE CONCAT('%', #{filter.meetingName}, '%')</if>
|
||||
<if test="filter.status != null and filter.status != ''">AND status = #{filter.status}</if>
|
||||
<if test="filter.creatorId != null and filter.creatorId != ''">AND creator_id = #{filter.creatorId}</if>
|
||||
<if test="filter.creator != null and filter.creator != ''">AND creator = #{filter.creator}</if>
|
||||
<if test="filter.creatorType != null and filter.creatorType != ''">AND creator_type = #{filter.creatorType}</if>
|
||||
AND deleted = false
|
||||
</where>
|
||||
@@ -159,7 +178,7 @@
|
||||
<if test="filter.workcaseId != null and filter.workcaseId != ''">AND workcase_id = #{filter.workcaseId}</if>
|
||||
<if test="filter.meetingName != null and filter.meetingName != ''">AND meeting_name LIKE CONCAT('%', #{filter.meetingName}, '%')</if>
|
||||
<if test="filter.status != null and filter.status != ''">AND status = #{filter.status}</if>
|
||||
<if test="filter.creatorId != null and filter.creatorId != ''">AND creator_id = #{filter.creatorId}</if>
|
||||
<if test="filter.creator != null and filter.creator != ''">AND creator = #{filter.creator}</if>
|
||||
<if test="filter.creatorType != null and filter.creatorType != ''">AND creator_type = #{filter.creatorType}</if>
|
||||
AND deleted = false
|
||||
</where>
|
||||
|
||||
@@ -154,7 +154,6 @@ jwt_token -- JWT Token(身份验证)
|
||||
jitsi_room_name -- Jitsi房间名(格式:workcase_{workcase_id}_{timestamp})
|
||||
jitsi_server_url -- Jitsi服务器地址(默认:https://meet.jit.si)
|
||||
status -- 状态:scheduled ongoing ended cancelled
|
||||
creator_id -- 创建者ID
|
||||
creator_type -- 创建者类型:guest-来客 agent-客服
|
||||
creator_name -- 创建者名称
|
||||
participant_count -- 参与人数
|
||||
|
||||
Reference in New Issue
Block a user