java生成二维码

java使用hutool工具生成二维码

工具类代码较长,放在文章末尾提供

一、 先引入需要使用到的依赖

maven引入

 <!-- hutool -->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.8.25</version>
        </dependency>
                <!-- ZXing 核心 -->
        <dependency>
            <groupId>com.google.zxing</groupId>
            <artifactId>core</artifactId>
            <version>3.5.3</version>
        </dependency>

        <!-- ZXing Java SE 扩展(用于 BufferedImage 转换) -->
        <dependency>
            <groupId>com.google.zxing</groupId>
            <artifactId>javase</artifactId>
            <version>3.5.3</version>
        </dependency>

gradle引入

implementation 'cn.hutool:hutool-all:5.8.25'
implementation 'com.google.zxing:core:3.4.1'
implementation 'com.google.zxing:javase:3.4.1'

二、代码使用示例

工具类中提供直接生成图像、或者在指定路径生成对应的二维码,考虑到对于二维码的安全性,提供安全生成和没有限制的生成方式,根据自身业务选择即可

此处只做安全生成二维码的示例,直接生成二维码传入需要参数即可

生成单个二维码

使用CustomQrCodeGenerator.generateSecureQrImage方法生成,传入给定的类型、id,这两个参数会进行加密存储,传入-1则代表当前二维码永不过期,输入正整数作为秒数计算二维过期时间。

/**
 * 生成二维码 不限制过期时间
 * @param type 设备类型
 * @param id 设备id
 * @param response 响应
 * @throws Exception 异常
 */
@GetMapping("/generateQrSecure")
public void generateQr(
        @RequestParam String type,
        @RequestParam Long id,
        HttpServletResponse response) throws Exception {

    // 安全生成二维码图像
    BufferedImage bufferedImage = CustomQrCodeGenerator.generateSecureQrImage(type, id, -1);
    response.setContentType("image/png");
    ImageIO.write(bufferedImage, "png", response.getOutputStream());
}

生成指定样式二维码

BufferedImage传入二维码logo格式

当前方法通过BufferedImage传入二维码logo格式,CustomQrCodeGenerator.generateSecureQrImage(type, id, -1, null, null, logoImage);在设置null处是指定二维码的颜色,传null会默认处理样式,传入指定的颜色,会定制生成

InputStream logoInputStream = getClass().getClassLoader()
.getResourceAsStream(“static/logo/apple-touch-icon.png”);是需要在你的项目的resources下有对应的logo图片,否则无法获取生成对应的二维码含logo样式

/**
 * 生成指定logo的二维码 BufferedImage指定logo类型
 * @param type 设备类型
 * @param id 设备id
 * @param response 响应
 * @throws Exception 异常
 */
@GetMapping("/generateQrSecureWithLogo")
public void generateQrSecureWithLogo(
        @RequestParam String type,
        @RequestParam Long id,
        HttpServletResponse response) throws Exception {

    // 从 classpath 加载 logo
    InputStream logoInputStream = getClass().getClassLoader()
            .getResourceAsStream("static/logo/apple-touch-icon.png");

    if (logoInputStream == null) {
        throw new FileNotFoundException("Logo 文件未找到: logo/apple-touch-icon.png");
    }
    BufferedImage logoImage = ImageIO.read(logoInputStream);

    // 安全生成二维码图像
    BufferedImage bufferedImage = CustomQrCodeGenerator.generateSecureQrImage(type, id, -1, null, null, logoImage);
    response.setContentType("image/png");
    ImageIO.write(bufferedImage, "png", response.getOutputStream());
}

File传入二维码logo格式

当前使用File格式进行二维码的生成,要确保指定路径的图片存在,在生产环境可配置在配置文件中获取,但建议可以放在resorces下获取

/**
     * 生成指定logo的二维码 File指定logo类型
     * @param type 设备类型
     * @param id 设备id
     * @param response 响应
     * @throws Exception 异常
     */
    @GetMapping("/generateQrSecureFileWithLogo")
    public void generateQrSecureFileWithLogo(
            @RequestParam String type,
            @RequestParam Long id,
            HttpServletResponse response) throws Exception {

        // 指定 Logo 文件路径(必须是真实存在的文件)
        File logoFile = new File("D:/qrCode/apple-touch-icon.png");

        // 验证 Logo 文件是否存在
        if (!logoFile.exists()) {
            throw new RuntimeException("Logo 文件不存在: " + logoFile.getAbsolutePath());
        }

        // 安全生成二维码图像
        BufferedImage bufferedImage = CustomQrCodeGenerator.generateSecureQrImage(type, id, -1, null, null, logoFile);
        response.setContentType("image/png");
        ImageIO.write(bufferedImage, "png", response.getOutputStream());
    }

批量生成二维码

idList.parallelStream()使用并行流处理数据,保证速度,如果数据过多建议分批处理,路径可以在配置文件配置再进行获取,此处方便展示,可根据实际需求更改

CustomQrCodeGenerator.saveSecureQrToFile当前方法,会将二维码生成为文件的形式,保存在指定的路径,如果有定制二维码的需求,可以调用对应的方法,原理同上述一致

/**
     * 批量生成二维码
     * @throws Exception 异常
     */
    @GetMapping("/generateBatchQr")
    public void generateBatchQr() throws Exception {
        String basePath = "D:/qrCode/";
        File baseDir = new File(basePath);
        if (!baseDir.exists()) {
            boolean created = baseDir.mkdirs();
            if (!created) {
                throw new RuntimeException("创建目录失败: " + basePath);
            }
        }

        List<Device> list = deviceService.list();
        List<Long> idList = list.stream().map(Device::getId).toList();

        AtomicInteger count = new AtomicInteger(0);
        idList.parallelStream().forEach(id -> {
            try {
                CustomQrCodeGenerator.saveSecureQrToFile("device", id, -1, basePath);
                count.incrementAndGet();
            }catch (Exception e){
                e.printStackTrace();
            }
        });

        System.out.println("共计设备:" + idList.size() + "生成二维码数量:" + count.get());
    }

加密二维码解析

CustomQrCodeGenerator.verifyAndDecrypt方法是用来解析你在扫描二维码之后,获取到的加密数据,将加密的数据传入进来,方法会解析为type=xxx&id=123这样的数据,就是你生成二维码时候指定的数据,parseQueryString是方便将数据解析,以便于拿到对应的类型、id,可以在后续你需要获取某个id数据的详细信息使用,同时可通过使用规定的type类型调用不同接口获取不同数据,如果还需要别的参数可以自行在工具类添加。

    /**
     * 测试扫描二维码获得加密后的数据 进行解析
     * @param payload 加密后的数
     */
    @GetMapping("/scan")
    public CommonResult<Object> scan(@RequestParam String payload) {
        String decrypt = CustomQrCodeGenerator.verifyAndDecrypt(payload);
        Map<String, String> queryMap = parseQueryString(decrypt);
        String s = queryMap.get("type");
        System.out.println("type:" + s);
        String s1 = queryMap.get("id");
        System.out.println("id:" + s1);
        return CommonResult.ok(s1);
    }
    // 辅助方法:将 "type=xxx&id=123" 转为 Map
    private static Map<String, String> parseQueryString(String query) {
        Map<String, String> map = new HashMap<>();
        if (query == null || query.isEmpty()) {
            return map;
        }

        for (String pair : query.split("&")) {
            String[] kv = pair.split("=", 2);
            if (kv.length == 2) {
                map.put(kv[0], kv[1]);
            } else if (kv.length == 1) {
                map.put(kv[0], "");
            }
        }
        return map;
    }

三、工具类代码

当前工具类在测试使用hutool的5.4.0、5.8.25均可使用

package com.basic.localcode.common.util;

import cn.hutool.core.codec.Base64;
import cn.hutool.core.util.HexUtil;
import cn.hutool.crypto.SecureUtil;
import cn.hutool.crypto.symmetric.AES;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.time.Instant;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.EncodeHintType;
import com.google.zxing.MultiFormatWriter;
import com.google.zxing.client.j2se.MatrixToImageConfig;
import com.google.zxing.client.j2se.MatrixToImageWriter;
import com.google.zxing.common.BitMatrix;

