修改统计数据显示问题

This commit is contained in:
MerCry 2026-03-07 18:49:12 +08:00
parent 1bf27164d3
commit 143a9e90be
18 changed files with 1581 additions and 31 deletions

View File

@ -59,6 +59,10 @@
<artifactId>fastjson</artifactId> <artifactId>fastjson</artifactId>
<version>1.2.83</version> <version>1.2.83</version>
</dependency> </dependency>
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-system</artifactId>
</dependency>
</dependencies> </dependencies>

View File

@ -66,6 +66,9 @@ public class CustomerExportData implements Serializable {
*/ */
private Date addDate; private Date addDate;
//成交日期
private Date finishDate;
/** /**
* 来源 * 来源
*/ */

View File

@ -519,6 +519,20 @@ import java.util.concurrent.atomic.AtomicInteger;
GROUP_FIELD_MAP.put("AE组", "tagGroup18"); GROUP_FIELD_MAP.put("AE组", "tagGroup18");
} }
private static final Map<String, String> GROUP_ATTR_MAP = new LinkedHashMap<>();
static {
GROUP_ATTR_MAP.put("N组", "tag_group1");
GROUP_ATTR_MAP.put("O组", "tag_group2");
GROUP_ATTR_MAP.put("P组", "tag_group3");
GROUP_ATTR_MAP.put("W组", "tag_group10");
GROUP_ATTR_MAP.put("X组", "tag_group11");
GROUP_ATTR_MAP.put("Y组", "tag_group12");
GROUP_ATTR_MAP.put("Z组", "tag_group13");
GROUP_ATTR_MAP.put("AA组", "tag_group14");
GROUP_ATTR_MAP.put("AC组", "tag_group16");
GROUP_ATTR_MAP.put("AD组", "tag_group17");
GROUP_ATTR_MAP.put("AE组", "tag_group18");
}
/** /**
* 字段反射缓存 * 字段反射缓存
*/ */
@ -729,7 +743,6 @@ import java.util.concurrent.atomic.AtomicInteger;
String customerAttr = data.getTagGroup6(); String customerAttr = data.getTagGroup6();
if (customerAttr != null && !customerAttr.trim().isEmpty()) { if (customerAttr != null && !customerAttr.trim().isEmpty()) {
stats.setTotalCustomerAttr(stats.getTotalCustomerAttr() + 1); stats.setTotalCustomerAttr(stats.getTotalCustomerAttr() + 1);
if (customerAttr.contains("家长")) { if (customerAttr.contains("家长")) {
stats.setParentCount(stats.getParentCount() + 1); stats.setParentCount(stats.getParentCount() + 1);
//todo 存在订单未完成 但是有标签的场景 //todo 存在订单未完成 但是有标签的场景
@ -751,9 +764,24 @@ import java.util.concurrent.atomic.AtomicInteger;
if (matchesQValue(data, date) && isTimelyOrder(data)) { if (matchesQValue(data, date) && isTimelyOrder(data)) {
stats.setStudentDailyOrderCount(stats.getStudentDailyOrderCount() + 1); stats.setStudentDailyOrderCount(stats.getStudentDailyOrderCount() + 1);
} }
} else if (customerAttr.contains("老师")) {
stats.setTeacherCount(stats.getTeacherCount() + 1);
stats.setTeacherOrderCount(stats.getTeacherOrderCount() + 1);
} else { // 新增N组老师出单率统计当日
stats.setUnknownAttrCount(stats.getUnknownAttrCount() + 1); stats.setTeacherDailyCount(stats.getTeacherDailyCount() + 1);
if (matchesQValue(data, date) && isTimelyOrder(data)) {
stats.setTeacherDailyOrderCount(stats.getTeacherDailyOrderCount() + 1);
}
}
} else {
stats.setUnknownAttrCount(stats.getUnknownAttrCount() + 1);
stats.setUnkownOrderCount(stats.getUnkownOrderCount() + 1);
// 新增N组未知出单率统计当日
stats.setUnkownDailyCount(stats.getUnkownDailyCount() + 1);
if (matchesQValue(data, date) && isTimelyOrder(data)) {
stats.setUnkownDailyOrderCount(stats.getUnkownDailyOrderCount() + 1);
} }
} }
@ -825,6 +853,7 @@ import java.util.concurrent.atomic.AtomicInteger;
int sortNo = 0; int sortNo = 0;
// 为每个组生成23个统计指标 // 为每个组生成23个统计指标
for (Map.Entry<String, StatisticsAccumulator.GroupStatistics> entry : accumulator.getGroupStatsMap().entrySet()) { for (Map.Entry<String, StatisticsAccumulator.GroupStatistics> entry : accumulator.getGroupStatsMap().entrySet()) {
String groupName = entry.getKey(); String groupName = entry.getKey();
StatisticsAccumulator.GroupStatistics stats = entry.getValue(); StatisticsAccumulator.GroupStatistics stats = entry.getValue();
@ -834,7 +863,9 @@ import java.util.concurrent.atomic.AtomicInteger;
setIndicatorValue(corpId,indicatorMap,curDate, "成单成本(当日)", groupName, "需手工填写",(10*sortNo++)); setIndicatorValue(corpId,indicatorMap,curDate, "成单成本(当日)", groupName, "需手工填写",(10*sortNo++));
// 1. 成单数 // 1. 成单数
setIndicatorValue(corpId,indicatorMap,curDate, "成单数(当日)", groupName, String.valueOf(stats.getOrderCount()),(10*sortNo++)); //成单数 需要从历史的所有数据中获取 成交日期 = date的数据
Long finishCount = customerExportDataMapper.selectByFinishDate(corpId,curDate,GROUP_ATTR_MAP.get(groupName));
setIndicatorValue(corpId,indicatorMap,curDate, "成单数(当日)", groupName, String.valueOf(finishCount),(10*sortNo++));
// 2. 进粉数 // 2. 进粉数
setIndicatorValue(corpId,indicatorMap,curDate, "进粉数(当日)", groupName, String.valueOf(stats.getCustomerCount()),(10*sortNo++)); setIndicatorValue(corpId,indicatorMap,curDate, "进粉数(当日)", groupName, String.valueOf(stats.getCustomerCount()),(10*sortNo++));
@ -866,6 +897,11 @@ import java.util.concurrent.atomic.AtomicInteger;
setIndicatorValue(corpId,indicatorMap,curDate, "学生占比(当日)", groupName, studentRate,(10*sortNo++)); setIndicatorValue(corpId,indicatorMap,curDate, "学生占比(当日)", groupName, studentRate,(10*sortNo++));
setIndicatorValue(corpId,indicatorMap,curDate, "学生数量(当日)", groupName, String.valueOf(stats.getStudentCount()),(10*sortNo++),true); setIndicatorValue(corpId,indicatorMap,curDate, "学生数量(当日)", groupName, String.valueOf(stats.getStudentCount()),(10*sortNo++),true);
// 8. 老师占比
String teacherRate = calculateRate(stats.getTeacherCount(), stats.getTotalCustomerAttr());
setIndicatorValue(corpId,indicatorMap,curDate, "老师占比(当日)", groupName, teacherRate,(10*sortNo++));
setIndicatorValue(corpId,indicatorMap,curDate, "老师数量(当日)", groupName, String.valueOf(stats.getTeacherCount()),(10*sortNo++),true);
// 9. 未知占比 // 9. 未知占比
String unknownRate = calculateRate(stats.getUnknownAttrCount(), stats.getTotalCustomerAttr()); String unknownRate = calculateRate(stats.getUnknownAttrCount(), stats.getTotalCustomerAttr());
setIndicatorValue(corpId,indicatorMap,curDate, "未知占比(当日)", groupName, unknownRate,(10*sortNo++)); setIndicatorValue(corpId,indicatorMap,curDate, "未知占比(当日)", groupName, unknownRate,(10*sortNo++));
@ -914,12 +950,12 @@ import java.util.concurrent.atomic.AtomicInteger;
// 19. 家长出单占比 // 19. 家长出单占比
String parentOrderRate = calculateRate(stats.getParentOrderCount(), stats.getParentCount()); String parentOrderRate = calculateRate(stats.getParentOrderCount(), stats.getParentCount());
setIndicatorValue(corpId,indicatorMap,curDate, "家长出单占比(当日)", groupName, parentOrderRate,(10*sortNo++)); setIndicatorValue(corpId,indicatorMap,curDate, "家长出单占比(当日)", groupName, parentOrderRate,(10*sortNo++),true);
setIndicatorValue(corpId,indicatorMap,curDate, "家长出单数量(当日)", groupName, String.valueOf(stats.getParentOrderCount()),(10*sortNo++),true); setIndicatorValue(corpId,indicatorMap,curDate, "家长出单数量(当日)", groupName, String.valueOf(stats.getParentOrderCount()),(10*sortNo++),true);
// 20. 学生出单占比 // 20. 学生出单占比
String studentOrderRate = calculateRate(stats.getStudentOrderCount(), stats.getStudentCount()); String studentOrderRate = calculateRate(stats.getStudentOrderCount(), stats.getStudentCount());
setIndicatorValue(corpId,indicatorMap,curDate, "学生出单占比(当日)", groupName, studentOrderRate,(10*sortNo++)); setIndicatorValue(corpId,indicatorMap,curDate, "学生出单占比(当日)", groupName, studentOrderRate,(10*sortNo++),true);
setIndicatorValue(corpId,indicatorMap,curDate, "学生出单数量(当日)", groupName, String.valueOf(stats.getStudentOrderCount()),(10*sortNo++),true); setIndicatorValue(corpId,indicatorMap,curDate, "学生出单数量(当日)", groupName, String.valueOf(stats.getStudentOrderCount()),(10*sortNo++),true);
// 21. 家长出单率当日 // 21. 家长出单率当日

View File

@ -78,6 +78,10 @@ public class StatisticsAccumulator {
* 学生数 * 学生数
*/ */
private int studentCount = 0; private int studentCount = 0;
/**
* 老师
*/
private int teacherCount = 0;
/** /**
* 未知空白 * 未知空白
@ -141,6 +145,13 @@ public class StatisticsAccumulator {
* 学生出单数 * 学生出单数
*/ */
private int studentOrderCount = 0; private int studentOrderCount = 0;
/**
* 老师出单数
*
*/
private int teacherOrderCount = 0;
private int unkownOrderCount = 0;
// ========== 新增家长/学生出单率统计当日 ========== // ========== 新增家长/学生出单率统计当日 ==========
/** /**
@ -158,10 +169,29 @@ public class StatisticsAccumulator {
*/ */
private int studentDailyOrderCount = 0; private int studentDailyOrderCount = 0;
/** /**
* 学生总数当日进粉- 用于出单率分母 * 学生总数当日进粉- 用于出单率分母
*/ */
private int studentDailyCount = 0; private int studentDailyCount = 0;
/**
* 老师总数当日进粉- 用于出单率分母
*/
private int teacherDailyCount = 0;
/**
* 未知总数当日进粉- 用于出单率分母
*/
private int unkownDailyCount = 0;
/**
* 学生出单数当日成交- 满足Q列=当日 AND T列=已成交及时单9元+
*/
private int teacherDailyOrderCount = 0;
/**
* 学生出单数当日成交- 满足Q列=当日 AND T列=已成交及时单9元+
*/
private int unkownDailyOrderCount = 0;
// ========== 成本数据 ========== // ========== 成本数据 ==========
/** /**

View File

@ -57,4 +57,42 @@ public interface CustomerExportDataMapper extends BaseMapper<CustomerExportData>
@Param("addUserAccount") String addUserAccount, @Param("addUserAccount") String addUserAccount,
@Param("addTime") Date addTime @Param("addTime") Date addTime
); );
Long selectByFinishDate(
@Param("corpId") String corpId,@Param("date") Date date,@Param("attr") String attr);
/**
* 统计客户导出数据VO数量(用于异步导出)
* @param corpId 企业ID
* @param startDate 开始日期
* @param endDate 结束日期
* @param customerName 客户名称(可选)
* @return 数据总数
*/
int countCustomerExportDataVOList(
@Param("corpId") String corpId,
@Param("startDate") Date startDate,
@Param("endDate") Date endDate,
@Param("customerName") String customerName
);
/**
* 分页查询客户导出数据VO列表(用于异步导出)
* @param corpId 企业ID
* @param startDate 开始日期
* @param endDate 结束日期
* @param customerName 客户名称(可选)
* @param offset 偏移量
* @param limit 每页数量
* @return 客户导出数据VO列表
*/
List<CustomerExportDataVO> selectCustomerExportDataVOListByPage(
@Param("corpId") String corpId,
@Param("startDate") Date startDate,
@Param("endDate") Date endDate,
@Param("customerName") String customerName,
@Param("offset") int offset,
@Param("limit") int limit
);
} }

View File

@ -116,6 +116,7 @@ public class CustomerDataChangeTrackingService {
sb.append(data.getTagGroup15()).append("|"); sb.append(data.getTagGroup15()).append("|");
sb.append(data.getTagGroup16()).append("|"); sb.append(data.getTagGroup16()).append("|");
sb.append(data.getTagGroup17()).append("|"); sb.append(data.getTagGroup17()).append("|");
sb.append(data.getFinishDate()).append("|");
sb.append(data.getTagGroup18()); sb.append(data.getTagGroup18());
// 计算MD5 // 计算MD5

View File

@ -0,0 +1,250 @@
package com.ruoyi.excel.wecom.service;
import com.ruoyi.common.config.RuoYiConfig;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.excel.wecom.mapper.CustomerExportDataMapper;
import com.ruoyi.excel.wecom.vo.CustomerExportDataVO;
import com.ruoyi.system.service.ISysExportTaskService;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.streaming.SXSSFWorkbook;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import java.io.File;
import java.io.FileOutputStream;
import java.util.Date;
import java.util.List;
/**
* 客户数据异步导出服务
*/
@Service
public class CustomerExportAsyncService
{
private static final Logger log = LoggerFactory.getLogger(CustomerExportAsyncService.class);
private static final int BATCH_SIZE = 5000;
private static final int ROW_ACCESS_WINDOW = 1000;
@Autowired
private CustomerExportDataMapper customerExportDataMapper;
@Autowired
private ISysExportTaskService exportTaskService;
@Async("threadPoolTaskExecutor")
public void executeExport(Long taskId, String corpId, Date startDate, Date endDate, String customerName)
{
log.info("开始执行异步导出任务taskId: {}, corpId: {}", taskId, corpId);
String filePath = null;
String fileName = null;
File file = null;
SXSSFWorkbook workbook = null;
FileOutputStream fos = null;
try
{
int totalCount = customerExportDataMapper.countCustomerExportDataVOList(corpId, startDate, endDate, customerName);
log.info("导出任务 taskId: {}, 总数据量: {}", taskId, totalCount);
if (totalCount == 0)
{
exportTaskService.markSuccess(taskId, "", "无数据.xlsx", 0L);
return;
}
exportTaskService.updateProgress(taskId, 0, totalCount);
String dateStr = DateUtils.dateTimeNow("yyyyMMddHHmmss");
fileName = "客户统计数据_" + dateStr + ".xlsx";
String downloadPath = RuoYiConfig.getDownloadPath();
File dir = new File(downloadPath);
if (!dir.exists())
{
dir.mkdirs();
}
filePath = downloadPath + fileName;
file = new File(filePath);
workbook = new SXSSFWorkbook(ROW_ACCESS_WINDOW);
Sheet sheet = workbook.createSheet("客户统计数据");
CellStyle headerStyle = createHeaderStyle(workbook);
CellStyle dataStyle = createDataStyle(workbook);
String[] headers = {
"客户名称", "描述", "添加人", "添加人账号", "添加人所属部门", "添加时间", "来源",
"手机", "企业", "邮箱", "地址", "职务", "电话",
"标签组1(投放)", "标签组2(公司孵化)", "标签组3(商务)", "标签组4(成交日期)", "标签组5(年级组)",
"标签组6(客户属性)", "标签组7(成交)", "标签组8(成交品牌)", "标签组9(线索通标签)", "标签组10(A1组)",
"标签组11(B1组)", "标签组12(C1组)", "标签组13(D1组)", "标签组14(E1组)", "标签组15(意向度)",
"标签组16(自然流)", "标签组17(F1组)", "标签组18(G1组)"
};
Row headerRow = sheet.createRow(0);
for (int i = 0; i < headers.length; i++)
{
Cell cell = headerRow.createCell(i);
cell.setCellValue(headers[i]);
cell.setCellStyle(headerStyle);
sheet.setColumnWidth(i, 20 * 256);
}
int processedCount = 0;
int rowIndex = 1;
int offset = 0;
while (offset < totalCount)
{
List<CustomerExportDataVO> dataList = customerExportDataMapper.selectCustomerExportDataVOListByPage(
corpId, startDate, endDate, customerName, offset, BATCH_SIZE);
if (dataList == null || dataList.isEmpty())
{
break;
}
for (CustomerExportDataVO data : dataList)
{
Row row = sheet.createRow(rowIndex++);
fillRowData(row, data, dataStyle);
processedCount++;
}
offset += BATCH_SIZE;
exportTaskService.updateProgress(taskId, processedCount, totalCount);
}
fos = new FileOutputStream(file);
workbook.write(fos);
fos.flush();
long fileSize = file.length();
exportTaskService.markSuccess(taskId, filePath, fileName, fileSize);
log.info("导出任务完成taskId: {}, 文件: {}, 大小: {} bytes", taskId, fileName, fileSize);
}
catch (Exception e)
{
log.error("导出任务执行失败taskId: {}", taskId, e);
exportTaskService.markFailed(taskId, "导出失败:" + e.getMessage());
}
finally
{
try
{
if (fos != null)
{
fos.close();
}
if (workbook != null)
{
workbook.dispose();
workbook.close();
}
}
catch (Exception e)
{
log.error("关闭资源失败", e);
}
}
}
private CellStyle createHeaderStyle(Workbook workbook)
{
CellStyle style = workbook.createCellStyle();
style.setAlignment(HorizontalAlignment.CENTER);
style.setVerticalAlignment(VerticalAlignment.CENTER);
style.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex());
style.setFillPattern(FillPatternType.SOLID_FOREGROUND);
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 font = workbook.createFont();
font.setBold(true);
font.setFontName("Arial");
font.setFontHeightInPoints((short) 10);
style.setFont(font);
return style;
}
private CellStyle createDataStyle(Workbook workbook)
{
CellStyle style = workbook.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 font = workbook.createFont();
font.setFontName("Arial");
font.setFontHeightInPoints((short) 10);
style.setFont(font);
return style;
}
private void fillRowData(Row row, CustomerExportDataVO data, CellStyle style)
{
int col = 0;
createCell(row, col++, data.getCustomerName(), style);
createCell(row, col++, data.getDescription(), style);
createCell(row, col++, data.getAddUserName(), style);
createCell(row, col++, data.getAddUserAccount(), style);
createCell(row, col++, data.getAddUserDepartment(), style);
createCell(row, col++, formatDate(data.getAddTime()), style);
createCell(row, col++, data.getSource(), style);
createCell(row, col++, data.getMobile(), style);
createCell(row, col++, data.getCompany(), style);
createCell(row, col++, data.getEmail(), style);
createCell(row, col++, data.getAddress(), style);
createCell(row, col++, data.getPosition(), style);
createCell(row, col++, data.getPhone(), style);
createCell(row, col++, data.getTagGroup1(), style);
createCell(row, col++, data.getTagGroup2(), style);
createCell(row, col++, data.getTagGroup3(), style);
createCell(row, col++, data.getTagGroup4(), style);
createCell(row, col++, data.getTagGroup5(), style);
createCell(row, col++, data.getTagGroup6(), style);
createCell(row, col++, data.getTagGroup7(), style);
createCell(row, col++, data.getTagGroup8(), style);
createCell(row, col++, data.getTagGroup9(), style);
createCell(row, col++, data.getTagGroup10(), style);
createCell(row, col++, data.getTagGroup11(), style);
createCell(row, col++, data.getTagGroup12(), style);
createCell(row, col++, data.getTagGroup13(), style);
createCell(row, col++, data.getTagGroup14(), style);
createCell(row, col++, data.getTagGroup15(), style);
createCell(row, col++, data.getTagGroup16(), style);
createCell(row, col++, data.getTagGroup17(), style);
createCell(row, col++, data.getTagGroup18(), style);
}
private void createCell(Row row, int column, String value, CellStyle style)
{
Cell cell = row.createCell(column);
cell.setCellValue(value == null ? "" : value);
cell.setCellStyle(style);
}
private String formatDate(Date date)
{
if (date == null)
{
return "";
}
return DateUtils.parseDateToStr("yyyy-MM-dd", date);
}
}

View File

@ -235,6 +235,79 @@ public class CustomerExportService {
return exportData; return exportData;
} }
private Date parseCompleteDate(String orderDate, Date addDate) {
if (orderDate == null || orderDate.trim().isEmpty()) {
return null;
}
if (addDate == null) {
log.warn("parseCompleteDate: addDate is null, orderDate: {}", orderDate);
return null;
}
try {
String[] dates = orderDate.trim().split(",");
String lastDateStr = dates[dates.length - 1].trim();
log.debug("parseCompleteDate: input orderDate: {}, lastDateStr: {}", orderDate, lastDateStr);
Calendar orderCal = Calendar.getInstance();
orderCal.setTime(addDate);
orderCal.set(Calendar.HOUR_OF_DAY, 0);
orderCal.set(Calendar.MINUTE, 0);
orderCal.set(Calendar.SECOND, 0);
orderCal.set(Calendar.MILLISECOND, 0);
boolean parsed = false;
java.util.regex.Pattern fullDatePattern = java.util.regex.Pattern.compile("(\\d{4})[/\\-](\\d{1,2})[/\\-](\\d{1,2})");
java.util.regex.Matcher fullDateMatcher = fullDatePattern.matcher(lastDateStr);
if (fullDateMatcher.find()) {
int year = Integer.parseInt(fullDateMatcher.group(1));
int month = Integer.parseInt(fullDateMatcher.group(2));
int day = Integer.parseInt(fullDateMatcher.group(3));
orderCal.set(Calendar.YEAR, year);
orderCal.set(Calendar.MONTH, month - 1);
orderCal.set(Calendar.DAY_OF_MONTH, day);
parsed = true;
log.info("parseCompleteDate: matched full date pattern, input: {}, result: {}-{}-{}", lastDateStr, year, month, day);
}
if (!parsed) {
java.util.regex.Pattern monthDayPattern = java.util.regex.Pattern.compile("^(\\d{1,2})[.\\-/](\\d{1,2})");
java.util.regex.Matcher monthDayMatcher = monthDayPattern.matcher(lastDateStr);
if (monthDayMatcher.find()) {
int month = Integer.parseInt(monthDayMatcher.group(1));
int day = Integer.parseInt(monthDayMatcher.group(2));
orderCal.set(Calendar.MONTH, month - 1);
orderCal.set(Calendar.DAY_OF_MONTH, day);
Calendar tempCal = Calendar.getInstance();
tempCal.setTime(addDate);
tempCal.set(Calendar.MONTH, month - 1);
tempCal.set(Calendar.DAY_OF_MONTH, day);
tempCal.set(Calendar.HOUR_OF_DAY, 0);
tempCal.set(Calendar.MINUTE, 0);
tempCal.set(Calendar.SECOND, 0);
tempCal.set(Calendar.MILLISECOND, 0);
if (tempCal.getTime().before(addDate)) {
orderCal.add(Calendar.YEAR, 1);
}
parsed = true;
log.info("parseCompleteDate: matched month-day pattern, input: {}, result: {}-{}-{}", lastDateStr, orderCal.get(Calendar.YEAR), month, day);
}
}
if (!parsed) {
log.warn("parseCompleteDate: no pattern matched, input: {}", lastDateStr);
return null;
}
return orderCal.getTime();
} catch (Exception e) {
log.error("parseCompleteDate error, orderDate: {}, addDate: {}", orderDate, addDate, e);
return null;
}
}
/** /**
* 处理客户标签按标签组分类填充到对应字段 * 处理客户标签按标签组分类填充到对应字段
* *
@ -277,8 +350,7 @@ public class CustomerExportService {
* @param tagNames 标签名称逗号分隔 * @param tagNames 标签名称逗号分隔
*/ */
private void fillTagGroupField(CustomerExportData exportData, String groupName, String tagNames) { private void fillTagGroupField(CustomerExportData exportData, String groupName, String tagNames) {
// 根据标签组名称映射到对应字段
// 这里的映射关系需要根据实际业务调整
if (groupName.contains("投放")) { if (groupName.contains("投放")) {
exportData.setTagGroup1(tagNames); exportData.setTagGroup1(tagNames);
} else if (groupName.contains("公司孵化")) { } else if (groupName.contains("公司孵化")) {
@ -287,12 +359,15 @@ public class CustomerExportService {
exportData.setTagGroup3(tagNames); exportData.setTagGroup3(tagNames);
} else if (groupName.contains("成交日期")) { } else if (groupName.contains("成交日期")) {
exportData.setTagGroup4(tagNames); exportData.setTagGroup4(tagNames);
Date finishDate = parseCompleteDate(tagNames, exportData.getAddTime());
exportData.setFinishDate(finishDate);
} else if (groupName.contains("年级组")) { } else if (groupName.contains("年级组")) {
exportData.setTagGroup5(tagNames); exportData.setTagGroup5(tagNames);
} else if (groupName.contains("客户属性")) { } else if (groupName.contains("客户属性")) {
exportData.setTagGroup6(tagNames); exportData.setTagGroup6(tagNames);
} else if (groupName.contains("成交") && !groupName.contains("日期") && !groupName.contains("品牌")) { } else if (groupName.contains("成交") && !groupName.contains("日期") && !groupName.contains("品牌")) {
exportData.setTagGroup7(tagNames); exportData.setTagGroup7(tagNames);
} else if (groupName.contains("成交品牌")) { } else if (groupName.contains("成交品牌")) {
exportData.setTagGroup8(tagNames); exportData.setTagGroup8(tagNames);
} else if (groupName.contains("线索通")) { } else if (groupName.contains("线索通")) {

View File

@ -1,6 +1,22 @@
<?xml version="1.0" encoding="UTF-8" ?> <?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.excel.wecom.mapper.CustomerExportDataMapper"> <mapper namespace="com.ruoyi.excel.wecom.mapper.CustomerExportDataMapper">
<select id="countCustomerExportDataVOList" resultType="int">
SELECT count(1)
FROM customer_export_data
<where>
corp_id = #{corpId}
<if test="startDate != null">
AND add_time &gt;= #{startDate}
</if>
<if test="endDate != null">
AND add_time &lt;= #{endDate}
</if>
<if test="customerName != null">
AND customer_name like concat('%',#{customerName},'%')
</if>
</where>
</select>
<select id="getDistinctDate" resultType="java.util.Date"> <select id="getDistinctDate" resultType="java.util.Date">
@ -120,4 +136,116 @@
LIMIT 1 LIMIT 1
</select> </select>
<select id="selectByFinishDate" resultType="java.lang.Long">
select count(1) from customer_export_data
WHERE
corp_id = #{corpId} and finish_date = #{date}
<if test="attr != null and attr != '' and attr == 'tag_group1'">
AND tag_group1 is not null
</if>
<if test="attr != null and attr != '' and attr == 'tag_group2'">
AND tag_group2 is not null
</if>
<if test="attr != null and attr != '' and attr == 'tag_group3'">
AND tag_group3 is not null
</if>
<if test="attr != null and attr != '' and attr == 'tag_group4'">
AND tag_group4 is not null
</if>
<if test="attr != null and attr != '' and attr == 'tag_group5'">
AND tag_group5 is not null
</if>
<if test="attr != null and attr != '' and attr == 'tag_group6'">
AND tag_group6 is not null
</if>
<if test="attr != null and attr != '' and attr == 'tag_group7'">
AND tag_group7 is not null
</if>
<if test="attr != null and attr != '' and attr == 'tag_group8'">
AND tag_group8 is not null
</if>
<if test="attr != null and attr != '' and attr == 'tag_group9'">
AND tag_group9 is not null
</if>
<if test="attr != null and attr != '' and attr == 'tag_group10'">
AND tag_group10 is not null
</if>
<if test="attr != null and attr != '' and attr == 'tag_group11'">
AND tag_group11 is not null
</if>
<if test="attr != null and attr != '' and attr == 'tag_group12'">
AND tag_group12 is not null
</if>
<if test="attr != null and attr != '' and attr == 'tag_group13'">
AND tag_group13 is not null
</if>
<if test="attr != null and attr != '' and attr == 'tag_group14'">
AND tag_group14 is not null
</if>
<if test="attr != null and attr != '' and attr == 'tag_group15'">
AND tag_group15 is not null
</if>
<if test="attr != null and attr != '' and attr == 'tag_group16'">
AND tag_group16 is not null
</if>
<if test="attr != null and attr != '' and attr == 'tag_group17'">
AND tag_group17 is not null
</if>
<if test="attr != null and attr != '' and attr == 'tag_group18'">
AND tag_group18 is not null
</if>
</select>
<!-- 分页查询客户导出数据VO列表(用于异步导出) -->
<select id="selectCustomerExportDataVOListByPage" resultType="com.ruoyi.excel.wecom.vo.CustomerExportDataVO">
SELECT
customer_name as customerName,
description,
add_user_name as addUserName,
add_user_account as addUserAccount,
add_user_department as addUserDepartment,
add_time as addTime,
add_date as addDate,
source,
mobile,
company,
email,
address,
position,
phone,
tag_group1 as tagGroup1,
tag_group2 as tagGroup2,
tag_group3 as tagGroup3,
tag_group4 as tagGroup4,
tag_group5 as tagGroup5,
tag_group6 as tagGroup6,
tag_group7 as tagGroup7,
tag_group8 as tagGroup8,
tag_group9 as tagGroup9,
tag_group10 as tagGroup10,
tag_group11 as tagGroup11,
tag_group12 as tagGroup12,
tag_group13 as tagGroup13,
tag_group14 as tagGroup14,
tag_group15 as tagGroup15,
tag_group16 as tagGroup16,
tag_group17 as tagGroup17,
tag_group18 as tagGroup18
FROM customer_export_data
<where>
corp_id = #{corpId}
<if test="startDate != null">
AND add_time &gt;= #{startDate}
</if>
<if test="endDate != null">
AND add_time &lt;= #{endDate}
</if>
<if test="customerName != null">
AND customer_name like concat('%',#{customerName},'%')
</if>
</where>
ORDER BY add_time DESC
LIMIT #{limit} OFFSET #{offset}
</select>
</mapper> </mapper>

View File

@ -2,19 +2,26 @@ package com.ruoyi.web.controller.wocom;
import com.ruoyi.common.annotation.Log; import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.core.controller.BaseController; import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.page.TableDataInfo; import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.enums.BusinessType; import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.common.utils.CorpContextHolder; import com.ruoyi.common.utils.CorpContextHolder;
import com.ruoyi.common.utils.poi.ExcelUtil; import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.excel.wecom.domain.CustomerExportData; import com.ruoyi.excel.wecom.domain.CustomerExportData;
import com.ruoyi.excel.wecom.service.CustomerExportAsyncService;
import com.ruoyi.excel.wecom.service.ICustomerExportDataService; import com.ruoyi.excel.wecom.service.ICustomerExportDataService;
import com.ruoyi.excel.wecom.vo.CustomerExportDataVO; import com.ruoyi.excel.wecom.vo.CustomerExportDataVO;
import com.ruoyi.system.domain.SysExportTask;
import com.ruoyi.system.service.ISysExportTaskService;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.format.annotation.DateTimeFormat; import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
@ -23,11 +30,17 @@ import java.util.List;
*/ */
@RestController @RestController
@RequestMapping("/wecom/customerExport") @RequestMapping("/wecom/customerExport")
public class CustomerExportDataController extends BaseController { public class CustomerExportDataController extends BaseController
{
@Autowired @Autowired
private ICustomerExportDataService customerExportDataService; private ICustomerExportDataService customerExportDataService;
@Autowired
private CustomerExportAsyncService customerExportAsyncService;
@Autowired
private ISysExportTaskService exportTaskService;
/** /**
* 查询客户统计数据列表 * 查询客户统计数据列表
*/ */
@ -36,16 +49,17 @@ public class CustomerExportDataController extends BaseController {
public TableDataInfo list( public TableDataInfo list(
@RequestParam(value = "startDate", required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") Date startDate, @RequestParam(value = "startDate", required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") Date startDate,
@RequestParam(value = "endDate", required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") Date endDate, @RequestParam(value = "endDate", required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") Date endDate,
@RequestParam(value = "customerName", required = false) String customerName) { @RequestParam(value = "customerName", required = false) String customerName)
{
String corpId = CorpContextHolder.getCurrentCorpId(); String corpId = CorpContextHolder.getCurrentCorpId();
startPage(); startPage();
List<CustomerExportData> list = customerExportDataService.selectCustomerExportDataList(corpId,startDate, endDate, customerName); List<CustomerExportData> list = customerExportDataService.selectCustomerExportDataList(corpId, startDate, endDate, customerName);
return getDataTable(list); return getDataTable(list);
} }
/** /**
* 导出客户统计数据列表 * 导出客户统计数据列表同步方式保留兼容
*/ */
@PreAuthorize("@ss.hasPermi('wecom:customerExport:export')") @PreAuthorize("@ss.hasPermi('wecom:customerExport:export')")
@Log(title = "客户统计数据", businessType = BusinessType.EXPORT) @Log(title = "客户统计数据", businessType = BusinessType.EXPORT)
@ -53,13 +67,149 @@ public class CustomerExportDataController extends BaseController {
public void export(HttpServletResponse response, public void export(HttpServletResponse response,
@RequestParam(value = "startDate", required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") Date startDate, @RequestParam(value = "startDate", required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") Date startDate,
@RequestParam(value = "endDate", required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") Date endDate, @RequestParam(value = "endDate", required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") Date endDate,
@RequestParam(value = "customerName", required = false) String customerName) { @RequestParam(value = "customerName", required = false) String customerName)
{
String corpId = CorpContextHolder.getCurrentCorpId(); String corpId = CorpContextHolder.getCurrentCorpId();
List<CustomerExportDataVO> list = customerExportDataService.selectCustomerExportDataVOList(corpId,startDate, endDate, customerName); List<CustomerExportDataVO> list = customerExportDataService.selectCustomerExportDataVOList(corpId, startDate, endDate, customerName);
ExcelUtil<CustomerExportDataVO> util = new ExcelUtil<>(CustomerExportDataVO.class); ExcelUtil<CustomerExportDataVO> util = new ExcelUtil<>(CustomerExportDataVO.class);
util.exportExcel(response, list, "客户统计数据"); util.exportExcel(response, list, "客户统计数据");
} }
/**
* 异步导出客户统计数据列表
*/
@PreAuthorize("@ss.hasPermi('wecom:customerExport:export')")
@Log(title = "客户统计数据-异步导出", businessType = BusinessType.EXPORT)
@PostMapping("/exportAsync")
public AjaxResult exportAsync(
@RequestParam(value = "startDate", required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") Date startDate,
@RequestParam(value = "endDate", required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") Date endDate,
@RequestParam(value = "customerName", required = false) String customerName)
{
String corpId = CorpContextHolder.getCurrentCorpId();
SysExportTask task = new SysExportTask();
task.setTaskName("客户统计数据导出");
task.setTaskType("customer_export");
task.setCorpId(corpId);
task.setCreateBy(getUsername());
Long taskId = exportTaskService.createExportTask(task);
customerExportAsyncService.executeExport(taskId, corpId, startDate, endDate, customerName);
return AjaxResult.success("导出任务已创建,请稍后查看进度", taskId);
}
/**
* 查询导出任务状态
*/
@PreAuthorize("@ss.hasPermi('wecom:customerExport:export')")
@GetMapping("/export/status/{taskId}")
public AjaxResult getExportStatus(@PathVariable Long taskId)
{
SysExportTask task = exportTaskService.selectExportTaskById(taskId);
if (task == null)
{
return AjaxResult.error("任务不存在");
}
return AjaxResult.success(task);
}
/**
* 下载导出文件
*/
@PreAuthorize("@ss.hasPermi('wecom:customerExport:export')")
@GetMapping("/export/download/{taskId}")
public void downloadExportFile(@PathVariable Long taskId, HttpServletResponse response)
{
SysExportTask task = exportTaskService.selectExportTaskById(taskId);
if (task == null)
{
return;
}
if (!SysExportTask.STATUS_SUCCESS.equals(task.getStatus()))
{
return;
}
String filePath = task.getFilePath();
if (filePath == null || filePath.isEmpty())
{
return;
}
File file = new File(filePath);
if (!file.exists())
{
return;
}
try
{
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("utf-8");
String fileName = URLEncoder.encode(task.getFileName(), StandardCharsets.UTF_8.toString()).replaceAll("\\+", "%20");
response.setHeader("Content-Disposition", "attachment;filename=" + fileName + ";filename*=utf-8''" + fileName);
response.setHeader("Content-Length", String.valueOf(file.length()));
java.io.FileInputStream fis = new java.io.FileInputStream(file);
java.io.OutputStream os = response.getOutputStream();
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = fis.read(buffer)) != -1)
{
os.write(buffer, 0, bytesRead);
}
fis.close();
os.flush();
}
catch (Exception e)
{
logger.error("下载文件失败", e);
}
}
/**
* 查询导出任务列表
*/
@PreAuthorize("@ss.hasPermi('wecom:customerExport:export')")
@GetMapping("/export/list")
public TableDataInfo exportTaskList(SysExportTask task)
{
String corpId = CorpContextHolder.getCurrentCorpId();
task.setCorpId(corpId);
task.setCreateBy(getUsername());
startPage();
List<SysExportTask> list = exportTaskService.selectExportTaskList(task);
return getDataTable(list);
}
/**
* 删除导出任务
*/
@PreAuthorize("@ss.hasPermi('wecom:customerExport:export')")
@Log(title = "客户统计数据-删除导出任务", businessType = BusinessType.DELETE)
@DeleteMapping("/export/{taskId}")
public AjaxResult deleteExportTask(@PathVariable Long taskId)
{
SysExportTask task = exportTaskService.selectExportTaskById(taskId);
if (task == null)
{
return AjaxResult.error("任务不存在");
}
if (task.getFilePath() != null && !task.getFilePath().isEmpty())
{
File file = new File(task.getFilePath());
if (file.exists())
{
file.delete();
}
}
return toAjax(exportTaskService.deleteExportTaskById(taskId));
}
} }

View File

@ -0,0 +1,209 @@
package com.ruoyi.system.domain;
import com.alibaba.fastjson2.annotation.JSONField;
import com.ruoyi.common.annotation.Excel;
import com.ruoyi.common.annotation.Excel.ColumnType;
import com.ruoyi.common.core.domain.BaseEntity;
import java.util.Date;
/**
* 导出任务表 sys_export_task
*/
public class SysExportTask extends BaseEntity
{
private static final long serialVersionUID = 1L;
public static final String STATUS_PENDING = "0";
public static final String STATUS_PROCESSING = "1";
public static final String STATUS_SUCCESS = "2";
public static final String STATUS_FAILED = "3";
@Excel(name = "任务ID", cellType = ColumnType.NUMERIC)
private Long taskId;
@Excel(name = "任务名称")
private String taskName;
@Excel(name = "任务类型")
private String taskType;
private String corpId;
private String queryParams;
@Excel(name = "状态", readConverterExp = "0=待处理,1=处理中,2=成功,3=失败")
private String status;
@Excel(name = "进度")
private Integer progress;
@Excel(name = "总数据量")
private Integer totalCount;
@Excel(name = "已处理数量")
private Integer processedCount;
private String filePath;
@Excel(name = "文件名")
private String fileName;
private Long fileSize;
private String errorMsg;
@JSONField(format = "yyyy-MM-dd HH:mm:ss")
@Excel(name = "完成时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
private Date finishTime;
public Long getTaskId()
{
return taskId;
}
public void setTaskId(Long taskId)
{
this.taskId = taskId;
}
public String getTaskName()
{
return taskName;
}
public void setTaskName(String taskName)
{
this.taskName = taskName;
}
public String getTaskType()
{
return taskType;
}
public void setTaskType(String taskType)
{
this.taskType = taskType;
}
public String getCorpId()
{
return corpId;
}
public void setCorpId(String corpId)
{
this.corpId = corpId;
}
public String getQueryParams()
{
return queryParams;
}
public void setQueryParams(String params)
{
this.queryParams = params;
}
public String getStatus()
{
return status;
}
public void setStatus(String status)
{
this.status = status;
}
public Integer getProgress()
{
return progress;
}
public void setProgress(Integer progress)
{
this.progress = progress;
}
public Integer getTotalCount()
{
return totalCount;
}
public void setTotalCount(Integer totalCount)
{
this.totalCount = totalCount;
}
public Integer getProcessedCount()
{
return processedCount;
}
public void setProcessedCount(Integer processedCount)
{
this.processedCount = processedCount;
}
public String getFilePath()
{
return filePath;
}
public void setFilePath(String filePath)
{
this.filePath = filePath;
}
public String getFileName()
{
return fileName;
}
public void setFileName(String fileName)
{
this.fileName = fileName;
}
public Long getFileSize()
{
return fileSize;
}
public void setFileSize(Long fileSize)
{
this.fileSize = fileSize;
}
public String getErrorMsg()
{
return errorMsg;
}
public void setErrorMsg(String errorMsg)
{
this.errorMsg = errorMsg;
}
public Date getFinishTime()
{
return finishTime;
}
public void setFinishTime(Date finishTime)
{
this.finishTime = finishTime;
}
public boolean isFinished()
{
return STATUS_SUCCESS.equals(status) || STATUS_FAILED.equals(status);
}
public boolean isSuccess()
{
return STATUS_SUCCESS.equals(status);
}
}

View File

@ -0,0 +1,66 @@
package com.ruoyi.system.mapper;
import java.util.List;
import com.ruoyi.system.domain.SysExportTask;
/**
* 导出任务 数据层
*/
public interface SysExportTaskMapper
{
/**
* 新增导出任务
*
* @param task 导出任务对象
* @return 结果
*/
public int insertExportTask(SysExportTask task);
/**
* 修改导出任务
*
* @param task 导出任务对象
* @return 结果
*/
public int updateExportTask(SysExportTask task);
/**
* 根据ID查询导出任务
*
* @param taskId 任务ID
* @return 导出任务对象
*/
public SysExportTask selectExportTaskById(Long taskId);
/**
* 查询导出任务列表
*
* @param task 导出任务对象
* @return 导出任务集合
*/
public List<SysExportTask> selectExportTaskList(SysExportTask task);
/**
* 删除导出任务
*
* @param taskId 任务ID
* @return 结果
*/
public int deleteExportTaskById(Long taskId);
/**
* 批量删除导出任务
*
* @param taskIds 任务ID数组
* @return 结果
*/
public int deleteExportTaskByIds(Long[] taskIds);
/**
* 查询过期的导出任务
*
* @param expireDays 过期天数
* @return 导出任务集合
*/
public List<SysExportTask> selectExpiredExportTasks(int expireDays);
}

View File

@ -0,0 +1,93 @@
package com.ruoyi.system.service;
import java.util.List;
import com.ruoyi.system.domain.SysExportTask;
/**
* 导出任务Service接口
*/
public interface ISysExportTaskService
{
/**
* 创建导出任务
*
* @param task 导出任务对象
* @return 任务ID
*/
public Long createExportTask(SysExportTask task);
/**
* 更新导出任务
*
* @param task 导出任务对象
* @return 结果
*/
public int updateExportTask(SysExportTask task);
/**
* 根据ID查询导出任务
*
* @param taskId 任务ID
* @return 导出任务对象
*/
public SysExportTask selectExportTaskById(Long taskId);
/**
* 查询导出任务列表
*
* @param task 导出任务对象
* @return 导出任务集合
*/
public List<SysExportTask> selectExportTaskList(SysExportTask task);
/**
* 删除导出任务
*
* @param taskId 任务ID
* @return 结果
*/
public int deleteExportTaskById(Long taskId);
/**
* 批量删除导出任务
*
* @param taskIds 任务ID数组
* @return 结果
*/
public int deleteExportTaskByIds(Long[] taskIds);
/**
* 更新任务进度
*
* @param taskId 任务ID
* @param processedCount 已处理数量
* @param totalCount 总数量
*/
public void updateProgress(Long taskId, int processedCount, int totalCount);
/**
* 标记任务成功
*
* @param taskId 任务ID
* @param filePath 文件路径
* @param fileName 文件名
* @param fileSize 文件大小
*/
public void markSuccess(Long taskId, String filePath, String fileName, Long fileSize);
/**
* 标记任务失败
*
* @param taskId 任务ID
* @param errorMsg 错误信息
*/
public void markFailed(Long taskId, String errorMsg);
/**
* 查询过期的导出任务
*
* @param expireDays 过期天数
* @return 导出任务集合
*/
public List<SysExportTask> selectExpiredExportTasks(int expireDays);
}

View File

@ -0,0 +1,107 @@
package com.ruoyi.system.service.impl;
import java.util.Date;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.system.domain.SysExportTask;
import com.ruoyi.system.mapper.SysExportTaskMapper;
import com.ruoyi.system.service.ISysExportTaskService;
/**
* 导出任务Service业务层处理
*/
@Service
public class SysExportTaskServiceImpl implements ISysExportTaskService
{
@Autowired
private SysExportTaskMapper exportTaskMapper;
@Override
public Long createExportTask(SysExportTask task)
{
task.setStatus(SysExportTask.STATUS_PENDING);
task.setProgress(0);
task.setCreateTime(DateUtils.getNowDate());
exportTaskMapper.insertExportTask(task);
return task.getTaskId();
}
@Override
public int updateExportTask(SysExportTask task)
{
return exportTaskMapper.updateExportTask(task);
}
@Override
public SysExportTask selectExportTaskById(Long taskId)
{
return exportTaskMapper.selectExportTaskById(taskId);
}
@Override
public List<SysExportTask> selectExportTaskList(SysExportTask task)
{
return exportTaskMapper.selectExportTaskList(task);
}
@Override
public int deleteExportTaskById(Long taskId)
{
return exportTaskMapper.deleteExportTaskById(taskId);
}
@Override
public int deleteExportTaskByIds(Long[] taskIds)
{
return exportTaskMapper.deleteExportTaskByIds(taskIds);
}
@Override
public void updateProgress(Long taskId, int processedCount, int totalCount)
{
SysExportTask task = new SysExportTask();
task.setTaskId(taskId);
task.setProcessedCount(processedCount);
task.setTotalCount(totalCount);
if (totalCount > 0)
{
int progress = (int) ((processedCount * 100.0) / totalCount);
task.setProgress(Math.min(progress, 99));
}
task.setStatus(SysExportTask.STATUS_PROCESSING);
exportTaskMapper.updateExportTask(task);
}
@Override
public void markSuccess(Long taskId, String filePath, String fileName, Long fileSize)
{
SysExportTask task = new SysExportTask();
task.setTaskId(taskId);
task.setStatus(SysExportTask.STATUS_SUCCESS);
task.setProgress(100);
task.setFilePath(filePath);
task.setFileName(fileName);
task.setFileSize(fileSize);
task.setFinishTime(new Date());
exportTaskMapper.updateExportTask(task);
}
@Override
public void markFailed(Long taskId, String errorMsg)
{
SysExportTask task = new SysExportTask();
task.setTaskId(taskId);
task.setStatus(SysExportTask.STATUS_FAILED);
task.setErrorMsg(errorMsg);
task.setFinishTime(new Date());
exportTaskMapper.updateExportTask(task);
}
@Override
public List<SysExportTask> selectExpiredExportTasks(int expireDays)
{
return exportTaskMapper.selectExpiredExportTasks(expireDays);
}
}

View File

@ -0,0 +1,134 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.system.mapper.SysExportTaskMapper">
<resultMap type="com.ruoyi.system.domain.SysExportTask" id="SysExportTaskResult">
<id property="taskId" column="task_id" />
<result property="taskName" column="task_name" />
<result property="taskType" column="task_type" />
<result property="corpId" column="corp_id" />
<result property="queryParams" column="query_params" />
<result property="status" column="status" />
<result property="progress" column="progress" />
<result property="totalCount" column="total_count" />
<result property="processedCount" column="processed_count" />
<result property="filePath" column="file_path" />
<result property="fileName" column="file_name" />
<result property="fileSize" column="file_size" />
<result property="errorMsg" column="error_msg" />
<result property="createBy" column="create_by" />
<result property="createTime" column="create_time" />
<result property="finishTime" column="finish_time" />
</resultMap>
<sql id="selectExportTaskVo">
select task_id, task_name, task_type, corp_id, query_params, status, progress, total_count, processed_count,
file_path, file_name, file_size, error_msg, create_by, create_time, finish_time
from sys_export_task
</sql>
<select id="selectExportTaskById" parameterType="Long" resultMap="SysExportTaskResult">
<include refid="selectExportTaskVo"/>
where task_id = #{taskId}
</select>
<select id="selectExportTaskList" parameterType="com.ruoyi.system.domain.SysExportTask" resultMap="SysExportTaskResult">
<include refid="selectExportTaskVo"/>
<where>
<if test="taskName != null and taskName != ''">
AND task_name like concat('%', #{taskName}, '%')
</if>
<if test="taskType != null and taskType != ''">
AND task_type = #{taskType}
</if>
<if test="corpId != null and corpId != ''">
AND corp_id = #{corpId}
</if>
<if test="status != null and status != ''">
AND status = #{status}
</if>
<if test="createBy != null and createBy != ''">
AND create_by = #{createBy}
</if>
</where>
order by create_time desc
</select>
<select id="selectExpiredExportTasks" parameterType="int" resultMap="SysExportTaskResult">
<include refid="selectExportTaskVo"/>
where status in ('2', '3')
and create_time &lt; date_sub(now(), interval #{expireDays} day)
</select>
<insert id="insertExportTask" parameterType="com.ruoyi.system.domain.SysExportTask" useGeneratedKeys="true" keyProperty="taskId">
insert into sys_export_task
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="taskName != null and taskName != ''">task_name,</if>
<if test="taskType != null and taskType != ''">task_type,</if>
<if test="corpId != null and corpId != ''">corp_id,</if>
<if test="queryParams != null">query_params,</if>
<if test="status != null and status != ''">status,</if>
<if test="progress != null">progress,</if>
<if test="totalCount != null">total_count,</if>
<if test="processedCount != null">processed_count,</if>
<if test="filePath != null and filePath != ''">file_path,</if>
<if test="fileName != null and fileName != ''">file_name,</if>
<if test="fileSize != null">file_size,</if>
<if test="errorMsg != null">error_msg,</if>
<if test="createBy != null and createBy != ''">create_by,</if>
<if test="createTime != null">create_time,</if>
<if test="finishTime != null">finish_time,</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="taskName != null and taskName != ''">#{taskName},</if>
<if test="taskType != null and taskType != ''">#{taskType},</if>
<if test="corpId != null and corpId != ''">#{corpId},</if>
<if test="queryParams != null">#{queryParams},</if>
<if test="status != null and status != ''">#{status},</if>
<if test="progress != null">#{progress},</if>
<if test="totalCount != null">#{totalCount},</if>
<if test="processedCount != null">#{processedCount},</if>
<if test="filePath != null and filePath != ''">#{filePath},</if>
<if test="fileName != null and fileName != ''">#{fileName},</if>
<if test="fileSize != null">#{fileSize},</if>
<if test="errorMsg != null">#{errorMsg},</if>
<if test="createBy != null and createBy != ''">#{createBy},</if>
<if test="createTime != null">#{createTime},</if>
<if test="finishTime != null">#{finishTime},</if>
</trim>
</insert>
<update id="updateExportTask" parameterType="com.ruoyi.system.domain.SysExportTask">
update sys_export_task
<set>
<if test="taskName != null and taskName != ''">task_name = #{taskName},</if>
<if test="taskType != null and taskType != ''">task_type = #{taskType},</if>
<if test="corpId != null and corpId != ''">corp_id = #{corpId},</if>
<if test="queryParams != null">query_params = #{queryParams},</if>
<if test="status != null and status != ''">status = #{status},</if>
<if test="progress != null">progress = #{progress},</if>
<if test="totalCount != null">total_count = #{totalCount},</if>
<if test="processedCount != null">processed_count = #{processedCount},</if>
<if test="filePath != null">file_path = #{filePath},</if>
<if test="fileName != null">file_name = #{fileName},</if>
<if test="fileSize != null">file_size = #{fileSize},</if>
<if test="errorMsg != null">error_msg = #{errorMsg},</if>
<if test="finishTime != null">finish_time = #{finishTime},</if>
</set>
where task_id = #{taskId}
</update>
<delete id="deleteExportTaskById" parameterType="Long">
delete from sys_export_task where task_id = #{taskId}
</delete>
<delete id="deleteExportTaskByIds" parameterType="Long">
delete from sys_export_task where task_id in
<foreach item="taskId" collection="array" open="(" separator="," close=")">
#{taskId}
</foreach>
</delete>
</mapper>

View File

@ -52,3 +52,37 @@ export function exportCustomerExport(query) {
responseType: 'blob' responseType: 'blob'
}) })
} }
// 异步导出客户导出数据
export function exportAsync(query) {
return request({
url: '/wecom/customerExport/exportAsync',
method: 'post',
params: query
})
}
// 查询导出任务状态
export function getExportStatus(taskId) {
return request({
url: '/wecom/customerExport/export/status/' + taskId,
method: 'get'
})
}
// 查询导出任务列表
export function listExportTask(query) {
return request({
url: '/wecom/customerExport/export/list',
method: 'get',
params: query
})
}
// 删除导出任务
export function delExportTask(taskId) {
return request({
url: '/wecom/customerExport/export/' + taskId,
method: 'delete'
})
}

View File

@ -42,7 +42,27 @@
size="mini" size="mini"
@click="handleExport" @click="handleExport"
v-hasPermi="['wecom:customerExport:export']" v-hasPermi="['wecom:customerExport:export']"
>导出</el-button> >同步导出</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="primary"
plain
icon="el-icon-download"
size="mini"
@click="handleAsyncExport"
v-hasPermi="['wecom:customerExport:export']"
>异步导出</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="info"
plain
icon="el-icon-tickets"
size="mini"
@click="showExportTaskDialog"
v-hasPermi="['wecom:customerExport:export']"
>导出任务</el-button>
</el-col> </el-col>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar> <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
</el-row> </el-row>
@ -99,45 +119,105 @@
:limit.sync="queryParams.pageSize" :limit.sync="queryParams.pageSize"
@pagination="getList" @pagination="getList"
/> />
<!-- 导出任务对话框 -->
<el-dialog title="导出任务列表" :visible.sync="taskDialogVisible" width="800px" append-to-body>
<el-table v-loading="taskLoading" :data="taskList">
<el-table-column label="任务ID" align="center" prop="taskId" width="80" />
<el-table-column label="任务名称" align="center" prop="taskName" width="150" />
<el-table-column label="状态" align="center" prop="status" width="100">
<template slot-scope="scope">
<el-tag :type="getStatusType(scope.row.status)">{{ getStatusLabel(scope.row.status) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="进度" align="center" prop="progress" width="150">
<template slot-scope="scope">
<el-progress
:percentage="scope.row.progress || 0"
:status="scope.row.status === '2' ? 'success' : scope.row.status === '3' ? 'exception' : ''"
/>
</template>
</el-table-column>
<el-table-column label="文件大小" align="center" prop="fileSize" width="100">
<template slot-scope="scope">
<span>{{ formatFileSize(scope.row.fileSize) }}</span>
</template>
</el-table-column>
<el-table-column label="创建时间" align="center" prop="createTime" width="160">
<template slot-scope="scope">
<span>{{ parseTime(scope.row.createTime) }}</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="150" fixed="right">
<template slot-scope="scope">
<el-button
size="mini"
type="text"
icon="el-icon-download"
@click="handleDownload(scope.row)"
:disabled="scope.row.status !== '2'"
>下载</el-button>
<el-button
size="mini"
type="text"
icon="el-icon-delete"
@click="handleDeleteTask(scope.row)"
>删除</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-show="taskTotal>0"
:total="taskTotal"
:page.sync="taskQueryParams.pageNum"
:limit.sync="taskQueryParams.pageSize"
@pagination="getTaskList"
/>
</el-dialog>
</div> </div>
</template> </template>
<script> <script>
import { listCustomerExport, exportCustomerExport } from "@/api/wecom/customerExport" import { listCustomerExport, exportAsync, getExportStatus, listExportTask, delExportTask } from "@/api/wecom/customerExport"
import { getToken } from "@/utils/auth"
import axios from 'axios'
export default { export default {
name: "CustomerExport", name: "CustomerExport",
data() { data() {
return { return {
//
loading: true, loading: true,
//
ids: [], ids: [],
//
single: true, single: true,
//
multiple: true, multiple: true,
//
showSearch: true, showSearch: true,
//
total: 0, total: 0,
//
dataList: [], dataList: [],
//
queryParams: { queryParams: {
pageNum: 1, pageNum: 1,
pageSize: 10, pageSize: 10,
startDate: undefined, startDate: undefined,
endDate: undefined, endDate: undefined,
customerName: undefined customerName: undefined
} },
taskDialogVisible: false,
taskLoading: false,
taskList: [],
taskTotal: 0,
taskQueryParams: {
pageNum: 1,
pageSize: 10
},
pollingTimers: {}
} }
}, },
created() { created() {
this.getList() this.getList()
}, },
beforeDestroy() {
Object.values(this.pollingTimers).forEach(timer => clearTimeout(timer))
},
methods: { methods: {
/** 查询客户导出数据列表 */
getList() { getList() {
this.loading = true this.loading = true
listCustomerExport(this.queryParams).then(response => { listCustomerExport(this.queryParams).then(response => {
@ -146,27 +226,115 @@ export default {
this.loading = false this.loading = false
}) })
}, },
/** 搜索按钮操作 */
handleQuery() { handleQuery() {
this.queryParams.pageNum = 1 this.queryParams.pageNum = 1
this.getList() this.getList()
}, },
/** 重置按钮操作 */
resetQuery() { resetQuery() {
this.resetForm("queryForm") this.resetForm("queryForm")
this.handleQuery() this.handleQuery()
}, },
//
handleSelectionChange(selection) { handleSelectionChange(selection) {
this.ids = selection.map(item => item.id) this.ids = selection.map(item => item.id)
this.single = selection.length !== 1 this.single = selection.length !== 1
this.multiple = !selection.length this.multiple = !selection.length
}, },
/** 导出按钮操作 */
handleExport() { handleExport() {
this.download('/wecom/customerExport/export', { this.download('/wecom/customerExport/export', {
...this.queryParams ...this.queryParams
}, `客户列表数据_${new Date().getTime()}.xlsx`) }, `客户列表数据_${new Date().getTime()}.xlsx`)
},
handleAsyncExport() {
const params = {
startDate: this.queryParams.startDate,
endDate: this.queryParams.endDate,
customerName: this.queryParams.customerName
}
exportAsync(params).then(response => {
this.$modal.msgSuccess("导出任务已创建,请稍后在导出任务列表中查看")
const taskId = response.data
this.startPolling(taskId)
})
},
startPolling(taskId) {
const poll = () => {
getExportStatus(taskId).then(response => {
const task = response.data
if (task.status === '2') {
this.$modal.msgSuccess(`导出任务完成,文件:${task.fileName}`)
delete this.pollingTimers[taskId]
} else if (task.status === '3') {
this.$modal.msgError(`导出任务失败:${task.errorMsg}`)
delete this.pollingTimers[taskId]
} else {
this.pollingTimers[taskId] = setTimeout(poll, 2000)
}
})
}
this.pollingTimers[taskId] = setTimeout(poll, 2000)
},
showExportTaskDialog() {
this.taskDialogVisible = true
this.getTaskList()
},
getTaskList() {
this.taskLoading = true
listExportTask(this.taskQueryParams).then(response => {
this.taskList = response.rows
this.taskTotal = response.total
this.taskLoading = false
})
},
getStatusType(status) {
const map = {
'0': 'info',
'1': 'warning',
'2': 'success',
'3': 'danger'
}
return map[status] || 'info'
},
getStatusLabel(status) {
const map = {
'0': '待处理',
'1': '处理中',
'2': '已完成',
'3': '失败'
}
return map[status] || '未知'
},
formatFileSize(bytes) {
if (!bytes) return '-'
if (bytes < 1024) return bytes + ' B'
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(2) + ' KB'
return (bytes / (1024 * 1024)).toFixed(2) + ' MB'
},
handleDownload(row) {
const url = process.env.VUE_APP_BASE_API + '/wecom/customerExport/export/download/' + row.taskId
axios({
method: 'get',
url: url,
responseType: 'blob',
headers: { 'Authorization': 'Bearer ' + getToken() }
}).then((res) => {
const blob = new Blob([res.data], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' })
const link = document.createElement('a')
link.href = window.URL.createObjectURL(blob)
link.download = row.fileName || '导出文件.xlsx'
link.click()
window.URL.revokeObjectURL(link.href)
}).catch((error) => {
console.error('下载失败', error)
this.$modal.msgError('下载失败,请重试')
})
},
handleDeleteTask(row) {
this.$modal.confirm('是否确认删除该导出任务?').then(() => {
return delExportTask(row.taskId)
}).then(() => {
this.getTaskList()
this.$modal.msgSuccess("删除成功")
}).catch(() => {})
} }
} }
} }

24
sql/sys_export_task.sql Normal file
View File

@ -0,0 +1,24 @@
-- 导出任务表
DROP TABLE IF EXISTS sys_export_task;
CREATE TABLE sys_export_task (
task_id BIGINT NOT NULL AUTO_INCREMENT COMMENT '任务ID',
task_name VARCHAR(100) DEFAULT '' COMMENT '任务名称',
task_type VARCHAR(50) DEFAULT '' COMMENT '任务类型',
corp_id VARCHAR(64) DEFAULT '' COMMENT '企业ID',
query_params TEXT COMMENT '导出参数(JSON格式)',
status CHAR(1) DEFAULT '0' COMMENT '状态:0待处理,1处理中,2成功,3失败',
progress INT DEFAULT 0 COMMENT '进度百分比',
total_count INT DEFAULT 0 COMMENT '总数据量',
processed_count INT DEFAULT 0 COMMENT '已处理数量',
file_path VARCHAR(500) DEFAULT '' COMMENT '文件路径',
file_name VARCHAR(200) DEFAULT '' COMMENT '文件名',
file_size BIGINT DEFAULT 0 COMMENT '文件大小(字节)',
error_msg TEXT COMMENT '错误信息',
create_by VARCHAR(64) DEFAULT '' COMMENT '创建者',
create_time DATETIME COMMENT '创建时间',
finish_time DATETIME COMMENT '完成时间',
PRIMARY KEY (task_id),
INDEX idx_corp_id (corp_id),
INDEX idx_status (status),
INDEX idx_create_time (create_time)
) ENGINE=InnoDB AUTO_INCREMENT=1 COMMENT='导出任务表';