|
|
@@ -0,0 +1,341 @@
|
|
|
+package cn.start.tz.module.pressure2.controller.appapi.excel;
|
|
|
+
|
|
|
+import cn.hutool.http.HttpResponse;
|
|
|
+import cn.hutool.http.HttpUtil;
|
|
|
+import cn.start.tz.module.infra.api.file.FileApi;
|
|
|
+import cn.start.tz.module.pressure2.controller.admin.dynamictb.vo.DynamicTbRespVO;
|
|
|
+import cn.start.tz.module.pressure2.controller.admin.dynamictbcol.vo.DynamicTbColRespVO;
|
|
|
+import cn.start.tz.module.pressure2.controller.admin.dynamictbval.vo.DynamicTBAndColVO;
|
|
|
+import cn.start.tz.module.pressure2.controller.admin.dynamictbval.vo.DynamicTBViewOrAddVO;
|
|
|
+import cn.start.tz.module.pressure2.controller.admin.dynamictbval.vo.DynamicTbValRespVO;
|
|
|
+import cn.start.tz.module.pressure2.controller.admin.excel.dto.GrapeCityReqDTO;
|
|
|
+import cn.start.tz.module.pressure2.service.dynamictb.DynamicTbService;
|
|
|
+import cn.start.tz.module.pressure2.service.dynamictbcol.DynamicTbColService;
|
|
|
+import cn.start.tz.module.pressure2.service.dynamictbval.DynamicTbValService;
|
|
|
+import cn.start.tz.module.pressure2.service.excel.ExcelService;
|
|
|
+import cn.start.tz.module.pressure2.service.standardfile.StandardTemplateService;
|
|
|
+import cn.start.tz.module.system.api.standard.StandardTemplateApi;
|
|
|
+import com.alibaba.fastjson2.JSON;
|
|
|
+import com.alibaba.fastjson2.JSONArray;
|
|
|
+import com.alibaba.fastjson2.JSONObject;
|
|
|
+import io.swagger.v3.oas.annotations.Operation;
|
|
|
+import io.swagger.v3.oas.annotations.tags.Tag;
|
|
|
+import jakarta.annotation.Resource;
|
|
|
+import jakarta.servlet.http.HttpServletResponse;
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
+import org.springframework.beans.factory.annotation.Value;
|
|
|
+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.net.URLEncoder;
|
|
|
+import java.nio.charset.StandardCharsets;
|
|
|
+import java.util.*;
|
|
|
+import java.util.stream.Collectors;
|
|
|
+
|
|
|
+/**
|
|
|
+ * Excel / 葡萄城文件 控制器
|
|
|
+ * pressure2 负责收集业务数据,通过 HTTP 调用 grape-city 服务处理 GcExcel 逻辑
|
|
|
+ * pressure2 没有葡萄城授权,所有 GcExcel 操作委托给 grape-city 服务
|
|
|
+ */
|
|
|
+@RestController
|
|
|
+@RequestMapping("/pressure2/excel")
|
|
|
+@Tag(name = "管理后台 - 葡萄城 Excel 处理")
|
|
|
+@Slf4j
|
|
|
+public class AppapiExcelController {
|
|
|
+
|
|
|
+ @Value("${grapecity.url:null}")
|
|
|
+ private String grapecityUrl;
|
|
|
+
|
|
|
+ @Resource
|
|
|
+ private StandardTemplateApi standardTemplateApi;
|
|
|
+
|
|
|
+ @Resource
|
|
|
+ private StandardTemplateService standardTemplateService;
|
|
|
+
|
|
|
+ @Resource
|
|
|
+ private FileApi fileApi;
|
|
|
+
|
|
|
+ @Resource
|
|
|
+ private DynamicTbValService dynamicTbValService;
|
|
|
+
|
|
|
+ @Resource
|
|
|
+ private DynamicTbColService dynamicTbColService;
|
|
|
+
|
|
|
+ @Resource
|
|
|
+ private DynamicTbService dynamicTbService;
|
|
|
+
|
|
|
+ @Resource
|
|
|
+ private ExcelService excelService;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 原有接口(保留兼容)
|
|
|
+ * 调用葡萄城服务 /excel/process
|
|
|
+ */
|
|
|
+ @RequestMapping("/excel")
|
|
|
+ public void excel(@RequestBody GrapeCityReqDTO grapeCityReqDTO, HttpServletResponse response) {
|
|
|
+ callGrapeCityProcess(grapeCityReqDTO.getTemplateId(), grapeCityReqDTO.getRefId(), true, response);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 原有接口(保留兼容)
|
|
|
+ * 调用葡萄城服务 /excel/process-no-page
|
|
|
+ */
|
|
|
+ @RequestMapping("/excel-no-page")
|
|
|
+ public void excelNoPage(@RequestBody GrapeCityReqDTO grapeCityReqDTO, HttpServletResponse response) {
|
|
|
+ callGrapeCityProcess(grapeCityReqDTO.getTemplateId(), grapeCityReqDTO.getRefId(), false, response);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 处理并返回编辑好的葡萄城 SJS 文件(含续页)
|
|
|
+ *
|
|
|
+ */
|
|
|
+ @PostMapping("/process")
|
|
|
+ @Operation(summary = "后端处理葡萄城 Excel 文件(数据绑定、图片、可编辑字段、续页)")
|
|
|
+ public void processExcel(@RequestBody GrapeCityReqDTO reqDTO, HttpServletResponse response) throws Exception {
|
|
|
+ writeSjsResponse(excelService.processExcel(reqDTO.getTemplateId(), reqDTO.getRefId()), response);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 处理并返回编辑好的葡萄城 SJS 文件(不处理续页)
|
|
|
+ *
|
|
|
+ */
|
|
|
+ @PostMapping("/process-no-page")
|
|
|
+ @Operation(summary = "后端处理葡萄城 Excel 文件(不处理续页)")
|
|
|
+ public void processExcelNoPage(@RequestBody GrapeCityReqDTO reqDTO, HttpServletResponse response) throws Exception {
|
|
|
+ writeSjsResponse(excelService.processExcel(reqDTO.getTemplateId(), reqDTO.getRefId(), false), response);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 核心方法:收集 pressure2 的业务数据,调用 grape-city 服务处理 GcExcel 逻辑
|
|
|
+ * 流程:获取模板 → 获取动态数据 → 收集图片 → 构建请求 → 调用 grape-city → 返回 SJS
|
|
|
+ */
|
|
|
+ private void callGrapeCityProcess(String templateId, String refId, boolean continuePage,
|
|
|
+ HttpServletResponse response) {
|
|
|
+ try {
|
|
|
+ // 校验葡萄城服务地址
|
|
|
+ if (grapecityUrl == null || "null".equals(grapecityUrl) || grapecityUrl.isEmpty()) {
|
|
|
+ throw new IllegalStateException("葡萄城服务地址未配置(grapecity.url),无法处理 Excel");
|
|
|
+ }
|
|
|
+
|
|
|
+ log.info("开始收集 Excel 数据, templateId={}, refId={}, continuePage={}", templateId, refId, continuePage);
|
|
|
+
|
|
|
+ // 1. 获取模板 SJS 字节流
|
|
|
+// CommonResult<StandardTemplateRespDTO> standardTemplate = standardTemplateApi.getStandardTemplate(templateId);
|
|
|
+// String fileUrl = standardTemplate.getCheckedData().getFileUrl();
|
|
|
+ String fileUrl = standardTemplateService.getById(templateId).getFileUrl();
|
|
|
+ byte[] templateBytes = fileApi.getFileByPath(fileUrl).getCheckedData();
|
|
|
+
|
|
|
+ // 2. 获取动态报表数据(实例 + 值列表 + 字段列表)
|
|
|
+ DynamicTBViewOrAddVO queryVO = new DynamicTBViewOrAddVO();
|
|
|
+ queryVO.setTemplateId(templateId);
|
|
|
+ queryVO.setRefId(refId);
|
|
|
+ DynamicTBAndColVO tbData = dynamicTbValService.getDynamicTbInsAndValByRefId(queryVO);
|
|
|
+
|
|
|
+ if (tbData == null || tbData.getDynamicTbInsRespVO() == null) {
|
|
|
+ log.warn("未找到动态报表实例数据, templateId={}, refId={}", templateId, refId);
|
|
|
+ writeSjsResponse(templateBytes, response);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 3. 构建数据 Map(将 JSON 字符串解析为实际对象,确保 GcExcel 模板引擎能正确处理)
|
|
|
+ Map<String, Object> dataMap = buildDataMap(tbData.getDynamicTbValRespVOList());
|
|
|
+
|
|
|
+ // 4. 获取可编辑字段列表
|
|
|
+ List<String> editCols = tbData.getDynamicTbColRespVOList().stream()
|
|
|
+ .filter(i -> Boolean.TRUE.equals(i.getIsEdit()))
|
|
|
+ .map(DynamicTbColRespVO::getColCode)
|
|
|
+ .collect(Collectors.toList());
|
|
|
+
|
|
|
+ // 5. 获取图片字段列表(colValType == 4)
|
|
|
+ List<String> imgCols = tbData.getDynamicTbColRespVOList().stream()
|
|
|
+ .filter(i -> i.getColValType() != null && i.getColValType() == 4)
|
|
|
+ .map(DynamicTbColRespVO::getColCode)
|
|
|
+ .collect(Collectors.toList());
|
|
|
+
|
|
|
+ // 6. 收集所有图片路径并下载图片字节
|
|
|
+ Map<String, byte[]> fileBytes = collectImageBytes(dataMap, imgCols);
|
|
|
+
|
|
|
+ // 7. 获取续页配置
|
|
|
+ String copyConfigJson = null;
|
|
|
+ List<String> existingColCodes = null;
|
|
|
+ if (continuePage) {
|
|
|
+ DynamicTbRespVO templateInfo = dynamicTbService.getDynamicTb(templateId);
|
|
|
+ Object rawConfig = templateInfo != null ? templateInfo.getCopyConfig() : null;
|
|
|
+ if (rawConfig != null) {
|
|
|
+ // 兼容 fastjson1 JSONObject → JSON 字符串
|
|
|
+ copyConfigJson = rawConfig instanceof JSONObject
|
|
|
+ ? ((JSONObject) rawConfig).toJSONString()
|
|
|
+ : rawConfig.toString();
|
|
|
+ }
|
|
|
+ existingColCodes = tbData.getDynamicTbColRespVOList().stream()
|
|
|
+ .map(DynamicTbColRespVO::getColCode)
|
|
|
+ .collect(Collectors.toList());
|
|
|
+ }
|
|
|
+
|
|
|
+ // 8. 构建请求参数,发送到 grape-city 服务
|
|
|
+ Map<String, Object> requestMap = new HashMap<>();
|
|
|
+ requestMap.put("templateBytes", templateBytes);
|
|
|
+ requestMap.put("data", dataMap);
|
|
|
+ requestMap.put("fileBytes", fileBytes);
|
|
|
+ requestMap.put("editCols", editCols);
|
|
|
+ requestMap.put("imgCols", imgCols);
|
|
|
+ requestMap.put("continuePage", continuePage);
|
|
|
+ if (copyConfigJson != null) {
|
|
|
+ requestMap.put("copyConfigJson", copyConfigJson);
|
|
|
+ }
|
|
|
+ if (existingColCodes != null) {
|
|
|
+ requestMap.put("existingColCodes", existingColCodes);
|
|
|
+ }
|
|
|
+
|
|
|
+ String endpoint = continuePage ? "/excel/process" : "/excel/process-no-page";
|
|
|
+ String jsonString = JSON.toJSONString(requestMap);
|
|
|
+
|
|
|
+ log.info("调用葡萄城服务: {}{}, 数据大小={}KB", grapecityUrl, endpoint, jsonString.length() / 1024);
|
|
|
+
|
|
|
+ HttpResponse httpResponse = HttpUtil.createPost(grapecityUrl + endpoint)
|
|
|
+ .body(jsonString)
|
|
|
+ .contentType("application/json")
|
|
|
+ .execute();
|
|
|
+
|
|
|
+ // 9. 读取响应
|
|
|
+ byte[] sjsBytes = httpResponse.bodyBytes();
|
|
|
+
|
|
|
+ // 10. 处理续页新字段(grape-city 通过响应头返回新字段编码)
|
|
|
+ String newColCodesHeader = httpResponse.header("X-New-Col-Codes");
|
|
|
+ if (newColCodesHeader != null && !newColCodesHeader.isEmpty()) {
|
|
|
+ try {
|
|
|
+ List<String> newColCodes = JSON.parseArray(newColCodesHeader, String.class);
|
|
|
+ if (!newColCodes.isEmpty()) {
|
|
|
+ dynamicTbColService.createDynamicTbColByCodes(newColCodes, templateId);
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("创建续页字段失败: {}", e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 11. 返回 SJS 文件
|
|
|
+ writeSjsResponse(sjsBytes, response);
|
|
|
+
|
|
|
+ log.info("Excel 处理完成, templateId={}, refId={}, 文件大小={}KB", templateId, refId, sjsBytes.length / 1024);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("处理 Excel 失败, templateId={}, refId={}", templateId, refId, e);
|
|
|
+ response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
|
|
|
+ try {
|
|
|
+ response.getWriter().write("{\"error\":\"" + e.getMessage() + "\"}");
|
|
|
+ } catch (Exception ignored) {
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 构建数据 Map,将 JSON 字符串解析为实际对象
|
|
|
+ * 确保 GcExcel 模板引擎能正确处理表格绑定
|
|
|
+ */
|
|
|
+ private Map<String, Object> buildDataMap(List<DynamicTbValRespVO> valRespVOList) {
|
|
|
+ Map<String, Object> dataMap = new HashMap<>();
|
|
|
+ if (valRespVOList == null) return dataMap;
|
|
|
+
|
|
|
+ for (DynamicTbValRespVO item : valRespVOList) {
|
|
|
+ String colCode = item.getColCode();
|
|
|
+ String valValue = item.getValValue();
|
|
|
+ if (colCode == null || valValue == null) continue;
|
|
|
+
|
|
|
+ String trimmed = valValue.trim();
|
|
|
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
|
|
+ try {
|
|
|
+ dataMap.put(colCode, JSON.parse(trimmed));
|
|
|
+ } catch (Exception e) {
|
|
|
+ dataMap.put(colCode, valValue);
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ dataMap.put(colCode, valValue);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return dataMap;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 收集所有图片路径并下载图片字节
|
|
|
+ * 三种图片来源:
|
|
|
+ * 1. 数据值中以 .png/.jpg 结尾的字符串(单元格背景图)
|
|
|
+ * 2. 图片字段(imgCols)中的 JSON 数组 [{url, x, y, width, height}]
|
|
|
+ * 3. Illustration 浮动图片字段中的 JSON 数组 [{sheet, url, x, y, width, height}]
|
|
|
+ */
|
|
|
+ private Map<String, byte[]> collectImageBytes(Map<String, Object> dataMap, List<String> imgCols) {
|
|
|
+ Map<String, byte[]> fileBytes = new HashMap<>();
|
|
|
+ Set<String> imagePaths = new HashSet<>();
|
|
|
+
|
|
|
+ // 扫描所有数据值,收集图片路径
|
|
|
+ for (Map.Entry<String, Object> entry : dataMap.entrySet()) {
|
|
|
+ collectImagePathsFromValue(entry.getValue(), imagePaths,
|
|
|
+ imgCols != null && imgCols.contains(entry.getKey()));
|
|
|
+ }
|
|
|
+
|
|
|
+ // 下载所有图片
|
|
|
+ for (String path : imagePaths) {
|
|
|
+ try {
|
|
|
+ byte[] data = fileApi.getFileByPath(path).getCheckedData();
|
|
|
+ fileBytes.put(path, data);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("获取图片失败: path={}", path);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return fileBytes;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 从数据值中递归收集图片路径
|
|
|
+ *
|
|
|
+ * @param value 数据值(可能是 String、JSONArray、JSONObject 等)
|
|
|
+ * @param paths 收集到的图片路径集合
|
|
|
+ * @param isImgCol 是否是图片字段(colValType == 4)
|
|
|
+ */
|
|
|
+ private void collectImagePathsFromValue(Object value, Set<String> paths, boolean isImgCol) {
|
|
|
+ if (value == null) return;
|
|
|
+
|
|
|
+ if (value instanceof String str) {
|
|
|
+ // 单元格背景图片:以 .png/.jpg 结尾的路径
|
|
|
+ if (str.endsWith(".png") || str.endsWith(".jpg")) {
|
|
|
+ if (str.contains(",")) {
|
|
|
+ for (String path : str.split(",")) {
|
|
|
+ String trimmed = path.trim();
|
|
|
+ if (!trimmed.isEmpty()) {
|
|
|
+ paths.add(trimmed);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ paths.add(str);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } else if (value instanceof JSONArray arr) {
|
|
|
+ // 图片字段或 Illustration 中的 JSON 数组
|
|
|
+ for (int i = 0; i < arr.size(); i++) {
|
|
|
+ Object item = arr.get(i);
|
|
|
+ if (item instanceof JSONObject obj) {
|
|
|
+ String url = obj.getString("url");
|
|
|
+ if (url != null && !url.isEmpty()) {
|
|
|
+ paths.add(url);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 将 SJS 字节流写入 HTTP 响应
|
|
|
+ */
|
|
|
+ private void writeSjsResponse(byte[] sjsBytes, HttpServletResponse response) throws Exception {
|
|
|
+ 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(sjsBytes.length);
|
|
|
+ response.getOutputStream().write(sjsBytes);
|
|
|
+ response.getOutputStream().flush();
|
|
|
+ }
|
|
|
+}
|