/**
 * 自定义二维码生成器
 * 功能:
 * - 普通二维码:支持 Logo、前景/背景色(可选)
 * - 安全二维码:加密 + 签名 + 过期时间(支持 -1 表示永不过期)
 *
 * 使用说明:
 * - 调用简洁方法(如 saveSecureQrToFile(type, id, expire, path))将使用默认样式
 * - 如需自定义颜色或 Logo,请使用完整参数版本
 * - Logo 支持 File 或 BufferedImage(推荐从 classpath 加载为 BufferedImage)
 */
public class CustomQrCodeGenerator {

    private static final int DEFAULT_WIDTH = 300;
    private static final int DEFAULT_HEIGHT = 300;
    private static final int DEFAULT_MARGIN = 1;

    // 安全密钥(生产环境请外部化到配置文件或环境变量)
    // 必须 16 字节(AES-128)
    private static final String SECRET_KEY = "MySecureKey12345";
    private static final String HMAC_SECRET = "HmacSecretForQR!@#";

    // ==================== 【1】基础普通二维码(默认样式)====================

    public static BufferedImage generateQrImage(String type, Long id) {
        return generateQrImage(type, id, null, null, (File) null);
    }

    public static void saveQrToFile(String type, Long id, String basePath) {
        saveQrToFile(type, id, basePath, null, null, (File) null);
    }

    // ==================== 【2】普通二维码(带样式,File logo)====================

    public static BufferedImage generateQrImage(
            String type,
            Long id,
            Color foreColor,
            Color backColor,
            File logoFile) {

        String content = "type=" + type + "&id=" + id;
        return createQrCode(content, foreColor, backColor, logoFile);
    }

    public static void saveQrToFile(
            String type,
            Long id,
            String basePath,
            Color foreColor,
            Color backColor,
            File logoFile) {

        BufferedImage image = generateQrImage(type, id, foreColor, backColor, logoFile);
        String fileName = String.format("%s_%s.png", type, id);
        File file = new File(basePath, fileName);
        file.getParentFile().mkdirs();
        try {
            ImageIO.write(image, "png", file);
        } catch (IOException e) {
            throw new RuntimeException("生成二维码失败", e);
        }
    }

    // ==================== 【2b】普通二维码(带样式,BufferedImage logo)====================

    public static BufferedImage generateQrImage(
            String type,
            Long id,
            Color foreColor,
            Color backColor,
            BufferedImage logoImage) {

        String content = "type=" + type + "&id=" + id;
        return createQrCode(content, foreColor, backColor, logoImage);
    }

    public static void saveQrToFile(
            String type,
            Long id,
            String basePath,
            Color foreColor,
            Color backColor,
            BufferedImage logoImage) {

        BufferedImage image = generateQrImage(type, id, foreColor, backColor, logoImage);
        String fileName = String.format("%s_%s.png", type, id);
        File file = new File(basePath, fileName);
        file.getParentFile().mkdirs();
        try {
            ImageIO.write(image, "png", file);
        } catch (IOException e) {
            throw new RuntimeException("生成二维码失败", e);
        }
    }

    // ==================== 【3】安全二维码(简洁重载,默认样式)====================

    public static BufferedImage generateSecureQrImage(String type, Long id, long expireInSeconds) {
        return generateSecureQrImage(type, id, expireInSeconds, null, null, (File) null);
    }

    public static void saveSecureQrToFile(String type, Long id, long expireInSeconds, String basePath) {
        saveSecureQrToFile(type, id, expireInSeconds, basePath, null, null, (File) null);
    }

    // ==================== 【3a】安全二维码(完整参数,File logo)====================

    public static BufferedImage generateSecureQrImage(
            String type,
            Long id,
            long expireInSeconds,
            Color foreColor,
            Color backColor,
            File logoFile) {

        String raw = buildRawContent(type, id, expireInSeconds);
        String encrypted = encryptContent(raw);
        String signed = signContent(encrypted);
        return createQrCode(signed, foreColor, backColor, logoFile);
    }

