三七 3 тижнів тому
батько
коміт
13c97fee6a

+ 44 - 3
fs-admin/src/main/java/com/fs/course/controller/FsUserVideoController.java

@@ -13,6 +13,7 @@ import com.fs.course.domain.FsUserCourseVideo;
 import com.fs.course.domain.FsUserVideo;
 import com.fs.course.domain.FsVideoResource;
 import com.fs.course.param.*;
+import com.fs.course.service.HsyAssumeRoleService;
 import com.fs.course.service.IFsUserCourseVideoService;
 import com.fs.course.service.IFsUserVideoService;
 import com.fs.course.service.IFsVideoResourceService;
@@ -23,17 +24,17 @@ import com.fs.his.service.IFsPackageService;
 import com.fs.his.vo.FsPackageVO;
 import com.fs.system.oss.CloudStorageService;
 import com.fs.system.oss.OSSFactory;
+import com.volcengine.model.response.AssumeRoleResponse;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.io.FilenameUtils;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.core.RedisTemplate;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.*;
 import org.springframework.web.multipart.MultipartFile;
 
 import java.io.*;
-import java.util.Date;
-import java.util.List;
-import java.util.UUID;
+import java.util.*;
 
 /**
  * 课堂视频Controller
@@ -60,6 +61,12 @@ public class FsUserVideoController extends BaseController
     @Autowired
     private IFsUserCourseVideoService fsUserCourseVideoService;
 
+    @Autowired
+    private RedisTemplate redisTemplate;
+
+    @Autowired
+    private HsyAssumeRoleService hsyAssumeRoleService;
+
     /**
      * 查询课堂视频列表
      */
@@ -372,4 +379,38 @@ public class FsUserVideoController extends BaseController
             return R.error("服务器内部错误");
         }
     }
+
+    @PreAuthorize("@ss.hasPermi('course:userVideo:uploadUserVideo')")
+    @Log(title = "课堂视频", businessType = BusinessType.INSERT)
+    @PostMapping("/uploadUserVideo")
+    public AjaxResult upload(@RequestParam("file") MultipartFile file, @RequestParam("uploadId")String uploadId) {
+        String vid = fsUserVideoService.uploadVideo(file, uploadId);
+
+        Map<String, Object> res = new HashMap<>();
+        res.put("uploadId", uploadId);
+        res.put("vid", vid);
+        return new AjaxResult(200, "上传成功", res);
+    }
+
+
+    @GetMapping("/uploadProgress")
+    public AjaxResult getUploadProgress(@RequestParam String uploadId) {
+        Object percentObj = redisTemplate.opsForValue()
+                .get("vod:upload:" + uploadId);
+
+        int percent = 0;
+        if (percentObj != null) {
+            percent = Integer.parseInt(percentObj.toString());
+        }
+
+        Map<String, Object> res = new HashMap<>();
+        res.put("percent", percent);
+        return AjaxResult.success(res);
+    }
+
+    @GetMapping("/HsyAssumeRoleService")
+    public AjaxResult HsyAssumeRoleService() throws Exception {
+        AssumeRoleResponse roleResponse = hsyAssumeRoleService.getRoleResponse();
+        return AjaxResult.success(roleResponse);
+    }
 }

+ 8 - 0
fs-service/pom.xml

@@ -291,6 +291,14 @@
             <version>1.1.26</version>
         </dependency>
 
+        <!--火山云sdk-->
+        <dependency>
+            <groupId>com.volcengine</groupId>
+            <artifactId>volc-sdk-java</artifactId>
+            <version>1.0.250</version>
+        </dependency>
+
+
     </dependencies>
 
 </project>

+ 8 - 0
fs-service/src/main/java/com/fs/config/cloud/CloudHostProper.java

@@ -15,4 +15,12 @@ public class CloudHostProper {
 
     @Value("${cloud_host.projectCode}")
     private String projectCode;
+
+    //火山云空间名称
+    @Value("${cloud_host.spaceName}")
+    public String spaceName;
+
+    //火山云空间绑定域名
+    @Value("${cloud_host.volcengineUrl}")
+    public String volcengineUrl;
 }

