优化三、java XWPFDocument 生成word doc、docx 功能优化(插入图片,循环数据新增)

接上篇

注:Consts 类放到了本篇文章最下面
上篇的文章地址(也可直接点我主页去看):https://blog.csdn.net/C__jx/article/details/127110219?spm=1001.2014.3001.5501

一、优化点:

优化过后
1、纯文本模版也支持图片插入
2、 兼容部分 a a , {aa}, aa,{3, }问题
3、可自由设置图片宽高

直接上代码了,代码里面有注释,如有问题,希望指出,感谢指导

package com.zjjw.jxtest.util.util;

import com.zjjw.platform.core.consts.Consts;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.openxml4j.exceptions.InvalidFormatException;
import org.apache.poi.util.Units;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.apache.poi.xwpf.usermodel.XWPFParagraph;
import org.apache.poi.xwpf.usermodel.XWPFRun;
import org.apache.poi.xwpf.usermodel.XWPFTable;
import org.apache.poi.xwpf.usermodel.XWPFTableCell;
import org.apache.poi.xwpf.usermodel.XWPFTableRow;
import org.springframework.core.io.ClassPathResource;
import org.springframework.util.StringUtils;
import sun.misc.BASE64Decoder;
import sun.misc.BASE64Encoder;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

/**
 * 生成word 工具类
 *
 * @author: chenjiaxiang
 * @create: 2022/9/29 11:27
 **/
@Slf4j
public class CreateWordPoiUtils {


    private static final String FILE = "file";
    private static final String FILE_F = "File";
    private static final String BASE64 = "base64";
    private static final String BASE64_B = "Base64";

    private static final String JSON_OBJECT_START_$ = "${";



    /**
     * 根据docx模版生成docx文件 适用于做测试用的 main
     *
     * @param templateName  模版路径名称
     * @param insertTextMap ${} 字符替换
     * @param map           表格数据需循环新增 动态数据
     * @param filePath      输出到本地的文件路径
     * @param width         插入图片的宽度 不传默认为128
     * @param high          插入图片的高度 不传默认为128
     */
    public static void generateWord(String templateName, Map<String, Object> insertTextMap, Map<Integer, List<String[]>> map, String filePath, Integer width, Integer high) {
        getMain(templateName, insertTextMap, map, filePath, width, high);
    }

    /**
     * 根据docx模版生成docx文件 适用于做测试用的 main
     *
     * @param templateName  模版路径名称
     * @param insertTextMap ${} 字符替换
     * @param map           表格数据需循环新增 动态数据
     * @param filePath      输出到本地的文件路径
     */
    public static void generateWord(String templateName, Map<String, Object> insertTextMap, Map<Integer, List<String[]>> map, String filePath) {
        getMain(templateName, insertTextMap, map, filePath, 128, 128);
    }

    /**
     * 根据docx模版生成docx文件
     *
     * @param templateName  模版路径名称
     * @param insertTextMap ${} 字符替换
     * @param map           表格数据需循环新增 动态数据
     * @param width         插入图片的宽度 不传默认为128
     * @param high          插入图片的高度 不传默认为128
     * @return InputStream流
     */
    public static InputStream generateWord(String templateName, Map<String, Object> insertTextMap, Map<Integer, List<String[]>> map, Integer width, Integer high) {
        return getInputStream(templateName, insertTextMap, map, width, high);
    }

    /**
     * 根据docx模版生成docx文件
     *
     * @param templateName  模版路径名称
     * @param insertTextMap ${} 字符替换
     * @param map           表格数据需循环新增 动态数据
     * @return InputStream流
     */
    public static InputStream generateWord(String templateName, Map<String, Object> insertTextMap, Map<Integer, List<String[]>> map) {
        return getInputStream(templateName, insertTextMap, map, 128, 128);
    }

    /**
     * 根据docx模版生成docx文件 适用于做测试用的 main
     *
     * @param templateName  模版路径名称
     * @param insertTextMap ${} 字符替换
     * @param map           表格数据需循环新增 动态数据
     * @param filePath      输出到本地的文件路径
     * @param width         插入图片的宽度 不传默认为128
     * @param high          插入图片的高度 不传默认为128
     */
    private static void getMain(String templateName, Map<String, Object> insertTextMap, Map<Integer, List<String[]>> map, String filePath, Integer width, Integer high) {
        InputStream inputStream = null;
        //获取docx解析对象
        XWPFDocument xwpfDocument = null;
        FileOutputStream fileOutputStream = null;
        try {
            String fileLocal = String.format("%s%s.docx", filePath, UUID.randomUUID());

            // 设置模板输入和结果输出
            fileOutputStream = new FileOutputStream(fileLocal);

            inputStream = Files.newInputStream(new File(templateName).toPath());
            //获取docx解析对象
            xwpfDocument = new XWPFDocument(inputStream);
            // 处理所有文段数据,除了表格
            handleParagraphs(xwpfDocument, insertTextMap, width, high);
            // 处理表格数据
            handleTable(xwpfDocument, insertTextMap, map, width, high);
            //文档写入流
            xwpfDocument.write(fileOutputStream);
            log.info("word生成成功");
        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            try {
                if (inputStream != null) {
                    inputStream.close();
                }
                if (xwpfDocument != null) {
                    xwpfDocument.close();
                }
                if (fileOutputStream != null) {
                    fileOutputStream.close();
                }
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
    }

    /**
     * 根据docx模版生成docx文件
     *
     * @param templateName  模版路径名称
     * @param insertTextMap ${} 字符替换
     * @param map           表格数据需循环新增 动态数据
     * @param width         插入图片的宽度 不传默认为128
     * @param high          插入图片的高度 不传默认为128
     */
    private static InputStream getInputStream(String templateName, Map<String, Object> insertTextMap, Map<Integer, List<String[]>> map, Integer width, Integer high) {
        ClassPathResource classPathResource = new ClassPathResource(templateName);
        InputStream inputStream = null;
        //获取docx解析对象
        XWPFDocument xwpfDocument = null;
        ByteArrayOutputStream baos = null;
        try {
            inputStream = classPathResource.getInputStream();
            //获取docx解析对象
            xwpfDocument = new XWPFDocument(inputStream);
            // 处理所有文段数据,除了表格
            handleParagraphs(xwpfDocument, insertTextMap, width, high);
            // 处理表格数据
            handleTable(xwpfDocument, insertTextMap, map, width, high);
            //二进制OutputStream
            baos = new ByteArrayOutputStream();
            //文档写入流
            xwpfDocument.write(baos);
            log.info("word生成成功");
            //OutputStream写入InputStream二进制流
            return new ByteArrayInputStream(baos.toByteArray());
        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            try {
                if (inputStream != null) {
                    inputStream.close();
                }
                if (xwpfDocument != null) {
                    xwpfDocument.close();
                }
                if (baos != null) {
                    baos.close();
                }
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
    }

    /**
     * 处理需要替换的数据和 需要表格新增的数据
     *
     * @param xwpfDocument  docx
     * @param insertTextMap ${} 数据
     * @param map           表格数据
     * @param width         图片的宽度
     * @param high          图片的高度
     */
    public static void handleTable(XWPFDocument xwpfDocument, Map<String, Object> insertTextMap, Map<Integer, List<String[]>> map, Integer width, Integer high) {
        List<XWPFTable> tables = xwpfDocument.getTables();
        int tableIndex = 0;
        for (XWPFTable table : tables) {
            //表格坐标
            tableIndex++;
            List<XWPFTableRow> rows = table.getRows();
            if (rows.size() > 0) {
                if (isReplacement(table.getText())) {
                    // 替换数据
                    rows.stream().map(XWPFTableRow::getTableCells).forEach(tableCells -> {
                        tableCells.stream().filter(tableCell -> isReplacement(tableCell.getText())).forEach(tableCell -> {
                            List<XWPFParagraph> paragraphs = tableCell.getParagraphs();
                            //替换数据
                            paragraphs.stream().map(XWPFParagraph::getRuns).forEach(runs -> {
                                //获取map的key
                                runs.forEach(run -> {
                                    String key = matchesKey(run.text(), insertTextMap);
                                    if (Objects.isNull(insertTextMap.get(key))) {
                                        return;
                                    }
                                    String string = insertTextMap.get(key).toString().replace(JSON_OBJECT_START_$, Consts.EMPTY_STRING).replace(Consts.JSON_OBJECT_END, Consts.EMPTY_STRING);
                                    //表格替换
                                    tableReplace(key, insertTextMap, run, string, width, high);
                                });
                            });
                        });
                    });
                } else {
                    //处理新增表格数据
                    if (Objects.nonNull(map.get(tableIndex))) {
                        List<String[]> strings = map.get(tableIndex);
                        // 插入数据
                        IntStream.range(1, strings.size()).forEach(i -> table.createRow());
                        List<XWPFTableRow> rowList = table.getRows();
                        for (int i = 1; i < rowList.size(); i++) {
                            XWPFTableRow xwpfTableRow = rowList.get(i);
                            List<XWPFTableCell> tableCells = xwpfTableRow.getTableCells();
                            for (int j = 0; j < tableCells.size(); j++) {
                                XWPFTableCell xwpfTableCell = tableCells.get(j);
                                xwpfTableCell.setText(strings.get(i - 1)[j]);
                            }
                        }
                    }
                }
            }
        }
    }

    /**
     * 替换带有 ${}的数据
     *
     * @param wordValue 表格文本
     * @param map       数据
     * @param run       文档数据
     * @param width     图片的宽度
     * @param high      图片的高度
     */
    public static void matchesValue(String wordValue, Map<String, Object> map, XWPFRun run, Integer width, Integer high) {
        if (JSON_OBJECT_START_$.equals(wordValue) || Consts.JSON_OBJECT_END.equals(wordValue) || Consts.JSON_OBJECT_START.equals(wordValue)) {
            setNull(run);
        }
        for (String key : map.keySet()) {
            String s1 = JSON_OBJECT_START_$ + key + Consts.JSON_OBJECT_END;
            String s2 = JSON_OBJECT_START_$ + key;
            String s3 = key + Consts.JSON_OBJECT_END;
            String string = map.get(key).toString().replace(JSON_OBJECT_START_$, Consts.EMPTY_STRING).replace(Consts.JSON_OBJECT_END, Consts.EMPTY_STRING);
            if (s1.equals(wordValue) || key.equals(wordValue) || s2.equals(wordValue) || s3.equals(wordValue)) {
                //表格替换
                tableReplace(key, map, run, string, width, high);
            }
        }
    }


    /**
     * 表格、纯文本的数据转换
     *
     * @param key   map的key
     * @param map   map数据
     * @param run   文件行
     * @param file  图片的url或者base64字符传,多个以,分割
     * @param width 图片的宽度
     * @param high  图片的高度
     */
    private static void tableReplace(String key, Map<String, Object> map, XWPFRun run, String file, Integer width, Integer high) {
        try {
            if (key.contains(FILE) || key.contains(FILE_F)) {
                setNull(run);
                for (String ss : file.split(Consts.DEFAULT_SEPARATOR)) {
                    //url转input
                    //参数1:图片流数据 参数2:图片类型  参数3 图片名称  参数4:图片宽度  参数5:图片高度
                    InputStream inputStream = getInputStream(ss);
                    run.addPicture(inputStream, XWPFDocument.PICTURE_TYPE_PNG, UUID.randomUUID().toString(), Units.toEMU(width), Units.toEMU(high));
                    inputStream.close();
                }
            } else if (key.contains(BASE64) || key.contains(BASE64_B)) {
                setNull(run);
                for (String ss : file.split(Consts.DEFAULT_SEPARATOR)) {
                    //base64转input
                    //参数1:图片流数据 参数2:图片类型  参数3 图片名称  参数4:图片宽度  参数5:图片高度
                    InputStream inputStream = base64Input(ss);
                    run.addPicture(inputStream, XWPFDocument.PICTURE_TYPE_PNG, UUID.randomUUID().toString(), Units.toEMU(width), Units.toEMU(high));
                    inputStream.close();
                }
            } else if (Consts.JSON_OBJECT_END.equals(key)) {
                setNull(run);
            } else {
                run.setText(String.valueOf(map.get(key)), 0);
            }
        } catch (InvalidFormatException | IOException e) {
            throw new RuntimeException(e);
        }
    }


    /**
     * 获取map的key
     */
    public static String matchesKey(String wordValue, Map<String, Object> map) {
        if (JSON_OBJECT_START_$.equals(wordValue) || Consts.JSON_OBJECT_END.equals(wordValue) || Consts.JSON_OBJECT_START.equals(wordValue)) {
            wordValue = "";
            return wordValue;
        }
        for (String s : map.keySet()) {
            String s1 = JSON_OBJECT_START_$ + s + Consts.JSON_OBJECT_END;
            String s2 = JSON_OBJECT_START_$ + s;
            String s3 = s + Consts.JSON_OBJECT_END;
            if (s1.equals(wordValue) || s.equals(wordValue) || s2.equals(wordValue) || s3.equals(wordValue)) {
                wordValue = s;
            }
        }
        return wordValue;
    }

    /**
     * 判断是否有包含需替换的数据
     *
     * @param text 文本
     * @return boolean
     */
    public static boolean isReplacement(String text) {
        boolean check = false;
        if (text.contains(Consts.DOLLAR)) {
            check = true;
        }
        return check;
    }

    /**
     * 处理文段数据
     *
     * @param xwpfDocument  docx
     * @param insertTextMap ${}数据
     */
    public static void handleParagraphs(XWPFDocument xwpfDocument, Map<String, Object> insertTextMap, Integer width, Integer high) {
        xwpfDocument.getParagraphs().forEach(paragraph -> {
            String text = paragraph.getText();
            if (isReplacement(text)) {
                for (XWPFRun run : paragraph.getRuns()) {
                    // 判断带有${}的run
//                    run.setText(matchesValue(run.text(), insertTextMap), 0);
                    matchesValue(run.text(), insertTextMap, run, width, high);
                }
            }
        });

    }

    /**
     * base64转为输入流
     *
     * @param base64 编码
     * @return 输入流
     */
    public static InputStream base64Input(String base64) {
        BASE64Decoder decoder = new BASE64Decoder();
        InputStream inputStream;
        // Base64解码
        byte[] byteArr = new byte[0];
        try {
            byteArr = decoder.decodeBuffer(base64);
            inputStream = new ByteArrayInputStream(byteArr);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        return inputStream;
    }

    /**
     * 【获取网络文件的输入流】
     *
     * @param filePath: 网络文件路径
     * @return java.io.InputStream
     */
    public static InputStream getInputStream(String filePath) throws IOException {
        InputStream inputStream = null;
        //创建URL
        URL url = new URL(filePath);
        //试图连接并取得返回状态码
        URLConnection urlconn = url.openConnection();
        urlconn.connect();
        HttpURLConnection httpconn = (HttpURLConnection) urlconn;
        int httpResult = httpconn.getResponseCode();
        if (httpResult == HttpURLConnection.HTTP_OK) {
            inputStream = urlconn.getInputStream();
        }
        return inputStream;
    }


    /**
     * 图片URL转Base64编码
     *
     * @param imgUrl 图片URL
     * @return Base64编码
     */
    public static String imageUrlToBase64(String imgUrl) {
        URL url = null;
        InputStream is = null;
        ByteArrayOutputStream outStream = null;
        HttpURLConnection httpUrl = null;
        try {
            url = new URL(imgUrl);
            httpUrl = (HttpURLConnection) url.openConnection();
            httpUrl.connect();
            httpUrl.getInputStream();

            is = httpUrl.getInputStream();
            outStream = new ByteArrayOutputStream();

            //创建一个Buffer字符串
            byte[] buffer = new byte[1024];
            //每次读取的字符串长度,如果为-1,代表全部读取完毕
            int len = 0;
            //使用输入流从buffer里把数据读取出来
            while ((len = is.read(buffer)) != -1) {
                //用输出流往buffer里写入数据,中间参数代表从哪个位置开始读,len代表读取的长度
                outStream.write(buffer, 0, len);
            }
            // 对字节数组Base64编码
            return encode(outStream.toByteArray());
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if (is != null) {
                    is.close();
                }
                if (outStream != null) {
                    outStream.close();
                }
                if (httpUrl != null) {
                    httpUrl.disconnect();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        return null;
    }

    /**
     * 图片转字符串
     *
     * @param image 图片Buffer
     * @return Base64编码
     */
    public static String encode(byte[] image) {
        BASE64Encoder decoder = new BASE64Encoder();
        return replaceEnter(decoder.encode(image));
    }

    /**
     * 字符替换
     *
     * @param str 字符串
     * @return 替换后的字符串
     */
    public static String replaceEnter(String str) {
        String reg = "[\n-\r]";
        Pattern p = Pattern.compile(reg);
        Matcher m = p.matcher(str);
        return m.replaceAll("");
    }

    /**
     * 将对象集合转为一个String[] 集合
     *
     * @param tList 对象集合
     * @param <T>   参数
     * @return 数组集合
     */
    public static <T> List<String[]> listToArray(List<T> tList) {
        ArrayList<String[]> arrayList = new ArrayList<>();
        tList.forEach(t -> {
            String[] filedName = getFiledName(t);
            List<String> to = Arrays.stream(filedName).map(s -> getFieldValueByName(s, t))
                    .map(fieldValueByName -> StringUtils.isEmpty(fieldValueByName) ? "" : String.valueOf(fieldValueByName)).collect(Collectors.toList());
            arrayList.add(to.toArray(new String[]{}));
        });
        return arrayList;
    }

    /**
     * 获取对象的 属性名
     *
     * @param o 对象名称
     * @return 属性数组
     */

    private static String[] getFiledName(Object o) {
        Field[] fields = o.getClass().getDeclaredFields();
        return Arrays.stream(fields).map(Field::getName).toArray(String[]::new);
    }

    /**
     * 根据字段名 获取该字段值
     *
     * @param fieldName 字段名
     * @param o         对象
     * @return 值
     */
    private static Object getFieldValueByName(String fieldName, Object o) {
        try {
            String firstLetter = fieldName.substring(0, 1).toUpperCase();
            String getter = "get" + firstLetter + fieldName.substring(1);
            Method method = o.getClass().getMethod(getter);
            return method.invoke(o);
        } catch (Exception e) {
            log.error("获取属性值失败!" + e, e);
        }
        return null;
    }

    /**
     * setNull
     */
    private static void setNull(XWPFRun run) {
        run.setText(Consts.EMPTY_STRING, 0);
    }
}

Consts类

public class Consts {
    public static final int ZERO = 0;
    public static final String[] BREAKERS = new String[]{"\r\n", "\n\r", "\n"};
    public static final String[] BREAKERS_REPLACEMENT = new String[]{"\\\\r\\\\n", "\\\\n\\\\r", "\\\\n"};
    public static final String DEFAULT_FILE_SEPARATOR = "/";
    public static final String DOUBLE_FILE_SEPARATOR = "//";
    public static final String HTTP_PROTOCOL = "http";
    public static final String HTTPS_PROTOCOL = "https";
    public static final String EMPTY_STRING = "";
    public static final String DEFAULT_SEPARATOR = ",";
    public static final String DOT = ".";
    public static final String DOLLAR = "$";
    public static final String STRING_FORMAT_PLACEHOLDER = "%s";
    public static final String SEPARATOR_AND = "&";
    public static final String EQUAL = "=";
    public static final String QUESTION = "?";
    public static final String PATTERN_ALL = "*";
    public static final String ADD_OP = "+";
    public static final String COLON = ":";
    public static final String SEMICOLON_CHAR = ";";
    public static final String JSON_ARRAY_START = "[";
    public static final String JSON_ARRAY_END = "]";
    public static final String JSON_OBJECT_START = "{";
    public static final String JSON_OBJECT_END = "}";
    public static final String APP_NAME = "app.name";
    public static final String APPLICATION_NAME = "spring.application.name";
    public static final String RUN_MODE = "run.mode";
    public static final String CONFIG_ADDRESS = "config.address";
    public static final String DISCOVERY_ADDRESS = "discovery.address";
    public static final String CD_NAMESPACE = "config_discovery.namespace";
    public static final String SERVICE_SIDE = "service.side";
    public static final String SPRING_PROFILE = "spring.profiles.active";
    public static final String SERVICE_PUBLIC = "public";
    public static final String SERVICE_PRIVATE = "private";
    public static final String SENTINEL_DASHBOARD = "sentinel.dashboard";
    public static final String SENTINEL_PORT = "sentinel.port";
    public static final String ACCESS_TOKEN = "accessToken";
    public static final String DEVICE_NUMBER = "deviceNumber";
    public static final String USER_CODE = "userCode";
    public static final String APP_KEY = "appKey";
    public static final String PLATFORM = "platform";
    public static final String TIMESTAMP = "timestamp";
    public static final String SIGN = "sign";
    public static final String NONCE = "nonce";
    public static final String MIDDLE_LINE = "-";
    public static final int DEFAULT_PAGE_INDEX = 0;
    public static final int DEFAULT_PAGE_SIZE = 20;
    public static final int DEFAULT_MAX_PAGE_SIZE = 200;
    public static final int ONT_YEAR_SECONDS = 31536000;
    public static final int DEFAULT_THRESHOLD = 900;

    private Consts() {
    }
}
  • 0
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
Java中,可以使用Apache POI库来生成Word文档并插入表格和图片。下面是一个简单的示例代码: 首先,需要引入Apache POI库的依赖: ```xml <dependency> <groupId>org.apache.poi</groupId> <artifactId>poi</artifactId> <version>4.1.2</version> </dependency> <dependency> <groupId>org.apache.poi</groupId> <artifactId>poi-ooxml</artifactId> <version>4.1.2</version> </dependency> ``` 然后,可以使用以下代码生成一个Word文件并插入表格和图片: ```java import org.apache.poi.util.IOUtils; import org.apache.poi.xwpf.usermodel.*; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.InputStream; public class WordGenerator { public static void main(String[] args) { try { // 创建一个新的Word文档 XWPFDocument document = new XWPFDocument(); // 创建一个表格 XWPFTable table = document.createTable(3, 3); // 往表格中添加内容 table.getRow(0).getCell(0).setText("姓名"); table.getRow(0).getCell(1).setText("性别"); table.getRow(0).getCell(2).setText("年龄"); table.getRow(1).getCell(0).setText("张"); table.getRow(1).getCell(1).setText("男"); table.getRow(1).getCell(2).setText("20"); table.getRow(2).getCell(0).setText("李四"); table.getRow(2).getCell(1).setText("女"); table.getRow(2).getCell(2).setText("22"); // 插入一张图片 InputStream imageStream = new FileInputStream("path/to/image.jpg"); XWPFParagraph paragraph = document.createParagraph(); XWPFRun run = paragraph.createRun(); run.addPicture(imageStream, XWPFDocument.PICTURE_TYPE_JPEG, "image.jpg", Units.toEMU(200), Units.toEMU(200)); imageStream.close(); // 保存Word文档 FileOutputStream out = new FileOutputStream("path/to/output.docx"); document.write(out); out.close(); System.out.println("Word文档生成成功!"); } catch (Exception e) { e.printStackTrace(); } } } ``` 需要注意的是,上述代码中的图片路径和输出路径需要根据实际情况修改。另外,还需要根据实际需求来调整表格的行数、列数以及单元格内容。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

C__jx

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值