    public static void saveSecureQrToFile(
            String type,
            Long id,
            long expireInSeconds,
            String basePath,
            Color foreColor,
            Color backColor,
            File logoFile) {

        BufferedImage image = generateSecureQrImage(type, id, expireInSeconds, foreColor, backColor, logoFile);
        String fileName = String.format("%s_%s_secure.png", type, id);
        File file = new File(basePath, fileName);
        file.getParentFile().mkdirs();
        try {
            ImageIO.write(image, "png", file);
        } catch (IOException e) {
            throw new RuntimeException("生成安全二维码失败", e);
        }
    }

    // ==================== 【3b】安全二维码(完整参数,BufferedImage logo)✅ 新增 ====================

    public static BufferedImage generateSecureQrImage(
            String type,
            Long id,
            long expireInSeconds,
            Color foreColor,
            Color backColor,
            BufferedImage logoImage) {

        String raw = buildRawContent(type, id, expireInSeconds);
        String encrypted = encryptContent(raw);
        String signed = signContent(encrypted);
        return createQrCode(signed, foreColor, backColor, logoImage);
    }

    public static void saveSecureQrToFile(
            String type,
            Long id,
            long expireInSeconds,
            String basePath,
            Color foreColor,
            Color backColor,
            BufferedImage logoImage) {

        BufferedImage image = generateSecureQrImage(type, id, expireInSeconds, foreColor, backColor, logoImage);
        String fileName = String.format("%s_%s_secure.png", type, id);
        File file = new File(basePath, fileName);
        file.getParentFile().mkdirs();
        try {
            ImageIO.write(image, "png", file);
        } catch (IOException e) {
            throw new RuntimeException("生成安全二维码失败", e);
        }
    }

    // ==================== 【4】核心:使用 ZXing 生成二维码(File logo)====================

    private static BufferedImage createQrCode(
            String content,
            Color foreColor,
            Color backColor,
            File logoFile) {

        try {
            java.util.Map<EncodeHintType, Object> hints = new java.util.HashMap<>();
            hints.put(EncodeHintType.CHARACTER_SET, "UTF-8");
            hints.put(EncodeHintType.MARGIN, DEFAULT_MARGIN);

            BitMatrix bitMatrix = new MultiFormatWriter().encode(
                    content,
                    BarcodeFormat.QR_CODE,
                    DEFAULT_WIDTH,
                    DEFAULT_HEIGHT,
                    hints
            );

            int fore = foreColor != null ? foreColor.getRGB() : 0xFF000000;
            int back = backColor != null ? backColor.getRGB() : 0xFFFFFFFF;
            MatrixToImageConfig config = new MatrixToImageConfig(fore, back);

            BufferedImage qrImage = MatrixToImageWriter.toBufferedImage(bitMatrix, config);

            if (logoFile != null && logoFile.exists()) {
                BufferedImage logo = ImageIO.read(logoFile);
                qrImage = addLogo(qrImage, logo);
            }

            return qrImage;
        } catch (Exception e) {
            throw new RuntimeException("生成二维码失败", e);
        }
    }

    // ==================== 【4b】核心:使用 ZXing 生成二维码(BufferedImage logo)✅ 新增 ====================

    private static BufferedImage createQrCode(
            String content,
            Color foreColor,
            Color backColor,
            BufferedImage logoImage) {

        try {
            java.util.Map<EncodeHintType, Object> hints = new java.util.HashMap<>();
            hints.put(EncodeHintType.CHARACTER_SET, "UTF-8");
            hints.put(EncodeHintType.MARGIN, DEFAULT_MARGIN);

            BitMatrix bitMatrix = new MultiFormatWriter().encode(
                    content,
                    BarcodeFormat.QR_CODE,
                    DEFAULT_WIDTH,
                    DEFAULT_HEIGHT,
                    hints
            );

            int fore = foreColor != null ? foreColor.getRGB() : 0xFF000000;
            int back = backColor != null ? backColor.getRGB() : 0xFFFFFFFF;
            MatrixToImageConfig config = new MatrixToImageConfig(fore, back);

            BufferedImage qrImage = MatrixToImageWriter.toBufferedImage(bitMatrix, config);

            if (logoImage != null) {
                qrImage = addLogo(qrImage, logoImage);
            }

            return qrImage;
        } catch (Exception e) {
            throw new RuntimeException("生成二维码失败", e);
        }
    }

