Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
Y
yd-communication
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
xingmin
yd-communication
Commits
ed85d9a7
Commit
ed85d9a7
authored
Aug 13, 2026
by
zhangxingmin
Browse files
Options
Browse Files
Download
Plain Diff
Merge remote-tracking branch 'origin/dev' into prod
parents
53cb6cfe
22b90e6a
Show whitespace changes
Inline
Side-by-side
Showing
72 changed files
with
4419 additions
and
86 deletions
+4419
-86
.idea/ProjectStorage.xml
+8
-0
.idea/jarRepositories.xml
+5
-0
yd-communication-api/Dockerfile
+12
-0
yd-communication-api/pom.xml
+0
-6
yd-communication-api/src/main/java/com/yd/communication/api/config/AliyunRtcConfig.java
+18
-0
yd-communication-api/src/main/java/com/yd/communication/api/config/AsyncConfig.java
+23
-0
yd-communication-api/src/main/java/com/yd/communication/api/config/RestTemplateConfig.java
+20
-1
yd-communication-api/src/main/java/com/yd/communication/api/config/WebSocketConfig.java
+14
-0
yd-communication-api/src/main/java/com/yd/communication/api/controller/ApiCoDesensitizationRuleController.java
+52
-0
yd-communication-api/src/main/java/com/yd/communication/api/controller/ApiCoSessionController.java
+82
-0
yd-communication-api/src/main/java/com/yd/communication/api/controller/ApiRecordingTaskController.java
+58
-0
yd-communication-api/src/main/java/com/yd/communication/api/controller/CoDesensitizationRuleController.java
+0
-18
yd-communication-api/src/main/java/com/yd/communication/api/controller/CoSessionController.java
+0
-18
yd-communication-api/src/main/java/com/yd/communication/api/controller/RecordingTaskController.java
+0
-18
yd-communication-api/src/main/java/com/yd/communication/api/record/AliyunRtcRecordService.java
+131
-0
yd-communication-api/src/main/java/com/yd/communication/api/service/ApiCoDesensitizationRuleService.java
+12
-0
yd-communication-api/src/main/java/com/yd/communication/api/service/ApiCoSessionService.java
+29
-0
yd-communication-api/src/main/java/com/yd/communication/api/service/ApiRecordingTaskService.java
+17
-0
yd-communication-api/src/main/java/com/yd/communication/api/service/impl/ApiCoDesensitizationRuleServiceImpl.java
+69
-0
yd-communication-api/src/main/java/com/yd/communication/api/service/impl/ApiCoSessionServiceImpl.java
+738
-0
yd-communication-api/src/main/java/com/yd/communication/api/service/impl/ApiRecordingTaskServiceImpl.java
+176
-0
yd-communication-api/src/main/java/com/yd/communication/api/utils/ClientInfoUtils.java
+84
-0
yd-communication-api/src/main/java/com/yd/communication/api/utils/IpContextHolder.java
+55
-0
yd-communication-api/src/main/java/com/yd/communication/api/utils/QRCodeUtils.java
+43
-0
yd-communication-api/src/main/java/com/yd/communication/api/websocket/CoWebSocketServer.java
+1077
-0
yd-communication-api/src/main/resources/banner.txt
+11
-0
yd-communication-api/src/main/resources/bootstrap.yml
+106
-0
yd-communication-api/src/main/resources/spy.properties
+24
-0
yd-communication-feign/pom.xml
+5
-0
yd-communication-feign/src/main/java/com/yd/communication/feign/client/ApiCoDesensitizationRuleFeignClient.java
+37
-0
yd-communication-feign/src/main/java/com/yd/communication/feign/client/ApiCoSessionFeignClient.java
+71
-0
yd-communication-feign/src/main/java/com/yd/communication/feign/client/ApiRecordingTaskFeignClient.java
+44
-0
yd-communication-feign/src/main/java/com/yd/communication/feign/constant/RedisConstants.java
+15
-0
yd-communication-feign/src/main/java/com/yd/communication/feign/dto/RoomRedisInfoDTO.java
+54
-0
yd-communication-feign/src/main/java/com/yd/communication/feign/enums/CoSessionStatusEnum.java
+32
-0
yd-communication-feign/src/main/java/com/yd/communication/feign/enums/ControlHolderTypeEnum.java
+30
-0
yd-communication-feign/src/main/java/com/yd/communication/feign/enums/RedisEnum.java
+44
-0
yd-communication-feign/src/main/java/com/yd/communication/feign/fallback/ApiCoDesensitizationRuleFeignFallbackFactory.java
+34
-0
yd-communication-feign/src/main/java/com/yd/communication/feign/fallback/ApiCoSessionFeignFallbackFactory.java
+52
-0
yd-communication-feign/src/main/java/com/yd/communication/feign/fallback/ApiRecordingTaskFeignFallbackFactory.java
+39
-0
yd-communication-feign/src/main/java/com/yd/communication/feign/request/CreateRequest.java
+68
-0
yd-communication-feign/src/main/java/com/yd/communication/feign/request/EndSessionRequest.java
+13
-0
yd-communication-feign/src/main/java/com/yd/communication/feign/request/GetStatusRequest.java
+15
-0
yd-communication-feign/src/main/java/com/yd/communication/feign/request/JoinRequest.java
+46
-0
yd-communication-feign/src/main/java/com/yd/communication/feign/request/TransferControlRequest.java
+23
-0
yd-communication-feign/src/main/java/com/yd/communication/feign/request/http/GenerateTokenRequest.java
+18
-0
yd-communication-feign/src/main/java/com/yd/communication/feign/request/recording/ApiStartRecordingRequest.java
+21
-0
yd-communication-feign/src/main/java/com/yd/communication/feign/request/recording/ApiStopRecordingRequest.java
+15
-0
yd-communication-feign/src/main/java/com/yd/communication/feign/response/CommonResponse.java
+21
-0
yd-communication-feign/src/main/java/com/yd/communication/feign/response/CreateResponse.java
+50
-0
yd-communication-feign/src/main/java/com/yd/communication/feign/response/GetStatusResponse.java
+12
-0
yd-communication-feign/src/main/java/com/yd/communication/feign/response/JoinResponse.java
+61
-0
yd-communication-feign/src/main/java/com/yd/communication/feign/response/SessionDetailResponse.java
+107
-0
yd-communication-feign/src/main/java/com/yd/communication/feign/response/desensitization/ApiCoDesensitizationRuleResponse.java
+67
-0
yd-communication-feign/src/main/java/com/yd/communication/feign/response/http/GenerateTokenResponse.java
+32
-0
yd-communication-feign/src/main/java/com/yd/communication/feign/response/recording/ApiQueryRecordingResponse.java
+158
-0
yd-communication-service/pom.xml
+30
-0
yd-communication-service/src/main/java/com/yd/communication/service/dao/CoSessionMapper.java
+14
-0
yd-communication-service/src/main/java/com/yd/communication/service/model/CoSession.java
+16
-3
yd-communication-service/src/main/java/com/yd/communication/service/model/RecordingTask.java
+3
-3
yd-communication-service/src/main/java/com/yd/communication/service/service/ICoDesensitizationRuleService.java
+26
-7
yd-communication-service/src/main/java/com/yd/communication/service/service/ICoOperationLogService.java
+3
-0
yd-communication-service/src/main/java/com/yd/communication/service/service/ICoSessionService.java
+12
-0
yd-communication-service/src/main/java/com/yd/communication/service/service/IRecordingTaskService.java
+3
-0
yd-communication-service/src/main/java/com/yd/communication/service/service/impl/CoDesensitizationRuleServiceImpl.java
+121
-11
yd-communication-service/src/main/java/com/yd/communication/service/service/impl/CoOperationLogServiceImpl.java
+24
-0
yd-communication-service/src/main/java/com/yd/communication/service/service/impl/CoSessionServiceImpl.java
+31
-1
yd-communication-service/src/main/java/com/yd/communication/service/service/impl/RecordingTaskServiceImpl.java
+72
-0
yd-communication-service/src/main/java/com/yd/communication/service/utils/RandomUtil.java
+16
-0
yd-communication-service/target/classes/com/yd/communication/service/config/CommunicationMybatisPlusConfig$1.class
+0
-0
yd-communication-service/target/classes/com/yd/communication/service/config/CommunicationMybatisPlusConfig.class
+0
-0
yd-communication-service/target/classes/com/yd/communication/service/utils/MyBatisPlusCodeGenerator.class
+0
-0
No files found.
.idea/ProjectStorage.xml
0 → 100644
View file @
ed85d9a7
<?xml version="1.0" encoding="UTF-8"?>
<project
version=
"4"
>
<component
name=
"ProjectStorage"
>
<option
name=
"path"
value=
"项目群组->银盾-微服务"
/>
<option
name=
"projectId"
value=
"4cc05386e898000"
/>
</component>
</project>
\ No newline at end of file
.idea/jarRepositories.xml
View file @
ed85d9a7
...
...
@@ -17,6 +17,11 @@
<option
name=
"url"
value=
"http://139.224.145.34:8081/repository/yd-maven-public/"
/>
</remote-repository>
<remote-repository>
<option
name=
"id"
value=
"aliyun-public"
/>
<option
name=
"name"
value=
"aliyun-public"
/>
<option
name=
"url"
value=
"http://139.224.145.34:8081/repository/yd-maven-public/"
/>
</remote-repository>
<remote-repository>
<option
name=
"id"
value=
"central"
/>
<option
name=
"name"
value=
"Maven Central repository"
/>
<option
name=
"url"
value=
"https://repo1.maven.org/maven2"
/>
...
...
yd-communication-api/Dockerfile
0 → 100644
View file @
ed85d9a7
# 基础镜像
FROM
openjdk:8
# 维护人
LABEL
maintainer="zxm<2060197959@qq.com>"
# 创建目录
RUN
mkdir
-p
/home/app
# 拷贝项目jar - 使用可执行的 fat JAR
COPY
target/yd-communication-api-1.0-SNAPSHOT-exec.jar /home/app/yd-communication-api.jar
# 执行命令启动jar,并设置JVM内存参数
ENTRYPOINT
["java","-Duser.timezone=Asia/Shanghai", "-Xmx256m", "-Xms128m", "-jar", "/home/app/yd-communication-api.jar"]
# 暴露端口
EXPOSE
9482
yd-communication-api/pom.xml
View file @
ed85d9a7
...
...
@@ -29,12 +29,6 @@
<artifactId>
spring-boot-starter-web
</artifactId>
</dependency>
<!-- Spring Boot Starter WebSocket -->
<dependency>
<groupId>
org.springframework.boot
</groupId>
<artifactId>
spring-boot-starter-websocket
</artifactId>
</dependency>
<dependency>
<groupId>
org.springframework.boot
</groupId>
<artifactId>
spring-boot-starter
</artifactId>
...
...
yd-communication-api/src/main/java/com/yd/communication/api/config/AliyunRtcConfig.java
0 → 100644
View file @
ed85d9a7
//package com.yd.communication.api.config;
//
//import lombok.Data;
//import org.springframework.boot.context.properties.ConfigurationProperties;
//import org.springframework.stereotype.Component;
//
//@Data
//@Component
//@ConfigurationProperties(prefix = "aliyun.rtc")
//public class AliyunRtcConfig {
// private String accessKeyId;
// private String accessKeySecret;
// private String regionId = "cn-shanghai";
// private String appId;
// private String ossBucket;
// private String ossEndpoint = "oss-cn-shanghai.aliyuncs.com";
//}
\ No newline at end of file
yd-communication-api/src/main/java/com/yd/communication/api/config/AsyncConfig.java
0 → 100644
View file @
ed85d9a7
package
com
.
yd
.
communication
.
api
.
config
;
import
org.springframework.context.annotation.Bean
;
import
org.springframework.context.annotation.Configuration
;
import
org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor
;
import
java.util.concurrent.Executor
;
@Configuration
public
class
AsyncConfig
{
@Bean
(
name
=
"communicationExecutor"
)
public
Executor
taskExecutor
()
{
ThreadPoolTaskExecutor
executor
=
new
ThreadPoolTaskExecutor
();
executor
.
setCorePoolSize
(
5
);
// 核心线程数
executor
.
setMaxPoolSize
(
10
);
// 最大线程数
executor
.
setQueueCapacity
(
100
);
// 队列容量
executor
.
setThreadNamePrefix
(
"communication-"
);
// 线程名前缀
executor
.
initialize
();
return
executor
;
}
}
\ No newline at end of file
yd-communication-api/src/main/java/com/yd/communication/api/config/RestTemplateConfig.java
View file @
ed85d9a7
package
com
.
yd
.
communication
.
api
.
config
;
import
lombok.extern.slf4j.Slf4j
;
import
org.springframework.context.annotation.Bean
;
import
org.springframework.context.annotation.Configuration
;
import
org.springframework.http.client.ClientHttpResponse
;
import
org.springframework.web.client.RestTemplate
;
import
java.nio.charset.StandardCharsets
;
import
java.util.Collections
;
@Slf4j
@Configuration
public
class
RestTemplateConfig
{
@Bean
public
RestTemplate
restTemplate
()
{
return
new
RestTemplate
();
RestTemplate
restTemplate
=
new
RestTemplate
();
restTemplate
.
setInterceptors
(
Collections
.
singletonList
((
request
,
body
,
execution
)
->
{
log
.
info
(
"【RestTemplate】请求URI: {}"
,
request
.
getURI
());
log
.
info
(
"【RestTemplate】请求方法: {}"
,
request
.
getMethod
());
log
.
info
(
"【RestTemplate】请求头: {}"
,
request
.
getHeaders
());
log
.
info
(
"【RestTemplate】请求体: {}"
,
new
String
(
body
,
StandardCharsets
.
UTF_8
));
ClientHttpResponse
response
=
execution
.
execute
(
request
,
body
);
log
.
info
(
"【RestTemplate】响应状态码: {}"
,
response
.
getStatusCode
());
// 如果状态码异常,打印响应体(注意只能读取一次,这里简单处理,读一次打印,但不影响后续)
// 这里不消费流,因为流可能后续还需要使用
return
response
;
}));
return
restTemplate
;
}
}
\ No newline at end of file
yd-communication-api/src/main/java/com/yd/communication/api/config/WebSocketConfig.java
0 → 100644
View file @
ed85d9a7
package
com
.
yd
.
communication
.
api
.
config
;
import
org.springframework.context.annotation.Bean
;
import
org.springframework.context.annotation.Configuration
;
import
org.springframework.web.socket.server.standard.ServerEndpointExporter
;
@Configuration
public
class
WebSocketConfig
{
@Bean
public
ServerEndpointExporter
serverEndpointExporter
()
{
return
new
ServerEndpointExporter
();
}
}
\ No newline at end of file
yd-communication-api/src/main/java/com/yd/communication/api/controller/ApiCoDesensitizationRuleController.java
0 → 100644
View file @
ed85d9a7
package
com
.
yd
.
communication
.
api
.
controller
;
import
com.yd.common.result.Result
;
import
com.yd.communication.api.service.ApiCoDesensitizationRuleService
;
import
com.yd.communication.feign.client.ApiCoDesensitizationRuleFeignClient
;
import
com.yd.communication.feign.response.desensitization.ApiCoDesensitizationRuleResponse
;
import
lombok.extern.slf4j.Slf4j
;
import
org.springframework.validation.annotation.Validated
;
import
org.springframework.web.bind.annotation.*
;
import
javax.annotation.Resource
;
import
javax.servlet.http.HttpServletRequest
;
import
java.util.List
;
/**
* 脱敏信息
*
* @author zxm
* @since 2026-07-28
*/
@Slf4j
@RestController
@RequestMapping
(
"/desensitization"
)
@Validated
public
class
ApiCoDesensitizationRuleController
implements
ApiCoDesensitizationRuleFeignClient
{
@Resource
private
ApiCoDesensitizationRuleService
apiCoDesensitizationRuleService
;
/**
* 根据 ID 查询单条规则
* @param bizId
* @return
*/
@Override
public
Result
<
ApiCoDesensitizationRuleResponse
>
getByBizId
(
String
bizId
)
{
return
apiCoDesensitizationRuleService
.
getByBizId
(
bizId
);
}
/**
* 根据资源类型和资源ID获取生效的脱敏规则列表(供脱敏引擎调用)
* @param resourceType
* @param resourceId
* @return
*/
@Override
public
Result
<
List
<
ApiCoDesensitizationRuleResponse
>>
getEnabledRules
(
String
resourceType
,
String
resourceId
)
{
return
apiCoDesensitizationRuleService
.
getEnabledRules
(
resourceType
,
resourceId
);
}
}
\ No newline at end of file
yd-communication-api/src/main/java/com/yd/communication/api/controller/ApiCoSessionController.java
0 → 100644
View file @
ed85d9a7
package
com
.
yd
.
communication
.
api
.
controller
;
import
com.yd.common.result.Result
;
import
com.yd.communication.api.service.ApiCoSessionService
;
import
com.yd.communication.feign.client.ApiCoSessionFeignClient
;
import
com.yd.communication.feign.request.*
;
import
com.yd.communication.feign.response.*
;
import
org.springframework.validation.annotation.Validated
;
import
org.springframework.web.bind.annotation.*
;
import
javax.annotation.Resource
;
/**
* 会话信息
*
* @author zxm
* @since 2026-07-28
*/
@RestController
@RequestMapping
(
"/coSession"
)
@Validated
public
class
ApiCoSessionController
implements
ApiCoSessionFeignClient
{
@Resource
private
ApiCoSessionService
apiCoSessionService
;
/**
* 创建会话
* @param request 创建会话请求
* @return
*/
@Override
public
Result
<
CreateResponse
>
create
(
CreateRequest
request
)
{
return
apiCoSessionService
.
create
(
request
);
}
/**
* 加入会话
* @param request
* @return
*/
public
Result
<
JoinResponse
>
join
(
JoinRequest
request
)
{
return
apiCoSessionService
.
join
(
request
);
}
/**
* 获取会话状态
* @param request
* @return
*/
@Override
public
Result
<
GetStatusResponse
>
getStatus
(
GetStatusRequest
request
)
{
return
apiCoSessionService
.
getStatus
(
request
);
}
/**
* 获取会话详情
* @param bizId
* @return
*/
public
Result
<
SessionDetailResponse
>
get
(
String
bizId
)
{
return
apiCoSessionService
.
get
(
bizId
);
}
/**
* 结束协同会话(关闭共享,仅客户可调用)
* @return
*/
public
Result
<
CommonResponse
>
end
(
EndSessionRequest
request
)
{
return
apiCoSessionService
.
end
(
request
);
}
/**
* 切换控制权(仅参与者(顾问)可调用)
* @param request
* @return
*/
public
Result
<
CommonResponse
>
transferControl
(
TransferControlRequest
request
)
{
return
apiCoSessionService
.
transferControl
(
request
);
}
}
yd-communication-api/src/main/java/com/yd/communication/api/controller/ApiRecordingTaskController.java
0 → 100644
View file @
ed85d9a7
package
com
.
yd
.
communication
.
api
.
controller
;
import
com.yd.common.result.Result
;
import
com.yd.communication.api.service.ApiRecordingTaskService
;
import
com.yd.communication.feign.client.ApiRecordingTaskFeignClient
;
import
com.yd.communication.feign.request.recording.ApiStartRecordingRequest
;
import
com.yd.communication.feign.request.recording.ApiStopRecordingRequest
;
import
com.yd.communication.feign.response.recording.ApiQueryRecordingResponse
;
import
com.yd.communication.service.service.IRecordingTaskService
;
import
lombok.extern.slf4j.Slf4j
;
import
org.springframework.web.bind.annotation.*
;
import
org.springframework.web.multipart.MultipartFile
;
import
javax.annotation.Resource
;
import
java.util.Map
;
/**
* 录制信息
*
* @author zxm
* @since 2026-07-28
*/
@Slf4j
@RestController
@RequestMapping
(
"/recordingTask"
)
public
class
ApiRecordingTaskController
implements
ApiRecordingTaskFeignClient
{
@Resource
private
ApiRecordingTaskService
apiRecordingTaskService
;
/**
* 开始录制
* @param request
* @return
*/
public
Result
<
String
>
startRecording
(
ApiStartRecordingRequest
request
)
{
return
apiRecordingTaskService
.
startRecording
(
request
);
}
/**
* 停止录制并上传视频
* @param taskId 录制任务ID
* @return
*/
public
Result
<
Map
<
String
,
String
>>
stopRecording
(
String
taskId
)
{
return
apiRecordingTaskService
.
stopRecording
(
taskId
);
}
/**
* 查询录制信息
* @param taskId
* @return
*/
public
Result
<
ApiQueryRecordingResponse
>
queryRecording
(
String
taskId
)
{
return
apiRecordingTaskService
.
queryRecording
(
taskId
);
}
}
yd-communication-api/src/main/java/com/yd/communication/api/controller/CoDesensitizationRuleController.java
deleted
100644 → 0
View file @
53cb6cfe
package
com
.
yd
.
communication
.
api
.
controller
;
import
org.springframework.web.bind.annotation.RequestMapping
;
import
org.springframework.web.bind.annotation.RestController
;
/**
* <p>
* 协同-脱敏设置表(通用) 前端控制器
* </p>
*
* @author zxm
* @since 2026-07-28
*/
@RestController
@RequestMapping
(
"/coDesensitizationRule"
)
public
class
CoDesensitizationRuleController
{
}
yd-communication-api/src/main/java/com/yd/communication/api/controller/CoSessionController.java
deleted
100644 → 0
View file @
53cb6cfe
package
com
.
yd
.
communication
.
api
.
controller
;
import
org.springframework.web.bind.annotation.RequestMapping
;
import
org.springframework.web.bind.annotation.RestController
;
/**
* <p>
* 协同-会话表(通用) 前端控制器
* </p>
*
* @author zxm
* @since 2026-07-28
*/
@RestController
@RequestMapping
(
"/coSession"
)
public
class
CoSessionController
{
}
yd-communication-api/src/main/java/com/yd/communication/api/controller/RecordingTaskController.java
deleted
100644 → 0
View file @
53cb6cfe
package
com
.
yd
.
communication
.
api
.
controller
;
import
org.springframework.web.bind.annotation.RequestMapping
;
import
org.springframework.web.bind.annotation.RestController
;
/**
* <p>
* 录制任务表(通用) 前端控制器
* </p>
*
* @author zxm
* @since 2026-07-28
*/
@RestController
@RequestMapping
(
"/recordingTask"
)
public
class
RecordingTaskController
{
}
yd-communication-api/src/main/java/com/yd/communication/api/record/AliyunRtcRecordService.java
0 → 100644
View file @
ed85d9a7
//package com.yd.communication.api.record;
//
//import com.alibaba.fastjson2.JSONObject;
//import com.aliyuncs.CommonRequest;
//import com.aliyuncs.CommonResponse;
//import com.aliyuncs.DefaultAcsClient;
//import com.aliyuncs.IAcsClient;
//import com.aliyuncs.http.MethodType;
//import com.aliyuncs.live.model.v20161101.StopRtcCloudRecordingRequest;
//import com.aliyuncs.live.model.v20161101.StopRtcCloudRecordingResponse;
//import com.aliyuncs.profile.DefaultProfile;
//import com.yd.communication.api.config.AliyunRtcConfig;
//import lombok.extern.slf4j.Slf4j;
//import org.springframework.stereotype.Service;
//import javax.annotation.Resource;
//import java.util.HashMap;
//import java.util.Map;
//
//@Service
//@Slf4j
//public class AliyunRtcRecordService {
//
// @Resource
// private AliyunRtcConfig rtcConfig;
//
// private IAcsClient getClient() {
// DefaultProfile profile = DefaultProfile.getProfile(
// rtcConfig.getRegionId(),
// rtcConfig.getAccessKeyId(),
// rtcConfig.getAccessKeySecret()
// );
// return new DefaultAcsClient(profile);
// }
//
// public String startRecording(String channelId) {
// try {
// IAcsClient client = getClient();
// CommonRequest request = new CommonRequest();
// request.setSysMethod(MethodType.POST);
// request.setSysDomain("live.aliyuncs.com");
// request.setSysVersion("2016-11-01");
// request.setSysAction("StartRtcCloudRecording");
//
// request.putQueryParameter("AppId", rtcConfig.getAppId());
// request.putQueryParameter("ChannelId", channelId);
// // 订阅所有用户(不传订阅参数则默认订阅所有)
// request.putQueryParameter("RecordParams.RecordMode", "1");
// request.putQueryParameter("StorageParams.StorageType", "1");
// request.putQueryParameter("StorageParams.OSSParams.OSSEndpoint", rtcConfig.getOssEndpoint());
// request.putQueryParameter("StorageParams.OSSParams.OSSBucket", rtcConfig.getOssBucket());
//
// CommonResponse response = client.getCommonResponse(request);
// String responseData = response.getData();
// JSONObject jsonObject = JSONObject.parseObject(responseData);
// JSONObject body = jsonObject.getJSONObject("Body");
// String taskId = body.getString("TaskId");
// if (taskId == null || taskId.isEmpty()) {
// throw new BusinessException("启动录制失败,未返回 TaskId,响应:" + responseData);
// }
// log.info("启动录制成功, TaskId: {}", taskId);
// return taskId;
// } catch (Exception e) {
// log.error("启动录制失败", e);
// throw new BusinessException("启动云端录制失败: " + e.getMessage(), e);
// }
// }
//
// public void stopRecording(String taskId) {
// try {
// IAcsClient client = getClient();
// StopRtcCloudRecordingRequest request = new StopRtcCloudRecordingRequest();
// request.setTaskId(taskId);
// StopRtcCloudRecordingResponse response = client.getAcsResponse(request);
// log.info("停止录制成功, TaskId: {}", taskId);
// } catch (Exception e) {
// log.error("停止录制失败", e);
// throw new BusinessException("停止云端录制失败: " + e.getMessage(), e);
// }
// }
//
// /**
// * 查询录制任务详情(文件信息)
// * @param taskId
// * @return
// */
// public Map<String, String> queryRecordingInfo(String taskId) {
// try {
// IAcsClient client = getClient();
// CommonRequest request = new CommonRequest();
// request.setSysMethod(MethodType.POST);
// request.setSysDomain("live.aliyuncs.com");
// request.setSysVersion("2016-11-01");
// request.setSysAction("DescribeCloudRecording");
// request.putQueryParameter("TaskId", taskId);
//
// CommonResponse response = client.getCommonResponse(request);
// String responseData = response.getData();
// log.info("查询录制信息响应: {}", responseData);
//
// JSONObject jsonObject = JSONObject.parseObject(responseData);
// JSONObject body = jsonObject.getJSONObject("Body");
// if (body == null) {
// return new HashMap<>();
// }
//
// Map<String, String> info = new HashMap<>();
// // 根据实际返回字段解析,常见字段名
// if (body.containsKey("FileUrl")) {
// info.put("fileUrl", body.getString("FileUrl"));
// } else if (body.containsKey("fileUrl")) {
// info.put("fileUrl", body.getString("fileUrl"));
// }
// if (body.containsKey("Duration")) {
// info.put("duration", body.getString("Duration"));
// } else if (body.containsKey("duration")) {
// info.put("duration", body.getString("duration"));
// }
// if (body.containsKey("FileSize")) {
// info.put("size", body.getString("FileSize"));
// } else if (body.containsKey("fileSize")) {
// info.put("size", body.getString("fileSize"));
// }
// // 可能还有其他字段,如 RecordingStartTime, RecordingEndTime 等
// return info;
// } catch (Exception e) {
// log.error("查询录制信息失败", e);
// throw new BusinessException("查询录制信息失败: " + e.getMessage(), e);
// }
// }
//}
\ No newline at end of file
yd-communication-api/src/main/java/com/yd/communication/api/service/ApiCoDesensitizationRuleService.java
0 → 100644
View file @
ed85d9a7
package
com
.
yd
.
communication
.
api
.
service
;
import
com.yd.common.result.Result
;
import
com.yd.communication.feign.response.desensitization.ApiCoDesensitizationRuleResponse
;
import
java.util.List
;
public
interface
ApiCoDesensitizationRuleService
{
Result
<
ApiCoDesensitizationRuleResponse
>
getByBizId
(
String
bizId
);
Result
<
List
<
ApiCoDesensitizationRuleResponse
>>
getEnabledRules
(
String
resourceType
,
String
resourceId
);
}
yd-communication-api/src/main/java/com/yd/communication/api/service/ApiCoSessionService.java
0 → 100644
View file @
ed85d9a7
package
com
.
yd
.
communication
.
api
.
service
;
import
com.yd.common.result.Result
;
import
com.yd.communication.feign.dto.RoomRedisInfoDTO
;
import
com.yd.communication.feign.request.*
;
import
com.yd.communication.feign.response.*
;
import
com.yd.communication.service.model.CoSession
;
public
interface
ApiCoSessionService
{
Result
<
CreateResponse
>
create
(
CreateRequest
request
);
Result
<
JoinResponse
>
join
(
JoinRequest
request
);
Result
<
GetStatusResponse
>
getStatus
(
GetStatusRequest
request
);
Result
<
SessionDetailResponse
>
get
(
String
bizId
);
Result
<
CommonResponse
>
end
(
EndSessionRequest
request
);
Result
<
CommonResponse
>
transferControl
(
TransferControlRequest
request
);
RoomRedisInfoDTO
getCurrentController
(
String
roomId
);
void
updateCurrentPage
(
String
roomId
,
String
newPageJson
,
String
operatorId
);
CoSession
getByRoomId
(
String
roomId
);
}
yd-communication-api/src/main/java/com/yd/communication/api/service/ApiRecordingTaskService.java
0 → 100644
View file @
ed85d9a7
package
com
.
yd
.
communication
.
api
.
service
;
import
com.yd.common.result.Result
;
import
com.yd.communication.feign.request.recording.ApiStartRecordingRequest
;
import
com.yd.communication.feign.response.recording.ApiQueryRecordingResponse
;
import
java.util.Map
;
public
interface
ApiRecordingTaskService
{
Result
<
String
>
startRecording
(
ApiStartRecordingRequest
request
);
Result
<
Map
<
String
,
String
>>
stopRecording
(
String
taskId
);
Result
<
ApiQueryRecordingResponse
>
queryRecording
(
String
taskId
);
}
yd-communication-api/src/main/java/com/yd/communication/api/service/impl/ApiCoDesensitizationRuleServiceImpl.java
0 → 100644
View file @
ed85d9a7
package
com
.
yd
.
communication
.
api
.
service
.
impl
;
import
com.alibaba.fastjson2.JSON
;
import
com.yd.common.exception.BusinessException
;
import
com.yd.common.result.Result
;
import
com.yd.communication.api.service.ApiCoDesensitizationRuleService
;
import
com.yd.communication.feign.response.desensitization.ApiCoDesensitizationRuleResponse
;
import
com.yd.communication.service.model.CoDesensitizationRule
;
import
com.yd.communication.service.service.ICoDesensitizationRuleService
;
import
lombok.extern.slf4j.Slf4j
;
import
org.apache.commons.collections.CollectionUtils
;
import
org.springframework.beans.BeanUtils
;
import
org.springframework.stereotype.Service
;
import
javax.annotation.Resource
;
import
java.util.List
;
import
java.util.stream.Collectors
;
@Slf4j
@Service
public
class
ApiCoDesensitizationRuleServiceImpl
implements
ApiCoDesensitizationRuleService
{
@Resource
private
ICoDesensitizationRuleService
ruleService
;
/**
* 根据 ID 查询单条规则
* @param bizId
* @return
*/
@Override
public
Result
<
ApiCoDesensitizationRuleResponse
>
getByBizId
(
String
bizId
)
{
CoDesensitizationRule
rule
=
ruleService
.
lambdaQuery
()
.
eq
(
CoDesensitizationRule:
:
getRuleBizId
,
bizId
)
.
last
(
" limit 1 "
)
.
one
();
if
(
rule
==
null
)
{
throw
new
BusinessException
(
"脱敏信息不存在"
);
}
ApiCoDesensitizationRuleResponse
response
=
new
ApiCoDesensitizationRuleResponse
();
BeanUtils
.
copyProperties
(
rule
,
response
);
return
Result
.
success
(
response
);
}
/**
* 根据资源类型和资源ID获取生效的脱敏规则列表(供脱敏引擎调用)
* @param resourceType
* @param resourceId
* @return
*/
@Override
public
Result
<
List
<
ApiCoDesensitizationRuleResponse
>>
getEnabledRules
(
String
resourceType
,
String
resourceId
)
{
log
.
info
(
"【根据资源类型和资源ID获取生效的脱敏规则列表】入参值, resourceType={}"
,
resourceType
);
log
.
info
(
"【根据资源类型和资源ID获取生效的脱敏规则列表】入参值, resourceId={}"
,
resourceId
);
List
<
CoDesensitizationRule
>
rules
=
ruleService
.
getEnabledRulesByResource
(
resourceType
,
resourceId
);
log
.
info
(
"【根据资源类型和资源ID获取生效的脱敏规则列表】查询列表, rules={}"
,
JSON
.
toJSONString
(
rules
));
if
(
CollectionUtils
.
isEmpty
(
rules
))
{
return
Result
.
success
();
}
List
<
ApiCoDesensitizationRuleResponse
>
responses
=
rules
.
stream
().
map
(
dto
->
{
ApiCoDesensitizationRuleResponse
response
=
new
ApiCoDesensitizationRuleResponse
();
BeanUtils
.
copyProperties
(
dto
,
response
);
return
response
;
}).
collect
(
Collectors
.
toList
());
return
Result
.
success
(
responses
);
}
}
yd-communication-api/src/main/java/com/yd/communication/api/service/impl/ApiCoSessionServiceImpl.java
0 → 100644
View file @
ed85d9a7
package
com
.
yd
.
communication
.
api
.
service
.
impl
;
import
com.alibaba.fastjson2.JSONObject
;
import
com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper
;
import
com.yd.common.enums.CommonEnum
;
import
com.yd.common.enums.ResultCode
;
import
com.yd.common.exception.BusinessException
;
import
com.yd.common.result.JsonResult
;
import
com.yd.common.result.Result
;
import
com.yd.common.utils.ByteArrayMultipartFile
;
import
com.yd.common.utils.RandomStringGenerator
;
import
com.yd.common.utils.RedisUtil
;
import
com.yd.communication.api.service.ApiCoSessionService
;
import
com.yd.communication.api.utils.ClientInfoUtils
;
import
com.yd.communication.api.utils.QRCodeUtils
;
import
com.yd.communication.feign.dto.RoomRedisInfoDTO
;
import
com.yd.communication.feign.enums.CoSessionStatusEnum
;
import
com.yd.communication.feign.enums.ControlHolderTypeEnum
;
import
com.yd.communication.feign.enums.RedisEnum
;
import
com.yd.communication.feign.request.*
;
import
com.yd.communication.feign.request.http.GenerateTokenRequest
;
import
com.yd.communication.feign.response.*
;
import
com.yd.communication.feign.response.http.GenerateTokenResponse
;
import
com.yd.communication.service.model.CoSession
;
import
com.yd.communication.service.service.ICoOperationLogService
;
import
com.yd.communication.service.service.ICoSessionService
;
import
com.yd.communication.service.service.IRecordingTaskService
;
import
com.yd.communication.service.utils.RandomUtil
;
import
com.yd.oss.feign.client.ApiOssFeignClient
;
import
com.yd.oss.feign.request.ApiUploadFileRequest
;
import
com.yd.oss.feign.response.ApiUploadResponse
;
import
lombok.extern.slf4j.Slf4j
;
import
org.apache.commons.lang3.StringUtils
;
import
org.springframework.beans.BeanUtils
;
import
org.springframework.beans.factory.annotation.Value
;
import
org.springframework.http.HttpEntity
;
import
org.springframework.http.HttpHeaders
;
import
org.springframework.http.MediaType
;
import
org.springframework.http.ResponseEntity
;
import
org.springframework.stereotype.Service
;
import
org.springframework.transaction.annotation.Transactional
;
import
org.springframework.web.client.RestTemplate
;
import
org.springframework.web.multipart.MultipartFile
;
import
javax.annotation.Resource
;
import
java.time.LocalDateTime
;
import
java.util.Date
;
import
java.util.UUID
;
@Slf4j
@Service
public
class
ApiCoSessionServiceImpl
implements
ApiCoSessionService
{
@Resource
private
ICoSessionService
iCoSessionService
;
@Resource
private
IRecordingTaskService
recordingService
;
@Resource
private
ICoOperationLogService
operationLogService
;
@Resource
private
RedisUtil
redisUtil
;
@Resource
private
ApiOssFeignClient
apiOssFeignClient
;
@Value
(
"${co.room.base-url:https://mdev.zuihuibi.cn/repotKYC/#/pages/launch/index?visitType=1&targetPage=showcasePage}"
)
private
String
roomBaseUrl
;
@Resource
private
RestTemplate
restTemplate
;
@Value
(
"${sfp.api.base-url:https://mdev.anjibao.cn/sfpApi}"
)
private
String
sfpApiBaseUrl
;
/**
* 客户创建会话
* @param scope
* @param resourceType
* @param resourceId
* @param resourceInit
* @param ownerId
* @param ownerType
* @param userId
* @return
*/
@Transactional
(
rollbackFor
=
Exception
.
class
)
public
CoSession
createSession
(
String
scope
,
String
resourceType
,
String
resourceId
,
String
resourceInit
,
String
ownerId
,
String
ownerType
,
String
userId
)
{
log
.
info
(
"【创建会话-内部】开始创建, scope={}, resourceType={}, resourceId={}, ownerId={}, ownerType={}, userId={}"
,
scope
,
resourceType
,
resourceId
,
ownerId
,
ownerType
,
userId
);
try
{
// 创建会话
CoSession
session
=
new
CoSession
();
session
.
setCoSessionBizId
(
RandomStringGenerator
.
generateBizId16
(
CommonEnum
.
UID_TYPE_CO_SESSION
.
getCode
()));
session
.
setCoSessionNo
(
"S"
+
System
.
currentTimeMillis
());
session
.
setScope
(
scope
);
session
.
setResourceType
(
resourceType
);
session
.
setResourceId
(
resourceId
);
session
.
setResourceInit
(
resourceInit
);
session
.
setOwnerId
(
ownerId
);
session
.
setOwnerType
(
ownerType
);
// 房间号
String
roomId
=
"room_"
+
UUID
.
randomUUID
().
toString
().
substring
(
0
,
8
);
String
roomPwd
=
RandomUtil
.
generateNumericCode
(
6
);
session
.
setRoomId
(
roomId
);
session
.
setRoomPwd
(
roomPwd
);
session
.
setChannelPrefix
(
"co"
);
// 初始化控制权为所有者(客户)
session
.
setControlHolderType
(
ControlHolderTypeEnum
.
OWNER
.
getItemValue
());
session
.
setControlHolderId
(
ownerId
);
// 待开始状态
session
.
setStatus
(
CoSessionStatusEnum
.
DKS
.
getItemValue
());
session
.
setCurrentPage
(
resourceInit
);
session
.
setPageHistory
(
"["
+
resourceInit
+
"]"
);
session
.
setCreatorId
(
ownerId
);
session
.
setUpdaterId
(
ownerId
);
iCoSessionService
.
save
(
session
);
log
.
info
(
"【创建会话-内部】数据库保存成功, id={}"
,
session
.
getId
());
// 获取 Token(调用 /user/generate/token)
String
token
=
null
;
GenerateTokenResponse
tokenResp
=
null
;
try
{
// 构建请求体
GenerateTokenRequest
tokenRequest
=
new
GenerateTokenRequest
();
// 注意:userId 是 String,转为 Long。若 userId 可能为空,需做判空处理
if
(
StringUtils
.
isNotBlank
(
userId
))
{
tokenRequest
.
setSfpUserId
(
Long
.
valueOf
(
userId
));
}
else
{
// 如果 userId 为空,可以尝试用 ownerId 或抛出业务异常,根据业务定
throw
new
BusinessException
(
"userId 不能为空,无法生成 Token"
);
}
// 发起 POST 请求
String
url
=
sfpApiBaseUrl
+
"/user/generate/token"
;
HttpHeaders
headers
=
new
HttpHeaders
();
headers
.
setContentType
(
MediaType
.
APPLICATION_JSON
);
HttpEntity
<
GenerateTokenRequest
>
entity
=
new
HttpEntity
<>(
tokenRequest
,
headers
);
log
.
info
(
"【Token生成】sfpApiBaseUrl = {}"
,
sfpApiBaseUrl
);
log
.
info
(
"【Token生成】完整URL = {}"
,
sfpApiBaseUrl
+
"/user/generate/token"
);
log
.
info
(
"【Token生成】请求体GenerateTokenRequest = {}"
,
JSONObject
.
toJSONString
(
tokenRequest
));
log
.
info
(
"【Token生成】请求体entity = {}"
,
JSONObject
.
toJSONString
(
entity
));
ResponseEntity
<
JsonResult
>
responseEntity
=
restTemplate
.
postForEntity
(
url
,
entity
,
JsonResult
.
class
);
log
.
info
(
"【Token生成】返回体responseEntity = {}"
,
JSONObject
.
toJSONString
(
responseEntity
));
if
(
responseEntity
.
getStatusCode
().
is2xxSuccessful
())
{
JsonResult
body
=
responseEntity
.
getBody
();
if
(
body
!=
null
&&
body
.
isSuccess
())
{
// 假设 body.getData() 返回的是 LinkedHashMap,需要转为 GenerateTokenResponse
// 或者直接使用 fastjson 转换
Object
data
=
body
.
getData
();
if
(
data
!=
null
)
{
tokenResp
=
JSONObject
.
parseObject
(
JSONObject
.
toJSONString
(
data
),
GenerateTokenResponse
.
class
);
token
=
tokenResp
.
getToken
();
}
}
}
if
(
StringUtils
.
isBlank
(
token
))
{
log
.
error
(
"【创建会话】获取Token失败,userId={}"
,
userId
);
throw
new
BusinessException
(
"生成会话 Token 失败,请稍后重试"
);
}
log
.
info
(
"【创建会话】获取Token成功,token前缀={}"
,
token
.
substring
(
0
,
Math
.
min
(
10
,
token
.
length
())));
}
catch
(
Exception
e
)
{
log
.
error
(
"【创建会话】调用生成Token接口异常"
,
e
);
throw
new
BusinessException
(
"生成会话 Token 异常:"
+
e
.
getMessage
());
}
// 存入 Redis
RoomRedisInfoDTO
roomRedisInfoDTO
=
new
RoomRedisInfoDTO
();
if
(
tokenResp
!=
null
)
{
roomRedisInfoDTO
.
setName
(
tokenResp
.
getName
());
roomRedisInfoDTO
.
setMobile
(
tokenResp
.
getMobile
());
}
roomRedisInfoDTO
.
setToken
(
token
);
// 设置 token
roomRedisInfoDTO
.
setUserId
(
userId
);
roomRedisInfoDTO
.
setControlHolderType
(
session
.
getControlHolderType
());
roomRedisInfoDTO
.
setControlHolderId
(
session
.
getControlHolderId
());
roomRedisInfoDTO
.
setRoomId
(
roomId
);
roomRedisInfoDTO
.
setRoomPwd
(
roomPwd
);
redisUtil
.
setCacheObject
(
RedisEnum
.
ROOM
.
getPrefix
()
+
roomId
+
":"
+
roomPwd
,
roomRedisInfoDTO
,
RedisEnum
.
ROOM
.
getTimeout
(),
RedisEnum
.
ROOM
.
getTimeUnit
());
log
.
info
(
"【创建会话-内部】Redis缓存已设置, key={}"
,
RedisEnum
.
ROOM
.
getPrefix
()
+
roomId
);
// 自动开启录制(当前注释)
if
(
autoStartRecording
())
{
// recordingService.startRecording(session.getCoSessionBizId(), roomId);
log
.
info
(
"【创建会话-内部】自动录制已触发(未实际启动)"
);
}
log
.
info
(
"【创建会话-内部】创建成功, roomId={}, roomPwd={}"
,
roomId
,
roomPwd
);
//生成协同操作日志
operationLogService
.
log
(
session
.
getCoSessionBizId
(),
ownerId
,
ownerType
,
ownerId
,
"create_session"
,
String
.
format
(
"创建会话 roomId=%s, roomPwd=%s"
,
session
.
getRoomId
(),
session
.
getRoomPwd
()),
ClientInfoUtils
.
getDevice
(),
ClientInfoUtils
.
getClientIp
()
);
return
session
;
}
catch
(
Exception
e
)
{
log
.
error
(
"【创建会话-内部】异常"
,
e
);
throw
e
;
}
}
/**
* 创建会话
* @param request
* @return
*/
@Override
@Transactional
(
rollbackFor
=
Exception
.
class
)
public
Result
<
CreateResponse
>
create
(
CreateRequest
request
)
{
log
.
info
(
"【创建会话】收到创建请求, request={}"
,
request
);
try
{
// 1. 创建会话
CoSession
session
=
createSession
(
request
.
getScope
(),
request
.
getResourceType
(),
request
.
getResourceId
(),
request
.
getResourceInit
(),
request
.
getOwnerId
(),
request
.
getOwnerType
(),
request
.
getUserId
()
);
if
(
session
==
null
)
{
log
.
warn
(
"【创建会话-生成二维码】创建会话失败"
);
return
Result
.
success
();
}
// 2. 构建房间链接(从配置读取)
String
roomLink
=
roomBaseUrl
+
"&roomId="
+
session
.
getRoomId
()
+
"&pwd="
+
session
.
getRoomPwd
();
log
.
info
(
"【创建会话】生成房间链接: {}"
,
roomLink
);
// 3. 生成二维码图片字节数组(PNG格式)
byte
[]
qrCodeBytes
=
QRCodeUtils
.
generateQRCode
(
roomLink
,
300
,
300
);
// 4. 将字节数组转换为 MultipartFile
String
fileName
=
session
.
getCoSessionBizId
()
+
".png"
;
MultipartFile
multipartFile
=
new
ByteArrayMultipartFile
(
qrCodeBytes
,
// 图片字节数组
"file"
,
// 字段名,与 Feign @RequestPart("file") 匹配
fileName
,
// 原始文件名
"image/png"
// 内容类型
);
// 5. 构建 OSS 上传请求参数
ApiUploadFileRequest
ossRequest
=
new
ApiUploadFileRequest
();
ossRequest
.
setObjectBizId
(
session
.
getCoSessionBizId
());
// 必填,使用会话业务ID
ossRequest
.
setObjectType
(
"co_session_qr"
);
// 自定义对象类型
ossRequest
.
setObjectName
(
"协同会话二维码"
);
ossRequest
.
setProjectBizId
(
""
);
ossRequest
.
setTenantBizId
(
""
);
String
requestJson
=
JSONObject
.
toJSONString
(
ossRequest
);
// 6. 调用 Feign 上传
Result
<
ApiUploadResponse
>
uploadResult
=
apiOssFeignClient
.
uploadFileBodyWithJson
(
multipartFile
,
requestJson
);
if
(
uploadResult
==
null
||
uploadResult
.
getCode
()
!=
200
||
uploadResult
.
getData
()
==
null
)
{
log
.
error
(
"【创建会话】OSS上传失败,result={}"
,
uploadResult
);
throw
new
BusinessException
(
"二维码上传失败:"
+
(
uploadResult
!=
null
?
uploadResult
.
getMsg
()
:
"未知错误"
));
}
// 7. 获取文件访问 URL(假设 ApiUploadResponse 包含 url 字段)
ApiUploadResponse
uploadData
=
uploadResult
.
getData
();
String
roomQrCodeUrl
=
uploadData
.
getUrl
();
// 若字段名为 fileUrl 则对应调整
log
.
info
(
"【创建会话】OSS上传成功,文件URL: {}"
,
roomQrCodeUrl
);
//8. 更新会话信息表数据
session
.
setRoomQrCode
(
roomQrCodeUrl
);
session
.
setRoomLink
(
roomLink
);
iCoSessionService
.
saveOrUpdate
(
session
);
//设置房间链接失效时间(用于扫码和链接直接访问,加载页面后调用校验接口判断是否失效)
//设置房间链接失效时间(用于扫码和链接直接访问)
redisUtil
.
setCacheObject
(
RedisEnum
.
ROOM_LINK
.
getPrefix
()
+
session
.
getRoomId
()
+
":"
+
session
.
getRoomPwd
(),
session
.
getRoomLink
(),
RedisEnum
.
ROOM_LINK
.
getTimeout
(),
RedisEnum
.
ROOM_LINK
.
getTimeUnit
());
// 计算链接失效时间点(当前时间 + 缓存过期时长)
long
expireMillis
=
RedisEnum
.
ROOM_LINK
.
getTimeUnit
().
toMillis
(
RedisEnum
.
ROOM_LINK
.
getTimeout
());
Date
expirationDate
=
new
Date
(
System
.
currentTimeMillis
()
+
expireMillis
);
// 组装响应
CreateResponse
response
=
new
CreateResponse
();
response
.
setRoomId
(
session
.
getRoomId
());
response
.
setRoomPwd
(
session
.
getRoomPwd
());
response
.
setSessionBizId
(
session
.
getCoSessionBizId
());
response
.
setStatus
(
session
.
getStatus
());
response
.
setRoomLink
(
roomLink
);
response
.
setRoomQrCode
(
roomQrCodeUrl
);
response
.
setExpirationTime
(
expirationDate
);
// 设置过期时间
log
.
info
(
"【创建会话】成功,roomId={}, sessionBizId={}"
,
session
.
getRoomId
(),
session
.
getCoSessionBizId
());
return
Result
.
success
(
response
);
}
catch
(
Exception
e
)
{
log
.
error
(
"【创建会话】异常"
,
e
);
throw
new
BusinessException
(
"创建会话失败:"
+
e
.
getMessage
());
}
}
/**
* 加入协同会话
*/
@Override
@Transactional
(
rollbackFor
=
Exception
.
class
)
public
Result
<
JoinResponse
>
join
(
JoinRequest
request
)
{
log
.
info
(
"【加入会话】收到加入请求, request={}"
,
request
);
try
{
CoSession
session
=
joinSession
(
request
.
getRoomId
(),
request
.
getRoomPwd
(),
request
.
getParticipantId
(),
request
.
getParticipantType
()
);
if
(
session
==
null
)
{
log
.
warn
(
"【加入会话】加入失败,返回空会话"
);
return
Result
.
success
();
}
JoinResponse
joinResponse
=
new
JoinResponse
();
joinResponse
.
setControlHolderId
(
session
.
getControlHolderId
());
joinResponse
.
setControlHolderType
(
session
.
getControlHolderType
());
joinResponse
.
setResourceInit
(
session
.
getResourceInit
());
joinResponse
.
setCurrentPage
(
session
.
getCurrentPage
());
joinResponse
.
setRoomId
(
session
.
getRoomId
());
joinResponse
.
setSessionBizId
(
session
.
getCoSessionBizId
());
// 获取资源所有者缓存中的登录信息
RoomRedisInfoDTO
roomRedisInfoDTO
=
redisUtil
.
getCacheObject
(
RedisEnum
.
ROOM
.
getPrefix
()
+
session
.
getRoomId
()
+
":"
+
session
.
getRoomPwd
(),
RoomRedisInfoDTO
.
class
);
if
(
roomRedisInfoDTO
==
null
)
{
log
.
error
(
"【加入会话】会话发起者缓存信息不存在,roomId={}"
,
session
.
getRoomId
());
throw
new
BusinessException
(
"会话发起者登录信息失效,建议联系会话发起者再次发起"
);
}
joinResponse
.
setUserId
(
roomRedisInfoDTO
.
getUserId
());
joinResponse
.
setToken
(
roomRedisInfoDTO
.
getToken
());
joinResponse
.
setName
(
roomRedisInfoDTO
.
getName
());
joinResponse
.
setMobile
(
roomRedisInfoDTO
.
getMobile
());
log
.
info
(
"【加入会话】成功, roomId={}, participantId={}, controlHolderType={}"
,
session
.
getRoomId
(),
session
.
getParticipantId
(),
session
.
getControlHolderType
());
//删除房间链接缓存(只能访问一次,下次重新生成)
redisUtil
.
deleteObject
(
RedisEnum
.
ROOM_LINK
.
getPrefix
()
+
session
.
getRoomId
()
+
":"
+
session
.
getRoomPwd
());
return
Result
.
success
(
joinResponse
);
}
catch
(
Exception
e
)
{
log
.
error
(
"【加入会话】异常"
,
e
);
throw
e
;
}
}
/**
* 顾问加入会话
* @param roomId
* @param roomPwd
* @param participantId
* @param participantType
* @return
*/
@Transactional
(
rollbackFor
=
Exception
.
class
)
public
CoSession
joinSession
(
String
roomId
,
String
roomPwd
,
String
participantId
,
String
participantType
)
{
log
.
info
(
"【加入会话-内部】开始, roomId={}, roomPwd={}, participantId={}, participantType={}"
,
roomId
,
roomPwd
,
participantId
,
participantType
);
try
{
//校验房间链接是否失效
String
roomLink
=
redisUtil
.
getCacheObject
(
RedisEnum
.
ROOM_LINK
.
getPrefix
()
+
roomId
+
":"
+
roomPwd
);
if
(
StringUtils
.
isBlank
(
roomLink
))
{
throw
new
BusinessException
(
ResultCode
.
LINK_INVALID
.
getCode
(),
"访问链接已失效,请重新生成"
);
}
LambdaQueryWrapper
<
CoSession
>
wrapper
=
new
LambdaQueryWrapper
<>();
wrapper
.
eq
(
CoSession:
:
getRoomId
,
roomId
)
.
eq
(
CoSession:
:
getRoomPwd
,
roomPwd
)
.
last
(
" limit 1 "
);
CoSession
session
=
iCoSessionService
.
getOne
(
wrapper
);
if
(
session
==
null
)
{
log
.
error
(
"【加入会话-内部】未找到会话,roomId={}, roomPwd={}"
,
roomId
,
roomPwd
);
throw
new
BusinessException
(
"房间号或者密码错误,无法加入"
);
}
log
.
info
(
"【加入会话-内部】找到会话, roomId={}, status={}, currentHolder={}:{}"
,
session
.
getRoomId
(),
session
.
getStatus
(),
session
.
getControlHolderType
(),
session
.
getControlHolderId
());
if
(
CoSessionStatusEnum
.
YJS
.
getItemValue
().
equals
(
session
.
getStatus
()))
{
log
.
warn
(
"【加入会话-内部】会话已结束,roomId={}"
,
session
.
getRoomId
());
throw
new
BusinessException
(
"会话已结束,不能再次加入房间"
);
}
// if (StringUtils.isNotBlank(session.getParticipantId()) && !session.getParticipantId().equals(participantId)) {
// log.warn("【加入会话-内部】房间被占用,当前参与者={}, 新参与者={}",
// session.getParticipantId(), participantId);
// throw new BusinessException("当前房间被占用,不能加入到房间");
// }
// 若为待开始状态,设置开始时间并转移控制权
if
(
CoSessionStatusEnum
.
DKS
.
getItemValue
().
equals
(
session
.
getStatus
()))
{
if
(
session
.
getStartTime
()
==
null
)
{
session
.
setStartTime
(
LocalDateTime
.
now
());
log
.
info
(
"【加入会话-内部】设置开始时间={}"
,
session
.
getStartTime
());
}
// 待开始状态下,控制权自动移交给参与者(顾问)
session
.
setControlHolderType
(
ControlHolderTypeEnum
.
PARTICIPANT
.
getItemValue
());
session
.
setControlHolderId
(
participantId
);
log
.
info
(
"【加入会话-内部】待开始状态,控制权移交给参与者={}"
,
participantId
);
}
else
{
// 如果会话已经是进行中,但可能控制权不在顾问,此处强制转移给顾问(根据业务需求,可调整)
// 如果不希望强制转移,可注释掉以下行
session
.
setControlHolderType
(
ControlHolderTypeEnum
.
PARTICIPANT
.
getItemValue
());
session
.
setControlHolderId
(
participantId
);
log
.
info
(
"【加入会话-内部】强制控制权移交给参与者={}"
,
participantId
);
}
// 更新会话状态为进行中
session
.
setStatus
(
CoSessionStatusEnum
.
JXZ
.
getItemValue
());
session
.
setParticipantId
(
participantId
);
session
.
setParticipantType
(
participantType
);
session
.
setUpdaterId
(
participantId
);
iCoSessionService
.
updateById
(
session
);
log
.
info
(
"【加入会话-内部】数据库更新成功,新状态={}"
,
session
.
getStatus
());
// 更新 Redis 缓存
RoomRedisInfoDTO
roomRedisInfoDTO
=
redisUtil
.
getCacheObject
(
RedisEnum
.
ROOM
.
getPrefix
()
+
session
.
getRoomId
()
+
":"
+
session
.
getRoomPwd
(),
RoomRedisInfoDTO
.
class
);
if
(
roomRedisInfoDTO
!=
null
)
{
roomRedisInfoDTO
.
setControlHolderType
(
session
.
getControlHolderType
());
roomRedisInfoDTO
.
setControlHolderId
(
session
.
getControlHolderId
());
redisUtil
.
setCacheObject
(
RedisEnum
.
ROOM
.
getPrefix
()
+
session
.
getRoomId
()
+
":"
+
session
.
getRoomPwd
(),
roomRedisInfoDTO
,
RedisEnum
.
ROOM
.
getTimeout
(),
RedisEnum
.
ROOM
.
getTimeUnit
());
log
.
info
(
"【加入会话-内部】Redis缓存更新成功,新控制者={}:{}"
,
session
.
getControlHolderType
(),
session
.
getControlHolderId
());
}
else
{
log
.
warn
(
"【加入会话-内部】Redis缓存不存在,可能已过期,roomId={}"
,
session
.
getRoomId
());
}
//生成协同操作日志
operationLogService
.
log
(
session
.
getCoSessionBizId
(),
participantId
,
participantType
,
participantId
,
"join_session"
,
String
.
format
(
"加入会话 roomId=%s, 控制权转移至%s"
,
session
.
getRoomId
(),
session
.
getControlHolderType
()),
ClientInfoUtils
.
getDevice
(),
ClientInfoUtils
.
getClientIp
()
);
return
session
;
}
catch
(
Exception
e
)
{
log
.
error
(
"【加入会话-内部】异常"
,
e
);
throw
e
;
}
}
/**
* 获取会话状态
* @param request
* @return
*/
@Override
public
Result
<
GetStatusResponse
>
getStatus
(
GetStatusRequest
request
)
{
CoSession
coSession
=
iCoSessionService
.
lambdaQuery
()
.
eq
(
CoSession:
:
getCoSessionBizId
,
request
.
getSessionBizId
())
.
last
(
" limit 1 "
)
.
one
();
if
(
coSession
==
null
)
{
throw
new
BusinessException
(
"会话信息不存在"
);
}
GetStatusResponse
response
=
new
GetStatusResponse
();
String
roomLink
=
redisUtil
.
getCacheObject
(
RedisEnum
.
ROOM_LINK
.
getPrefix
()
+
coSession
.
getRoomId
()
+
":"
+
coSession
.
getRoomPwd
());
if
(
StringUtils
.
isBlank
(
roomLink
))
{
//2-已失效
response
.
setStatus
(
2
);
}
else
{
response
.
setStatus
(
1
);
}
return
Result
.
success
(
response
);
}
/**
* 获取会话详情
*/
@Override
public
Result
<
SessionDetailResponse
>
get
(
String
bizId
)
{
log
.
info
(
"【获取会话详情】bizId={}"
,
bizId
);
try
{
CoSession
session
=
iCoSessionService
.
getByBizId
(
bizId
);
if
(
session
==
null
)
{
log
.
warn
(
"【获取会话详情】未找到会话,bizId={}"
,
bizId
);
return
Result
.
success
();
}
SessionDetailResponse
response
=
new
SessionDetailResponse
();
BeanUtils
.
copyProperties
(
session
,
response
);
log
.
info
(
"【获取会话详情】成功,roomId={}"
,
session
.
getRoomId
());
return
Result
.
success
(
response
);
}
catch
(
Exception
e
)
{
log
.
error
(
"【获取会话详情】异常"
,
e
);
throw
e
;
}
}
/**
* 结束会话(客户调用)
*/
@Override
@Transactional
(
rollbackFor
=
Exception
.
class
)
public
Result
<
CommonResponse
>
end
(
EndSessionRequest
request
)
{
log
.
info
(
"【结束会话】收到请求, roomId={}"
,
request
.
getRoomId
());
try
{
CoSession
coSession
=
iCoSessionService
.
lambdaQuery
()
.
eq
(
CoSession:
:
getRoomId
,
request
.
getRoomId
())
.
last
(
" limit 1"
)
.
one
();
if
(
coSession
==
null
)
{
log
.
error
(
"【结束会话】会话不存在,roomId={}"
,
request
.
getRoomId
());
throw
new
BusinessException
(
"会话不存在"
);
}
log
.
info
(
"【结束会话】找到会话,当前状态={}"
,
coSession
.
getStatus
());
// 结束会话关闭共享,更新信息
coSession
.
setStatus
(
CoSessionStatusEnum
.
YJS
.
getItemValue
());
coSession
.
setEndTime
(
LocalDateTime
.
now
());
iCoSessionService
.
saveOrUpdate
(
coSession
);
log
.
info
(
"【结束会话】数据库更新成功,状态已结束"
);
// 销毁redis房间缓存信息
redisUtil
.
deleteObject
(
RedisEnum
.
ROOM
.
getPrefix
()
+
coSession
.
getRoomId
()
+
":"
+
coSession
.
getRoomPwd
());
log
.
info
(
"【结束会话】Redis缓存已删除"
);
// 添加操作日志,协同-操作日志表
operationLogService
.
log
(
coSession
.
getCoSessionBizId
(),
null
,
null
,
null
,
"end_session"
,
String
.
format
(
"结束会话 roomId=%s"
,
coSession
.
getRoomId
()),
ClientInfoUtils
.
getDevice
(),
ClientInfoUtils
.
getClientIp
()
);
CommonResponse
response
=
new
CommonResponse
();
response
.
setMessage
(
"会话已结束"
);
log
.
info
(
"【结束会话】成功, roomId={}"
,
request
.
getRoomId
());
return
Result
.
success
(
response
);
}
catch
(
Exception
e
)
{
log
.
error
(
"【结束会话】异常"
,
e
);
throw
e
;
}
}
/**
* 切换控制权(顾问调用)
*/
@Override
@Transactional
(
rollbackFor
=
Exception
.
class
)
public
Result
<
CommonResponse
>
transferControl
(
TransferControlRequest
request
)
{
log
.
info
(
"【切换控制权】收到请求, oprType={}, roomId={}"
,
request
.
getOprType
(),
request
.
getRoomId
());
try
{
transferControlUp
(
request
.
getOprType
(),
request
.
getRoomId
());
CommonResponse
response
=
new
CommonResponse
();
response
.
setMessage
(
"控制权已切换"
);
log
.
info
(
"【切换控制权】成功, roomId={}"
,
request
.
getRoomId
());
return
Result
.
success
(
response
);
}
catch
(
Exception
e
)
{
log
.
error
(
"【切换控制权】异常"
,
e
);
throw
e
;
}
}
/**
* 切换控制权(内部方法)
*/
@Transactional
(
rollbackFor
=
Exception
.
class
)
public
void
transferControlUp
(
Integer
oprType
,
String
roomId
)
{
log
.
info
(
"【切换控制权-内部】开始, oprType={}, roomId={}"
,
oprType
,
roomId
);
try
{
// 根据房间号查询会话信息
LambdaQueryWrapper
<
CoSession
>
wrapper
=
new
LambdaQueryWrapper
<>();
wrapper
.
eq
(
CoSession:
:
getRoomId
,
roomId
).
last
(
" limit 1 "
);
CoSession
session
=
iCoSessionService
.
getOne
(
wrapper
);
if
(
session
==
null
)
{
log
.
error
(
"【切换控制权-内部】会话不存在, roomId={}"
,
roomId
);
throw
new
BusinessException
(
"会话不存在"
);
}
log
.
info
(
"【切换控制权-内部】找到会话, 当前控制者={}:{}"
,
session
.
getControlHolderType
(),
session
.
getControlHolderId
());
// 移交控制权
if
(
oprType
==
1
)
{
// 1-开启客户操作,控制权移交给资源所有者(客户)
session
.
setControlHolderType
(
ControlHolderTypeEnum
.
OWNER
.
getItemValue
());
session
.
setControlHolderId
(
session
.
getOwnerId
());
log
.
info
(
"【切换控制权-内部】开启客户操作,控制权移交给所有者={}"
,
session
.
getOwnerId
());
}
else
if
(
oprType
==
2
)
{
// 2-关闭客户操作,控制权移交给参与者(顾问)
session
.
setControlHolderType
(
ControlHolderTypeEnum
.
PARTICIPANT
.
getItemValue
());
session
.
setControlHolderId
(
session
.
getParticipantId
());
log
.
info
(
"【切换控制权-内部】关闭客户操作,控制权移交给参与者={}"
,
session
.
getParticipantId
());
}
else
{
log
.
warn
(
"【切换控制权-内部】未知oprType={}, 忽略"
,
oprType
);
return
;
}
session
.
setUpdaterId
(
session
.
getParticipantId
());
iCoSessionService
.
updateById
(
session
);
log
.
info
(
"【切换控制权-内部】数据库更新成功,新控制者={}:{}"
,
session
.
getControlHolderType
(),
session
.
getControlHolderId
());
// 更新房间缓存redis信息
RoomRedisInfoDTO
roomRedisInfoDTO
=
redisUtil
.
getCacheObject
(
RedisEnum
.
ROOM
.
getPrefix
()
+
roomId
+
":"
+
session
.
getRoomPwd
(),
RoomRedisInfoDTO
.
class
);
if
(
roomRedisInfoDTO
!=
null
)
{
roomRedisInfoDTO
.
setControlHolderId
(
session
.
getControlHolderId
());
roomRedisInfoDTO
.
setControlHolderType
(
session
.
getControlHolderType
());
redisUtil
.
setCacheObject
(
RedisEnum
.
ROOM
.
getPrefix
()
+
roomId
+
":"
+
session
.
getRoomPwd
(),
roomRedisInfoDTO
,
RedisEnum
.
ROOM
.
getTimeout
(),
RedisEnum
.
ROOM
.
getTimeUnit
());
log
.
info
(
"【切换控制权-内部】Redis缓存更新成功"
);
}
else
{
log
.
warn
(
"【切换控制权-内部】Redis缓存不存在,可能已过期"
);
}
// 添加操作日志,协同-操作日志表
operationLogService
.
log
(
session
.
getCoSessionBizId
(),
session
.
getParticipantId
(),
ControlHolderTypeEnum
.
PARTICIPANT
.
getItemValue
(),
session
.
getParticipantId
(),
"transfer_control"
,
String
.
format
(
"切换控制权至 %s:%s"
,
session
.
getControlHolderType
(),
session
.
getControlHolderId
()),
ClientInfoUtils
.
getDevice
(),
ClientInfoUtils
.
getClientIp
()
);
}
catch
(
Exception
e
)
{
log
.
error
(
"【切换控制权-内部】异常"
,
e
);
throw
e
;
}
}
/**
* 获取当前控制者
*/
@Override
public
RoomRedisInfoDTO
getCurrentController
(
String
roomId
)
{
log
.
info
(
"【获取当前控制者】roomId={}"
,
roomId
);
try
{
RoomRedisInfoDTO
roomRedisInfoDTO
=
redisUtil
.
getCacheObject
(
RedisEnum
.
ROOM
.
getPrefix
()
+
roomId
);
if
(
roomRedisInfoDTO
==
null
||
(
roomRedisInfoDTO
!=
null
&&
StringUtils
.
isBlank
(
roomRedisInfoDTO
.
getControlHolderId
())))
{
log
.
info
(
"【获取当前控制者】Redis缓存不存在或控制者ID为空,从数据库查询"
);
LambdaQueryWrapper
<
CoSession
>
wrapper
=
new
LambdaQueryWrapper
<>();
wrapper
.
eq
(
CoSession:
:
getRoomId
,
roomId
).
last
(
" limit 1 "
);
CoSession
session
=
iCoSessionService
.
getOne
(
wrapper
);
if
(
session
==
null
)
{
log
.
error
(
"【获取当前控制者】会话不存在, roomId={}"
,
roomId
);
throw
new
BusinessException
(
"会话不存在"
);
}
if
(
roomRedisInfoDTO
==
null
)
{
roomRedisInfoDTO
=
new
RoomRedisInfoDTO
();
}
roomRedisInfoDTO
.
setControlHolderType
(
session
.
getControlHolderType
());
roomRedisInfoDTO
.
setControlHolderId
(
session
.
getControlHolderId
());
// 查询出来的信息更新回缓存里面
redisUtil
.
setCacheObject
(
RedisEnum
.
ROOM
.
getPrefix
()
+
roomId
+
":"
+
session
.
getRoomPwd
(),
roomRedisInfoDTO
,
RedisEnum
.
ROOM
.
getTimeout
(),
RedisEnum
.
ROOM
.
getTimeUnit
());
log
.
info
(
"【获取当前控制者】从数据库加载并更新缓存,控制者={}:{}"
,
session
.
getControlHolderType
(),
session
.
getControlHolderId
());
}
else
{
log
.
info
(
"【获取当前控制者】从Redis获取,控制者={}:{}"
,
roomRedisInfoDTO
.
getControlHolderType
(),
roomRedisInfoDTO
.
getControlHolderId
());
}
return
roomRedisInfoDTO
;
}
catch
(
Exception
e
)
{
log
.
error
(
"【获取当前控制者】异常"
,
e
);
throw
e
;
}
}
/**
* 更新当前页面
*/
@Override
@Transactional
(
rollbackFor
=
Exception
.
class
)
public
void
updateCurrentPage
(
String
roomId
,
String
newPageJson
,
String
operatorId
)
{
log
.
info
(
"【更新当前页面】roomId={}, operatorId={}, newPageJson={}"
,
roomId
,
operatorId
,
newPageJson
);
try
{
LambdaQueryWrapper
<
CoSession
>
wrapper
=
new
LambdaQueryWrapper
<>();
wrapper
.
eq
(
CoSession:
:
getRoomId
,
roomId
).
last
(
" limit 1 "
);
CoSession
session
=
iCoSessionService
.
getOne
(
wrapper
);
if
(
session
==
null
)
{
log
.
error
(
"【更新当前页面】会话不存在, roomId={}"
,
roomId
);
throw
new
BusinessException
(
"会话不存在"
);
}
String
history
=
session
.
getPageHistory
();
if
(
history
==
null
||
history
.
equals
(
"[]"
)
||
history
.
isEmpty
())
{
history
=
"["
+
newPageJson
+
"]"
;
}
else
{
// 简单追加(生产环境建议用JSONArray处理)
history
=
history
.
substring
(
0
,
history
.
length
()
-
1
)
+
","
+
newPageJson
+
"]"
;
}
iCoSessionService
.
updateCurrentPageAndHistory
(
roomId
,
newPageJson
,
history
,
operatorId
);
log
.
info
(
"【更新当前页面】成功, 新历史={}"
,
history
);
}
catch
(
Exception
e
)
{
log
.
error
(
"【更新当前页面】异常"
,
e
);
throw
e
;
}
}
/**
* 根据房间号获取会话信息
*/
@Override
public
CoSession
getByRoomId
(
String
roomId
)
{
log
.
info
(
"【根据房间号获取会话】roomId={}"
,
roomId
);
try
{
CoSession
session
=
iCoSessionService
.
getByRoomId
(
roomId
);
if
(
session
==
null
)
{
log
.
warn
(
"【根据房间号获取会话】未找到会话, roomId={}"
,
roomId
);
}
else
{
log
.
info
(
"【根据房间号获取会话】找到会话, controlHolder={}:{}"
,
session
.
getControlHolderType
(),
session
.
getControlHolderId
());
}
return
session
;
}
catch
(
Exception
e
)
{
log
.
error
(
"【根据房间号获取会话】异常"
,
e
);
throw
e
;
}
}
private
boolean
autoStartRecording
()
{
return
true
;
// 从配置读取
}
}
\ No newline at end of file
yd-communication-api/src/main/java/com/yd/communication/api/service/impl/ApiRecordingTaskServiceImpl.java
0 → 100644
View file @
ed85d9a7
package
com
.
yd
.
communication
.
api
.
service
.
impl
;
import
com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper
;
import
com.yd.common.enums.CommonEnum
;
import
com.yd.common.exception.BusinessException
;
import
com.yd.common.result.Result
;
import
com.yd.common.utils.RandomStringGenerator
;
import
com.yd.communication.api.service.ApiRecordingTaskService
;
import
com.yd.communication.api.utils.ClientInfoUtils
;
import
com.yd.communication.feign.request.recording.ApiStartRecordingRequest
;
import
com.yd.communication.feign.response.recording.ApiQueryRecordingResponse
;
import
com.yd.communication.service.model.RecordingTask
;
import
com.yd.communication.service.service.ICoOperationLogService
;
import
com.yd.communication.service.service.IRecordingTaskService
;
import
com.yd.oss.feign.client.ApiChunkedUploadContextFeignClient
;
import
lombok.extern.slf4j.Slf4j
;
import
org.apache.commons.lang3.StringUtils
;
import
org.springframework.beans.BeanUtils
;
import
org.springframework.stereotype.Service
;
import
org.springframework.transaction.annotation.Transactional
;
import
javax.annotation.Resource
;
import
java.time.LocalDateTime
;
import
java.util.HashMap
;
import
java.util.Map
;
import
java.util.UUID
;
@Service
@Slf4j
public
class
ApiRecordingTaskServiceImpl
implements
ApiRecordingTaskService
{
@Resource
private
IRecordingTaskService
iRecordingTaskService
;
@Resource
private
ApiChunkedUploadContextFeignClient
contextFeignClient
;
@Resource
private
ICoOperationLogService
operationLogService
;
/**
* 开始录制
* @param request
* @return
*/
@Override
@Transactional
(
rollbackFor
=
Exception
.
class
)
public
Result
<
String
>
startRecording
(
ApiStartRecordingRequest
request
)
{
log
.
info
(
"开始录制: bizType={}, bizId={}"
,
request
.
getBizType
(),
request
.
getBizId
());
// 检查是否有未结束的任务
LambdaQueryWrapper
<
RecordingTask
>
checkWrapper
=
new
LambdaQueryWrapper
<>();
checkWrapper
.
eq
(
RecordingTask:
:
getBizId
,
request
.
getBizId
())
.
eq
(
RecordingTask:
:
getStatus
,
"2"
);
RecordingTask
task
=
new
RecordingTask
();
task
.
setRecordingTaskBizId
(
RandomStringGenerator
.
generateBizId16
(
CommonEnum
.
UID_TYPE_RECORDING_TASK
.
getCode
()));
task
.
setTaskNo
(
"R"
+
System
.
currentTimeMillis
());
task
.
setTaskId
(
UUID
.
randomUUID
().
toString
().
replace
(
"-"
,
""
));
task
.
setBizType
(
request
.
getBizType
());
task
.
setBizId
(
request
.
getBizId
());
task
.
setStatus
(
"2"
);
// 录制中
task
.
setStartTime
(
LocalDateTime
.
now
());
task
.
setCreatorId
(
"system"
);
task
.
setIsDeleted
(
0
);
task
.
setRecordingMode
(
"screen"
);
iRecordingTaskService
.
save
(
task
);
log
.
info
(
"录制任务创建成功,taskId={}"
,
task
.
getTaskId
());
//生成协同操作日志
operationLogService
.
log
(
request
.
getBizId
(),
"system"
,
"system"
,
"system"
,
"start_recording"
,
String
.
format
(
"开始录制 taskId=%s, bizType=%s"
,
task
.
getTaskId
(),
request
.
getBizType
()),
ClientInfoUtils
.
getDevice
(),
ClientInfoUtils
.
getClientIp
()
);
return
Result
.
success
(
task
.
getTaskId
());
}
/**
* 停止录制并上传视频文件到 OSS
* @param taskId 录制任务ID
* @return
*/
@Override
@Transactional
(
rollbackFor
=
Exception
.
class
)
public
Result
<
Map
<
String
,
String
>>
stopRecording
(
String
taskId
)
{
log
.
info
(
"停止录制并上传文件: taskId={}"
,
taskId
);
// 查询录制任务
LambdaQueryWrapper
<
RecordingTask
>
wrapper
=
new
LambdaQueryWrapper
<>();
wrapper
.
eq
(
RecordingTask:
:
getTaskId
,
taskId
);
RecordingTask
task
=
iRecordingTaskService
.
getOne
(
wrapper
);
if
(
task
==
null
)
{
throw
new
BusinessException
(
"录制任务不存在,taskId="
+
taskId
);
}
// 远程调用 OSS 微服务,完成分片上传(合并)
String
projectBizId
=
""
;
Result
<
Map
<
String
,
Object
>>
finishResult
=
contextFeignClient
.
finishChunks
(
taskId
,
projectBizId
);
if
(
finishResult
==
null
||
finishResult
.
getCode
()
!=
200
)
{
String
errorMsg
=
finishResult
!=
null
?
finishResult
.
getMsg
()
:
"OSS服务返回空"
;
log
.
error
(
"完成分片上传失败: {}"
,
errorMsg
);
throw
new
BusinessException
(
"录制文件上传失败: "
+
errorMsg
);
}
// 提取上传结果
Map
<
String
,
Object
>
data
=
finishResult
.
getData
();
if
(
data
==
null
)
{
throw
new
BusinessException
(
"上传成功但未返回文件信息"
);
}
String
fileUrl
=
(
String
)
data
.
get
(
"fileUrl"
);
// 安全获取 fileSize(可能是 Integer 或 Long)
Number
fileSizeNumber
=
(
Number
)
data
.
get
(
"fileSize"
);
Long
fileSize
=
fileSizeNumber
!=
null
?
fileSizeNumber
.
longValue
()
:
null
;
String
fileKey
=
(
String
)
data
.
get
(
"fileKey"
);
// 更新录制任务
task
.
setFileUrl
(
fileUrl
);
task
.
setFileSize
(
fileSize
);
task
.
setStatus
(
"3"
);
// 已录制
task
.
setFileFormat
(
"webm"
);
task
.
setStopTime
(
LocalDateTime
.
now
());
iRecordingTaskService
.
updateById
(
task
);
// 返回结果
Map
<
String
,
String
>
result
=
new
HashMap
<>();
result
.
put
(
"taskId"
,
task
.
getTaskId
());
result
.
put
(
"fileUrl"
,
fileUrl
);
result
.
put
(
"fileKey"
,
fileKey
);
log
.
info
(
"录制文件上传成功,fileUrl={}"
,
fileUrl
);
// 生成协同操作日志
operationLogService
.
log
(
task
.
getBizId
(),
"system"
,
"system"
,
"system"
,
"stop_recording"
,
String
.
format
(
"停止录制 taskId=%s, fileUrl=%s"
,
task
.
getTaskId
(),
fileUrl
),
ClientInfoUtils
.
getDevice
(),
ClientInfoUtils
.
getClientIp
()
);
return
Result
.
success
(
result
);
}
/**
* 查询录制信息
* @param taskId
* @return
*/
@Override
public
Result
<
ApiQueryRecordingResponse
>
queryRecording
(
String
taskId
)
{
if
(
StringUtils
.
isBlank
(
taskId
))
{
throw
new
BusinessException
(
"任务ID不能为空"
);
}
ApiQueryRecordingResponse
response
=
new
ApiQueryRecordingResponse
();
RecordingTask
recordingTask
=
iRecordingTaskService
.
lambdaQuery
()
.
eq
(
RecordingTask:
:
getTaskId
,
taskId
)
.
last
(
" limit 1 "
)
.
one
();
if
(
recordingTask
==
null
)
{
throw
new
BusinessException
(
"录制任务信息不存在"
);
}
BeanUtils
.
copyProperties
(
recordingTask
,
response
);
return
Result
.
success
(
response
);
}
}
\ No newline at end of file
yd-communication-api/src/main/java/com/yd/communication/api/utils/ClientInfoUtils.java
0 → 100644
View file @
ed85d9a7
package
com
.
yd
.
communication
.
api
.
utils
;
import
org.apache.commons.lang3.StringUtils
;
import
org.springframework.web.context.request.RequestContextHolder
;
import
org.springframework.web.context.request.ServletRequestAttributes
;
import
javax.servlet.http.HttpServletRequest
;
/**
* 客户端信息工具类(IP、设备号等)
* 优先从 HTTP 请求获取,若不存在则从 IpContextHolder 获取(WebSocket 场景)
*
* @author zxm
* @date 2026-08-03
*/
public
class
ClientInfoUtils
{
/**
* 获取客户端真实 IP
* 优先级:X-Forwarded-For > X-Real-IP > RemoteAddr > IpContextHolder
*
* @return 客户端 IP
*/
public
static
String
getClientIp
()
{
// 1. 从 HTTP 请求获取
try
{
ServletRequestAttributes
attributes
=
(
ServletRequestAttributes
)
RequestContextHolder
.
getRequestAttributes
();
if
(
attributes
!=
null
)
{
HttpServletRequest
request
=
attributes
.
getRequest
();
if
(
request
!=
null
)
{
// 获取 X-Forwarded-For(经过代理时)
String
ip
=
request
.
getHeader
(
"X-Forwarded-For"
);
if
(
StringUtils
.
isBlank
(
ip
))
{
ip
=
request
.
getHeader
(
"X-Real-IP"
);
}
if
(
StringUtils
.
isBlank
(
ip
))
{
ip
=
request
.
getRemoteAddr
();
}
// X-Forwarded-For 可能包含多个 IP,取第一个
if
(
StringUtils
.
isNotBlank
(
ip
)
&&
ip
.
contains
(
","
))
{
ip
=
ip
.
split
(
","
)[
0
].
trim
();
}
if
(
StringUtils
.
isNotBlank
(
ip
))
{
return
ip
;
}
}
}
}
catch
(
Exception
e
)
{
// 忽略异常,继续回退
}
// 2. 回退到 IpContextHolder(WebSocket 场景由 CoWebSocketServer 设置)
return
IpContextHolder
.
getIp
();
}
/**
* 获取设备号(User-Agent)
* 优先级:HTTP 请求头 > IpContextHolder
*
* @return 设备号(User-Agent)
*/
public
static
String
getDevice
()
{
try
{
ServletRequestAttributes
attributes
=
(
ServletRequestAttributes
)
RequestContextHolder
.
getRequestAttributes
();
if
(
attributes
!=
null
)
{
HttpServletRequest
request
=
attributes
.
getRequest
();
if
(
request
!=
null
)
{
return
request
.
getHeader
(
"User-Agent"
);
}
}
}
catch
(
Exception
e
)
{
// 忽略异常
}
return
IpContextHolder
.
getDevice
();
}
/**
* 获取完整客户端信息(IP + 设备)
* @return 格式:IP | User-Agent
*/
public
static
String
getClientInfo
()
{
return
getClientIp
()
+
" | "
+
getDevice
();
}
}
\ No newline at end of file
yd-communication-api/src/main/java/com/yd/communication/api/utils/IpContextHolder.java
0 → 100644
View file @
ed85d9a7
package
com
.
yd
.
communication
.
api
.
utils
;
/**
* 用于在 WebSocket 等非 HTTP 环境下传递客户端 IP 和设备信息
* <p>
* 使用 ThreadLocal 存储,避免参数透传,确保线程安全。
* 通常在 WebSocket 的 @OnMessage 方法中设置,在业务 Service 中获取,
* 并在 finally 块中清除,防止内存泄漏。
* </p>
*
* @author zxm
* @date 2026-08-03
*/
public
class
IpContextHolder
{
private
static
final
ThreadLocal
<
String
>
IP_HOLDER
=
new
ThreadLocal
<>();
private
static
final
ThreadLocal
<
String
>
DEVICE_HOLDER
=
new
ThreadLocal
<>();
/**
* 设置当前线程的客户端 IP
*/
public
static
void
setIp
(
String
ip
)
{
IP_HOLDER
.
set
(
ip
);
}
/**
* 获取当前线程的客户端 IP
*/
public
static
String
getIp
()
{
return
IP_HOLDER
.
get
();
}
/**
* 设置当前线程的设备号(User-Agent 等)
*/
public
static
void
setDevice
(
String
device
)
{
DEVICE_HOLDER
.
set
(
device
);
}
/**
* 获取当前线程的设备号
*/
public
static
String
getDevice
()
{
return
DEVICE_HOLDER
.
get
();
}
/**
* 清除当前线程的上下文(防止内存泄漏)
*/
public
static
void
clear
()
{
IP_HOLDER
.
remove
();
DEVICE_HOLDER
.
remove
();
}
}
\ No newline at end of file
yd-communication-api/src/main/java/com/yd/communication/api/utils/QRCodeUtils.java
0 → 100644
View file @
ed85d9a7
package
com
.
yd
.
communication
.
api
.
utils
;
import
com.google.zxing.BarcodeFormat
;
import
com.google.zxing.EncodeHintType
;
import
com.google.zxing.client.j2se.MatrixToImageWriter
;
import
com.google.zxing.common.BitMatrix
;
import
com.google.zxing.qrcode.QRCodeWriter
;
import
lombok.extern.slf4j.Slf4j
;
import
java.io.ByteArrayOutputStream
;
import
java.util.HashMap
;
import
java.util.Map
;
@Slf4j
public
class
QRCodeUtils
{
/**
* 生成二维码图片的字节数组(PNG格式)
*
* @param content 二维码内容(如URL)
* @param width 图片宽度(像素)
* @param height 图片高度(像素)
* @return PNG图片的字节数组
*/
public
static
byte
[]
generateQRCode
(
String
content
,
int
width
,
int
height
)
{
try
{
Map
<
EncodeHintType
,
Object
>
hints
=
new
HashMap
<>();
hints
.
put
(
EncodeHintType
.
CHARACTER_SET
,
"UTF-8"
);
hints
.
put
(
EncodeHintType
.
MARGIN
,
1
);
// 边距
QRCodeWriter
writer
=
new
QRCodeWriter
();
BitMatrix
bitMatrix
=
writer
.
encode
(
content
,
BarcodeFormat
.
QR_CODE
,
width
,
height
,
hints
);
ByteArrayOutputStream
baos
=
new
ByteArrayOutputStream
();
MatrixToImageWriter
.
writeToStream
(
bitMatrix
,
"PNG"
,
baos
);
return
baos
.
toByteArray
();
}
catch
(
Exception
e
)
{
log
.
error
(
"生成二维码失败,content={}, width={}, height={}"
,
content
,
width
,
height
,
e
);
throw
new
RuntimeException
(
"生成二维码失败"
,
e
);
}
}
}
\ No newline at end of file
yd-communication-api/src/main/java/com/yd/communication/api/websocket/CoWebSocketServer.java
0 → 100644
View file @
ed85d9a7
package
com
.
yd
.
communication
.
api
.
websocket
;
import
com.alibaba.fastjson2.JSON
;
import
com.fasterxml.jackson.databind.JsonNode
;
import
com.fasterxml.jackson.databind.ObjectMapper
;
import
com.yd.communication.api.service.ApiCoSessionService
;
import
com.yd.communication.api.utils.IpContextHolder
;
import
com.yd.communication.feign.dto.RoomRedisInfoDTO
;
import
com.yd.communication.feign.enums.ControlHolderTypeEnum
;
import
com.yd.communication.feign.request.EndSessionRequest
;
import
com.yd.communication.feign.request.TransferControlRequest
;
import
com.yd.communication.service.model.CoSession
;
import
com.yd.communication.service.service.ICoOperationLogService
;
import
lombok.extern.slf4j.Slf4j
;
import
org.apache.commons.lang3.StringUtils
;
import
org.redisson.api.RTopic
;
import
org.redisson.api.RedissonClient
;
import
org.springframework.beans.factory.annotation.Autowired
;
import
org.springframework.boot.context.event.ApplicationReadyEvent
;
import
org.springframework.context.event.EventListener
;
import
org.springframework.data.redis.core.RedisTemplate
;
import
org.springframework.stereotype.Component
;
import
javax.websocket.*
;
import
javax.websocket.server.PathParam
;
import
javax.websocket.server.ServerEndpoint
;
import
java.io.IOException
;
import
java.lang.reflect.Method
;
import
java.net.InetSocketAddress
;
import
java.net.URLDecoder
;
import
java.nio.charset.StandardCharsets
;
import
java.util.*
;
import
java.util.concurrent.ConcurrentHashMap
;
import
java.util.concurrent.CopyOnWriteArraySet
;
import
java.util.concurrent.TimeUnit
;
/**
* 协同 WebSocket 服务端(支持多节点部署)
* <p>
* 设计思路:
* 1. 本地内存 Map 管理当前节点的 Session(无法跨节点共享)
* 2. Redis 存储房间成员信息、当前状态(跨节点共享)
* 3. Redis Pub/Sub 实现跨节点实时消息广播
* 4. 新节点接入时,从 Redis 拉取最新状态,实现"状态同步"而非"历史重放"
* </p>
*
* @author zxm
* @date 2026-07-28
*/
@Component
// 将当前类注入 Spring 容器,使其成为 Bean
@ServerEndpoint
(
"/ws/{roomId}"
)
// 声明 WebSocket 端点,路径为 /ws/{roomId},{roomId} 是路径参数
@Slf4j
public
class
CoWebSocketServer
{
// ==================== 本地内存(当前节点) ====================
/**
* 房间 -> 当前节点内的 WebSocket 会话集合
* key: 房间号 (roomId)
* value: 该房间内当前节点上的所有会话(Session)
* 使用 ConcurrentHashMap 保证多线程安全,CopyOnWriteArraySet 保证读写分离
*/
private
static
final
Map
<
String
,
Set
<
Session
>>
ROOMS
=
new
ConcurrentHashMap
<>();
/**
* 会话 -> 房间号 反向映射
* key: WebSocket Session 对象
* value: 该会话所在的房间号
* 用于在收到消息时快速查找房间号
*/
private
static
final
Map
<
Session
,
String
>
SESSION_ROOM
=
new
ConcurrentHashMap
<>();
/**
* 会话 -> 用户ID 映射
* key: WebSocket Session 对象
* value: 该会话对应的用户ID
*/
private
static
final
Map
<
Session
,
String
>
SESSION_USER_ID
=
new
ConcurrentHashMap
<>();
/**
* 会话 -> 用户类型 映射
* key: WebSocket Session 对象
* value: 用户类型 (owner: 客户, participant: 顾问)
*/
private
static
final
Map
<
Session
,
String
>
SESSION_USER_TYPE
=
new
ConcurrentHashMap
<>();
/**
* 房间 -> 当前投屏者用户类型
* key: 房间号 (roomId)
* value: "owner" 或 "participant"
*/
private
static
final
Map
<
String
,
String
>
PRESENTER_MAP
=
new
ConcurrentHashMap
<>();
// ==================== Spring Bean 静态注入 ====================
/**
* API 层会话服务(静态化,供 WebSocket 生命周期方法使用)
* 因为 WebSocket 端点不由 Spring 直接管理,需要静态注入
*/
private
static
ApiCoSessionService
sessionService
;
/**
* 操作日志服务(静态化)
*/
private
static
ICoOperationLogService
operationLogService
;
/**
* Redis 模板(静态化),用于跨节点通信和状态存储
*/
private
static
RedisTemplate
<
String
,
String
>
redisTemplate
;
/**
* 注入会话服务
* @param service ApiCoSessionService 实例
*/
@Autowired
public
void
setSessionService
(
ApiCoSessionService
service
)
{
CoWebSocketServer
.
sessionService
=
service
;
}
/**
* 注入操作日志服务
* @param service ICoOperationLogService 实例
*/
@Autowired
public
void
setOperationLogService
(
ICoOperationLogService
service
)
{
CoWebSocketServer
.
operationLogService
=
service
;
}
/**
* 注入 Redis 模板
* @param redisTemplate RedisTemplate 实例
*/
@Autowired
public
void
setRedisTemplate
(
RedisTemplate
<
String
,
String
>
redisTemplate
)
{
CoWebSocketServer
.
redisTemplate
=
redisTemplate
;
}
private
static
RedissonClient
redissonClient
;
@Autowired
public
void
setRedissonClient
(
RedissonClient
redissonClient
)
{
CoWebSocketServer
.
redissonClient
=
redissonClient
;
}
// ==================== Redis 键名常量 ====================
/**
* 房间成员信息 Redis Hash 键模板
* 实际键名:room:members:{roomId}
* 存储结构:Hash,field 为 sessionId,value 为成员信息 JSON
*/
private
static
final
String
ROOM_MEMBERS_KEY
=
"room:members:%s"
;
/**
* 房间当前页面状态 Redis String 键模板
* 实际键名:room:state:{roomId}:currentPage
* 存储内容:当前页面的 JSON 字符串
*/
private
static
final
String
ROOM_STATE_PAGE_KEY
=
"room:state:%s:currentPage"
;
/**
* 房间广播频道 Redis Pub/Sub 频道模板
* 实际频道名:room:channel:{roomId}
* 用于跨节点消息广播
*/
private
static
final
String
ROOM_BROADCAST_CHANNEL
=
"room:channel:%s"
;
// ==================== 新增:节点标识 ====================
/**
* 当前节点的唯一标识(IP:端口 或 UUID)
* 用于区分消息是否为本节点发出,避免回环
*/
private
static
final
String
NODE_ID
=
UUID
.
randomUUID
().
toString
()
+
"@"
+
System
.
currentTimeMillis
();
// ==================== 跨节点订阅(每个节点启动时执行) ====================
@EventListener
(
ApplicationReadyEvent
.
class
)
public
void
startRedisSubscriber
()
{
log
.
info
(
"应用已完全启动,开始初始化 Redisson RTopic 订阅..."
);
new
Thread
(()
->
{
// 订阅全局频道
RTopic
topic
=
redissonClient
.
getTopic
(
"coordination:channel"
);
topic
.
addListener
(
String
.
class
,
(
channel
,
msg
)
->
{
log
.
debug
(
"收到跨节点消息: {}"
,
msg
);
handleCrossNodeMessage
(
msg
);
});
log
.
info
(
"Redisson RTopic 订阅已启动,频道: coordination:channel"
);
// 保持线程存活(监听在后台异步进行,线程无需阻塞,但保留以防止退出)
while
(
true
)
{
try
{
Thread
.
sleep
(
60000
);
}
catch
(
InterruptedException
e
)
{
break
;
}
}
}).
start
();
}
/**
* Bean 初始化完成后执行,启动 Redis 订阅线程
* 每个节点启动时都会订阅通配符频道 "room:channel:*"
* 用于接收其他节点发来的跨节点广播消息
*/
// @PostConstruct
// public void init() {
// // 启动一个独立的后台线程,避免阻塞主线程
// new Thread(() -> {
// // 无限循环,支持断线自动重连
// while (true) {
// try {
// // 通过 Redis 连接执行订阅命令
// redisTemplate.execute((connection) -> {
// // 订阅所有以 "room:channel:" 开头的频道
// // 第二个参数是监听器:收到消息时调用 handleCrossNodeMessage 处理
// connection.subscribe(
// (message, pattern) -> handleCrossNodeMessage(new String(message.getBody())),
// "room:channel:*".getBytes()
// );
// // 返回 null,因为 subscribe 是阻塞方法,执行到这里说明订阅已结束(异常断开)
// return null;
// }, true); // true 表示使用事务(此处无实际影响)
// } catch (Exception e) {
// // 订阅断开(如 Redis 连接超时、网络抖动),记录错误日志
// log.error("Redis 订阅断开,5秒后重试...", e);
// try {
// // 等待 5 秒后重连
// Thread.sleep(5000);
// } catch (InterruptedException ex) {
// // 线程被中断,退出循环
// Thread.currentThread().interrupt();
// break;
// }
// }
// }
// }).start(); // 启动线程
// log.info("Redis 跨节点广播订阅已启动,当前节点ID: {}", NODE_ID);
// }
/**
* 处理跨节点广播消息(由 Redis 订阅触发)
* 当其他节点向 Redis 频道发布消息时,此方法会被调用
* 注意:此方法仅负责将消息转发给本节点的 Session,不处理状态同步
*
* @param body Redis 消息体(JSON 字符串)
*/
private
void
handleCrossNodeMessage
(
String
body
)
{
try
{
// 创建 Jackson 对象映射器,解析 JSON
ObjectMapper
mapper
=
new
ObjectMapper
();
// 将 JSON 字符串解析为树节点
JsonNode
json
=
mapper
.
readTree
(
body
);
// 提取消息中的节点ID
String
sourceNodeId
=
json
.
has
(
"sourceNodeId"
)
?
json
.
get
(
"sourceNodeId"
).
asText
()
:
null
;
// 如果消息是本节点发出的,直接忽略,避免回环
if
(
NODE_ID
.
equals
(
sourceNodeId
))
{
log
.
debug
(
"忽略本节点发出的消息,sourceNodeId={}"
,
sourceNodeId
);
return
;
}
// 提取房间号
String
roomId
=
json
.
get
(
"roomId"
).
asText
();
// 提取消息内容
String
msg
=
json
.
get
(
"message"
).
asText
();
// 提取需要排除的会话 ID(即原始发送者,避免消息回环)
String
excludeSessionId
=
json
.
has
(
"excludeSessionId"
)
?
json
.
get
(
"excludeSessionId"
).
asText
()
:
null
;
// 从本地内存中获取该房间在本节点的所有会话
Set
<
Session
>
sessions
=
ROOMS
.
get
(
roomId
);
// 如果本节点没有该房间的会话,直接忽略(其他节点会处理)
if
(
sessions
==
null
||
sessions
.
isEmpty
())
{
return
;
}
// 遍历该房间在本节点的所有会话
for
(
Session
s
:
sessions
)
{
// 跳过需要排除的会话(即消息发送者所在的节点已处理,本节点不再转发给同一个人)
if
(
excludeSessionId
!=
null
&&
excludeSessionId
.
equals
(
s
.
getId
()))
{
continue
;
}
// 检查会话是否还处于打开状态
if
(
s
.
isOpen
())
{
// 发送消息给客户端
s
.
getBasicRemote
().
sendText
(
msg
);
}
}
}
catch
(
Exception
e
)
{
// 处理消息异常,记录日志但不影响其他节点
log
.
error
(
"处理跨节点消息异常"
,
e
);
}
}
// ==================== WebSocket 生命周期 ====================
/**
* Jackson 对象映射器,用于解析 JSON 消息
*/
private
final
ObjectMapper
objectMapper
=
new
ObjectMapper
();
@OnOpen
public
void
onOpen
(
Session
session
,
@PathParam
(
"roomId"
)
String
roomId
)
{
log
.
info
(
"========== WebSocket 连接请求开始 =========="
);
log
.
info
(
"房间号: {}, 会话ID: {}"
,
roomId
,
session
.
getId
());
// 1. 解析 URL 查询参数
String
queryString
=
session
.
getQueryString
();
log
.
info
(
"查询字符串: {}"
,
queryString
);
Map
<
String
,
String
>
params
=
parseQueryString
(
queryString
);
String
userId
=
params
.
getOrDefault
(
"userId"
,
"unknown"
);
String
userType
=
params
.
getOrDefault
(
"userType"
,
"unknown"
);
log
.
info
(
"解析到的 userId: {}, userType: {}"
,
userId
,
userType
);
// 2. 检查关键依赖是否注入成功
log
.
info
(
"检查依赖注入 - sessionService: {}, redisTemplate: {}, redissonClient: {}"
,
sessionService
==
null
?
"NULL"
:
"OK"
,
redisTemplate
==
null
?
"NULL"
:
"OK"
,
redissonClient
==
null
?
"NULL"
:
"OK"
);
if
(
sessionService
==
null
||
redisTemplate
==
null
||
redissonClient
==
null
)
{
log
.
error
(
"依赖注入失败,无法处理连接!"
);
try
{
session
.
close
(
new
CloseReason
(
CloseReason
.
CloseCodes
.
UNEXPECTED_CONDITION
,
"服务未就绪"
));
}
catch
(
IOException
e
)
{
log
.
error
(
"关闭session失败"
,
e
);
}
return
;
}
try
{
// 3. 加入本地内存
log
.
info
(
"将会话加入本地内存 ROOMS..."
);
ROOMS
.
computeIfAbsent
(
roomId
,
k
->
new
CopyOnWriteArraySet
<>()).
add
(
session
);
SESSION_ROOM
.
put
(
session
,
roomId
);
SESSION_USER_ID
.
put
(
session
,
userId
);
SESSION_USER_TYPE
.
put
(
session
,
userType
);
log
.
info
(
"本地内存添加成功,当前房间 {} 的会话数: {}"
,
roomId
,
ROOMS
.
get
(
roomId
).
size
());
// 4. 将成员信息存入 Redis
String
memberKey
=
String
.
format
(
ROOM_MEMBERS_KEY
,
roomId
);
Map
<
String
,
String
>
memberInfo
=
new
HashMap
<>();
memberInfo
.
put
(
"userId"
,
userId
);
memberInfo
.
put
(
"userType"
,
userType
);
memberInfo
.
put
(
"sessionId"
,
session
.
getId
());
log
.
info
(
"准备将成员信息存入 Redis, key: {}, info: {}"
,
memberKey
,
memberInfo
);
try
{
redisTemplate
.
opsForHash
().
put
(
memberKey
,
session
.
getId
(),
JSON
.
toJSONString
(
memberInfo
));
redisTemplate
.
expire
(
memberKey
,
60
,
TimeUnit
.
MINUTES
);
log
.
info
(
"Redis 成员信息存储成功"
);
}
catch
(
Exception
e
)
{
log
.
error
(
"Redis 成员信息存储失败"
,
e
);
throw
e
;
// 继续抛出以便外层捕获
}
// 5. 状态同步
log
.
info
(
"开始状态同步..."
);
try
{
// 5.1 获取控制权
RoomRedisInfoDTO
dto
=
sessionService
.
getCurrentController
(
roomId
);
String
controlHolderType
=
dto
.
getControlHolderType
();
String
controlHolderId
=
dto
.
getControlHolderId
();
log
.
info
(
"获取控制权成功: holderType={}, holderId={}"
,
controlHolderType
,
controlHolderId
);
// 5.2 获取当前页面状态
String
stateKey
=
String
.
format
(
ROOM_STATE_PAGE_KEY
,
roomId
);
String
currentPageJson
=
redisTemplate
.
opsForValue
().
get
(
stateKey
);
log
.
info
(
"从Redis获取当前页面状态: {}"
,
currentPageJson
);
if
(
StringUtils
.
isBlank
(
currentPageJson
))
{
CoSession
coSession
=
sessionService
.
getByRoomId
(
roomId
);
currentPageJson
=
coSession
!=
null
?
coSession
.
getCurrentPage
()
:
"{}"
;
if
(
StringUtils
.
isNotBlank
(
currentPageJson
))
{
redisTemplate
.
opsForValue
().
set
(
stateKey
,
currentPageJson
,
60
,
TimeUnit
.
MINUTES
);
log
.
info
(
"从数据库获取并回填Redis状态: {}"
,
currentPageJson
);
}
else
{
log
.
warn
(
"未找到当前页面状态,使用默认空对象"
);
}
}
// 5.3 发送初始化消息
String
initMsg
=
String
.
format
(
"{\"action\":\"init_sync\",\"holderType\":\"%s\",\"holderId\":\"%s\",\"currentPage\":%s}"
,
controlHolderType
,
controlHolderId
,
currentPageJson
);
session
.
getBasicRemote
().
sendText
(
initMsg
);
log
.
info
(
"发送 init_sync 消息: {}"
,
initMsg
);
String
controlMsg
=
String
.
format
(
"{\"action\":\"control_transfer\",\"holderType\":\"%s\",\"holderId\":\"%s\"}"
,
controlHolderType
,
controlHolderId
);
session
.
getBasicRemote
().
sendText
(
controlMsg
);
log
.
info
(
"发送 control_transfer 消息: {}"
,
controlMsg
);
log
.
info
(
"用户 {} 加入房间 {},状态同步完成"
,
userId
,
roomId
);
}
catch
(
Exception
e
)
{
log
.
error
(
"状态同步过程中发生异常"
,
e
);
// 状态同步失败不影响连接建立,但应告知客户端(可选)
// 这里重新抛出以便外层统一处理
throw
e
;
}
log
.
info
(
"========== WebSocket 连接处理完成 =========="
);
}
catch
(
Exception
e
)
{
log
.
error
(
"WebSocket 连接处理失败,房间号: {}, 用户: {}"
,
roomId
,
userId
,
e
);
// 发送错误消息给客户端(可选)
try
{
session
.
getBasicRemote
().
sendText
(
"{\"error\":\"服务器处理异常: "
+
e
.
getMessage
()
+
"\"}"
);
}
catch
(
IOException
ex
)
{
log
.
error
(
"发送错误消息失败"
,
ex
);
}
// 关闭连接
try
{
session
.
close
(
new
CloseReason
(
CloseReason
.
CloseCodes
.
UNEXPECTED_CONDITION
,
"服务器错误: "
+
e
.
getMessage
()));
}
catch
(
IOException
ex
)
{
log
.
error
(
"关闭session失败"
,
ex
);
}
}
}
/**
* WebSocket 连接建立时触发
* 1. 解析 URL 查询参数(userId, userType)
* 2. 将当前会话加入本地内存(ROOMS, SESSION_ROOM 等)
* 3. 将成员信息存入 Redis(跨节点共享)
* 4. 从 Redis 拉取当前房间状态并同步给新连接的用户(状态同步)
*
* @param session 当前 WebSocket 会话对象
* @param roomId 路径参数:房间号
*/
// @OnOpen
// public void onOpen(Session session, @PathParam("roomId") String roomId) {
// // 1. 获取 URL 查询字符串(如 ?userId=123&userType=owner)
// String queryString = session.getQueryString();
// // 解析查询参数为 Map
// Map<String, String> params = parseQueryString(queryString);
// // 获取用户ID,默认为 "unknown"
// String userId = params.getOrDefault("userId", "unknown");
// // 获取用户类型,默认为 "unknown"(owner: 客户, participant: 顾问)
// String userType = params.getOrDefault("userType", "unknown");
//
// // 2. 将当前会话加入本地内存
// // computeIfAbsent: 如果房间不存在则创建新的 Set 集合
// ROOMS.computeIfAbsent(roomId, k -> new CopyOnWriteArraySet<>()).add(session);
// // 记录会话对应的房间号
// SESSION_ROOM.put(session, roomId);
// // 记录会话对应的用户ID
// SESSION_USER_ID.put(session, userId);
// // 记录会话对应的用户类型
// SESSION_USER_TYPE.put(session, userType);
//
// // 3. 将成员信息存入 Redis(供跨节点查询使用)
// // 构造 Redis Hash 的键名:room:members:{roomId}
// String memberKey = String.format(ROOM_MEMBERS_KEY, roomId);
// // 构造成员信息 Map
// Map<String, String> memberInfo = new HashMap<>();
// memberInfo.put("userId", userId);
// memberInfo.put("userType", userType);
// memberInfo.put("sessionId", session.getId()); // sessionId 作为唯一标识
// // 以 sessionId 为 field,成员信息 JSON 为 value,存入 Redis Hash
// redisTemplate.opsForHash().put(memberKey, session.getId(), JSON.toJSONString(memberInfo));
// // 设置 Hash 过期时间为 60 分钟(与会话超时一致)
// redisTemplate.expire(memberKey, 60, TimeUnit.MINUTES);
//
// // 4. 状态同步(核心功能):向新用户推送当前房间的完整状态
// try {
// // 4.1 获取当前控制权信息(从 Redis 缓存中读取)
// RoomRedisInfoDTO dto = sessionService.getCurrentController(roomId);
// // 控制权持有者类型:owner 或 participant
// String controlHolderType = dto.getControlHolderType();
// // 控制权持有者ID
// String controlHolderId = dto.getControlHolderId();
//
// // 4.2 获取当前页面状态(从 Redis 中读取,由翻页操作实时更新)
// String stateKey = String.format(ROOM_STATE_PAGE_KEY, roomId);
// String currentPageJson = redisTemplate.opsForValue().get(stateKey);
//
// // 如果 Redis 中没有页面状态,则从数据库读取最新状态
// if (StringUtils.isBlank(currentPageJson)) {
// // 根据房间号查询数据库中的会话记录
// CoSession coSession = sessionService.getByRoomId(roomId);
// // 如果会话存在,取其中的当前页面 JSON
// currentPageJson = coSession != null ? coSession.getCurrentPage() : "{}";
// // 将数据库中的状态回填到 Redis,供后续新节点快速同步
// if (StringUtils.isNotBlank(currentPageJson)) {
// redisTemplate.opsForValue().set(stateKey, currentPageJson, 60, TimeUnit.MINUTES);
// }
// }
//
// // 4.3 组装初始化同步消息(包含控制权和当前页面)
// // 消息格式:{"action":"init_sync","holderType":"xxx","holderId":"xxx","currentPage":{...}}
// String initMsg = String.format(
// "{\"action\":\"init_sync\",\"holderType\":\"%s\",\"holderId\":\"%s\",\"currentPage\":%s}",
// controlHolderType, controlHolderId, currentPageJson
// );
// // 发送初始化同步消息给刚连接的客户端
// session.getBasicRemote().sendText(initMsg);
//
// // 4.4 单独再推送一次控制权消息(兼容前端只监听 control_transfer 的情况)
// String controlMsg = String.format(
// "{\"action\":\"control_transfer\",\"holderType\":\"%s\",\"holderId\":\"%s\"}",
// controlHolderType, controlHolderId
// );
// session.getBasicRemote().sendText(controlMsg);
//
// // 记录日志:用户已加入并完成状态同步
// log.info("用户 {} 加入房间 {},已同步状态", userId, roomId);
// } catch (Exception e) {
// // 状态同步失败,记录错误但不影响连接建立
// log.error("状态同步失败,用户 {} 可能无法恢复最新状态", userId, e);
// }
//
// // 记录连接建立日志,包含当前节点该房间的会话数
// log.info("用户 {} ({}) 加入房间 {}", userId, userType, roomId);
// }
/**
* 接收客户端发送的 WebSocket 消息(核心业务分发器)
* 根据 action 类型分发到不同的业务处理逻辑
*
* @param session 当前会话
* @param message 客户端发送的 JSON 字符串
*/
@OnMessage
public
void
onMessage
(
Session
session
,
String
message
)
{
// 获取 IP
String
remoteIp
=
getClientIp
(
session
);
IpContextHolder
.
setIp
(
remoteIp
);
IpContextHolder
.
setDevice
(
null
);
// 根据会话查询对应的房间号
String
roomId
=
SESSION_ROOM
.
get
(
session
);
if
(
roomId
==
null
)
{
log
.
warn
(
"【WebSocket】会话未关联房间,忽略消息"
);
return
;
}
// 打印收到的原始消息
log
.
info
(
"【WebSocket】收到消息,房间号: {}, 消息内容: {}"
,
roomId
,
message
);
try
{
// 1. 解析 JSON 消息
JsonNode
json
=
objectMapper
.
readTree
(
message
);
String
action
=
json
.
get
(
"action"
).
asText
();
String
userId
=
SESSION_USER_ID
.
get
(
session
);
String
userType
=
SESSION_USER_TYPE
.
get
(
session
);
log
.
info
(
"【WebSocket】解析结果: action={}, userId={}, userType={}"
,
action
,
userId
,
userType
);
// 2. 获取会话业务ID(用于操作日志记录)
CoSession
coSession
=
sessionService
.
getByRoomId
(
roomId
);
String
bizId
=
coSession
!=
null
?
coSession
.
getCoSessionBizId
()
:
null
;
// ========== 业务逻辑分发 ==========
// --- 操作1:控制权切换(仅顾问可操作) ---
if
(
"control_transfer"
.
equals
(
action
))
{
// 打印开始处理日志,包含当前操作用户类型(owner/participant)
log
.
info
(
"【控制权切换】开始处理,当前用户类型: {}"
,
userType
);
// 校验权限:只有参与者(顾问)才能切换控制权
if
(!
"participant"
.
equals
(
userType
))
{
log
.
warn
(
"【控制权切换】权限不足,非顾问用户尝试切换,userType={}"
,
userType
);
sendError
(
session
,
"只有顾问可以切换控制权"
);
return
;
}
// 从WebSocket消息中解析目标控制权持有者类型(owner/participant)
// 注意:前端可能不传递 holderId 或传递空值,因此不能直接信任
String
newHolderType
=
json
.
get
(
"holderType"
).
asText
();
log
.
info
(
"【控制权切换】目标控制者类型: holderType={}"
,
newHolderType
);
// 构造控制权切换请求
TransferControlRequest
req
=
new
TransferControlRequest
();
req
.
setRoomId
(
roomId
);
// 根据目标类型确定操作类型:1=授权客户(owner),2=收回控制权(participant)
Integer
oprType
=
ControlHolderTypeEnum
.
OWNER
.
getItemValue
().
equals
(
newHolderType
)
?
1
:
2
;
req
.
setOprType
(
oprType
);
log
.
info
(
"【控制权切换】操作类型: {}"
,
oprType
);
// 调用服务层切换控制权(更新数据库 + Redis)
try
{
sessionService
.
transferControl
(
req
);
log
.
info
(
"【控制权切换】服务层调用成功,数据库和Redis已更新"
);
}
catch
(
Exception
e
)
{
log
.
error
(
"【控制权切换】服务层调用失败"
,
e
);
sendError
(
session
,
"切换控制权失败:"
+
e
.
getMessage
());
return
;
}
// 重新从 Redis(或数据库)获取最新的控制者信息,确保准确性
// 因为服务层已经更新了控制权,此时 Redis 中的值是最新的
RoomRedisInfoDTO
latest
=
sessionService
.
getCurrentController
(
roomId
);
String
correctHolderType
=
latest
.
getControlHolderType
();
String
correctHolderId
=
latest
.
getControlHolderId
();
log
.
info
(
"【控制权切换】最新控制者: holderType={}, holderId={}"
,
correctHolderType
,
correctHolderId
);
// 构造包含正确 holderId 的广播消息,避免前端因空 ID 无法匹配
String
correctMsg
=
String
.
format
(
"{\"action\":\"control_transfer\",\"holderType\":\"%s\",\"holderId\":\"%s\"}"
,
correctHolderType
,
correctHolderId
);
// 广播正确的控制权变更消息给房间内所有用户(包括自己,以便本端也能更新状态)
broadcast
(
roomId
,
correctMsg
,
session
);
log
.
info
(
"【控制权切换】已广播正确的控制权消息: {}"
,
correctMsg
);
// 记录操作日志(用于合规审计)
if
(
bizId
!=
null
)
{
operationLogService
.
log
(
bizId
,
userId
,
userType
,
userId
,
action
,
correctMsg
,
IpContextHolder
.
getDevice
(),
IpContextHolder
.
getIp
());
}
log
.
info
(
"控制权切换成功: roomId={}, holder={}:{}"
,
roomId
,
correctHolderType
,
correctHolderId
);
return
;
}
// --- 操作2:结束共享(仅客户可操作) ---
if
(
"end_sharing"
.
equals
(
action
))
{
log
.
info
(
"【结束共享】开始处理,当前用户类型: {}"
,
userType
);
// 校验权限:只有所有者(客户)才能结束共享
if
(!
"owner"
.
equals
(
userType
))
{
log
.
warn
(
"【结束共享】权限不足,非客户用户尝试结束共享,userType={}"
,
userType
);
sendError
(
session
,
"只有客户可以结束共享"
);
return
;
}
// 构造结束会话请求
EndSessionRequest
req
=
new
EndSessionRequest
();
req
.
setRoomId
(
roomId
);
log
.
info
(
"【结束共享】调用服务层结束会话"
);
sessionService
.
end
(
req
);
// 广播结束消息给所有用户
broadcast
(
roomId
,
"{\"action\":\"end_sharing\"}"
,
null
);
log
.
info
(
"【结束共享】结束消息已广播"
);
// 清理 Redis 中的页面状态
redisTemplate
.
delete
(
String
.
format
(
ROOM_STATE_PAGE_KEY
,
roomId
));
log
.
info
(
"【结束共享】Redis页面状态已清理"
);
// 关闭房间所有连接并清理资源
closeRoom
(
roomId
);
log
.
info
(
"共享已结束: roomId={}"
,
roomId
);
if
(
bizId
!=
null
)
{
operationLogService
.
log
(
bizId
,
userId
,
userType
,
userId
,
"end_sharing"
,
"结束共享"
,
IpContextHolder
.
getDevice
(),
IpContextHolder
.
getIp
()
);
}
return
;
}
// --- 操作3:脱敏开关(客户操作,全房间生效) ---
if
(
"desensitization_switch"
.
equals
(
action
))
{
log
.
info
(
"【脱敏开关】收到请求,enabled={}"
,
json
.
get
(
"enabled"
).
asBoolean
());
// 广播脱敏状态给所有人(同步显示脱敏效果)
broadcast
(
roomId
,
message
,
session
);
log
.
info
(
"【脱敏开关】广播消息已发送"
);
// 记录操作日志
if
(
bizId
!=
null
)
{
operationLogService
.
log
(
bizId
,
userId
,
userType
,
userId
,
action
,
message
,
IpContextHolder
.
getDevice
(),
IpContextHolder
.
getIp
());
}
return
;
}
// --- 操作4:同步操作(翻页/滚动/缩放)——仅控制权持有者可操作 ---
if
(
"turn_the_page"
.
equals
(
action
)
||
"scroll"
.
equals
(
action
)
||
"scaling"
.
equals
(
action
))
{
log
.
info
(
"【同步操作】开始处理,action={}, 当前用户类型={}"
,
action
,
userType
);
// 4.1 获取当前控制权持有者信息
RoomRedisInfoDTO
dto
=
sessionService
.
getCurrentController
(
roomId
);
String
holderType
=
dto
.
getControlHolderType
();
String
holderId
=
dto
.
getControlHolderId
();
log
.
info
(
"【同步操作】当前控制权: holderType={}, holderId={}"
,
holderType
,
holderId
);
// 4.2 校验:当前用户是否持有控制权
boolean
isController
=
holderType
.
equals
(
userType
)
&&
holderId
.
equals
(
userId
);
log
.
info
(
"【同步操作】是否持有控制权: {}"
,
isController
);
if
(!
isController
)
{
log
.
warn
(
"【同步操作】用户无控制权,拒绝操作,userType={}, userId={}"
,
userType
,
userId
);
sendError
(
session
,
"您没有控制权,无法操作"
);
return
;
}
// 4.3 如果是翻页或滚动操作,需要更新数据库和 Redis 状态
if
(
"turn_the_page"
.
equals
(
action
)
||
"scroll"
.
equals
(
action
))
{
// 获取当前页面 JSON(从消息中提取)
String
currentPage
=
json
.
has
(
"currentPage"
)
?
json
.
get
(
"currentPage"
).
toString
()
:
"{}"
;
log
.
info
(
"【同步操作】更新页面状态: {}"
,
currentPage
);
// 更新数据库中的当前页面和页面历史轨迹
sessionService
.
updateCurrentPage
(
roomId
,
currentPage
,
userId
);
log
.
info
(
"【同步操作】数据库已更新"
);
// 更新 Redis 状态(供新节点连接时同步)
String
stateKey
=
String
.
format
(
ROOM_STATE_PAGE_KEY
,
roomId
);
redisTemplate
.
opsForValue
().
set
(
stateKey
,
currentPage
,
60
,
TimeUnit
.
MINUTES
);
log
.
info
(
"【同步操作】Redis状态已更新"
);
}
// 4.4 广播操作指令给房间内其他用户(排除自己,避免回环)
broadcast
(
roomId
,
message
,
session
);
log
.
info
(
"【同步操作】广播消息已发送"
);
// 4.5 记录操作日志(合规审计)
if
(
bizId
!=
null
)
{
operationLogService
.
log
(
bizId
,
userId
,
userType
,
userId
,
action
,
message
,
IpContextHolder
.
getDevice
(),
IpContextHolder
.
getIp
());
}
return
;
}
// --- 操作5:WebRTC 信令转发(Offer/Answer/ICE) ---
if
(
"webrtc_offer"
.
equals
(
action
)
||
"webrtc_answer"
.
equals
(
action
)
||
"webrtc_ice_candidate"
.
equals
(
action
))
{
log
.
info
(
"【WebRTC信令】转发 {} 消息, roomId={}, 发送者={}"
,
action
,
roomId
,
userId
);
// 打印信令内容摘要
if
(
"webrtc_offer"
.
equals
(
action
)
||
"webrtc_answer"
.
equals
(
action
))
{
String
sdpType
=
json
.
has
(
"sdp"
)
?
json
.
get
(
"sdp"
).
get
(
"type"
).
asText
()
:
"unknown"
;
log
.
info
(
"【WebRTC信令】SDP类型: {}"
,
sdpType
);
}
else
{
String
candidate
=
json
.
has
(
"candidate"
)
?
json
.
get
(
"candidate"
).
get
(
"candidate"
).
asText
()
:
"unknown"
;
log
.
info
(
"【WebRTC信令】ICE候选: {}"
,
candidate
);
}
// 广播给房间内其他人(不包含发送者)
broadcast
(
roomId
,
message
,
session
);
return
;
}
if
(
"request_offer"
.
equals
(
action
))
{
log
.
info
(
"收到 request_offer 请求,转发给房间内其他成员"
);
// 广播给房间内其他人(不包括发送者)
broadcast
(
roomId
,
message
,
session
);
return
;
}
// ========== 投屏切换 ==========
if
(
"presenter_change"
.
equals
(
action
)
||
"presenter_invite"
.
equals
(
action
)
||
"presenter_response"
.
equals
(
action
)
||
"presenter_revoke"
.
equals
(
action
))
{
if
(
"presenter_invite"
.
equals
(
action
))
{
// 强制发送给客户(owner),因为投屏邀请的目标一定是客户
String
targetUserType
=
"owner"
;
log
.
info
(
"投屏邀请定向发送给: {}"
,
targetUserType
);
sendToUserType
(
roomId
,
targetUserType
,
message
);
}
else
if
(
"presenter_response"
.
equals
(
action
))
{
boolean
accept
=
json
.
has
(
"accept"
)
&&
json
.
get
(
"accept"
).
asBoolean
();
if
(
accept
)
{
String
target
=
json
.
has
(
"target"
)
?
json
.
get
(
"target"
).
asText
()
:
null
;
if
(
target
!=
null
)
{
String
mappedTarget
=
"customer"
.
equals
(
target
)
?
"owner"
:
"consultant"
.
equals
(
target
)
?
"participant"
:
target
;
PRESENTER_MAP
.
put
(
roomId
,
mappedTarget
);
log
.
info
(
"房间 {} 投屏者更新为: {} (来自 presenter_response 接受)"
,
roomId
,
mappedTarget
);
}
}
broadcast
(
roomId
,
message
,
session
);
}
else
if
(
"presenter_change"
.
equals
(
action
))
{
String
from
=
json
.
has
(
"from"
)
?
json
.
get
(
"from"
).
asText
()
:
null
;
String
target
=
json
.
has
(
"target"
)
?
json
.
get
(
"target"
).
asText
()
:
null
;
if
(
target
!=
null
)
{
String
mappedTarget
=
"customer"
.
equals
(
target
)
?
"owner"
:
"consultant"
.
equals
(
target
)
?
"participant"
:
target
;
// 如果 from 为空,或 from == target(自己发起),或当前无人投屏,则更新
if
(
from
==
null
||
from
.
equals
(
target
)
||
PRESENTER_MAP
.
get
(
roomId
)
==
null
)
{
PRESENTER_MAP
.
put
(
roomId
,
mappedTarget
);
log
.
info
(
"房间 {} 投屏者更新为: {} (来自 presenter_change)"
,
roomId
,
mappedTarget
);
}
}
broadcast
(
roomId
,
message
,
session
);
}
else
if
(
"presenter_revoke"
.
equals
(
action
))
{
PRESENTER_MAP
.
remove
(
roomId
);
log
.
info
(
"房间 {} 投屏者已清除"
,
roomId
);
broadcast
(
roomId
,
message
,
session
);
}
return
;
}
// ========== 远程控制 ==========
if
(
"request_remote_control"
.
equals
(
action
)
||
"remote_control_response"
.
equals
(
action
)
||
"remote_control_stop"
.
equals
(
action
))
{
if
(
"request_remote_control"
.
equals
(
action
))
{
String
presenterType
=
PRESENTER_MAP
.
get
(
roomId
);
if
(
presenterType
==
null
)
{
// 后备:从数据库加载控制者
CoSession
sessionFromDb
=
sessionService
.
getByRoomId
(
roomId
);
if
(
sessionFromDb
!=
null
)
{
String
holderType
=
sessionFromDb
.
getControlHolderType
();
if
(
holderType
!=
null
)
{
presenterType
=
holderType
;
PRESENTER_MAP
.
put
(
roomId
,
presenterType
);
log
.
info
(
"从数据库加载投屏者: roomId={}, holderType={}"
,
roomId
,
presenterType
);
}
}
}
if
(
presenterType
!=
null
)
{
sendToUserType
(
roomId
,
presenterType
,
message
);
}
else
{
log
.
warn
(
"房间 {} 无人投屏,忽略远程控制请求"
,
roomId
);
sendError
(
session
,
"当前无人投屏,无法请求远程控制"
);
}
}
else
{
// response 或 stop 广播给所有人
broadcast
(
roomId
,
message
,
session
);
}
return
;
}
// --- 未知操作:记录警告日志 ---
log
.
warn
(
"未知 action: {}"
,
action
);
}
catch
(
Exception
e
)
{
// 处理消息异常,向客户端返回错误信息
log
.
error
(
"处理WebSocket消息异常,消息内容: {}"
,
message
,
e
);
sendError
(
session
,
"服务器处理异常"
);
}
finally
{
//清除上下文,防止内存泄漏
IpContextHolder
.
clear
();
}
}
/**
* WebSocket 连接关闭时触发
* 1. 从本地内存中移除该会话
* 2. 从 Redis 中移除该成员信息
* 3. 如果房间为空,清理本地房间映射
*
* @param session 关闭的会话
*/
@OnClose
public
void
onClose
(
Session
session
)
{
// 从反向映射中移除会话,获取该会话对应的房间号
String
roomId
=
SESSION_ROOM
.
remove
(
session
);
if
(
roomId
!=
null
)
{
// 从本地内存中获取该房间的会话集合
Set
<
Session
>
sessions
=
ROOMS
.
get
(
roomId
);
if
(
sessions
!=
null
)
{
// 从集合中移除当前会话
sessions
.
remove
(
session
);
// 如果集合为空,移除该房间的映射,释放内存
if
(
sessions
.
isEmpty
())
{
ROOMS
.
remove
(
roomId
);
}
}
// 从 Redis 中移除该成员信息
String
memberKey
=
String
.
format
(
ROOM_MEMBERS_KEY
,
roomId
);
redisTemplate
.
opsForHash
().
delete
(
memberKey
,
session
.
getId
());
}
// 移除会话对应的用户ID映射
SESSION_USER_ID
.
remove
(
session
);
// 移除会话对应的用户类型映射
SESSION_USER_TYPE
.
remove
(
session
);
// 记录连接关闭日志
log
.
info
(
"WebSocket 关闭: sessionId={}"
,
session
.
getId
());
}
/**
* WebSocket 发生异常时触发
*
* @param session 发生异常的会话
* @param error 异常信息
*/
@OnError
public
void
onError
(
Session
session
,
Throwable
error
)
{
log
.
error
(
"WebSocket 错误"
,
error
);
}
// ==================== 广播方法 ====================
/**
* 向房间内所有成员广播消息(支持跨节点)
* 1. 本节点直接发送:遍历本地 ROOMS 中的会话,直接发送消息
* 2. 跨节点广播:将消息发布到 Redis,其他节点的订阅者收到后会转发给各自的客户端
*
* @param roomId 房间号
* @param message JSON 消息内容
* @param exclude 本节点需要排除的会话(通常为消息发送者,避免回环)
*/
private
void
broadcast
(
String
roomId
,
String
message
,
Session
exclude
)
{
// 1. 本节点直接发送消息给当前节点内的所有会话
Set
<
Session
>
localSessions
=
ROOMS
.
get
(
roomId
);
if
(
localSessions
!=
null
&&
!
localSessions
.
isEmpty
())
{
for
(
Session
s
:
localSessions
)
{
// 跳过需要排除的会话(发送者自己)
if
(
s
!=
exclude
&&
s
.
isOpen
())
{
try
{
// 发送消息
s
.
getBasicRemote
().
sendText
(
message
);
}
catch
(
IOException
e
)
{
// 发送失败,忽略(日志不打印,避免刷屏)
}
}
}
}
// 2. 跨节点广播(发布到 Redisson RTopic)
Map
<
String
,
String
>
data
=
new
HashMap
<>();
data
.
put
(
"roomId"
,
roomId
);
data
.
put
(
"message"
,
message
);
data
.
put
(
"sourceNodeId"
,
NODE_ID
);
if
(
exclude
!=
null
)
{
data
.
put
(
"excludeSessionId"
,
exclude
.
getId
());
}
String
jsonMsg
=
JSON
.
toJSONString
(
data
);
RTopic
topic
=
redissonClient
.
getTopic
(
"coordination:channel"
);
topic
.
publish
(
jsonMsg
);
}
/**
* 向指定会话发送错误消息
*
* @param session 目标会话
* @param errorMsg 错误描述信息
*/
private
void
sendError
(
Session
session
,
String
errorMsg
)
{
try
{
// 发送 JSON 格式的错误消息
session
.
getBasicRemote
().
sendText
(
"{\"error\":\""
+
errorMsg
+
"\"}"
);
}
catch
(
IOException
e
)
{
// 发送失败,忽略
}
}
/**
* 关闭房间所有连接并清理 Redis 状态
* 在结束共享时调用
*
* @param roomId 房间号
*/
private
void
closeRoom
(
String
roomId
)
{
// 1. 从本地内存移除该房间并获取所有会话
Set
<
Session
>
sessions
=
ROOMS
.
remove
(
roomId
);
if
(
sessions
!=
null
)
{
// 遍历所有会话,逐个关闭连接
for
(
Session
s
:
sessions
)
{
try
{
// 正常关闭连接,附带关闭原因
s
.
close
(
new
CloseReason
(
CloseReason
.
CloseCodes
.
NORMAL_CLOSURE
,
"会话结束"
));
}
catch
(
IOException
e
)
{
// 关闭失败,忽略
}
}
}
PRESENTER_MAP
.
remove
(
roomId
);
// 2. 清理 Redis 中的成员信息 Hash
redisTemplate
.
delete
(
String
.
format
(
ROOM_MEMBERS_KEY
,
roomId
));
// 3. 清理 Redis 中的页面状态
redisTemplate
.
delete
(
String
.
format
(
ROOM_STATE_PAGE_KEY
,
roomId
));
// 注意:控制权缓存由 sessionService.end() 负责清理
}
// ==================== 工具方法 ====================
/**
* 解析 URL 查询参数字符串
* 示例输入: "userId=123&userType=owner"
* 示例输出: {"userId": "123", "userType": "owner"}
*
* @param queryString 原始查询字符串(不含问号)
* @return 解析后的参数键值对 Map
*/
private
Map
<
String
,
String
>
parseQueryString
(
String
queryString
)
{
Map
<
String
,
String
>
result
=
new
HashMap
<>();
// 如果查询字符串为空,返回空 Map
if
(
queryString
==
null
||
queryString
.
isEmpty
())
{
return
result
;
}
// 按 & 符号分割多个参数
for
(
String
param
:
queryString
.
split
(
"&"
))
{
// 按 = 分割键和值
String
[]
pair
=
param
.
split
(
"="
,
2
);
// 解码键名
String
key
=
pair
.
length
>
0
?
decode
(
pair
[
0
])
:
""
;
// 解码值
String
value
=
pair
.
length
>
1
?
decode
(
pair
[
1
])
:
""
;
// 如果键不为空,存入 Map
if
(!
key
.
isEmpty
())
{
result
.
put
(
key
,
value
);
}
}
return
result
;
}
/**
* URL 解码(UTF-8)
*
* @param value 需要解码的字符串
* @return 解码后的字符串,如果解码失败则返回原值
*/
private
String
decode
(
String
value
)
{
try
{
// 使用 UTF-8 进行 URL 解码
return
URLDecoder
.
decode
(
value
,
StandardCharsets
.
UTF_8
.
name
());
}
catch
(
Exception
e
)
{
// 解码失败,返回原值
return
value
;
}
}
// ==================== 获取客户端 IP 的工具方法 ====================
/**
* 获取客户端 IP(由于 javax.websocket.Session 接口没有 getRemoteAddress,通过反射调用)
*/
private
String
getClientIp
(
Session
session
)
{
try
{
// 尝试直接通过反射调用 getRemoteAddress(Tomcat 等实现类有该方法)
Method
method
=
session
.
getClass
().
getMethod
(
"getRemoteAddress"
);
Object
result
=
method
.
invoke
(
session
);
if
(
result
instanceof
InetSocketAddress
)
{
InetSocketAddress
address
=
(
InetSocketAddress
)
result
;
return
address
.
getAddress
().
getHostAddress
();
}
}
catch
(
Exception
e
)
{
log
.
debug
(
"通过反射获取 getRemoteAddress 失败,尝试其他方式"
);
}
// 备选:从请求参数中获取(前端可传 ?ip=xxx)
Map
<
String
,
List
<
String
>>
params
=
session
.
getRequestParameterMap
();
if
(
params
!=
null
&&
params
.
containsKey
(
"ip"
))
{
List
<
String
>
ips
=
params
.
get
(
"ip"
);
if
(
ips
!=
null
&&
!
ips
.
isEmpty
())
{
return
ips
.
get
(
0
);
}
}
// 再备选:从 getUserProperties 中获取(如由 configurator 存储)
Object
storedIp
=
session
.
getUserProperties
().
get
(
"remoteIp"
);
if
(
storedIp
!=
null
)
{
return
storedIp
.
toString
();
}
return
"unknown"
;
}
/**
* 向房间内指定用户类型的所有用户发送消息(定向发送)
*/
private
void
sendToUserType
(
String
roomId
,
String
targetUserType
,
String
message
)
{
// 将前端传入的 customer/consultant 映射为 owner/participant
String
mappedType
=
targetUserType
;
if
(
"customer"
.
equals
(
targetUserType
))
{
mappedType
=
"owner"
;
}
else
if
(
"consultant"
.
equals
(
targetUserType
))
{
mappedType
=
"participant"
;
}
log
.
info
(
"定向发送: roomId={}, 目标类型={} (映射为 {}), 消息={}"
,
roomId
,
targetUserType
,
mappedType
,
message
);
Set
<
Session
>
sessions
=
ROOMS
.
get
(
roomId
);
if
(
sessions
==
null
||
sessions
.
isEmpty
())
{
log
.
warn
(
"房间 {} 无会话,无法定向发送"
,
roomId
);
return
;
}
int
count
=
0
;
for
(
Session
s
:
sessions
)
{
String
userType
=
SESSION_USER_TYPE
.
get
(
s
);
if
(
mappedType
.
equals
(
userType
)
&&
s
.
isOpen
())
{
try
{
s
.
getBasicRemote
().
sendText
(
message
);
count
++;
}
catch
(
IOException
e
)
{
log
.
warn
(
"向用户 {} 发送消息失败: {}"
,
s
.
getId
(),
e
.
getMessage
());
}
}
}
log
.
info
(
"定向发送给 {} (映射为 {}),共 {} 个会话"
,
targetUserType
,
mappedType
,
count
);
}
}
\ No newline at end of file
yd-communication-api/src/main/resources/banner.txt
0 → 100644
View file @
ed85d9a7
${AnsiColor.GREEN}
_ _ _ __ __ ____ _ _ _ _
| | (_)_ __ | | _\ \ / /__ / ___| |__ __ _| |_ / \ _ __ (_)
| | | | '_ \| |/ /\ \ /\ / / _ \ | | '_ \ / _` | __| / _ \ | '_ \| |
| |___| | | | | < \ V V / __/ |___| | | | (_| | |_ / ___ \| |_) | |
|_____|_|_| |_|_|\_\ \_/\_/ \___|\____|_| |_|\__,_|\__/_/ \_\ .__/|_|
|_|
${AnsiColor.BRIGHT_WHITE}
Spring Boot Version: ${spring-boot.version}
\ No newline at end of file
yd-communication-api/src/main/resources/bootstrap.yml
0 → 100644
View file @
ed85d9a7
spring
:
profiles
:
active
:
test
# active: '@spring.profiles.active@'
---
spring
:
application
:
name
:
yd-communication-api
profiles
:
dev
main
:
allow-bean-definition-overriding
:
true
allow-circular-references
:
true
cloud
:
nacos
:
# 配置中心
config
:
# 命名空间id(此处不用public,因public初始化的空间, id为空) 4e237601-cea8-414d-b7b9-d7adc8cbcf95
namespace
:
22f9d61e-9011-4d45-88cb-24f9857e3eec
# nacos的ip地址和端口 120.79.64.17:10848
server-addr
:
127.0.0.1:8848
# 这个就表示 在我们nacos命名空间id为 dev中 有一个data-id 为 demo-service.yml 的配置文件 读取这个里面的配置
file-extension
:
yml
config-retry-time
:
300000
# 共享配置, 可以把公共配置放在同个命名空间下,然后创建一个 common.yml 文件 ,里面可以放共用的配置
shared-configs[0]
:
dataId
:
linkwe-common.yml
refresh
:
true
# 发布到注册中心 (如果没有使用可以不配)
discovery
:
# 命名空间id(此处不用public,因public初始化的空间, id为空)
namespace
:
${spring.cloud.nacos.config.namespace}
# nacos的ip地址和端口
server-addr
:
${spring.cloud.nacos.config.server-addr}
---
spring
:
application
:
name
:
yd-communication-api
profiles
:
test
main
:
allow-bean-definition-overriding
:
true
allow-circular-references
:
true
cloud
:
nacos
:
# 配置中心
config
:
# 命名空间id(此处不用public,因public初始化的空间, id为空)
namespace
:
b3b01715-eb85-4242-992a-5aff03d864d4
# nacos的ip地址和端口
server-addr
:
139.224.145.34:8848
# 这个就表示 在我们nacos命名空间id为 dev中 有一个data-id 为 demo-service.yml 的配置文件 读取这个里面的配置
file-extension
:
yml
config-retry-time
:
300000
# 共享配置, 可以把公共配置放在同个命名空间下,然后创建一个 common.yml 文件 ,里面可以放共用的配置
shared-configs[0]
:
dataId
:
yd-common.yml
group
:
YD_GROUP
refresh
:
true
extension-configs
:
# 扩展配置
-
data-id
:
yd-communication-api.yml
group
:
YD_GROUP
refresh
:
true
# 发布到注册中心 (如果没有使用可以不配)
discovery
:
# 命名空间id(此处不用public,因public初始化的空间, id为空)
namespace
:
${spring.cloud.nacos.config.namespace}
# nacos的ip地址和端口
server-addr
:
${spring.cloud.nacos.config.server-addr}
group
:
YD_GROUP
---
spring
:
profiles
:
prod
application
:
name
:
yd-communication-api
server
:
port
:
9482
main
:
allow-bean-definition-overriding
:
true
allow-circular-references
:
true
cloud
:
nacos
:
# 配置中心
config
:
# 命名空间id(此处不用public,因public初始化的空间, id为空)
namespace
:
cb587d6d-d3b2-45ca-a3ef-5b5c80ece5b3
# nacos的ip地址和端口
server-addr
:
139.224.150.79:8848
# 这个就表示 在我们nacos命名空间id为 dev中 有一个data-id 为 demo-service.yml 的配置文件 读取这个里面的配置
file-extension
:
yml
config-retry-time
:
300000
# 共享配置, 可以把公共配置放在同个命名空间下,然后创建一个 common.yml 文件 ,里面可以放共用的配置
shared-configs[0]
:
dataId
:
yd-common.yml
group
:
YD_GROUP
refresh
:
true
extension-configs
:
# 扩展配置
-
data-id
:
yd-communication-api.yml
group
:
YD_GROUP
refresh
:
true
# 发布到注册中心 (如果没有使用可以不配)
discovery
:
# 命名空间id(此处不用public,因public初始化的空间, id为空)
namespace
:
${spring.cloud.nacos.config.namespace}
# nacos的ip地址和端口
server-addr
:
${spring.cloud.nacos.config.server-addr}
group
:
YD_GROUP
yd-communication-api/src/main/resources/spy.properties
0 → 100644
View file @
ed85d9a7
#3.2.1\u4EE5\u4E0A\u4F7F\u7528
modulelist
=
com.baomidou.mybatisplus.extension.p6spy.MybatisPlusLogFactory,com.p6spy.engine.outage.P6OutageFactory
#3.2.1\u4EE5\u4E0B\u4F7F\u7528\u6216\u8005\u4E0D\u914D\u7F6E
#modulelist=com.p6spy.engine.logging.P6LogFactory,com.p6spy.engine.outage.P6OutageFactory
# \u81EA\u5B9A\u4E49\u65E5\u5FD7\u6253\u5370
logMessageFormat
=
com.baomidou.mybatisplus.extension.p6spy.P6SpyLogger
#\u65E5\u5FD7\u8F93\u51FA\u5230\u63A7\u5236\u53F0
appender
=
com.baomidou.mybatisplus.extension.p6spy.StdoutLogger
# \u4F7F\u7528\u65E5\u5FD7\u7CFB\u7EDF\u8BB0\u5F55 sql
#appender=com.p6spy.engine.spy.appender.Slf4JLogger
# \u8BBE\u7F6E p6spy driver \u4EE3\u7406
deregisterdrivers
=
true
# \u53D6\u6D88JDBC URL\u524D\u7F00
useprefix
=
true
# \u914D\u7F6E\u8BB0\u5F55 Log \u4F8B\u5916,\u53EF\u53BB\u6389\u7684\u7ED3\u679C\u96C6\u6709error,info,batch,debug,statement,commit,rollback,result,resultset.
excludecategories
=
info,debug,result,commit,resultset
# \u65E5\u671F\u683C\u5F0F
dateformat
=
yyyy-MM-dd HH:mm:ss
# \u5B9E\u9645\u9A71\u52A8\u53EF\u591A\u4E2A
#driverlist=org.h2.Driver
# \u662F\u5426\u5F00\u542F\u6162SQL\u8BB0\u5F55
outagedetection
=
true
# \u6162SQL\u8BB0\u5F55\u6807\u51C6 2 \u79D2
outagedetectioninterval
=
2
yd-communication-feign/pom.xml
View file @
ed85d9a7
...
...
@@ -24,6 +24,11 @@
<version>
${project.version}
</version>
</dependency>
<dependency>
<groupId>
com.yd
</groupId>
<artifactId>
yd-oss-feign
</artifactId>
<version>
${project.version}
</version>
</dependency>
<dependency>
<groupId>
com.fasterxml.jackson.core
</groupId>
<artifactId>
jackson-annotations
</artifactId>
</dependency>
...
...
yd-communication-feign/src/main/java/com/yd/communication/feign/client/ApiCoDesensitizationRuleFeignClient.java
0 → 100644
View file @
ed85d9a7
package
com
.
yd
.
communication
.
feign
.
client
;
import
com.yd.common.result.Result
;
import
com.yd.communication.feign.fallback.ApiCoDesensitizationRuleFeignFallbackFactory
;
import
com.yd.communication.feign.response.desensitization.ApiCoDesensitizationRuleResponse
;
import
org.springframework.cloud.openfeign.FeignClient
;
import
org.springframework.web.bind.annotation.GetMapping
;
import
org.springframework.web.bind.annotation.PathVariable
;
import
org.springframework.web.bind.annotation.RequestParam
;
import
javax.servlet.http.HttpServletRequest
;
import
javax.validation.constraints.NotBlank
;
import
java.util.List
;
/**
* 通信服务-脱敏信息Feign 客户端
*
* @author zxm
* @date 2026-07-28
*/
@FeignClient
(
name
=
"yd-communication-api"
,
path
=
"/communication/api/desensitization"
,
fallbackFactory
=
ApiCoDesensitizationRuleFeignFallbackFactory
.
class
)
public
interface
ApiCoDesensitizationRuleFeignClient
{
/**
* 根据业务ID查询单条规则
*/
@GetMapping
(
"/{bizId}"
)
Result
<
ApiCoDesensitizationRuleResponse
>
getByBizId
(
@PathVariable
@NotBlank
(
message
=
"业务ID不能为空"
)
String
bizId
);
/**
* 根据资源类型和资源ID获取生效的脱敏规则列表(供脱敏引擎调用)
*/
@GetMapping
(
"/enabled"
)
Result
<
List
<
ApiCoDesensitizationRuleResponse
>>
getEnabledRules
(
@RequestParam
(
value
=
"resourceType"
,
required
=
false
)
String
resourceType
,
@RequestParam
(
value
=
"resourceId"
,
required
=
false
)
String
resourceId
);
}
yd-communication-feign/src/main/java/com/yd/communication/feign/client/ApiCoSessionFeignClient.java
0 → 100644
View file @
ed85d9a7
package
com
.
yd
.
communication
.
feign
.
client
;
import
com.yd.common.result.Result
;
import
com.yd.communication.feign.fallback.ApiCoSessionFeignFallbackFactory
;
import
com.yd.communication.feign.request.*
;
import
com.yd.communication.feign.response.*
;
import
org.springframework.cloud.openfeign.FeignClient
;
import
org.springframework.validation.annotation.Validated
;
import
org.springframework.web.bind.annotation.*
;
/**
* 通信服务-协同会话信息 Feign 客户端
*
* @author zxm
* @date 2026-07-28
*/
@FeignClient
(
name
=
"yd-communication-api"
,
path
=
"/communication/api/coSession"
,
fallbackFactory
=
ApiCoSessionFeignFallbackFactory
.
class
)
public
interface
ApiCoSessionFeignClient
{
/**
* 创建会话
*
* @param request 创建会话请求
* @return 会话信息
*/
@PostMapping
(
"/create"
)
Result
<
CreateResponse
>
create
(
@Validated
@RequestBody
CreateRequest
request
);
/**
* 加入会话
*
* @param request 加入会话请求
* @return 会话详情
*/
@PostMapping
(
"/join"
)
Result
<
JoinResponse
>
join
(
@Validated
@RequestBody
JoinRequest
request
);
/**
* 获取会话状态
* @param request
* @return
*/
@PostMapping
(
"/status"
)
Result
<
GetStatusResponse
>
getStatus
(
@Validated
@RequestBody
GetStatusRequest
request
);
/**
* 获取协同会话详情
*
* @param bizId 会话业务ID
* @return 完整会话信息
*/
@GetMapping
(
"/{bizId}"
)
Result
<
SessionDetailResponse
>
get
(
@PathVariable
(
"bizId"
)
String
bizId
);
/**
* 结束协同会话(关闭共享,仅客户可调用)
*
*/
@PostMapping
(
"/end"
)
Result
<
CommonResponse
>
end
(
@Validated
@RequestBody
EndSessionRequest
request
);
/**
* 切换控制权(仅参与者(顾问)可调用)
* @param request
* @return
*/
@PostMapping
(
"/control/transfer"
)
Result
<
CommonResponse
>
transferControl
(
@Validated
@RequestBody
TransferControlRequest
request
);
}
\ No newline at end of file
yd-communication-feign/src/main/java/com/yd/communication/feign/client/ApiRecordingTaskFeignClient.java
0 → 100644
View file @
ed85d9a7
package
com
.
yd
.
communication
.
feign
.
client
;
import
com.yd.common.result.Result
;
import
com.yd.communication.feign.fallback.ApiRecordingTaskFeignFallbackFactory
;
import
com.yd.communication.feign.request.recording.ApiStartRecordingRequest
;
import
com.yd.communication.feign.response.recording.ApiQueryRecordingResponse
;
import
org.springframework.cloud.openfeign.FeignClient
;
import
org.springframework.validation.annotation.Validated
;
import
org.springframework.web.bind.annotation.*
;
import
org.springframework.web.multipart.MultipartFile
;
import
java.util.Map
;
/**
* 通信服务-录制信息 Feign 客户端
*
* @author zxm
* @date 2026-07-28
*/
@FeignClient
(
name
=
"yd-communication-api"
,
path
=
"/communication/api/recordingTask"
,
fallbackFactory
=
ApiRecordingTaskFeignFallbackFactory
.
class
)
public
interface
ApiRecordingTaskFeignClient
{
/**
* 开始录制
* @return
*/
@PostMapping
(
"/start"
)
Result
<
String
>
startRecording
(
@Validated
@RequestBody
ApiStartRecordingRequest
request
);
/**
* 停止录制并上传视频
* @param taskId 录制任务ID
* @return
*/
@PostMapping
(
"/stop"
)
Result
<
Map
<
String
,
String
>>
stopRecording
(
@RequestParam
(
"taskId"
)
String
taskId
);
/**
* 查询录制信息
* @param taskId
* @return
*/
@GetMapping
(
"/query/{taskId}"
)
Result
<
ApiQueryRecordingResponse
>
queryRecording
(
@PathVariable
String
taskId
);
}
yd-communication-feign/src/main/java/com/yd/communication/feign/constant/RedisConstants.java
0 → 100644
View file @
ed85d9a7
package
com
.
yd
.
communication
.
feign
.
constant
;
/**
* redis的key前缀常量
*/
public
class
RedisConstants
{
/**
* 协同房间缓存信息redis前缀
*/
public
static
final
String
ROOM_KEY_PREFIX
=
"room:"
;
public
static
final
String
ROOM_LINK_KEY_PREFIX
=
"room:link:"
;
}
yd-communication-feign/src/main/java/com/yd/communication/feign/dto/RoomRedisInfoDTO.java
0 → 100644
View file @
ed85d9a7
package
com
.
yd
.
communication
.
feign
.
dto
;
import
lombok.Data
;
import
java.io.Serializable
;
/**
* 存储当前房间内的一些缓存字段信息
*/
@Data
public
class
RoomRedisInfoDTO
implements
Serializable
{
private
static
final
long
serialVersionUID
=
1L
;
/**
* 协同房间号(房间ID)
*/
private
String
roomId
;
/**
* 协同房间密码(输入密码进入房间,协同共享码,进入这个协同房间号的密码)
*/
private
String
roomPwd
;
/**
* 控制权持有者类型:owner(资源所有者类型)/participant(参与者类型)
*/
private
String
controlHolderType
;
/**
* 控制权持有者ID(具体人的ID)
*/
private
String
controlHolderId
;
/**
* 资源所有者的登录用户ID
*/
private
String
userId
;
/**
* 资源所有者的登录token信息
*/
private
String
token
;
/**
* 姓名
*/
private
String
name
;
/**
* 手机号
*/
private
String
mobile
;
}
yd-communication-feign/src/main/java/com/yd/communication/feign/enums/CoSessionStatusEnum.java
0 → 100644
View file @
ed85d9a7
package
com
.
yd
.
communication
.
feign
.
enums
;
/**
* 协同会话状态枚举
*/
public
enum
CoSessionStatusEnum
{
DKS
(
"待开始"
,
"1"
),
JXZ
(
"进行中"
,
"2"
),
YJS
(
"已结束"
,
"3"
),
YCS
(
"已超时"
,
"4"
),
;
//字典项标签(名称)
private
String
itemLabel
;
//字典项值
private
String
itemValue
;
//构造函数
CoSessionStatusEnum
(
String
itemLabel
,
String
itemValue
)
{
this
.
itemLabel
=
itemLabel
;
this
.
itemValue
=
itemValue
;
}
public
String
getItemLabel
()
{
return
itemLabel
;
}
public
String
getItemValue
()
{
return
itemValue
;
}
}
yd-communication-feign/src/main/java/com/yd/communication/feign/enums/ControlHolderTypeEnum.java
0 → 100644
View file @
ed85d9a7
package
com
.
yd
.
communication
.
feign
.
enums
;
/**
* 控制权持有者类型枚举
*/
public
enum
ControlHolderTypeEnum
{
OWNER
(
"资源所有者类型"
,
"owner"
),
PARTICIPANT
(
"参与者类型"
,
"participant"
),
;
//字典项标签(名称)
private
String
itemLabel
;
//字典项值
private
String
itemValue
;
//构造函数
ControlHolderTypeEnum
(
String
itemLabel
,
String
itemValue
)
{
this
.
itemLabel
=
itemLabel
;
this
.
itemValue
=
itemValue
;
}
public
String
getItemLabel
()
{
return
itemLabel
;
}
public
String
getItemValue
()
{
return
itemValue
;
}
}
yd-communication-feign/src/main/java/com/yd/communication/feign/enums/RedisEnum.java
0 → 100644
View file @
ed85d9a7
package
com
.
yd
.
communication
.
feign
.
enums
;
import
com.yd.communication.feign.constant.RedisConstants
;
import
java.util.concurrent.TimeUnit
;
/**
* redis枚举
*/
public
enum
RedisEnum
{
//协同房间缓存信息redis参数
ROOM
(
RedisConstants
.
ROOM_KEY_PREFIX
,
120
,
TimeUnit
.
MINUTES
),
//房间链接缓存信息redis参数
ROOM_LINK
(
RedisConstants
.
ROOM_LINK_KEY_PREFIX
,
120
,
TimeUnit
.
MINUTES
),
;
//缓存key前缀
private
String
prefix
;
//缓存过期时长
private
Integer
timeout
;
//缓存过期时长单位
private
TimeUnit
timeUnit
;
RedisEnum
(
String
prefix
,
Integer
timeout
,
TimeUnit
timeUnit
)
{
this
.
prefix
=
prefix
;
this
.
timeout
=
timeout
;
this
.
timeUnit
=
timeUnit
;
}
public
String
getPrefix
()
{
return
prefix
;
}
public
Integer
getTimeout
()
{
return
timeout
;
}
public
TimeUnit
getTimeUnit
()
{
return
timeUnit
;
}
}
yd-communication-feign/src/main/java/com/yd/communication/feign/fallback/ApiCoDesensitizationRuleFeignFallbackFactory.java
0 → 100644
View file @
ed85d9a7
package
com
.
yd
.
communication
.
feign
.
fallback
;
import
com.yd.common.result.Result
;
import
com.yd.communication.feign.client.ApiCoDesensitizationRuleFeignClient
;
import
com.yd.communication.feign.response.desensitization.ApiCoDesensitizationRuleResponse
;
import
lombok.extern.slf4j.Slf4j
;
import
org.springframework.cloud.openfeign.FallbackFactory
;
import
org.springframework.stereotype.Component
;
import
javax.servlet.http.HttpServletRequest
;
import
java.util.List
;
/**
* 通信服务-脱敏信息Feign降级处理
*/
@Slf4j
@Component
public
class
ApiCoDesensitizationRuleFeignFallbackFactory
implements
FallbackFactory
<
ApiCoDesensitizationRuleFeignClient
>
{
@Override
public
ApiCoDesensitizationRuleFeignClient
create
(
Throwable
cause
)
{
return
new
ApiCoDesensitizationRuleFeignClient
()
{
@Override
public
Result
<
ApiCoDesensitizationRuleResponse
>
getByBizId
(
String
bizId
)
{
return
null
;
}
@Override
public
Result
<
List
<
ApiCoDesensitizationRuleResponse
>>
getEnabledRules
(
String
resourceType
,
String
resourceId
)
{
return
null
;
}
};
}
}
yd-communication-feign/src/main/java/com/yd/communication/feign/fallback/ApiCoSessionFeignFallbackFactory.java
0 → 100644
View file @
ed85d9a7
package
com
.
yd
.
communication
.
feign
.
fallback
;
import
com.yd.common.result.Result
;
import
com.yd.communication.feign.client.ApiCoSessionFeignClient
;
import
com.yd.communication.feign.request.*
;
import
com.yd.communication.feign.response.*
;
import
lombok.extern.slf4j.Slf4j
;
import
org.springframework.cloud.openfeign.FallbackFactory
;
import
org.springframework.stereotype.Component
;
/**
* 通信服务-协同会话信息Feign降级处理
*/
@Slf4j
@Component
public
class
ApiCoSessionFeignFallbackFactory
implements
FallbackFactory
<
ApiCoSessionFeignClient
>
{
@Override
public
ApiCoSessionFeignClient
create
(
Throwable
cause
)
{
return
new
ApiCoSessionFeignClient
()
{
@Override
public
Result
<
CreateResponse
>
create
(
CreateRequest
request
)
{
return
null
;
}
@Override
public
Result
<
JoinResponse
>
join
(
JoinRequest
request
)
{
return
null
;
}
@Override
public
Result
<
GetStatusResponse
>
getStatus
(
GetStatusRequest
request
)
{
return
null
;
}
@Override
public
Result
<
SessionDetailResponse
>
get
(
String
bizId
)
{
return
null
;
}
@Override
public
Result
<
CommonResponse
>
end
(
EndSessionRequest
request
)
{
return
null
;
}
@Override
public
Result
<
CommonResponse
>
transferControl
(
TransferControlRequest
request
)
{
return
null
;
}
};
}
}
yd-communication-feign/src/main/java/com/yd/communication/feign/fallback/ApiRecordingTaskFeignFallbackFactory.java
0 → 100644
View file @
ed85d9a7
package
com
.
yd
.
communication
.
feign
.
fallback
;
import
com.yd.common.result.Result
;
import
com.yd.communication.feign.client.ApiRecordingTaskFeignClient
;
import
com.yd.communication.feign.request.recording.ApiStartRecordingRequest
;
import
com.yd.communication.feign.response.recording.ApiQueryRecordingResponse
;
import
lombok.extern.slf4j.Slf4j
;
import
org.springframework.cloud.openfeign.FallbackFactory
;
import
org.springframework.stereotype.Component
;
import
org.springframework.web.multipart.MultipartFile
;
import
java.util.Map
;
/**
* 通信服务-录制信息Feign降级处理
*/
@Slf4j
@Component
public
class
ApiRecordingTaskFeignFallbackFactory
implements
FallbackFactory
<
ApiRecordingTaskFeignClient
>
{
@Override
public
ApiRecordingTaskFeignClient
create
(
Throwable
cause
)
{
return
new
ApiRecordingTaskFeignClient
()
{
@Override
public
Result
<
String
>
startRecording
(
ApiStartRecordingRequest
request
)
{
return
null
;
}
@Override
public
Result
<
Map
<
String
,
String
>>
stopRecording
(
String
taskId
)
{
return
null
;
}
@Override
public
Result
<
ApiQueryRecordingResponse
>
queryRecording
(
String
taskId
)
{
return
null
;
}
};
}
}
yd-communication-feign/src/main/java/com/yd/communication/feign/request/CreateRequest.java
0 → 100644
View file @
ed85d9a7
package
com
.
yd
.
communication
.
feign
.
request
;
import
lombok.Data
;
import
javax.validation.constraints.NotBlank
;
/**
* 创建协同会话请求对象
* 客户调用此接口生成共享码,开启协同讲解
*/
@Data
public
class
CreateRequest
{
/**
* 协同作用域
* single: 单个资源(如单份报告、资讯)
* global: 全局协同(如小程序全局协同,切换页面自动跟随)
*/
@NotBlank
(
message
=
"协同作用域不能为空"
)
private
String
scope
;
/**
* 资源类型
* 用于区分不同的业务资源类型
* 可选值:report(报告)、news(资讯)、mini_program(小程序)等
*/
@NotBlank
(
message
=
"资源类型不能为空"
)
private
String
resourceType
;
/**
* 资源业务ID
* 具体资源的唯一标识
* 如:report-报告ID、news-资讯ID、mini_program-小程序应用标识
*/
@NotBlank
(
message
=
"资源业务ID不能为空"
)
private
String
resourceId
;
/**
* 资源初始化JSON串
* 创建会话时记录当前所在页面的完整信息,后续不做修改,用于历史追溯
* 示例:{"url":"https://mini.xxx.com/pages/index/index?userId=xxx"}
*/
@NotBlank
(
message
=
"资源初始化JSON串不能为空"
)
private
String
resourceInit
;
/**
* 资源所有者ID(即客户ID)
* 报告/资源的归属人,通常为发起协同的客户
*/
@NotBlank
(
message
=
"资源所有者ID不能为空"
)
private
String
ownerId
;
/**
* 资源所有者类型
* 默认:customer(客户)
* 可扩展:member(会员)、user(普通用户)等
*/
@NotBlank
(
message
=
"资源所有者类型不能为空"
)
private
String
ownerType
;
/**
* 资源所有者的登录用户ID
*/
@NotBlank
(
message
=
"资源所有者的登录用户ID不能为空"
)
private
String
userId
;
}
\ No newline at end of file
yd-communication-feign/src/main/java/com/yd/communication/feign/request/EndSessionRequest.java
0 → 100644
View file @
ed85d9a7
package
com
.
yd
.
communication
.
feign
.
request
;
import
lombok.Data
;
@Data
public
class
EndSessionRequest
{
/**
* 房间号不能为空
*/
private
String
roomId
;
}
yd-communication-feign/src/main/java/com/yd/communication/feign/request/GetStatusRequest.java
0 → 100644
View file @
ed85d9a7
package
com
.
yd
.
communication
.
feign
.
request
;
import
lombok.Data
;
import
javax.validation.constraints.NotBlank
;
@Data
public
class
GetStatusRequest
{
/**
* 会话唯一业务ID
*/
@NotBlank
(
message
=
"会话唯一业务ID不能为空"
)
private
String
sessionBizId
;
}
yd-communication-feign/src/main/java/com/yd/communication/feign/request/JoinRequest.java
0 → 100644
View file @
ed85d9a7
package
com
.
yd
.
communication
.
feign
.
request
;
import
lombok.Data
;
import
javax.validation.constraints.NotBlank
;
/**
* 加入协同会话请求对象
* 顾问输入房间号和共享码加入协同
*/
@Data
public
class
JoinRequest
{
/**
* 协同房间号(房间ID)
* 客户创建会话时生成的唯一房间标识
* 示例:room_abc12345
*/
private
String
roomId
;
/**
* 协同房间密码(共享码)
* 客户生成共享会话时返回的6位数字密码
* 顾问需要输入此密码才能加入协同
*/
@NotBlank
(
message
=
"房间密码不能为空"
)
private
String
roomPwd
;
/**
* 参与者ID(即顾问ID)
* 加入协同的顾问/专家的唯一标识
*/
@NotBlank
(
message
=
"参与者ID不能为空"
)
private
String
participantId
;
/**
* 参与者类型
* 默认:consultant(顾问)
* 可扩展:expert(专家)、trainer(培训师)等
*/
@NotBlank
(
message
=
"参与者类型不能为空"
)
private
String
participantType
;
}
\ No newline at end of file
yd-communication-feign/src/main/java/com/yd/communication/feign/request/TransferControlRequest.java
0 → 100644
View file @
ed85d9a7
package
com
.
yd
.
communication
.
feign
.
request
;
import
lombok.Data
;
import
javax.validation.constraints.NotBlank
;
import
javax.validation.constraints.NotNull
;
@Data
public
class
TransferControlRequest
{
/**
* 操作类型:1-开启客户操作 2-关闭客户操作
*/
@NotNull
(
message
=
"操作类型不能为空"
)
private
Integer
oprType
;
/**
* 房间号
*/
@NotBlank
(
message
=
"房间号不能为空"
)
private
String
roomId
;
}
yd-communication-feign/src/main/java/com/yd/communication/feign/request/http/GenerateTokenRequest.java
0 → 100644
View file @
ed85d9a7
package
com
.
yd
.
communication
.
feign
.
request
.
http
;
import
lombok.Data
;
@Data
public
class
GenerateTokenRequest
{
/**
* 商城用户ID
*/
private
Long
sfpUserId
;
/**
* 分销用户ID
*/
private
Long
cffpUserId
;
}
yd-communication-feign/src/main/java/com/yd/communication/feign/request/recording/ApiStartRecordingRequest.java
0 → 100644
View file @
ed85d9a7
package
com
.
yd
.
communication
.
feign
.
request
.
recording
;
import
lombok.Data
;
import
javax.validation.constraints.NotBlank
;
@Data
public
class
ApiStartRecordingRequest
{
/**
* 业务类型:co_session/meeting
*/
@NotBlank
(
message
=
"业务类型不能为空"
)
private
String
bizType
;
/**
* 业务ID(如co_session_biz_id(协同会话表唯一业务ID)、会议ID)
*/
@NotBlank
(
message
=
"业务ID不能为空"
)
private
String
bizId
;
}
yd-communication-feign/src/main/java/com/yd/communication/feign/request/recording/ApiStopRecordingRequest.java
0 → 100644
View file @
ed85d9a7
package
com
.
yd
.
communication
.
feign
.
request
.
recording
;
import
lombok.Data
;
import
javax.validation.constraints.NotBlank
;
@Data
public
class
ApiStopRecordingRequest
{
/**
* 第三方RTC录制任务ID
*/
@NotBlank
(
message
=
"录制任务ID不能为空"
)
private
String
taskId
;
}
yd-communication-feign/src/main/java/com/yd/communication/feign/response/CommonResponse.java
0 → 100644
View file @
ed85d9a7
package
com
.
yd
.
communication
.
feign
.
response
;
import
lombok.AllArgsConstructor
;
import
lombok.Data
;
import
lombok.NoArgsConstructor
;
/**
* 通用操作响应对象
* 用于结束会话、切换控制权等无需返回业务数据的操作
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public
class
CommonResponse
{
/**
* 操作结果消息
*/
private
String
message
;
}
\ No newline at end of file
yd-communication-feign/src/main/java/com/yd/communication/feign/response/CreateResponse.java
0 → 100644
View file @
ed85d9a7
package
com
.
yd
.
communication
.
feign
.
response
;
import
com.fasterxml.jackson.annotation.JsonFormat
;
import
lombok.Data
;
import
java.util.Date
;
/**
* 创建协同会话响应对象
*/
@Data
public
class
CreateResponse
{
/**
* 会话唯一业务ID
*/
private
String
sessionBizId
;
/**
* 房间号
*/
private
String
roomId
;
/**
* 共享码(6位数字)
*/
private
String
roomPwd
;
/**
* 会话状态:0-进行中,1-已结束,2-已超时
*/
private
String
status
;
/**
* 协同房间二维码(扫码进入房间)
*/
private
String
roomQrCode
;
/**
* 协同房间链接(访问链接进入房间)
*/
private
String
roomLink
;
/**
* 链接失效时间
*/
@JsonFormat
(
pattern
=
"yyyy-MM-dd HH:mm"
,
timezone
=
"GMT+8"
)
private
Date
expirationTime
;
}
\ No newline at end of file
yd-communication-feign/src/main/java/com/yd/communication/feign/response/GetStatusResponse.java
0 → 100644
View file @
ed85d9a7
package
com
.
yd
.
communication
.
feign
.
response
;
import
lombok.Data
;
@Data
public
class
GetStatusResponse
{
/**
* 1-未失效 2-已失效
*/
private
Integer
status
;
}
yd-communication-feign/src/main/java/com/yd/communication/feign/response/JoinResponse.java
0 → 100644
View file @
ed85d9a7
package
com
.
yd
.
communication
.
feign
.
response
;
import
lombok.Data
;
/**
* 加入协同会话响应对象
*/
@Data
public
class
JoinResponse
{
/**
* 会话唯一业务ID
*/
private
String
sessionBizId
;
/**
* 房间号
*/
private
String
roomId
;
/**
* 资源初始化JSON(用于初始加载)
*/
private
String
resourceInit
;
/**
* 当前操作页面(实时更新,示例:{url:https://mini.xxx.com/pages/my/index?userId=xxx})
*/
private
String
currentPage
;
/**
* 控制权持有者类型:owner-客户,participant-顾问
*/
private
String
controlHolderType
;
/**
* 控制权持有者ID
*/
private
String
controlHolderId
;
/**
* 资源所有者的登录用户ID
*/
private
String
userId
;
/**
* 资源所有者的登录token信息
*/
private
String
token
;
/**
* 姓名
*/
private
String
name
;
/**
* 手机号
*/
private
String
mobile
;
}
\ No newline at end of file
yd-communication-feign/src/main/java/com/yd/communication/feign/response/SessionDetailResponse.java
0 → 100644
View file @
ed85d9a7
package
com
.
yd
.
communication
.
feign
.
response
;
import
lombok.Data
;
import
java.time.LocalDateTime
;
/**
* 协同会话详情响应对象
*/
@Data
public
class
SessionDetailResponse
{
/**
* 数据库主键
*/
private
Long
id
;
/**
* 协同-会话表唯一业务ID
*/
private
String
coSessionBizId
;
/**
* 会话编号
*/
private
String
coSessionNo
;
/**
* 协同作用域:single-单资源,global-全域
*/
private
String
scope
;
/**
* 资源类型:report/news/mini_program
*/
private
String
resourceType
;
/**
* 资源业务ID(如报告ID、小程序标识等)
*/
private
String
resourceId
;
/**
* 资源初始化JSON(创建时页面快照)
*/
private
String
resourceInit
;
/**
* 所有者类型:customer-客户
*/
private
String
ownerType
;
/**
* 所有者ID(客户ID)
*/
private
String
ownerId
;
/**
* 参与者类型:consultant-顾问
*/
private
String
participantType
;
/**
* 参与者ID(顾问ID)
*/
private
String
participantId
;
/**
* 房间号
*/
private
String
roomId
;
/**
* 控制权持有者类型:owner-客户,participant-顾问
*/
private
String
controlHolderType
;
/**
* 控制权持有者ID
*/
private
String
controlHolderId
;
/**
* 会话状态:0-进行中,1-已结束,2-已超时
*/
private
String
status
;
/**
* 会话开始时间
*/
private
LocalDateTime
startTime
;
/**
* 会话结束时间
*/
private
LocalDateTime
endTime
;
/**
* 当前页面JSON(实时更新)
*/
private
String
currentPage
;
/**
* 页面访问历史轨迹(JSON数组)
*/
private
String
pageHistory
;
}
\ No newline at end of file
yd-communication-feign/src/main/java/com/yd/communication/feign/response/desensitization/ApiCoDesensitizationRuleResponse.java
0 → 100644
View file @
ed85d9a7
package
com
.
yd
.
communication
.
feign
.
response
.
desensitization
;
import
lombok.Data
;
@Data
public
class
ApiCoDesensitizationRuleResponse
{
/**
* 主键
*/
private
Long
id
;
/**
* 脱敏规则唯一业务ID
*/
private
String
ruleBizId
;
/**
* 规则名称(如:报告-客户姓名脱敏)
*/
private
String
ruleName
;
/**
* 资源类型:report/news/mini_program
*/
private
String
resourceType
;
/**
* 资源业务ID(NULL表示全局规则,适用于该类型下所有资源)
*/
private
String
resourceId
;
/**
* 脱敏字段路径(JSON Path,如:$.customer.name)
*/
private
String
fieldPath
;
/**
* 字段显示名称(冗余,便于运营配置)
*/
private
String
fieldName
;
/**
* 脱敏方式:mask-掩码(如:张**)、hide-完全隐藏(****)、replace-替换(***)、partial-部分显示(如:138****8000)
*/
private
String
maskType
;
/**
* 脱敏配置JSON(如:{"prefix":1,"suffix":1,"replace_char":"*"})
*/
private
String
maskConfig
;
/**
* 规则是否启用:0-停用,1-启用
*/
private
Integer
enabled
;
/**
* 是否为默认规则:0-否,1-是(资源未配置时使用默认)
*/
private
Integer
isDefault
;
/**
* 排序顺序
*/
private
Integer
sortOrder
;
}
yd-communication-feign/src/main/java/com/yd/communication/feign/response/http/GenerateTokenResponse.java
0 → 100644
View file @
ed85d9a7
package
com
.
yd
.
communication
.
feign
.
response
.
http
;
import
lombok.Data
;
@Data
public
class
GenerateTokenResponse
{
/**
* SFP的token
*/
private
String
token
;
/**
* SFP商城用户ID
*/
private
Long
sfpUserId
;
/**
* SFP姓名
*/
private
String
name
;
/**
* SFP手机号
*/
private
String
mobile
;
/**
* CFFP分销用户ID
*/
private
Long
cffpUserId
;
}
yd-communication-feign/src/main/java/com/yd/communication/feign/response/recording/ApiQueryRecordingResponse.java
0 → 100644
View file @
ed85d9a7
package
com
.
yd
.
communication
.
feign
.
response
.
recording
;
import
lombok.Data
;
import
java.time.LocalDateTime
;
@Data
public
class
ApiQueryRecordingResponse
{
/**
* 主键
*/
private
Long
id
;
/**
* 录制任务表唯一业务ID
*/
private
String
recordingTaskBizId
;
/**
* 任务编号
*/
private
String
taskNo
;
/**
* 第三方RTC录制任务ID
*/
private
String
taskId
;
/**
* 业务类型:co_session/meeting
*/
private
String
bizType
;
/**
* 业务ID(如co_session_biz_id(协同会话表唯一业务ID)、会议ID)
*/
private
String
bizId
;
/**
* RTC房间ID
*/
private
String
roomId
;
/**
* RTC频道名
*/
private
String
channel
;
/**
* 录制模式:mix-混合, single-单流, screen-屏幕
*/
private
String
recordingMode
;
/**
* 布局:grid/speaker/custom
*/
private
String
layout
;
/**
* 1-初始化,2-录制中,3-已停止,4-失败,5-已归档
*/
private
String
status
;
/**
* 录制开始时间
*/
private
LocalDateTime
startTime
;
/**
* 录制结束时间
*/
private
LocalDateTime
stopTime
;
/**
* 文件地址
*/
private
String
fileUrl
;
/**
* 视频时长(秒)
*/
private
Integer
fileDuration
;
/**
* 文件大小(字节)
*/
private
Long
fileSize
;
/**
* 文件MD5
*/
private
String
fileMd5
;
/**
* 文件格式
*/
private
String
fileFormat
;
/**
* 回放地址(带鉴权)
*/
private
String
playbackUrl
;
/**
* 存储类型:oss/cos/minio/local
*/
private
String
storageType
;
/**
* 存储桶
*/
private
String
storageBucket
;
/**
* 存储路径
*/
private
String
storagePath
;
/**
* 错误信息
*/
private
String
errorMsg
;
/**
* 扩展信息
*/
private
String
extInfo
;
/**
* 通用备注
*/
private
String
remark
;
/**
* 删除标识: 0-正常, 1-删除
*/
private
Integer
isDeleted
;
/**
* 创建人ID
*/
private
String
creatorId
;
/**
* 更新人ID
*/
private
String
updaterId
;
/**
* 创建时间
*/
private
LocalDateTime
createTime
;
/**
* 更新时间
*/
private
LocalDateTime
updateTime
;
}
yd-communication-service/pom.xml
View file @
ed85d9a7
...
...
@@ -50,6 +50,12 @@
<artifactId>
freemarker
</artifactId>
</dependency>
<!-- Spring Boot Starter WebSocket -->
<dependency>
<groupId>
org.springframework.boot
</groupId>
<artifactId>
spring-boot-starter-websocket
</artifactId>
</dependency>
<dependency>
<groupId>
com.yd
</groupId>
<artifactId>
yd-communication-feign
</artifactId>
...
...
@@ -67,6 +73,29 @@
<artifactId>
yd-framework
</artifactId>
<version>
${project.version}
</version>
</dependency>
<dependency>
<groupId>
com.google.zxing
</groupId>
<artifactId>
core
</artifactId>
</dependency>
<dependency>
<groupId>
com.google.zxing
</groupId>
<artifactId>
javase
</artifactId>
</dependency>
<!-- <!– 阿里云 Java SDK 核心库 –>-->
<!-- <dependency>-->
<!-- <groupId>com.aliyun</groupId>-->
<!-- <artifactId>aliyun-java-sdk-core</artifactId>-->
<!-- <version>4.6.3</version>-->
<!-- </dependency>-->
<!-- <!– 阿里云视频直播 SDK(包含 RTC 云端录制 API) –>-->
<!-- <dependency>-->
<!-- <groupId>com.aliyun</groupId>-->
<!-- <artifactId>aliyun-java-sdk-live</artifactId>-->
<!-- <version>3.9.76</version>-->
<!-- </dependency>-->
</dependencies>
</project>
\ No newline at end of file
yd-communication-service/src/main/java/com/yd/communication/service/dao/CoSessionMapper.java
View file @
ed85d9a7
...
...
@@ -2,6 +2,10 @@ package com.yd.communication.service.dao;
import
com.yd.communication.service.model.CoSession
;
import
com.baomidou.mybatisplus.core.mapper.BaseMapper
;
import
org.apache.ibatis.annotations.Param
;
import
org.apache.ibatis.annotations.Update
;
import
java.time.LocalDateTime
;
/**
* <p>
...
...
@@ -13,4 +17,14 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper;
*/
public
interface
CoSessionMapper
extends
BaseMapper
<
CoSession
>
{
@Update
(
"UPDATE co_session SET status = #{status}, end_time = #{endTime} WHERE room_id = #{roomId}"
)
int
updateStatusAndEndTimeByRoomId
(
@Param
(
"roomId"
)
String
roomId
,
@Param
(
"status"
)
Integer
status
,
@Param
(
"endTime"
)
LocalDateTime
endTime
);
@Update
(
"UPDATE co_session SET current_page = #{currentPage}, page_history = #{pageHistory}, updater_id = #{updaterId} WHERE room_id = #{roomId}"
)
int
updateCurrentPageAndHistory
(
@Param
(
"roomId"
)
String
roomId
,
@Param
(
"currentPage"
)
String
currentPage
,
@Param
(
"pageHistory"
)
String
pageHistory
,
@Param
(
"updaterId"
)
String
updaterId
);
}
yd-communication-service/src/main/java/com/yd/communication/service/model/CoSession.java
View file @
ed85d9a7
...
...
@@ -97,12 +97,24 @@ public class CoSession implements Serializable {
private
String
roomId
;
/**
* 协同房间密码(协同共享码,进入这个协同房间号的密码)
* 协同房间密码(
输入密码进入房间,
协同共享码,进入这个协同房间号的密码)
*/
@TableField
(
"room_pwd"
)
private
String
roomPwd
;
/**
* 协同房间二维码(扫码进入房间)
*/
@TableField
(
"room_qr_code"
)
private
String
roomQrCode
;
/**
* 协同房间链接(访问链接进入房间)
*/
@TableField
(
"room_link"
)
private
String
roomLink
;
/**
* RTC频道前缀,便于区分业务
*/
@TableField
(
"channel_prefix"
)
...
...
@@ -121,10 +133,10 @@ public class CoSession implements Serializable {
private
String
controlHolderId
;
/**
*
0-进行中,1-已结束,2
-已超时
*
1-待开始,2-进行中,3-已结束,4
-已超时
*/
@TableField
(
"status"
)
private
Integer
status
;
private
String
status
;
/**
* 开始时间
...
...
@@ -185,4 +197,5 @@ public class CoSession implements Serializable {
*/
@TableField
(
"update_time"
)
private
LocalDateTime
updateTime
;
}
yd-communication-service/src/main/java/com/yd/communication/service/model/RecordingTask.java
View file @
ed85d9a7
...
...
@@ -49,7 +49,7 @@ public class RecordingTask implements Serializable {
private
String
taskId
;
/**
* 业务类型:co
-
session/meeting
* 业务类型:co
_
session/meeting
*/
@TableField
(
"biz_type"
)
private
String
bizType
;
...
...
@@ -85,10 +85,10 @@ public class RecordingTask implements Serializable {
private
String
layout
;
/**
*
0-初始化,1-录制中,2-已停止,3-失败,4
-已归档
*
1-初始化,2-录制中,3-已停止,4-失败,5
-已归档
*/
@TableField
(
"status"
)
private
Integer
status
;
private
String
status
;
/**
* 录制开始时间
...
...
yd-communication-service/src/main/java/com/yd/communication/service/service/ICoDesensitizationRuleService.java
View file @
ed85d9a7
package
com
.
yd
.
communication
.
service
.
service
;
import
com.yd.communication.service.model.CoDesensitizationRule
;
import
com.baomidou.mybatisplus.core.metadata.IPage
;
import
com.baomidou.mybatisplus.extension.plugins.pagination.Page
;
import
com.baomidou.mybatisplus.extension.service.IService
;
import
com.yd.communication.service.model.CoDesensitizationRule
;
import
java.util.List
;
/**
* <p>
* 协同-脱敏设置表(通用) 服务类
* </p>
*
* @author zxm
* @since 2026-07-28
* 协同-脱敏设置表 服务类
*/
public
interface
ICoDesensitizationRuleService
extends
IService
<
CoDesensitizationRule
>
{
/**
* 分页查询脱敏规则列表
*/
IPage
<
CoDesensitizationRule
>
pageList
(
Page
<
CoDesensitizationRule
>
page
,
String
resourceType
,
String
resourceId
,
Integer
enabled
);
/**
* 根据资源类型和资源ID获取生效的脱敏规则列表
*/
List
<
CoDesensitizationRule
>
getEnabledRulesByResource
(
String
resourceType
,
String
resourceId
);
/**
* 启用/停用规则
*/
void
toggleEnabled
(
Long
id
,
Integer
enabled
);
/**
* 逻辑删除规则
*/
void
deleteById
(
Long
id
);
}
\ No newline at end of file
yd-communication-service/src/main/java/com/yd/communication/service/service/ICoOperationLogService.java
View file @
ed85d9a7
...
...
@@ -13,4 +13,7 @@ import com.baomidou.mybatisplus.extension.service.IService;
*/
public
interface
ICoOperationLogService
extends
IService
<
CoOperationLog
>
{
void
log
(
String
bizId
,
String
operatorId
,
String
operatorType
,
String
operatorName
,
String
action
,
String
content
,
String
deviceNumber
,
String
ip
);
}
yd-communication-service/src/main/java/com/yd/communication/service/service/ICoSessionService.java
View file @
ed85d9a7
...
...
@@ -2,6 +2,7 @@ package com.yd.communication.service.service;
import
com.yd.communication.service.model.CoSession
;
import
com.baomidou.mybatisplus.extension.service.IService
;
import
org.springframework.transaction.annotation.Transactional
;
/**
* <p>
...
...
@@ -13,4 +14,15 @@ import com.baomidou.mybatisplus.extension.service.IService;
*/
public
interface
ICoSessionService
extends
IService
<
CoSession
>
{
/**
* 根据房间ID获取会话(WebSocket 使用)
*/
CoSession
getByRoomId
(
String
roomId
);
/**
* 根据业务ID获取会话
*/
CoSession
getByBizId
(
String
bizId
);
int
updateCurrentPageAndHistory
(
String
roomId
,
String
currentPage
,
String
pageHistory
,
String
updaterId
);
}
yd-communication-service/src/main/java/com/yd/communication/service/service/IRecordingTaskService.java
View file @
ed85d9a7
...
...
@@ -13,4 +13,7 @@ import com.baomidou.mybatisplus.extension.service.IService;
*/
public
interface
IRecordingTaskService
extends
IService
<
RecordingTask
>
{
String
startRecording
(
String
bizId
,
String
roomId
);
void
stopRecording
(
String
taskId
);
}
yd-communication-service/src/main/java/com/yd/communication/service/service/impl/CoDesensitizationRuleServiceImpl.java
View file @
ed85d9a7
package
com
.
yd
.
communication
.
service
.
service
.
impl
;
import
com.yd.communication.service.model.CoDesensitizationRule
;
import
com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper
;
import
com.baomidou.mybatisplus.core.metadata.IPage
;
import
com.baomidou.mybatisplus.extension.plugins.pagination.Page
;
import
com.baomidou.mybatisplus.extension.service.impl.ServiceImpl
;
import
com.yd.common.enums.CommonEnum
;
import
com.yd.common.exception.BusinessException
;
import
com.yd.common.utils.RandomStringGenerator
;
import
com.yd.communication.service.dao.CoDesensitizationRuleMapper
;
import
com.yd.communication.service.model.CoDesensitizationRule
;
import
com.yd.communication.service.service.ICoDesensitizationRuleService
;
import
com.baomidou.mybatisplus.extension.service.impl.ServiceImpl
;
import
lombok.extern.slf4j.Slf4j
;
import
org.apache.commons.lang3.StringUtils
;
import
org.springframework.stereotype.Service
;
import
org.springframework.transaction.annotation.Transactional
;
/**
* <p>
* 协同-脱敏设置表(通用) 服务实现类
* </p>
*
* @author zxm
* @since 2026-07-28
*/
import
java.time.LocalDateTime
;
import
java.util.List
;
@Slf4j
@Service
public
class
CoDesensitizationRuleServiceImpl
extends
ServiceImpl
<
CoDesensitizationRuleMapper
,
CoDesensitizationRule
>
implements
ICoDesensitizationRuleService
{
public
class
CoDesensitizationRuleServiceImpl
extends
ServiceImpl
<
CoDesensitizationRuleMapper
,
CoDesensitizationRule
>
implements
ICoDesensitizationRuleService
{
@Override
public
IPage
<
CoDesensitizationRule
>
pageList
(
Page
<
CoDesensitizationRule
>
page
,
String
resourceType
,
String
resourceId
,
Integer
enabled
)
{
LambdaQueryWrapper
<
CoDesensitizationRule
>
wrapper
=
new
LambdaQueryWrapper
<>();
wrapper
.
eq
(
CoDesensitizationRule:
:
getIsDeleted
,
0
);
if
(
StringUtils
.
isNotBlank
(
resourceType
))
{
wrapper
.
eq
(
CoDesensitizationRule:
:
getResourceType
,
resourceType
);
}
if
(
StringUtils
.
isNotBlank
(
resourceId
))
{
wrapper
.
eq
(
CoDesensitizationRule:
:
getResourceId
,
resourceId
);
}
if
(
enabled
!=
null
)
{
wrapper
.
eq
(
CoDesensitizationRule:
:
getEnabled
,
enabled
);
}
wrapper
.
orderByAsc
(
CoDesensitizationRule:
:
getSortOrder
)
.
orderByDesc
(
CoDesensitizationRule:
:
getCreateTime
);
return
this
.
page
(
page
,
wrapper
);
}
@Override
public
List
<
CoDesensitizationRule
>
getEnabledRulesByResource
(
String
resourceType
,
String
resourceId
)
{
LambdaQueryWrapper
<
CoDesensitizationRule
>
wrapper
=
new
LambdaQueryWrapper
<>();
wrapper
.
eq
(
CoDesensitizationRule:
:
getIsDeleted
,
0
)
.
eq
(
CoDesensitizationRule:
:
getEnabled
,
1
)
.
eq
(
StringUtils
.
isNotBlank
(
resourceType
),
CoDesensitizationRule:
:
getResourceType
,
resourceType
)
.
eq
(
StringUtils
.
isNotBlank
(
resourceId
),
CoDesensitizationRule:
:
getResourceId
,
resourceId
)
.
orderByAsc
(
CoDesensitizationRule:
:
getSortOrder
);
return
this
.
list
(
wrapper
);
}
@Override
@Transactional
(
rollbackFor
=
Exception
.
class
)
public
void
toggleEnabled
(
Long
id
,
Integer
enabled
)
{
CoDesensitizationRule
rule
=
this
.
getById
(
id
);
if
(
rule
==
null
||
rule
.
getIsDeleted
()
==
1
)
{
throw
new
BusinessException
(
"规则不存在或已删除"
);
}
rule
.
setEnabled
(
enabled
);
rule
.
setUpdaterId
(
"system"
);
rule
.
setUpdateTime
(
LocalDateTime
.
now
());
this
.
updateById
(
rule
);
log
.
info
(
"脱敏规则 {} 状态已切换为: {}"
,
id
,
enabled
==
1
?
"启用"
:
"停用"
);
}
@Override
@Transactional
(
rollbackFor
=
Exception
.
class
)
public
void
deleteById
(
Long
id
)
{
CoDesensitizationRule
rule
=
this
.
getById
(
id
);
if
(
rule
==
null
||
rule
.
getIsDeleted
()
==
1
)
{
throw
new
BusinessException
(
"规则不存在或已删除"
);
}
rule
.
setIsDeleted
(
1
);
rule
.
setUpdaterId
(
"system"
);
rule
.
setUpdateTime
(
LocalDateTime
.
now
());
this
.
updateById
(
rule
);
log
.
info
(
"脱敏规则 {} 已逻辑删除"
,
id
);
}
/**
* 保存或更新前的公共逻辑
*/
@Override
@Transactional
(
rollbackFor
=
Exception
.
class
)
public
boolean
saveOrUpdate
(
CoDesensitizationRule
entity
)
{
if
(
entity
==
null
)
{
return
false
;
}
if
(
entity
.
getId
()
==
null
)
{
// 新增
entity
.
setRuleBizId
(
RandomStringGenerator
.
generateBizId16
(
CommonEnum
.
UID_TYPE_DESENSITIZATION_RULE
.
getCode
()));
entity
.
setIsDeleted
(
0
);
entity
.
setCreateTime
(
LocalDateTime
.
now
());
entity
.
setUpdateTime
(
LocalDateTime
.
now
());
if
(
StringUtils
.
isBlank
(
entity
.
getCreatorId
()))
{
entity
.
setCreatorId
(
"system"
);
}
if
(
StringUtils
.
isBlank
(
entity
.
getUpdaterId
()))
{
entity
.
setUpdaterId
(
"system"
);
}
// 默认启用
if
(
entity
.
getEnabled
()
==
null
)
{
entity
.
setEnabled
(
1
);
}
}
else
{
// 更新
CoDesensitizationRule
exist
=
this
.
getById
(
entity
.
getId
());
if
(
exist
==
null
||
exist
.
getIsDeleted
()
==
1
)
{
throw
new
BusinessException
(
"规则不存在或已删除"
);
}
entity
.
setUpdateTime
(
LocalDateTime
.
now
());
if
(
StringUtils
.
isBlank
(
entity
.
getUpdaterId
()))
{
entity
.
setUpdaterId
(
"system"
);
}
// 不允许修改 ruleBizId 和 isDeleted
entity
.
setRuleBizId
(
null
);
entity
.
setIsDeleted
(
null
);
entity
.
setCreateTime
(
null
);
}
return
super
.
saveOrUpdate
(
entity
);
}
}
\ No newline at end of file
yd-communication-service/src/main/java/com/yd/communication/service/service/impl/CoOperationLogServiceImpl.java
View file @
ed85d9a7
...
...
@@ -4,6 +4,8 @@ import com.yd.communication.service.model.CoOperationLog;
import
com.yd.communication.service.dao.CoOperationLogMapper
;
import
com.yd.communication.service.service.ICoOperationLogService
;
import
com.baomidou.mybatisplus.extension.service.impl.ServiceImpl
;
import
lombok.extern.slf4j.Slf4j
;
import
org.springframework.scheduling.annotation.Async
;
import
org.springframework.stereotype.Service
;
/**
...
...
@@ -14,7 +16,29 @@ import org.springframework.stereotype.Service;
* @author zxm
* @since 2026-07-28
*/
@Slf4j
@Service
public
class
CoOperationLogServiceImpl
extends
ServiceImpl
<
CoOperationLogMapper
,
CoOperationLog
>
implements
ICoOperationLogService
{
@Override
@Async
(
"communicationExecutor"
)
public
void
log
(
String
bizId
,
String
operatorId
,
String
operatorType
,
String
operatorName
,
String
action
,
String
content
,
String
deviceNumber
,
String
ip
)
{
CoOperationLog
log
=
new
CoOperationLog
();
log
.
setBizType
(
"co_session"
);
log
.
setBizId
(
bizId
);
log
.
setOperatorId
(
operatorId
);
log
.
setOperatorType
(
operatorType
);
log
.
setOperatorName
(
operatorName
);
log
.
setAction
(
action
);
log
.
setActionCategory
(
"control"
);
log
.
setContent
(
content
);
log
.
setOperatorDeviceNumber
(
deviceNumber
);
log
.
setOperatorIp
(
ip
);
log
.
setCreatorId
(
operatorId
);
log
.
setUpdaterId
(
operatorId
);
this
.
save
(
log
);
}
}
yd-communication-service/src/main/java/com/yd/communication/service/service/impl/CoSessionServiceImpl.java
View file @
ed85d9a7
...
...
@@ -2,9 +2,11 @@ package com.yd.communication.service.service.impl;
import
com.yd.communication.service.model.CoSession
;
import
com.yd.communication.service.dao.CoSessionMapper
;
import
com.yd.communication.service.service.ICoSessionService
;
import
com.baomidou.mybatisplus.extension.service.impl.ServiceImpl
;
import
com.yd.communication.service.service.ICoSessionService
;
import
org.springframework.stereotype.Service
;
import
com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper
;
import
lombok.extern.slf4j.Slf4j
;
/**
* <p>
...
...
@@ -15,6 +17,34 @@ import org.springframework.stereotype.Service;
* @since 2026-07-28
*/
@Service
@Slf4j
public
class
CoSessionServiceImpl
extends
ServiceImpl
<
CoSessionMapper
,
CoSession
>
implements
ICoSessionService
{
/**
* 根据房间ID获取会话(WebSocket 使用)
*/
@Override
public
CoSession
getByRoomId
(
String
roomId
)
{
LambdaQueryWrapper
<
CoSession
>
wrapper
=
new
LambdaQueryWrapper
<>();
wrapper
.
eq
(
CoSession:
:
getRoomId
,
roomId
)
.
eq
(
CoSession:
:
getIsDeleted
,
0
);
return
this
.
getOne
(
wrapper
);
}
/**
* 根据业务ID获取会话
*/
@Override
public
CoSession
getByBizId
(
String
bizId
)
{
return
this
.
lambdaQuery
()
.
eq
(
CoSession:
:
getCoSessionBizId
,
bizId
)
.
last
(
" limit 1 "
)
.
one
();
}
@Override
public
int
updateCurrentPageAndHistory
(
String
roomId
,
String
currentPage
,
String
pageHistory
,
String
updaterId
){
return
baseMapper
.
updateCurrentPageAndHistory
(
roomId
,
currentPage
,
pageHistory
,
updaterId
);
}
}
yd-communication-service/src/main/java/com/yd/communication/service/service/impl/RecordingTaskServiceImpl.java
View file @
ed85d9a7
package
com
.
yd
.
communication
.
service
.
service
.
impl
;
import
com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper
;
import
com.yd.common.enums.CommonEnum
;
import
com.yd.common.exception.BusinessException
;
import
com.yd.common.utils.RandomStringGenerator
;
import
com.yd.communication.service.model.RecordingTask
;
import
com.yd.communication.service.dao.RecordingTaskMapper
;
import
com.yd.communication.service.service.IRecordingTaskService
;
import
com.baomidou.mybatisplus.extension.service.impl.ServiceImpl
;
import
lombok.extern.slf4j.Slf4j
;
import
org.springframework.stereotype.Service
;
import
java.time.LocalDateTime
;
import
java.util.UUID
;
/**
* <p>
...
...
@@ -14,7 +21,72 @@ import org.springframework.stereotype.Service;
* @author zxm
* @since 2026-07-28
*/
@Slf4j
@Service
public
class
RecordingTaskServiceImpl
extends
ServiceImpl
<
RecordingTaskMapper
,
RecordingTask
>
implements
IRecordingTaskService
{
/**
* 初始化录制信息(协同生成共享码的时候就初始化信息)
* @param bizId
* @param roomId
* @return
*/
@Override
public
String
startRecording
(
String
bizId
,
String
roomId
)
{
//任务ID
String
taskId
=
"agora_"
+
System
.
currentTimeMillis
();
RecordingTask
task
=
new
RecordingTask
();
//录制任务表唯一业务ID
task
.
setRecordingTaskBizId
(
RandomStringGenerator
.
generateBizId16
(
CommonEnum
.
UID_TYPE_RECORDING_TASK
.
getCode
()));
//任务编号
task
.
setTaskNo
(
"R"
+
System
.
currentTimeMillis
());
task
.
setTaskId
(
taskId
);
//关联的任务类型: 协同会话
task
.
setBizType
(
"co_session"
);
//关联的任务类型表的ID: 协同会话表唯一业务ID
task
.
setBizId
(
bizId
);
//房间号
task
.
setRoomId
(
roomId
);
//RTC频道名
task
.
setChannel
(
"channel_"
+
roomId
);
//录制模式
task
.
setRecordingMode
(
"mix"
);
//布局
task
.
setLayout
(
"grid"
);
//1-初始化
task
.
setStatus
(
"1"
);
task
.
setStartTime
(
LocalDateTime
.
now
());
task
.
setCreatorId
(
"system"
);
this
.
save
(
task
);
log
.
info
(
"启动录制成功, taskId={}, roomId={}"
,
taskId
,
roomId
);
return
taskId
;
}
@Override
public
void
stopRecording
(
String
taskId
)
{
LambdaQueryWrapper
<
RecordingTask
>
wrapper
=
new
LambdaQueryWrapper
<>();
wrapper
.
eq
(
RecordingTask:
:
getTaskId
,
taskId
);
RecordingTask
task
=
this
.
getOne
(
wrapper
);
if
(
task
==
null
)
{
throw
new
BusinessException
(
"录制任务不存在"
);
}
// 模拟停止录制
task
.
setStatus
(
"3"
);
task
.
setStopTime
(
LocalDateTime
.
now
());
task
.
setFileUrl
(
"https://oss.example.com/recordings/"
+
taskId
+
".mp4"
);
task
.
setFileDuration
(
120
);
task
.
setFileSize
(
1024000L
);
task
.
setFileMd5
(
UUID
.
randomUUID
().
toString
().
substring
(
0
,
32
));
task
.
setFileFormat
(
"mp4"
);
task
.
setStorageType
(
"oss"
);
task
.
setStorageBucket
(
"coordination-recordings"
);
task
.
setStoragePath
(
"/recordings/"
+
taskId
+
".mp4"
);
task
.
setUpdaterId
(
"system"
);
this
.
updateById
(
task
);
log
.
info
(
"停止录制成功, taskId={}"
,
taskId
);
}
}
yd-communication-service/src/main/java/com/yd/communication/service/utils/RandomUtil.java
0 → 100644
View file @
ed85d9a7
package
com
.
yd
.
communication
.
service
.
utils
;
import
java.security.SecureRandom
;
public
class
RandomUtil
{
private
static
final
SecureRandom
random
=
new
SecureRandom
();
public
static
String
generateNumericCode
(
int
length
)
{
StringBuilder
sb
=
new
StringBuilder
();
for
(
int
i
=
0
;
i
<
length
;
i
++)
{
sb
.
append
(
random
.
nextInt
(
10
));
}
return
sb
.
toString
();
}
}
\ No newline at end of file
yd-communication-service/target/classes/com/yd/communication/service/config/CommunicationMybatisPlusConfig$1.class
deleted
100644 → 0
View file @
53cb6cfe
File deleted
yd-communication-service/target/classes/com/yd/communication/service/config/CommunicationMybatisPlusConfig.class
deleted
100644 → 0
View file @
53cb6cfe
File deleted
yd-communication-service/target/classes/com/yd/communication/service/utils/MyBatisPlusCodeGenerator.class
deleted
100644 → 0
View file @
53cb6cfe
File deleted
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment