基于itextpdf组件动态生成报告,支持表格、饼状图、柱状图、曲线图等

pom.xml引入组件itextpdf

       <dependency>
            <groupId>com.itextpdf</groupId>
            <artifactId>itextpdf</artifactId>
            <version>5.4.2</version>
        </dependency>

初始化全局变量

    public static BaseFont chineseFont;
    public static BaseFont chineseFont2;
    public static Font bodyFootnoteBold;
    public static Font bodyFontNormal;
    public static Font paragraphFont;

    static {
        try {
            chineseFont = BaseFont.createFont("STSong-Light", "UniGB-UCS2-H", BaseFont.NOT_EMBEDDED);
            chineseFont2 = BaseFont.createFont(ScrmConfig.projectPath + "/font/msyhl.ttc"+",1", BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);
            bodyFootnoteBold = new Font(chineseFont, 14, Font.BOLD, BaseColor.BLACK);
            bodyFontNormal = new Font(chineseFont, 9, Font.NORMAL, BaseColor.BLACK);
            paragraphFont = new Font(chineseFont, 12, Font.NORMAL, BaseColor.BLACK);
            paragraphFont.setFamily("Simsun");
        } catch (DocumentException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

定义生成报告的公共方法 generateReport,返回PDF,其中主要核心方法fillData

PDF定义:

@Data
@Builder
public class PDF {
    // pdf字节流
    private byte[] data;
    // 页面宽度
    private float width;
    // 页面高度
    private float height;
    // 文件名
    private String name;
    @Builder.Default
    private String extName = "pdf";
}
/**
     * 根据pdf模板生成报告 返回字节流
     * @param templatePath 模板文件路径
     * @param template 模板文件名
     * @param data 数据 bean对象或者map对象
     * @param safety 是否设置权限
     * @param password 设置权限的密码
     * @param addPageNum 是否追加页码
     * @param <T>
     * @return
     */
    public static <T> PDF generateReport(String templatePath, String template, T data, boolean safety, String password, boolean addPageNum){
        if(StringUtils.isAnyBlank(templatePath, template)){
            log.warn("模板路径或模板文件为空");
            return null;
        }
        ByteArrayOutputStream bos = null;
        PdfStamper pdfStamper = null;
        try {
            PdfReader templateReader = new PdfReader(templatePath+template);
            bos = new ByteArrayOutputStream();
            pdfStamper = new PdfStamper(templateReader, bos);

            // 设置权限
            if(safety) {
                pdfStamper.setEncryption(null, password.getBytes(),
                        PdfWriter.ALLOW_PRINTING|PdfWriter.ALLOW_DEGRADED_PRINTING, PdfWriter.STANDARD_ENCRYPTION_128);
            }

            // 使用中文字体
            ArrayList<BaseFont> fonts = Lists.newArrayList(chineseFont);

            AcroFields fields = pdfStamper.getAcroFields();
            fields.setSubstitutionFonts(fonts);

            //pdfStamper.getWriter().setPageEvent(new PageNumPdfPageEvent());

            // 填充模板数据
            fillData(pdfStamper, fields, data);


            // 必须要调佣这个,否则文档不会生成
            pdfStamper.setFormFlattening(true);

            try {
                pdfStamper.close();
            } catch (Exception e) {
                //
            }

            // 返回pdf byte数组
            Rectangle rectangle = templateReader.getPageSize(1);
            PDF pdf = PDF.builder()
                    .data(bos.toByteArray())
                    .width(rectangle.getWidth())
                    .height(rectangle.getHeight())
                    .build();
            // 添加页码
            if(addPageNum) addPageNum(pdf, password);
            return pdf;
        } catch (IOException e) {
            log.error("generate report failed. IOException[{}]", e.getMessage());
        } catch (DocumentException e){
            log.error("generate report failed. DocumentException[{}]", e.getMessage());
        } finally {
            try {
                if(bos != null) bos.close();
                if(pdfStamper != null) pdfStamper.close();
            } catch (Exception e) {
                log.error("stream close exception, [{}]", e.getMessage());
            }
        }
        return null;
    }

填充字段域方法 fillData 的参数data支持文本、图片、表格类型等

   private static <T> void fillData(PdfStamper ps, AcroFields fields, T data){
        if(fields == null || data == null) return;
        Map<String, Object> dataMap = BeanUtils.transBeanToMap(data);
        try {
            for (String key : dataMap.keySet()){
                Object object = dataMap.get(key);
                if(object instanceof ImageField){
                    ImageField image = (ImageField) object;
                    fillImage(ps, fields, key, image.getImages());
                }else if(object instanceof Table) {
                    Table table = (Table) object;
                    fillTable(ps, fields, key, table);
                }else if(object instanceof TableField) {
                    TableField tables = (TableField) object;
                    fillTables(ps, fields, key, tables);
                }else if(object instanceof RichField) {
                    fillRichText(ps, fields, key, (RichField) object);
                }else if(object instanceof PdfField){
                    PdfField pdf = (PdfField)object;
                    fillPdf(ps, fields, key, pdf.getPdfs());
                }else{
                    String value = CommonUtils.evalString(object);
                    fields.setField(key, value);
                }
            }
        } catch (Exception e) {
            log.error("fill data exception", e);
        }
    }

data各参数类型定义如下:

/**
 * pdf图片域
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ImageField {
    private List<byte[]> images = Lists.newLinkedList();

    public ImageField(byte[] image){
        if(images == null) images = Lists.newLinkedList();
        images.add(image);
    }
}

/**
 * 表格
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class Table<T> {
    /**
     * 表头
     */
    private Map<String, Cell> head;
    /**
     * 数据列表
     */
    private List<T> data;
    /**
     * 表格列宽比例
     */
    private float[] colWidthRatio;
    /**
     * 是否显示边框
     */
    private boolean borderFlag;

    private BaseColor borderColor;

    @Builder.Default
    private float borderWidth = -1f;

    private List<String> footnotes;

    /**
     * 单元格信息
     * @param <T>
     */
    @Data
    @Builder
    public static class Cell<T> {
        private T value;
        @Builder.Default
        private int colNum = 1;
        private Font font; //字体
        /**
         * 位置 如 {@link com.itextpdf.text.Element#ALIGN_CENTER}
         */
        @Builder.Default
        private int align = Element.ALIGN_CENTER;
        private BaseColor backgroundColor; //背景色
        private BaseColor borderColor;
        @Builder.Default
        private float paddingLeft = 2;
        @Builder.Default
        private float paddingRight = 2;
        private Float height;
    }
}

/**
 * 表格列表
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class TableField {
    private List<Table> tables = Lists.newLinkedList();

    public TableField(Table... table){
        if(tables == null) tables = Lists.newLinkedList();
        if (ArrayUtil.isNotEmpty(table)) {
            tables.addAll(Arrays.stream(table).collect(Collectors.toList()));
        }
    }
}

/**
 * 文本
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class RichField {
    private List<String> texts;

    public RichField(String text){
        if(texts == null) texts = Lists.newLinkedList();
        texts.add(text);
    }
}

/**
 * pdf文件
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class PdfField {
    private List<PDF> pdfs = Lists.newLinkedList();

    public PdfField(PDF pdf){
        if(pdfs == null) pdfs = Lists.newLinkedList();
        pdfs.add(pdf);
    }
}

接下来看各参数类型数据填充方式实现

    /**
     * 填充图片数据
     * @param ps
     * @param fields
     * @param field
     * @param images
     */
    private static void fillImage(PdfStamper ps, AcroFields fields, String field, List<byte[]> images){
        if(CollUtil.isEmpty(images)) return;
        List<AcroFields.FieldPosition> photograph = fields.getFieldPositions(field);
        if(CollUtil.isNotEmpty(photograph)){

            // 页面大小
            PdfReader reader = ps.getReader();
            Rectangle pageSize = reader.getPageSize(1);

            // 当前页
            int currentPageNum = photograph.get(0).page;
            PdfContentByte content = ps.getOverContent(currentPageNum);

            // 字段域大小
            Rectangle rectangle = photograph.get(0).position;

            float totalWidth = rectangle.getRight() - rectangle.getLeft() - 1;
            PdfPTable pdfPTable = createTable(null, width(totalWidth, new float[]{1}), false);
            for (byte[] data : images) {
                try {
                    Image image = Image.getInstance(data, true);
                    // 通过这个比例来调整图片高度
                    float width = rectangle.getWidth()>image.getScaledWidth()?image.getScaledWidth():rectangle.getWidth();
                    float height = pageSize.getHeight()>image.getScaledHeight()?image.getScaledHeight():pageSize.getHeight();
                    float ratio1 = Arith.div(width, rectangle.getWidth());
                    float ratio2 = Arith.div(height, rectangle.getHeight());
                    //log.info(String.format("field:%s width:%f height:%f ratio1:%f ratio2:%f", field, width, height, ratio1, ratio2));
                    float ratio = ratio1 > ratio2 ? ratio2 : ratio1;
                    image.scaleToFit(new Rectangle(Arith.mul(width, ratio), Arith.mul(height, ratio)));
                    image.setBorder(1);
                    // 判断是否要新增页 并且写入数据
                    float resultHeight = pdfPTable.getTotalHeight()+image.getScaledHeight();
                    if(resultHeight >= pageSize.getHeight()){
                        ps.insertPage(currentPageNum+1, reader.getPageSizeWithRotation(1));
                        PdfContentByte canvas = ps.getOverContent(currentPageNum + 1);
                        PdfPTable newTable = createTable(null, width(totalWidth, new float[]{1}), false);
                        image.setAbsolutePosition(rectangle.getLeft(), rectangle.getBottom());
                        newTable.addCell(createImageCell(image));
                        image.setAbsolutePosition(rectangle.getLeft(), image.getHeight());
                        newTable.writeSelectedRows(0, -1, rectangle.getLeft(), rectangle.getTop(), canvas);
                        currentPageNum += 1;
                    }else {
                        image.setAbsolutePosition(rectangle.getLeft(), rectangle.getBottom());
                        pdfPTable.addCell(createImageCell(image));
                    }
                } catch (BadElementException | IOException e){
                    log.error("fill image exception", e);
                } catch (DocumentException e) {
                    e.printStackTrace();
                }
            };
            pdfPTable.writeSelectedRows(0, -1, rectangle.getLeft(), rectangle.getTop(), content);
        }
    }
    /**
     * 往指定域生成多张表格,注意每个表上下需要相隔一定距离
     * @param ps
     * @param fields
     * @param field
     * @param tables
     * @param <T>
     */
    private static <T> void fillTables(PdfStamper ps, AcroFields fields, String field, TableField tables) {
        List<AcroFields.FieldPosition> photograph = fields.getFieldPositions(field);
        // 判断字段域是否有配置
        if (CollUtil.isNotEmpty(photograph)) {
            try {
                PdfReader reader = ps.getReader();
                // 页面位置
                Rectangle pageSize = reader.getPageSize(1);

                // 组件位置
                Rectangle rectangle = photograph.get(0).position;
                // 组件的TOP值
                float fieldTop = rectangle.getTop();
                // 当前页
                int currentPageNum = photograph.get(0).page;

                PdfContentByte content = ps.getOverContent(currentPageNum);

                float maxHeight = fieldTop - pageSize.getBottom()- 108;

                float totalWidth = rectangle.getRight() - rectangle.getLeft() - 1;
                for (Table<T> tableData : tables.getTables()) {
                    PdfPTable table = createTable(tableData.getHead(), width(totalWidth, tableData.getColWidthRatio()),
                            tableData.isBorderFlag(), tableData.getBorderColor(), tableData.getBorderWidth());
                    for (int i=0, row=tableData.getData().size(); i<row; i++){
                        T data = tableData.getData().get(i);
                        if(tableData.getHead() == null || tableData.getHead().isEmpty()){
                            Map<String, Object> map = BeanUtils.transBeanToMap(data);
                            for (Map.Entry<String, Object> entry : map.entrySet()){
                                PdfPCell cell = createCell(false, entry.getValue(), bodyFontNormal, Element.ALIGN_CENTER,
                                        null, tableData.getBorderColor(), tableData.getBorderWidth());
                                if (cell != null) {
                                    table.addCell(cell);
                                }
                            }
                        } else {
                            for (String fieldName : tableData.getHead().keySet()) {
                                PdfPCell cell = createCell(true, BeanUtils.getProperties(data, fieldName), bodyFontNormal,
                                        Element.ALIGN_CENTER, null, tableData.getBorderColor(), tableData.getBorderWidth());
                                if (cell != null) {
                                    table.addCell(cell);
                                }
                            }
                        }
                        if (table.getTotalHeight() > maxHeight) {
                            // 如果表格行数小于2,则创建新页,表格不再创建
                            table.writeSelectedRows(0, -1, rectangle.getLeft(), fieldTop, content);
                            // 插入一新页 并重新创建一个表格
                            if ((i+1) <= row) {
                                ps.insertPage(currentPageNum + 1, reader.getPageSizeWithRotation(1));
                                content = ps.getOverContent(currentPageNum + 1);
                            }
                            if ((i+1) < row) {
                                table = createTable(tableData.getHead(), width(totalWidth, tableData.getColWidthRatio()),
                                        tableData.isBorderFlag(), tableData.getBorderColor(), tableData.getBorderWidth());
                            } else {
                                table = null;
                            }
                            currentPageNum += 1;
                            fieldTop = pageSize.getTop() - 36;
                            maxHeight = fieldTop - pageSize.getBottom()- 72;
                        }
                    }
                    if (table != null) {
                        table.writeSelectedRows(0, -1, rectangle.getLeft(), fieldTop, content);
                        fieldTop = fieldTop - table.getTotalHeight() - 18;
                        maxHeight = fieldTop - pageSize.getBottom() - 72;
                    }

                    if (tableData.getFootnotes() != null) {
                        content.saveState();
                        content.beginText();
                        // 页脚的页码 展示
                        String footnote = tableData.getFootnotes().stream().collect(Collectors.joining());
                        Phrase phrase = new Phrase(footnote, bodyFootnoteBold);
                        // 页码的 横轴 坐标 居中
                        float x = (pageSize.getLeft() + pageSize.getRight()) / 2;
                        // 页码的 纵轴 坐标
                        float y = pageSize.getBottom() + 36;
                        // 添加文本内容,进行展示页码
                        ColumnText.showTextAligned(content, Element.ALIGN_CENTER, phrase, x, y, 0);
                        content.endText();
                        content.restoreState();
                    }
                }
            } catch (Exception e) {

            }
        }
    }
   /**
     * 填充段落
     * @param ps
     * @param fields
     * @param field
     * @param value
     */
    private static void fillRichText(PdfStamper ps, AcroFields fields, String field, RichField value){
        if(value == null || CollUtil.isEmpty(value.getTexts())) return;
        List<AcroFields.FieldPosition> photograph = fields.getFieldPositions(field);
        if(CollUtil.isNotEmpty(photograph)){
            try {
                Rectangle rectangle = photograph.get(0).position;

                PdfContentByte content = ps.getOverContent((int) photograph.get(0).page);
                float totalWidth = rectangle.getRight() - rectangle.getLeft() - 1;
                PdfPTable pdfPTable = createTable(null, width(totalWidth, new float[]{1}), false);
                value.getTexts().forEach(text -> {
                    pdfPTable.addCell(createParagraphCell(text));
                });
                pdfPTable.writeSelectedRows(0, -1, rectangle.getLeft(), rectangle.getTop(), content);
            } catch (Exception e){
                log.error("fill richtext exception", e);
            }
        }
    }
   /**
     * 填充PDF文档
     * @param ps
     * @param fields
     * @param field
     * @param pdfs
     */
    private static void fillPdf(PdfStamper ps, AcroFields fields, String field, List<PDF> pdfs) {
        if(CollUtil.isEmpty(pdfs)) return;
        List<AcroFields.FieldPosition> photograph = fields.getFieldPositions(field);
        if(CollUtil.isNotEmpty(photograph)){
            // 页面大小
            PdfReader reader = ps.getReader();
            PdfWriter writer = ps.getWriter();
            Rectangle pageSize = reader.getPageSize(1);
            // 当前页
            int currentPageNum = photograph.get(0).page;
            boolean first = true;
            for (PDF pdf : pdfs) {
                try {
                    PdfReader pdfReader = new PdfReader(pdf.getData());
                    for (int i=1,num=pdfReader.getNumberOfPages();i<=num;i++) {
                        if(!first) {
                            ps.insertPage(currentPageNum, pageSize);
                        }
                        PdfContentByte canvas = ps.getOverContent(currentPageNum);
                        PdfImportedPage page = writer.getImportedPage(pdfReader, i);
                        float scaleX = pageSize.getWidth() / page.getWidth();
                        float scaleY = pageSize.getHeight() / page.getHeight();
                        canvas.addTemplate(page, scaleX, 0, 0, scaleY, 0, 0);
                        currentPageNum += 1;
                        first = false;
                    }
                } catch (Exception e) {
                    log.error("fill pdf exception", e);
                }
            }
        }
    }

其他一些方法:

   private static float[] width(float totalWidth, float[] colWidthRatio) {
        float[] result = new float[colWidthRatio.length];
        for (int i=0; i<colWidthRatio.length; i++){
            result[i] = Arith.mul(totalWidth, colWidthRatio[i]);
        }
        return result;
    }

    /**
     * 创建表格
     * @param head <字段名,字段中文名>表头信息
     * @param width 列宽
     * @param borderFlag 边框
     * @param borderColor 边框颜色
     * @param borderWidth 边框宽度
     * @return
     */
    private static PdfPTable createTable(Map<String, Table.Cell> head, float[] width, boolean borderFlag, BaseColor borderColor, float borderWidth) {
        PdfPTable pdfPTable = new PdfPTable(width.length);
        try {
            pdfPTable.setSplitLate(false);
            pdfPTable.setSplitRows(true);
            pdfPTable.setTotalWidth(width);
            pdfPTable.setLockedWidth(true);
            pdfPTable.setHorizontalAlignment(Element.ALIGN_CENTER);
            pdfPTable.getDefaultCell().setBorder(borderFlag?1:0);
            if (borderWidth > -1) {
                pdfPTable.getDefaultCell().setBorderWidth(borderWidth);
            }
            if (borderColor != null) {
                pdfPTable.getDefaultCell().setBorderColor(borderColor);
            }
            if(head != null && !head.isEmpty()) {
                for (Table.Cell title : head.values()) {
                    PdfPCell pdfPCell = createCell(true, title.getValue(), title.getFont(), Element.ALIGN_CENTER,
                            title.getBackgroundColor(), title.getBorderColor() == null ? borderColor :title.getBorderColor(), borderWidth);
                    if(pdfPCell != null) {
                        pdfPTable.addCell(pdfPCell);
                    }
                }
            }
        } catch (Exception e) {
            log.error("create table exception", e);
        }
        return pdfPTable;
    }

    /**
     * 创建表格
     * @param head <字段名,字段中文名>表头信息
     * @param width 列宽
     * @param borderFlag 边框
     * @return
     */
    private static PdfPTable createTable(Map<String, Table.Cell> head, float[] width, boolean borderFlag) {
        return createTable(head, width, borderFlag, null, -1);
    }

    private static PdfPCell createParagraphCell(String text){
        Paragraph paragraph = new Paragraph();
        paragraph.setFirstLineIndent(12f);
        paragraph.setFont(paragraphFont);
        paragraph.add(text);
        PdfPCell cell = new PdfPCell(paragraph);
        cell.setBorder(0);
        //cell.setPaddingTop(15.0f);
        //cell.setPaddingBottom(8.0f);
        cell.setLeading(2.25f, 2.25f);
        return cell;
    }

    private static PdfPCell createImageCell(Image image){
        PdfPCell cell = new PdfPCell(image);
        cell.setBorder(0);
        //cell.setPaddingTop(15.0f);
        //cell.setPaddingBottom(8.0f);
        cell.setLeading(0, 3.25f);
        return cell;
    }

    /**
     * 创建表格单元格
     * @param value 单元格值,支持文本和图片
     * @param font 字体
     * @param align 对齐方式
     * @param background 背景色,为空不设置
     * @param <T>
     * @return
     */
    private static <T> PdfPCell createCell(boolean hasHead, T value, Font font, int align, BaseColor background, BaseColor borderColor, float borderWidth) {
        PdfPCell cell = new PdfPCell();
        if (hasHead) {
            if (value == null) return null;
        } else {
            if (value == null) {
                cell.setBorder(0);
                return cell;
            }
        }
        cell.setVerticalAlignment(cell.ALIGN_MIDDLE);
        cell.setHorizontalAlignment(align);
        cell.setNoWrap(false);
        if (background != null) cell.setBackgroundColor(background);
        if (borderColor != null) cell.setBorderColor(borderColor);
        if (borderWidth > -1) cell.setBorderWidth(borderWidth);
        if (value instanceof Image) {
            cell.setImage((Image) value);
        } else if (value instanceof Table.Cell) {
            Table.Cell column = (Table.Cell) value;
            cell.setColspan(column.getColNum());
            if (column.getHeight() != null && column.getHeight() > 0) {
                cell.setFixedHeight(column.getHeight());
            }
            if (column.getBackgroundColor() != null) {
                cell.setBackgroundColor(column.getBackgroundColor());
            }
            if (column.getBorderColor() != null) {
                cell.setBorderColor(column.getBorderColor());
            }
            if (column.getValue() == null) {
                cell.setBorder(0);
                return cell;
            }
            if (column.getValue() instanceof Image) {
                cell.setImage((Image) value);
            } else {
                //cell.setPhrase(new Phrase(CommonUtils.evalString(column.getValue()), column.getFont()));
                Paragraph paragraph = new Paragraph(CommonUtils.evalString(column.getValue()), column.getFont());
                paragraph.setAlignment(Paragraph.ALIGN_CENTER);
                if (column.getAlign() > -1) {
                    paragraph.setAlignment(column.getAlign());
                }
                cell.setPaddingLeft(column.getPaddingLeft());
                cell.setPaddingRight(column.getPaddingRight());
                cell.addElement(paragraph);
            }
        } else {
            Paragraph paragraph = new Paragraph(CommonUtils.evalString(value), font);
            paragraph.setAlignment(Paragraph.ALIGN_CENTER);
            cell.addElement(paragraph);
        }
        cell.setPaddingTop(0f);
        cell.setPaddingBottom(8f);
        cell.setLeading(0, 1.4f);
        return cell;
    }

    /**
     * 给PDF文档追加页码
     * @param pdf
     */
    private static void addPageNum(PDF pdf, String password) {
        ByteArrayOutputStream bos = null;
        PdfStamper pdfStamper = null;
        try {
            PdfReader pdfReader = new PdfReader(pdf.getData(), password.getBytes());
            bos = new ByteArrayOutputStream();
            pdfStamper = new PdfStamper(pdfReader, bos);
            Rectangle pageSize = pdfReader.getPageSize(1);
            for (int i=1,num=pdfReader.getNumberOfPages();i<=num;i++) {
                PdfContentByte page = pdfStamper.getOverContent(i);
                page.saveState();
                page.beginText();
                // 页脚的页码 展示
                String footerNum = String.format("%d", i);
                Phrase phrase = new Phrase(footerNum, bodyFontNormal);
                // 页码的 横轴 坐标 居中
                float x = (pageSize.getLeft() + pageSize.getRight()) / 2;
                // 页码的 纵轴 坐标
                float y = pageSize.getBottom() + 36;
                // 添加文本内容,进行展示页码
                ColumnText.showTextAligned(page, Element.ALIGN_CENTER, phrase, x, y, 0);
                page.endText();
                page.restoreState();
            }
            // 必须要调佣这个,否则文档不会生成
            pdfStamper.setFormFlattening(true);
            pdfStamper.close();
            pdf.setData(bos.toByteArray());
        } catch (Exception e) {
            log.error("generate page num failed. IOException[{}]", e.getMessage());
        } finally {
            try {
                if(pdfStamper != null) pdfStamper.close();
                if(bos != null) bos.close();
            } catch (Exception e) {
                log.error("stream close exception, [{}]", e.getMessage());
            }
        }
    }

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值