Java工程师实现视频文件上传minio文件系统存储及网页实现分批加载视频播放

news2025/1/4 15:46:58

Java工程师实现minio存储大型视频文件网页实现分批加载视频播放

在这里插入图片描述

一、需求说明

老板给我出个题目,让我把的电影文件上传到minio文件系统,再通过WEB端分配加载视频播放,类似于我们普通的电影网站。小编把Java代码共享出来。是真正的能拿过来直接用的。小编我亲自测过。

1.1minio版本

<dependency>
		    <groupId>io.minio</groupId>
		    <artifactId>minio</artifactId>
		    <version>7.1.4</version>
		</dependency>

1.2maven依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.sun</groupId>
    <artifactId>springboot-sun</artifactId>
    <version>1.0-SNAPSHOT</version>

    <!--springboot工程需要继承的父工程-->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.8.RELEASE</version>
    </parent>

    <dependencies>
    
    
    
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>
    
    
    
        <!--web开发的起步依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>


        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.0</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <!--<scope>runtime</scope>-->
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
		<dependency>
		    <groupId>io.minio</groupId>
		    <artifactId>minio</artifactId>
		    <version>7.1.4</version>
		</dependency>
		<dependency>
		    <groupId>org.apache.commons</groupId>
		    <artifactId>commons-lang3</artifactId>
		    <version>3.12.0</version>
		</dependency>
	<dependency>
	    <groupId>org.projectlombok</groupId>
	    <artifactId>lombok</artifactId>
	    <version>1.18.28</version>
	    <scope>provided</scope>
	</dependency>
	<dependency>
    <groupId>joda-time</groupId>
    <artifactId>joda-time</artifactId>
    <version>2.10.12</version>
</dependency>
    </dependencies>

</project>

1.3分段视频播放Controller

package com.sun.controller;

import io.minio.GetObjectArgs;
import io.minio.MinioClient;
import io.minio.ObjectStat;
import io.minio.StatObjectArgs;
import io.minio.errors.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RestController;

import com.sun.minio.util.MinIoUtil;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;

@RestController
public class VideoController2 {

 

    @GetMapping("/testDisplay2/{fileName}")
    public ResponseEntity<byte[]> getVideo(
            @PathVariable String fileName,
            @RequestHeader(value = "Range", required = false) String range) {
        try {
        	 String bucketName = "movie";
             String objectName = fileName + ".mp4";
             MinioClient minioClient= MinIoUtil.minioClient;
            // 根据Range请求头来设置分批加载的范围
            GetObjectArgs.Builder getObjectArgsBuilder = GetObjectArgs.builder()
                 .bucket(bucketName)
                 .object(objectName);
            long start=0l;
            if (range!= null && range.startsWith("bytes=")) {
                String[] values = range.split("=")[1].split("-");
                 start = Long.parseLong(values[0]);
                long end = values.length > 1? Long.parseLong(values[1]) : -1;
                if (end!= -1) {
                    long length = end - start + 1;
                    getObjectArgsBuilder.offset(start).length(length);
                } else {
                    getObjectArgsBuilder.offset(start);
                }
            }

            // 获取视频流
            try (InputStream inputStream = minioClient.getObject(getObjectArgsBuilder.build())) {
                ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
                byte[] buffer = new byte[1024 * 1024]; // 1MB缓冲区
                int length;
                while ((length = inputStream.read(buffer))!= -1) {
                    outputStream.write(buffer, 0, length);
                }
                byte[] videoBytes = outputStream.toByteArray();

                HttpHeaders headers = new HttpHeaders();
                headers.setContentType(MediaType.parseMediaType("video/mp4"));

                // 如果是分段请求,设置相应的响应头
                if (range!= null) {
                    ObjectStat objectStat = minioClient.statObject(StatObjectArgs.builder().bucket(bucketName).object(objectName).build());
                    long fileSize = objectStat.length();
                    headers.set("Content-Range", "bytes " + start + "-" + (start + videoBytes.length - 1) + "/" + fileSize);
                    return new ResponseEntity<>(videoBytes, headers, HttpStatus.PARTIAL_CONTENT);
                } else {
                    headers.setContentLength(videoBytes.length);
                    return new ResponseEntity<>(videoBytes, headers, HttpStatus.OK);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
            return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }
}

1.4 html5 播放

<html>
<title>test</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<head>
    <style type="text/css">
        .upload {
            margin-top: 100px;
            margin-left: 100px;
            text-align: center;
        }

    </style>

</head>
<body>
<h1 style="text-align: center;margin-top: 20px">test</h1>
<div>
 <!--  <img src="/minio_demo/testClick/testDisplay?url=/data/temp/img/a6efce1b9d16fdfabf36882ab08f8c5495ee7b9f.jpg"> -->
</div>

<div>

  <!--   
   
 <video src="/minio_demo/testClick/testDisplay?md5=a172e15a869fd7224618840c0815dcb1" controls width="640" height="360">
    您的浏览器不支持视频标签。
</video>
   -->
   
    <video src="/minio_demo/testDisplay2/test" controls width="640" height="360">
    您的浏览器不支持视频标签。
</video>
   
   
</div>
</body>
</html>

1.5上传Controller

 package com.sun.controller;


import io.minio.errors.*;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.support.PropertiesLoaderUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.ModelAndView;

import com.sun.minio.util.MinIoContentType;
import com.sun.minio.util.MinIoUtil;
import com.sun.minio.util.MinioBucketEnum;
import com.sun.service.MinioServiceImpl;

import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Properties;

@RestController
@RequestMapping("testClick")
public class MinioDemoController {
	 @Autowired
	private MinioServiceImpl minioServiceImpl;

    /**
     * 直接页面展示
     * @param response
     */
    @RequestMapping("/testDisplay")
    public void testDisplay(HttpServletResponse response,String md5) throws Exception {
        MinIoUtil.displayFile(minioServiceImpl,MinioBucketEnum.VIDEO_FILES,response,md5);
    }
    @RequestMapping("/testDownload")
    public void testDownLoad(HttpServletResponse response,String md5) throws Exception {
        MinIoUtil.downloadFile(minioServiceImpl,MinioBucketEnum.VIDEO_FILES,response,md5,"");
    }
    @RequestMapping("/testLoad")
    public void testLoadFile(HttpServletResponse response,String md5,String targetPath) throws  Exception{
        MinIoUtil.loadObject(MinioBucketEnum.VIDEO_FILES,targetPath,md5);
    }
    @RequestMapping("/deleteFile")
    public String deleteFile(String md5) throws  Exception{
        MinIoUtil.deleteObject(minioServiceImpl,MinioBucketEnum.VIDEO_FILES, md5);
        return "deleteFileSucceed";
    }
    
    
    @RequestMapping("/testUpload")
    @ResponseBody
    public String testUpload(HttpServletRequest request, @RequestParam("multipartFile") MultipartFile[] multipartfiles,String url,String pucketName)throws  Exception{
        String filePath = "";
        for(MultipartFile multipartFile:multipartfiles){
            String contentType = multipartFile.getContentType();
            InputStream inputStream = multipartFile.getInputStream();
            if(!StringUtils.isBlank(url)&&!url.startsWith("/")){
                url = "/"+url;
            }
            if(!StringUtils.isBlank(url)&&!url.endsWith("/")){
                url += "/";
            }
            MinioBucketEnum minioBucketEnum = MinioBucketEnum.VIDEO_FILES;
			/*
			 * if(pucketName.equals("monthlytext")){ minioBucketEnum =
			 * MinioBucketEnum.MONTHLY_TEXT; } if(pucketName.equals("email")){
			 * minioBucketEnum = MinioBucketEnum.VIDEO_FILES; }
			 * if(pucketName.equals("excel")){ minioBucketEnum = MinioBucketEnum.EXCEL; }
			 */
            
            String md5 = MinIoUtil.upload(minioServiceImpl,minioBucketEnum,url + multipartFile.getOriginalFilename(), inputStream, MinIoContentType.getContentType(contentType));
            filePath+="<p>"+md5+"</p>";
            if(multipartFile.getOriginalFilename().contains(".mp4")) {
                filePath = "<video src=\"/minio_demo/testClick/testDisplay?md5="+md5+"\" controls width=\"640\" height=\"360\">\n" +
                        "    您的浏览器不支持视频标签。\n" +
                        "</video>";
            }
            // 构建包含两个a标签的HTML代码字符串
            StringBuilder htmlBuilder = new StringBuilder();
            htmlBuilder.append("<a href=\"/minio_demo/testClick/deleteFile?md5=").append(md5).append("\">删除文件</a>");
            htmlBuilder.append("&nbsp;&nbsp;&nbsp;&nbsp;"); // 增加一些空格间隔两个标签
            htmlBuilder.append("<a href=\"/minio_demo/testClick/testDownload?md5=").append(md5).append("\">下载文件</a>");
            // 得到最终的HTML字符串
            String htmlString = htmlBuilder.toString();
            filePath=filePath+htmlString;
           // filePath+="<p>"+md5+"</p>";
        }
        return filePath;
    }
    @RequestMapping("/testMv1")
    public ModelAndView testMv1(HttpServletRequest request){
        ModelAndView modelAndView = new ModelAndView();
        modelAndView.setViewName("/minio/demo");
        return modelAndView;
    }
    @RequestMapping("/testMv2")
    public ModelAndView testMv2(HttpServletRequest request){
        ModelAndView modelAndView = new ModelAndView();
        modelAndView.setViewName("/minio/demo2");
        return modelAndView;
    }
    @RequestMapping("/testMv3")
    public ModelAndView testMv3(){
        ModelAndView mv = new ModelAndView();
        mv.setViewName("/minio/demo3");
        return mv;
    }
}

1.6 上传HTML5

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <title>文件上传页面</title>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <style type="text/css">
        /* 整体页面样式 */
        body {
            font-family: Arial, sans-serif;
            margin: 0;
            padding: 0;
            display: flex;
            flex-direction: column;
            justify-content: center;
            align-items: center;
            min-height: 100vh;
            background-color: #f4f4f4;
        }

        /* 标题样式 */
        h1 {
            color: #333;
            margin-bottom: 30px;
        }

        /* 上传表单容器样式 */
      .upload {
            background-color: #fff;
            padding: 30px;
            border-radius: 10px;
            box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
            width: 400px;
        }

        /* 表单段落样式 */
      .upload p {
            margin-bottom: 20px;
        }

        /* 文件选择输入框样式 */
      .upload input[type="file"] {
            width: 100%;
            padding: 10px;
            border: 1px solid #ccc;
            border-radius: 5px;
        }

        /* 文本输入框样式 */
      .upload input[type="text"] {
            width: 100%;
            padding: 10px;
            border: 1px solid #ccc;
            border-radius: 5px;
        }

        /* 提交按钮样式 */
      .upload input[type="submit"] {
            background-color: #007BFF;
            color: #fff;
            padding: 10px 20px;
            border: none;
            border-radius: 5px;
            cursor: pointer;
            transition: background-color 0.3s ease;
        }

      .upload input[type="submit"]:hover {
            background-color: #0056b3;
        }
    </style>
</head>
<body>
    <h1>文件上传至MinIO</h1>
    <div class="upload">
        <form action="/minio_demo/testClick/testUpload" method="post" enctype="multipart/form-data">
            <p>
                选择文件: <input type="file" name="multipartFile" />
            </p>
            <p>
                桶名称: <input type="text" name="pucketName" value="movie" />
            </p>
            <p>
                文件目录: <input type="text" name="url" hidden />
            </p>
            <p>
                <input type="submit" value="上传并检测" />
            </p>
        </form>
    </div>
</body>
</html>

在这里插入图片描述

1.7 MinIoUtil工具类

package com.sun.minio.util;


import io.minio.*;
import io.minio.errors.*;
import org.apache.commons.lang3.StringUtils;
import org.joda.time.DateTime;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.io.support.PropertiesLoaderUtils;
import org.springframework.web.multipart.MultipartFile;

import com.sun.doman.MinioRecord;
import com.sun.service.MinioServiceImpl;

import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import javax.validation.constraints.NotNull;
import java.io.*;
import java.net.URLEncoder;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.TimeUnit;

/**
 * minio工具类
 */

/**
 * minio工具类
 */
public class MinIoUtil {
    private static MinioServiceImpl minioService = null;
    private static final org.slf4j.Logger Logger = LoggerFactory.getLogger(MinIoUtil.class);
    public static MinioClient minioClient = null;
    private static  String enpoint;
    private static  String username;
    private static  String password;
    static {
        try{
            Properties properties = PropertiesLoaderUtils.loadAllProperties("application.properties");
            enpoint = properties.getProperty("minioEnpoint");
            Logger.info("获取minio[enpoint]:"+enpoint);
            username = properties.getProperty("minioUserName");
            Logger.info("获取minio[minioUserName]:"+username);
            password = properties.getProperty("minioPassword");
            Logger.info("获取minio[minioPassword]"+password);
            minioClient = MinioClient.builder().endpoint(enpoint).credentials(username,password).build();
            for(MinioBucketEnum minioBucketEnum: MinioBucketEnum.values()){
                if(!bucketExist(minioBucketEnum.getBucketName())){
                    createBucket(minioBucketEnum.getBucketName());
                }
            }
        }catch (Exception e){
            Logger.error("获取minio连接失败",e);
            e.printStackTrace();
        }

    }
    
    
    
   
  
    
    
    
    //检查桶是否存在
    public static boolean bucketExist(String buckeyName) throws IOException, InvalidKeyException, InvalidResponseException, InsufficientDataException, NoSuchAlgorithmException, ServerException, InternalException, XmlParserException, InvalidBucketNameException, ErrorResponseException {
        return minioClient.bucketExists(BucketExistsArgs.builder().bucket(buckeyName).build());
    }
    //创建桶
    public static  void createBucket(String bucketName) throws IOException, InvalidKeyException, InvalidResponseException, RegionConflictException, InsufficientDataException, NoSuchAlgorithmException, ServerException, InternalException, XmlParserException, InvalidBucketNameException, ErrorResponseException {
        minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
    }



    /**
     *
     * @return
     */
    public Map<String,String> getAccess(){
        Map<String,String> map = new HashMap<>();
        map.put("username",username);
        map.put("password",password);
        return map;
    }
    /**
     *
     * @param name 文件名,可以是单纯的文件名test.pdf,也可以是类似aaa/bbb/ccc/test.pdf,如果没有重复文件名,则会为在minio中的文件路径
     * @param inputStream 文件流
     * @param contentType
     * @return
     */
    public static String upload(MinioServiceImpl minioService ,MinioBucketEnum minioBucketEnum, String name, InputStream inputStream, MinIoContentType contentType)throws Exception{
        if(StringUtils.isBlank(name)||inputStream==null){
            return null;
        }
        try{
            BufferedInputStream bis = new BufferedInputStream(inputStream);
            String md5Val = MD5Util.md5HashCode32(bis);
            MinioRecord byMd5Val = minioService.getByMd5Val(minioBucketEnum.getBucketName(),md5Val);
            if(byMd5Val==null){
                if(objectExist(minioBucketEnum.getBucketName(),name)){//minio存在同名文件,需要改一下文件名
                    name = getOtherName(name,md5Val);
                }
                boolean flag = false;
                int count = 3;
                for(int i = 0;i<count;i++){//失败尝试3次.最后一次还失败抛异常
                    if(flag){
                        break;
                    }
                    try{
                        minioClient.putObject(PutObjectArgs.builder().bucket(minioBucketEnum.getBucketName()).object(name).contentType(contentType == null ? MinIoContentType.STREAM.contentType : contentType.contentType)
                                .stream(bis, bis.available(), partSize).build());
                        flag = true;
                    }catch (Exception e){
                        if (i==count-1){
                            throw e;
                        }
                        TimeUnit.MILLISECONDS.sleep(200);
                    }
                }
                minioService.insertOne(MinioRecord.builder().bucketName(minioBucketEnum.getBucketName()).md5Val(md5Val).minioFilePath(name).remainNum(1)
                        .createTime(DateTime.now().toString("yyyy-MM-dd HH:mm:ss")).updateTime(DateTime.now().toString("yyyy-MM-dd HH:mm:ss")).build());
            }else{
                byMd5Val.setRemainNum((byMd5Val.getRemainNum()==null?0:byMd5Val.getRemainNum())+1);
                minioService.updateRemainNum(byMd5Val);
            }
            return md5Val;
        }catch (Exception e){
            Logger.error("上传文件失败,name:"+name,e);
            throw e;
        }finally {
            try{
                inputStream.close();
            }catch (Exception e){

            }
        }
    }
    
    
    
    
    
    
    public static String upload(MinioBucketEnum minioBucketEnum, String name, InputStream inputStream, MinIoContentType contentType)throws Exception{
        if(StringUtils.isBlank(name)||inputStream==null){
            return null;
        }
        try{
            BufferedInputStream bis = new BufferedInputStream(inputStream);
            String md5Val = MD5Util.md5HashCode32(bis);
            MinioRecord byMd5Val = minioService.getByMd5Val(minioBucketEnum.getBucketName(),md5Val);
            if(byMd5Val==null){
                if(objectExist(minioBucketEnum.getBucketName(),name)){//minio存在同名文件,需要改一下文件名
                    name = getOtherName(name,md5Val);
                }
                boolean flag = false;
                int count = 3;
                for(int i = 0;i<count;i++){//失败尝试3次.最后一次还失败抛异常
                    if(flag){
                        break;
                    }
                    try{
                        minioClient.putObject(PutObjectArgs.builder().bucket(minioBucketEnum.getBucketName()).object(name).contentType(contentType == null ? MinIoContentType.STREAM.contentType : contentType.contentType)
                                .stream(bis, bis.available(), partSize).build());
                        flag = true;
                    }catch (Exception e){
                        if (i==count-1){
                            throw e;
                        }
                        TimeUnit.MILLISECONDS.sleep(200);
                    }
                }
                minioService.insertOne(MinioRecord.builder().bucketName(minioBucketEnum.getBucketName()).md5Val(md5Val).minioFilePath(name).remainNum(1)
                        .createTime(DateTime.now().toString("yyyy-MM-dd HH:mm:ss")).updateTime(DateTime.now().toString("yyyy-MM-dd HH:mm:ss")).build());
            }else{
                byMd5Val.setRemainNum((byMd5Val.getRemainNum()==null?0:byMd5Val.getRemainNum())+1);
                minioService.updateRemainNum(byMd5Val);
            }
            return md5Val;
        }catch (Exception e){
            Logger.error("上传文件失败,name:"+name,e);
            throw e;
        }finally {
            try{
                inputStream.close();
            }catch (Exception e){

            }
        }
    }
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    /**
     *
     * @param name 文件名,可以是单纯的文件名test.pdf,也可以是类似aaa/bbb/ccc/test.pdf,如果没有重复文件名,则会为在minio中的文件路径
     * @param filePath 需要上传的文件路径
     * @param contentType
     * @return
     */
    public static String upload(MinioBucketEnum minioBucketEnum, String name, String filePath, MinIoContentType contentType) throws Exception {
        File file = new File(filePath);
        if(!file.exists()||minioBucketEnum==null){
            return null;
        }
        return upload(minioBucketEnum,name, new FileInputStream(file), contentType);
    }

    /**
     * 基于MultipartFile文件上传
     * @param minioBucketEnum 桶信息
     * @param file 文件
     * @param contentType 内容类型
     * @return 返回结果
     * @throws Exception 异常
     */
    public static String upload(@NotNull MinioBucketEnum minioBucketEnum, @NotNull MultipartFile file, MinIoContentType contentType) throws Exception {
        String filename = file.getOriginalFilename();
        byte[] bytes = file.getBytes();
        ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
        String md5Str = upload(minioBucketEnum, filename, bais, contentType);
        bais.close();
        return md5Str;
    }


    /**
     *

     * @param response
     * @param md5ValOrOldPath 新文件上传则是md5值,旧文件则依然使用旧文件的路径
     * @param exportFileName 最终给到前端的文件名
     */
    public static void downloadFile(MinioServiceImpl minioService ,MinioBucketEnum minioBucketEnum, HttpServletResponse response, String md5ValOrOldPath,String exportFileName){
        downloadFileByMd5(minioService,minioBucketEnum,response,checkOutMd5Val(md5ValOrOldPath),exportFileName);
    }
    /**
     * 根据文件md5值获取
     * @param md5Val
     * @return
     */
    private static void downloadFileByMd5(MinioServiceImpl minioService ,MinioBucketEnum minioBucketEnum, HttpServletResponse response, String md5Val,String exportFileName){
        if(StringUtils.isBlank(md5Val)||response==null){
            return ;
        }
        try {
            MinioRecord byMd5Val = minioService.getByMd5Val(minioBucketEnum.getBucketName(),md5Val);
            if(byMd5Val!=null){
                ObjectStat objectStat = minioClient.statObject(StatObjectArgs.builder().bucket(minioBucketEnum.getBucketName()).object(byMd5Val.getMinioFilePath()).build());
                DownloadUtils.downloadFile(response, minioClient.getObject(GetObjectArgs.builder().bucket(minioBucketEnum.getBucketName()).object(byMd5Val.getMinioFilePath()).build())
                        , objectStat.name(), objectStat.length(), objectStat.contentType(), true, -1,exportFileName);
            }
        } catch (Exception e) {
            Logger.error("下载文件失败",e);
            e.printStackTrace();
        }
    }

    /**
     *
     * @param response
     * @param md5ValOrOldPath 新文件上传则是md5值,旧文件则依然使用旧文件的路径,最终都是通过文件md5找文件
     * @throws Exception
     */
    public static void displayFile(MinioServiceImpl minioService ,MinioBucketEnum minioBucketEnum, HttpServletResponse response, String md5ValOrOldPath) throws Exception {

        displayFileByMd5( minioService ,minioBucketEnum,response,checkOutMd5Val(md5ValOrOldPath));
    }

    /**
     * 根据MD5值展示文件
     * @param response
     * @param md5Val
     */
    private static void displayFileByMd5(MinioServiceImpl minioService ,MinioBucketEnum minioBucketEnum, HttpServletResponse response, String md5Val) throws Exception {
        MinioRecord byMd5Val = minioService.getByMd5Val(minioBucketEnum.getBucketName(),md5Val);
        ObjectStat objectStat = minioClient.statObject(StatObjectArgs.builder().bucket(minioBucketEnum.getBucketName()).object(byMd5Val.getMinioFilePath()).build());
        String objectName = objectStat.name();
        String fileName = objectName.substring(objectName.lastIndexOf("/")+1);
        response.setCharacterEncoding("utf-8");
        response.setHeader("Content-Type",objectStat.contentType());
        response.addHeader("Content-Disposition","inline;filename=" +  URLEncoder.encode(fileName, "UTF-8"));
        response.addHeader("Content-Length","" + objectStat.length());
        try(
                ServletOutputStream outputStream = response.getOutputStream();
                BufferedInputStream bufferedInputStream = new BufferedInputStream(getObjectInputStream(minioBucketEnum.getBucketName(),objectName));
                BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(outputStream);
        ){
            int lenth;
            byte[] bytes = new byte[1024];
            while ((lenth=bufferedInputStream.read(bytes,0,bytes.length))!=-1){
                bufferedOutputStream.write(bytes,0,lenth);
            }
            bufferedOutputStream.flush();

        }catch (Exception e){
            Logger.error("下载失败objectname:"+objectName,e);
        }

    }


    /**
     *
     * @param name
     * @param md5Val
     * @return
     */
    private static String getOtherName(String name,String md5Val){
        try{
            String preFix = name.substring(0,name.lastIndexOf("/")+1);
            if(!name.contains(".")){
                return preFix+md5Val;
            }
            String suffix = name.substring(name.lastIndexOf("."));
            return preFix+md5Val+suffix;
        }catch (Exception e){
            //最起码把md5的值返回作为文件名
            return md5Val;
        }
    }

    /**
     * 文件下载,可以下载到本地某个地址
     * @param targetFilaPath 目标文件
     * @param md5ValOrOldPath 要下载到的目标文件地址,文件如果已经存在就无法下载
     * @throws Exception
     */
    public static void loadObject(MinioBucketEnum minioBucketEnum, String targetFilaPath, String md5ValOrOldPath) throws Exception{

        loadOBbjectByMD5(minioBucketEnum,targetFilaPath,checkOutMd5Val(md5ValOrOldPath));
    }
    /**
     * 文件下载,可以下载到本地某个地址
     * @param targetFilePath 要下载到的目标文件地址,文件如果已经存在就无法下载
     * @throws Exception
     */
    private static void loadOBbjectByMD5(MinioBucketEnum minioBucketEnum, String targetFilePath, String md5Val) throws Exception {
        if(StringUtils.isBlank(targetFilePath)){
            return ;
        }
        MinioRecord byMd5Val = minioService.getByMd5Val(minioBucketEnum.getBucketName(),md5Val);
        if(byMd5Val==null){
            return ;
        }
        File file = new File(targetFilePath);
        if(file.exists()){
            return ;
        }
        file.createNewFile();
        loadOBbjectByMD5(minioBucketEnum,new FileOutputStream(file),md5Val);
    }


    /**
     *
     * @param os
     * @param md5ValOrOldPath 新文件上传则是md5值,旧文件则依然使用旧文件的路径,最终都是通过文件md5找文件
     * @throws Exception
     */
    public static void loadObject(MinioBucketEnum minioBucketEnum, OutputStream os, String md5ValOrOldPath) throws Exception{
        //先判断是不是32位的md5值

        loadOBbjectByMD5(minioBucketEnum,os,checkOutMd5Val(md5ValOrOldPath));
    }

    /**
     * 看下传入参数是否是md5值,为md5值则直接返回..如果是旧文件系统保留的文件路径,需要看下迁移的文件中是否有过该文件,有则返回该文件的实际md5值
     * 虽然可能会查两次数据库,不过问题不大,这表数据并不大
     * @param md5ValOrOldPath
     * @return
     */
    private static String checkOutMd5Val(String md5ValOrOldPath){
        //先判断是不是32位的md5值
        if(StringUtils.isBlank(md5ValOrOldPath)){
            return "";
        }
        if(md5ValOrOldPath.length()!=32||md5ValOrOldPath.contains(".")||md5ValOrOldPath.contains("/")||md5ValOrOldPath.contains("\\")){
            String code = MD5Util.md5HashCode32Str(md5ValOrOldPath);
            MinioRecord byOldPath = minioService.getByOldPathMD5(code);
            if(byOldPath==null){
                return "";
            }
            md5ValOrOldPath = byOldPath.getMd5Val();
        }
        return md5ValOrOldPath;
    }

    /**
     * 文件下载
     * @param os 目标输出流
     * @throws Exception
     */
    private static boolean loadOBbjectByMD5(MinioBucketEnum minioBucketEnum, OutputStream os, String md5Val)throws Exception{
        if(StringUtils.isBlank(md5Val)||os==null){
            return false;
        }
        MinioRecord byMd5Val = minioService.getByMd5Val(minioBucketEnum.getBucketName(),md5Val);
        if(byMd5Val==null){
            return false;
        }
        try(
                BufferedInputStream bufferedInputStream = new BufferedInputStream(getObjectInputStream(minioBucketEnum.getBucketName(),byMd5Val.getMinioFilePath()));
                BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(os);
        ){
            int lenth;
            byte[] bytes = new byte[1024];
            while ((lenth=bufferedInputStream.read(bytes,0,bytes.length))!=-1){
                bufferedOutputStream.write(bytes,0,lenth);
            }
            bufferedOutputStream.flush();
            return true;
        }catch (Exception e){
            Logger.error("下载失败objname:"+byMd5Val.getMinioFilePath(),e);
            throw e;
        }
    }
    /**
     * true存在
     * @param bucketName
     * @param name
     * @return
     */
    private static boolean objectExist(String bucketName,String name)throws Exception{
        try{
            ObjectStat objectStat = minioClient.statObject(StatObjectArgs.builder().bucket(bucketName).object(name).build());
            if(!StringUtils.isBlank(objectStat.name())){
                return true;
            }
            return false;
        }catch (Exception e){
            return false;

        }
    }


    private static final long partSize = 1024*1024*5l;
    /**
     * 覆盖并上传文件
     * @param bucketName
     * @param name
     * @param inputStream
     * @throws Exception
     */


    /**
     * 获取对象流
     * @param bucketName
     * @param objectName
     * @return
     */
    private static InputStream getObjectInputStream(String bucketName,String objectName) throws Exception{
        int count = 3;
        for(int i = 0;i<count;i++){
            try{
                InputStream object = minioClient.getObject(GetObjectArgs.builder().bucket(bucketName).object(objectName).build());
                return object;
            }catch (Exception e2){
                if(i==count-1){
                    throw e2;
                }
                TimeUnit.MILLISECONDS.sleep(200);
            }
        }
        return null;
    }
    /**
     * 获取对象流
     * @param bucketName
     * @param objectName
     * @return
     */
    public static InputStream getObjectInputStreamByMd5Val(String bucketName,String objectName) throws IOException, InvalidKeyException, InvalidResponseException, InsufficientDataException, NoSuchAlgorithmException, ServerException, InternalException, XmlParserException, InvalidBucketNameException, ErrorResponseException {
        MinioRecord byMd5Val = minioService.getByMd5Val(bucketName, objectName);
        InputStream object = null;
        try {
            object = minioClient.getObject(GetObjectArgs.builder().bucket(bucketName).object(byMd5Val.getMinioFilePath()).build());
        } catch (Exception e) {
            e.printStackTrace();
            object = minioClient.getObject(GetObjectArgs.builder().bucket(bucketName).object(byMd5Val.getMinioFilePath()).build());
        }
        return object;
    }

    /**
     * 根据md5值删除
     * 新文件上传则是md5值,旧文件则依然使用旧文件的路径,最终都是通过文件md5找文件
     * @return
     */
    public static void deleteObject(MinioServiceImpl minioService,MinioBucketEnum minioBucketEnum, String md5ValOrOldPath) throws Exception{
        String md5Val = checkOutMd5Val(md5ValOrOldPath);
        MinioRecord byMd5Val = minioService.getByMd5Val(minioBucketEnum.getBucketName(),md5Val);
        if(byMd5Val==null){
            return;
        }
        if(byMd5Val.getRemainNum()!=null&&byMd5Val.getRemainNum()<=1){
            if(objectExist(minioBucketEnum.getBucketName(),byMd5Val.getMinioFilePath())){
                minioClient.removeObject(RemoveObjectArgs.builder().bucket(minioBucketEnum.getBucketName()).object(byMd5Val.getMinioFilePath()).build());
                minioService.deleteRecord(minioBucketEnum.getBucketName(),md5Val);
            }
        }else{
            Integer remainNum = byMd5Val.getRemainNum();
            if(remainNum==null){
                byMd5Val.setRemainNum(1);
            }else{
                byMd5Val.setRemainNum(remainNum-1);
            }
            minioService.updateRemainNum(byMd5Val);
        }
    }
    /**
     * 仅供判断该文件是否存在.
     * @param minioMd5Value
     * @return
     */
    public static boolean isExist(MinioBucketEnum minioBucketEnum, String minioMd5Value) {
        MinioRecord byMd5Val = minioService.getByMd5Val(minioBucketEnum.getBucketName(),minioMd5Value);
        if(byMd5Val==null){
            return false;
        }else{
            return true;
        }
    }

    /**
     * 返回minio数据库表的记录
     * @return
     */
    public static MinioRecord getMinioRecordByMd5Value(MinioBucketEnum minioBucketEnum, String minioMd5Value){
        return minioService.getByMd5Val(minioBucketEnum.getBucketName(),minioMd5Value);
    }
	
}

package com.sun.minio.util;

import org.apache.commons.lang3.StringUtils;


public enum MinIoContentType {
    PDF("application/pdf"),
    STREAM("application/octet-stream"),
    IMAGE("image/jpeg"),
    VEDIO("video/mp4"),
    TEXT("text/html")
    ;
    public String contentType;
    private MinIoContentType(String contentType){
        this.contentType = contentType;
    }
    public static MinIoContentType getContentType(String contentType){
        for(MinIoContentType minIoContentType : MinIoContentType.values()){
            if(minIoContentType.contentType.equals(contentType)){
                return minIoContentType;
            }
        }
        return STREAM;
    }
    public static MinIoContentType getContentTypeByFileName(String fileName){
        if(StringUtils.isBlank(fileName)){
            return STREAM;
        }
        String substring = fileName.substring(fileName.lastIndexOf("."), fileName.length());
        if(StringUtils.isBlank(substring)){
            return STREAM;
        }
        substring = substring.toLowerCase();
        if("pdf".equals(substring)){
            return PDF;
        }
        if("png".equals(substring)||"jpg".equals(substring)||"jpng".equals(substring)||"gif".equals(substring)){
            return IMAGE;
        }
        if("mp4".equals(substring)||"avi".equals(substring)||"mkv".equals(substring)||"mov".equals(substring)||"rmvb".equals(substring)
                ||"FLV".equals(substring)||"rmvb".equals(substring)||"rm".equals(substring)){
            return VEDIO;
        }
        if("txt".equals(substring)){
            return TEXT;
        }
        return STREAM;
    }
}

package com.sun.minio.util;

public enum MinioBucketEnum {
    //EMAIL("email"),
    //EXCEL("excel"),
    //TYPICAL("typical"),
    //FIRE_CONTROL("firecontrol"),
    //MONTHLY_TEXT("monthlytext");
	
	VIDEO_FILES("movie");
	
	
    private String bucketName;
    private MinioBucketEnum(String bucketName){
        this.bucketName = bucketName;
    }
    public String getBucketName(){
        return bucketName;
    }
}

package com.sun.minio.util;

import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.security.MessageDigest;
/**
 * 网上copy的代码,获取文件的32位MD5值
 */
public class MD5Util {
    /**
     * 保证文件的MD5值为32位
     * @param filePath	文件路径
     * @return
     * @throws FileNotFoundException
     */
    public static String md5HashCode32(String filePath) throws Exception{

        BufferedInputStream bfis = new BufferedInputStream(new FileInputStream(filePath));
        return md5HashCode32(bfis);
    }

    /**
     * java计算文件32位md5值
     * @param fis 输入流
     * @return
     */
    public static String md5HashCode32(InputStream fis) {
        try {

            fis.mark(Integer.MAX_VALUE);
            //拿到一个MD5转换器,如果想使用SHA-1或SHA-256,则传入SHA-1,SHA-256
            MessageDigest md = MessageDigest.getInstance("MD5");

            //分多次将一个文件读入,对于大型文件而言,比较推荐这种方式,占用内存比较少。
            byte[] buffer = new byte[1024];
            int length = -1;
            while ((length = fis.read(buffer, 0, 1024)) != -1) {
                md.update(buffer, 0, length);
            }
            fis.reset();
            //转换并返回包含16个元素字节数组,返回数值范围为-128到127
            byte[] md5Bytes  = md.digest();
            StringBuffer hexValue = new StringBuffer();
            for (int i = 0; i < md5Bytes.length; i++) {
                int val = ((int) md5Bytes[i]) & 0xff;//解释参见最下方
                if (val < 16) {
                    /**
                     * 如果小于16,那么val值的16进制形式必然为一位,
                     * 因为十进制0,1...9,10,11,12,13,14,15 对应的 16进制为 0,1...9,a,b,c,d,e,f;
                     * 此处高位补0*/
                    hexValue.append("0");
                }
                //这里借助了Integer类的方法实现16进制的转换
                hexValue.append(Integer.toHexString(val));
            }
            return hexValue.toString();
        } catch (Exception e) {
            e.printStackTrace();
            return "";
        }
    }

    /**
     *字符串经过md5加密成32位
     */
    public static String md5HashCode32Str(String plainText) {
        try {
            MessageDigest md = MessageDigest.getInstance("MD5");
            md.update(plainText.getBytes());
            byte[] md5Bytes  = md.digest();
            StringBuffer hexValue = new StringBuffer();
            for (int i = 0; i < md5Bytes.length; i++) {
                int val = ((int) md5Bytes[i]) & 0xff;//解释参见最下方
                if (val < 16) {
                    /**
                     * 如果小于16,那么val值的16进制形式必然为一位,
                     * 因为十进制0,1...9,10,11,12,13,14,15 对应的 16进制为 0,1...9,a,b,c,d,e,f;
                     * 此处高位补0*/
                    hexValue.append("0");
                }
                //这里借助了Integer类的方法实现16进制的转换
                hexValue.append(Integer.toHexString(val));
            }
            return hexValue.toString();

        } catch (Exception e) {
            e.printStackTrace();
            return "";
        }

    }
}

package com.sun.minio.util;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.StringUtils;

import javax.servlet.http.HttpServletResponse;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URLEncoder;


public class DownloadUtils {

    private static final String JSON_APPLICATION = "application/json";

    private static final Logger log = LoggerFactory.getLogger(DownloadUtils.class.getName());

    public static boolean downloadFile( HttpServletResponse response, InputStream inputStream
            , String objectName, long fileLen, String contentType
            , boolean closeInputStream, long maxAge,String exportFileName) throws Exception {
        String fileName = StringUtils.isEmpty(exportFileName)?objectName.substring(objectName.lastIndexOf("/")+1):exportFileName;
        if (!StringUtils.isEmpty(contentType)) {
            response.setContentType(contentType);
        } else {
            response.setContentType("application/octet-stream");
        }
        response.setCharacterEncoding("utf-8");
        response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(fileName, "UTF-8"));
        response.setHeader("X-Actual-Content-Length", String.valueOf(fileLen));
        cors(response, maxAge);
        OutputStream out = response.getOutputStream();
        byte[] buffer = new byte[1024];
        int len;
        try {
            while ((len = inputStream.read(buffer)) != -1) {
                out.write(buffer, 0, len);
            }
        } catch (Exception e) {
            log.info("download file error.e=" + e);
            return false;
        } finally {
            if (closeInputStream) {
                inputStream.close();
            }
            out.flush();
            out.close();
        }
        return true;
    }
    public static void cors(HttpServletResponse response, long maxAge) {
        response.setHeader("Access-Control-Allow-Origin", "*");
        response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE, HEAD");
        if (maxAge > 0) {
            response.setHeader("Access-Control-Max-Age", String.valueOf(maxAge));
            response.setHeader("Cache-Control", "max-age=" + maxAge);
        }
        response.setHeader("Access-Control-Allow-Headers", "*");
        response.setHeader("Access-Control-Expose-Headers", "Cache-Control, Accept-Ranges, Content-Encoding, Content-Length, Content-Range, X-Actual-Content-Length, Content-Disposition");
    }
}

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2269341.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Three.js教程004:坐标辅助器与轨道控制器

文章目录 坐标辅助器与轨道控制器实现效果添加坐标辅助器添加轨道控制器完整代码完整代码下载坐标辅助器与轨道控制器 实现效果 添加坐标辅助器 创建坐标辅助器: const axesHelper = new Three.AxesHelper(5);添加到场景中: scene.

【优选算法 分治】深入理解分治算法:分治算法入门小专题详解

快速排序算法 (1) 快速排序法 (2) 快排前后指针 (3) 快排挖坑法 颜色分类 题目解析 算法原理 算法原理和移动零非常相似 简述移动零的算法原理 cur 在从前往后扫描的过程中&#xff0c;如果扫描的数符合 f 性质&#xff0c;就把这个数放到 dest 之…

Qt5 中 QGroupBox 标题下沉问题解决

我们设置了QGroupBox 样式之后,发现标题下沉了,那么如何解决呢? QGroupBox {font: 12pt "微软雅黑";color:white;border:1px solid white;border-radius:6px; } 解决后的效果 下面是解决方法: QGroupBox {font: 12pt "微软雅黑";color:white;bo…

sentinel-请求限流、线程隔离、本地回调、熔断

请求限流&#xff1a;控制QPS来达到限流的目的 线程隔离&#xff1a;控制线程数量来达到限流的目录 本地回调&#xff1a;当线程被限流、隔离、熔断之后、就不会发起远程调用、而是使用本地已经准备好的回调去提醒用户 服务熔断&#xff1a;熔断也叫断路器&#xff0c;当失败、…

体验Cursor一段时间后的感受和技巧

用这种LLM辅助的IDE一段时间了&#xff0c;断断续续做了几个小项目了&#xff0c;总结一下整体的感受和自己的一些使用经验。 从Cursor开始又回到Cursor 第一个真正开始使用的LLM的辅助开发IDE就是Cursor&#xff0c;Github的Copilot支持尝试过&#xff0c;但是并没有真正的在…

【数据仓库】hadoop3.3.6 安装配置

文章目录 概述下载解压安装伪分布式模式配置hdfs配置hadoop-env.shssh免密登录模式设置初始化HDFS启动hdfs配置yarn启动yarn 概述 该文档是基于hadoop3.2.2版本升级到hadoop3.3.6版本&#xff0c;所以有些配置&#xff0c;是可以不用做的&#xff0c;下面仅记录新增操作&#…

宽带、光猫、路由器、WiFi、光纤之间的关系

1、宽带&#xff08;Broadband&#xff09; 1.1 宽带的定义宽带指的是一种高速互联网接入技术&#xff0c;通常包括ADSL、光纤、4G/5G等不同类型的接入方式。宽带的关键特点是能够提供较高的数据传输速率&#xff0c;使得用户可以享受到稳定的上网体验。 1.2 宽带的作用宽带是…

[2025] 如何在 Windows 计算机上轻松越狱 IOS 设备

笔记 1. 首次启动越狱工具时&#xff0c;会提示您安装驱动程序。单击“是”确认安装&#xff0c;然后再次运行越狱工具。 2. 对于Apple 6s-7P和iPad系列&#xff08;iOS14.4及以上&#xff09;&#xff0c;您应该点击“Optinos”并勾选“允许未经测试的iOS/iPadOS/tvOS版本”&…

Linux SVN下载安装配置客户端

参考&#xff1a; linux下svn服务器搭建及使用&#xff08;包含图解&#xff09;_小乌龟svn新建用户名和密码-CSDN博客 1.ubuntu安装svn客户端 “subversion” sudo apt-get update sudo apt-get install subversion 查看安装的版本信息&#xff0c;同时看是否安装成功 s…

【Windows】Windows系统查看目录中子目录占用空间大小

在对应目录下通过powershell命令查看文件夹及文件大小&#xff0c;不需要管理员权限。 以下为方式汇总&#xff1a; 方式1&#xff08;推荐&#xff0c;免费下载使用&#xff0c;界面友好&#xff09;&#xff1a; 使用工具以下是一些第三方工具treesize_free https://www.ja…

【论文阅读笔记】IceNet算法与代码 | 低照度图像增强 | IEEE | 2021.12.25

目录 1 导言 2 相关工作 A 传统方法 B 基于CNN的方法 C 交互方式 3 算法 A 交互对比度增强 1)Gamma estimation 2)颜色恢复 3)个性化初始η B 损失函数 1)交互式亮度控制损失 2)熵损失 3)平滑损失 4)总损失 C 实现细节 4 实验 5 IceNet环境配置和运行 1 下载…

L25.【LeetCode笔记】 三步问题的四种解法(含矩阵精彩解法!)

目录 1.题目 2.三种常规解法 方法1:递归做 ​编辑 方法2:改用循环做 初写的代码 提交结果 分析 修改后的代码 提交结果 for循环的其他写法 提交结果 方法3:循环数组 提交结果 3.方法4:矩阵 算法 代码实践 1.先计算矩阵n次方 2.后将矩阵n次方嵌入递推式中 提…

小白投资理财 - 看懂 PE Ratio 市盈率

小白投资理财 - 看懂 PE Ratio 市盈率 什么是 PE RatioPE 缺陷PE 优点总结 无论是在菜市还是股票市场&#xff0c;每个人都想捡便宜&#xff0c;而买股票就像市场买菜&#xff0c;必须货比三家&#xff0c;投资股票最重要就是要知道回本时间要多久&#xff0c;市场上很多时候散…

python利用selenium实现大麦网抢票

大麦网&#xff08;damai.cn&#xff09;是中国领先的现场娱乐票务平台&#xff0c;涵盖演唱会、音乐会、话剧、歌剧、体育赛事等多种门票销售。由于其平台上经常会有热门演出&#xff0c;抢票成为许多用户关注的焦点。然而&#xff0c;由于票务资源的有限性&#xff0c;以及大…