    // ==================== 【5】Logo 处理逻辑 ====================

    /**
     * 将 BufferedImage 类型的 logo 添加到二维码中心
     */
    private static BufferedImage addLogo(BufferedImage qrImage, BufferedImage logoImage) {
        int qrWidth = qrImage.getWidth();
        int qrHeight = qrImage.getHeight();
        int logoWidth = logoImage.getWidth();
        int logoHeight = logoImage.getHeight();

        int maxSize = Math.min(qrWidth / 5, qrHeight / 5);
        if (logoWidth > maxSize || logoHeight > maxSize) {
            double scale = (double) maxSize / Math.max(logoWidth, logoHeight);
            logoWidth = (int) (logoWidth * scale);
            logoHeight = (int) (logoHeight * scale);
            Image scaledLogo = logoImage.getScaledInstance(logoWidth, logoHeight, Image.SCALE_SMOOTH);
            BufferedImage scaledBufferedLogo = new BufferedImage(logoWidth, logoHeight, BufferedImage.TYPE_INT_ARGB);
            Graphics2D g = scaledBufferedLogo.createGraphics();
            g.drawImage(scaledLogo, 0, 0, null);
            g.dispose();
            logoImage = scaledBufferedLogo;
        }

        int x = (qrWidth - logoWidth) / 2;
        int y = (qrHeight - logoHeight) / 2;

        Graphics2D g = qrImage.createGraphics();
        g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1f));
        g.drawImage(logoImage, x, y, null);
        g.dispose();

        return qrImage;
    }

    // ==================== 【6】安全逻辑 ====================

    private static String buildRawContent(String type, Long id, long expireInSeconds) {
        StringBuilder sb = new StringBuilder();
        sb.append("type=").append(type)
                .append("&id=").append(id);

        if (expireInSeconds != -1) {
            long expireAt = Instant.now().plus(Duration.ofSeconds(expireInSeconds)).toEpochMilli();
            sb.append("&expire=").append(expireAt);
        } else {
            sb.append("&expire=-1");
        }

        return sb.toString();
    }

    private static String encryptContent(String content) {
        AES aes = SecureUtil.aes(SECRET_KEY.getBytes(StandardCharsets.UTF_8));
        return Base64.encode(aes.encrypt(content));
    }

    private static String signContent(String encryptedContent) {
        byte[] hmac = SecureUtil.hmacSha256(HMAC_SECRET).digest(encryptedContent);
        return encryptedContent + "." + HexUtil.encodeHexStr(hmac);
    }

    // ==================== 【7】验证与解密 ====================

    public static String verifyAndDecrypt(String qrPayload) {
        String[] parts = qrPayload.split("\\.", 2);
        if (parts.length != 2) {
            throw new IllegalArgumentException("二维码格式无效");
        }

        String encrypted = parts[0];
        String sig = parts[1];

        String expectedSig = HexUtil.encodeHexStr(SecureUtil.hmacSha256(HMAC_SECRET).digest(encrypted));
        if (!expectedSig.equals(sig)) {
            throw new SecurityException("签名不匹配!");
        }

        AES aes = SecureUtil.aes(SECRET_KEY.getBytes(StandardCharsets.UTF_8));
        String raw = new String(aes.decrypt(Base64.decode(encrypted)), StandardCharsets.UTF_8);

        for (String param : raw.split("&")) {
            if (param.startsWith("expire=")) {
                String expireStr = param.substring(7);
                if ("-1".equals(expireStr)) {
                    return raw;
                }
                try {
                    long expireTime = Long.parseLong(expireStr);
                    if (System.currentTimeMillis() > expireTime) {
                        throw new IllegalStateException("二维码失效!");
                    }
                } catch (NumberFormatException e) {
                    throw new IllegalArgumentException("无效的过期时间戳");
                }
                break;
            }
        }

        return raw;
    }
}
### 使用 Java 生成二维码 (QR Code) 在 Java生成二维码,最常用的方法是使用 **ZXing**(Zebra Crossing)库。ZXing 是一个开源的、跨平台的条形码和二维码生成与解析库,支持多种编码格式和纠错机制。 #### 1. Maven 依赖配置 首先,在 `pom.xml` 文件中添加 ZXing 的依赖: ```xml <dependency> <groupId>com.google.zxing</groupId> <artifactId>core</artifactId> <version>3.5.2</version> </dependency> <dependency> <groupId>com.google.zxing</groupId> <artifactId>javase</artifactId> <version>3.5.2</version> </dependency> ``` #### 2. 生成二维码的核心代码 以下是一个简单的 Java 示例,展示如何使用 ZXing 生成二维码并将其保存为图像文件: ```java import com.google.zxing.*; import com.google.zxing.client.j2se.MatrixToImageWriter; import com.google.zxing.common.BitMatrix; import com.google.zxing.qrcode.QRCodeWriter; import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel; import java.io.File; import java.io.IOException; import java.nio.file.FileSystems; import java.nio.file.Path; public class QRCodeGenerator { public static void generateQRCode(String data, String filePath, int width, int height) throws WriterException, IOException { // 设置二维码的纠错级别 QRCodeWriter qrCodeWriter = new QRCodeWriter(); BitMatrix bitMatrix = qrCodeWriter.encode(data, BarcodeFormat.QR_CODE, width, height, com.google.zxing.EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.L); // 写入文件 Path path = FileSystems.getDefault().getPath(filePath); MatrixToImageWriter.writeToPath(bitMatrix, "PNG", path); } public static void main(String[] args) { String data = "https://www.example.com"; String filePath = "qrcode.png"; int width = 300; int height = 300; try { generateQRCode(data, filePath, width, height); System.out.println("二维码生成成功!"); } catch (Exception e) { System.err.println("生成二维码失败:" + e.getMessage()); } } } ``` #### 3. 自定义二维码(添加 Logo) 如果希望在二维码中心添加 Logo,可以在生成二维码图像后,使用 `BufferedImage` 和 `Graphics2D` 将 Logo 叠加到二维码图像上: ```java import javax.imageio.ImageIO; import java.awt.*; import java.awt.image.BufferedImage; import java.io.File; public class QRCodeWithLogo { public static void addLogoToQRCode(String qrCodePath, String logoPath, String outputFilePath, int logoSize) throws Exception { BufferedImage qrImage = ImageIO.read(new File(qrCodePath)); BufferedImage logo = ImageIO.read(new File(logoPath)); // 调整 Logo 大小 Image scaledLogo = logo.getScaledInstance(logoSize, logoSize, Image.SCALE_SMOOTH); BufferedImage resizedLogo = new BufferedImage(logoSize, logoSize, BufferedImage.TYPE_INT_ARGB); Graphics2D g = resizedLogo.createGraphics(); g.drawImage(scaledLogo, 0, 0, null); g.dispose(); // 在二维码中心绘制 Logo Graphics2D graphics = qrImage.createGraphics(); int x = (qrImage.getWidth() - logoSize) / 2; int y = (qrImage.getHeight() - logoSize) / 2; graphics.drawImage(resizedLogo, x, y, null); graphics.dispose(); // 保存结果 ImageIO.write(qrImage, "PNG", new File(outputFilePath)); } public static void main(String[] args) throws Exception { String qrCodePath = "qrcode.png"; String logoPath = "logo.png"; String outputFilePath = "qrcode_with_logo.png"; int logoSize = 50; addLogoToQRCode(qrCodePath, logoPath, outputFilePath, logoSize); System.out.println("二维码添加 Logo 成功!"); } } ``` #### 4. 常见问题处理 在使用 ZXing 生成二维码时,可能会遇到如下异常: - **`data bits cannot fit in the QR Code`**:表示数据量超出了当前版本二维码的容量限制。可以通过提高二维码的版本(`Version`)或降低数据量来解决。例如,使用 `EncodeHintType.QR_VERSION` 设置更高的版本[^4]。 --- ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值