密码学-Java与C++混合编程-AES加密与解密的实现与GUI


所调用的动态链接库详见 密码学-可编译为动态链接库(DLL)的AES加密算法-C++实现.


软件实现的主要功能

  1. 密钥生成

    • 自定义口令生成密钥
    • 随机生成口令与密钥
    • 使用自定义密钥
    • 密钥格式规范性检查
  2. 字符串加密与解密

    • 文本区输入明文字符串与密文字符串
    • 实时显示当前输入字符串的编码(明文编码方案为UTF-8, 密文字符串编码方案为ISO-8859-1)
    • 实时统计明文与密文的字符数与字节数
    • 加解密分组模式支持ECB模式与CBC模式
    • 可以活动与折叠的文本区与编码显示区
  3. 文件加密

    • 可使用对话框选择任意待加密的文件
    • 可使用对话框选择加密文件的输出目录
    • 显示选择得待加密文件的路径
    • 显示选择得加密文件输出目录的路径
    • 显示待加密文件的大小
    • 加密分组模式支持ECB模式与CBC模式
    • 支持使用多线程并行加密多个文件
    • 加密时显示当前状态, 进度条, 文件加密实时速度, 已用时间, 预计剩余时间
    • 加密完成后显示当前状态, 进度条, 文件加密平均速度, 已用时间
    • 加密后的文件原文件名称不变, 文件拓展名附加.encrypted字段
  4. 文件解密

    • 可使用对话框选择任意待解密的文件
    • 可使用对话框选择解密文件的输出目录
    • 选择待解密文件的对话框支持使用过滤器加密文件 (*.encrypted)
    • 若加密文件数据格式错误则弹出警告并停止解密
    • 拖加密文件数据正确但拓展名错误则弹出警告并询问用户是否继续解密
    • 显示选择得待解密文件的路径
    • 显示选择得解密文件输出目录的路径
    • 显示待解密文件的大小
    • 解密分组模式支持ECB模式与CBC模式, 并可以自动判断文件所使用的模式
    • 支持使用多线程并行解密多个文件
    • 解密时显示当前状态, 进度条, 文件加密实时速度, 已用时间, 预计剩余时间
    • 解密完成后显示当前状态, 进度条, 文件加密平均速度, 已用时间

程序效果图

文字符串加密与解密界面

文字符串加密与界面界面件


文件加密界面

文件加密界面


文件解密界面

文件解密界面


文件加密

文件加密


文件解密

文件解密


程序源代码

AESGUI.java

package assignment.authentication.experiment_2.project.gui;

import com.sun.jna.*;

import javax.swing.*;
import javax.swing.event.*;
import javax.swing.filechooser.FileFilter;

import java.awt.event.*;
import java.awt.*;

import java.io.*;

public class AESGUI
{
   
    private static final int    WINDOW_WIDTH                  = 1440; // 窗口宽度
    private static final int    WINDOW_HEIGHT                 = 480;  // 窗口高度

    public static final int     KEY_LENGTH                    = 32;   // 密钥长度(单位字节)
    public static final int     CIPHERTEXT_BLOCK_LENGTH       = 16;   // 密文数据分组长度(单位字节)
    // public static final int CIPHERTEXT_INIT_VECTOR_LENGTH = 16; // 密文数据初始向量长度
    public static final int     CIPHERTEXT_MODE_OFFSET        = 0;    // 密文数据中 分组模式的偏移量
    public static final int     CIPHERTEXT_EFFECTIVE_OFFSET   = 1;    // 密文数据中 末尾16字节有效位数的偏移量
    public static final int     CIPHERTEXT_INIT_VECTOR_OFFSET = 2;    // 密文数据中 初始向量的的偏移量
    public static final int     CIPHERTEXT_DATA_OFFSET        = 18;   // 密文数据中 加密数据的的偏移量

    public static final byte    CIPHERTEXT_FULL_VALUE         = 0;    // 密文数据中 末尾16字节填充值

    public static final int     ENCRYPT_MODE_ECB              = 0;    // 分组加密模式:ECB
    public static final int     ENCRYPT_MODE_CBC              = 1;    // 分组加密模式:CBC

    private static final String DEFAULT_CHAR_SET;                     // 默认字符集
    private static final String ENCRYPT_CHAR_SET;                     // 密文展示字符集

    private static final String ENCRYPT_MODE_LIST[];                  // 下拉选项卡选项, 分组加密模式

    public static final String  ROOT_PATH_SOURCE;                     // 源文件选择窗口的起始目录
    public static final String  ROOT_PATH_TARGET;                     // 目标文件选择窗口的起始目录

    public static final String  FORMAT_CHARACTERS;                    // 格式化显示字符数
    public static final String  FORMAT_FILE_SIZE;                     // 格式化显示文件大小
    public static final String  FORMAT_TARGET_FILE_PATH;              // 格式化目标文件路径
    public static final String  FORMAT_KEY_FORMAT_MESSAGE;            // 格式化密钥格式不规范输出信息

    public static final String  ENCRYPTED_FILENAME_EXTENSION;         // 加密文件拓展名
    public static final String  DECRYPTED_FILENAME_EXTENSION;         // 若待解密文件拓展名不为加密文件拓展名, 则使用该解密文件拓展名

    private JFrame              frame;                                // 窗口对象

    private JTabbedPane         pane_tab;                             // 选项卡窗格对象

    private JSplitPane          pane_split_plaintext;                 // 明文文本区的分隔面板
    private JSplitPane          pane_split_ciphertext;                // 密文文本区的分隔面板

    private JScrollPane         pane_scroll_plaintext;                // 明文文本区滚动条
    private JScrollPane         pane_scroll_ciphertext;               // 密文文本区滚动条
    private JScrollPane         pane_scroll_plaintext_hex;            // 明文16进制编码文本区滚动条
    private JScrollPane         pane_scroll_ciphertext_hex;           // 密文16进制编码文本区滚动条

    private JPanel              panel_operator;                       // 操作面板
    private JPanel              panel_string;                         // 字符串加解密选项卡
    private JPanel              panel_file_encrypt;                   // 文件加密选项卡
    private JPanel              panel_file_decrypt;                   // 文件解密选项卡

    private JLabel              label_password;                       // 用于生成密钥的口令
    private JLabel              label_key;                            // 密钥
    private JLabel              label_encrypt_mode;                   // 加密方式(分组密码模式)
    private JLabel              label_plaintext;                      // 明文
    private JLabel              label_ciphertext;                     // 密文
    private JLabel              label_plaintext_characters;           // 明文字符数
    private JLabel              label_ciphertext_characters;          // 密文字符数
    // private JLabel label_plaintext_encode; // 明文编码(16进制编码)
    // private JLabel label_ciphertext_encode; // 密文编码(16进制编码)
    // private JLabel label_plaintext_file; // 明文文件
    // private JLabel label_ciphertext_file; // 密文文件
    private JLabel              label_plaintext_target_file;          // 待加密文件
    private JLabel              label_ciphertext_target_file;         // 待解密文件
    private JLabel              label_plaintext_target_dir;           // 解密文件输出目录
    private JLabel              label_ciphertext_target_dir;          // 加密文件输出目录
    private JLabel              label_plaintext_file_size;            // 明文文件大小
    private JLabel              label_ciphertext_file_size;           // 密文文件大小

    private JTextField          field_password;                       // 用于生成密钥的口令
    private JTextField          field_key;                            // 密钥
    private JTextField          field_plaintext_encode;               // 明文编码(16进制编码)
    private JTextField          field_ciphertext_encode;              // 密文编码(16进制编码)
    private JTextField          field_plaintext_file_path;            // 明文文件路径
    private JTextField          field_ciphertext_file_path;           // 密文文件路径
    private JTextField          field_plaintext_dir_path;             // 明文目录路径
    private JTextField          field_ciphertext_dir_path;            // 密文目录路径

    private JTextArea           area_plaintext;                       // 明文字符串文本区
    private JTextArea           area_ciphertext;                      // 密文字符串文本区
    private JTextArea           area_plaintext_hex;                   // 明文16进制编码文本区
    private JTextArea           area_ciphertext_hex;                  // 密文16进制编码文本区

    private JFileChooser        file_chooser_plaintext;               // 明文文件选择对话框
    private JFileChooser        file_chooser_ciphertext;              // 密文文件选择对话框
    private JFileChooser        dir_chooser_plaintext;                // 解密文件输出目录选择对话框
    private JFileChooser        dir_chooser_ciphertext;               // 加密文件输出目录选择对话框

    private JButton             button_key_generation_random;         // 随机生成口令与密钥
    private JButton             button_key_standard;                  // 检查密钥格式是否规范
    private JButton             button_select_plaintext_file;         // 选择明文文件
    private JButton             button_select_ciphertext_file;        // 选择密文文件
    private JButton             button_select_plaintext_dir;          // 选择明文目录
    private JButton             button_select_ciphertext_dir;         // 选择密文目录
    private JButton             button_encrypt;                       // 加密
    private JButton             button_decrypt;                       // 解密

    private JComboBox<String>   box_encrypt_mode;                     // 下拉选项卡, 分组加密模式

    // private JProgressBar bar_encrypt_progress; // 文件加密进度条
    // private JProgressBar bar_decrypt_progress; // 文件解密进度条

    private GridBagConstraints  gbc_both;                             // 网格包管理器(全部填充)
    private GridBagConstraints  gbc_horizontal;                       // 网格包管理器(仅水平填充)

    private byte                key[];                                // 密钥

    private File                file_plaintext;                       // 待加密文件
    private File                file_ciphertext;                      // 待解密文件
    private File                dir_plaintext;                        // 解密后文件保存目录
    private File                dir_ciphertext;                       // 加密后文件保存目录

    // private EncryptAndDecryptDialog dialog_encryption; // 文件加密对话框
    // private EncryptAndDecryptDialog dialog_decryption; // 文件解密对话框

    /*
    interface DLL_AES extends Library
    {
    DLL_AES dll_AES = (DLL_AES) Native.loadLibrary("lib\\DLL_AES", DLL_AES.class);
    public void AES_encrypt(byte key[], int data[]);
    public void AES_decrypt(byte key[], int data[]);
    }
    DLL_AES.dll_AES.AES_encrypt(key, data);
    DLL_AES.dll_AES.AES_decrypt(key, data);
    */

    static {
   
        Native.register("lib/DLL_AES.dll"); // 加载动态链接库

        // 静态属性初始化
        DEFAULT_CHAR_SET = "UTF-8"; // 默认字符集
        ENCRYPT_CHAR_SET = "ISO-8859-1"; // 密文字符集
        ENCRYPT_MODE_LIST = new String[] {
    "ECB", "CBC" }; // 下拉选项卡选项, 分组加密模式
        // ROOT_PATH_SOURCE = "assignment/authentication/experiment_2/data"; // 文件选择窗口的起始目录
        ROOT_PATH_SOURCE = "../Hybrid/assignment/authentication/experiment_2/data/source"; // 源文件选择窗口的起始目录
        ROOT_PATH_TARGET = "../Hybrid/assignment/authentication/experiment_2/data/target"; // 目标文件选择窗口的起始目录
        FORMAT_CHARACTERS = "字符数: %d 字节数: %d"; // 格式化显示字符数
        FORMAT_FILE_SIZE = "文件大小: %d 字节"; // 格式化显示文件大小
        FORMAT_TARGET_FILE_PATH = "%s/%s"; // 格式化目标文件路径
        FORMAT_KEY_FORMAT_MESSAGE = "密钥格式不规范: 应为16进制表示的%d个字节(共%d个字符)"; // 格式化目标文件路径
        ENCRYPTED_FILENAME_EXTENSION = ".encrypted"; // 加密文件拓展名
        DECRYPTED_FILENAME_EXTENSION = ".decrypted"; // 若待解密文件拓展名不为加密文件拓展名, 则使用该解密文件拓展名
    }

    /** 动态链接库方法, 解密16字节数据
    * @param: key: 密钥(16/24/32字节, 128/192/256位)
    @param key_len 密钥长度, 单位字节, 仅支持16/24/32
    * @param: data: 4个32位数据/16个8位数据(32位int*4, 或8位byte*16, 128位)
    * @return: data
    */
    public static native void AES_encrypt(byte key[], int key_len, int data[]);

    public static native void AES_ByteEncrypt(byte key[], int key_len, byte data[]);

    /** 动态链接库方法, 加密16字节数据
     * @param: key: 密钥(16/24/32字节, 128/192/256位)
     * @param key_len 密钥长度, 单位字节, 仅支持16/24/32
     * @param: data: 4个32位数据/16个8位数据(32位int*4, 或8位byte*16, 128位)
     * @return: data
     */
    public static native void AES_decrypt(byte key[], int key_len, int data[]);

    public static native void AES_ByteDecrypt(byte key[], int key_len, byte data[]);

    public static void main(String[] args)
    {
   
        // AESGUI.testCryption(); // 加解密测试

        // AESGUI.testFont(); // 打印支持的字体

        Utils.initGlobalFont(new Font("微软雅黑", Font.PLAIN, 14)); // 统一设置字体
        new AESGUI().display();
    }

    public static void testCryption() // 加解密测试
    {
   
        byte key[] = {
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
        int data_int[] = {
    1, 2, 3, 4 };
        byte data_byte[] = {
    1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 };
        // 加密32位int * 4
        for (int i : data_int) {
   
            System.out.println(i);
        }
        AES_encrypt(key, AESGUI.KEY_LENGTH, data_int);
        for (int i : data_int) {
   
            System.out.println(i);
        }
        AES_decrypt(key, AESGUI.KEY_LENGTH, data_int);
        for (int i : data_int) {
   
            System.out.println(i);
        }

        // 加密8位byte * 16
        for (byte b : data_byte) {
   
            System.out.println(b);
        }
        AES_ByteEncrypt(key, AESGUI.KEY_LENGTH, data_byte);
        for (byte b : data_byte) {
   
            System.out.println(b);
        }
        AES_ByteDecrypt(key, AESGUI.KEY_LENGTH, data_byte);
        for (byte b : data_byte) {
   
            System.out.println(b);
        }
    }

    public static void testFont() // 打印java支持的字体
    {
   
        GraphicsEnvironment env = GraphicsEnvironment.getLocalGraphicsEnvironment();
        String font[] = env.getAvailableFontFamilyNames();
        for (String str : font) {
   
            System.out.println(str);
        }
    }

    /** 加密(不进行数据规范性检查)
    * @param mode int 加密方式 0:ECB 1:CBC
    * @param key byte[] 密钥, 16字节
    * @param data byte[] 输入明文数据, 输出密文数据, 16字节
    * @param init_vecotr byte[] 初始化向量, 16字节, ECB模式可设置为null
    */
    public static void encrypt(int mode, byte[] key, byte[] data, byte[] init_vecotr)
    {
   
        switch (mode) {
   
        case AESGUI.ENCRYPT_MODE_ECB: // ECB 模式
            AES_ByteEncrypt(key, AESGUI.KEY_LENGTH, data);
            break;
        case AESGUI.ENCRYPT_MODE_CBC: // CBC 模式
            for (int i = 0; i < init_vecotr.length; i++) {
    // 异或
                data[i] ^= init_vecotr[i];
            }
            AES_ByteEncrypt(key, AESGUI.KEY_LENGTH, data);
            break;
        default:
            break;
        }
    }

    /** 解密(不进行数据规范性检查)
     * @param mode int 加密方式 0:ECB 1:CBC
     * @param key byte[] 密钥, 16字节
     * @param data byte[] 输入密文数据, 输出明文数据, 16字节
     * @param init_vecotr byte[] 初始化向量, 16字节, ECB模式可设置为null
     */
    public static void decrypt(int mode, byte[] key, byte[] data, byte[] init_vecotr)
    {
   
        switch (mode) {
   
        case AESGUI.ENCRYPT_MODE_ECB: // ECB 模式
            AES_ByteDecrypt(key, AESGUI.KEY_LENGTH, data);
            break;
        case AESGUI.ENCRYPT_MODE_CBC: // CBC 模式
            AES_ByteDecrypt(key, AESGUI.KEY_LENGTH, data);
            for (int i = 0; i < init_vecotr.length; i++) {
   
                data[i] ^= init_vecotr[i]; // 异或
            }
            break;
        default:
            break;
        }
    }

    /** 检查密文格式是否规范
     * @param mode int 加密方式 0:ECB 1:CBC
     * @param key byte[] 密钥, 16字节
     * @param data byte[] 输入密文数据, 输出明文数据, 16字节
     * @param init_vecotr byte[] 初始化向量, 16字节, ECB模式可设置为null
     */
    public static boolean isCiphertextStandard(byte[] ciphertext)
    {
   
        if (ciphertext.length < AESGUI.CIPHERTEXT_DATA_OFFSET) return false;
        if (ciphertext[AESGUI.CIPHERTEXT_MODE_OFFSET] >= AESGUI.ENCRYPT_MODE_LIST.length) return false;
        if (ciphertext[AESGUI.CIPHERTEXT_EFFECTIVE_OFFSET] >= AESGUI.CIPHERTEXT_BLOCK_LENGTH) return false;
        if ((ciphertext.length - AESGUI.CIPHERTEXT_DATA_OFFSET) % AESGUI.CIPHERTEXT_BLOCK_LENGTH != 0) return false;
        return true;
    }

    public void display()
    {
   
        this.initLabel();
        this.initTextField();
        this.initComboBox();
        this.initTextArea();
        this.initFileChooser();
        this.initButton();
        this.initGridBagConstraints();
        this.initScollPane();
        this.initSplitPane();
        this.initPanel();
        this.initTabbedPane();
        this.initFrame();

        // new EncryptAndDecryptDialog(this.frame, this.frame, "测试", EncryptAndDecryptDialog.MODE_TEST);
    }

    private void initLabel() // 初始化标签
    {
   
        // 定义标签
        this.label_password = new JLabel("口令");
        this.label_key = new JLabel("密钥");
        this.label_encrypt_mode = new JLabel("模式");
        // this.label_plaintext = new JLabel("明文");
        // this.label_ciphertext = new JLabel("密文");
        this.label_plaintext = new JLabel("明文 & 明文编码");
        this.label_ciphertext = new JLabel("密文 & 密文编码");
        this.label_plaintext_characters = new JLabel(String.format(FORMAT_CHARACTERS, 0, 0));
        this.label_ciphertext_characters = new JLabel(String.format(FORMAT_CHARACTERS, 0, 0));
        // this.label_plaintext_encode = new JLabel("明文编码");
        // this.label_ciphertext_encode = new JLabel("密文编码");
        this.label_plaintext_target_file = new JLabel("待加密文件");
        this.label_ciphertext_target_file = new JLabel("待解密文件");
        this.label_plaintext_target_dir = new JLabel("解密文件输出目录");
        this.label_ciphertext_target_dir = new JLabel("加密文件输出目录");
        this.label_plaintext_file_size = new JLabel(String.format(FORMAT_FILE_SIZE, 0));
        this.label_ciphertext_file_size = new JLabel(String.format(FORMAT_FILE_SIZE, 0));
    }

    private void initTextField() // 初始化文本框
    {
   
        // 定义文本框
        this.field_password = new JTextField(38);
        this.field_key = new JTextField(38);
        this.field_plaintext_encode = new JTextField();
        this.field_ciphertext_encode = new JTextField();
        this.field_plaintext_file_path = new JTextField();
        this.field_ciphertext_file_path = new JTextField();
        this.field_plaintext_dir_path = new JTextField();
        this.field_ciphertext_dir_path = new JTextField();

        // 文件路径不能编辑
        this.field_plaintext_file_path.setEditable(false);
        this.field_ciphertext_file_path.setEditable(false);
        this.field_plaintext_dir_path.setEditable(false);
        this.field_ciphertext_dir_path.setEditable(false);

        // 密钥为16进制显示, 使用等宽字体
        this.field_key.setFont(new Font("Consolas", Font.PLAIN, 14));

        // 为文本框添加事件监听器
        this.field_password.addCaretListener(new CaretListener() // 口令变化自动更新密钥
        {
   
            public void caretUpdate(CaretEvent event)
            {
   
                String password = field_password.getText();
                field_key.setText(Utils.byteToHexString(Utils.getHashByteString(password, AESGUI.KEY_LENGTH)));
            }
        });
        this.field_key.addCaretListener(new CaretListener() // 密钥文本变化若合法自动更新key
        {
   
            public void caretUpdate(CaretEvent event)
            {
   
                try {
   
                    String key_str = field_key.getText();
                    if (key_str.length() != AESGUI.KEY_LENGTH * 2) return;
                    key = Utils.hexStringToByte(key_str);
                } catch (RuntimeException e) {
   
                    Utils.showExceptionDialog("密钥格式不规范: 应为 16进制 表示的" + AESGUI.KEY_LENGTH + "个 16 " + AESGUI.KEY_LENGTH * 2 + "te", e);
                }
            }
        });
    }

    private void initComboBox() // 初始化下拉选项
    {
   
        this.box_encrypt_mode = new JComboBox<String>(AESGUI.ENCRYPT_MODE_LIST);
    }

    private void initTextArea() // 初始化文本区
    {
   
        // 定义文本区
        this.area_plaintext = new JTextArea();
        this.area_ciphertext = new JTextArea();
        this.area_plaintext_hex = new JTextArea();
        this.area_ciphertext_hex = new JTextArea();

        // 设置自动换行
        // this.area_plaintext_hex.setLineWrap(true);
        // this.area_ciphertext_hex.setLineWrap(true);

        // 16进制显示文本区禁止编辑
        this.area_plaintext_hex.setEditable(false);
        this.area_ciphertext_hex.setEditable(false);

        // 16进制显示文本区设置为等宽的consoles字体
        this.area_plaintext_hex.setFont(new Font("Consolas", Font.PLAIN, 12));
        this.area_ciphertext_hex.setFont(new Font("Consolas", Font.PLAIN, 12));

        // 为文本区添加事件监听器: 内容变化更新对应十六进制区域与字符个数
        this.area_plaintext.addCaretListener(new CaretListener()
        {
   
            public void caretUpdate(CaretEvent event)
            {
   
                String text_str = area_plaintext.getText();
                byte text_byte_arr[] = Utils.stringEncoding(text_str, AESGUI.DEFAULT_CHAR_SET);
                area_plaintext_hex.setText("");
                for (int i = 0; i < text_byte_arr.length; i++) {
   
                    area_plaintext_hex.append(String.format("%02X", text_byte_arr[i]));
                    area_plaintext_hex.append(((i + 1) % 16 != 0 ? " " : "\n"));
                }

                label_plaintext_characters.setText(String.format(AESGUI.FORMAT_CHARACTERS, text_str.length(), text_byte_arr.length));
            }
        });
        this.area_ciphertext.addCaretListener(new CaretListener()
        {
   
            public void caretUpdate(CaretEvent event)
            {
   
                String text_str = area_ciphertext.getText();
                byte text_byte_arr[] = Utils.stringEncoding(text_str, AESGUI.ENCRYPT_CHAR_SET);
                area_ciphertext_hex.setText("");
                for (int i = 0; i < text_byte_arr.length; i++) {
   
                    area_ciphertext_hex.append(String.format("%02X", text_byte_arr[i]));
                    area_ciphertext_hex.append(((i + 1) % 16 != 0 ? " " : "\n"));
                }

                label_ciphertext_characters.setText(String.format(AESGUI.FORMAT_CHARACTERS, text_str.length(), text_byte_arr.length));

            }
        });
    }

    private void initFileChooser()
    {
   
        // 定义文件选择对话框
        this.file_chooser_plaintext = new JFileChooser(AESGUI.ROOT_PATH_SOURCE);
        this.file_chooser_ciphertext = new JFileChooser(AESGUI.ROOT_PATH_TARGET);
        this.dir_chooser_plaintext = new JFileChooser(AESGUI.ROOT_PATH_TARGET);
        this.dir_chooser_ciphertext = new JFileChooser(AESGUI.ROOT_PATH_TARGET);

        // 对话框标题
        this.file_chooser_plaintext.setDialogTitle("待加密文件选择");
        this.file_chooser_ciphertext.setDialogTitle("待解密文件选择");
        this.dir_chooser_plaintext.setDialogTitle("解密文件输出目录选择");
        this.dir_chooser_ciphertext.setDialogTitle("加密文件输出目录选择");

        // 选择文件/目录
        this.file_chooser_plaintext.setFileSelectionMode(JFileChooser.FILES_ONLY);
        this.file_chooser_ciphertext.setFileSelectionMode(JFileChooser.FILES_ONLY);
        this.dir_chooser_plaintext.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
        
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值