Forráskód Böngészése

Merge remote-tracking branch 'origin/dev' into dev

xuzhancheng 4 napja
szülő
commit
bdaf767fa6

+ 15 - 5
tz-module-pressure2/tz-module-pressure2-biz/src/main/java/cn/start/tz/module/pressure2/controller/admin/appointmentconfirmorderrefuseitem/AppointmentConfirmOrderRefuseItemController.java

@@ -1,8 +1,11 @@
 package cn.start.tz.module.pressure2.controller.admin.appointmentconfirmorderrefuseitem;
 
 import cn.hutool.core.util.ObjectUtil;
+import cn.start.tz.module.pressure2.controller.admin.appointmentconfirmrefuseyearitem.vo.AppointmentConfirmRefuseLegalItemExportVO;
 import cn.start.tz.module.pressure2.controller.admin.appointmentconfirmrefuseyearitem.vo.AppointmentConfirmRefuseYearItemExportVO;
 import cn.start.tz.module.pressure2.controller.admin.acceptorder.vo.AcceptOrderCancelVO;
+import cn.start.tz.module.system.api.dict.DictDataApi;
+import cn.start.tz.module.system.api.dict.dto.DictDataRespDTO;
 import org.apache.commons.lang3.StringUtils;
 import org.springframework.web.bind.annotation.*;
 import jakarta.annotation.Resource;
