Java实现大批量数据导入导出(100W以上)
原文链接:Java实现大批量数据导入导出(100W以上)
一、文件导入#
一、为什么一定要在代码实现#
说说为什么不能通过SQL直接导入到数据库,而是通过程序实现:
-
首先,这个导入功能开始提供页面导入,只是开始业务方保证的一次只有<3W的数据导入;
-
其次,业务方导入的内容需要做校验,比如门店号,商品号等是否系统存在,需要程序校验;
-
最后,业务方导入的都是编码,数据库中还要存入对应名称,方便后期查询,SQL导入也是无法实现的。
基于以上上三点,就无法直接通过SQL语句导入数据库。那就只能老老实实的想办法通过程序实现。
二、程序实现有以下技术难点#
-
一次读取这么大的数据量,肯定会导致服务器内存溢出;
-
调用接口保存一次传输数据量太大,网络传输压力会很大;
-
最终通过SQL一次批量插入,对数据库压力也比较大,如果业务同时操作这个表数据,很容易造成死锁
三、解决思路#
根据列举的技术难点我的解决思路是:
-
既然一次读取整个导入文件,那就先将文件流上传到服务器磁盘,然后分批从磁盘读取(支持多线程读取),这样就防止内存溢出;
-
调用插入数据库接口也是根据分批读取的内容进行调用;
-
分批插入数据到数据库。
四、具体实现代码#
1. 流式上传文件到服务器磁盘#
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
public class FileUploader {
private static final int BUFFER_SIZE = 4096;
public static void uploadFile(String targetUrl, String filePath) throws IOException {
File file = new File(filePath);
FileInputStream inputStream = new FileInputStream(file);
URL url = new URL(targetUrl);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setDoOutput(true);
connection.setRequestMethod("POST");
OutputStream outputStream = connection.getOutputStream();
byte[] buffer = new byte[BUFFER_SIZE];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
outputStream.close();
inputStream.close();
int responseCode = connection.getResponseCode();
if (responseCode == HttpURLConnection.HTTP_OK) {
System.out.println("File uploaded successfully.");
} else {
System.out.println("File upload failed.");
}
connection.disconnect();
}
public static void main(String[] args) {
String targetUrl = "
String filePath = "path/to/file";
try {
uploadFile(targetUrl, filePath);
} catch (IOException e) {
e.printStackTrace();
}
}
}
流程图:
下面是使用mermaid语法的流程图,表示流式上传的流程
2. 多线程分批从磁盘读取#
批量读取文件:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
/**
* 类功能描述:批量读取文件
*
* @author WangXueXing create at 19-3-14 下午6:47
* @version 1.0.0
/
public class BatchReadFile {
private final Logger LOGGER = LoggerFactory.getLogger(BatchReadFile.class);
/*
* 字符集UTF-8
/
public static final String CHARSET_UTF8 = "UTF-8";
/*
* 字符集GBK
/
public static final String CHARSET_GBK = "GBK";
/*
* 字符集gb2312
/
public static final String CHARSET_GB2312 = "gb2312";
/*
* 文件内容分割符-逗号
*/
public static final String SEPARATOR_COMMA = ",";
private int bufSize = 1024;
// 换行符
private byte key = "\n".getBytes()[0];
// 当前行数
private long lineNum = 0;
// 文件编码,默认为gb2312
private String encode = CHARSET_GB2312;
// 具体业务逻辑监听器
private ReaderFileListener readerListener;
public void setEncode(String encode) {
this.encode = encode;
}
public void setReaderListener(ReaderFileListener readerListener) {
this.readerListener = readerListener;
}
/**
* 获取准确开始位置
* @param file
* @param position
* @return
* @throws Exception
*/
public long getStartNum(File file, long position) throws Exception {
long startNum = position;
FileChannel fcin = new RandomAccessFile(file, "r").getChannel();
fcin.position(position);
try {
int cache = 1024;
ByteBuffer rBuffer = ByteBuffer.allocate(cache);
// 每次读取的内容
byte[] bs = new byte[cache];
// 缓存
byte[] tempBs = new byte[0];
while (fcin.read(rBuffer) != -1) {
int rSize = rBuffer.position();
rBuffer.rewind();
rBuffer.get(bs);
rBuffer.clear();
byte[] newStrByte = bs;
// 如果发现有上次未读完的缓存,则将它加到当前读取的内容前面
if (null != tempBs) {
int tL = tempBs.length;
newStrByte = new byte[rSize + tL];
System.arraycopy(tempBs, 0, newStrByte, 0, tL);
System.arraycopy(bs, 0, newStrByte, tL, rSize);
}
// 获取开始位置之后的第一个换行符
int endIndex = indexOf(newStrByte, 0);
if (endIndex != -1) {
return startNum + endIndex;
}
tempBs = substring(newStrByte, 0, newStrByte.length);
startNum += 1024;
}
} finally {
fcin.close();
}
return position;
}
/**
* 从设置的开始位置读取文件,一直到结束为止。如果 end设置为负数,刚读取到文件末尾
* @param fullPath
* @param start
* @param end
* @throws Exception
*/
public void readFileByLine(String fullPath, long start, long end) throws Exception {
File fin = new File(fullPath);
if (!fin.exists()) {
throw new FileNotFoundException("没有找到文件:" + fullPath);
}
FileChannel fileChannel = new RandomAccessFile(fin, "r").getChannel();
fileChannel.position(start);
try {
ByteBuffer rBuffer = ByteBuffer.allocate(bufSize);
// 每次读取的内容
byte[] bs = new byte[bufSize];
// 缓存
byte[] tempBs = new byte[0];
String line;
// 当前读取文件位置
long nowCur = start;
while (fileChannel.read(rBuffer) != -1) {
int rSize = rBuffer.position();
rBuffer.rewind();
rBuffer.get(bs);
rBuffer.clear();
byte[] newStrByte;
//去掉表头
if(nowCur == start){
int firstLineIndex = indexOf(bs, 0);
int newByteLenth = bs.length-firstLineIndex-1;
newStrByte = new byte[newByteLenth];
System.arraycopy(bs, firstLineIndex+1, newStrByte, 0, newByteLenth);
} else {
newStrByte = bs;
}
// 如果发现有上次未读完的缓存,则将它加到当前读取的内容前面
if (null != tempBs && tempBs.length != 0) {
int tL = tempBs.length;
newStrByte = new byte[rSize + tL];
System.arraycopy(tempBs, 0, newStrByte, 0, tL);
System.arraycopy(bs, 0, newStrByte, tL, rSize);
}
// 是否已经读到最后一位
boolean isEnd = false;
nowCur += bufSize;
// 如果当前读取的位数已经比设置的结束位置大的时候,将读取的内容截取到设置的结束位置
if (end > 0 && nowCur > end) {
// 缓存长度 - 当前已经读取位数 - 最后位数
int l = newStrByte.length - (int) (nowCur - end);
newStrByte = substring(newStrByte, 0, l);
isEnd = true;
}
int fromIndex = 0;
int endIndex = 0;
// 每次读一行内容,以 key(默认为\n) 作为结束符
while ((endIndex = indexOf(newStrByte, fromIndex)) != -1) {
byte[] bLine = substring(newStrByte, fromIndex, endIndex);
line = new String(bLine, 0, bLine.length, encode);
lineNum++;
// 输出一行内容,处理方式由调用方提供
readerListener.outLine(line.trim(), lineNum, false);
fromIndex = endIndex + 1;
}
// 将未读取完成的内容放到缓存中
tempBs = substring(newStrByte, fromIndex, newStrByte.length);
if (isEnd) {
break;
}
}
// 将剩下的最后内容作为一行,输出,并指明这是最后一行
String lineStr = new String(tempBs, 0, tempBs.length, encode);
readerListener.outLine(lineStr.trim(), lineNum, true);
} finally {
fileChannel.close();
fin.deleteOnExit();
}
}
/**
* 查找一个byte[]从指定位置之后的一个换行符位置
*
* @param src
* @param fromIndex
* @return
* @throws Exception
*/
private int indexOf(byte[] src, int fromIndex) throws Exception {
for (int i = fromIndex; i < src.length; i++) {
if (src[i] == key) {
return i;
}
}
return -1;
}
/**
* 从指定开始位置读取一个byte[]直到指定结束位置为止生成一个全新的byte[]
*
* @param src
* @param fromIndex
* @param endIndex
* @return
* @throws Exception
*/
private byte[] substring(byte[] src, int fromIndex, int endIndex) throws Exception {
int size = endIndex - fromIndex;
byte[] ret = new byte[size];
System.arraycopy(src, fromIndex, ret, 0, size);
return ret;
}
}
以上是关键代码:利用FileChannel与ByteBuffer从磁盘中分批读取数据
多线程调用批量读取:
/**
* 类功能描述: 线程读取文件
*
* @author WangXueXing create at 19-3-14 下午6:51
* @version 1.0.0
*/
public class ReadFileThread extends Thread {
private ReaderFileListener processDataListeners;
private String filePath;
private long start;
private long end;
private Thread preThread;
public ReadFileThread(ReaderFileListener processDataListeners,
long start,long end,
String file) {
this(processDataListeners, start, end, file, null);
}
public ReadFileThread(ReaderFileListener processDataListeners,
long start,long end,
String file,
Thread preThread) {
this.setName(this.getName()+"-ReadFileThread");
this.start = start;
this.end = end;
this.filePath = file;
this.processDataListeners = processDataListeners;
this.preThread = preThread;
}
@Override
public void run() {
BatchReadFile readFile = new BatchReadFile();
readFile.setReaderListener(processDataListeners);
readFile.setEncode(processDataListeners.getEncode());
try {
readFile.readFileByLine(filePath, start, end + 1);
if(this.preThread != null){
this.preThread.join();
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
监听读取:
import java.util.ArrayList;
import java.util.List;
/**
* 类功能描述:读文件监听父类
*
* @author WangXueXing create at 19-3-14 下午6:52
* @version 1.0.0
*/
public abstract class ReaderFileListener
// 一次读取行数,默认为1000
private int readColNum = 1000;
/**
* 文件编码
*/
private String encode;
/**
* 分批读取行列表
*/
private List
/**
*其他参数
*/
private T otherParams;
/**
* 每读取到一行数据,添加到缓存中
* @param lineStr 读取到的数据
* @param lineNum 行号
* @param over 是否读取完成
* @throws Exception
*/
public void outLine(String lineStr, long lineNum, boolean over) throws Exception {
if(null != lineStr && !lineStr.trim().equals("")){
rowList.add(lineStr);
}
if (!over && (lineNum % readColNum == 0)) {
output(rowList);
rowList = new ArrayList<>();
} else if (over) {
output(rowList);
rowList = new ArrayList<>();
}
}
/**
* 批量输出
*
* @param stringList
* @throws Exception
*/
public abstract void output(List
/**
* 设置一次读取行数
* @param readColNum
*/
protected void setReadColNum(int readColNum) {
this.readColNum = readColNum;
}
public String getEncode() {
return encode;
}
public void setEncode(String encode) {
this.encode = encode;
}
public T getOtherParams() {
return otherParams;
}
public void setOtherParams(T otherParams) {
this.otherParams = otherParams;
}
public List
return rowList;
}
public void setRowList(List
this.rowList = rowList;
}
}
实现监听读取并分批调用插入数据接口:
import com.today.api.finance.ImportServiceClient;
import com.today.api.finance.request.ImportRequest;
import com.today.api.finance.response.ImportResponse;
import com.today.api.finance.service.ImportService;
import com.today.common.Constants;
import com.today.domain.StaffSimpInfo;
import com.today.util.EmailUtil;
import com.today.util.UserSessionHelper;
import com.today.util.readfile.ReadFile;
import com.today.util.readfile.ReadFileThread;
import com.today.util.readfile.ReaderFileListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.io.File;
import java.io.FileInputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.FutureTask;
import java.util.stream.Collectors;
/**
* 类功能描述:报表导入服务实现
*
* @author WangXueXing create at 19-3-19 下午1:43
* @version 1.0.0
/
@Service
public class ImportReportServiceImpl extends ReaderFileListener
private final Logger LOGGER = LoggerFactory.getLogger(ImportReportServiceImpl.class);
@Value("${READ_COL_NUM_ONCE}")
private String readColNum;
@Value("${REPORT_IMPORT_RECEIVER}")
private String reportImportReceiver;
/
* 财务报表导入接口
*/
private ImportService service = new ImportServiceClient();
/**
* 读取文件内容
* @param file
*/
public void readTxt(File file, ImportRequest importRequest) throws Exception {
this.setOtherParams(importRequest);
ReadFile readFile = new ReadFile();
try(FileInputStream fis = new FileInputStream(file)){
int available = fis.available();
long maxThreadNum = 3L;
// 线程粗略开始位置
long i = available / maxThreadNum;
this.setRowList(new ArrayList<>());
StaffSimpInfo staffSimpInfo = ((StaffSimpInfo)UserSessionHelper.getCurrentUserInfo().getData());
String finalReportReceiver = getEmail(staffSimpInfo.getEmail(), reportImportReceiver);
this.setReadColNum(Integer.parseInt(readColNum));
this.setEncode(ReadFile.CHARSET_GB2312);
//这里单独使用一个线程是为了当maxThreadNum大于1的时候,统一管理这些线程
new Thread(()->{
Thread preThread = null;
FutureTask futureTask = null ;
try {
for (long j = 0; j < maxThreadNum; j++) {
//计算精确开始位置
long startNum = j == 0 ? 0 : readFile.getStartNum(file, i * j);
long endNum = j + 1 < maxThreadNum ? readFile.getStartNum(file, i * (j + 1)) : -2L;
//具体监听实现
preThread = new ReadFileThread(this, startNum, endNum, file.getPath(), preThread);
futureTask = new FutureTask(preThread, new Object());
futureTask.run();
}
if(futureTask.get() != null) {
EmailUtil.sendEmail(EmailUtil.REPORT_IMPORT_EMAIL_PREFIX, finalReportReceiver, "导入报表成功", "导入报表成功" ); //todo 等文案
}
} catch (Exception e){
futureTask.cancel(true);
try {
EmailUtil.sendEmail(EmailUtil.REPORT_IMPORT_EMAIL_PREFIX, finalReportReceiver, "导入报表失败", e.getMessage());
} catch (Exception e1){
//ignore
LOGGER.error("发送邮件失败", e1);
}
LOGGER.error("导入报表类型:"+importRequest.getReportType()+"失败", e);
} finally {
futureTask.cancel(true);
}
}).start();
}
}
private String getEmail(String infoEmail, String reportImportReceiver){
if(StringUtils.isEmpty(infoEmail)){
return reportImportReceiver;
}
return infoEmail;
}
/**
* 每批次调用导入接口
* @param stringList
* @throws Exception
*/
@Override
public void output(List
ImportRequest importRequest = this.getOtherParams();
List<List
.map(x->Arrays.asList(x.split(ReadFile.SEPARATOR_COMMA)).stream().map(String::trim).collect(Collectors.toList()))
.collect(Collectors.toList());
LOGGER.info("上传数据:{}", dataList);
importRequest.setDataList(dataList);
// LOGGER.info("request对象:{}",importRequest, "request增加请求字段:{}", importRequest.data);
ImportResponse importResponse = service.batchImport(importRequest);
LOGGER.info("=====SUCESS_CODE="+importResponse.getCode());
//导入错误,输出错误信息
if(!Constants.SUCESS_CODE.equals(importResponse.getCode())){
LOGGER.error("导入报表类型:"+importRequest.getReportType()+"失败","返回码为:", importResponse.getCode() ,"返回信息:",importResponse.getMessage());
throw new RuntimeException("导入报表类型:"+importRequest.getReportType()+"失败"+"返回码为:"+ importResponse.getCode() +"返回信息:"+importResponse.getMessage());
}
// if(importResponse.data != null && importResponse.data.get().get("batchImportFlag")!=null) {
// LOGGER.info("eywa-service请求batchImportFlag不为空");
// }
importRequest.setData(importResponse.data);
}
}
注意:
第53行代码:
long maxThreadNum = 3L;
就是设置分批读取磁盘文件的线程数,我设置为3,大家不要设置太大,不然多个线程读取到内存,也会造成服务器内存溢出。以上所有批次的批量读取并调用插入接口都成功发送邮件通知给导入人,任何一个批次失败直接发送失败邮件。
数据库分批插入数据:
/**
* 批量插入非联机第三方导入账单
* @param dataList
*/
def insertNonOnlinePayment(dataList: List[NonOnlineSourceData]) : Unit = {
if (dataList.nonEmpty) {
CheckAccountDataSource.mysqlData.withConnection { conn =>
val sql =
s""" INSERT INTO t_pay_source_data
(store_code,
store_name,
source_date,
order_type,
trade_type,
third_party_payment_no,
business_type,
business_amount,
trade_time,
created_at,
updated_at)
VALUES (?,?,?,?,?,?,?,?,?,NOW(),NOW())"""
conn.setAutoCommit(false)
var stmt = conn.prepareStatement(sql)
var i = 0
dataList.foreach { x =>
stmt.setString(1, x.storeCode)
stmt.setString(2, x.storeName)
stmt.setString(3, x.sourceDate)
stmt.setInt(4, x.orderType)
stmt.setInt(5, x.tradeType)
stmt.setString(6, x.tradeNo)
stmt.setInt(7, x.businessType)
stmt.setBigDecimal(8, x.businessAmount.underlying())
stmt.setString(9, x.tradeTime.getOrElse(null))
stmt.addBatch()
if ((i % 5000 == 0) && (i != 0)) { //分批提交
stmt.executeBatch
conn.commit
conn.setAutoCommit(false)
stmt = conn.prepareStatement(sql)
}
i += 1
}
stmt.executeBatch()
conn.commit()
}
}
}
以上代码实现每5000 行提交一次批量插入,防止一次提较数据库的压力。
二、文件导出#
使用POI或JXLS导出大数据量(百万级)Excel报表常常面临两个问题:
-
服务器内存溢出;
-
一次从数据库查询出这么大数据,查询缓慢。
当然也可以分页查询出数据,分别生成多个Excel打包下载,但这种生成还是很缓慢。
那么如何解决呢?
我们可以借助XML格式利用模板替换,分页查询出数据从磁盘写入XML,最终会以Excel多sheet形式生成。亲测2400万行数据,生成Excel文件4.5G,总耗时1.5分钟。
我利用StringTemplate模板解析技术对XML模板进行填充。当然也可以使用FreeMarker, Velocity等Java模板技术实现。
首先引入StringTemplate所需Jar包:
使用技术为 stringTemplate
pom.xml:
首先准备导出Excel模板,然后打开-》另存为-》选择格式为XML,然后用文本打开XML,提取XML头模板(head.st可通用),数据体模板(boday.st):
head.st可通用:
boday.st:
$worksheet:{
$it.rows:{
}$
}$
生成大数据量Excel类:
ExcelGenerator:
package test.exportexcel;
import org.antlr.stringtemplate.StringTemplate;
import org.antlr.stringtemplate.StringTemplateGroup;
import test.exportexcel.bean.Row;
import test.exportexcel.bean.Worksheet;
import java.io.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
/**
* 类功能描述:generator big data Excel
*
* @author WangXueXing create at 19-4-13 下午10:23
* @version 1.0.0
*/
public class ExcelGenerator {
public static void main(String[] args) throws FileNotFoundException{
ExcelGenerator template = new ExcelGenerator();
template.output2();
}
/**
* 生成数据量大的时候,该方法会出现内存溢出
* @throws FileNotFoundException
*/
public void output1() throws FileNotFoundException{
StringTemplateGroup stGroup = new StringTemplateGroup("stringTemplate");
StringTemplate st4 = stGroup.getInstanceOf("test/exportexcel/template/test");
List
File file = new File("/home/barry/data/output.xls");
PrintWriter writer = new PrintWriter(new BufferedOutputStream(new FileOutputStream(file)));
for(int i=0;i<30;i++){
Worksheet worksheet = new Worksheet();
worksheet.setSheet("第"+(i+1)+"页");
List
for(int j=0;j<6000;j++){
Row row = new Row();
row.setName1("zhangzehao");
row.setName2(""+j);
row.setName3(i+" "+j);
rows.add(row);
}
worksheet.setRows(rows);
worksheets.add(worksheet);
}
st4.setAttribute("worksheets", worksheets);
writer.write(st4.toString());
writer.flush();
writer.close();
System.out.println("生成excel完成");
}
/**
* 该方法不管生成多大的数据量,都不会出现内存溢出,只是时间的长短
* 经测试,生成2400万数据,2分钟内,4.5G大的文件,打开大文件就看内存是否足够大了
* 数据量小的时候,推荐用JXLS的模板技术生成excel文件,谁用谁知道,大数据量可以结合该方法使用
* @throws FileNotFoundException
*/
public void output2() throws FileNotFoundException{
long startTimne = System.currentTimeMillis();
StringTemplateGroup stGroup = new StringTemplateGroup("stringTemplate");
//写入excel文件头部信息
StringTemplate head = stGroup.getInstanceOf("test/exportexcel/template/head");
File file = new File("/home/barry/data/output.xls");
PrintWriter writer = new PrintWriter(new BufferedOutputStream(new FileOutputStream(file)));
writer.print(head.toString());
writer.flush();
int sheets = 400;
//excel单表最大行数是65535
int maxRowNum = 60000;
//写入excel文件数据信息
for(int i=0;i<sheets;i++){
StringTemplate body = stGroup.getInstanceOf("test/exportexcel/template/body");
Worksheet worksheet = new Worksheet();
worksheet.setSheet(" "+(i+1)+" ");
worksheet.setColumnNum(3);
worksheet.setRowNum(maxRowNum);
List
for(int j=0;j<maxRowNum;j++){
Row row = new Row();
row.setName1(""+new Random().nextInt(100000));
row.setName2(""+j);
row.setName3(i+""+j);
rows.add(row);
}
worksheet.setRows(rows);
body.setAttribute("worksheet", worksheet);
writer.print(body.toString());
writer.flush();
rows.clear();
rows = null;
worksheet = null;
body = null;
Runtime.getRuntime().gc();
System.out.println("正在生成excel文件的 sheet"+(i+1));
}
//写入excel文件尾部
writer.print("");
writer.flush();
writer.close();
System.out.println("生成excel文件完成");
long endTime = System.currentTimeMillis();
System.out.println("用时="+((endTime-startTimne)/1000)+"秒");
}
}
定义JavaBean:
WorkSheet.java:
package test.exportexcel.bean;
import java.util.List;
/**
* 类功能描述:Excel sheet Bean
*
* @author WangXueXing create at 19-4-13 下午10:21
* @version 1.0.0
*/
public class Worksheet {
private String sheet;
private int columnNum;
private int rowNum;
private List
public String getSheet() {
return sheet;
}
public void setSheet(String sheet) {
this.sheet = sheet;
}
public List
return rows;
}
public void setRows(List
this.rows = rows;
}
public int getColumnNum() {
return columnNum;
}
public void setColumnNum(int columnNum) {
this.columnNum = columnNum;
}
public int getRowNum() {
return rowNum;
}
public void setRowNum(int rowNum) {
this.rowNum = rowNum;
}
}
Row.java:
package test.exportexcel.bean;
/**
* 类功能描述:Excel row bean
*
* @author WangXueXing create at 19-4-13 下午10:22
* @version 1.0.0
*/
public class Row {
private String name1;
private String name2;
private String name3;
public String getName1() {
return name1;
}
public void setName1(String name1) {
this.name1 = name1;
}
public String getName2() {
return name2;
}
public void setName2(String name2) {
this.name2 = name2;
}
public String getName3() {
return name3;
}
public void setName3(String name3) {
this.name3 = name3;
}
}
三、超过25列Excel导出#
模块二:导出 在Excel列较少时,按以上实际验证能很快实现生成。但如果列较多时用StringTemplate写入时会出现内存溢出。那么我的解决方案如下:
将数据列表分成多份,如果从数据库查询就是分页查询出多页数据进行分批在磁盘插入
1. 创建模板#
举例Excel截图如下(有27列):
模板分三部分(head,body及foot),分别如下:
operation_data_head.st
基础信息
订单信息(统计周期内)
销售信息(统计周期内)
库存信息
保理/融资信息(统计周期内)
供应商名称
供应商组号
首次合同签署时间
卡号数量
卡号账期
异常状态卡号数量
订货单金额
送货单金额
退货单金额
订货单数量
未税销售金额
综合毛利
净毛利
费用
是否有进货记录
是否有销售记录
90天销售额(T-1至T-90)
90天销售额(T-91至T-180)
90天综合毛利(T-1至T-90)
期末库存
放款笔数
放款金额
放款利息
保理手续费
逾期罚息
逾期次数
月均放款额度
坏账笔数
operation_data_body.st
$worksheet:{
$it.rows:{
$it.supplierName$
$it.groupNumber$
$it.firstContYear$
$it.cardNumber$
$it.cardPeriod$
$it.badCardNumber$
$it.orderAmount$
$it.receiveOrderAmount$
$it.backOrderAmount$
$it.orderNumber$
$it.saleAmount$
$it.conPg$
$it.netPg$
$it.fee$
$it.receiveRecord$
$it.saleRecord$
$it.saleAmount90$
$it.saleAmount180$
$it.conPg90$
$it.endInventAm$
$it.makeLoanNum$
$it.makeLoanAm$
$it.makeLoanInt$
$it.factFee$
$it.overdueInt$
$it.overdueNum$
$it.avgMakeLoanAm$
$it.lossNum$
}$
}$
operation_data_foot.st



浙公网安备 33010602011771号