+ 5 - 0
fs-service/src/main/java/com/fs/course/domain/FsVideoResource.java

@@ -99,4 +99,9 @@ public class FsVideoResource {
     private Integer sort;
 
     private Long userId;
+
+    //火山云vid
+    private String hsyVid;
+
+    private String jobId;
 }

+ 17 - 0
fs-service/src/main/java/com/fs/course/mapper/FsUserCourseVideoMapper.java

@@ -1,6 +1,7 @@
 package com.fs.course.mapper;
 
 import com.fs.course.domain.FsUserCourseVideo;
+import com.fs.course.domain.FsVideoResource;
 import com.fs.course.param.CourseVideoUpdates;
 import com.fs.course.param.FsCourseListBySidebarParam;
 import com.fs.course.param.FsUserCourseVideoListUParam;
@@ -217,9 +218,25 @@ public interface FsUserCourseVideoMapper
             "WHERE file_key = #{fileKey}")
     void updateFsUserCourseVideoByFileKey(FsUserCourseVideo courseVideo);
 
+    @Update("UPDATE fs_user_course_video " +
+            "SET line_two = #{lineTwo}, " +
+            "    update_time = NOW() " +  // 添加更新时间
+            "WHERE file_key = #{fileKey}")
+    void updateFsUserCourseVideoByFileKeyForHsy(FsUserCourseVideo courseVideo);
+
     @Select("select title from fs_user_course_video WHERE video_id=#{videoId}")
     String selectFsUserCourseVideoByVideoForTitle(@Param("videoId") Long videoId);
 
     FsUserCourseVideo selectFsUserCourseVideoByVideoIdAndUserId(@Param("videoId") Long videoId,@Param("userId") Long userId);
 
+    @Select("select * from fs_video_resource where line2 is not null and (job_id is null or job_id='')")
+    List<FsVideoResource> selectVideoByHuaWei();
+
+    @Select("select * from fs_video_resource where job_id is not null and  (hsy_vid is null or hsy_vid='')")
+    List<FsVideoResource> selectVideoByJobId();
+
+
+    @Select("select * from fs_video_resource where hsy_vid is not null")
+    List<FsVideoResource> selectVideoByVid();
+
 }

+ 58 - 0
fs-service/src/main/java/com/fs/course/service/HsyAssumeRoleService.java

@@ -0,0 +1,58 @@
+package com.fs.course.service;
+
+import com.fs.config.cloud.CloudHostProper;
+import com.hc.openapi.tool.fastjson.JSON;
+import com.volcengine.model.request.AssumeRoleRequest;
+import com.volcengine.model.response.AssumeRoleResponse;
+import com.volcengine.service.sts.ISTSService;
+import com.volcengine.service.sts.impl.STSServiceImpl;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+
+@Service
+public class HsyAssumeRoleService {
+    @Autowired
+    private CloudHostProper cloudHostProper;
+
+    @Value("${hsy.role_access_key:''}")
+    private String role_access_key;
+
+    @Value("${hsy.role_secret_key:''}")
+    private String role_secret_key;
+
+    @Value("${hsy.role_trn:''}")
+    private String role_trn;
+    public AssumeRoleResponse getRoleResponse() throws Exception {
+        ISTSService stsService = STSServiceImpl.getInstance();
+
+        stsService.setAccessKey(role_access_key);
+        stsService.setSecretKey(role_secret_key);
+
+//        stsService.setAccessKey("AKLTZTc4YTE4ZjI2OWViNDNjZGI2NjhiYTI5Njc5ZjA1Mzk");
+//        stsService.setSecretKey("WXpjelpUYzFOakF5TUdObE5EZGtNR0ZsWXpKaU1tTmtZakk1WXpObE4yRQ==");
+
+        AssumeRoleRequest request = new AssumeRoleRequest();
+        request.setRoleSessionName(cloudHostProper.spaceName);
+        request.setDurationSeconds(900);
+        request.setRoleTrn(role_trn);
+
+        AssumeRoleResponse resp = stsService.assumeRole(request);
+        return resp;
+    }
+
+    public static void main(String[] args) throws Exception {
+        ISTSService stsService = STSServiceImpl.getInstance();
+
+        stsService.setAccessKey("AKLTNmMwNjJkNDFhYTVjNDIzYzhhNzEyZmZmZTlmYzBhNGM");
+        stsService.setSecretKey("T0RaaFl6UmhZV1V4WXpKbU5EWTBNMkZpT0RNNU9UY3daak0wTjJFd09XUQ==");
+
+        AssumeRoleRequest request = new AssumeRoleRequest();
+        request.setRoleSessionName("just_for_test");
+        request.setDurationSeconds(900);
+        request.setRoleTrn("trn:iam::2114522511:role/hylj");
+        AssumeRoleResponse resp = stsService.assumeRole(request);
+        System.out.println(JSON.toJSONString(resp));
+
+    }
+}

