Commit ed85d9a7 by zhangxingmin

Merge remote-tracking branch 'origin/dev' into prod

parents 53cb6cfe 22b90e6a
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectStorage">
<option name="path" value="项目群组-&gt;银盾-微服务" />
<option name="projectId" value="4cc05386e898000" />
</component>
</project>
\ No newline at end of file
...@@ -17,6 +17,11 @@ ...@@ -17,6 +17,11 @@
<option name="url" value="http://139.224.145.34:8081/repository/yd-maven-public/" /> <option name="url" value="http://139.224.145.34:8081/repository/yd-maven-public/" />
</remote-repository> </remote-repository>
<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="id" value="central" />
<option name="name" value="Maven Central repository" /> <option name="name" value="Maven Central repository" />
<option name="url" value="https://repo1.maven.org/maven2" /> <option name="url" value="https://repo1.maven.org/maven2" />
......
# 基础镜像
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
...@@ -29,12 +29,6 @@ ...@@ -29,12 +29,6 @@
<artifactId>spring-boot-starter-web</artifactId> <artifactId>spring-boot-starter-web</artifactId>
</dependency> </dependency>
<!-- Spring Boot Starter WebSocket -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId> <artifactId>spring-boot-starter</artifactId>
......
//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
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
package com.yd.communication.api.config; package com.yd.communication.api.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.web.client.RestTemplate; import org.springframework.web.client.RestTemplate;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
@Slf4j
@Configuration @Configuration
public class RestTemplateConfig { public class RestTemplateConfig {
@Bean @Bean
public RestTemplate restTemplate() { 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
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
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
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);
}
}
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);
}
}
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 {
}
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 {
}
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 {
}
//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
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);
}
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);
}
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);
}
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);
}
}
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
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
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
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
${AnsiColor.GREEN}
_ _ _ __ __ ____ _ _ _ _
| | (_)_ __ | | _\ \ / /__ / ___| |__ __ _| |_ / \ _ __ (_)
| | | | '_ \| |/ /\ \ /\ / / _ \ | | '_ \ / _` | __| / _ \ | '_ \| |
| |___| | | | | < \ V V / __/ |___| | | | (_| | |_ / ___ \| |_) | |
|_____|_|_| |_|_|\_\ \_/\_/ \___|\____|_| |_|\__,_|\__/_/ \_\ .__/|_|
|_|
${AnsiColor.BRIGHT_WHITE}
Spring Boot Version: ${spring-boot.version}
\ No newline at end of file
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
#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
...@@ -24,6 +24,11 @@ ...@@ -24,6 +24,11 @@
<version>${project.version}</version> <version>${project.version}</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.yd</groupId>
<artifactId>yd-oss-feign</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId> <groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId> <artifactId>jackson-annotations</artifactId>
</dependency> </dependency>
......
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);
}
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
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);
}
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:";
}
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;
}
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;
}
}
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;
}
}
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;
}
}
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;
}
};
}
}
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;
}
};
}
}
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;
}
};
}
}
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
package com.yd.communication.feign.request;
import lombok.Data;
@Data
public class EndSessionRequest {
/**
* 房间号不能为空
*/
private String roomId;
}
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;
}
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
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;
}
package com.yd.communication.feign.request.http;
import lombok.Data;
@Data
public class GenerateTokenRequest {
/**
* 商城用户ID
*/
private Long sfpUserId;
/**
* 分销用户ID
*/
private Long cffpUserId;
}
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;
}
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;
}
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
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
package com.yd.communication.feign.response;
import lombok.Data;
@Data
public class GetStatusResponse {
/**
* 1-未失效 2-已失效
*/
private Integer status;
}
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
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
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;
}
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;
}
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;
}
...@@ -50,6 +50,12 @@ ...@@ -50,6 +50,12 @@
<artifactId>freemarker</artifactId> <artifactId>freemarker</artifactId>
</dependency> </dependency>
<!-- Spring Boot Starter WebSocket -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency> <dependency>
<groupId>com.yd</groupId> <groupId>com.yd</groupId>
<artifactId>yd-communication-feign</artifactId> <artifactId>yd-communication-feign</artifactId>
...@@ -67,6 +73,29 @@ ...@@ -67,6 +73,29 @@
<artifactId>yd-framework</artifactId> <artifactId>yd-framework</artifactId>
<version>${project.version}</version> <version>${project.version}</version>
</dependency> </dependency>
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>core</artifactId>
</dependency>
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>javase</artifactId>
</dependency>
<!-- &lt;!&ndash; 阿里云 Java SDK 核心库 &ndash;&gt;-->
<!-- <dependency>-->
<!-- <groupId>com.aliyun</groupId>-->
<!-- <artifactId>aliyun-java-sdk-core</artifactId>-->
<!-- <version>4.6.3</version>-->
<!-- </dependency>-->
<!-- &lt;!&ndash; 阿里云视频直播 SDK(包含 RTC 云端录制 API) &ndash;&gt;-->
<!-- <dependency>-->
<!-- <groupId>com.aliyun</groupId>-->
<!-- <artifactId>aliyun-java-sdk-live</artifactId>-->
<!-- <version>3.9.76</version>-->
<!-- </dependency>-->
</dependencies> </dependencies>
</project> </project>
\ No newline at end of file
...@@ -2,6 +2,10 @@ package com.yd.communication.service.dao; ...@@ -2,6 +2,10 @@ package com.yd.communication.service.dao;
import com.yd.communication.service.model.CoSession; import com.yd.communication.service.model.CoSession;
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Update;
import java.time.LocalDateTime;
/** /**
* <p> * <p>
...@@ -13,4 +17,14 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper; ...@@ -13,4 +17,14 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper;
*/ */
public interface CoSessionMapper extends BaseMapper<CoSession> { 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);
} }
...@@ -97,12 +97,24 @@ public class CoSession implements Serializable { ...@@ -97,12 +97,24 @@ public class CoSession implements Serializable {
private String roomId; private String roomId;
/** /**
* 协同房间密码(协同共享码,进入这个协同房间号的密码) * 协同房间密码(输入密码进入房间,协同共享码,进入这个协同房间号的密码)
*/ */
@TableField("room_pwd") @TableField("room_pwd")
private String roomPwd; private String roomPwd;
/** /**
* 协同房间二维码(扫码进入房间)
*/
@TableField("room_qr_code")
private String roomQrCode;
/**
* 协同房间链接(访问链接进入房间)
*/
@TableField("room_link")
private String roomLink;
/**
* RTC频道前缀,便于区分业务 * RTC频道前缀,便于区分业务
*/ */
@TableField("channel_prefix") @TableField("channel_prefix")
...@@ -121,10 +133,10 @@ public class CoSession implements Serializable { ...@@ -121,10 +133,10 @@ public class CoSession implements Serializable {
private String controlHolderId; private String controlHolderId;
/** /**
* 0-进行中,1-已结束,2-已超时 * 1-待开始,2-进行中,3-已结束,4-已超时
*/ */
@TableField("status") @TableField("status")
private Integer status; private String status;
/** /**
* 开始时间 * 开始时间
...@@ -185,4 +197,5 @@ public class CoSession implements Serializable { ...@@ -185,4 +197,5 @@ public class CoSession implements Serializable {
*/ */
@TableField("update_time") @TableField("update_time")
private LocalDateTime updateTime; private LocalDateTime updateTime;
} }
...@@ -49,7 +49,7 @@ public class RecordingTask implements Serializable { ...@@ -49,7 +49,7 @@ public class RecordingTask implements Serializable {
private String taskId; private String taskId;
/** /**
* 业务类型:co-session/meeting * 业务类型:co_session/meeting
*/ */
@TableField("biz_type") @TableField("biz_type")
private String bizType; private String bizType;
...@@ -85,10 +85,10 @@ public class RecordingTask implements Serializable { ...@@ -85,10 +85,10 @@ public class RecordingTask implements Serializable {
private String layout; private String layout;
/** /**
* 0-初始化,1-录制中,2-已停止,3-失败,4-已归档 * 1-初始化,2-录制中,3-已停止,4-失败,5-已归档
*/ */
@TableField("status") @TableField("status")
private Integer status; private String status;
/** /**
* 录制开始时间 * 录制开始时间
......
package com.yd.communication.service.service; 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.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> { 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
...@@ -13,4 +13,7 @@ import com.baomidou.mybatisplus.extension.service.IService; ...@@ -13,4 +13,7 @@ import com.baomidou.mybatisplus.extension.service.IService;
*/ */
public interface ICoOperationLogService extends IService<CoOperationLog> { public interface ICoOperationLogService extends IService<CoOperationLog> {
void log(String bizId, String operatorId, String operatorType,
String operatorName, String action, String content,
String deviceNumber, String ip);
} }
...@@ -2,6 +2,7 @@ package com.yd.communication.service.service; ...@@ -2,6 +2,7 @@ package com.yd.communication.service.service;
import com.yd.communication.service.model.CoSession; import com.yd.communication.service.model.CoSession;
import com.baomidou.mybatisplus.extension.service.IService; import com.baomidou.mybatisplus.extension.service.IService;
import org.springframework.transaction.annotation.Transactional;
/** /**
* <p> * <p>
...@@ -13,4 +14,15 @@ import com.baomidou.mybatisplus.extension.service.IService; ...@@ -13,4 +14,15 @@ import com.baomidou.mybatisplus.extension.service.IService;
*/ */
public interface ICoSessionService extends IService<CoSession> { 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);
} }
...@@ -13,4 +13,7 @@ import com.baomidou.mybatisplus.extension.service.IService; ...@@ -13,4 +13,7 @@ import com.baomidou.mybatisplus.extension.service.IService;
*/ */
public interface IRecordingTaskService extends IService<RecordingTask> { public interface IRecordingTaskService extends IService<RecordingTask> {
String startRecording(String bizId, String roomId);
void stopRecording(String taskId);
} }
package com.yd.communication.service.service.impl; 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.dao.CoDesensitizationRuleMapper;
import com.yd.communication.service.model.CoDesensitizationRule;
import com.yd.communication.service.service.ICoDesensitizationRuleService; 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.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/** import java.time.LocalDateTime;
* <p> import java.util.List;
* 协同-脱敏设置表(通用) 服务实现类
* </p> @Slf4j
*
* @author zxm
* @since 2026-07-28
*/
@Service @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
...@@ -4,6 +4,8 @@ import com.yd.communication.service.model.CoOperationLog; ...@@ -4,6 +4,8 @@ import com.yd.communication.service.model.CoOperationLog;
import com.yd.communication.service.dao.CoOperationLogMapper; import com.yd.communication.service.dao.CoOperationLogMapper;
import com.yd.communication.service.service.ICoOperationLogService; import com.yd.communication.service.service.ICoOperationLogService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
/** /**
...@@ -14,7 +16,29 @@ import org.springframework.stereotype.Service; ...@@ -14,7 +16,29 @@ import org.springframework.stereotype.Service;
* @author zxm * @author zxm
* @since 2026-07-28 * @since 2026-07-28
*/ */
@Slf4j
@Service @Service
public class CoOperationLogServiceImpl extends ServiceImpl<CoOperationLogMapper, CoOperationLog> implements ICoOperationLogService { 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);
}
} }
...@@ -2,9 +2,11 @@ package com.yd.communication.service.service.impl; ...@@ -2,9 +2,11 @@ package com.yd.communication.service.service.impl;
import com.yd.communication.service.model.CoSession; import com.yd.communication.service.model.CoSession;
import com.yd.communication.service.dao.CoSessionMapper; import com.yd.communication.service.dao.CoSessionMapper;
import com.yd.communication.service.service.ICoSessionService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.yd.communication.service.service.ICoSessionService;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import lombok.extern.slf4j.Slf4j;
/** /**
* <p> * <p>
...@@ -15,6 +17,34 @@ import org.springframework.stereotype.Service; ...@@ -15,6 +17,34 @@ import org.springframework.stereotype.Service;
* @since 2026-07-28 * @since 2026-07-28
*/ */
@Service @Service
@Slf4j
public class CoSessionServiceImpl extends ServiceImpl<CoSessionMapper, CoSession> implements ICoSessionService { 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);
}
} }
package com.yd.communication.service.service.impl; 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.model.RecordingTask;
import com.yd.communication.service.dao.RecordingTaskMapper; import com.yd.communication.service.dao.RecordingTaskMapper;
import com.yd.communication.service.service.IRecordingTaskService; import com.yd.communication.service.service.IRecordingTaskService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.UUID;
/** /**
* <p> * <p>
...@@ -14,7 +21,72 @@ import org.springframework.stereotype.Service; ...@@ -14,7 +21,72 @@ import org.springframework.stereotype.Service;
* @author zxm * @author zxm
* @since 2026-07-28 * @since 2026-07-28
*/ */
@Slf4j
@Service @Service
public class RecordingTaskServiceImpl extends ServiceImpl<RecordingTaskMapper, RecordingTask> implements IRecordingTaskService { 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);
}
} }
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
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment