Java实现二维码和文字合成一张新图片

很久没来写文章了,自己忙又懒。最近实现了一个需求,有点意思,就想起来记录一下。

业务需求:生成一个二维码,旁边还要加点解释说明什么的,(类似)最终效果如下

一.实现思路

1.二维码生成:使用hutool提供的google二维码生成工具

2.背景图(那个纯白色背景)+文字:使用java自带的Graphics绘制工具

3.批量下载:选择使用java自带的ZipOutputStream压缩流工具

4.性能问题:全部图片的操作都在内存上操作,性能不错,但耗内存

 

二.实现步骤(编码)

随便拿个springBoot的工程来写个demo

1.添加用到的依赖(pom.xml):

        <!-- lombok工具类 -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.16.10</version>
        </dependency>
        <!-- google工具类 -->
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>19.0</version>
        </dependency>
        <!-- 生成二维码所需 -->
        <dependency>
            <groupId>com.google.zxing</groupId>
            <artifactId>core</artifactId>
            <version>3.3.3</version>
        </dependency>
        <!-- hutool工具类 -->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>4.5.15</version>
        </dependency>

 

2.编写两个工具类

①.QrCodeUtils.java,图片相关操作以及文件下载工具类(当然,可以再进行分割,这里是demo,就随便封装一下)

package com.cloud.sbjm.common;


import com.cloud.sbjm.onput.vo.DownloadFile;
import com.cloud.sbjm.onput.vo.MessageQrCodeVo;
import com.google.common.io.ByteStreams;
import com.google.common.io.Closer;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import cn.hutool.extra.qrcode.QrCodeUtil;
import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletResponse;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;

/**
 * @author :ouyangzhicheng
 * @date :Created in 2019-9-17 14:54
 * @description:图片工具类
 * @version: 1.0.0
 */
@Component
@Slf4j
public class QrCodeUtils {

    private static final String MESSAGE_NUM = "XX编码";

    private static final String MESSAGE_CATEGORY = "XX类别";

    private static final String MESSAGE_QRCODE = "XX二维码";

    private static final String FONT_TYPE = "宋体";

    private static String MESSAGE_QRCODE_URL;

    private static final String DISPLAYNAME = "XX二维码.png";


    @Value("${message-qrcode-url}")
    public void setMESSAGE_QRCODE_URL(String MESSAGE_QRCODE_URL) {
        this.MESSAGE_QRCODE_URL = MESSAGE_QRCODE_URL;
    }

    /**
     * 生成资产管理二维码图片
     */
    public static InputStream generateAssetsQrCodeImage(MessageQrCodeVo messageQrCodeVo) throws IOException{

        // 获取图片的缓冲区,也就是所谓的画布
        BufferedImage bufferedImage = new BufferedImage(600, 350, BufferedImage.TYPE_INT_RGB);
        //获取画笔,画笔用于在画布上进行绘制
        Graphics paint = bufferedImage.getGraphics();
        //设置画笔的颜色
        paint.setColor(Color.white);
        //绘制画布的背景色
        paint.fillRect(0, 0, 600, 350);

        //生成二维码图片字节流
        byte[]  qrCodeFile = QrCodeUtil.generatePng(MESSAGE_QRCODE_URL+"?id="+messageQrCodeVo.getId(), 180, 180);
        return overlapImage(bufferedImage,qrCodeFile,messageQrCodeVo);
    }

    /**
     * 图片重叠
     * @param bufferedImage
     * @param qrCodeFile
     * @param messageQrCodeVo
     * @return
     */
    public static InputStream overlapImage(BufferedImage bufferedImage, byte[]  qrCodeFile, MessageQrCodeVo messageQrCodeVo) throws IOException {
        ByteArrayOutputStream os = null;
        InputStream qrCodeInputStream =null;
        try {
            BufferedImage qrCode = ImageIO.read(new ByteArrayInputStream(qrCodeFile));

            //在背景图片中添加入需要写入的信息
            Graphics2D g = bufferedImage.createGraphics();
            g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_HRGB);
            g.setColor(Color.black);
            g.setFont(new Font(FONT_TYPE,Font.BOLD,25));
            g.drawString(messageQrCodeVo.getName(),240 ,120);
            g.setColor(Color.gray);
            g.setFont(new Font(FONT_TYPE,Font.BOLD,20));
            g.drawString(MESSAGE_NUM,240 ,160);
            g.drawString(MESSAGE_CATEGORY,240 ,200);
            g.drawString(MESSAGE_QRCODE,70 ,270);
            g.setColor(Color.black);
            g.drawString(messageQrCodeVo.getNumber(),345 ,160);
            g.drawString(messageQrCodeVo.getTypeName(),345 ,200);

            //在背景图片上添加二维码图片
            g.drawImage(qrCode, 30, 65, qrCode.getWidth(), qrCode.getHeight(), null);
            g.dispose();
            os = new ByteArrayOutputStream();
            ImageIO.write(bufferedImage, "png", os);
            qrCodeInputStream = new ByteArrayInputStream(os.toByteArray());
        }catch (Exception e){
            log.warn("生成二维码出错:"+e.getMessage());
        }finally {
            if(os!=null){
                os.close();
            }
            if(qrCodeInputStream!=null){
                qrCodeInputStream.close();
            }
        }
        return qrCodeInputStream;
    }

    /**
     * 资产二维码下载(包含文字信息)
     */
    public static void messageQrCodeDownload(HttpServletResponse response, List<MessageQrCodeVo> messageQrCodeVos) throws IOException {

        InputStream qrCodeInputStream = null;
        OutputStream outputStream = null;
        try (Closer closer = Closer.create()) {
            //单个二维码导出
            if(messageQrCodeVos.size() == 1){
                qrCodeInputStream = generateAssetsQrCodeImage(messageQrCodeVos.get(0));
                closer.register(qrCodeInputStream);
                // String charset = StandardCharsets.UTF_8.displayName();
                String charset = "GB2312";
                response.setCharacterEncoding(charset);
                response.setContentType(MediaType.APPLICATION_OCTET_STREAM.toString());
                String filename = new String(DISPLAYNAME.getBytes(charset), StandardCharsets.ISO_8859_1);
                response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + filename);
                outputStream = closer.register(response.getOutputStream());
                ByteStreams.copy(qrCodeInputStream, outputStream);
                outputStream.flush();
            }

            //批量二维码导出
            if(messageQrCodeVos.size()>1){
                response.setContentType("application/zip");
                response.setHeader("Content-Disposition", "attachment; filename=QrCodes.zip");
                List<DownloadFile> downloadFiles =new ArrayList<>();
                int i = 0;
                for(MessageQrCodeVo messageQrCodeVo:messageQrCodeVos){
                    i++;
                    DownloadFile downloadFile = new DownloadFile();
                    downloadFile.setFileName("XX二维码"+i+".png");
                    downloadFile.setInputStream(generateAssetsQrCodeImage(messageQrCodeVo));
                    downloadFiles.add(downloadFile);
                }
                ZipUtil.toZip3(downloadFiles, response.getOutputStream());
            }

        }catch (Exception e){
            log.warn("二维码下载出错:"+e.getMessage());
        }finally {
            if(qrCodeInputStream!=null){
                qrCodeInputStream.close();
            }
            if(outputStream!=null){
                outputStream.close();
            }
        }
    }

    /**
     * 普通文件下载
     */
    public static void imageDownload(HttpServletResponse response, InputStream inputStream, String fileName) throws IOException {

        OutputStream outputStream = null;
        try (Closer closer = Closer.create()) {
            closer.register(inputStream);
            String charset = "GB2312";
            response.setCharacterEncoding(charset);
            response.setContentType(MediaType.IMAGE_PNG.toString());
            String filename = new String(fileName.getBytes(charset), StandardCharsets.ISO_8859_1);
            response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + filename);
            outputStream = closer.register(response.getOutputStream());
            ByteStreams.copy(inputStream, outputStream);
            outputStream.flush();
        }catch (Exception e){
            log.warn("图片下载出错:"+e.getMessage());
        }finally {
            if(inputStream!=null){
                inputStream.close();
            }
            if(outputStream!=null){
                outputStream.close();
            }
        }
    }
}

②.ZipUtil.java,压缩文件操作工具类

package com.cloud.sbjm.common;


import com.cloud.sbjm.onput.vo.DownloadFile;
import com.google.common.base.Strings;
import com.google.common.io.ByteStreams;
import com.google.common.io.Closer;

import java.io.*;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipOutputStream;

/**
 * @author :ouyangzhicheng
 * @date :Created in 2019-9-24 16:03
 * @description:压缩文件处理
 * @version: 1.0.0
 */
public class ZipUtil {

    /**
     * 解压缩zip包
     *
     * @param zipFilePath        zip文件的全路径
     * @param unzipFilePath      解压后的文件保存的路径
     * @param includeZipFileName 解压后的文件保存的路径是否包含压缩文件的文件名。true-包含;false-不包含,目前只支持true
     * @param charset            The {@linkplain Charset charset} to be
     *                           used to decode the ZIP entry name and comment (ignored if
     *                           the <a href="package-summary.html#lang_encoding"> language
     *                           encoding bit</a> of the ZIP entry's general purpose bit
     *                           flag is set
     */
    public static List<String> unZip(String zipFilePath, String unzipFilePath, boolean includeZipFileName, Charset charset) throws IOException {
        List<String> filePaths = new ArrayList<>(32);
        if (Strings.isNullOrEmpty(zipFilePath)) {
            return filePaths;
        }
        File zipFile = new File(zipFilePath);
        // 如果解压后的文件保存路径包含压缩文件的文件名,则追加该文件名到解压路径
        String fileName = zipFile.getName();
        int pos;
        if (includeZipFileName) {
            pos = fileName.lastIndexOf(".");
            if (pos > 0) {
                fileName = fileName.substring(0, pos);
            }
            unzipFilePath = unzipFilePath + File.separator + fileName;
        }
        // 开始解压
        try (ZipFile zip = new ZipFile(zipFile, charset); Closer closer = Closer.create()) {
            for (Enumeration<? extends ZipEntry> zipEntries = zip.entries(); zipEntries.hasMoreElements(); ) {
                ZipEntry zipEntry = zipEntries.nextElement();
                if (zipEntry.isDirectory()) {
                    continue;
                }
                String zipEntryName = zipEntry.getName();
                String outPath = unzipFilePath + File.separator + zipEntryName;
                File outFile = new File(outPath);
                //目录不存在就创建吧
                com.google.common.io.Files.createParentDirs(outFile);
                BufferedInputStream bufferedIn = closer.register(new BufferedInputStream(zip.getInputStream(zipEntry)));
                BufferedOutputStream bufferedOut = closer.register(new BufferedOutputStream(new FileOutputStream(outFile)));
                ByteStreams.copy(bufferedIn, bufferedOut);
                filePaths.add(outPath.replace("\\", "/"));
            }
        }
        return filePaths;
    }

    /**
     * 解压,返回所有文件的路径
     *
     * @param zipFilePath        压缩包的路径
     * @param unZipFilePath      解压之后的目录
     * @param includeZipFileName 解压之后是否用压缩包名作为父目录
     * @return 返回文件的路径
     * @throws IOException I/O异常
     */
    public static List<String> unZip(String zipFilePath, String unZipFilePath, boolean includeZipFileName) throws IOException {
        Charset defaultCharset = Charset.forName("GBK");
        return unZip(zipFilePath, unZipFilePath, includeZipFileName, defaultCharset);
    }


    private static final int BUFFER_SIZE = 2 * 1024;

    /**
     * 压缩成ZIP 方法1
     *
     * @param srcDir           压缩文件夹路径
     * @param out              压缩文件输出流
     * @param KeepDirStructure 是否保留原来的目录结构,true:保留目录结构;
     *                         false:所有文件跑到压缩包根目录下(注意:不保留目录结构可能会出现同名文件,会压缩失败)
     * @throws RuntimeException 压缩失败会抛出运行时异常
     */
    public static void toZip(String srcDir, OutputStream out, boolean KeepDirStructure) throws RuntimeException {

        long start = System.currentTimeMillis();
        ZipOutputStream zos = null;
        try {
            zos = new ZipOutputStream(out);
            File sourceFile = new File(srcDir);
            compress(sourceFile, zos, sourceFile.getName(), KeepDirStructure);
            long end = System.currentTimeMillis();
            System.out.println("压缩完成,耗时:" + (end - start) + " ms");
        } catch (Exception e) {
            throw new RuntimeException("zip error from ZipUtils", e);
        } finally {
            if (zos != null) {
                try {
                    zos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * 压缩成ZIP 方法2
     *
     * @param srcFiles 需要压缩的文件列表
     * @param out      压缩文件输出流
     * @throws RuntimeException 压缩失败会抛出运行时异常
     */
    public static void toZip(List<File> srcFiles, List<String> fileNames, OutputStream out) throws RuntimeException {
        long start = System.currentTimeMillis();
        ZipOutputStream zos = null;
        int i = 0;
        try {
            zos = new ZipOutputStream(out);
            FileInputStream in=null;
            for (File srcFile : srcFiles) {
                byte[] buf = new byte[BUFFER_SIZE];
                zos.putNextEntry(new ZipEntry(fileNames.get(i)));
                i++;
                int len;
                try{
                    in = new FileInputStream(srcFile);
                    while ((len = in.read(buf)) != -1) {
                        zos.write(buf, 0, len);
                    }
                }
                finally {
                    zos.closeEntry();
                    if(in!=null){
                        in.close();
                    }
                }
            }
            long end = System.currentTimeMillis();
            System.out.println("压缩完成,耗时:" + (end - start) + " ms");
        } catch (Exception e) {
            throw new RuntimeException("zip error from ZipUtils", e);
        } finally {
            if (zos != null) {
                try {
                    zos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * 压缩成ZIP 方法3
     * @param downloadFiles 需要压缩的文件列表
     * @param out      压缩文件输出流
     * @throws RuntimeException 压缩失败会抛出运行时异常
     */
    public static void toZip3(List<DownloadFile> downloadFiles, OutputStream out) throws RuntimeException {
        long start = System.currentTimeMillis();
        ZipOutputStream zos = null;
        try {
            zos = new ZipOutputStream(out);
             InputStream in=null;
            for (DownloadFile downloadFile : downloadFiles) {
                byte[] buf = new byte[BUFFER_SIZE];
                zos.putNextEntry(new ZipEntry(downloadFile.getFileName()));
                int len;
                try{
                    in = downloadFile.getInputStream();
                    while ((len = in.read(buf)) != -1) {
                        zos.write(buf, 0, len);
                    }
                }finally {
                    zos.closeEntry();
                    in.close();
                }
            }
            long end = System.currentTimeMillis();
            System.out.println("压缩完成,耗时:" + (end - start) + " ms");
        } catch (Exception e) {
            throw new RuntimeException("zip error from ZipUtils", e);
        } finally {
            if (zos != null) {
                try {
                    zos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }


    /**
     * 递归压缩方法
     *
     * @param sourceFile       源文件
     * @param zos              zip输出流
     * @param name             压缩后的名称
     * @param KeepDirStructure 是否保留原来的目录结构,true:保留目录结构;
     *                         false:所有文件跑到压缩包根目录下(注意:不保留目录结构可能会出现同名文件,会压缩失败)
     * @throws Exception
     */
    private static void compress(File sourceFile, ZipOutputStream zos, String name, boolean KeepDirStructure) throws Exception {
        byte[] buf = new byte[BUFFER_SIZE];
        FileInputStream in=null;
        try {
            if (sourceFile.isFile()) {
                // 向zip输出流中添加一个zip实体,构造器中name为zip实体的文件的名字
                zos.putNextEntry(new ZipEntry(name));
                // copy文件到zip输出流中
                int len;
                in = new FileInputStream(sourceFile);
                while ((len = in.read(buf)) != -1) {
                    zos.write(buf, 0, len);
                }
                // Complete the entry
                zos.closeEntry();
                in.close();
            } else {
                File[] listFiles = sourceFile.listFiles();
                if (listFiles == null || listFiles.length == 0) {
                    // 需要保留原来的文件结构时,需要对空文件夹进行处理
                    if (KeepDirStructure) {
                        // 空文件夹的处理
                        zos.putNextEntry(new ZipEntry(name + "/"));
                        // 没有文件,不需要文件的copy
                        zos.closeEntry();
                    }

                } else {
                    for (File file : listFiles) {
                        // 判断是否需要保留原来的文件结构
                        if (KeepDirStructure) {
                            // 注意:file.getName()前面需要带上父文件夹的名字加一斜杠,
                            // 不然最后压缩包中就不能保留原来的文件结构,即:所有文件都跑到压缩包根目录下了
                            compress(file, zos, name + "/" + file.getName(), KeepDirStructure);
                        } else {
                            compress(file, zos, file.getName(), KeepDirStructure);
                        }

                    }
                }
            }
        }finally {
            if(in !=null){
                in.close();
            }
        }
    }

}

以上2个工具写好了,就完成了大部分的流程了。

 

3.编写Controller、Service,将业务模拟出来

①FileController.java

package com.cloud.sbjm.boot;

import com.cloud.sbjm.service.FileService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;

import javax.servlet.http.HttpServletResponse;


/**
 * @author :ouyangzhicheng
 * @date :Created in 2019-10-10 9:30
 * @description:文件处理
 * @version: 1.0.0
 */
@Controller
@RequestMapping(value = "/file")
public class FileController {

    @Autowired
    private FileService fileService;

    /**
     * 生成简单二维码(单个)
     * @author: oyzc
     * @date: 2019-09-10
     */
    @GetMapping(value = "/getQrCode")
    public void getQrCode(HttpServletResponse httpResponse, String id){
        fileService.getQrCode(httpResponse,id);
    }


    /**
     * 导出包含其他文字信息二维码(单个/批量)
     * @author: oyzc
     * @date: 2019-09-10
     */
    @GetMapping(value = "/generateMessageQrCode")
    public void generateMessageQrCode(HttpServletResponse httpResponse, String[] ids){
        fileService.generateMessageQrCode(httpResponse,ids);
    }
}

②.FileService.java

package com.cloud.sbjm.service;

import javax.servlet.http.HttpServletResponse; /**
 * @author :ouyangzhicheng
 * @date :Created in 2019-10-10 9:32
 * @description:文件处理实现类
 * @version: 1.0.0
 */
public interface FileService {

    /**
     * 导出包含其他文字信息二维码(单个/批量)
     * @author: oyzc
     * @date: 2019-09-10
     */
    void generateMessageQrCode(HttpServletResponse httpResponse, String[] ids);

    /**
     * 生成简单二维码(单个)
     * @author: oyzc
     * @date: 2019-09-10
     */
    void getQrCode(HttpServletResponse httpResponse, String id);
}

③.FileServiceImpl.java

package com.cloud.sbjm.service.Imp;

import cn.hutool.extra.qrcode.QrCodeUtil;
import com.cloud.sbjm.common.QrCodeUtils;
import com.cloud.sbjm.onput.vo.MessageQrCodeVo;
import com.cloud.sbjm.service.FileService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

/**
 * @author :ouyangzhicheng
 * @date :Created in 2019-10-10 9:33
 * @description:文件处理实现类
 * @version: 1.0.0
 */
@Slf4j
@Service
public class FileServiceImpl implements FileService{
    /**
     * 导出包含其他文字信息二维码(单个/批量)
     * @author: oyzc
     * @date: 2019-09-10
     */

    @Value("${message-qrcode-url}")
    private String MESSAGE_QRCODE_URL;

    @Override
    public void generateMessageQrCode(HttpServletResponse httpResponse, String[] ids) {
        if(ids == null){
            return;
        }
        //测试模拟查询数据库(根据业务需求改动)
        List<MessageQrCodeVo> messageQrCodeVoList = new ArrayList<>();
        for(String id:ids){
            MessageQrCodeVo messageQrCodeVo = new MessageQrCodeVo();
            messageQrCodeVo.setId(id);
            messageQrCodeVo.setName("XXXXX");
            messageQrCodeVo.setNumber("XXXXXXXXXXX");
            messageQrCodeVo.setTypeName("XXXXXXXXXXXXXXX");
            messageQrCodeVoList.add(messageQrCodeVo);
        }
        try {
            QrCodeUtils.messageQrCodeDownload(httpResponse,messageQrCodeVoList);
        } catch (IOException e) {
            log.warn("生成二维码出错:"+e.getMessage());
        }
    }

    /**
     * 生成简单二维码(单个)
     * @author: oyzc
     * @date: 2019-09-10
     */
    @Override
    public void getQrCode(HttpServletResponse httpResponse, String id) {
        //生成二维码图片字节流
        byte[]  qrCodeFile = QrCodeUtil.generatePng(MESSAGE_QRCODE_URL+"?id="+id, 180, 180);
        try {
            QrCodeUtils.imageDownload(httpResponse,new ByteArrayInputStream(qrCodeFile),"qrcode.png");
        } catch (IOException e) {
            log.warn("获取二维码出错:"+e.getMessage());
        }
    }
}

以上基本模拟完一个简单的业务流程,因为是demo,没有涉及dao层,可根据自身业务进行改造

 

4.相关的vo类和配置文件也贴一下

①.DownloadFile.java

package com.cloud.sbjm.onput.vo;

import lombok.Data;

import java.io.InputStream;

/**
 * @author :ouyangzhicheng
 * @date :Created in 2019-9-24 16:14
 * @description:下载文件列表(流文件)
 * @version: 1.0.0
 */
@Data
public class DownloadFile {

    /**
     * 文件名称
     */
    private String fileName;

    /**
     * 文件输入流
     */
    private InputStream inputStream;
}

②.MessageQrCodeVo.java

package com.cloud.sbjm.onput.vo;

import lombok.Data;

/**
 * @author :ouyangzhicheng
 * @date :Created in 2019-9-17 19:18
 * @description:二维码所需参数实体
 * @version: 1.0.0
 */
@Data
public class MessageQrCodeVo {

    /**
     * Id
     */
    private String id;
    /**
     * 名称
     */
    private String name;

    /**
     * 编码
     */
    private String number;

    /**
     * 类别
     */
    private String typeName;
}

③.application.yml配置文件,添加一个参数,二维码的链接

message-qrcode-url = www.baidu.com

自此,把所有涉及的代码都贴出来了,各位有需要可以根据自身业务进行改造



三.测试那些事

为了测试,我过滤掉了所有的校验,直接通过浏览器发送请求来进行测试

demo是根据参数ids所传的个数来决定是单个下载还是批量下载

1.单个下载

图片打开后:



2.批量下载(压缩包)

打开文件后:

自此,功能测试通过。

如有错漏,请大伙指正,谢谢。

  • 7
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值