+ 19 - 0
fs-service/src/main/java/com/fs/course/service/IFsUserCourseVideoService.java

@@ -193,4 +193,23 @@ public interface IFsUserCourseVideoService
     R isAddKfIsOpen(FsUserCourseVideoAddKfUParam param);
 
     R getInternetTrafficIsOpen(FsUserCourseVideoFinishUParam param);
+
+    /**
+     * 上传视频到火山云通过URL(把华为云的视频传到火山去并且存储JOBID)
+     * @return
+     */
+    R uploadVideoToHuoShanByUrl();
+
+    /**
+     * 通过jobId拿vid
+     * @return
+     */
+    R getVidByJob();
+
+    /**
+     * 通过vid获取视频路径并且组装地址存到替换线路二
+     * @return
+     */
+    R getVideoInfoByVid();
+
 }

+ 6 - 0
fs-service/src/main/java/com/fs/course/service/IFsUserVideoService.java

@@ -9,6 +9,7 @@ import com.fs.course.vo.FsUserVideoListPVO;
 import com.fs.course.vo.FsUserVideoPVO;
 import com.fs.course.param.FsUserVideoParam;
 import com.fs.course.vo.FsUserVideoListUVO;
+import org.springframework.web.multipart.MultipartFile;
 
 /**
  * 课堂视频Service接口
@@ -92,4 +93,9 @@ public interface IFsUserVideoService {
     void updateVideoUrl();
 
     List<FsUserVideoListUVO> addNum(List<FsUserVideoListUVO> oldList);
+
+    String getVideoInfoByVid(String vid);
+
+
+    String uploadVideo(MultipartFile file, String uploadId);
 }

+ 301 - 2
fs-service/src/main/java/com/fs/course/service/impl/FsUserCourseVideoServiceImpl.java

@@ -70,6 +70,16 @@ 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.commons.lang.exception.ExceptionUtils;
@@ -95,8 +105,8 @@ import java.text.SimpleDateFormat;
 import java.time.*;
 import java.time.format.DateTimeFormatter;
 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;
 
@@ -2770,6 +2780,295 @@ public class FsUserCourseVideoServiceImpl implements IFsUserCourseVideoService
         return R.ok();
     }
 
+    @Override
+    public R uploadVideoToHuoShanByUrl() {
+        List <FsVideoResource> videos = fsUserCourseVideoMapper.selectVideoByHuaWei();
+        for (FsVideoResource video : videos){
+            uploadVideoByUrl(video);
+        }
+        return R.ok();
+    }
+
+    @Autowired
+    private IVodService vodService;
+
+    @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();
+    }
+
+    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 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());
+                }
+            }
+        }
+    }
+
+    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;
+    }
+
+
+    @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();
+    }
+
+    //根据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 storeUri = resp.getResult().getMediaInfoList(0).getSourceInfo().getStoreUri();
+//                String uri = storeUri.substring(storeUri.indexOf("/") + 1);
+                String url = cloudHostProper.volcengineUrl+"/"+resp.getResult().getMediaInfoList(0).getSourceInfo().getFileName();
+
+                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();
+        }
+    }
+
+    //根据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 {
+                FsVideoResource video = new FsVideoResource();
+                video.setId(videoResource.getId());
+                //视频上传失败,清空jobid
+                if (StringUtils.isEmpty(resp.getResult().getData().getMediaInfoList(0).getVid())){
+                    video.setJobId("");
+                }else {
+                    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);
+        }
+    }
+
+
+    //通过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);
+        }
+    }
+
     private R createWatchIsOpen(FsUserCourseVideoAddKfUParam param) {
         try {
             FsCourseWatchLog log = new FsCourseWatchLog();

+ 126 - 0
fs-service/src/main/java/com/fs/course/service/impl/FsUserVideoServiceImpl.java

@@ -1,10 +1,14 @@
 package com.fs.course.service.impl;
 
+import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
+import java.text.SimpleDateFormat;
 import java.util.*;
+import java.util.concurrent.TimeUnit;
 import java.util.stream.Collectors;
 
+import com.alibaba.fastjson.JSON;
 import com.fs.common.core.domain.R;
 import com.fs.common.utils.DateUtils;
 import com.fs.common.utils.StringUtils;
@@ -20,11 +24,20 @@ import com.fs.course.vo.FsUserVideoPVO;
 import com.fs.course.param.FsUserVideoParam;
 import com.fs.course.vo.FsUserVideoListUVO;
 
+import com.volcengine.helper.VodUploadProgressListener;
+import com.volcengine.service.vod.IVodService;
+import com.volcengine.service.vod.model.request.VodGetMediaInfosRequest;
+import com.volcengine.service.vod.model.request.VodUploadMediaRequest;
+import com.volcengine.service.vod.model.response.VodCommitUploadInfoResponse;
+import com.volcengine.service.vod.model.response.VodGetMediaInfosResponse;
+import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
+import com.volcengine.model.beans.Functions;
 import org.springframework.data.redis.core.RedisTemplate;
 import org.springframework.stereotype.Service;
 import com.fs.course.domain.FsUserVideo;
 import com.fs.course.service.IFsUserVideoService;
+import org.springframework.web.multipart.MultipartFile;
 
 /**
  * 课堂视频Service业务层处理
@@ -32,6 +45,7 @@ import com.fs.course.service.IFsUserVideoService;
  * @author fs
  * @date 2024-05-15
  */
