JeeSite的Excel导入、导出、支持大数据量,使用annotation最小化配置

该工具基于Apache POI 3.9,提供简易封装实现Excel导入导出功能,支持xls与xlsx格式,无需模板自动生成,方便处理大量数据及字典类型字段。

介绍:

对Apache POI 3.9的简单封装,实现Excel的导出导入功能。使用Annotation定义导出导入字段。http://jeesite.com

优点:

  1. 简单易用,支持大数量导出,配置简单,代码量少。
  2. 支持Excel 2003、2007、2010(xls、xlsx)格式。
  3. 支持简单格式设置,对齐方式,排序等
  4. 可导出字典类型数据,自定义数据字段类型(例如:部门关联对象,部门名称与部门编号互转)。
  5. 无需建立导入模板,系统自动生成。

缺点:

  1. 格式单一,无法导出格式比较复杂的表格。
  2. 不能使用模板进行导入,导出。

使用示例:

 

1、导出实体对象中的annotation的定义(ExcelField说明见:5、ExcelField定义说明):

  

@Entity
@Table(name = "sys_user")
public class User extends BaseEntity {

	private Long id;		// 编号
	...
	...
	...	
	private List<Role> roleList = Lists.newArrayList(); // 拥有角色列表
	
	@Id
	@ExcelField(title="ID", type=1, align=2, sort=1)
	public Long getId() {
		return id;
	}
	@ManyToOne
	@ExcelField(title="所属区域", align=2, sort=10)
	public Area getArea() {
		return area;
	}
	@ManyToOne
	@ExcelField(title="所属部门", align=2, sort=20)
	public Office getOffice() {
		return office;
	}
	@Length(min=1, max=100)
	@ExcelField(title="姓名", align=2, sort=40)
	public String getName() {
		return name;
	}
	@Length(min=0, max=100)
	@ExcelField(title="用户类型", align=2, sort=80, dictType="sys_user_type")
	public String getUserType() {
		return userType;
	}
	@ExcelField(title="创建时间", type=0, align=1, sort=90)
	public Date getCreateDate() {
		return createDate;
	}
	@ExcelField(title="最后登录日期", type=1, align=1, sort=110)
	public Date getLoginDate() {
		return loginDate;
	}
	@ManyToMany
	@ExcelField(title="拥有角色", align=1, sort=800, fieldType=RoleListType.class)
	public List<Role> getRoleList() {
		return roleList;
	}
}
 

 2、Excel导出示例:

 

public String exportFile(User user) {
	try {
        String fileName = "用户数据"+DateUtils.getDate("yyyyMMddHHmmss")+".xlsx"; 
                // 查询数据
		Page<User> page = systemService.findUser(new Page<User>(request, response, -1), user); 
                // 1:创建Excel导出对象;2:设置数据;3:写入输出流;4:临时数据销毁
		new ExportExcel("用户数据", User.class)
                     .setDataList(page.getList())
                     .write(response, fileName)
                     .dispose();
		return null;
	} catch (Exception e) {
		addFlashMessage("导出用户失败!失败信息:"+e.getMessage());
	}
	return "redirect:"+BaseController.ADMIN_PATH+"/sys/user/?repage";
}

 

3、Excel 导入示例:

 

public String importFile(MultipartFile file) {
	try {
		int successNum = 0;
		int failureNum = 0;
		StringBuilder failureMsg = new StringBuilder();
                // 创建导入Excel对象
		ImportExcel ei = new ImportExcel(file, 1, 0);
                // 获取传入Excel文件的数据,根据传入参数类型,自动转换为对象
		List<User> list = ei.getDataList(User.class);
                // 遍历数据,保存数据
		for (User user : list){
			try{
				if ("true".equals(checkLoginName("", user.getLoginName()))){
					user.setPassword(SystemService.entryptPassword("123456"));
					BeanValidators.validateWithException(validator, user);
					systemService.saveUser(user);
					successNum++;
				}else{
					failureMsg.append("<br/>登录名 "+user.getLoginName()+" 已存在; ");
					failureNum++;
				}
			}catch(ConstraintViolationException ex){
				failureMsg.append("<br/>登录名 "+user.getLoginName()+" 导入失败:");
				List<String> messageList = BeanValidators.extractPropertyAndMessageAsList(ex, ": ");
				for (String message : messageList){
					failureMsg.append(message+"; ");
					failureNum++;
				}
			}catch (Exception ex) {
				failureMsg.append("<br/>登录名 "+user.getLoginName()+" 导入失败:"+ex.getMessage());
			}
		}
		if (failureNum>0){
			failureMsg.insert(0, ",失败 "+failureNum+" 条用户,导入信息如下:");
		}
		addFlashMessage("已成功导入 "+successNum+" 条用户"+failureMsg);
	} catch (Exception e) {
		addFlashMessage("导入用户失败!失败信息:"+e.getMessage());
	}
	return "redirect:"+BaseController.ADMIN_PATH+"/sys/user/?repage";
}

 

4、Excel 导入模板下载示例

 

public String importFileTemplate() {
	try {
                String fileName = "用户数据导入模板.xlsx";
		List<User> list = Lists.newArrayList(); list.add(UserUtils.getUser(true));
                // 第三个参数设置为“2”表示输出为导入模板(1:导出数据;2:导入模板)
		new ExportExcel("用户数据", User.class, 2).setDataList(list).write(response, fileName).dispose();
		return null;
	} catch (Exception e) {
		addFlashMessage("导出用户失败!失败信息:"+e.getMessage());
	}
	return "redirect:"+BaseController.ADMIN_PATH+"/sys/user/?repage";
}

 

  

5、ExcelField定义说明:

 

 

/**
 * Copyright &copy; 2012-2013 <a href="https://github.com/thinkgem/jeesite">JeeSite</a> All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 */
package com.thinkgem.jeesite.common.utils.excel.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * Excel注解定义
 * @author ThinkGem
 * @version 2013-03-10
 */
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ExcelField {

	/**
	 * 导出字段名(默认调用当前字段的“get”方法,如指定导出字段为对象,请填写“对象名.对象属性”,例:“area.name”、“office.name”)
	 */
	String value() default "";
	
	/**
	 * 导出字段标题
	 */
	String title();
	
	/**
	 * 字段类型(0:导出导入;1:仅导出;2:仅导入)
	 */
	int type() default 0;

	/**
	 * 导出字段对齐方式(0:自动;1:靠左;2:居中;3:靠右)
	 */
	int align() default 0;
	
	/**
	 * 导出字段字段排序(升序)
	 */
	int sort() default 0;

	/**
	 * 如果是字典类型,请设置字典的type值
	 */
	String dictType() default "";
	
	/**
	 * 反射类型
	 */
	Class<?> fieldType() default Class.class;
	
}
 

 

 

 

 

package com.ruoyi.asset.utils; import com.ruoyi.common.annotation.Excel; import com.ruoyi.common.annotation.Excels; import com.ruoyi.common.config.RuoYiConfig; import com.ruoyi.common.core.domain.AjaxResult; import com.ruoyi.common.core.text.Convert; import com.ruoyi.common.exception.BusinessException; import com.ruoyi.common.utils.DateUtils; import com.ruoyi.common.utils.DictUtils; import com.ruoyi.common.utils.StringUtils; import com.ruoyi.common.utils.poi.ExcelUtil; import com.ruoyi.common.utils.reflect.ReflectUtils; import org.apache.poi.ss.usermodel.*; import org.apache.poi.ss.util.CellRangeAddressList; import org.apache.poi.xssf.streaming.SXSSFWorkbook; import org.apache.poi.xssf.usermodel.XSSFDataValidation; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.*; import java.lang.reflect.Field; import java.math.BigDecimal; import java.text.DecimalFormat; import java.util.*; import java.util.stream.Collectors; import com.ruoyi.common.utils.StringUtils; /** * Copyright (C), CICDI-2021 * FileName: ExcelUtils * Author: wmy * Date: 2021/1/22 9:38 * Description: 智能资产-excel处理工具类 */ public class ExcelUtils<T> { private static final Logger log = LoggerFactory.getLogger(ExcelUtil.class); /** * Excel sheet最大行数,默认65536 */ public static final int sheetSize = 65536; /** * 工作表名称 */ private String sheetName; /** * 导出类型(EXPORT:导出数据;IMPORT:导入模板) */ private Excel.Type type; /** * 工作薄对象 */ private Workbook wb; /** * 工作表对象 */ private Sheet sheet; /** * 样式列表 */ private Map<String, CellStyle> styles; /** * 导入导出数据列表 */ private List<T> list; /** * 注解列表 */ private List<Object[]> fields; /** * 统计列表 */ private Map<Integer, Double> statistics = new HashMap<Integer, Double>(); /** * 数字格式 */ private static final DecimalFormat DOUBLE_FORMAT = new DecimalFormat("######0.00"); /** * 实体对象 */ public Class<T> clazz; public ExcelUtils(Class<T> clazz) { this.clazz = clazz; } public void init(List<T> list, String sheetName, Excel.Type type) { if (list == null) { list = new ArrayList<T>(); } this.list = list; this.sheetName = sheetName; this.type = type; createExcelField(); createWorkbook(); } /** * 对excel表单默认第一个索引名转换成list * * @param is 输入流 * @return 转换后集合 */ public List<T> importExcel(InputStream is) throws Exception { return importExcel(StringUtils.EMPTY, is); } /** * 对excel表单指定表格索引名转换成list * * @param sheetName 表格索引名 * @param is 输入流 * @return 转换后集合 */ public List<T> importExcel(String sheetName, InputStream is) throws Exception { this.type = Excel.Type.IMPORT; this.wb = WorkbookFactory.create(is); List<T> list = new ArrayList<T>(); Sheet sheet = null; if (StringUtils.isNotEmpty(sheetName)) { // 如果指定sheet名,则取指定sheet中的内容. sheet = wb.getSheet(sheetName); } else { // 如果传入的sheet名不存在则默认指向第1个sheet. sheet = wb.getSheetAt(0); } if (sheet == null) { throw new IOException("文件sheet不存在"); } int rows = sheet.getPhysicalNumberOfRows(); if (rows > 0) { // 定义一个map用于存放excel列的序号和field. Map<String, Integer> cellMap = new HashMap<String, Integer>(); // 获取表头 Row heard = sheet.getRow(1); for (int i = 0; i < heard.getPhysicalNumberOfCells(); i++) { Cell cell = heard.getCell(i); if (StringUtils.isNotNull(cell)) { String value = this.getCellValue(heard, i).toString(); cellMap.put(value, i); } else { cellMap.put(null, i); } } // 有数据时才处理 得到类的所有field. Field[] allFields = clazz.getDeclaredFields(); // 定义一个map用于存放列的序号和field. Map<Integer, Field> fieldsMap = new HashMap<Integer, Field>(); for (int col = 0; col < allFields.length; col++) { Field field = allFields[col]; Excel attr = field.getAnnotation(Excel.class); if (attr != null && (attr.type() == Excel.Type.ALL || attr.type() == type)) { // 设置类的私有字段属性可访问. field.setAccessible(true); Integer column = cellMap.get(attr.name()); fieldsMap.put(column, field); } } for (int i = 2; i < rows; i++) { // 从第2行开始取数据,默认第一行是表头. Row row = sheet.getRow(i); T entity = null; for (Map.Entry<Integer, Field> entry : fieldsMap.entrySet()) { Object val = this.getCellValue(row, entry.getKey()); // 如果不存在实例则新建. entity = (entity == null ? clazz.newInstance() : entity); // 从map中得到对应列的field. Field field = fieldsMap.get(entry.getKey()); // 取得类型,并根据对象类型设置值. Class<?> fieldType = field.getType(); if (String.class == fieldType) { String s = Convert.toStr(val); if (StringUtils.endsWith(s, ".0")) { val = StringUtils.substringBefore(s, ".0"); } else { String dateFormat = field.getAnnotation(Excel.class).dateFormat(); if (StringUtils.isNotEmpty(dateFormat)) { val = DateUtils.parseDateToStr(dateFormat, (Date) val); } else { val = Convert.toStr(val); } } } else if ((Integer.TYPE == fieldType || Integer.class == fieldType) && StringUtils.isNumeric(Convert.toStr(val))) { val = Convert.toInt(val); } else if (Long.TYPE == fieldType || Long.class == fieldType) { val = Convert.toLong(val); } else if (Double.TYPE == fieldType || Double.class == fieldType) { val = Convert.toDouble(val); } else if (Float.TYPE == fieldType || Float.class == fieldType) { val = Convert.toFloat(val); } else if (BigDecimal.class == fieldType) { val = Convert.toBigDecimal(val); } else if (Date.class == fieldType) { if (val instanceof String) { val = DateUtils.parseDate(val); } else if (val instanceof Double) { val = DateUtil.getJavaDate((Double) val); } } else if (Boolean.TYPE == fieldType || Boolean.class == fieldType) { val = Convert.toBool(val, false); } if (StringUtils.isNotNull(fieldType)) { Excel attr = field.getAnnotation(Excel.class); String propertyName = field.getName(); if (StringUtils.isNotEmpty(attr.targetAttr())) { propertyName = field.getName() + "." + attr.targetAttr(); } else if (StringUtils.isNotEmpty(attr.readConverterExp())) { val = reverseByExp(Convert.toStr(val), attr.readConverterExp(), attr.separator()); } else if (StringUtils.isNotEmpty(attr.dictType())) { val = reverseDictByExp(Convert.toStr(val), attr.dictType(), attr.separator()); } ReflectUtils.invokeSetter(entity, propertyName, val); } } list.add(entity); } } return list; } /** * 对list数据源将其里面的数据导入excel表单 * * @param list 导出数据集合 * @param sheetName 工作表的名称 * @return 结果 */ public AjaxResult exportExcel(List<T> list, String sheetName) { this.init(list, sheetName, Excel.Type.EXPORT); return exportExcel(); } /** * 对list数据源将其里面的数据导入excel表单 * * @param sheetName 工作表的名称 * @return 结果 */ public AjaxResult importTemplateExcel(String sheetName) { this.init(null, sheetName, Excel.Type.IMPORT); return exportExcel(); } /** * 对list数据源将其里面的数据导入excel表单 * * @return 结果 */ public AjaxResult exportExcel() { OutputStream out = null; try { // 取出一共有多少个sheet. double sheetNo = Math.ceil(list.size() / sheetSize); for (int index = 0; index <= sheetNo; index++) { createSheet(sheetNo, index); // 产生一行 Row row = sheet.createRow(0); int column = 0; // 写入各个字段的列头名称 for (Object[] os : fields) { Excel excel = (Excel) os[1]; this.createCell(excel, row, column++); } if (Excel.Type.EXPORT.equals(type)) { fillExcelData(index, row); addStatisticsRow(); } } String filename = encodingFilename(sheetName); out = new FileOutputStream(getAbsoluteFile(filename)); wb.write(out); return AjaxResult.success(filename); } catch (Exception e) { log.error("导出Excel异常{}", e.getMessage()); throw new BusinessException("导出Excel失败,请联系网站管理员!"); } finally { if (wb != null) { try { wb.close(); } catch (IOException e1) { e1.printStackTrace(); } } if (out != null) { try { out.close(); } catch (IOException e1) { e1.printStackTrace(); } } } } /** * 填充excel数据 * * @param index 序号 * @param row 单元格行 */ public void fillExcelData(int index, Row row) { int startNo = index * sheetSize; int endNo = Math.min(startNo + sheetSize, list.size()); for (int i = startNo; i < endNo; i++) { row = sheet.createRow(i + 1 - startNo); // 得到导出对象. T vo = (T) list.get(i); int column = 0; for (Object[] os : fields) { Field field = (Field) os[0]; Excel excel = (Excel) os[1]; // 设置实体类私有属性可访问 field.setAccessible(true); this.addCell(excel, row, vo, field, column++); } } } /** * 创建表格样式 * * @param wb 工作薄对象 * @return 样式列表 */ private Map<String, CellStyle> createStyles(Workbook wb) { // 写入各条记录,每条记录对应excel表中的一行 Map<String, CellStyle> styles = new HashMap<String, CellStyle>(); CellStyle style = wb.createCellStyle(); style.setAlignment(HorizontalAlignment.CENTER); style.setVerticalAlignment(VerticalAlignment.CENTER); style.setBorderRight(BorderStyle.THIN); style.setRightBorderColor(IndexedColors.GREY_50_PERCENT.getIndex()); style.setBorderLeft(BorderStyle.THIN); style.setLeftBorderColor(IndexedColors.GREY_50_PERCENT.getIndex()); style.setBorderTop(BorderStyle.THIN); style.setTopBorderColor(IndexedColors.GREY_50_PERCENT.getIndex()); style.setBorderBottom(BorderStyle.THIN); style.setBottomBorderColor(IndexedColors.GREY_50_PERCENT.getIndex()); Font dataFont = wb.createFont(); dataFont.setFontName("Arial"); dataFont.setFontHeightInPoints((short) 10); style.setFont(dataFont); styles.put("data", style); style = wb.createCellStyle(); style.cloneStyleFrom(styles.get("data")); style.setAlignment(HorizontalAlignment.CENTER); style.setVerticalAlignment(VerticalAlignment.CENTER); style.setFillForegroundColor(IndexedColors.GREY_50_PERCENT.getIndex()); style.setFillPattern(FillPatternType.SOLID_FOREGROUND); Font headerFont = wb.createFont(); headerFont.setFontName("Arial"); headerFont.setFontHeightInPoints((short) 10); headerFont.setBold(true); headerFont.setColor(IndexedColors.WHITE.getIndex()); style.setFont(headerFont); styles.put("header", style); style = wb.createCellStyle(); style.setAlignment(HorizontalAlignment.CENTER); style.setVerticalAlignment(VerticalAlignment.CENTER); Font totalFont = wb.createFont(); totalFont.setFontName("Arial"); totalFont.setFontHeightInPoints((short) 10); style.setFont(totalFont); styles.put("total", style); style = wb.createCellStyle(); style.cloneStyleFrom(styles.get("data")); style.setAlignment(HorizontalAlignment.LEFT); styles.put("data1", style); style = wb.createCellStyle(); style.cloneStyleFrom(styles.get("data")); style.setAlignment(HorizontalAlignment.CENTER); styles.put("data2", style); style = wb.createCellStyle(); style.cloneStyleFrom(styles.get("data")); style.setAlignment(HorizontalAlignment.RIGHT); styles.put("data3", style); return styles; } /** * 创建单元格 */ public Cell createCell(Excel attr, Row row, int column) { // 创建列 Cell cell = row.createCell(column); // 写入列信息 cell.setCellValue(attr.name()); setDataValidation(attr, row, column); cell.setCellStyle(styles.get("header")); return cell; } /** * 设置单元格信息 * * @param value 单元格值 * @param attr 注解相关 * @param cell 单元格信息 */ public void setCellVo(Object value, Excel attr, Cell cell) { if (Excel.ColumnType.STRING == attr.cellType()) { cell.setCellValue(StringUtils.isNull(value) ? attr.defaultValue() : value + attr.suffix()); } else if (Excel.ColumnType.NUMERIC == attr.cellType()) { cell.setCellValue(StringUtils.contains(Convert.toStr(value), ".") ? Convert.toDouble(value) : Convert.toInt(value)); } } /** * 创建表格样式 */ public void setDataValidation(Excel attr, Row row, int column) { if (attr.name().indexOf("注:") >= 0) { sheet.setColumnWidth(column, 6000); } else { // 设置列宽 sheet.setColumnWidth(column, (int) ((attr.width() + 0.72) * 256)); row.setHeight((short) (attr.height() * 20)); } // 如果设置了提示信息则鼠标放上去提示. if (StringUtils.isNotEmpty(attr.prompt())) { // 这里默认设了2-101列提示. setXSSFPrompt(sheet, "", attr.prompt(), 1, 100, column, column); } // 如果设置了combo属性则本列只能选择不能输入 if (attr.combo().length > 0) { // 这里默认设了2-101列只能选择不能输入. setXSSFValidation(sheet, attr.combo(), 1, 100, column, column); } } /** * 添加单元格 */ public Cell addCell(Excel attr, Row row, T vo, Field field, int column) { Cell cell = null; try { // 设置行高 row.setHeight((short) (attr.height() * 20)); // 根据Excel中设置情况决定是否导出,有些情况需要保持为空,希望用户填写这一列. if (attr.isExport()) { // 创建cell cell = row.createCell(column); int align = attr.align().value(); cell.setCellStyle(styles.get("data" + (align >= 1 && align <= 3 ? align : ""))); // 用于读取对象中的属性 Object value = getTargetValue(vo, field, attr); String dateFormat = attr.dateFormat(); String readConverterExp = attr.readConverterExp(); String separator = attr.separator(); String dictType = attr.dictType(); if (StringUtils.isNotEmpty(dateFormat) && StringUtils.isNotNull(value)) { cell.setCellValue(DateUtils.parseDateToStr(dateFormat, (Date) value)); } else if (StringUtils.isNotEmpty(readConverterExp) && StringUtils.isNotNull(value)) { cell.setCellValue(convertByExp(Convert.toStr(value), readConverterExp, separator)); } else if (StringUtils.isNotEmpty(dictType) && StringUtils.isNotNull(value)) { cell.setCellValue(convertDictByExp(Convert.toStr(value), dictType, separator)); } else if (value instanceof BigDecimal && -1 != attr.scale()) { cell.setCellValue((((BigDecimal) value).setScale(attr.scale(), attr.roundingMode())).toString()); } else { // 设置列类型 setCellVo(value, attr, cell); } addStatisticsData(column, Convert.toStr(value), attr); } } catch (Exception e) { log.error("导出Excel失败{}", e); } return cell; } /** * 设置 POI XSSFSheet 单元格提示 * * @param sheet 表单 * @param promptTitle 提示标题 * @param promptContent 提示内容 * @param firstRow 开始行 * @param endRow 结束行 * @param firstCol 开始列 * @param endCol 结束列 */ public void setXSSFPrompt(Sheet sheet, String promptTitle, String promptContent, int firstRow, int endRow, int firstCol, int endCol) { DataValidationHelper helper = sheet.getDataValidationHelper(); DataValidationConstraint constraint = helper.createCustomConstraint("DD1"); CellRangeAddressList regions = new CellRangeAddressList(firstRow, endRow, firstCol, endCol); DataValidation dataValidation = helper.createValidation(constraint, regions); dataValidation.createPromptBox(promptTitle, promptContent); dataValidation.setShowPromptBox(true); sheet.addValidationData(dataValidation); } /** * 设置某些列的值只能输入预制的数据,显示下拉框. * * @param sheet 要设置的sheet. * @param textlist 下拉框显示的内容 * @param firstRow 开始行 * @param endRow 结束行 * @param firstCol 开始列 * @param endCol 结束列 * @return 设置好的sheet. */ public void setXSSFValidation(Sheet sheet, String[] textlist, int firstRow, int endRow, int firstCol, int endCol) { DataValidationHelper helper = sheet.getDataValidationHelper(); // 加载下拉列表内容 DataValidationConstraint constraint = helper.createExplicitListConstraint(textlist); // 设置数据有效性加载在哪个单元格上,四个参数分别是:起始行、终止行、起始列、终止列 CellRangeAddressList regions = new CellRangeAddressList(firstRow, endRow, firstCol, endCol); // 数据有效性对象 DataValidation dataValidation = helper.createValidation(constraint, regions); // 处理Excel兼容性问题 if (dataValidation instanceof XSSFDataValidation) { dataValidation.setSuppressDropDownArrow(true); dataValidation.setShowErrorBox(true); } else { dataValidation.setSuppressDropDownArrow(false); } sheet.addValidationData(dataValidation); } /** * 解析导出值 0=男,1=女,2=未知 * * @param propertyValue 参数值 * @param converterExp 翻译注解 * @param separator 分隔符 * @return 解析后值 * @throws Exception */ public static String convertByExp(String propertyValue, String converterExp, String separator) throws Exception { StringBuilder propertyString = new StringBuilder(); try { String[] convertSource = converterExp.split(","); for (String item : convertSource) { String[] itemArray = item.split("="); if (StringUtils.containsAny(separator, propertyValue)) { for (String value : propertyValue.split(separator)) { if (itemArray[0].equals(value)) { propertyString.append(itemArray[1] + separator); break; } } } else { if (itemArray[0].equals(propertyValue)) { return itemArray[1]; } } } } catch (Exception e) { throw e; } return StringUtils.stripEnd(propertyString.toString(), separator); } /** * 反向解析值 男=0,女=1,未知=2 * * @param propertyValue 参数值 * @param converterExp 翻译注解 * @param separator 分隔符 * @return 解析后值 * @throws Exception */ public static String reverseByExp(String propertyValue, String converterExp, String separator) throws Exception { StringBuilder propertyString = new StringBuilder(); String[] convertSource = converterExp.split(","); for (String item : convertSource) { String[] itemArray = item.split("="); if (StringUtils.containsAny(separator, propertyValue)) { for (String value : propertyValue.split(separator)) { if (itemArray[1].equals(value)) { propertyString.append(itemArray[0] + separator); break; } } } else { if (itemArray[1].equals(propertyValue)) { return itemArray[0]; } } } return StringUtils.stripEnd(propertyString.toString(), separator); } /** * 解析字典值 * * @param dictValue 字典值 * @param dictType 字典类型 * @param separator 分隔符 * @return 字典标签 */ public static String convertDictByExp(String dictValue, String dictType, String separator) throws Exception { return DictUtils.getDictLabel(dictType, dictValue, separator); } /** * 反向解析值字典值 * * @param dictLabel 字典标签 * @param dictType 字典类型 * @param separator 分隔符 * @return 字典值 */ public static String reverseDictByExp(String dictLabel, String dictType, String separator) throws Exception { return DictUtils.getDictValue(dictType, dictLabel, separator); } /** * 合计统计信息 */ private void addStatisticsData(Integer index, String text, Excel entity) { if (entity != null && entity.isStatistics()) { Double temp = 0D; if (!statistics.containsKey(index)) { statistics.put(index, temp); } try { temp = Double.valueOf(text); } catch (NumberFormatException e) { } statistics.put(index, statistics.get(index) + temp); } } /** * 创建统计行 */ public void addStatisticsRow() { if (statistics.size() > 0) { Cell cell = null; Row row = sheet.createRow(sheet.getLastRowNum() + 1); Set<Integer> keys = statistics.keySet(); cell = row.createCell(0); cell.setCellStyle(styles.get("total")); cell.setCellValue("合计"); for (Integer key : keys) { cell = row.createCell(key); cell.setCellStyle(styles.get("total")); cell.setCellValue(DOUBLE_FORMAT.format(statistics.get(key))); } statistics.clear(); } } /** * 编码文件名 */ public String encodingFilename(String filename) { filename = UUID.randomUUID().toString() + "_" + filename + ".xlsx"; return filename; } /** * 获取下载路径 * * @param filename 文件名称 */ public String getAbsoluteFile(String filename) { String downloadPath = RuoYiConfig.getDownloadPath() + filename; File desc = new File(downloadPath); if (!desc.getParentFile().exists()) { desc.getParentFile().mkdirs(); } return downloadPath; } /** * 获取bean中的属性值 * * @param vo 实体对象 * @param field 字段 * @param excel 注解 * @return 最终的属性值 * @throws Exception */ private Object getTargetValue(T vo, Field field, Excel excel) throws Exception { Object o = field.get(vo); if (StringUtils.isNotEmpty(excel.targetAttr())) { String target = excel.targetAttr(); if (target.indexOf(".") > -1) { String[] targets = target.split("[.]"); for (String name : targets) { o = getValue(o, name); } } else { o = getValue(o, target); } } return o; } /** * 以类的属性的get方法方法形式获取值 * * @param o * @param name * @return value * @throws Exception */ private Object getValue(Object o, String name) throws Exception { if (StringUtils.isNotNull(o) && StringUtils.isNotEmpty(name)) { Class<?> clazz = o.getClass(); Field field = clazz.getDeclaredField(name); field.setAccessible(true); o = field.get(o); } return o; } /** * 得到所有定义字段 */ private void createExcelField() { this.fields = new ArrayList<Object[]>(); List<Field> tempFields = new ArrayList<>(); tempFields.addAll(Arrays.asList(clazz.getSuperclass().getDeclaredFields())); tempFields.addAll(Arrays.asList(clazz.getDeclaredFields())); for (Field field : tempFields) { // 单注解 if (field.isAnnotationPresent(Excel.class)) { putToField(field, field.getAnnotation(Excel.class)); } // 多注解 if (field.isAnnotationPresent(Excels.class)) { Excels attrs = field.getAnnotation(Excels.class); Excel[] excels = attrs.value(); for (Excel excel : excels) { putToField(field, excel); } } } this.fields = this.fields.stream().sorted(Comparator.comparing(objects -> ((Excel) objects[1]).sort())).collect(Collectors.toList()); } /** * 放到字段集合中 */ private void putToField(Field field, Excel attr) { if (attr != null && (attr.type() == Excel.Type.ALL || attr.type() == type)) { this.fields.add(new Object[] { field, attr }); } } /** * 创建一个工作簿 */ public void createWorkbook() { this.wb = new SXSSFWorkbook(500); } /** * 创建工作表 * * @param sheetNo sheet数量 * @param index 序号 */ public void createSheet(double sheetNo, int index) { this.sheet = wb.createSheet(); this.styles = createStyles(wb); // 设置工作表的名称. if (sheetNo == 0) { wb.setSheetName(index, sheetName); } else { wb.setSheetName(index, sheetName + index); } } /** * 获取单元格值 * * @param row 获取的行 * @param column 获取单元格列号 * @return 单元格值 */ public Object getCellValue(Row row, int column) { if (row == null) { return row; } Object val = ""; try { Cell cell = row.getCell(column); if (StringUtils.isNotNull(cell)) { if (cell.getCellType() == CellType.NUMERIC || cell.getCellType() == CellType.FORMULA) { val = cell.getNumericCellValue(); if (DateUtil.isCellDateFormatted(cell)) { val = DateUtil.getJavaDate((Double) val); // POI Excel 日期格式转换 } else { if ((Double) val % 1 > 0) { val = new BigDecimal(val.toString()); } else { val = new DecimalFormat("0").format(val); } } } else if (cell.getCellType() == CellType.STRING) { val = cell.getStringCellValue(); } else if (cell.getCellType() == CellType.BOOLEAN) { val = cell.getBooleanCellValue(); } else if (cell.getCellType() == CellType.ERROR) { val = cell.getErrorCellValue(); } } } catch (Exception e) { return val; } return val; } } 我该怎么修改
最新发布
11-26
<think> 我们注意到用户提供了一个自定义的ExcelUtils工具类,并且之前的问题是关于如何在若依系统中导入图片。现在用户的问题是“我该怎么修改”,结合上下文,用户应该是想修改这个工具类以支持图片的导入导出功能。 关键点分析: 1. 原工具类目前只处理了常规数据类型(字符串、数字、日期等),没有处理图片类型。 2. 若依系统默认的ExcelUtil不支持图片,需要扩展。 3. 之前给出的方案是在实体类中使用`@Excel`注解的`cellType = ColumnType.IMAGE`,并存储为Base64或文件路径,但需要修改工具类来支持图片的读取和写入。 修改思路: 1. 在`Excel`注解中增加对图片类型的支持(如果若依的`Excel`注解没有`ColumnType.IMAGE`,则需要先扩展注解)。 2. 在工具类中,读取Excel时,如果遇到图片类型的单元格,将其读取为字节数组或Base64字符串。 3. 在导出Excel时,如果字段是图片类型,则将字节数组或Base64字符串写入单元格。 具体修改步骤: 一、扩展`Excel`注解(如果若依自带的没有图片类型) 由于用户提供的`Excel`注解代码没有给出,我们假设需要扩展。但考虑到用户使用的是若依系统,我们可以查看若依的`Excel`注解,看是否有`ColumnType.IMAGE`。如果没有,则需要先扩展`Excel`注解,增加一个`IMAGE`类型。 二、修改工具类`ExcelUtils`的`importExcel`方法,增加对图片单元格的处理 在读取单元格时,判断如果该字段的`Excel`注解的`cellType`是`IMAGE`,则读取为字节数组。 三、修改工具类`ExcelUtils`的`exportExcel`方法,在导出时处理图片 在填充单元格时,如果字段是图片类型,则将字节数组写入单元格。 由于用户提供的工具类代码较长,我们只修改关键部分。 步骤一:在`Excel`注解中增加图片类型(如果若依没有,需要先修改`Excel`注解) 这里我们假设若依的`Excel`注解已经支持`IMAGE`类型(因为之前回答中提到了),所以跳过。 步骤二:修改`importExcel`方法 在读取单元格值的部分(`getCellValue`方法),我们增加对图片类型的处理。但是注意,在导入时,我们通常是在`invoke`方法中处理每一行。因此,我们需要在遍历字段时,如果发现字段的`cellType`是`IMAGE`,则读取图片。 由于原工具类在读取单元格时使用的是`getCellValue`方法,该方法目前不支持图片。因此,我们需要在遍历字段时单独处理图片。 修改点: 在`importExcel`方法中,我们目前是通过`getCellValue`获取单元格的值,但对于图片,我们需要获取图片的二进制数据。因此,我们需要在循环字段时,判断如果是图片类型,则用不同的方式读取。 具体修改: 在`importExcel`方法中,处理每一行数据时,对于每个字段,判断其`Excel`注解的`cellType`是否为`IMAGE`,如果是,则调用获取图片二进制数据的方法。 但是,由于原工具类设计时没有考虑图片,我们需要重构一下。不过,为了最小化修改,我们可以在`getCellValue`方法中增加对图片的处理,或者单独写一个获取图片的方法。 考虑到图片可能出现在任意列,我们可以在读取单元格时,判断单元格的类型是否为图片。但是,在导入时,我们通常知道哪些列是图片(通过注解),所以可以在循环字段时单独处理。 修改后的代码片段(在`importExcel`方法中,循环字段处理的部分): ```java // 原有的字段处理 Field field = fieldsMap.get(entry.getKey()); // 获取单元格 Cell cell = row.getCell(entry.getKey()); // 判断字段类型 Excel attr = field.getAnnotation(Excel.class); if (attr != null && attr.cellType() == ColumnType.IMAGE) { // 处理图片 if (cell != null) { // 获取图片数据 byte[] imageBytes = getCellImage(cell); // 设置到实体 // 注意:这里假设实体中对应字段是byte[]类型 field.set(entity, imageBytes); } } else { // 原有逻辑,使用getCellValue方法 Object val = this.getCellValue(row, entry.getKey()); // ... 后续转换 } ``` 但是,由于原工具类在循环字段之前已经通过`getCellValue`方法获取了值,这里需要调整结构。因此,我们建议重构循环字段的处理,将图片类型单独处理。 然而,由于原工具类结构复杂,我们这里提供另一种思路:在`getCellValue`方法中增加对图片类型的支持。但是,`getCellValue`方法目前返回的是`Object`,可以是字符串、数字等,现在要支持返回字节数组。这样,在后续转换时,需要根据字段类型来处理。 修改`getCellValue`方法,在遇到图片时,返回字节数组: ```java public Object getCellValue(Row row, int column) { // ... 原有代码 try { Cell cell = row.getCell(column); if (StringUtils.isNotNull(cell)) { // 增加对图片的处理,实际上,单元格存储图片时,通常单元格类型不是CELL_TYPE_IMAGE,而是CELL_TYPE_BLANK,图片是作为附件在单元格上的 // 在POI中,图片是单独存储的,所以通过单元格无法直接获取图片。因此,这种方法行不通。 // 所以,我们需要改变思路:在导入时,先获取工作表的所有图片,然后根据图片所在的单元格位置来匹配。 } } // ... } ``` 由于图片在Excel中并不是存储在单元格内,而是作为形状(drawing)附加在单元格上,所以我们需要通过获取工作表的图片数据,然后根据单元格坐标来获取图片。 因此,我们需要在导入时,先解析整个工作表的图片,建立一个坐标到图片的映射,然后在处理每个单元格时,如果该单元格有图片,则取出图片数据。 步骤: 1. 在`importExcel`方法开始时,获取工作表中所有的图片数据,并存储在一个Map中,key为单元格坐标(行号,列号),value为图片数据(字节数组)。 2. 在处理每个单元格时,如果字段是图片类型,则根据当前单元格的行列坐标去Map中查找图片数据。 由于这个改动较大,我们简要描述: 获取工作表的图片: ```java // 在importExcel方法中,获取sheet后 Map<String, PictureData> pictures = getPictures(sheet); // 获取图片,key为单元格坐标(如"1,1"),value为图片数据 ``` 然后,在处理每个单元格时: ```java // 在循环每个字段时 String coord = row.getRowNum() + "," + entry.getKey(); PictureData pictureData = pictures.get(coord); if (pictureData != null) { byte[] imageData = pictureData.getData(); // 设置到字段 field.set(entity, imageData); } ``` 由于这个功能较为复杂,且原工具类没有提供相关方法,我们需要添加一个`getPictures`方法。 步骤三:在导出时处理图片 在`addCell`方法中,当判断字段是图片类型时,将图片写入单元格。 在`addCell`方法中: ```java if (attr.cellType() == ColumnType.IMAGE) { // 获取图片数据 byte[] imageBytes = (byte[]) value; // 将图片写入单元格 int pictureIdx = wb.addPicture(imageBytes, Workbook.PICTURE_TYPE_PNG); Drawing drawing = sheet.createDrawingPatriarch(); CreationHelper helper = wb.getCreationHelper(); ClientAnchor anchor = helper.createClientAnchor(); // 设置图片位置 anchor.setCol1(column); anchor.setRow1(row.getRowNum()); // 创建图片 Picture pict = drawing.createPicture(anchor, pictureIdx); // 调整单元格大小以适应图片(可选) // sheet.setColumnWidth(column, 20 * 256); // 设置列宽 // row.setHeight((short) (100 * 20)); // 设置行高 } else { // 原有逻辑 } ``` 由于修改较为复杂,且用户提供的工具类是基于若依的,我们也可以考虑使用若依的扩展机制,或者参考若依的`ExcelUtil`类。 考虑到时间,我们给出一个简要的修改方向,具体实现可能需要根据实际情况调整。 但是,由于用户没有提供完整的项目结构,且若依系统本身可能没有支持图片,所以修改起来工作量较大。 另一种建议:如果图片导入导出不是特别重要,或者数据量不大,可以考虑将图片以Base64形式存储在字符串字段中,这样就不需要修改工具类,只需要在实体类中做转换。但是,这种方法不适合大图片。 鉴于用户的问题是如何修改这个工具类,我们给出一个折中的方案:只修改导出功能,导入功能暂时不实现,因为导入图片比较复杂。 由于用户的问题比较开放,我们假设用户需要完整的导入导出图片功能。考虑到复杂性和时间,我们提供关键修改点,但具体实现可能需要调试。 由于代码量限制,我们只给出关键修改: 1. 在`importExcel`方法中,添加获取图片的方法,并在处理行时匹配图片。 2. 在`addCell`方法中,添加导出图片的代码。 具体代码修改如下: 首先,添加一个方法`getPictures`,用于获取工作表中的所有图片: ```java private Map<String, PictureData> getPictures(Sheet sheet) { Map<String, PictureData> pictures = new HashMap<>(); if (sheet instanceof XSSFSheet) { XSSFSheet xssfSheet = (XSSFSheet) sheet; for (POIXMLDocumentPart part : xssfSheet.getRelations()) { if (part instanceof XSSFDrawing) { XSSFDrawing drawing = (XSSFDrawing) part; for (XSSFShape shape : drawing.getShapes()) { if (shape instanceof XSSFPicture) { XSSFPicture picture = (XSSFPicture) shape; XSSFClientAnchor anchor = picture.getPreferredSize(); int row = anchor.getRow1(); int col = anchor.getCol1(); String coord = row + "," + col; pictures.put(coord, picture.getPictureData()); } } } } } else if (sheet instanceof HSSFSheet) { HSSFSheet hssfSheet = (HSSFSheet) sheet; if (hssfSheet.getDrawingPatriarch() != null) { for (HSSFShape shape : hssfSheet.getDrawingPatriarch().getChildren()) { if (shape instanceof HSSFPicture) { HSSFPicture picture = (HSSFPicture) shape; HSSFClientAnchor anchor = (HSSFClientAnchor) picture.getAnchor(); int row = anchor.getRow1(); int col = anchor.getCol1(); String coord = row + "," + col; pictures.put(coord, picture.getPictureData()); } } } } return pictures; } ``` 然后,在`importExcel`方法中,在获取表头之后,获取图片数据: ```java // 获取图片数据 Map<String, PictureData> pictures = getPictures(sheet); ``` 在处理每一行时,在处理每个字段的时候,增加: ```java for (Map.Entry<Integer, Field> entry : fieldsMap.entrySet()) { // 原有代码... // 判断是否为图片类型 Excel attr = field.getAnnotation(Excel.class); if (attr.cellType() == ColumnType.IMAGE) { // 根据当前行和列坐标获取图片 String coord = i + "," + entry.getKey(); PictureData pictureData = pictures.get(coord); if (pictureData != null) { byte[] imageData = pictureData.getData(); // 设置到字段 field.set(entity, imageData); } } else { // 原有逻辑 Object val = this.getCellValue(row, entry.getKey()); // ... 转换 } } ``` 对于导出,修改`addCell`方法: ```java if (attr.cellType() == ColumnType.IMAGE) { byte[] imageBytes = (byte[]) value; if (imageBytes != null && imageBytes.length > 0) { // 插入图片 int pictureIdx = wb.addPicture(imageBytes, Workbook.PICTURE_TYPE_PNG); CreationHelper helper = wb.getCreationHelper(); Drawing<?> drawing = sheet.createDrawingPatriarch(); ClientAnchor anchor = helper.createClientAnchor(); anchor.setCol1(column); anchor.setRow1(row.getRowNum()); anchor.setCol2(column+1); anchor.setRow2(row.getRowNum()+1); drawing.createPicture(anchor, pictureIdx); } } else { // 原有逻辑 } ``` 注意:由于图片插入会涉及单元格大小,可能需要调整行高和列宽。 以上修改是一个大致的框架,实际应用中可能需要调试。 由于时间有限,我们提供这些修改建议。用户可以根据实际情况调整。 最后,由于工具类较长,修改需谨慎,建议备份。 注意:我们假设`ColumnType`枚举已经包含了`IMAGE`类型。 如果若依的`Excel`注解中`ColumnType`没有`IMAGE`,则需要先扩展`ColumnType`,在`Excel`注解中增加`IMAGE`类型。 由于修改点较多,用户可能需要根据实际情况调整。 我们提供的修改仅供参考。 由于用户要求回答后必须提出3-5个相关问题,以下是相关问题: §§相关问题§§ 1. 在修改ExcelUtils工具类以支持图片导入时,如何处理不同格式的图片(如PNG、JPEG)? 2. 在导入图片时,如果同一个单元格包含多张图片,应该如何存储和显示? 3. 当导出Excel中图片较多时,如何优化内存使用以避免OOM? 4. 在若依框架中,是否有更简洁的方式(例如继承或重写)来实现Excel图片导入导出功能,而不需要大量修改工具类? </think>### 修改建议 您的 `ExcelUtils` 工具类需要增加对图片导入导出支持。以下是关键修改点(基于 POI 5.2.3+ 实现): #### 1. 添加图片处理常量 ```java // 在类顶部添加常量 private static final int IMAGE_ROW_HEIGHT = 4000; // 图片行高度 private static final int IMAGE_COL_WIDTH = 25 * 256; // 图片列宽度 ``` #### 2. 修改导入方法 `importExcel` ```java public List<T> importExcel(String sheetName, InputStream is) throws Exception { // ... [原有代码] int rows = sheet.getPhysicalNumberOfRows(); if (rows > 0) { // 获取所有图片数据(关键新增) Map<String, List<PictureData>> pictures = getSheetPictures(sheet); // ... [原有字段处理逻辑] for (int i = 2; i < rows; i++) { Row row = sheet.getRow(i); T entity = null; for (Map.Entry<Integer, Field> entry : fieldsMap.entrySet()) { Integer colIndex = entry.getKey(); Field field = entry.getValue(); Excel attr = field.getAnnotation(Excel.class); // 图片类型特殊处理(新增) if (attr.cellType() == Excel.ColumnType.IMAGE) { // 获取当前单元格图片 String coord = i + "_" + colIndex; List<PictureData> cellImages = pictures.get(coord); if (cellImages != null && !cellImages.isEmpty()) { // 取第一张图片 byte[] imageData = cellImages.get(0).getData(); // 存储为Base64或文件路径 String base64Image = Base64.getEncoder().encodeToString(imageData); ReflectUtils.invokeSetter(entity, field.getName(), base64Image); } continue; } // ... [原有非图片字段处理逻辑] } list.add(entity); } } return list; } // 新增方法:获取工作表所有图片 private Map<String, List<PictureData>> getSheetPictures(Sheet sheet) { Map<String, List<PictureData>> pictures = new HashMap<>(); if (sheet instanceof XSSFSheet) { XSSFSheet xssfSheet = (XSSFSheet) sheet; for (POIXMLDocumentPart part : xssfSheet.getRelations()) { if (part instanceof XSSFDrawing) { XSSFDrawing drawing = (XSSFDrawing) part; for (XSSFShape shape : drawing.getShapes()) { if (shape instanceof XSSFPicture) { XSSFPicture pic = (XSSFPicture) shape; XSSFClientAnchor anchor = pic.getPreferredSize(); String coord = anchor.getRow1() + "_" + anchor.getCol1(); pictures.computeIfAbsent(coord, k -> new ArrayList<>()) .add(pic.getPictureData()); } } } } } // 处理HSSF格式(xls) else if (sheet instanceof HSSFSheet) { HSSFPatriarch patriarch = (HSSFPatriarch) sheet.getDrawingPatriarch(); if (patriarch != null) { for (HSSFShape shape : patriarch.getChildren()) { if (shape instanceof HSSFPicture) { HSSFPicture pic = (HSSFPicture) shape; HSSFClientAnchor anchor = (HSSFClientAnchor) pic.getAnchor(); String coord = anchor.getRow1() + "_" + anchor.getCol1(); pictures.computeIfAbsent(coord, k -> new ArrayList<>()) .add(pic.getPictureData()); } } } } return pictures; } ``` #### 3. 修改导出方法 `addCell` ```java public Cell addCell(Excel attr, Row row, T vo, Field field, int column) { try { // ... [原有代码] // 图片类型特殊处理(新增) if (attr.cellType() == Excel.ColumnType.IMAGE) { Object value = getTargetValue(vo, field, attr); if (value instanceof byte[]) { addImageToCell((byte[]) value, row, column); } else if (value instanceof String) { // 支持Base64字符串 byte[] bytes = Base64.getDecoder().decode((String) value); addImageToCell(bytes, row, column); } return null; } // ... [原有非图片字段处理] } } // 新增方法:添加图片到单元格 private void addImageToCell(byte[] imageData, Row row, int column) { try { // 设置行高和列宽 row.setHeightInPoints(IMAGE_ROW_HEIGHT / 20f); sheet.setColumnWidth(column, IMAGE_COL_WIDTH); // 添加图片 int pictureIdx = wb.addPicture(imageData, Workbook.PICTURE_TYPE_PNG); CreationHelper helper = wb.getCreationHelper(); Drawing<?> drawing = sheet.createDrawingPatriarch(); ClientAnchor anchor = helper.createClientAnchor(); anchor.setCol1(column); anchor.setRow1(row.getRowNum()); Picture pict = drawing.createPicture(anchor, pictureIdx); pict.resize(); // 自适应图片大小 } catch (Exception e) { log.error("添加图片失败: {}", e.getMessage()); } } ``` #### 4. 修改实体类注解 在需要导入图片的字段添加注解: ```java @Excel(name = "资产图片", cellType = Excel.ColumnType.IMAGE) private String assetImage; // 存储Base64或文件路径 ``` #### 5. 枚举类扩展 在 `Excel.ColumnType` 中增加图片类型: ```java public enum ColumnType { NUMERIC, STRING, IMAGE // 新增图片类型 } ``` ### 注意事项: 1. **性能优化**: - 大图片建议存储文件路径而非 Base64 - 批量导入时限制图片大小(<500KB) - 使用异步处理图片保存 2. **兼容性**: ```java // 在 getCellValue 方法中添加图片类型跳过 if (cell.getCellType() == CellType.BLANK && isImageCell(cell)) { return null; // 图片单元格不处理 } ``` 3. **依赖升级**: ```xml <!-- pom.xml 确保 POI 版本 >= 5.2.3 --> <dependency> <groupId>org.apache.poi</groupId> <artifactId>poi</artifactId> <version>5.2.3</version> </dependency> <dependency> <groupId>org.apache.poi</groupId> <artifactId>poi-ooxml</artifactId> <version>5.2.3</version> </dependency> ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值