|
|
@@ -15,10 +15,8 @@ import com.fs.common.core.domain.entity.SysDictData;
|
|
|
import com.fs.common.core.redis.RedisCache;
|
|
|
import com.fs.common.enums.BizResponseEnum;
|
|
|
import com.fs.common.exception.CustomException;
|
|
|
-import com.fs.common.exception.base.BaseException;
|
|
|
import com.fs.common.utils.CloudHostUtils;
|
|
|
import com.fs.common.utils.DateUtils;
|
|
|
-import com.fs.common.utils.PubFun;
|
|
|
import com.fs.common.utils.StringUtils;
|
|
|
import com.fs.common.utils.date.DateUtil;
|
|
|
import com.fs.company.constant.CompanyTrafficConstants;
|
|
|
@@ -37,10 +35,7 @@ import com.fs.course.dto.CoursePackageDTO;
|
|
|
import com.fs.course.mapper.*;
|
|
|
import com.fs.course.param.*;
|
|
|
import com.fs.course.param.newfs.*;
|
|
|
-import com.fs.course.service.IFsUserCompanyBindService;
|
|
|
-import com.fs.course.service.IFsUserCompanyUserService;
|
|
|
-import com.fs.course.service.IFsUserCourseVideoService;
|
|
|
-import com.fs.course.service.IFsVideoResourceService;
|
|
|
+import com.fs.course.service.*;
|
|
|
import com.fs.course.vo.*;
|
|
|
import com.fs.course.vo.newfs.*;
|
|
|
import com.fs.his.domain.FsUser;
|
|
|
@@ -62,36 +57,38 @@ import com.fs.qw.mapper.QwUserMapper;
|
|
|
import com.fs.qw.param.FsUserCourseRedPageParam;
|
|
|
import com.fs.qw.service.IQwCompanyService;
|
|
|
import com.fs.qw.service.IQwExternalContactService;
|
|
|
-import com.fs.qw.vo.SortDayVo;
|
|
|
import com.fs.qwApi.Result.QwAddContactWayResult;
|
|
|
-import com.fs.qwApi.Result.QwGroupChatDetailsResult;
|
|
|
import com.fs.qwApi.param.QwAddContactWayParam;
|
|
|
import com.fs.qwApi.service.QwApiService;
|
|
|
-import com.fs.sop.domain.QwSopTempDay;
|
|
|
import com.fs.sop.domain.SopUserLogsInfo;
|
|
|
import com.fs.sop.mapper.QwSopLogsMapper;
|
|
|
import com.fs.sop.mapper.SopUserLogsInfoMapper;
|
|
|
import com.fs.sop.service.ISopUserLogsInfoService;
|
|
|
-import com.fs.system.domain.SysConfig;
|
|
|
import com.fs.system.mapper.SysDictDataMapper;
|
|
|
import com.fs.system.service.ISysConfigService;
|
|
|
import com.fs.voice.utils.StringUtil;
|
|
|
import com.github.binarywang.wxpay.bean.transfer.TransferBillsResult;
|
|
|
-import com.google.common.collect.Sets;
|
|
|
+import com.volcengine.service.vod.IVodService;
|
|
|
+import com.volcengine.service.vod.model.business.VodUrlUploadURLSet;
|
|
|
+import com.volcengine.service.vod.model.request.VodGetMediaInfosRequest;
|
|
|
+import com.volcengine.service.vod.model.request.VodQueryUploadTaskInfoRequest;
|
|
|
+import com.volcengine.service.vod.model.request.VodUpdateMediaPublishStatusRequest;
|
|
|
+import com.volcengine.service.vod.model.request.VodUrlUploadRequest;
|
|
|
+import com.volcengine.service.vod.model.response.VodGetMediaInfosResponse;
|
|
|
+import com.volcengine.service.vod.model.response.VodQueryUploadTaskInfoResponse;
|
|
|
+import com.volcengine.service.vod.model.response.VodUpdateMediaPublishStatusResponse;
|
|
|
+import com.volcengine.service.vod.model.response.VodUrlUploadResponse;
|
|
|
import lombok.extern.slf4j.Slf4j;
|
|
|
import org.apache.commons.collections4.CollectionUtils;
|
|
|
import org.apache.rocketmq.spring.core.RocketMQTemplate;
|
|
|
-import org.jetbrains.annotations.NotNull;
|
|
|
import org.redisson.api.RLock;
|
|
|
import org.redisson.api.RedissonClient;
|
|
|
-import org.redisson.client.RedisClient;
|
|
|
import org.slf4j.Logger;
|
|
|
import org.slf4j.LoggerFactory;
|
|
|
import org.springframework.beans.BeanUtils;
|
|
|
import org.springframework.beans.BeansException;
|
|
|
import org.springframework.beans.factory.annotation.Autowired;
|
|
|
import org.springframework.beans.factory.annotation.Value;
|
|
|
-import org.springframework.data.redis.core.RedisTemplate;
|
|
|
import org.springframework.stereotype.Service;
|
|
|
import org.springframework.transaction.annotation.Transactional;
|
|
|
|
|
|
@@ -100,10 +97,9 @@ import java.math.RoundingMode;
|
|
|
import java.text.SimpleDateFormat;
|
|
|
import java.time.*;
|
|
|
import java.time.format.DateTimeFormatter;
|
|
|
-import java.time.temporal.ChronoUnit;
|
|
|
import java.util.*;
|
|
|
-import java.util.concurrent.CompletableFuture;
|
|
|
-import java.util.concurrent.TimeUnit;
|
|
|
+import java.util.concurrent.*;
|
|
|
+import java.util.concurrent.atomic.AtomicInteger;
|
|
|
import java.util.concurrent.atomic.AtomicLong;
|
|
|
import java.util.stream.Collectors;
|
|
|
|
|
|
@@ -257,7 +253,8 @@ public class FsUserCourseVideoServiceImpl implements IFsUserCourseVideoService
|
|
|
@Autowired
|
|
|
private BalanceRollbackErrorMapper balanceRollbackErrorMapper;
|
|
|
|
|
|
-
|
|
|
+ @Autowired
|
|
|
+ private IFsCourseLinkService linkService;
|
|
|
|
|
|
|
|
|
/**
|
|
|
@@ -3317,5 +3314,319 @@ public class FsUserCourseVideoServiceImpl implements IFsUserCourseVideoService
|
|
|
return fsUserCourseVideoMapper.getChooseCourseVideoListByMap(params);
|
|
|
}
|
|
|
|
|
|
+ @Override
|
|
|
+ public R uploadVideoToHuoShanByUrl() {
|
|
|
+ List <FsVideoResource> videos = fsUserCourseVideoMapper.selectVideoByHuaWei();
|
|
|
+ for (FsVideoResource video : videos){
|
|
|
+ uploadVideoByUrl(video);
|
|
|
+ }
|
|
|
+ return R.ok();
|
|
|
+ }
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private IVodService vodService;
|
|
|
+
|
|
|
+ //通过Url上传视频
|
|
|
+ public void uploadVideoByUrl(FsVideoResource videoResource) {
|
|
|
+
|
|
|
+ try {
|
|
|
+ VodUrlUploadRequest.Builder reqBuilder = VodUrlUploadRequest.newBuilder();
|
|
|
+ //空间名称
|
|
|
+ reqBuilder.setSpaceName(cloudHostProper.getSpaceName());
|
|
|
+ VodUrlUploadURLSet.Builder uRLSetsBuilder = VodUrlUploadURLSet.newBuilder();
|
|
|
+ //源文件 URL
|
|
|
+ uRLSetsBuilder.setSourceUrl(videoResource.getLine2());//华为云
|
|
|
+ //存储类型。默认为 1。取值如下:
|
|
|
+ //1:标准存储。
|
|
|
+ //2:归档存储。
|
|
|
+ //3:低频存储。
|
|
|
+ uRLSetsBuilder.setStorageClass(1);
|
|
|
+ //文件后缀,即点播存储中文件的类型。
|
|
|
+ uRLSetsBuilder.setFileExtension(".mp4");
|
|
|
+ //用户额外信息,最大长度 512 字节。
|
|
|
+ uRLSetsBuilder.setCallbackArgs("");
|
|
|
+ // 火山云存储路径(文件上传后在火山云的路径)
|
|
|
+// String datePath = new SimpleDateFormat("yyyyMMdd").format(new Date());
|
|
|
+// String fileName = System.currentTimeMillis() + ".mp4";
|
|
|
+// String remoteFileName = "fs/" + datePath + "/" + fileName;
|
|
|
+
|
|
|
+ uRLSetsBuilder.setFileName(videoResource.getFileKey());
|
|
|
+ reqBuilder.addURLSets(uRLSetsBuilder);
|
|
|
+
|
|
|
+ VodUrlUploadResponse resp = vodService.uploadMediaByUrl(reqBuilder.build());
|
|
|
+
|
|
|
+ if (resp.getResponseMetadata().hasError()) {
|
|
|
+ log.info("上传返回异常:{}",resp.getResponseMetadata().getError());
|
|
|
+ System.exit(-1);
|
|
|
+ }else {
|
|
|
+ FsVideoResource video = new FsVideoResource();
|
|
|
+ video.setId(videoResource.getId());
|
|
|
+ video.setJobId(resp.getResult().getData(0).getJobId());
|
|
|
+ //更新JobId
|
|
|
+ fsVideoResourceMapper.updateById(video);
|
|
|
+ }
|
|
|
+ log.info("上传返回参数:{}",resp);
|
|
|
+ } catch (Exception e) {
|
|
|
+ throw new RuntimeException("视频上传失败: " + e.getMessage(), e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public R getVidByJob() {
|
|
|
+ // 查询有JobId的视频
|
|
|
+ List<FsVideoResource> list = fsUserCourseVideoMapper.selectVideoByJobId();
|
|
|
+ if (list.isEmpty()) {
|
|
|
+ log.info("没有待上传的视频任务");
|
|
|
+ return R.error();
|
|
|
+ }
|
|
|
+ // 按五百一批切割
|
|
|
+ List<List<FsVideoResource>> batches = splitList(list, 500);
|
|
|
+ log.info("总任务 {} 条,分成 {} 批", list.size(), batches.size());
|
|
|
+
|
|
|
+ int batchIndex = 1;
|
|
|
+
|
|
|
+ // 批次顺序执行,每批内部多线程并发执行
|
|
|
+ for (List<FsVideoResource> batch : batches) {
|
|
|
+
|
|
|
+ log.info("开始执行批次 {}/{},本批任务 {} 条", batchIndex, batches.size(), batch.size());
|
|
|
+
|
|
|
+ CountDownLatch latch = new CountDownLatch(batch.size());
|
|
|
+
|
|
|
+ for (FsVideoResource video : batch) {
|
|
|
+ uploadExecutor.submit(() -> {
|
|
|
+ try {
|
|
|
+ uploadSingleTaskWithRetry(video,1);
|
|
|
+ } finally {
|
|
|
+ latch.countDown();
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ // 等待这一批全部完成
|
|
|
+ try {
|
|
|
+ latch.await();
|
|
|
+ } catch (InterruptedException e) {
|
|
|
+ log.error("批次等待异常", e);
|
|
|
+ }
|
|
|
+
|
|
|
+ log.info("批次 {}/{} 执行完成", batchIndex, batches.size());
|
|
|
+ batchIndex++;
|
|
|
+ }
|
|
|
+
|
|
|
+ log.info("全部批次执行完成");
|
|
|
+ return R.ok();
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public R getVideoInfoByVid() {
|
|
|
+ // 查询有JobId的视频
|
|
|
+ List<FsVideoResource> list = fsUserCourseVideoMapper.selectVideoByVid();
|
|
|
+ if (list.isEmpty()) {
|
|
|
+ log.info("没有包含vid的视频");
|
|
|
+ return R.error();
|
|
|
+ }
|
|
|
+ // 按五百一批切割
|
|
|
+ List<List<FsVideoResource>> batches = splitList(list, 500);
|
|
|
+ log.info("总任务 {} 条,分成 {} 批", list.size(), batches.size());
|
|
|
+
|
|
|
+ int batchIndex = 1;
|
|
|
+
|
|
|
+ // 批次顺序执行,每批内部多线程并发执行
|
|
|
+ for (List<FsVideoResource> batch : batches) {
|
|
|
+
|
|
|
+ log.info("开始执行批次 {}/{},本批任务 {} 条", batchIndex, batches.size(), batch.size());
|
|
|
+
|
|
|
+ CountDownLatch latch = new CountDownLatch(batch.size());
|
|
|
+
|
|
|
+ for (FsVideoResource video : batch) {
|
|
|
+ uploadExecutor.submit(() -> {
|
|
|
+ try {
|
|
|
+ uploadSingleTaskWithRetry(video,2);
|
|
|
+ } finally {
|
|
|
+ latch.countDown();
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ // 等待这一批全部完成
|
|
|
+ try {
|
|
|
+ latch.await();
|
|
|
+ } catch (InterruptedException e) {
|
|
|
+ log.error("批次等待异常", e);
|
|
|
+ }
|
|
|
+
|
|
|
+ log.info("批次 {}/{} 执行完成", batchIndex, batches.size());
|
|
|
+ batchIndex++;
|
|
|
+ }
|
|
|
+
|
|
|
+ log.info("全部批次执行完成");
|
|
|
+ return R.ok();
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public R createRoomMiniLinkByCourse(FsCourseLinkRoomNewParam param) {
|
|
|
+
|
|
|
+ QwUser qwUser = qwUserMapper.selectQwUserById(param.getQwUserId());
|
|
|
+
|
|
|
+ if (qwUser == null || qwUser.getCompanyId() == null || qwUser.getCompanyUserId() == null) {
|
|
|
+ return R.error("员工未绑定 销售公司 或 销售 请先绑定");
|
|
|
+ }
|
|
|
+
|
|
|
+ QwCompany qwCompany = iQwCompanyService.getQwCompanyByRedis(param.getCorpId());
|
|
|
+
|
|
|
+ if (qwCompany == null) {
|
|
|
+ return R.error().put("msg", "企业不存在,请联系管理员");
|
|
|
+ }
|
|
|
+ if (qwCompany.getMiniAppId() == null) {
|
|
|
+ return R.error().put("msg", "当前企业未绑定小程序,请联系管理员");
|
|
|
+ }
|
|
|
+
|
|
|
+ //生成小程序链接
|
|
|
+ String linkByMiniApp = createLinkByMiniApp(new Date(), param.getCourseId(), param.getVideoId(), qwUser, null, 2, null, 1);
|
|
|
+
|
|
|
+ if(StringUtils.isNotEmpty(linkByMiniApp)){
|
|
|
+ linkByMiniApp = linkByMiniApp.replaceAll(".html", "");
|
|
|
+ }
|
|
|
+ String link = linkService.getGotoWxAppLink(linkByMiniApp, qwCompany.getMiniAppId());
|
|
|
+
|
|
|
+ System.out.println("生成课程通链: " + link + " :" + param);
|
|
|
+
|
|
|
+ return R.ok().put("link", link);
|
|
|
+ }
|
|
|
+
|
|
|
+ private final ExecutorService uploadExecutor = new ThreadPoolExecutor(
|
|
|
+ 8, // core
|
|
|
+ 16, // max
|
|
|
+ 60L, TimeUnit.SECONDS,
|
|
|
+ new LinkedBlockingQueue<>(2000),
|
|
|
+ new ThreadFactory() {
|
|
|
+ private final AtomicInteger index = new AtomicInteger(1);
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public Thread newThread(Runnable r) {
|
|
|
+ return new Thread(r, "video-upload-" + index.getAndIncrement());
|
|
|
+ }
|
|
|
+ },
|
|
|
+ new ThreadPoolExecutor.CallerRunsPolicy()
|
|
|
+ );
|
|
|
+
|
|
|
+ public <T> List<List<T>> splitList(List<T> list, int batchSize) {
|
|
|
+ List<List<T>> result = new ArrayList<>();
|
|
|
+ int total = list.size();
|
|
|
+ for (int i = 0; i < total; i += batchSize) {
|
|
|
+ result.add(list.subList(i, Math.min(total, i + batchSize)));
|
|
|
+ }
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ //根据jobid查询上传视频的vid
|
|
|
+ public void getVidByJobId(FsVideoResource videoResource){
|
|
|
+ try {
|
|
|
+ VodQueryUploadTaskInfoRequest.Builder reqBuilder = VodQueryUploadTaskInfoRequest.newBuilder();
|
|
|
+ reqBuilder.setJobIds(videoResource.getJobId());
|
|
|
+
|
|
|
+ VodQueryUploadTaskInfoResponse resp = vodService.queryUploadTaskInfo(reqBuilder.build());
|
|
|
+ if (resp.getResponseMetadata().hasError()) {
|
|
|
+ System.out.println(resp.getResponseMetadata().getError());
|
|
|
+ System.exit(-1);
|
|
|
+ }else {
|
|
|
+ if (StringUtils.isEmpty(resp.getResult().getData().getMediaInfoList(0).getVid())){
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ FsVideoResource video = new FsVideoResource();
|
|
|
+ video.setId(videoResource.getId());
|
|
|
+ video.setHsyVid(resp.getResult().getData().getMediaInfoList(0).getVid());
|
|
|
+ fsVideoResourceMapper.updateById(video);
|
|
|
+ }
|
|
|
+ System.out.println(resp);
|
|
|
+ } catch (Exception e) {
|
|
|
+ throw new RuntimeException("查询 URL 批量上传任务状态: " + e.getMessage(), e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ //根据vid获取视频详情
|
|
|
+ public void getVideoInfoByVid(FsVideoResource videoResource) {
|
|
|
+ try {
|
|
|
+ VodGetMediaInfosRequest.Builder reqBuilder = VodGetMediaInfosRequest.newBuilder();
|
|
|
+ reqBuilder.setVids(videoResource.getHsyVid());
|
|
|
+
|
|
|
+ VodGetMediaInfosResponse resp = vodService.getMediaInfos20230701(reqBuilder.build());
|
|
|
+ if (resp.getResponseMetadata().hasError()) {
|
|
|
+ System.out.println(resp.getResponseMetadata().getError());
|
|
|
+ System.exit(-1);
|
|
|
+ }else {
|
|
|
+ //如果路径是空的直接返回
|
|
|
+ if (StringUtils.isEmpty(resp.getResult().getMediaInfoList(0).getSourceInfo().getStoreUri())){
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ //如果是未发布状态修改发布状态
|
|
|
+ if (resp.getResult().getMediaInfoList(0).getBasicInfo().getPublishStatus().equals("Unpublished")){
|
|
|
+ updateMediaPublishStatus(videoResource.getHsyVid());
|
|
|
+ }
|
|
|
+ //更新视频资源
|
|
|
+ String url = cloudHostProper.volcengineUrl+"/"+resp.getResult().getMediaInfoList(0).getSourceInfo().getStoreUri();
|
|
|
+
|
|
|
+ FsVideoResource fsVideoResource = new FsVideoResource();
|
|
|
+ fsVideoResource.setId(videoResource.getId());
|
|
|
+ fsVideoResource.setLine2(url);
|
|
|
+ fsVideoResourceMapper.updateById(fsVideoResource);
|
|
|
+
|
|
|
+ //更新小节
|
|
|
+ FsUserCourseVideo courseVideo = new FsUserCourseVideo();
|
|
|
+ courseVideo.setFileKey(videoResource.getFileKey());
|
|
|
+ courseVideo.setLineTwo(url);
|
|
|
+ fsUserCourseVideoMapper.updateFsUserCourseVideoByFileKeyForHsy(courseVideo);
|
|
|
+
|
|
|
+ }
|
|
|
+ System.out.println(resp);
|
|
|
+ } catch (Exception e) {
|
|
|
+ e.printStackTrace();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ //修改发布状态
|
|
|
+ public void updateMediaPublishStatus(String vid){
|
|
|
+ String statusPublished = "Published";
|
|
|
+
|
|
|
+ try {
|
|
|
+ // publish
|
|
|
+ VodUpdateMediaPublishStatusRequest.Builder req = VodUpdateMediaPublishStatusRequest.newBuilder();
|
|
|
+ req.setVid(vid);
|
|
|
+ req.setStatus(statusPublished);
|
|
|
+
|
|
|
+ VodUpdateMediaPublishStatusResponse resp = vodService.updateMediaPublishStatus(req.build());
|
|
|
+ System.out.println(resp);
|
|
|
+
|
|
|
+ Thread.sleep(1000);
|
|
|
+
|
|
|
+ } catch (Exception e) {
|
|
|
+ e.printStackTrace();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ public void uploadSingleTaskWithRetry(FsVideoResource videoResource,Integer type) {
|
|
|
+ int maxRetry = 3;
|
|
|
+ for (int i = 1; i <= maxRetry; i++) {
|
|
|
+ try {
|
|
|
+ if (type == 1){
|
|
|
+ //获取上传成功的视频vid,同步到数据库
|
|
|
+ getVidByJobId(videoResource);
|
|
|
+ }else if (type == 2){
|
|
|
+ //获取视频地址同步到线路二
|
|
|
+ getVideoInfoByVid(videoResource);
|
|
|
+ }
|
|
|
+ return;
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("视频 {} 上传失败,第 {} 次重试,原因:{}",
|
|
|
+ videoResource.getId(), i, e.getMessage());
|
|
|
+ if (i == maxRetry) {
|
|
|
+ log.error("视频 {} 上传最终失败!", videoResource.getId());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
}
|
|
|
|