+@Slf4j
 @Service
 public class FsUserVideoServiceImpl implements IFsUserVideoService {
     @Autowired
@@ -46,6 +60,10 @@ public class FsUserVideoServiceImpl implements IFsUserVideoService {
     private IRecommendationService recommendationService;
     @Autowired
     private RedisTemplate redisTemplate;
+
+    @Autowired
+    private IVodService vodService;
+
     private static final String LIKE_KEY_PREFIX = "like:video:";
     private static final String FAVORITE_KEY_PREFIX = "favorite:video:";
 
@@ -338,6 +356,114 @@ public class FsUserVideoServiceImpl implements IFsUserVideoService {
         return oldList;
     }
 
+    //根据vid查询视频信息
+    @Override
+    public String getVideoInfoByVid(String vid) {
+        try {
+            VodGetMediaInfosRequest.Builder reqBuilder = VodGetMediaInfosRequest.newBuilder();
+            reqBuilder.setVids("vid");
+
+            VodGetMediaInfosResponse resp = vodService.getMediaInfos20230701(reqBuilder.build());
+            if (resp.getResponseMetadata().hasError()) {
+                System.out.println(resp.getResponseMetadata().getError());
+                System.exit(-1);
+            }else {
+                return resp.getResult().getMediaInfoList(0).getSourceInfo().getStoreUri();
+            }
+
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return null;
+    }
+
+
+    //本地文件视频上传
+    @Override
+    public String uploadVideo(MultipartFile file, String uploadId) {
+        log.info("上传视频开始",uploadId);
+        File tempFile = null;
+        try {
+            // 将 MultipartFile 转换为临时文件
+            String originalFilename = file.getOriginalFilename();
+            String suffix = originalFilename.substring(originalFilename.lastIndexOf("."));
+            tempFile = File.createTempFile("upload_", suffix);
+            file.transferTo(tempFile);
+
+            // 点播空间名称
+            String space = "rt-huoshan";
+            // 本地临时文件路径(用于上传)
+            String localFilePath = tempFile.getAbsolutePath();
+
+            // 火山云存储路径(文件上传后在火山云的路径)
+            String datePath = new SimpleDateFormat("yyyyMMdd").format(new Date());
+            String fileName = System.currentTimeMillis() + ".mp4";
+            String remoteFileName = "course/" + datePath + "/" + fileName;
+
+            // 上传功能函数,可用于实现截图、设置媒资信息、触发工作流进行转码等功能。详见[上传功能函数说明](https://www.volcengine.com/docs/4/1185644)
+            List<Functions> functionsList = new ArrayList<>();
+            Functions getMetaFunc = Functions.GetMetaFunction();
+            functionsList.add(getMetaFunc);
+            Functions snapShotFunc = Functions.SnapShotFunction(2.3);
+            functionsList.add(snapShotFunc);
+            Functions addOptionInfo = Functions.AddOptionInfoFunction(fileName, "test", "用户上传视频", 0, true);
+            functionsList.add(addOptionInfo);
+
+            // 使用工作流进行转码
+
+            List<String> impTemplateIds = new ArrayList<>();
+//            impTemplateIds.add("a4a7066c968344e9bf7a56ec0891fbb8");
+//            FunctionsWorkflowTemplate impTemplate = new FunctionsWorkflowTemplate(impTemplateIds, "imp");
+//
+//            List<String> transcodeTemplateIds = new ArrayList<>();
+//            transcodeTemplateIds.add("a4a7066c968344e9bf7a56ec0891fbb8");
+//            FunctionsWorkflowTemplate transcodeTemplate = new FunctionsWorkflowTemplate(transcodeTemplateIds,
+//                    "transcode");
+
+//            List<FunctionsWorkflowTemplate> templates = new ArrayList<>();
+//            templates.add(impTemplate);
+//            templates.add(transcodeTemplate);
+
+//            Functions workflowFunc = Functions.StartWorkFlowFunction(templates);
+//            functionsList.add(workflowFunc);
+
+            VodUploadMediaRequest vodUploadMediaRequest = VodUploadMediaRequest.newBuilder()
+                    .setSpaceName(space)
+                    .setFilePath(localFilePath)  // 使用本地临时文件路径
+                    .setFileName(remoteFileName)  // 火山云存储路径
+                    .setFunctions(JSON.toJSONString(functionsList))
+                    .setStorageClass(1)
+                    .build();
+
+            // 需要进度条功能时添加相应 listener,如无需求,传 null 值即可
+            VodUploadProgressListener listener = new VodUploadMediaProcessListener(uploadId, redisTemplate);
+
+            VodCommitUploadInfoResponse vodCommitUploadInfoResponse = vodService.uploadMedia(vodUploadMediaRequest, listener);
+
+            redisTemplate.opsForValue()
+                    .set("vod:upload:" + uploadId, 100, 10, TimeUnit.MINUTES);
+
+            if (vodCommitUploadInfoResponse.getResponseMetadata().hasError()) {
+                throw new RuntimeException("火山云上传失败: " + vodCommitUploadInfoResponse.getResponseMetadata().getError());
+            }
+
+            // 返回视频 VID
+            String vid = vodCommitUploadInfoResponse.getResult().getData().getVid();
+            System.out.println("视频上传成功,VID: " + vid);
+            System.out.println("RequestId: " + vodCommitUploadInfoResponse.getResponseMetadata().getRequestId());
+
+            return vid;
+        } catch (Exception e) {
+            throw new RuntimeException("视频上传失败: " + e.getMessage(), e);
+        } finally {
+            // 删除临时文件
+            if (tempFile != null && tempFile.exists()) {
+                tempFile.delete();
+            }
+        }
+    }
+
+
     public static String updateUrlPrefix(String url) {
         final String oldPrefix = "https://obs.ylrztop.com";
         final String newPrefix = "https://rtobs.ylrztop.com";

+ 47 - 0
fs-service/src/main/java/com/fs/course/service/impl/VodUploadMediaProcessListener.java

@@ -0,0 +1,47 @@
+package com.fs.course.service.impl;
+
+
+import com.volcengine.helper.VodUploadProgressEvent;
+import com.volcengine.helper.VodUploadProgressListener;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.data.redis.core.RedisTemplate;
+
+import java.util.concurrent.TimeUnit;
+
+@Slf4j
+public class VodUploadMediaProcessListener implements VodUploadProgressListener {
+
+    private long bytesUploaded = 0;
+    private long fileSize = -1;
+
+    private final String uploadId;
+    private final RedisTemplate<String, Object> redisTemplate;
+
+    public VodUploadMediaProcessListener(String uploadId,
+                                         RedisTemplate redisTemplate) {
+        this.uploadId = uploadId;
+        this.redisTemplate = redisTemplate;
+    }
+
+    @Override
+    public void progressChanged(VodUploadProgressEvent event) {
+        switch (event.getEventType()) {
+            case FILE_SIZE_EVENT:
+                fileSize = event.getByteSize();
+                break;
+
+            case UPLOAD_BYTES_EVENT:
+                bytesUploaded += event.getByteSize();
+                if (fileSize > 0) {
+                    int percent = Math.min(
+                            (int) (bytesUploaded * 100.0 / fileSize),
+                            100
+                    );
+                    redisTemplate.opsForValue()
+                            .set("vod:upload:" + uploadId, percent, 10, TimeUnit.MINUTES);
+                    log.info("uploadId={} progress={}%", uploadId, percent);
+                }
+                break;
+        }
+    }
+}

+ 3 - 1
fs-service/src/main/java/com/fs/huifuPay/service/test.java

@@ -7,6 +7,8 @@ import com.fs.huifuPay.domain.*;
 import com.fs.huifuPay.sdk.opps.core.request.*;
 import com.fs.huifuPay.sdk.opps.core.utils.DateTools;
 import com.fs.huifuPay.service.impl.HuiFuServiceImpl;
+
+import java.math.BigDecimal;
 import java.util.HashMap;
 import java.util.Map;
 
@@ -27,7 +29,7 @@ public class test {
         confirm.setReqSeqId(orderNo);
         confirm.setOrgReqSeqId("package-1976839880096874496");
         confirm.setOrgReqDate(DateTools.getCurrentDateYYYYMMDD());
-        confirm.setDivAmt("0.01");
+        confirm.setDivAmt(BigDecimal.valueOf(0.01));
         HuifuOrderConfirmResult result = huiFuService.confirmOrder(confirm);
         System.out.println(result);
     }

+ 15 - 0
fs-user-app/src/main/java/com/fs/app/controller/course/CourseQwController.java

@@ -398,6 +398,21 @@ public class CourseQwController extends AppBaseController {
 //        courseVideoService.updateVideoUrl();
     }
 
+    @GetMapping("/testJob")
+    public R testJob() {
+        return courseVideoService.uploadVideoToHuoShanByUrl();
+    }
+
+    @GetMapping("/testVid")
+    public R testVid() {
+        return courseVideoService.getVidByJob();
+    }
+
+    @GetMapping("/testUpdateLineTwo")
+    public R testUpdateLineTwo() {
+        return courseVideoService.getVideoInfoByVid();
+    }
+
     @Login
     @ApiOperation("保存评论数据")
     @PostMapping("/saveMsg")