@@ -43,6 +46,9 @@ public class AppointmentConfirmOrderRefuseItemController {
     @Resource
     private AppointmentConfirmOrderRefuseItemService appointmentConfirmOrderRefuseItemService;
 
+    @Resource
+    private DictDataApi dictDataApi;
+
     @PostMapping("/create")
     @Operation(summary = "创建约检确认单拒绝设备项目")
 //    @PreAuthorize("@ss.hasPermission('pressure2:appointment-confirm-order-refuse-item:create')")
@@ -237,8 +243,9 @@ public class AppointmentConfirmOrderRefuseItemController {
         PageResult<AppointmentConfirmOrderRefuseItemRespVO> pageResult = appointmentConfirmOrderRefuseItemService.getAppointmentConfirmOrderRefuseItemPage(pageReqVO);
 
         // 转换为导出VO并处理字段格式
-        List<AppointmentConfirmRefuseYearItemExportVO> exportList = BeanUtils.toBean(pageResult.getList(), AppointmentConfirmRefuseYearItemExportVO.class);
+        List<AppointmentConfirmRefuseLegalItemExportVO> exportList = BeanUtils.toBean(pageResult.getList(), AppointmentConfirmRefuseLegalItemExportVO.class);
 
+        List<DictDataRespDTO> mainTypeList = dictDataApi.getDictDataList("pressure2_equip_main_type").getData();
         // 处理字典值转换
         exportList.forEach(item -> {
             if (ObjectUtil.isNotEmpty(item.getSubmitUser())) {
@@ -248,6 +255,12 @@ public class AppointmentConfirmOrderRefuseItemController {
             // 处理检验性质
             item.setCheckTypeStr(item.getCheckTypeName());
 
+            //设备类型
+            if (item.getEquipMainType() != null) {
+                mainTypeList.stream().filter(mainType -> mainType.getValue().equals(item.getEquipMainType())).findFirst()
+                        .ifPresent(mainTypeDTO -> item.setEquipMainTypeName(mainTypeDTO.getLabel()));
+            }
+
             // 处理拒绝定检状态
             if (item.getRefuseCheckStatus() != null) {
                 switch (item.getRefuseCheckStatus()) {
@@ -277,9 +290,6 @@ public class AppointmentConfirmOrderRefuseItemController {
                 }
             }
 
-            // 处理运行状态
-            item.setEquipStatusStr(item.getEquipStatusName());
-
             // 处理拒检原因
             if (item.getReasonDict() != null) {
                 switch (item.getReasonDict()) {
@@ -316,7 +326,7 @@ public class AppointmentConfirmOrderRefuseItemController {
         });
 
         // 导出Excel
-        ExcelUtils.write(response, "约检确认单拒绝设备项目数据.xls", "数据", AppointmentConfirmRefuseYearItemExportVO.class, exportList);
+        ExcelUtils.write(response, "约检确认单拒绝设备项目数据.xls", "数据", AppointmentConfirmRefuseLegalItemExportVO.class, exportList);
     }
 
 }

+ 1 - 1
tz-module-pressure2/tz-module-pressure2-biz/src/main/java/cn/start/tz/module/pressure2/controller/admin/appointmentconfirmorderrefuseitem/vo/AppointmentConfirmOrderRefuseItemRespVO.java

@@ -221,7 +221,7 @@ public class AppointmentConfirmOrderRefuseItemRespVO {
     @Schema(description = "设备使用注册号")
     private String useRegisterNo;
 
-    private Integer RefuseCheckStatus;
+    private Integer refuseCheckStatus;
 
     private String checkTypeName;
 

+ 12 - 19
tz-module-pressure2/tz-module-pressure2-biz/src/main/java/cn/start/tz/module/pressure2/controller/admin/appointmentconfirmrefuseyearitem/AppointmentConfirmRefuseYearItemController.java

@@ -4,6 +4,8 @@ import cn.hutool.core.util.ObjectUtil;
 import cn.start.tz.module.pressure2.controller.admin.appointmentconfirmrefuseyearitem.vo.AppointmentConfirmRefuseYearItemExportVO;
 import cn.start.tz.module.pressure2.controller.admin.appointmentconfirmorderrefuseitem.vo.AppointmentConfirmOrderRefuseItemPageReqVO;
 import cn.start.tz.module.pressure2.controller.admin.appointmentconfirmorderrefuseitem.vo.AppointmentConfirmOrderRefuseItemRespVO;
+import cn.start.tz.module.system.api.dict.DictDataApi;
+import cn.start.tz.module.system.api.dict.dto.DictDataRespDTO;
 import org.springframework.web.bind.annotation.*;
 import jakarta.annotation.Resource;
 import org.springframework.validation.annotation.Validated;
@@ -42,6 +44,9 @@ public class AppointmentConfirmRefuseYearItemController {
     @Resource
     private AppointmentConfirmRefuseYearItemService appointmentConfirmRefuseYearItemService;
 
+    @Resource
+    private DictDataApi dictDataApi;
+
     @PostMapping("/create")
     @Operation(summary = "提交拒绝年检设备")
     public CommonResult<String> createAppointmentConfirmRefuseYearItem(@Valid @RequestBody YearCheckIdVO yearCheckIdVO) {
@@ -147,6 +152,7 @@ public class AppointmentConfirmRefuseYearItemController {
         // 转换为导出VO并处理字段格式
         List<AppointmentConfirmRefuseYearItemExportVO> exportList = BeanUtils.toBean(list, AppointmentConfirmRefuseYearItemExportVO.class);
 
+        List<DictDataRespDTO> mainTypeList = dictDataApi.getDictDataList("pressure2_equip_main_type").getData();
         // 处理字典值转换
         exportList.forEach(item -> {
             if (ObjectUtil.isNotEmpty(item.getSubmitUser())) {
@@ -154,14 +160,12 @@ public class AppointmentConfirmRefuseYearItemController {
             }
 
             // 处理检验性质
-            if (item.getCheckType() != null) {
-                if (item.getCheckType() == 100) {
-                    item.setCheckTypeStr("定期检验");
-                } else if (item.getCheckType() == 200) {
-                    item.setCheckTypeStr("年度检查");
-                } else if (item.getCheckType() == 300) {
-                    item.setCheckTypeStr("超年限检验");
-                }
+            item.setCheckTypeStr(item.getCheckTypeName());
+
+            //设备类型
+            if (item.getEquipMainType() != null) {
+                mainTypeList.stream().filter(mainType -> mainType.getValue().equals(item.getEquipMainType())).findFirst()
+                        .ifPresent(mainTypeDTO -> item.setEquipMainTypeName(mainTypeDTO.getLabel()));
             }
 
             // 处理状态
@@ -175,17 +179,6 @@ public class AppointmentConfirmRefuseYearItemController {
                 item.setProcessStatusStr("拒绝年检");
             }
 
-            // 处理运行状态
-            if (item.getEquipStatus() != null) {
-                if (item.getEquipStatus() == 100) {
-                    item.setEquipStatusStr("在用");
-                } else if (item.getEquipStatus() == 200) {
-                    item.setEquipStatusStr("停运");
-                } else if (item.getEquipStatus() == 300) {
-                    item.setEquipStatusStr("注销");
-                }
-            }
-
             // 处理拒检原因
             if (item.getReasonDict() != null) {
                 switch (item.getReasonDict()) {

+ 120 - 0
tz-module-pressure2/tz-module-pressure2-biz/src/main/java/cn/start/tz/module/pressure2/controller/admin/appointmentconfirmrefuseyearitem/vo/AppointmentConfirmRefuseLegalItemExportVO.java

@@ -0,0 +1,120 @@
+package cn.start.tz.module.pressure2.controller.admin.appointmentconfirmrefuseyearitem.vo;
+
+import cn.start.tz.module.system.api.user.dto.AdminUserRespDTO;
+import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
+import com.alibaba.excel.annotation.ExcelProperty;
+import com.alibaba.excel.annotation.write.style.ColumnWidth;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+
+@Schema(description = "管理后台 - 拒绝年检设备导出 Excel VO")
+@Data
+@ExcelIgnoreUnannotated
+public class AppointmentConfirmRefuseLegalItemExportVO {
+
+
+    @Schema(description = "使用单位")
+    @ExcelProperty(value = "单位名称")
+    @ColumnWidth(30)
+    private String unitName;
+
+    @Schema(description = "设备注册代码")
+    @ExcelProperty(value = "设备注册代码")
+    @ColumnWidth(20)
+    private String equipCode;
+
+    @Schema(description = "设备类型")
+    private String equipMainType;
+
+    @Schema(description = "设备类型")
+    @ExcelProperty(value = "设备类型")
+    @ColumnWidth(20)
+    private String equipMainTypeName;
+
+    @Schema(description = "检验性质(100=定期检验,200=年度检查,300=超年限检验)")
+    @ExcelProperty(value = "检验性质")
+    @ColumnWidth(15)
+    private String checkTypeStr;
+
+    @Schema(description = "区域")
+    @ExcelProperty(value = "区域")
+    @ColumnWidth(15)
+    private String equipDistrictName;
+
+    @Schema(description = "使用证编号")
+    @ExcelProperty(value = "使用证编号")
+    @ColumnWidth(20)
+    private String useRegisterNo;
+
+    @Schema(description = "拒绝定检状态(1=待处理,2=无需处理,3=审核中,4=审核已拒绝,5=已作废,6=待上报,7=已上报)")
+    @ExcelProperty(value = "拒绝定检状态")
+    @ColumnWidth(15)
+    private String refuseCheckStatusStr;
+
+    @Schema(description = "拒绝来源(1=窗口拒检,2=检验员拒检,3=客户拒检)")
+    @ExcelProperty(value = "拒绝来源")
+    @ColumnWidth(15)
+    private String rejectionSourceStr;
+
+    @Schema(description = "下次检验日期")
+    @ExcelProperty(value = "下次检验日期")
+    @ColumnWidth(15)
+    private LocalDate nextCheckDate;
+
+    @Schema(description = "拒检原因")
+    @ExcelProperty(value = "拒检原因")
+    @ColumnWidth(15)
+    private String reasonDictStr;
+
+    @Schema(description = "拒检说明")
+    @ExcelProperty(value = "说明")
+    @ColumnWidth(30)
+    private String reason;
+
+    @Schema(description = "提交人")
+    @ExcelProperty(value = "提交人")
+    @ColumnWidth(15)
+    private String submitUserName;
+
+    @Schema(description = "提交时间")
+    @ExcelProperty(value = "提交时间")
+    @ColumnWidth(20)
+    private LocalDateTime submitTime;
+
+    @Schema(description = "检验类型")
+    private Integer checkType;
+
+    @Schema(description = "处理状态(0=待处理,1=无需处理)")
+    private Integer processStatus;
+
+    @Schema(description = "运行状态(100=在用,200=停运,300=注销)")
+    private Integer equipStatus;
+
+    @Schema(description = "拒绝原因字典")
+    private String reasonDict;
+
+    @Schema(description = "拒绝定检状态(1=待处理,2=无需处理,3=审核中,4=审核已拒绝,5=已作废,6=待上报,7=已上报)")
+    private Integer refuseCheckStatus;
+
+    @Schema(description = "场景值(0=拒绝年检设备,1=拒绝约检)")
+    private Integer scene;
+
+    @Schema(description = "提交人")
+    private AdminUserRespDTO submitUser;
+
+    @Schema(description = "拒绝来源(1=窗口拒检,2=检验员拒检,3=客户拒检)")
+    private Integer rejectionSource;
+
+    @Schema(description = "约检联系人")
+    private String contact;
+
+    @Schema(description = "约检联系人电话")
+    private String contactPhone;
+
+    private String checkTypeName;
+
+    private String equipStatusName;
+}

+ 23 - 29
tz-module-pressure2/tz-module-pressure2-biz/src/main/java/cn/start/tz/module/pressure2/controller/admin/appointmentconfirmrefuseyearitem/vo/AppointmentConfirmRefuseYearItemExportVO.java

@@ -15,54 +15,48 @@ import java.time.LocalDateTime;
 @ExcelIgnoreUnannotated
 public class AppointmentConfirmRefuseYearItemExportVO {
 
-    @Schema(description = "设备注册代码")
-    @ExcelProperty(value = "设备注册代码")
-    @ColumnWidth(20)
-    private String equipCode;
-
-    @Schema(description = "产品编号")
-    @ExcelProperty(value = "产品编号")
-    @ColumnWidth(20)
-    private String productNo;
 
     @Schema(description = "使用单位")
-    @ExcelProperty(value = "使用单位")
+    @ExcelProperty(value = "单位名称")
     @ColumnWidth(30)
     private String unitName;
 
-    @Schema(description = "使用证编号")
-    @ExcelProperty(value = "使用证编号")
+    @Schema(description = "设备注册代码")
+    @ExcelProperty(value = "设备注册代码")
     @ColumnWidth(20)
-    private String useRegisterNo;
+    private String equipCode;
 
-    @Schema(description = "区域")
-    @ExcelProperty(value = "区域")
-    @ColumnWidth(15)
-    private String equipDistrictName;
+    @Schema(description = "设备类型")
+    private String equipMainType;
+
+    @Schema(description = "设备类型")
+    @ExcelProperty(value = "设备类型")
+    @ColumnWidth(20)
+    private String equipMainTypeName;
 
     @Schema(description = "检验性质(100=定期检验,200=年度检查,300=超年限检验)")
     @ExcelProperty(value = "检验性质")
     @ColumnWidth(15)
     private String checkTypeStr;
 
-    @Schema(description = "拒绝来源(1=窗口拒检,2=检验员拒检,3=客户拒检)")
-    @ExcelProperty(value = "拒绝来源")
+    @Schema(description = "区域")
+    @ExcelProperty(value = "区域")
     @ColumnWidth(15)
-    private String rejectionSourceStr;
+    private String equipDistrictName;
+
+    @Schema(description = "使用证编号")
+    @ExcelProperty(value = "使用证编号")
+    @ColumnWidth(20)
+    private String useRegisterNo;
 
     @ExcelProperty(value = "拒绝年检状态")
-    @ColumnWidth(12)
+    @ColumnWidth(15)
     private String processStatusStr;
 
-    @Schema(description = "拒绝定检状态(1=待处理,2=无需处理,3=审核中,4=审核已拒绝,5=已作废,6=待上报,7=已上报)")
-    @ExcelProperty(value = "拒绝定检状态")
+    @Schema(description = "拒绝来源(1=窗口拒检,2=检验员拒检,3=客户拒检)")
+    @ExcelProperty(value = "拒绝来源")
     @ColumnWidth(15)
-    private String refuseCheckStatusStr;
-
-    @Schema(description = "运行状态(100=在用,200=停运,300=注销)")
-    @ExcelProperty(value = "运行状态")
-    @ColumnWidth(12)
-    private String equipStatusStr;
+    private String rejectionSourceStr;
 
     @Schema(description = "下次检验日期")
     @ExcelProperty(value = "下次检验日期")

+ 341 - 0
tz-module-pressure2/tz-module-pressure2-biz/src/main/java/cn/start/tz/module/pressure2/controller/appapi/excel/AppapiExcelController.java

@@ -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();
+    }
+}