导出纳新

This commit is contained in:
2026-05-16 21:24:31 +08:00
parent 9171a93e46
commit f75c037ece
3 changed files with 293 additions and 6 deletions

View File

@@ -13,6 +13,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import jakarta.servlet.http.HttpServletResponse;
import java.time.LocalDate;
import java.util.Map;
@@ -154,6 +155,19 @@ public class LbPurchaseApplyController {
return ResponseEntity.badRequest().body(result);
}
@GetMapping("/export")
@Operation(summary = "导出进货申请Excel", description = "按租户ID与申请日期范围导出按申请日分组并合并首列")
public void export(
@Parameter(description = "申请开始日期(yyyy-MM-dd)", required = true)
@RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate applyDateStart,
@Parameter(description = "申请结束日期(yyyy-MM-dd)", required = true)
@RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate applyDateEnd,
@Parameter(description = "租户ID", required = true)
@RequestParam String tenantId,
HttpServletResponse response) {
lbPurchaseApplyService.exportExcel(applyDateStart, applyDateEnd, tenantId, response);
}
@GetMapping("/needPrivilegeRecommenders")
@Operation(summary = "按日期范围查询需要添加特权的推荐人")
public ResponseEntity<Map<String, Object>> needPrivilegeRecommenders(

View File

@@ -2,6 +2,7 @@ package com.rj.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.rj.entity.LbPurchaseApply;
import jakarta.servlet.http.HttpServletResponse;
import java.time.LocalDate;
import java.util.Map;
@@ -48,4 +49,9 @@ public interface ILbPurchaseApplyService extends IService<LbPurchaseApply> {
* @param tenantId 当前登录人租户 ID回填到结果实体覆盖模型输出
*/
Map<String, Object> parseFromOrderInfoByLlm(String orderInfo, String tenantId);
/**
* 按申请日期区间与租户导出进货申请 Excel按申请日分组首列合并单元格
*/
void exportExcel(LocalDate applyDateStart, LocalDate applyDateEnd, String tenantId, HttpServletResponse response);
}

View File

@@ -21,11 +21,23 @@ import com.rj.mapper.LbDailyUserTradeReportMapper;
import com.rj.mapper.LbPurchaseApplyMapper;
import com.rj.service.HxrAdminUserService;
import com.rj.service.ILbPurchaseApplyService;
import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.ss.util.CellRangeAddress;
import org.apache.poi.xssf.usermodel.XSSFCellStyle;
import org.apache.poi.xssf.usermodel.XSSFColor;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Service;
import java.awt.Color;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
@@ -33,6 +45,7 @@ import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@@ -60,6 +73,8 @@ public class LbPurchaseApplyServiceImpl
private String dashScopeChatModel;
private static final DateTimeFormatter TRY_DATE_MM_DD = DateTimeFormatter.ofPattern("MM.dd");
private static final DateTimeFormatter PRIVILEGE_RANGE_FMT = DateTimeFormatter.ofPattern("MM.dd");
private static final DateTimeFormatter EXPORT_FILENAME_TS = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
/** 试用期包含的工作日天数(周一至周五),周六日不计入。 */
private static final int TRIAL_WEEKDAY_COUNT = 3;
@@ -188,7 +203,7 @@ public class LbPurchaseApplyServiceImpl
}
String existingTryDate = entity.getTryDate();
LocalDate trialFirstWeekday = firstWeekdayOnOrAfter(applyDate);
LocalDate endDate = endOfInclusiveWeekdaySpan(trialFirstWeekday, TRIAL_WEEKDAY_COUNT);
LocalDate endDate = endOfInclusiveWeekdaySpan(trialFirstWeekday, TRIAL_WEEKDAY_COUNT, log);
String computed =
applyDate.format(TRY_DATE_MM_DD) + "" + endDate.format(TRY_DATE_MM_DD);
@@ -209,14 +224,14 @@ public class LbPurchaseApplyServiceImpl
* 从 {@code firstWeekday} 起连续 {@code inclusiveWeekdays} 个工作日(仅周一至周五,含起点当日),返回该段最后一天。
* 若起点落在周六日,会先顺延到下一工作日再计数,且返回值保证不为周六日。
*/
private LocalDate endOfInclusiveWeekdaySpan(LocalDate firstWeekday, int inclusiveWeekdays) {
private static LocalDate endOfInclusiveWeekdaySpan(LocalDate firstWeekday, int inclusiveWeekdays, Logger logger) {
if (inclusiveWeekdays < 1) {
throw new IllegalArgumentException("inclusiveWeekdays must be >= 1");
}
LocalDate d = firstWeekday;
while (IsoWorkdayUtils.isWeekend(d)) {
if (log.isDebugEnabled()) {
log.debug(
if (logger != null && logger.isDebugEnabled()) {
logger.debug(
"trialSpan起点为周末顺延 cursor={} dow={} isoDow={}",
d,
d.getDayOfWeek(),
@@ -228,8 +243,8 @@ public class LbPurchaseApplyServiceImpl
int guard = 0;
while (true) {
boolean weekend = IsoWorkdayUtils.isWeekend(d);
if (log.isDebugEnabled()) {
log.debug(
if (logger != null && logger.isDebugEnabled()) {
logger.debug(
"trialSpancursor={} dow={} isoDow={} weekend={} weekdaysRemaining={}",
d,
d.getDayOfWeek(),
@@ -787,4 +802,256 @@ public class LbPurchaseApplyServiceImpl
return result;
}
}
@Override
public void exportExcel(LocalDate applyDateStart, LocalDate applyDateEnd, String tenantId, HttpServletResponse response) {
try {
if (tenantId == null || tenantId.trim().isEmpty()) {
response.sendError(HttpServletResponse.SC_BAD_REQUEST, "tenantId不能为空");
return;
}
if (applyDateStart == null || applyDateEnd == null) {
response.sendError(HttpServletResponse.SC_BAD_REQUEST, "申请开始日期与结束日期不能为空");
return;
}
if (applyDateEnd.isBefore(applyDateStart)) {
response.sendError(HttpServletResponse.SC_BAD_REQUEST, "结束日期不能早于开始日期");
return;
}
LambdaQueryWrapper<LbPurchaseApply> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(LbPurchaseApply::getTenantId, tenantId.trim())
.ge(LbPurchaseApply::getApplyDate, applyDateStart)
.le(LbPurchaseApply::getApplyDate, applyDateEnd)
.orderByAsc(LbPurchaseApply::getApplyDate)
.orderByAsc(LbPurchaseApply::getId);
List<LbPurchaseApply> rows = this.list(queryWrapper);
Map<String, Integer> maxOrderByColleaguePhone = loadColleagueMaxOrderCache(rows);
LinkedHashMap<LocalDate, List<LbPurchaseApply>> grouped = new LinkedHashMap<>();
for (LbPurchaseApply row : rows) {
LocalDate key = row.getApplyDate();
if (key == null) {
key = LocalDate.MIN;
}
grouped.computeIfAbsent(key, k -> new ArrayList<>()).add(row);
}
try (Workbook workbook = new XSSFWorkbook()) {
Sheet sheet = workbook.createSheet("进货申请");
CellStyle headerStyle = createHeaderStyle(workbook);
CellStyle groupStyle = createGroupStyle(workbook);
CellStyle dataStyle = createDataStyle(workbook);
String[] headers = {
"日期和人数", "序号", "昵称", "手机号", "特权开通日期",
"推荐人", "手机号", "领导人", "开通日期"
};
Row headerRow = sheet.createRow(0);
headerRow.setHeightInPoints(22);
for (int i = 0; i < headers.length; i++) {
createExportCell(headerRow, i, headers[i], headerStyle);
}
int rowIndex = 1;
for (Map.Entry<LocalDate, List<LbPurchaseApply>> entry : grouped.entrySet()) {
LocalDate applyDate = entry.getKey();
List<LbPurchaseApply> groupRows = entry.getValue();
int groupStartRow = rowIndex;
String groupLabel = formatDatePeopleLabel(applyDate, groupRows.size());
int serial = 1;
for (LbPurchaseApply item : groupRows) {
Row row = sheet.createRow(rowIndex);
row.setHeightInPoints(20);
createExportCell(row, 1, String.valueOf(serial), dataStyle);
createExportCell(row, 2, nullToEmpty(item.getApplyUser()), dataStyle);
createExportCell(row, 3, nullToEmpty(item.getApplyPhone()), dataStyle);
createExportCell(row, 4, formatPrivilegeRange(item), dataStyle);
createExportCell(row, 5, formatRecommender(item, maxOrderByColleaguePhone), dataStyle);
createExportCell(row, 6, nullToEmpty(item.getColleaguePhone()), dataStyle);
createExportCell(row, 7, nullToEmpty(item.getTeamLeader()), dataStyle);
createExportCell(row, 8, formatActivationDate(item.getApplyDate()), dataStyle);
serial++;
rowIndex++;
}
int groupEndRow = rowIndex - 1;
Row firstRow = sheet.getRow(groupStartRow);
createExportCell(firstRow, 0, groupLabel, groupStyle);
if (groupEndRow > groupStartRow) {
sheet.addMergedRegion(new CellRangeAddress(groupStartRow, groupEndRow, 0, 0));
}
}
adjustExportColumnWidths(sheet, headers);
String filename = "进货申请_" + LocalDateTime.now().format(EXPORT_FILENAME_TS) + ".xlsx";
String encoded = URLEncoder.encode(filename, StandardCharsets.UTF_8).replaceAll("\\+", "%20");
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename*=UTF-8''" + encoded);
try (ServletOutputStream os = response.getOutputStream()) {
workbook.write(os);
os.flush();
}
}
} catch (Exception e) {
log.error("进货申请导出异常", e);
try {
response.reset();
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
response.setContentType("text/plain;charset=UTF-8");
response.getWriter().write("导出异常:" + e.getMessage());
} catch (Exception ignored) {
// ignore
}
}
}
private Map<String, Integer> loadColleagueMaxOrderCache(List<LbPurchaseApply> rows) {
Map<String, Integer> cache = new HashMap<>();
Set<String> phones = new HashSet<>();
for (LbPurchaseApply row : rows) {
if (row.getColleaguePhone() != null && !row.getColleaguePhone().trim().isEmpty()) {
phones.add(row.getColleaguePhone().trim());
}
}
for (String phone : phones) {
try {
Optional<HxrUserRow> userOpt = hxrAdminUserService.fetchFirstUserByMobile(phone);
userOpt.map(HxrUserRow::maxOrder).ifPresent(maxOrder -> cache.put(phone, maxOrder));
} catch (Exception ex) {
log.debug("导出时查询推荐人 max_order 失败phone={}", phone, ex);
}
}
return cache;
}
private static String formatDatePeopleLabel(LocalDate applyDate, int count) {
if (applyDate == null || applyDate.equals(LocalDate.MIN)) {
return "未知日期招商" + count + "";
}
return applyDate.getMonthValue() + "." + applyDate.getDayOfMonth() + "招商" + count + "";
}
private static String formatPrivilegeRange(LbPurchaseApply item) {
LocalDate applyDate = item.getApplyDate();
if (applyDate == null) {
return item.getTryDate() != null ? item.getTryDate().trim() : "";
}
LocalDate start = firstWeekdayOnOrAfter(applyDate);
LocalDate endDate = endOfInclusiveWeekdaySpan(start, TRIAL_WEEKDAY_COUNT, null);
return start.format(PRIVILEGE_RANGE_FMT) + "" + endDate.format(PRIVILEGE_RANGE_FMT);
}
private static String formatRecommender(LbPurchaseApply item, Map<String, Integer> maxOrderByPhone) {
String name = item.getColleagueName();
if (name == null || name.trim().isEmpty()) {
return "";
}
String trimmedName = name.trim();
String phone = item.getColleaguePhone() == null ? "" : item.getColleaguePhone().trim();
if (!phone.isEmpty()) {
Integer maxOrder = maxOrderByPhone.get(phone);
if (maxOrder != null) {
return trimmedName + " " + maxOrder;
}
}
return trimmedName;
}
private static String formatActivationDate(LocalDate applyDate) {
if (applyDate == null || applyDate.equals(LocalDate.MIN)) {
return "";
}
LocalDate activation = firstWeekdayOnOrAfter(applyDate);
return activation.getYear() + "/" + activation.getMonthValue() + "/" + activation.getDayOfMonth();
}
private static CellStyle createHeaderStyle(Workbook workbook) {
CellStyle style = workbook.createCellStyle();
Font font = workbook.createFont();
font.setBold(true);
style.setFont(font);
style.setAlignment(HorizontalAlignment.CENTER);
style.setVerticalAlignment(VerticalAlignment.CENTER);
style.setFillPattern(FillPatternType.SOLID_FOREGROUND);
((XSSFCellStyle) style).setFillForegroundColor(new XSSFColor(new Color(255, 217, 102), null));
setThinBorders(style);
return style;
}
private static CellStyle createGroupStyle(Workbook workbook) {
CellStyle style = workbook.createCellStyle();
style.setAlignment(HorizontalAlignment.CENTER);
style.setVerticalAlignment(VerticalAlignment.CENTER);
style.setFillPattern(FillPatternType.SOLID_FOREGROUND);
((XSSFCellStyle) style).setFillForegroundColor(new XSSFColor(new Color(255, 230, 204), null));
setThinBorders(style);
return style;
}
private static CellStyle createDataStyle(Workbook workbook) {
CellStyle style = workbook.createCellStyle();
style.setAlignment(HorizontalAlignment.CENTER);
style.setVerticalAlignment(VerticalAlignment.CENTER);
setThinBorders(style);
return style;
}
private static void setThinBorders(CellStyle style) {
style.setBorderTop(BorderStyle.THIN);
style.setBorderBottom(BorderStyle.THIN);
style.setBorderLeft(BorderStyle.THIN);
style.setBorderRight(BorderStyle.THIN);
}
private static void createExportCell(Row row, int col, String text, CellStyle style) {
Cell cell = row.createCell(col);
cell.setCellValue(text == null ? "" : text);
cell.setCellStyle(style);
}
private static String nullToEmpty(String value) {
return value == null ? "" : value;
}
private static void adjustExportColumnWidths(Sheet sheet, String[] headers) {
int columnCount = headers.length;
int[] maxWidths = new int[columnCount];
for (int i = 0; i < columnCount; i++) {
maxWidths[i] = calcExportDisplayWidth(headers[i]);
}
for (int r = 0; r <= sheet.getLastRowNum(); r++) {
Row row = sheet.getRow(r);
if (row == null) {
continue;
}
for (int c = 0; c < columnCount; c++) {
Cell cell = row.getCell(c);
if (cell != null && cell.getCellType() == CellType.STRING) {
maxWidths[c] = Math.max(maxWidths[c], calcExportDisplayWidth(cell.getStringCellValue()));
}
}
}
for (int i = 0; i < columnCount; i++) {
int width = Math.min(255 * 256, (maxWidths[i] + 3) * 256);
sheet.setColumnWidth(i, width);
}
}
private static int calcExportDisplayWidth(String text) {
if (text == null || text.isEmpty()) {
return 0;
}
int width = 0;
for (char ch : text.toCharArray()) {
width += ch > 127 ? 2 : 1;
}
return width;
}
}