Commit 3b15b4e7 by zhangxingmin

push

parent f9b0f3a2
...@@ -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" />
......
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
...@@ -44,7 +44,6 @@ public class ApiCoSessionController implements ApiCoSessionFeignClient { ...@@ -44,7 +44,6 @@ public class ApiCoSessionController implements ApiCoSessionFeignClient {
* @param request * @param request
* @return * @return
*/ */
@PostMapping("/join")
public Result<JoinResponse> join(JoinRequest request) { public Result<JoinResponse> join(JoinRequest request) {
return apiCoSessionService.join(request); return apiCoSessionService.join(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
* @param file
* @return
*/
public Result<Map<String, String>> stopRecording(String taskId,MultipartFile file) {
return apiRecordingTaskService.stopRecording(taskId,file);
}
/**
* 查询录制信息
* @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("/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 RuntimeException("启动录制失败,未返回 TaskId,响应:" + responseData);
}
log.info("启动录制成功, TaskId: {}", taskId);
return taskId;
} catch (Exception e) {
log.error("启动录制失败", e);
throw new RuntimeException("启动云端录制失败: " + 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 RuntimeException("停止云端录制失败: " + 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 RuntimeException("查询录制信息失败: " + 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.request.recording.ApiStartRecordingRequest;
import com.yd.communication.feign.response.recording.ApiQueryRecordingResponse;
import org.springframework.web.multipart.MultipartFile;
import java.util.Map;
public interface ApiRecordingTaskService {
Result<String> startRecording(ApiStartRecordingRequest request);
Result<Map<String, String>> stopRecording(String taskId, MultipartFile file);
Result<ApiQueryRecordingResponse> queryRecording(String taskId);
}
...@@ -168,6 +168,7 @@ public class ApiCoSessionServiceImpl implements ApiCoSessionService { ...@@ -168,6 +168,7 @@ public class ApiCoSessionServiceImpl implements ApiCoSessionService {
joinResponse.setControlHolderId(session.getControlHolderId()); joinResponse.setControlHolderId(session.getControlHolderId());
joinResponse.setControlHolderType(session.getControlHolderType()); joinResponse.setControlHolderType(session.getControlHolderType());
joinResponse.setResourceInit(session.getResourceInit()); joinResponse.setResourceInit(session.getResourceInit());
joinResponse.setCurrentPage(session.getCurrentPage());
joinResponse.setRoomId(session.getRoomId()); joinResponse.setRoomId(session.getRoomId());
joinResponse.setSessionBizId(session.getResourceInit()); joinResponse.setSessionBizId(session.getResourceInit());
......
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.enums.ProjectEnum;
import com.yd.common.enums.TenantEnum;
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.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.IRecordingTaskService;
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.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
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 ApiOssFeignClient apiOssFeignClient;
/**
* 开始录制
* @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");
if (iRecordingTaskService.count(checkWrapper) > 0) {
throw new BusinessException("该会话已有进行中的录制任务");
}
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.setRoomId(request.getRoomId());
task.setStatus("2"); // 录制中
task.setStartTime(LocalDateTime.now());
task.setCreatorId("system");
task.setIsDeleted(0);
task.setRecordingMode("screen");
iRecordingTaskService.save(task);
log.info("录制任务创建成功,taskId={}", task.getTaskId());
return Result.success(task.getTaskId());
}
/**
* 停止录制并上传视频文件到 OSS
* @param taskId
* @param file
* @return
*/
@Override
@Transactional(rollbackFor = Exception.class)
public Result<Map<String, String>> stopRecording(String taskId, MultipartFile file) {
log.info("停止录制并上传文件: taskId={}, fileName={}", taskId, file.getOriginalFilename());
// 1. 查询录制任务
LambdaQueryWrapper<RecordingTask> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(RecordingTask::getTaskId, taskId);
RecordingTask task = iRecordingTaskService.getOne(wrapper);
if (task == null) {
throw new BusinessException("录制任务不存在,taskId=" + taskId);
}
// 2. 构造 OSS 上传请求参数
ApiUploadFileRequest uploadRequest = new ApiUploadFileRequest();
uploadRequest.setTenantBizId(TenantEnum.YD.getTenantBizId());
uploadRequest.setProjectBizId(ProjectEnum.SFP_CLIENT.getProjectBizId());
// 对象业务ID
uploadRequest.setObjectBizId(task.getBizId());
// 对象类型:录制文件
uploadRequest.setObjectType("recording_task");
// 对象所属表名:录制任务表
uploadRequest.setObjectTableName("recording_task");
// 3. 调用 OSS Feign 客户端上传文件
Result<ApiUploadResponse> uploadResult = apiOssFeignClient.uploadFileBody(file, uploadRequest);
if (uploadResult == null || uploadResult.getCode() != 200) {
String errorMsg = uploadResult != null ? uploadResult.getMsg() : "上传服务返回空";
log.error("上传文件到OSS失败: {}", errorMsg);
throw new BusinessException("上传文件失败: " + errorMsg);
}
// 4. 获取上传结果
ApiUploadResponse uploadResponse = uploadResult.getData();
if (uploadResponse == null) {
throw new BusinessException("上传文件成功但未返回文件信息");
}
// 5. 更新录制任务
//文件URL
task.setFileUrl(uploadResponse.getUrl());
task.setFileSize(uploadResponse.getFileSize());
task.setStatus("3"); // 已停止
//文件扩展名
task.setFileFormat(uploadResponse.getFileType());
task.setStopTime(LocalDateTime.now());
iRecordingTaskService.updateById(task);
// 6. 返回结果
Map<String, String> result = new HashMap<>();
result.put("taskId", task.getTaskId());
result.put("fileUrl", task.getFileUrl());
result.put("fileKey", uploadResponse.getFileKey()); // 方便前端后续操作
log.info("录制文件上传成功,fileUrl={}", task.getFileUrl());
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
...@@ -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.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
* @param file 视频文件信息
* @return
*/
@PostMapping("/stop")
Result<Map<String, String>> stopRecording(@RequestParam("taskId") String taskId,
@RequestParam("file") MultipartFile file);
/**
* 查询录制信息
* @param taskId
* @return
*/
@GetMapping("/query/{taskId}")
Result<ApiQueryRecordingResponse> queryRecording(@PathVariable String taskId);
}
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, MultipartFile file) {
return null;
}
@Override
public Result<ApiQueryRecordingResponse> queryRecording(String taskId) {
return null;
}
};
}
}
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;
}
...@@ -26,6 +26,11 @@ public class JoinResponse { ...@@ -26,6 +26,11 @@ public class JoinResponse {
private String resourceInit; private String resourceInit;
/** /**
* 当前操作页面(实时更新,示例:{url:https://mini.xxx.com/pages/my/index?userId=xxx})
*/
private String currentPage;
/**
* 控制权持有者类型:owner-客户,participant-顾问 * 控制权持有者类型:owner-客户,participant-顾问
*/ */
private String controlHolderType; private String controlHolderType;
......
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;
}
...@@ -73,6 +73,20 @@ ...@@ -73,6 +73,20 @@
<artifactId>yd-framework</artifactId> <artifactId>yd-framework</artifactId>
<version>${project.version}</version> <version>${project.version}</version>
</dependency> </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> </dependencies>
</project> </project>
\ 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