SQL-leetcode-183. 从不订购的客户

183. 从不订购的客户 Customers 表&#xff1a; -------------------- | Column Name | Type | -------------------- | id | int | | name | varchar | -------------------- 在 SQL 中&#xff0c;id 是该表的主键。 该表的每一行都表示客户的 ID 和名称。 Orders 表&#…

充电桩语音提示芯片方案-支持平台自定义语音NV128H让充电更便捷

随着工业化与城市化进程的加速推进&#xff0c;传统燃油汽车的数量急剧攀升&#xff0c;这直接导致了石油资源的过度开采与消耗。石油&#xff0c;作为不可再生的化石燃料&#xff0c;其储量日益枯竭&#xff0c;价格波动频繁&#xff0c;给全球能源安全带来了前所未有的挑战。…

Android 旋转盘导航栏

1.直接上源码&#xff1a; package com.you.arc;import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Point; import android.graphics.RectF; import android.support…

手搓一个ChatUI需要分几步

只关注项目代码的同学可以直接跳转目录中的正文部分&#xff0c;查看项目仓库和功能介绍。 引言 Chatbot的UI界面设计&#xff0c;是和传统软件不同的&#xff1a;都是当面一个简洁的对话框&#xff0c;框里预备着热乎的工具&#xff0c;可以随时更新。 像我这样做工的牛马&a…

低代码开发深度剖析:JNPF 如何引领变革

在当今数字化转型加速的时代&#xff0c;低代码开发已成为众多企业提升效率、降低成本的关键利器。它打破了传统开发模式的高门槛和冗长流程&#xff0c;让应用开发变得更加高效、灵活与普惠。 低代码开发的核心优势 低代码开发平台通过可视化的操作界面&#xff0c;减少了对…

uniapp实现APP、小程序与webview页面间通讯

需求&#xff1a; 1、需要在Uniapp开发的APP或小程序页面嵌入一个H5网页&#xff0c;需要拿到H5给APP传递的数据。 2、并且这个H5是使用vuevant开发的。&#xff08;其实跟使用uniapp开发H5一样&#xff09; 实现步骤&#xff1a; 1、首先需要兼容多端和App端&#xff0c;因…