|
|
@@ -0,0 +1,631 @@
|
|
|
+package com.grapecity.controller;
|
|
|
+
|
|
|
+import com.alibaba.fastjson2.JSON;
|
|
|
+import com.alibaba.fastjson2.JSONArray;
|
|
|
+import com.alibaba.fastjson2.JSONObject;
|
|
|
+import com.grapecity.documents.excel.*;
|
|
|
+import com.grapecity.documents.excel.drawing.ImageType;
|
|
|
+import com.grapecity.documents.excel.template.DataSource.JsonDataSource;
|
|
|
+import jakarta.servlet.http.HttpServletResponse;
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
+import org.springframework.http.HttpHeaders;
|
|
|
+import org.springframework.http.MediaType;
|
|
|
+import org.springframework.web.bind.annotation.PostMapping;
|
|
|
+import org.springframework.web.bind.annotation.RequestBody;
|
|
|
+import org.springframework.web.bind.annotation.RequestMapping;
|
|
|
+import org.springframework.web.bind.annotation.RestController;
|
|
|
+
|
|
|
+import java.io.ByteArrayInputStream;
|
|
|
+import java.io.ByteArrayOutputStream;
|
|
|
+import java.net.URLEncoder;
|
|
|
+import java.nio.charset.StandardCharsets;
|
|
|
+import java.util.*;
|
|
|
+
|
|
|
+/**
|
|
|
+ * Excel / 葡萄城 SJS 文件 处理控制器
|
|
|
+ * 接收 pressure2 收集的完整数据,使用 GcExcel 处理后返回 SJS 文件
|
|
|
+ * 不依赖 pressure2 的任何服务,所有数据通过请求参数传入
|
|
|
+ */
|
|
|
+@RestController
|
|
|
+@Slf4j
|
|
|
+@RequestMapping("/excel")
|
|
|
+public class ExcelController {
|
|
|
+
|
|
|
+ @PostMapping("/process")
|
|
|
+ public void processExcel(@RequestBody Map<String, Object> request, HttpServletResponse response) {
|
|
|
+ doProcessExcel(request, true, response);
|
|
|
+ }
|
|
|
+
|
|
|
+ @PostMapping("/process-no-page")
|
|
|
+ public void processExcelNoPage(@RequestBody Map<String, Object> request, HttpServletResponse response) {
|
|
|
+ doProcessExcel(request, false, response);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 核心处理入口:从请求中提取数据,使用 GcExcel 处理工作簿,返回 SJS 文件
|
|
|
+ */
|
|
|
+ private void doProcessExcel(Map<String, Object> request, boolean defaultContinuePage, HttpServletResponse response) {
|
|
|
+ try {
|
|
|
+ byte[] templateBytes = convertToByteArray(request.get("templateBytes"), "templateBytes");
|
|
|
+ Map<String, Object> data = extractDataMap(request.get("data"));
|
|
|
+ Map<String, byte[]> fileBytes = convertFileBytesMap(request.get("fileBytes"));
|
|
|
+ List<String> editCols = extractStringList(request.get("editCols"));
|
|
|
+ List<String> imgCols = extractStringList(request.get("imgCols"));
|
|
|
+ Boolean continuePage = request.get("continuePage") != null
|
|
|
+ ? Boolean.valueOf(request.get("continuePage").toString())
|
|
|
+ : defaultContinuePage;
|
|
|
+ String copyConfigJson = request.get("copyConfigJson") != null
|
|
|
+ ? request.get("copyConfigJson").toString()
|
|
|
+ : null;
|
|
|
+ List<String> existingColCodes = extractStringList(request.get("existingColCodes"));
|
|
|
+
|
|
|
+ log.info("开始处理 Excel, continuePage={}", continuePage);
|
|
|
+
|
|
|
+ byte[] processedBytes = processWorkbook(templateBytes, data, fileBytes, imgCols, editCols);
|
|
|
+
|
|
|
+ List<String> newColCodes = new ArrayList<>();
|
|
|
+ if (Boolean.TRUE.equals(continuePage) && copyConfigJson != null) {
|
|
|
+ processedBytes = handleContinuationPages(processedBytes, copyConfigJson, existingColCodes, newColCodes);
|
|
|
+ }
|
|
|
+
|
|
|
+ String fileName = URLEncoder.encode("report.sjs", StandardCharsets.UTF_8);
|
|
|
+ response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
|
|
|
+ response.setHeader(HttpHeaders.CONTENT_DISPOSITION,
|
|
|
+ "attachment; filename*=UTF-8''" + fileName);
|
|
|
+ response.setContentLength(processedBytes.length);
|
|
|
+
|
|
|
+ if (!newColCodes.isEmpty()) {
|
|
|
+ response.setHeader("X-New-Col-Codes", JSON.toJSONString(newColCodes));
|
|
|
+ }
|
|
|
+
|
|
|
+ response.getOutputStream().write(processedBytes);
|
|
|
+ response.getOutputStream().flush();
|
|
|
+
|
|
|
+ log.info("Excel 处理完成, 文件大小={}KB", processedBytes.length / 1024);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("处理 Excel 失败", e);
|
|
|
+ response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
|
|
|
+ try {
|
|
|
+ response.getWriter().write("{\"error\":\"" + e.getMessage() + "\"}");
|
|
|
+ } catch (Exception ignored) {
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 核心处理逻辑:使用 GcExcel 处理工作簿
|
|
|
+ * 包含:设置数据源、图片处理、可编辑字段高亮、表格样式同步
|
|
|
+ */
|
|
|
+ private byte[] processWorkbook(byte[] templateBytes, Map<String, Object> dataMap,
|
|
|
+ Map<String, byte[]> fileBytes,
|
|
|
+ List<String> imgCols, List<String> editCols) throws Exception {
|
|
|
+ WorkbookOptions workbookOptions = new WorkbookOptions();
|
|
|
+ workbookOptions.setPixelBasedColumnWidth(true);
|
|
|
+
|
|
|
+ Workbook workbook = new Workbook(workbookOptions);
|
|
|
+ workbook.open(new ByteArrayInputStream(templateBytes), OpenFileFormat.Sjs);
|
|
|
+
|
|
|
+ for (int i = 0; i < workbook.getWorksheets().getCount(); i++) {
|
|
|
+ IWorksheet worksheet = workbook.getWorksheets().get(i);
|
|
|
+
|
|
|
+ Map<ITable, List<int[]>> tableMergeInfo = collectTableMergeInfo(worksheet);
|
|
|
+
|
|
|
+ worksheet.setDataSource(new JsonDataSource(JSON.toJSONString(dataMap)));
|
|
|
+
|
|
|
+ copyTableRowStyles(worksheet, tableMergeInfo);
|
|
|
+
|
|
|
+ handleCellImages(worksheet, fileBytes);
|
|
|
+
|
|
|
+ handleImageCols(worksheet, imgCols, dataMap, fileBytes);
|
|
|
+
|
|
|
+ handleFloatingImages(worksheet, dataMap, fileBytes);
|
|
|
+
|
|
|
+ handleCheckboxCells(worksheet);
|
|
|
+
|
|
|
+ highlightEditableCells(worksheet, editCols);
|
|
|
+ }
|
|
|
+
|
|
|
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
|
|
+ workbook.save(outputStream, SaveFileFormat.Sjs);
|
|
|
+ return outputStream.toByteArray();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 处理单元格中的图片:将以 .jpg/.png 结尾的值替换为背景图片
|
|
|
+ * 使用传入的 fileBytes 替代 fileApi 获取图片数据
|
|
|
+ */
|
|
|
+ private void handleCellImages(IWorksheet worksheet, Map<String, byte[]> fileBytes) {
|
|
|
+ IRange usedRange = worksheet.getUsedRange();
|
|
|
+ if (usedRange == null) return;
|
|
|
+
|
|
|
+ for (int row = 0; row < usedRange.getRowCount(); row++) {
|
|
|
+ for (int col = 0; col < usedRange.getColumnCount(); col++) {
|
|
|
+ IRange cell = worksheet.getRange(row, col);
|
|
|
+ if (cell.getBindingPath() == null || cell.getValue() == null) continue;
|
|
|
+
|
|
|
+ Object valueObj = cell.getValue();
|
|
|
+ String value = valueObj instanceof String ? (String) valueObj : String.valueOf(valueObj);
|
|
|
+
|
|
|
+ if (!value.endsWith(".png") && !value.endsWith(".jpg")) continue;
|
|
|
+
|
|
|
+ if (!value.contains(",")) {
|
|
|
+ byte[] imgBytes = fileBytes.get(value);
|
|
|
+ if (imgBytes != null) {
|
|
|
+ cell.setValue(null);
|
|
|
+ IRange mergeArea = cell.getMergeArea();
|
|
|
+ mergeArea.setBackgroundImage(imgBytes);
|
|
|
+ } else {
|
|
|
+ log.warn("未找到图片数据: {}", value);
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ String[] split = value.split(",");
|
|
|
+ byte[][] imgBytesArr = new byte[split.length][];
|
|
|
+ boolean allSuccess = true;
|
|
|
+ for (int k = 0; k < split.length; k++) {
|
|
|
+ String trimmedPath = split[k].trim();
|
|
|
+ imgBytesArr[k] = fileBytes.get(trimmedPath);
|
|
|
+ if (imgBytesArr[k] == null) {
|
|
|
+ log.warn("未找到图片数据: {}", trimmedPath);
|
|
|
+ allSuccess = false;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (allSuccess && imgBytesArr.length > 0) {
|
|
|
+ cell.setValue(null);
|
|
|
+ IRange mergeArea = cell.getMergeArea();
|
|
|
+ byte[] mergedImg = GrapeCityController.mergeImages(imgBytesArr);
|
|
|
+ mergeArea.setBackgroundImage(mergedImg);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 处理图片字段类型 (colValType == 4):单元格中存储 JSON 数组 [{url, x, y, width, height}]
|
|
|
+ * 使用传入的 fileBytes 替代 fileApi 获取图片数据
|
|
|
+ */
|
|
|
+ private void handleImageCols(IWorksheet worksheet, List<String> imgCols, Map<String, Object> dataMap,
|
|
|
+ Map<String, byte[]> fileBytes) {
|
|
|
+ if (imgCols == null || imgCols.isEmpty()) return;
|
|
|
+ if (dataMap == null) return;
|
|
|
+
|
|
|
+ IRange usedRange = worksheet.getUsedRange();
|
|
|
+ if (usedRange == null) return;
|
|
|
+
|
|
|
+ for (int row = 0; row < usedRange.getRowCount(); row++) {
|
|
|
+ for (int col = 0; col < usedRange.getColumnCount(); col++) {
|
|
|
+ String bindingPath = worksheet.getRange(row, col).getBindingPath();
|
|
|
+ if (bindingPath == null || !imgCols.contains(bindingPath)) continue;
|
|
|
+
|
|
|
+ Object valueObj = dataMap.get(bindingPath);
|
|
|
+ if (valueObj == null) continue;
|
|
|
+
|
|
|
+ JSONArray imgArr = toJSONArray(valueObj);
|
|
|
+ if (imgArr == null || imgArr.isEmpty()) continue;
|
|
|
+
|
|
|
+ worksheet.getRange(row, col).setValue(null);
|
|
|
+
|
|
|
+ for (int k = 0; k < imgArr.size(); k++) {
|
|
|
+ try {
|
|
|
+ JSONObject imgItem = toJSONObject(imgArr.get(k));
|
|
|
+ if (imgItem == null) continue;
|
|
|
+ String url = imgItem.getString("url");
|
|
|
+ if (url == null) continue;
|
|
|
+
|
|
|
+ byte[] imgBytes = fileBytes.get(url);
|
|
|
+ if (imgBytes == null) {
|
|
|
+ log.warn("未找到图片字段数据: {}", url);
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ ByteArrayInputStream imgStream = new ByteArrayInputStream(imgBytes);
|
|
|
+ worksheet.getShapes().addPictureInPixel(
|
|
|
+ JSON.toJSONString(imgItem),
|
|
|
+ imgStream, ImageType.JPG,
|
|
|
+ imgItem.getDoubleValue("x"),
|
|
|
+ imgItem.getDoubleValue("y"),
|
|
|
+ imgItem.getDoubleValue("width"),
|
|
|
+ imgItem.getDoubleValue("height")
|
|
|
+ );
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("添加图片形状失败: index={}", k, e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 处理浮动图片:从数据源的 Illustration 字段解析并添加
|
|
|
+ * 使用传入的 fileBytes 替代 fileApi 获取图片数据
|
|
|
+ */
|
|
|
+ private void handleFloatingImages(IWorksheet worksheet, Map<String, Object> dataMap,
|
|
|
+ Map<String, byte[]> fileBytes) {
|
|
|
+ if (dataMap == null) return;
|
|
|
+
|
|
|
+ Object illustrationObj = dataMap.get("Illustration");
|
|
|
+ if (illustrationObj == null) return;
|
|
|
+
|
|
|
+ JSONArray jsonArray = toJSONArray(illustrationObj);
|
|
|
+ if (jsonArray == null || jsonArray.isEmpty()) return;
|
|
|
+
|
|
|
+ List<JSONObject> remainingItems = new ArrayList<>();
|
|
|
+ for (int k = 0; k < jsonArray.size(); k++) {
|
|
|
+ JSONObject jsonObject = toJSONObject(jsonArray.get(k));
|
|
|
+ if (jsonObject == null) continue;
|
|
|
+ String sheetName = jsonObject.getString("sheet");
|
|
|
+ if (worksheet.getName().equals(sheetName)) {
|
|
|
+ try {
|
|
|
+ String url = jsonObject.getString("url");
|
|
|
+ byte[] imgBytes = fileBytes.get(url);
|
|
|
+ if (imgBytes == null) {
|
|
|
+ log.warn("未找到浮动图片数据: {}", url);
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ ByteArrayInputStream imgStream = new ByteArrayInputStream(imgBytes);
|
|
|
+ worksheet.getShapes().addPictureInPixel(
|
|
|
+ url,
|
|
|
+ imgStream, ImageType.JPG,
|
|
|
+ jsonObject.getDoubleValue("x"),
|
|
|
+ jsonObject.getDoubleValue("y"),
|
|
|
+ jsonObject.getDoubleValue("width"),
|
|
|
+ jsonObject.getDoubleValue("height")
|
|
|
+ );
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("添加浮动图片失败: {}", e.getMessage());
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ remainingItems.add(jsonObject);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!remainingItems.isEmpty()) {
|
|
|
+ dataMap.put("Illustration", JSON.toJSONString(remainingItems));
|
|
|
+ } else {
|
|
|
+ dataMap.remove("Illustration");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 处理复选框单元格:清空非 "true" 的值
|
|
|
+ */
|
|
|
+ private void handleCheckboxCells(IWorksheet worksheet) {
|
|
|
+ IRange usedRange = worksheet.getUsedRange();
|
|
|
+ if (usedRange == null) return;
|
|
|
+
|
|
|
+ for (int row = 0; row < usedRange.getRowCount(); row++) {
|
|
|
+ for (int col = 0; col < usedRange.getColumnCount(); col++) {
|
|
|
+ IRange cell = worksheet.getRange(row, col);
|
|
|
+ if (cell.getCellType() instanceof CheckBoxCellType) {
|
|
|
+ Object val = cell.getValue();
|
|
|
+ if (val == null) continue;
|
|
|
+ String strVal = val instanceof String ? (String) val : String.valueOf(val);
|
|
|
+ if (!"true".equals(strVal)) {
|
|
|
+ cell.setValue(null);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 高亮可编辑字段:全部锁定 → 可编辑字段解锁 + 设置 #FFF895 背景色
|
|
|
+ * 对齐前端 reportUtil.editReport 中的处理逻辑
|
|
|
+ */
|
|
|
+ private void highlightEditableCells(IWorksheet worksheet, List<String> editCols) {
|
|
|
+ if (editCols == null || editCols.isEmpty()) return;
|
|
|
+
|
|
|
+ IRange usedRange = worksheet.getUsedRange();
|
|
|
+ if (usedRange == null) return;
|
|
|
+ worksheet.protect();
|
|
|
+ // ① 全部单元格设为不可编辑(对齐前端 cellRange.allowEditInCell(false))
|
|
|
+ usedRange.setLocked(true);
|
|
|
+
|
|
|
+ // ② 遍历:解锁可编辑字段,同时收集地址
|
|
|
+ List<String> editableAddresses = new ArrayList<>();
|
|
|
+ for (int row = 0; row < usedRange.getRowCount(); row++) {
|
|
|
+ for (int col = 0; col < usedRange.getColumnCount(); col++) {
|
|
|
+ IRange cell = worksheet.getRange(row, col);
|
|
|
+ String bindingPath = cell.getBindingPath();
|
|
|
+ if (bindingPath != null && editCols.contains(bindingPath)) {
|
|
|
+ // 解锁可编辑(对齐前端 sheet.getCell(i,j).allowEditInCell(true))
|
|
|
+ cell.setLocked(false);
|
|
|
+ editableAddresses.add(cell.getAddress());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // ③ 批量设置条件格式(对齐前端 cfs.addSpecificTextRule(greaterThanOrEqualsTo, null, style, arr))
|
|
|
+ if (!editableAddresses.isEmpty()) {
|
|
|
+ String unionAddress = String.join(",", editableAddresses);
|
|
|
+ IFormatCondition condition = (IFormatCondition) worksheet.getRange(unionAddress)
|
|
|
+ .getFormatConditions().add(FormatConditionType.Expression, FormatConditionOperator.None, "=TRUE", null);
|
|
|
+ // #FFF895 = RGB(255, 248, 149)
|
|
|
+ condition.getInterior().setColor(Color.FromArgb(255, 255, 248, 149));
|
|
|
+ System.out.println(condition.getFormula1());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 表格行样式同步 ====================
|
|
|
+
|
|
|
+ private Map<ITable, List<int[]>> collectTableMergeInfo(IWorksheet worksheet) {
|
|
|
+ Map<ITable, List<int[]>> result = new HashMap<>();
|
|
|
+ if (worksheet.getTables() == null) return result;
|
|
|
+
|
|
|
+ for (int t = 0; t < worksheet.getTables().getCount(); t++) {
|
|
|
+ ITable table = worksheet.getTables().get(t);
|
|
|
+ IRange tableRange = table.getRange();
|
|
|
+ int firstDataRow = tableRange.getRow() + 1;
|
|
|
+ int firstDataCol = tableRange.getColumn();
|
|
|
+ int colCount = tableRange.getColumnCount();
|
|
|
+
|
|
|
+ List<int[]> mergeRanges = new ArrayList<>();
|
|
|
+ try {
|
|
|
+ for (int c = firstDataCol; c < firstDataCol + colCount; c++) {
|
|
|
+ IRange cell = worksheet.getRange(firstDataRow, c);
|
|
|
+ IRange mergeArea = cell.getMergeArea();
|
|
|
+ if (mergeArea != null && mergeArea.getColumnCount() > 1) {
|
|
|
+ mergeRanges.add(new int[]{mergeArea.getColumn(), mergeArea.getColumnCount()});
|
|
|
+ c = mergeArea.getColumn() + mergeArea.getColumnCount() - 1;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (Exception ignored) {
|
|
|
+ }
|
|
|
+ result.put(table, mergeRanges);
|
|
|
+ }
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ private void copyTableRowStyles(IWorksheet worksheet, Map<ITable, List<int[]>> tableMergeInfo) {
|
|
|
+ if (worksheet.getTables() == null) return;
|
|
|
+
|
|
|
+ for (int t = 0; t < worksheet.getTables().getCount(); t++) {
|
|
|
+ ITable table = worksheet.getTables().get(t);
|
|
|
+ IRange tableRange = table.getRange();
|
|
|
+ int rowCount = tableRange.getRowCount();
|
|
|
+ int colCount = tableRange.getColumnCount();
|
|
|
+ if (rowCount <= 1) continue;
|
|
|
+
|
|
|
+ int firstDataRow = tableRange.getRow();
|
|
|
+ int firstDataCol = tableRange.getColumn();
|
|
|
+
|
|
|
+ List<int[]> mergeColRanges = tableMergeInfo.getOrDefault(table, new ArrayList<>());
|
|
|
+
|
|
|
+ double firstRowHeight = worksheet.getRange(firstDataRow, firstDataCol).getRowHeight();
|
|
|
+ IRange sourceRow = worksheet.getRange(firstDataRow, firstDataCol, 1, colCount);
|
|
|
+
|
|
|
+ for (int r = 1; r < rowCount; r++) {
|
|
|
+ int targetRow = tableRange.getRow() + r;
|
|
|
+ IRange targetRowRange = worksheet.getRange(targetRow, firstDataCol, 1, colCount);
|
|
|
+ PasteOption pasteOption = new PasteOption();
|
|
|
+ pasteOption.setPasteType(EnumSet.of(PasteType.Formats));
|
|
|
+ sourceRow.copy(targetRowRange, pasteOption);
|
|
|
+ worksheet.getRange(targetRow, firstDataCol).setRowHeight(firstRowHeight);
|
|
|
+
|
|
|
+ for (int[] mergeRange : mergeColRanges) {
|
|
|
+ try {
|
|
|
+ worksheet.getRange(targetRow, mergeRange[0], 1, mergeRange[1]).merge();
|
|
|
+ } catch (Exception ignored) {
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ tableRange.setWrapText(true);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 续页处理 ====================
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 处理续页:检查 copyConfig 配置,处理隐藏空页和自动生成续页
|
|
|
+ * copyConfigJson 和 existingColCodes 由 pressure2 传入,newColCodes 通过参数返回给调用方
|
|
|
+ */
|
|
|
+ private byte[] handleContinuationPages(byte[] sjsBytes, String copyConfigJson,
|
|
|
+ List<String> existingColCodes, List<String> newColCodes) {
|
|
|
+ try {
|
|
|
+ JSONObject copyConfig = JSON.parseObject(copyConfigJson);
|
|
|
+
|
|
|
+ Boolean isContinuePage = copyConfig.getBoolean("isContinuePage");
|
|
|
+ if (!Boolean.TRUE.equals(isContinuePage)) return sjsBytes;
|
|
|
+
|
|
|
+ String sheetName = copyConfig.getString("sheetName");
|
|
|
+ if (sheetName == null) return sjsBytes;
|
|
|
+
|
|
|
+ JSONObject copyRange = copyConfig.getJSONObject("copyRange");
|
|
|
+ if (copyRange == null) return sjsBytes;
|
|
|
+
|
|
|
+ String[] topLeft = copyRange.getString("topLeft").split(",");
|
|
|
+ String[] topRight = copyRange.getString("topRight").split(",");
|
|
|
+ String[] bottomLeft = copyRange.getString("bottomLeft").split(",");
|
|
|
+
|
|
|
+ int col = Integer.parseInt(topLeft[0]);
|
|
|
+ int row = Integer.parseInt(topLeft[1]);
|
|
|
+ int colCount = Integer.parseInt(topRight[0]) - col;
|
|
|
+ int rowCount = Integer.parseInt(bottomLeft[1]) - row;
|
|
|
+
|
|
|
+ WorkbookOptions workbookOptions = new WorkbookOptions();
|
|
|
+ workbookOptions.setPixelBasedColumnWidth(true);
|
|
|
+ Workbook workbook = new Workbook(workbookOptions);
|
|
|
+ workbook.open(new ByteArrayInputStream(sjsBytes), OpenFileFormat.Sjs);
|
|
|
+
|
|
|
+ IWorksheet targetSheet = null;
|
|
|
+ for (int i = 0; i < workbook.getWorksheets().getCount(); i++) {
|
|
|
+ if (workbook.getWorksheets().get(i).getName().equals(sheetName)) {
|
|
|
+ targetSheet = workbook.getWorksheets().get(i);
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (targetSheet == null) return sjsBytes;
|
|
|
+
|
|
|
+ Boolean hidden = copyConfig.getBoolean("hidden");
|
|
|
+ if (Boolean.TRUE.equals(hidden)) {
|
|
|
+ boolean isEmpty = true;
|
|
|
+ for (int i = 0; i <= colCount; i++) {
|
|
|
+ Object cellValue = targetSheet.getRange(row, col + i).getValue();
|
|
|
+ if (cellValue != null && !cellValue.toString().isEmpty()) {
|
|
|
+ isEmpty = false;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (isEmpty) {
|
|
|
+ targetSheet.setVisible(Visibility.Hidden);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ List<String> effectiveExistingColCodes = existingColCodes != null ? existingColCodes : new ArrayList<>();
|
|
|
+
|
|
|
+ int sheetNum = 2;
|
|
|
+
|
|
|
+ boolean isNotEmpty = false;
|
|
|
+ for (int i = 0; i <= colCount; i++) {
|
|
|
+ Object cellValue = targetSheet.getRange(row + rowCount, col + i).getValue();
|
|
|
+ if (cellValue != null && !cellValue.toString().isEmpty()) {
|
|
|
+ isNotEmpty = true;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (isNotEmpty) {
|
|
|
+ generateContinuedSheet(workbook, targetSheet, sheetName, sheetNum,
|
|
|
+ col, row, colCount, rowCount, effectiveExistingColCodes, newColCodes);
|
|
|
+ sheetNum++;
|
|
|
+ }
|
|
|
+
|
|
|
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
|
|
+ workbook.save(outputStream, SaveFileFormat.Sjs);
|
|
|
+ return outputStream.toByteArray();
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("续页处理失败,回退返回原始 SJS", e);
|
|
|
+ return sjsBytes;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 生成续页工作表:复制 sheet 并调整绑定路径
|
|
|
+ * 新字段编码通过 newColCodes 参数返回给调用方
|
|
|
+ */
|
|
|
+ private void generateContinuedSheet(Workbook workbook, IWorksheet sourceSheet,
|
|
|
+ String sheetName, int sheetNum,
|
|
|
+ int col, int row, int colCount, int rowCount,
|
|
|
+ List<String> existingColCodes, List<String> newColCodes) {
|
|
|
+ String sheetJson = sourceSheet.toJson();
|
|
|
+ IWorksheet newSheet = workbook.getWorksheets().add();
|
|
|
+ newSheet.fromJson(sheetJson);
|
|
|
+ String newSheetName = sheetName + " (" + sheetNum + ")";
|
|
|
+ newSheet.setName(newSheetName);
|
|
|
+
|
|
|
+ for (int i = 0; i <= colCount; i++) {
|
|
|
+ for (int j = 0; j <= rowCount; j++) {
|
|
|
+ String bindingPath = sourceSheet.getRange(row + j, col + i).getBindingPath();
|
|
|
+ if (bindingPath != null && bindingPath.contains("_")) {
|
|
|
+ String prefix = bindingPath.substring(0, bindingPath.lastIndexOf("_") + 1);
|
|
|
+ String numStr = bindingPath.substring(bindingPath.lastIndexOf("_") + 1);
|
|
|
+ try {
|
|
|
+ int num = Integer.parseInt(numStr);
|
|
|
+ int newNum = num + (rowCount + 1) * (sheetNum - 1);
|
|
|
+ String newPath = prefix + newNum;
|
|
|
+ newSheet.getRange(row + j, col + i).setBindingPath(newPath);
|
|
|
+ if (!existingColCodes.contains(newPath) && !newColCodes.contains(newPath)) {
|
|
|
+ newColCodes.add(newPath);
|
|
|
+ }
|
|
|
+ } catch (NumberFormatException ignored) {
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 请求参数提取工具方法 ====================
|
|
|
+
|
|
|
+ @SuppressWarnings("unchecked")
|
|
|
+ private byte[] convertToByteArray(Object obj, String paramName) {
|
|
|
+ if (obj == null) {
|
|
|
+ throw new IllegalArgumentException(paramName + " 不能为空");
|
|
|
+ }
|
|
|
+ if (obj instanceof byte[]) {
|
|
|
+ return (byte[]) obj;
|
|
|
+ }
|
|
|
+ if (obj instanceof String str) {
|
|
|
+ return Base64.getDecoder().decode(str);
|
|
|
+ }
|
|
|
+ if (obj instanceof List) {
|
|
|
+ List<?> list = (List<?>) obj;
|
|
|
+ byte[] result = new byte[list.size()];
|
|
|
+ for (int i = 0; i < list.size(); i++) {
|
|
|
+ Object item = list.get(i);
|
|
|
+ if (item instanceof Number) {
|
|
|
+ result[i] = ((Number) item).byteValue();
|
|
|
+ } else {
|
|
|
+ throw new IllegalArgumentException(paramName + " 中包含非数字元素");
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+ throw new IllegalArgumentException(paramName + " 必须是 byte[]/List<Number>/Base64String 类型");
|
|
|
+ }
|
|
|
+
|
|
|
+ @SuppressWarnings("unchecked")
|
|
|
+ private Map<String, Object> extractDataMap(Object dataObj) {
|
|
|
+ if (dataObj == null) return new HashMap<>();
|
|
|
+ if (dataObj instanceof Map) return (Map<String, Object>) dataObj;
|
|
|
+ throw new IllegalArgumentException("data 必须是 Map 类型");
|
|
|
+ }
|
|
|
+
|
|
|
+ @SuppressWarnings("unchecked")
|
|
|
+ private Map<String, byte[]> convertFileBytesMap(Object fileBytesObj) {
|
|
|
+ if (fileBytesObj == null) return new HashMap<>();
|
|
|
+ if (!(fileBytesObj instanceof Map)) {
|
|
|
+ throw new IllegalArgumentException("fileBytes 必须是 Map 类型");
|
|
|
+ }
|
|
|
+ Map<?, ?> rawMap = (Map<?, ?>) fileBytesObj;
|
|
|
+ Map<String, byte[]> result = new HashMap<>(rawMap.size());
|
|
|
+ for (Map.Entry<?, ?> entry : rawMap.entrySet()) {
|
|
|
+ String key = entry.getKey() != null ? entry.getKey().toString() : null;
|
|
|
+ if (key == null || key.isEmpty()) continue;
|
|
|
+ byte[] value = convertToByteArray(entry.getValue(), "fileBytes[" + key + "]");
|
|
|
+ result.put(key, value);
|
|
|
+ }
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ @SuppressWarnings("unchecked")
|
|
|
+ private List<String> extractStringList(Object obj) {
|
|
|
+ if (obj == null) return new ArrayList<>();
|
|
|
+ if (obj instanceof List) return (List<String>) obj;
|
|
|
+ return new ArrayList<>();
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== JSON 类型兼容工具方法 ====================
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 将对象安全转换为 JSONArray
|
|
|
+ * 兼容 fastjson2 JSONArray 和 Jackson 反序列化的 ArrayList
|
|
|
+ */
|
|
|
+ private JSONArray toJSONArray(Object obj) {
|
|
|
+ if (obj instanceof JSONArray arr) return arr;
|
|
|
+ if (obj instanceof List<?> list) {
|
|
|
+ JSONArray arr = new JSONArray();
|
|
|
+ arr.addAll(list);
|
|
|
+ return arr;
|
|
|
+ }
|
|
|
+ if (obj instanceof String str && str.trim().startsWith("[")) {
|
|
|
+ try {
|
|
|
+ return JSON.parseArray(str);
|
|
|
+ } catch (Exception ignored) {
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 将对象安全转换为 JSONObject
|
|
|
+ * 兼容 fastjson2 JSONObject 和 Jackson 反序列化的 LinkedHashMap
|
|
|
+ */
|
|
|
+ private JSONObject toJSONObject(Object obj) {
|
|
|
+ if (obj instanceof JSONObject jo) return jo;
|
|
|
+ if (obj instanceof Map<?, ?> map) {
|
|
|
+ return new JSONObject(map);
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+}
|